diff --git a/.github/workflows/amd64_windows_cmake_java.yml b/.github/workflows/amd64_windows_cmake_java.yml index 3c15cd4a31c..842ef016328 100644 --- a/.github/workflows/amd64_windows_cmake_java.yml +++ b/.github/workflows/amd64_windows_cmake_java.yml @@ -42,10 +42,6 @@ jobs: with: distribution: ${{matrix.java.distrib}} java-version: ${{matrix.java.version}} - - name: Update maven - run: | - choco upgrade maven - echo "C:\ProgramData\chocolatey\lib\maven\apache-maven-3.9.9\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Check java run: | java -version diff --git a/CMakeLists.txt b/CMakeLists.txt index b06184dc019..12f42a9183a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ # limitations under the License. # This file is just an orchestration -cmake_minimum_required(VERSION 3.20) +cmake_minimum_required(VERSION 3.24) list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") # Enable output of compile commands during generation. @@ -467,6 +467,7 @@ foreach(SAMPLES IN ITEMS graph glop constraint_solver + routing linear_solver ${MATH_OPT_DIR} ${PDLP_DIR} diff --git a/Dependencies.txt b/Dependencies.txt index b1dc1ed9b7f..5d072a8acb3 100644 --- a/Dependencies.txt +++ b/Dependencies.txt @@ -1,6 +1,6 @@ ZLIB=1.3.1 abseil-cpp=20250512.0 -Protobuf=v31.0 +Protobuf=v31.1 Eigen=3.4.0 Re2=2024-07-02 CoinUtils=2.11.12 @@ -9,7 +9,7 @@ Clp=1.17.10 Cgl=0.60.9 Cbc=2.10.12 GLPK=5.0 -HiGHS=v1.10.0 +HiGHS=v1.11.0 Scip=v922 # Python pybind11=v2.13.6 diff --git a/MODULE.bazel b/MODULE.bazel index a6cafbe1e8e..08fe2aeacf2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -5,7 +5,7 @@ # For more details, please check https://github.com/bazelbuild/bazel/issues/18958 ############################################################################### -OR_TOOLS_VERSION = "9.13" +OR_TOOLS_VERSION = "10.0" module( name = "or-tools", @@ -24,9 +24,9 @@ bazel_dep(name = "gazelle", version = "0.43.0") bazel_dep(name = "glpk", version = "5.0.bcr.4") bazel_dep(name = "google_benchmark", version = "1.9.2") bazel_dep(name = "googletest", version = "1.17.0") -bazel_dep(name = "highs", version = "1.10.0") +bazel_dep(name = "highs", version = "1.11.0") bazel_dep(name = "platforms", version = "0.0.11") -bazel_dep(name = "protobuf", version = "31.0") +bazel_dep(name = "protobuf", version = "31.1") bazel_dep(name = "pybind11_abseil", version = "202402.0") bazel_dep(name = "pybind11_bazel", version = "2.13.6") bazel_dep(name = "pybind11_protobuf", version = "0.0.0-20240524-1d7a729") diff --git a/Version.txt b/Version.txt index 78863f2d95c..3dc39420642 100644 --- a/Version.txt +++ b/Version.txt @@ -1,3 +1,3 @@ -OR_TOOLS_MAJOR=9 -OR_TOOLS_MINOR=13 +OR_TOOLS_MAJOR=10 +OR_TOOLS_MINOR=0 #PRE_RELEASE=YES diff --git a/bazel/notebook_requirements.in b/bazel/notebook_requirements.in index d7c30c0201e..c2d02e6fcbb 100644 --- a/bazel/notebook_requirements.in +++ b/bazel/notebook_requirements.in @@ -2,8 +2,8 @@ absl-py==2.2.2 immutabledict==4.2.1 numpy==2.2.0 -protobuf==6.31.0 -requests==2.32.3 +protobuf==6.31.1 +requests==2.32.4 scipy==1.14.1 typing-extensions==4.13.1 diff --git a/bazel/notebook_requirements.txt b/bazel/notebook_requirements.txt index b8a7a5aa7a5..b656e14e685 100644 --- a/bazel/notebook_requirements.txt +++ b/bazel/notebook_requirements.txt @@ -215,7 +215,7 @@ prometheus-client==0.22.1 # via jupyter-server prompt-toolkit==3.0.51 # via ipython -protobuf==6.31.0 +protobuf==6.31.1 # via # -r bazel/notebook_requirements.in # mypy-protobuf diff --git a/bazel/ortools_requirements.in b/bazel/ortools_requirements.in index 0b4c89ab401..e893a8b6290 100644 --- a/bazel/ortools_requirements.in +++ b/bazel/ortools_requirements.in @@ -2,8 +2,8 @@ absl-py==2.2.2 immutabledict==4.2.1 numpy==2.2.0 -protobuf==6.31.0 -requests==2.32.3 +protobuf==6.31.1 +requests==2.32.4 scipy==1.14.1 typing-extensions==4.13.1 diff --git a/bazel/ortools_requirements.txt b/bazel/ortools_requirements.txt index 820668e0367..10c1d8b277e 100644 --- a/bazel/ortools_requirements.txt +++ b/bazel/ortools_requirements.txt @@ -45,7 +45,7 @@ platformdirs==3.10.0 # via # black # virtualenv -protobuf==6.31.0 +protobuf==6.31.1 # via # -r bazel/ortools_requirements.in # mypy-protobuf diff --git a/cmake/README.md b/cmake/README.md index a233fb81424..ff84170a29b 100644 --- a/cmake/README.md +++ b/cmake/README.md @@ -88,7 +88,7 @@ CMake as a standalone project or incorporate it into an existing CMake project. ## Requirement You'll need: -* `CMake >= 3.18`. +* `CMake >= 3.24`. * A C++20 compiler (GCC 10 or above) ## Solvers supported @@ -193,10 +193,10 @@ CMake Option | Default Value | Note `BUILD_DOTNET` | OFF | Build .Net wrapper and packages `BUILD_JAVA` | OFF | Build Java wrapper and packages `BUILD_PYTHON` | OFF | Build Python wrapper and package - | | +| | `BUILD_FLATZINC` | ON\* | Build the flatzinc library
**Forced** to OFF if `BUILD_CXX=OFF` `BUILD_GLOP` | OFF\* | Build the standalone Glop library
**Forced** to OFF if `BUILD_CXX=ON`, otherwise default to ON - | | +| **Dependencies** | `BUILD_DEPS` | OFF* | Default to ON if `BUILD_JAVA=ON` or `BUILD_PYTHON=ON` or `BUILD_DOTNET=ON` `BUILD_ZLIB` | OFF* | Build the zlib dynamic library
**Forced** to ON if `BUILD_DEPS=ON` `BUILD_BZip2` | OFF* | Build the bzip2 dynamic library
**Forced** to ON if `BUILD_DEPS=ON` @@ -204,44 +204,44 @@ CMake Option | Default Value | Note `BUILD_Protobuf` | OFF* | Build the protobuf dynamic libraries
**Forced** to ON if `BUILD_DEPS=ON` `BUILD_re2` | OFF* | Build the re2 dynamic libraries
**Forced** to ON if `BUILD_DEPS=ON` `BUILD_Eigen3` | OFF* | Build the Eigen3 libraries
**Forced** to ON if `BUILD_DEPS=ON` - | | +| Coin-OR | `USE_COINOR` | ON\* | Enable Coin-OR support
**Forced** to OFF if `BUILD_CXX=OFF` `BUILD_CoinUtils` | OFF\* | Build the CoinUtils dynamic library
**Forced** to ON if `USE_COINOR=ON` **and** `BUILD_DEPS=ON` `BUILD_Osi` | OFF\* | Build the Osi dynamic library
**Forced** to ON if `USE_COINOR=ON` **and** `BUILD_DEPS=ON` `BUILD_Clp` | OFF\* | Build the Clp dynamic library
**Forced** to ON if `USE_COINOR=ON` **and** `BUILD_DEPS=ON` `BUILD_Cgl` | OFF\* | Build the Cgl dynamic library
**Forced** to ON if `USE_COINOR=ON` **and** `BUILD_DEPS=ON` `BUILD_Cbc` | OFF\* | Build the Cbc dynamic library
**Forced** to ON if `USE_COINOR=ON` **and** `BUILD_DEPS=ON` - | | +| GLPK | `USE_GLPK` | OFF\* | Enable GLPK support
**Forced** to OFF if `BUILD_CXX=OFF` `BUILD_GLPK` | OFF\* | Build the GLPK dynamic libraries
**Forced** to ON if `USE_GLPK=ON` **and** `BUILD_DEPS=ON` - | | +| HiGHS | `USE_HIGHS` | ON\* | Enable HIGHS support
**Forced** to OFF if `BUILD_CXX=OFF` `BUILD_HIGHS` | OFF\* | Build the HiGHS dynamic libraries
**Forced** to ON if `USE_HIGHS=ON` **and** `BUILD_DEPS=ON` - | | +| SCIP | `USE_SCIP` | ON\* | Enable SCIP support
**Forced** to OFF if `BUILD_CXX=OFF` `BUILD_SCIP` | OFF\* | Build the SCIP dynamic libraries
**Forced** to ON if `USE_SCIP=ON` **and** `BUILD_DEPS=ON` - | | +| CPLEX `USE_CPLEX` | OFF | Enable CPLEX support - | | +| **Documentation** | `BUILD_DOC` | OFF\* | Build all documentations `BUILD_CXX_DOC` | OFF\* | Build C++ documentation
**Forced** to ON if `BUILD_DOC=ON` `BUILD_DOTNET_DOC` | OFF\* | Build .Net documentation
**Forced** to ON if `BUILD_DOC=ON` `BUILD_JAVA_DOC` | OFF\* | Build Java documentation
**Forced** to ON if `BUILD_DOC=ON` `BUILD_PYTHON_DOC` | OFF\* | Build Python documentation
**Forced** to ON if `BUILD_DOC=ON` `INSTALL_DOC` | OFF\* | Install all documentations
**Forced** to OFF if `BUILD_CXX=OFF` or `BUILD_DOC=OFF` - | | +| **Samples** | `BUILD_SAMPLES` | ON\* | Build all samples
Default to ON if `BUILD_DEPS=ON` `BUILD_CXX_SAMPLES` | ON\* | Build all C++ samples
**Forced** to OFF if `BUILD_CXX=OFF` or `BUILD_SAMPLE=OFF` `BUILD_DOTNET_SAMPLES` | ON\* | Build all .Net samples
**Forced** to OFF if `BUILD_DOTNET=OFF` or `BUILD_SAMPLE=OFF` `BUILD_JAVA_SAMPLES` | ON\* | Build all Java samples
**Forced** to OFF if `BUILD_JAVA=OFF` or `BUILD_SAMPLE=OFF` `BUILD_PYTHON_SAMPLES` | ON\* | Build all Python samples
**Forced** to OFF if `BUILD_PYTHON=OFF` or `BUILD_SAMPLE=OFF` - | | +| **Examples** | `BUILD_EXAMPLES` | ON\* | Build all examples
Default to ON if `BUILD_DEPS=ON` `BUILD_CXX_EXAMPLES` | ON\* | Build all C++ examples
**Forced** to OFF if `BUILD_CXX=OFF` or `BUILD_SAMPLE=OFF` `BUILD_DOTNET_EXAMPLES` | ON\* | Build all .Net examples
**Forced** to OFF if `BUILD_DOTNET=OFF` or `BUILD_SAMPLE=OFF` `BUILD_JAVA_EXAMPLES` | ON\* | Build all Java examples
**Forced** to OFF if `BUILD_JAVA=OFF` or `BUILD_SAMPLE=OFF` `BUILD_PYTHON_EXAMPLES` | ON\* | Build all Python examples
**Forced** to OFF if `BUILD_PYTHON=OFF` or `BUILD_SAMPLE=OFF` - | | +| **.Net** | `USE_DOTNET_46` | OFF | Enable .Net Framework 4.6 support
Only available if `BUILD_DOTNET=ON` `USE_DOTNET_461` | OFF | Enable .Net Framework 4.6.1 support
Only available if `BUILD_DOTNET=ON` `USE_DOTNET_462` | OFF | Enable .Net Framework 4.6.2 support
Only available if `BUILD_DOTNET=ON` @@ -253,11 +253,11 @@ CMake Option | Default Value | Note `USE_DOTNET_8` | ON | Enable .Net 8 LTS support
Only available if `BUILD_DOTNET=ON` `USE_DOTNET_9` | OFF | Enable .Net 9 support
Only available if `BUILD_DOTNET=ON` `UNIVERSAL_DOTNET_PACKAGE` | OFF | Build a multi platform package (i.e. `Google.OrTools` will depends on all runtime packages)
Only available if `BUILD_DOTNET=ON` - | | +| **Java** | `SKIP_GPG` | ON | Disable GPG sign
Only available if `BUILD_JAVA=ON` `UNIVERSAL_JAVA_PACKAGE` | OFF | Build a multi platform package (i.e. `ortools-java` will depends on all native packages)
Only available if `BUILD_JAVA=ON` `BUILD_FAT_JAR` | OFF | Build a `ortools-java` .jar that includes all of its own Maven dependencies, including the native package
Only available if `BUILD_JAVA=ON` - | | +| **Python** | `BUILD_pybind11` | `BUILD_DEPS` | Static build the pybind11 libraries
**Forced** to ON if `BUILD_DEPS=ON`
Only available if `BUILD_PYTHON=ON` `BUILD_pybind11_abseil` | `BUILD_DEPS` | Static build the pybind11_abseil libraries
**Forced** to ON if `BUILD_DEPS=ON`
Only available if `BUILD_PYTHON=ON` `BUILD_pybind11_protobuf` | `BUILD_DEPS` | Static build the pybind11_protobuf libraries
**Forced** to ON if `BUILD_DEPS=ON`
Only available if `BUILD_PYTHON=ON` @@ -265,7 +265,7 @@ CMake Option | Default Value | Note `BUILD_VENV` | `BUILD_TESTING` | Create python venv in `BINARY_DIR/python/venv`
**Forced** to ON if `BUILD_TESTING=ON`
Only available if `BUILD_PYTHON=ON` `VENV_USE_SYSTEM_SITE_PACKAGES` | OFF | Python venv can use system site package (e.g. `py3-numpy` on Alpine)
Only available if `BUILD_PYTHON=ON` and `BUILD_VENV=ON` `FETCH_PYTHON_DEPS` | `BUILD_DEPS` | Fetch python modules needed to build ortools package
Only available if `BUILD_PYTHON=ON` - | | +| | ## Integrating OR-Tools in your CMake Project diff --git a/cmake/cpp.cmake b/cmake/cpp.cmake index 9cdc27ec1e1..269275b0aff 100644 --- a/cmake/cpp.cmake +++ b/cmake/cpp.cmake @@ -426,6 +426,16 @@ generate_proto_library( NAME ortools FILES ${OR_TOOLS_PROTO_FILES}) +# Routing proto +file(GLOB_RECURSE ROUTING_PROTO_FILES RELATIVE ${PROJECT_SOURCE_DIR} + "ortools/routing/*.proto" + "ortools/routing/parsers/*.proto" +) +generate_proto_library( + NAME routing + FILES ${ROUTING_PROTO_FILES} + LINK_LIBRARIES ${PROJECT_NAMESPACE}::ortools_proto) + # MathOpt proto if(BUILD_MATH_OPT) file(GLOB_RECURSE MATH_OPT_PROTO_FILES RELATIVE ${PROJECT_SOURCE_DIR} @@ -497,6 +507,11 @@ target_sources(${PROJECT_NAME} PRIVATE $) add_dependencies(${PROJECT_NAME} ${PROJECT_NAMESPACE}::ortools_proto) +# Add ${PROJECT_NAMESPACE}::routing_proto to libortools +target_sources(${PROJECT_NAME} PRIVATE + $) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAMESPACE}::routing_proto) + if(BUILD_MATH_OPT) # Add ${PROJECT_NAMESPACE}::math_opt_proto to libortools target_sources(${PROJECT_NAME} PRIVATE @@ -518,12 +533,13 @@ foreach(SUBPROJECT IN ITEMS ${GUROBI_DIR} ${PDLP_DIR} sat - xpress lp_data packing + routing scheduling set_cover port + third_party_solvers util) add_subdirectory(ortools/${SUBPROJECT}) #target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_NAME}_${SUBPROJECT}) @@ -544,6 +560,10 @@ add_subdirectory(ortools/linear_solver/proto_solver) target_sources(${PROJECT_NAME} PRIVATE $) add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}_linear_solver_proto_solver) +add_subdirectory(ortools/routing/parsers) +target_sources(${PROJECT_NAME} PRIVATE $) +add_dependencies(${PROJECT_NAME} ${PROJECT_NAME}_routing_parsers) + # Dependencies if(APPLE) set_target_properties(${PROJECT_NAME} PROPERTIES @@ -695,6 +715,10 @@ install(DIRECTORY ortools/constraint_solver/docs/ DESTINATION "${CMAKE_INSTALL_DOCDIR}/constraint_solver" FILES_MATCHING PATTERN "*.md") +install(DIRECTORY ortools/routing/docs/ + DESTINATION "${CMAKE_INSTALL_DOCDIR}/routing" + FILES_MATCHING + PATTERN "*.md") endif() ################## diff --git a/cmake/dependencies/CMakeLists.txt b/cmake/dependencies/CMakeLists.txt index bdd1d4362cf..184fed78eb0 100644 --- a/cmake/dependencies/CMakeLists.txt +++ b/cmake/dependencies/CMakeLists.txt @@ -11,6 +11,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +# We are using FetchContent OVERRIDE_FIND_PACKAGE introduced in 3.24 +cmake_minimum_required(VERSION 3.24) + # ############################################################################## # SWIG (WIN32) # ############################################################################## @@ -136,11 +139,11 @@ if(BUILD_Protobuf) FetchContent_Declare( Protobuf GIT_REPOSITORY "https://github.com/protocolbuffers/protobuf.git" - GIT_TAG "v31.0" + GIT_TAG "v31.1" GIT_SHALLOW TRUE GIT_SUBMODULES "" PATCH_COMMAND git apply --ignore-whitespace - "${CMAKE_CURRENT_LIST_DIR}/../../patches/protobuf-v31.0.patch" + "${CMAKE_CURRENT_LIST_DIR}/../../patches/protobuf-v31.1.patch" ) FetchContent_MakeAvailable(Protobuf) list(POP_BACK CMAKE_MESSAGE_INDENT) @@ -285,10 +288,10 @@ if(BUILD_HIGHS) FetchContent_Declare( highs GIT_REPOSITORY "https://github.com/ERGO-Code/HiGHS.git" - GIT_TAG "v1.10.0" + GIT_TAG "v1.11.0" GIT_SHALLOW TRUE PATCH_COMMAND git apply --ignore-whitespace - "${CMAKE_CURRENT_LIST_DIR}/../../patches/highs-v1.10.0.patch" + "${CMAKE_CURRENT_LIST_DIR}/../../patches/highs-v1.11.0.patch" ) FetchContent_MakeAvailable(highs) list(POP_BACK CMAKE_MESSAGE_INDENT) diff --git a/cmake/docs/cmake.svg b/cmake/docs/cmake.svg index b837c5b6b64..9d3cbb5d261 100644 --- a/cmake/docs/cmake.svg +++ b/cmake/docs/cmake.svg @@ -1,190 +1,190 @@ - - - + + CMake - + clusterPrerequisite - -Prerequisite + +Prerequisite clusterOR - -OR-Tools (CMake) + +OR-Tools (CMake) clusterDeps - -Dependencies -(-DBUILD_DEPS=ON) + +Dependencies +(-DBUILD_DEPS=ON) clusterZLIB - -madler/zlib.git + cmake patch + +madler/zlib.git + cmake patch clusterBZIP2 - -bzip2/bzip2.git + cmake patch + +bzip2/bzip2.git + cmake patch clusterAbsl - -abseil/abseil-cpp.git + +abseil/abseil-cpp.git clusterRe2 - -google/re2.git + +google/re2.git clusterProtobuf - -protocolbuffers/protobuf.git + +protocolbuffers/protobuf.git clusterCoinOR - -Coin-OR Solver -(-DUSE_COINOR=ON) + +Coin-OR Solver +(-DUSE_COINOR=ON) clusterCoinUtils - -Mizux/CoinUtils.git + +Mizux/CoinUtils.git clusterOsi - -Mizux/Osi.git + +Mizux/Osi.git clusterClp - -Mizux/Clp.git + +Mizux/Clp.git clusterCgl - -Mizux/Cgl.git + +Mizux/Cgl.git clusterCbc - -Mizux/Cbc.git + +Mizux/Cbc.git clusterGLPKSolver - -GLPK Solver -(-DUSE_GLPK=ON) + +GLPK Solver +(-DUSE_GLPK=ON) clusterGLPK - -Mizux/GLPK.git + +Mizux/GLPK.git clusterHIGHSSolver - -HIGHS Solver -(-DUSE_HIGHS=ON) + +HIGHS Solver +(-DUSE_HIGHS=ON) clusterHIGHS - -ERGO-Code/HIGHS.git + +ERGO-Code/HIGHS.git clusterSCIPSolver - -SCIP Solver -(-DUSE_SCIP=ON) + +SCIP Solver +(-DUSE_SCIP=ON) clusterSoplex - -scipopt/soplex.git + +scipopt/soplex.git clusterSCIP - -scipopt/scip.git + +scipopt/scip.git clusterCXX - -C++ -(-DBUILD_CXX=ON) + +C++ +(-DBUILD_CXX=ON) clusterCXXTest - -Examples -(-DBUILD_TESTING=ON) + +Examples +(-DBUILD_TESTING=ON) clusterPython - -Python -(-DBUILD_PYTHON=ON) + +Python +(-DBUILD_PYTHON=ON) clusterPythonTest - -Examples -(-DBUILD_TESTING=ON) + +Examples +(-DBUILD_TESTING=ON) clusterJava - -Java -(-DBUILD_JAVA=ON) + +Java +(-DBUILD_JAVA=ON) clusterJavaTest - -Examples -(-DBUILD_TESTING=ON) + +Examples +(-DBUILD_TESTING=ON) clusterNet - -.Net -(-DBUILD_DOTNET=ON) + +.Net +(-DBUILD_DOTNET=ON) clusterNetTest - -Examples -(-DBUILD_TESTING=ON) + +Examples +(-DBUILD_TESTING=ON) CM - - - - -CMake + + + + +CMake SWIG - - - - -Swig -(Unix) + + + + +Swig +(Unix) @@ -193,12 +193,12 @@ PY - - - - -Python -(3.6+) + + + + +Python +(3.6+) @@ -207,12 +207,12 @@ JV - - - - -Java -(openJDK 8+) + + + + +Java +(openJDK 8+) @@ -221,12 +221,12 @@ DN - - - - -.Net Core SDK -(3.1) + + + + +.Net Core SDK +(3.1) @@ -235,11 +235,11 @@ FS - - - - -.Net F# + + + + +.Net F# @@ -248,584 +248,584 @@ ZLIB - -ZLIB::ZLIB + +ZLIB::ZLIB Protobuf - -protobuf::libprotobuf + +protobuf::libprotobuf ZLIB->Protobuf - - + + SPX - -libsoplex-pic + +libsoplex-pic ZLIB->SPX - - + + SCIP - -SCIP::libscip + +SCIP::libscip ZLIB->SCIP - - + + OR_SRC - -OR-Tools src -ortools/... + +OR-Tools src +ortools/... ZLIB->OR_SRC - - + + BZip2 - -BZip2::BZip2 + +BZip2::BZip2 BZip2->OR_SRC - - + + Absl - -absl::absl_* + +absl::absl_* Re2 - -re2::re2 + +re2::re2 Absl->Re2 - - + + Absl->Protobuf - - + + Absl->OR_SRC - - + + Re2->Protobuf - - + + Re2->OR_SRC - - + + Protobuf->OR_SRC - - + + CoinUtils - -Coin::CoinUtils + +Coin::CoinUtils Osi - -Coin::Osi + +Coin::Osi CoinUtils->Osi - - + + Clp - -Coin::Clp + +Coin::Clp CoinUtils->Clp - - + + OsiClp - -Coin::OsiClp + +Coin::OsiClp CoinUtils->OsiClp - - + + Cgl - -Coin::Cgl + +Coin::Cgl CoinUtils->Cgl - - + + Cbc - -Coin::Cbc + +Coin::Cbc CoinUtils->Cbc - - + + OsiCbc - -Coin::OsiCbc + +Coin::OsiCbc CoinUtils->OsiCbc - - + + Osi->Clp - - + + Osi->OsiClp - - + + Osi->Cgl - - + + Osi->Cbc - - + + Osi->OsiCbc - - + + Clp->OsiClp - - + + ClpSolver - -Coin::ClpSolver + +Coin::ClpSolver Clp->ClpSolver - - + + Clp->Cbc - - + + OsiClp->Cgl - - + + ClpSolver->OR_SRC - - + + Cgl->Cbc - - + + Cbc->OsiCbc - - + + CbcSolver - -Coin::CbcSolver + +Coin::CbcSolver Cbc->CbcSolver - - + + CbcSolver->OR_SRC - - + + GLPK - -glpk::glpk + +glpk::glpk GLPK->OR_SRC - - + + HIGHS - -highs::highs + +highs::highs SPX->SCIP - - + + SCIP->OR_SRC - - + + SWIG_WIN - -swigwin -(Windows) + +swigwin +(Windows) OR_CPP - -ortools::ortools + +ortools::ortools OR_SRC->OR_CPP - - + + OR_WPY - - - -C++ Python wrappers + + + +C++ Python wrappers OR_SRC->OR_WPY - - -swig + + +swig OR_PY - - - -Python files + + + +Python files OR_SRC->OR_PY - - -swig + + +swig OR_WJAVA - - - -C++ Java wrappers + + + +C++ Java wrappers OR_SRC->OR_WJAVA - - -swig + + +swig OR_JAVA - - - -Java files + + + +Java files OR_SRC->OR_JAVA - - -swig + + +swig OR_WNET - - - -C++ .Net wrappers + + + +C++ .Net wrappers OR_SRC->OR_WNET - - -swig + + +swig OR_NET - - - -.Net files + + + +.Net files OR_SRC->OR_NET - - -swig + + +swig PKG_CPP - - - - -CMake Package + + + + +CMake Package OR_CPP->PKG_CPP - - -install + + +install EX_CPP - -C++ samples + +C++ samples PKG_CPP->EX_CPP - - + + OR_WPY->OR_PY - - + + PKG_PY - - - - -Wheel package + + + + +Wheel package OR_PY->PKG_PY - - -python setup.py + + +python setup.py EX_PY - -Python samples + +Python samples PKG_PY->EX_PY - - + + OR_WJAVA->OR_JAVA - - + + PKG_JAVA - - - - -Maven package + + + + +Maven package OR_JAVA->PKG_JAVA - - -maven + + +maven EX_JAVA - -Java samples + +Java samples PKG_JAVA->EX_JAVA - - + + OR_WNET->OR_NET - - + + PKG_NET_RT - - - - -Nuget runtime package -Google.OrTools.runtime.rid.nupkg + + + + +Nuget runtime package +Google.OrTools.runtime.rid.nupkg OR_WNET->PKG_NET_RT - - -dotnet package + + +dotnet package PKG_NET - - - - -Nuget package -Google.OrTools.nupkg + + + + +Nuget package +Google.OrTools.nupkg OR_NET->PKG_NET - - -dotnet package + + +dotnet package PKG_NET_RT->PKG_NET - - + + EX_NET - -.Net samples + +.Net samples PKG_NET->EX_NET - - + + diff --git a/cmake/docs/deps.svg b/cmake/docs/deps.svg index 812629c5dc8..700f57e207c 100644 --- a/cmake/docs/deps.svg +++ b/cmake/docs/deps.svg @@ -1,528 +1,528 @@ - - - + + CMakeDeps - + clusterZLIB - -madler/zlib.git + cmake patch + +madler/zlib.git + cmake patch clusterBZip2 - -bzip2/bzip2.git + cmake patch + +bzip2/bzip2.git + cmake patch clusterAbsl - -abseil/abseil-cpp.git + +abseil/abseil-cpp.git clusterProtobuf - -protocolbuffers/protobuf.git + +protocolbuffers/protobuf.git clusterRe2 - -google/re2.git + +google/re2.git clusterEigen3 - -libeigen/eigen.git + +libeigen/eigen.git clusterCoinOR - --DUSE_COINOR=ON AND -DBUILD_DEPS=ON + +-DUSE_COINOR=ON AND -DBUILD_DEPS=ON clusterCoinUtils - -Mizux/CoinUtils.git + +Mizux/CoinUtils.git clusterOsi - -Mizux/Osi.git + +Mizux/Osi.git clusterClp - -Mizux/Clp.git + +Mizux/Clp.git clusterCgl - -Mizux/Cgl.git + +Mizux/Cgl.git clusterCbc - -Mizux/Cbc.git + +Mizux/Cbc.git clusterGLPKSolver - --DUSE_GLPK=ON AND -DBUILD_GLPK=ON + +-DUSE_GLPK=ON AND -DBUILD_GLPK=ON clusterGLPK - -Mizux/GLPK.git + +Mizux/GLPK.git clusterHIGHSSolver - --DUSE_HIGHS=ON AND -DBUILD_HIGHS=ON + +-DUSE_HIGHS=ON AND -DBUILD_HIGHS=ON clusterHIGHS - -ERGO-Code/HIGHS.git + +ERGO-Code/HIGHS.git clusterSCIPSolver - --DUSE_SCIP=ON AND -DBUILD_SCIP=ON + +-DUSE_SCIP=ON AND -DBUILD_SCIP=ON clusterSoplex - -scipopt/soplex.git + +scipopt/soplex.git clusterSCIP - -scipopt/scip.git + +scipopt/scip.git clusterTesting - --DBUILD_TESTING=ON + +-DBUILD_TESTING=ON clusterGTest - -google/googletest.git + +google/googletest.git clusterBenchmark - -google/benchmark.git + +google/benchmark.git clusterFuzzTest - -google/fuzztest.git + +google/fuzztest.git clusterPython - --DBUILD_PYTHON=ON + +-DBUILD_PYTHON=ON clusterPybind11 - -pybind/pybind11.git + +pybind/pybind11.git clusterPybind11Absl - -pybind/pybind11_abseil.git + +pybind/pybind11_abseil.git clusterPybind11Protobuf - -pybind/pybind11_protobuf.git + +pybind/pybind11_protobuf.git ZLIB - -ZLIB::ZLIB + +ZLIB::ZLIB BZip2 - -BZip2::BZip2 + +BZip2::BZip2 Absl - -absl::absl_* + +absl::absl_* Protobuf - -protobuf::libprotobuf + +protobuf::libprotobuf Protobuf->ZLIB - - + + Protobuf->Absl - - + + Re2 - -re2::re2 + +re2::re2 Protobuf->Re2 - - + + Protoc - - - -protobuf::protoc + + + +protobuf::protoc Re2->Absl - - + + Eigen3 - -Eigen3::eigen + +Eigen3::eigen CoinUtils - -Coin::CoinUtils + +Coin::CoinUtils Osi - -Coin::Osi + +Coin::Osi Osi->CoinUtils - - + + Clp - -Coin::Clp + +Coin::Clp Clp->CoinUtils - - + + Clp->Osi - - + + OsiClp - -Coin::OsiClp + +Coin::OsiClp OsiClp->CoinUtils - - + + OsiClp->Osi - - + + OsiClp->Clp - - + + ClpSolver - -Coin::ClpSolver + +Coin::ClpSolver ClpSolver->Clp - - + + Cgl - -Coin::Cgl + +Coin::Cgl Cgl->CoinUtils - - + + Cgl->Osi - - + + Cgl->OsiClp - - + + Cbc - -Coin::Cbc + +Coin::Cbc Cbc->ZLIB - - + + Cbc->CoinUtils - - + + Cbc->Osi - - + + Cbc->Clp - - + + Cbc->Cgl - - + + OsiCbc - -Coin::OsiCbc + +Coin::OsiCbc OsiCbc->CoinUtils - - + + OsiCbc->Osi - - + + OsiCbc->Cbc - - + + CbcSolver - -Coin::CbcSolver + +Coin::CbcSolver CbcSolver->Cbc - - + + GLPK - -glpk::glpk + +glpk::glpk HIGHS - -highs::highs + +highs::highs SPX - -libsoplex + +libsoplex SPX->ZLIB - - + + SCIP - -SCIP::libscip + +SCIP::libscip SCIP->ZLIB - - + + SCIP->SPX - - + + gtest - -GTest::gtest + +GTest::gtest gtest->Absl - - + + gtest->Re2 - - + + bench - -benchmark::benchmark + +benchmark::benchmark bench->gtest - - + + fuzz - -fuzztest::fuzztest + +fuzztest::fuzztest fuzz->Absl - - + + fuzz->Protobuf - - + + fuzz->Re2 - - + + fuzz->gtest - - + + Pybind11 - -pybind11::pybind11 + +pybind11::pybind11 Pybind11Absl - -pybind11::pybind11_abseil + +pybind11::pybind11_abseil Pybind11Absl->Absl - - + + Pybind11Absl->Pybind11 - - + + Pybind11Protobuf - -pybind11::pybind11_protobuf + +pybind11::pybind11_protobuf Pybind11Protobuf->Protobuf - - + + Pybind11Protobuf->Pybind11 - - + + diff --git a/cmake/docs/docker.svg b/cmake/docs/docker.svg index bdc54dfc8c0..5f0240e4a5d 100644 --- a/cmake/docs/docker.svg +++ b/cmake/docs/docker.svg @@ -1,426 +1,426 @@ - - - + + DockerDeps - + clusterDockerfile - -docker/<distro>/Dockerfile + +docker/<distro>/Dockerfile clusterLang - -docker/<distro>/<lang>.Dockerfile + +docker/<distro>/<lang>.Dockerfile clusterCache - -cache/<distro>/ + +cache/<distro>/ DISTRO_IMG - - -<distro>:latest + + +<distro>:latest BASE_IMG - - -ortools/cmake:<distro>_base -base + + +ortools/cmake:<distro>_base +base DISTRO_IMG->BASE_IMG - - + + PKG - - - - -Build packages -e.g. cmake, g++ + + + + +Build packages +e.g. cmake, g++ PKG->BASE_IMG - - -install + + +install PYPKG - - - - -Python packages -e.g. python-dev + + + + +Python packages +e.g. python-dev LANGENV_IMG - - -ortools/cmake:<distro>_<lang>_env -env + + +ortools/cmake:<distro>_<lang>_env +env PYPKG->LANGENV_IMG - - -install + + +install JAVAPKG - - - - -Java packages -e.g. openjdk + + + + +Java packages +e.g. openjdk JAVAPKG->LANGENV_IMG - - -install + + +install DOTNETPKG - - - - -.Net packages -e.g. dotnet-cli + + + + +.Net packages +e.g. dotnet-cli DOTNETPKG->LANGENV_IMG - - -install + + +install SRC - -git repo + +git repo LANGDEVEL_IMG - - -ortools/cmake:<distro>_<lang>_devel -devel + + +ortools/cmake:<distro>_<lang>_devel +devel SRC->LANGDEVEL_IMG - - -copy + + +copy SAMPLE - -sample + +sample LANGINSTALLDEVEL_IMG - - -ortools/cmake:<distro>_<lang>_install_devel -install_devel + + +ortools/cmake:<distro>_<lang>_install_devel +install_devel SAMPLE->LANGINSTALLDEVEL_IMG - - -copy + + +copy SWIG_IMG - - -ortools/cmake:<distro>_swig -swig + + +ortools/cmake:<distro>_swig +swig BASE_IMG->SWIG_IMG - - + + BASE_TAR - - - -docker_base.tar + + + +docker_base.tar BASE_IMG->BASE_TAR - - -make save_<distro>_base + + +make save_<distro>_base SWIG_IMG->LANGENV_IMG - - + + SWIG_TAR - - - -docker_swig.tar + + + +docker_swig.tar SWIG_IMG->SWIG_TAR - - -make save_<distro>_swig + + +make save_<distro>_swig LANGENV_IMG->LANGDEVEL_IMG - - + + LANGINSTALLENV_IMG - - -ortools/cmake:<distro>_<lang>_install_env -install_env + + +ortools/cmake:<distro>_<lang>_install_env +install_env LANGENV_IMG->LANGINSTALLENV_IMG - - + + LANGENV_TAR - - - -docker_<lang>_env.tar + + + +docker_<lang>_env.tar LANGENV_IMG->LANGENV_TAR - - -make save_<distro>_<lang>_env + + +make save_<distro>_<lang>_env LANGBUILD_IMG - - -ortools/cmake:<distro>_<lang>_build -build + + +ortools/cmake:<distro>_<lang>_build +build LANGDEVEL_IMG->LANGBUILD_IMG - - + + LANGDEVEL_TAR - - - -docker_<lang>_devel.tar + + + +docker_<lang>_devel.tar LANGDEVEL_IMG->LANGDEVEL_TAR - - -make save_<distro>_<lang>_devel + + +make save_<distro>_<lang>_devel LANGTEST_IMG - - -ortools/cmake:<distro>_<lang>_test -test + + +ortools/cmake:<distro>_<lang>_test +test LANGBUILD_IMG->LANGTEST_IMG - - + + LANGBUILD_IMG->LANGINSTALLENV_IMG - - -copy install + + +copy install LANGBUILD_TAR - - - -docker_<lang>_build.tar + + + +docker_<lang>_build.tar LANGBUILD_IMG->LANGBUILD_TAR - - -make save_<distro>_<lang>_build + + +make save_<distro>_<lang>_build LANGTEST_TAR - - - -docker_<lang>_test.tar + + + +docker_<lang>_test.tar LANGTEST_IMG->LANGTEST_TAR - - -make save_<distro>_<lang>_test + + +make save_<distro>_<lang>_test LANGINSTALLENV_IMG->LANGINSTALLDEVEL_IMG - - + + LANGINSTALLENV_TAR - - - -docker_<lang>_install_env.tar + + + +docker_<lang>_install_env.tar LANGINSTALLENV_IMG->LANGINSTALLENV_TAR - - -make save_<distro>_<lang>_install_env + + +make save_<distro>_<lang>_install_env LANGINSTALLBUILD_IMG - - -ortools/cmake:<distro>_<lang>_install_build -install_build + + +ortools/cmake:<distro>_<lang>_install_build +install_build LANGINSTALLDEVEL_IMG->LANGINSTALLBUILD_IMG - - + + LANGINSTALLDEVEL_TAR - - - -docker_<lang>_install_devel.tar + + + +docker_<lang>_install_devel.tar LANGINSTALLDEVEL_IMG->LANGINSTALLDEVEL_TAR - - -make save_<distro>_<lang>_install_devel + + +make save_<distro>_<lang>_install_devel LANGINSTALLTEST_IMG - - -ortools/cmake:<distro>_<lang>_install_test -install_test + + +ortools/cmake:<distro>_<lang>_install_test +install_test LANGINSTALLBUILD_IMG->LANGINSTALLTEST_IMG - - + + LANGINSTALLBUILD_TAR - - - -docker_<lang>_install_build.tar + + + +docker_<lang>_install_build.tar LANGINSTALLBUILD_IMG->LANGINSTALLBUILD_TAR - - -make save_<distro>_<lang>_install_build + + +make save_<distro>_<lang>_install_build LANGINSTALLTEST_TAR - - - -docker_<lang>_install_test.tar + + + +docker_<lang>_install_test.tar LANGINSTALLTEST_IMG->LANGINSTALLTEST_TAR - - -make save_<distro>_<lang>_install_test + + +make save_<distro>_<lang>_install_test diff --git a/cmake/dotnet.cmake b/cmake/dotnet.cmake index c74042c04c0..c6fca998bf8 100644 --- a/cmake/dotnet.cmake +++ b/cmake/dotnet.cmake @@ -133,6 +133,7 @@ file(GLOB_RECURSE proto_dotnet_files RELATIVE ${PROJECT_SOURCE_DIR} "ortools/glop/*.proto" "ortools/graph/*.proto" "ortools/linear_solver/*.proto" + "ortools/routing/*.proto" "ortools/sat/*.proto" "ortools/util/*.proto" ) @@ -313,6 +314,7 @@ foreach(SUBPROJECT IN ITEMS init linear_solver constraint_solver + routing sat util) add_subdirectory(ortools/${SUBPROJECT}/csharp) diff --git a/cmake/host.CMakeLists.txt b/cmake/host.CMakeLists.txt index 970c4e0e404..6b63f172578 100644 --- a/cmake/host.CMakeLists.txt +++ b/cmake/host.CMakeLists.txt @@ -125,11 +125,11 @@ set(protobuf_WITH_ZLIB OFF) FetchContent_Declare( protobuf GIT_REPOSITORY "https://github.com/protocolbuffers/protobuf.git" - GIT_TAG "v31.0" + GIT_TAG "v31.1" GIT_SHALLOW TRUE GIT_SUBMODULES "" PATCH_COMMAND git apply --ignore-whitespace - "${CMAKE_CURRENT_LIST_DIR}/@PATCHES_PATH@/protobuf-v31.0.patch" + "${CMAKE_CURRENT_LIST_DIR}/@PATCHES_PATH@/protobuf-v31.1.patch" ) FetchContent_MakeAvailable(protobuf) list(POP_BACK CMAKE_MESSAGE_INDENT) diff --git a/cmake/host.cmake b/cmake/host.cmake index fe303362fe4..f95949cb908 100644 --- a/cmake/host.cmake +++ b/cmake/host.cmake @@ -11,6 +11,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +if (OR_TOOLS_PROTOC_EXECUTABLE) + set(PROTOC_PRG ${OR_TOOLS_PROTOC_EXECUTABLE}) + return() +endif() + if(NOT CMAKE_CROSSCOMPILING) set(PROTOC_PRG protobuf::protoc) return() diff --git a/cmake/java.cmake b/cmake/java.cmake index 74184c1f4f2..c515c0c17bf 100644 --- a/cmake/java.cmake +++ b/cmake/java.cmake @@ -97,6 +97,7 @@ file(GLOB_RECURSE proto_java_files RELATIVE ${PROJECT_SOURCE_DIR} "ortools/glop/*.proto" "ortools/graph/*.proto" "ortools/linear_solver/*.proto" + "ortools/routing/*.proto" "ortools/sat/*.proto" "ortools/util/*.proto" ) @@ -259,6 +260,7 @@ foreach(SUBPROJECT IN ITEMS init linear_solver constraint_solver + routing sat util) add_subdirectory(ortools/${SUBPROJECT}/java) diff --git a/cmake/python.cmake b/cmake/python.cmake index 2112ceeb663..0a4641faa3d 100644 --- a/cmake/python.cmake +++ b/cmake/python.cmake @@ -148,6 +148,7 @@ file(GLOB_RECURSE OR_TOOLS_PROTO_PY_FILES RELATIVE ${PROJECT_SOURCE_DIR} "ortools/graph/*.proto" "ortools/linear_solver/*.proto" "ortools/packing/*.proto" + "ortools/routing/*.proto" "ortools/sat/*.proto" "ortools/scheduling/*.proto" "ortools/set_cover/*.proto" @@ -287,6 +288,7 @@ foreach(SUBPROJECT IN ITEMS linear_solver ${PDLP_DIR} constraint_solver + routing sat scheduling set_cover @@ -317,6 +319,7 @@ file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/algorithms/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/algorithms/python/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/bop/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/constraint_solver/__init__.py CONTENT "") +file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/constraint_solver/python/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/glop/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/graph/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/graph/python/__init__.py CONTENT "") @@ -349,6 +352,9 @@ if(USE_PDLP OR BUILD_MATH_OPT) file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/pdlp/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/pdlp/python/__init__.py CONTENT "") endif() +file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/routing/__init__.py CONTENT "") +file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/routing/python/__init__.py CONTENT "") + file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/sat/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/sat/python/__init__.py CONTENT "") file(GENERATE OUTPUT ${PYTHON_PROJECT_DIR}/sat/colab/__init__.py CONTENT "") @@ -667,6 +673,8 @@ add_custom_command( $ ${PYTHON_PROJECT}/graph/python COMMAND ${CMAKE_COMMAND} -E copy $ ${PYTHON_PROJECT}/constraint_solver + COMMAND ${CMAKE_COMMAND} -E copy + $ ${PYTHON_PROJECT}/constraint_solver/python COMMAND ${CMAKE_COMMAND} -E copy $ ${PYTHON_PROJECT}/linear_solver COMMAND ${CMAKE_COMMAND} -E copy @@ -686,6 +694,10 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E $,copy,true> $<$:$> ${PYTHON_PROJECT}/pdlp/python + COMMAND ${CMAKE_COMMAND} -E copy + $ ${PYTHON_PROJECT}/routing + COMMAND ${CMAKE_COMMAND} -E copy + $ ${PYTHON_PROJECT}/routing/python COMMAND ${CMAKE_COMMAND} -E copy $ ${PYTHON_PROJECT}/sat/python COMMAND ${CMAKE_COMMAND} -E copy @@ -704,6 +716,9 @@ add_custom_command( max_flow_pybind11 min_cost_flow_pybind11 pywrapcp + constraint_solver_pybind11 + pywraprouting + routing_pybind11 pywraplp model_builder_helper_pybind11 $<$:math_opt_core_pybind11> @@ -742,11 +757,14 @@ add_custom_command( COMMAND ${stubgen_EXECUTABLE} -p ortools.graph.python.max_flow --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.graph.python.min_cost_flow --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.constraint_solver.pywrapcp --output . + COMMAND ${stubgen_EXECUTABLE} -p ortools.constraint_solver.python.constraint_solver --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.linear_solver.pywraplp --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.linear_solver.python.model_builder_helper --output . COMMAND ${stubgen_EXECUTABLE} -p pybind11_abseil.status --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.math_opt.core.python.solver --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.pdlp.python.pdlp --output . + COMMAND ${stubgen_EXECUTABLE} -p ortools.routing.pywraprouting --output . + COMMAND ${stubgen_EXECUTABLE} -p ortools.routing.python.model --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.sat.python.cp_model_helper --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.scheduling.python.rcpsp --output . COMMAND ${stubgen_EXECUTABLE} -p ortools.set_cover.python.set_cover --output . diff --git a/cmake/samples/dotnet/CPSample.cs b/cmake/samples/dotnet/CPSample.cs index 35cb078ddce..f0a64857f6c 100644 --- a/cmake/samples/dotnet/CPSample.cs +++ b/cmake/samples/dotnet/CPSample.cs @@ -16,22 +16,26 @@ using Google.OrTools.ConstraintSolver; -namespace Google.OrTools.Tests { - public class ConstraintSolverTest { +namespace Google.OrTools.Tests +{ +public class ConstraintSolverTest +{ [Theory] [InlineData(false)] [InlineData(true)] - public void SolverTest(bool callGC) { - Solver solver = new Solver("Solver"); - IntVar x = solver.MakeIntVar(3, 7, "x"); + public void SolverTest(bool callGC) + { + Solver solver = new Solver("Solver"); + IntVar x = solver.MakeIntVar(3, 7, "x"); - if (callGC) { - GC.Collect(); - } + if (callGC) + { + GC.Collect(); + } - Assert.Equal(3, x.Min()); - Assert.Equal(7, x.Max()); - Assert.Equal("x(3..7)", x.ToString()); + Assert.Equal(3, x.Min()); + Assert.Equal(7, x.Max()); + Assert.Equal("x(3..7)", x.ToString()); } - } +} } // namespace Google.Sample.Tests diff --git a/cmake/samples/dotnet/LPSample.cs b/cmake/samples/dotnet/LPSample.cs index 523a3db9f27..fecda503124 100644 --- a/cmake/samples/dotnet/LPSample.cs +++ b/cmake/samples/dotnet/LPSample.cs @@ -16,19 +16,21 @@ using Google.OrTools.LinearSolver; -namespace Google.OrTools.Tests { - public class LinearSolverTest { +namespace Google.OrTools.Tests +{ +public class LinearSolverTest +{ [Theory] [InlineData(false)] [InlineData(true)] - public void SolverTest(bool callGC) { - Solver solver = new Solver( - "Solver", - Solver.OptimizationProblemType.CLP_LINEAR_PROGRAMMING); + public void SolverTest(bool callGC) + { + Solver solver = new Solver("Solver", Solver.OptimizationProblemType.CLP_LINEAR_PROGRAMMING); - if (callGC) { - GC.Collect(); - } + if (callGC) + { + GC.Collect(); + } } - } +} } // namespace Google.Sample.Tests diff --git a/cmake/samples/dotnet/RoutingSample.cs b/cmake/samples/dotnet/RoutingSample.cs index 7a1a44f4197..e2ccfb9996a 100644 --- a/cmake/samples/dotnet/RoutingSample.cs +++ b/cmake/samples/dotnet/RoutingSample.cs @@ -15,39 +15,42 @@ using Xunit; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; -namespace Google.OrTools.Tests { - public class RoutingSolverTest { +namespace Google.OrTools.Tests +{ +public class RoutingSolverTest +{ [Theory] [InlineData(false)] [InlineData(true)] - public void SolverTest(bool callGC) { - // Create Routing Index Manager - RoutingIndexManager manager = new RoutingIndexManager( - 5/*locations*/, 1/*vehicle*/, 0/*depot*/); - // Create Routing Model. - RoutingModel routing = new RoutingModel(manager); - // Create a distance callback. - int transitCallbackIndex = routing.RegisterTransitCallback( - (long fromIndex, long toIndex) => { - // Convert from routing variable Index to distance matrix NodeIndex. - var fromNode = manager.IndexToNode(fromIndex); - var toNode = manager.IndexToNode(toIndex); - return Math.Abs(toNode - fromNode); - }); - // Define cost of each arc. - routing.SetArcCostEvaluatorOfAllVehicles(transitCallbackIndex); - if (callGC) { - GC.Collect(); - } - // Setting first solution heuristic. - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); - searchParameters.FirstSolutionStrategy = - FirstSolutionStrategy.Types.Value.PathCheapestArc; - Assignment solution = routing.SolveWithParameters(searchParameters); - // 0 --(+1)-> 1 --(+1)-> 2 --(+1)-> 3 --(+1)-> 4 --(+4)-> 0 := +8 - Assert.Equal(8, solution.ObjectiveValue()); + public void SolverTest(bool callGC) + { + // Create Routing Index Manager + RoutingIndexManager manager = new RoutingIndexManager(5 /*locations*/, 1 /*vehicle*/, 0 /*depot*/); + // Create Routing Model. + RoutingModel routing = new RoutingModel(manager); + // Create a distance callback. + int transitCallbackIndex = routing.RegisterTransitCallback((long fromIndex, long toIndex) => + { + // Convert from routing variable Index to + // distance matrix NodeIndex. + var fromNode = manager.IndexToNode(fromIndex); + var toNode = manager.IndexToNode(toIndex); + return Math.Abs(toNode - fromNode); + }); + // Define cost of each arc. + routing.SetArcCostEvaluatorOfAllVehicles(transitCallbackIndex); + if (callGC) + { + GC.Collect(); + } + // Setting first solution heuristic. + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); + searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; + Assignment solution = routing.SolveWithParameters(searchParameters); + // 0 --(+1)-> 1 --(+1)-> 2 --(+1)-> 3 --(+1)-> 4 --(+4)-> 0 := +8 + Assert.Equal(8, solution.ObjectiveValue()); } - } +} } // namespace Google.Sample.Tests diff --git a/cmake/samples/dotnet/SATSample.cs b/cmake/samples/dotnet/SATSample.cs index 51e287e8047..7e74e248601 100644 --- a/cmake/samples/dotnet/SATSample.cs +++ b/cmake/samples/dotnet/SATSample.cs @@ -16,26 +16,30 @@ using Google.OrTools.Sat; -namespace Google.OrTools.Tests { - public class SatSolverTest { +namespace Google.OrTools.Tests +{ +public class SatSolverTest +{ [Theory] [InlineData(false)] [InlineData(true)] - public void SolverTest(bool callGC) { - CpModel model = new CpModel(); + public void SolverTest(bool callGC) + { + CpModel model = new CpModel(); - int num_vals = 3; - IntVar x = model.NewIntVar(0, num_vals - 1, "x"); - IntVar y = model.NewIntVar(0, num_vals - 1, "y"); - IntVar z = model.NewIntVar(0, num_vals - 1, "z"); + int num_vals = 3; + IntVar x = model.NewIntVar(0, num_vals - 1, "x"); + IntVar y = model.NewIntVar(0, num_vals - 1, "y"); + IntVar z = model.NewIntVar(0, num_vals - 1, "z"); - model.Add(x != y); + model.Add(x != y); - CpSolver solver = new CpSolver(); - if (callGC) { - GC.Collect(); - } - CpSolverStatus status = solver.Solve(model); + CpSolver solver = new CpSolver(); + if (callGC) + { + GC.Collect(); + } + CpSolverStatus status = solver.Solve(model); } - } +} } // namespace Google.Sample.Tests diff --git a/cmake/samples/python/sample.py b/cmake/samples/python/sample.py index 758346bcf17..14e3062ac69 100644 --- a/cmake/samples/python/sample.py +++ b/cmake/samples/python/sample.py @@ -14,12 +14,15 @@ """Sample to test or-tools installation.""" import ortools + # from ortools.algorithms import knapsack_solver from ortools.constraint_solver import pywrapcp + # from ortools.graph.python import linear_sum_assignment # from ortools.graph.python import max_flow # from ortools.graph.python import min_cost_flow from ortools.linear_solver import pywraplp + # from ortools.linear_solver import linear_solver_pb2 # from ortools.sat.python import cp_model_helper # from ortools.sat.python import cp_model @@ -29,26 +32,28 @@ def lpsolver_test(): """Test pywraplp.""" - print('Test lpsolver...') - lpsolver = pywraplp.Solver('LinearTest', - pywraplp.Solver.GLOP_LINEAR_PROGRAMMING) + print("Test lpsolver...") + lpsolver = pywraplp.Solver( + "LinearTest", pywraplp.Solver.GLOP_LINEAR_PROGRAMMING + ) lpsolver.Solve() - print('Test lpsolver...DONE') + print("Test lpsolver...DONE") def cpsolver_test(): """Test pywrapcp.""" - print('Test cpsolver...') - cpsolver = pywrapcp.Solver('ConstraintTest') + print("Test cpsolver...") + cpsolver = pywrapcp.Solver("ConstraintTest") num_vals = 3 - x = cpsolver.IntVar(0, num_vals - 1, 'x') - y = cpsolver.IntVar(0, num_vals - 1, 'y') - z = cpsolver.IntVar(0, num_vals - 1, 'z') + x = cpsolver.IntVar(0, num_vals - 1, "x") + y = cpsolver.IntVar(0, num_vals - 1, "y") + z = cpsolver.IntVar(0, num_vals - 1, "z") cpsolver.Add(x != y) - db = cpsolver.Phase([x, y, z], cpsolver.CHOOSE_FIRST_UNBOUND, - cpsolver.ASSIGN_MIN_VALUE) + db = cpsolver.Phase( + [x, y, z], cpsolver.CHOOSE_FIRST_UNBOUND, cpsolver.ASSIGN_MIN_VALUE + ) cpsolver.Solve(db) - print('Test cpsolver...DONE') + print("Test cpsolver...DONE") def main(): @@ -57,5 +62,5 @@ def main(): cpsolver_test() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/examples/contrib/3_jugs_mip.py b/examples/contrib/3_jugs_mip.py index 49b78a95292..e44a5bb8288 100644 --- a/examples/contrib/3_jugs_mip.py +++ b/examples/contrib/3_jugs_mip.py @@ -13,19 +13,19 @@ # limitations under the License. """ - 3 jugs problem using MIP in Google or-tools. +3 jugs problem using MIP in Google or-tools. - A.k.a. water jugs problem. +A.k.a. water jugs problem. - Problem from Taha 'Introduction to Operations Research', - page 245f . +Problem from Taha 'Introduction to Operations Research', +page 245f . - Compare with the CP model: - http://www.hakank.org/google_or_tools/3_jugs_regular +Compare with the CP model: + http://www.hakank.org/google_or_tools/3_jugs_regular - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp @@ -61,25 +61,27 @@ def main(sol='CBC'): '6,0,2', '1,5,2', '1,4,3', - '4,4,0' # goal! + '4,4,0', # goal! ] # distance - d = [[M, 1, M, M, M, M, M, M, 1, M, M, M, M, M, M], - [M, M, 1, M, M, M, M, M, M, M, M, M, M, M, M], - [M, M, M, 1, M, M, M, M, 1, M, M, M, M, M, M], - [M, M, M, M, 1, M, M, M, M, M, M, M, M, M, M], - [M, M, M, M, M, 1, M, M, 1, M, M, M, M, M, M], - [M, M, M, M, M, M, 1, M, M, M, M, M, M, M, M], - [M, M, M, M, M, M, M, 1, 1, M, M, M, M, M, M], - [M, M, M, M, M, M, M, M, M, M, M, M, M, M, 1], - [M, M, M, M, M, M, M, M, M, 1, M, M, M, M, M], - [M, 1, M, M, M, M, M, M, M, M, 1, M, M, M, M], - [M, M, M, M, M, M, M, M, M, M, M, 1, M, M, M], - [M, 1, M, M, M, M, M, M, M, M, M, M, 1, M, M], - [M, M, M, M, M, M, M, M, M, M, M, M, M, 1, M], - [M, 1, M, M, M, M, M, M, M, M, M, M, M, M, 1], - [M, M, M, M, M, M, M, M, M, M, M, M, M, M, M]] + d = [ + [M, 1, M, M, M, M, M, M, 1, M, M, M, M, M, M], + [M, M, 1, M, M, M, M, M, M, M, M, M, M, M, M], + [M, M, M, 1, M, M, M, M, 1, M, M, M, M, M, M], + [M, M, M, M, 1, M, M, M, M, M, M, M, M, M, M], + [M, M, M, M, M, 1, M, M, 1, M, M, M, M, M, M], + [M, M, M, M, M, M, 1, M, M, M, M, M, M, M, M], + [M, M, M, M, M, M, M, 1, 1, M, M, M, M, M, M], + [M, M, M, M, M, M, M, M, M, M, M, M, M, M, 1], + [M, M, M, M, M, M, M, M, M, 1, M, M, M, M, M], + [M, 1, M, M, M, M, M, M, M, M, 1, M, M, M, M], + [M, M, M, M, M, M, M, M, M, M, M, 1, M, M, M], + [M, 1, M, M, M, M, M, M, M, M, M, M, 1, M, M], + [M, M, M, M, M, M, M, M, M, M, M, M, M, 1, M], + [M, 1, M, M, M, M, M, M, M, M, M, M, M, M, 1], + [M, M, M, M, M, M, M, M, M, M, M, M, M, M, M], + ] # # variables @@ -98,7 +100,8 @@ def main(sol='CBC'): # length of path, to be minimized z = solver.Sum( - [d[i][j] * x[i, j] for i in range(n) for j in range(n) if d[i][j] < M]) + [d[i][j] * x[i, j] for i in range(n) for j in range(n) if d[i][j] < M] + ) # # constraints @@ -115,12 +118,14 @@ def main(sol='CBC'): # outflow constraint for i in range(n): solver.Add( - out_flow[i] == solver.Sum([x[i, j] for j in range(n) if d[i][j] < M])) + out_flow[i] == solver.Sum([x[i, j] for j in range(n) if d[i][j] < M]) + ) # inflow constraint for j in range(n): solver.Add( - in_flow[j] == solver.Sum([x[i, j] for i in range(n) if d[i][j] < M])) + in_flow[j] == solver.Sum([x[i, j] for i in range(n) if d[i][j] < M]) + ) # inflow = outflow for i in range(n): diff --git a/examples/contrib/3_jugs_regular.py b/examples/contrib/3_jugs_regular.py index 888ec0e2c0a..85161617719 100644 --- a/examples/contrib/3_jugs_regular.py +++ b/examples/contrib/3_jugs_regular.py @@ -13,37 +13,37 @@ # limitations under the License. """ - 3 jugs problem using regular constraint in Google CP Solver. +3 jugs problem using regular constraint in Google CP Solver. - A.k.a. water jugs problem. +A.k.a. water jugs problem. - Problem from Taha 'Introduction to Operations Research', - page 245f . +Problem from Taha 'Introduction to Operations Research', +page 245f . - For more info about the problem, see: - http://mathworld.wolfram.com/ThreeJugProblem.html +For more info about the problem, see: +http://mathworld.wolfram.com/ThreeJugProblem.html - This model use a regular constraint for handling the - transitions between the states. Instead of minimizing - the cost in a cost matrix (as shortest path problem), - we here call the model with increasing length of the - sequence array (x). +This model use a regular constraint for handling the +transitions between the states. Instead of minimizing +the cost in a cost matrix (as shortest path problem), +we here call the model with increasing length of the +sequence array (x). - Compare with other models that use MIP/CP approach, - as a shortest path problem: - * Comet: http://www.hakank.org/comet/3_jugs.co - * Comet: http://www.hakank.org/comet/water_buckets1.co - * MiniZinc: http://www.hakank.org/minizinc/3_jugs.mzn - * MiniZinc: http://www.hakank.org/minizinc/3_jugs2.mzn - * SICStus: http://www.hakank.org/sicstus/3_jugs.pl - * ECLiPSe: http://www.hakank.org/eclipse/3_jugs.ecl - * ECLiPSe: http://www.hakank.org/eclipse/3_jugs2.ecl - * Gecode: http://www.hakank.org/gecode/3_jugs2.cpp +Compare with other models that use MIP/CP approach, +as a shortest path problem: +* Comet: http://www.hakank.org/comet/3_jugs.co +* Comet: http://www.hakank.org/comet/water_buckets1.co +* MiniZinc: http://www.hakank.org/minizinc/3_jugs.mzn +* MiniZinc: http://www.hakank.org/minizinc/3_jugs2.mzn +* SICStus: http://www.hakank.org/sicstus/3_jugs.pl +* ECLiPSe: http://www.hakank.org/eclipse/3_jugs.ecl +* ECLiPSe: http://www.hakank.org/eclipse/3_jugs2.ecl +* Gecode: http://www.hakank.org/gecode/3_jugs2.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ @@ -117,7 +117,8 @@ def regular(x, Q, S, d, q0, F): # Determine a[i+1]: a[i+1] == d2[a[i], x[i]] solver.Add( - a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1))) + a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1)) + ) def main(n): @@ -175,7 +176,7 @@ def main(n): [12], # state 11 [13], # state 12 [14], # state 13 - [15] # state 14 + [15], # state 14 ] transition_fn = [] @@ -207,7 +208,7 @@ def main(n): '6,0,2', # 12 '1,5,2', # 13 '1,4,3', # 14 - '4,4,0' # 15 goal + '4,4,0', # 15 goal ] # @@ -218,8 +219,9 @@ def main(n): # # constraints # - regular(x, n_states, input_max, transition_fn, initial_state, - accepting_states) + regular( + x, n_states, input_max, transition_fn, initial_state, accepting_states + ) # # solution and search diff --git a/examples/contrib/SimpleRoutingTest.java b/examples/contrib/SimpleRoutingTest.java index 2fa141a399c..0d896644bd2 100644 --- a/examples/contrib/SimpleRoutingTest.java +++ b/examples/contrib/SimpleRoutingTest.java @@ -2,11 +2,11 @@ import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; +import com.google.ortools.routing.Globals; import java.util.ArrayList; import java.util.function.LongBinaryOperator; @@ -70,7 +70,7 @@ public void solve() { RoutingModel routing = new RoutingModel(manager); RoutingSearchParameters parameters = RoutingSearchParameters.newBuilder() - .mergeFrom(main.defaultRoutingSearchParameters()) + .mergeFrom(Globals.defaultRoutingSearchParameters()) .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); NodeDistance distances = new NodeDistance(manager, costMatrix); diff --git a/examples/contrib/a_round_of_golf.py b/examples/contrib/a_round_of_golf.py index 15b45be7afd..7fe4c8c3b9a 100644 --- a/examples/contrib/a_round_of_golf.py +++ b/examples/contrib/a_round_of_golf.py @@ -13,54 +13,54 @@ # limitations under the License. """ - A Round of Golf puzzle (Dell Logic Puzzles) in Google CP Solver. - - From http://brownbuffalo.sourceforge.net/RoundOfGolfClues.html - ''' - Title: A Round of Golf - Author: Ellen K. Rodehorst - Publication: Dell Favorite Logic Problems - Issue: Summer, 2000 - Puzzle #: 9 - Stars: 1 - - When the Sunny Hills Country Club golf course isn't in use by club members, - of course, it's open to the club's employees. Recently, Jack and three other - workers at the golf course got together on their day off to play a round of - eighteen holes of golf. - Afterward, all four, including Mr. Green, went to the clubhouse to total - their scorecards. Each man works at a different job (one is a short-order - cook), and each shot a different score in the game. No one scored below - 70 or above 85 strokes. From the clues below, can you discover each man's - full name, job and golf score? - - 1. Bill, who is not the maintenance man, plays golf often and had the lowest - score of the foursome. - 2. Mr. Clubb, who isn't Paul, hit several balls into the woods and scored ten - strokes more than the pro-shop clerk. - 3. In some order, Frank and the caddy scored four and seven more strokes than - Mr. Sands. - 4. Mr. Carter thought his score of 78 was one of his better games, even - though Frank's score was lower. - 5. None of the four scored exactly 81 strokes. - - Determine: First Name - Last Name - Job - Score - ''' - - Compare with the F1 model: - http://www.f1compiler.com/samples/A 20Round 20of 20Golf.f1.html - - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/a_round_of_golf.mzn - * Comet : http://www.hakank.org/comet/a_round_of_golf.co - * ECLiPSe : http://www.hakank.org/eclipse/a_round_of_golf.ecl - * Gecode : http://hakank.org/gecode/a_round_of_golf.cpp - * SICStus : http://hakank.org/sicstus/a_round_of_golf.pl - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +A Round of Golf puzzle (Dell Logic Puzzles) in Google CP Solver. + +From http://brownbuffalo.sourceforge.net/RoundOfGolfClues.html +''' +Title: A Round of Golf +Author: Ellen K. Rodehorst +Publication: Dell Favorite Logic Problems +Issue: Summer, 2000 +Puzzle #: 9 +Stars: 1 + +When the Sunny Hills Country Club golf course isn't in use by club members, +of course, it's open to the club's employees. Recently, Jack and three other +workers at the golf course got together on their day off to play a round of +eighteen holes of golf. +Afterward, all four, including Mr. Green, went to the clubhouse to total +their scorecards. Each man works at a different job (one is a short-order +cook), and each shot a different score in the game. No one scored below +70 or above 85 strokes. From the clues below, can you discover each man's +full name, job and golf score? + +1. Bill, who is not the maintenance man, plays golf often and had the lowest +score of the foursome. +2. Mr. Clubb, who isn't Paul, hit several balls into the woods and scored ten +strokes more than the pro-shop clerk. +3. In some order, Frank and the caddy scored four and seven more strokes than +Mr. Sands. +4. Mr. Carter thought his score of 78 was one of his better games, even + though Frank's score was lower. +5. None of the four scored exactly 81 strokes. + +Determine: First Name - Last Name - Job - Score +''' + +Compare with the F1 model: +http://www.f1compiler.com/samples/A 20Round 20of 20Golf.f1.html + + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/a_round_of_golf.mzn +* Comet : http://www.hakank.org/comet/a_round_of_golf.co +* ECLiPSe : http://www.hakank.org/eclipse/a_round_of_golf.ecl +* Gecode : http://hakank.org/gecode/a_round_of_golf.cpp +* SICStus : http://hakank.org/sicstus/a_round_of_golf.pl + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ @@ -117,13 +117,13 @@ def main(): b3_a_1 = solver.IsEqualVar(solver.Element(score, Sands) + 4, score[Frank]) b3_a_2 = solver.IsEqualVar( - solver.Element(score, caddy), - solver.Element(score, Sands) + 7) + solver.Element(score, caddy), solver.Element(score, Sands) + 7 + ) b3_b_1 = solver.IsEqualVar(solver.Element(score, Sands) + 7, score[Frank]) b3_b_2 = solver.IsEqualVar( - solver.Element(score, caddy), - solver.Element(score, Sands) + 4) + solver.Element(score, caddy), solver.Element(score, Sands) + 4 + ) solver.Add((b3_a_1 * b3_a_2) + (b3_b_1 * b3_b_2) == 1) @@ -144,8 +144,11 @@ def main(): solution.Add(job) solution.Add(score) - db = solver.Phase(last_name + job + score, solver.CHOOSE_FIRST_UNBOUND, - solver.INT_VALUE_DEFAULT) + db = solver.Phase( + last_name + job + score, + solver.CHOOSE_FIRST_UNBOUND, + solver.INT_VALUE_DEFAULT, + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/contrib/all_interval.py b/examples/contrib/all_interval.py index 5990ed28274..8d74a1e89ef 100644 --- a/examples/contrib/all_interval.py +++ b/examples/contrib/all_interval.py @@ -13,40 +13,40 @@ # limitations under the License. """ - All interval problem in Google CP Solver. - - CSPLib problem number 7 - http://www.cs.st-andrews.ac.uk/~ianm/CSPLib/prob/prob007/index.html - ''' - Given the twelve standard pitch-classes (c, c , d, ...), represented by - numbers 0,1,...,11, find a series in which each pitch-class occurs exactly - once and in which the musical intervals between neighbouring notes cover - the full set of intervals from the minor second (1 semitone) to the major - seventh (11 semitones). That is, for each of the intervals, there is a - pair of neigbhouring pitch-classes in the series, between which this - interval appears. The problem of finding such a series can be easily - formulated as an instance of a more general arithmetic problem on Z_n, - the set of integer residues modulo n. Given n in N, find a vector - s = (s_1, ..., s_n), such that (i) s is a permutation of - Z_n = {0,1,...,n-1}; and (ii) the interval vector - v = (|s_2-s_1|, |s_3-s_2|, ... |s_n-s_{n-1}|) is a permutation of - Z_n-{0} = {1,2,...,n-1}. A vector v satisfying these conditions is - called an all-interval series of size n; the problem of finding such - a series is the all-interval series problem of size n. We may also be - interested in finding all possible series of a given size. - ''' - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/all_interval.mzn - * Comet : http://www.hakank.org/comet/all_interval.co - * Gecode/R: http://www.hakank.org/gecode_r/all_interval.rb - * ECLiPSe : http://www.hakank.org/eclipse/all_interval.ecl - * SICStus : http://www.hakank.org/sicstus/all_interval.pl - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ + All interval problem in Google CP Solver. + + CSPLib problem number 7 + http://www.cs.st-andrews.ac.uk/~ianm/CSPLib/prob/prob007/index.html + ''' + Given the twelve standard pitch-classes (c, c , d, ...), represented by + numbers 0,1,...,11, find a series in which each pitch-class occurs exactly + once and in which the musical intervals between neighbouring notes cover + the full set of intervals from the minor second (1 semitone) to the major + seventh (11 semitones). That is, for each of the intervals, there is a + pair of neigbhouring pitch-classes in the series, between which this + interval appears. The problem of finding such a series can be easily + formulated as an instance of a more general arithmetic problem on Z_n, + the set of integer residues modulo n. Given n in N, find a vector + s = (s_1, ..., s_n), such that (i) s is a permutation of + Z_n = {0,1,...,n-1}; and (ii) the interval vector + v = (|s_2-s_1|, |s_3-s_2|, ... |s_n-s_{n-1}|) is a permutation of + Z_n-{0} = {1,2,...,n-1}. A vector v satisfying these conditions is + called an all-interval series of size n; the problem of finding such + a series is the all-interval series problem of size n. We may also be + interested in finding all possible series of a given size. + ''' + +Compare with the following models: + * MiniZinc: http://www.hakank.org/minizinc/all_interval.mzn + * Comet : http://www.hakank.org/comet/all_interval.co + * Gecode/R: http://www.hakank.org/gecode_r/all_interval.rb + * ECLiPSe : http://www.hakank.org/eclipse/all_interval.ecl + * SICStus : http://www.hakank.org/sicstus/all_interval.pl + + + This model was created by Hakan Kjellerstrand (hakank@gmail.com) + Also see my other Google CP Solver models: + http://www.hakank.org/google_or_tools/ """ diff --git a/examples/contrib/alldifferent_except_0.py b/examples/contrib/alldifferent_except_0.py index 6eebcd14230..dd8247d9e8c 100644 --- a/examples/contrib/alldifferent_except_0.py +++ b/examples/contrib/alldifferent_except_0.py @@ -13,38 +13,38 @@ # limitations under the License. """ - All different except 0 Google CP Solver. - - Decomposition of global constraint alldifferent_except_0. - - From Global constraint catalogue: - http://www.emn.fr/x-info/sdemasse/gccat/Calldifferent_except_0.html - ''' - Enforce all variables of the collection VARIABLES to take distinct - values, except those variables that are assigned to 0. - - Example - (<5, 0, 1, 9, 0, 3>) - - The alldifferent_except_0 constraint holds since all the values - (that are different from 0) 5, 1, 9 and 3 are distinct. - ''' - - Compare with the following models: - * Comet: http://hakank.org/comet/alldifferent_except_0.co - * ECLiPSe: http://hakank.org/eclipse/alldifferent_except_0.ecl - * Tailor/Essence': http://hakank.org/tailor/alldifferent_except_0.eprime - * Gecode: http://hakank.org/gecode/alldifferent_except_0.cpp - * Gecode/R: http://hakank.org/gecode_r/all_different_except_0.rb - * MiniZinc: http://hakank.org/minizinc/alldifferent_except_0.mzn - * SICStus_ http://hakank.org/sicstus/alldifferent_except_0.pl - * Choco: http://hakank.org/choco/AllDifferentExcept0_test.java - * JaCoP: http://hakank.org/JaCoP/AllDifferentExcept0_test.java - * Zinc: http://hakank.org/minizinc/alldifferent_except_0.zinc - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +All different except 0 Google CP Solver. + +Decomposition of global constraint alldifferent_except_0. + +From Global constraint catalogue: +http://www.emn.fr/x-info/sdemasse/gccat/Calldifferent_except_0.html +''' +Enforce all variables of the collection VARIABLES to take distinct +values, except those variables that are assigned to 0. + +Example + (<5, 0, 1, 9, 0, 3>) + +The alldifferent_except_0 constraint holds since all the values +(that are different from 0) 5, 1, 9 and 3 are distinct. +''' + +Compare with the following models: +* Comet: http://hakank.org/comet/alldifferent_except_0.co +* ECLiPSe: http://hakank.org/eclipse/alldifferent_except_0.ecl +* Tailor/Essence': http://hakank.org/tailor/alldifferent_except_0.eprime +* Gecode: http://hakank.org/gecode/alldifferent_except_0.cpp +* Gecode/R: http://hakank.org/gecode_r/all_different_except_0.rb +* MiniZinc: http://hakank.org/minizinc/alldifferent_except_0.mzn +* SICStus_ http://hakank.org/sicstus/alldifferent_except_0.pl +* Choco: http://hakank.org/choco/AllDifferentExcept0_test.java +* JaCoP: http://hakank.org/JaCoP/AllDifferentExcept0_test.java +* Zinc: http://hakank.org/minizinc/alldifferent_except_0.zinc + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ @@ -106,8 +106,13 @@ def main(unused_argv): collector = solver.AllSolutionCollector(solution) solver.Solve( - solver.Phase([x[i] for i in range(n)], solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE), [collector]) + solver.Phase( + [x[i] for i in range(n)], + solver.CHOOSE_FIRST_UNBOUND, + solver.ASSIGN_MIN_VALUE, + ), + [collector], + ) num_solutions = collector.SolutionCount() for s in range(num_solutions): diff --git a/examples/contrib/alphametic.py b/examples/contrib/alphametic.py index c2deec0cbcf..5af53d3cb3f 100644 --- a/examples/contrib/alphametic.py +++ b/examples/contrib/alphametic.py @@ -13,34 +13,34 @@ # limitations under the License. """ - Generic alphametic solver in Google CP Solver. +Generic alphametic solver in Google CP Solver. - This is a generic alphametic solver. +This is a generic alphametic solver. - Usage: - python alphametic.py - -> solves SEND+MORE=MONEY in base 10 +Usage: + python alphametic.py + -> solves SEND+MORE=MONEY in base 10 - python alphametic.py 'SEND+MOST=MONEY' 11 - -> solver SEND+MOST=MONEY in base 11 + python alphametic.py 'SEND+MOST=MONEY' 11 + -> solver SEND+MOST=MONEY in base 11 - python alphametic.py TEST - -> solve some test problems in base - (defined in test_problems()) + python alphametic.py TEST + -> solve some test problems in base + (defined in test_problems()) - Assumptions: - - we only solves problems of the form - NUMBER<1>+NUMBER<2>...+NUMBER = NUMBER - i.e. the last number is the sum - - the only nonletter characters are: +, =, \d (which are splitted upon) +Assumptions: +- we only solves problems of the form + NUMBER<1>+NUMBER<2>...+NUMBER = NUMBER + i.e. the last number is the sum +- the only nonletter characters are: +, =, \d (which are splitted upon) - Compare with the following model: - * Zinc: http://www.hakank.org/minizinc/alphametic.zinc +Compare with the following model: +* Zinc: http://www.hakank.org/minizinc/alphametic.zinc - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -78,7 +78,7 @@ def main(problem_str="SEND+MORE=MONEY", base=10): # the digits x = [solver.IntVar(0, base - 1, "x[%i]" % i) for i in range(n)] # the sums of each number (e.g. the three numbers SEND, MORE, MONEY) - sums = [solver.IntVar(1, 10**(lens[i]) - 1) for i in range(p_len)] + sums = [solver.IntVar(1, 10 ** (lens[i]) - 1) for i in range(p_len)] # # constraints @@ -91,8 +91,12 @@ def main(problem_str="SEND+MORE=MONEY", base=10): # sum all the digits with proper exponents to a number solver.Add( - sums[ix] == solver.Sum([(base**i) * x[lookup[prob[this_len - i - 1]]] - for i in range(this_len)[::-1]])) + sums[ix] + == solver.Sum([ + (base**i) * x[lookup[prob[this_len - i - 1]]] + for i in range(this_len)[::-1] + ]) + ) # leading digits must be > 0 solver.Add(x[lookup[prob[0]]] > 0) ix += 1 @@ -139,9 +143,13 @@ def main(problem_str="SEND+MORE=MONEY", base=10): def test_problems(base=10): problems = [ - "SEND+MORE=MONEY", "SEND+MOST=MONEY", "VINGT+CINQ+CINQ=TRENTE", - "EIN+EIN+EIN+EIN=VIER", "DONALD+GERALD=ROBERT", - "SATURN+URANUS+NEPTUNE+PLUTO+PLANETS", "WRONG+WRONG=RIGHT" + "SEND+MORE=MONEY", + "SEND+MOST=MONEY", + "VINGT+CINQ+CINQ=TRENTE", + "EIN+EIN+EIN+EIN=VIER", + "DONALD+GERALD=ROBERT", + "SATURN+URANUS+NEPTUNE+PLUTO+PLANETS", + "WRONG+WRONG=RIGHT", ] for p in problems: diff --git a/examples/contrib/assignment.py b/examples/contrib/assignment.py index 7815d9b127c..c9e5b46a547 100644 --- a/examples/contrib/assignment.py +++ b/examples/contrib/assignment.py @@ -13,22 +13,22 @@ # limitations under the License. """ - Assignment problem in Google CP Solver. - - Winston 'Operations Research', Assignment Problems, page 393f - (generalized version with added test column) - - Compare with the following models: - * Comet : http://www.hakank.org/comet/assignment.co - * ECLiPSE : http://www.hakank.org/eclipse/assignment.ecl - * Gecode : http://www.hakank.org/gecode/assignment.cpp - * MiniZinc: http://www.hakank.org/minizinc/assignment.mzn - * Tailor/Essence': http://www.hakank.org/tailor/assignment.eprime - * SICStus: http://hakank.org/sicstus/assignment.pl - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Assignment problem in Google CP Solver. + +Winston 'Operations Research', Assignment Problems, page 393f +(generalized version with added test column) + +Compare with the following models: +* Comet : http://www.hakank.org/comet/assignment.co +* ECLiPSE : http://www.hakank.org/eclipse/assignment.ecl +* Gecode : http://www.hakank.org/gecode/assignment.cpp +* MiniZinc: http://www.hakank.org/minizinc/assignment.mzn +* Tailor/Essence': http://www.hakank.org/tailor/assignment.eprime +* SICStus: http://hakank.org/sicstus/assignment.pl + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -56,20 +56,22 @@ def main(cost, rows, cols): # # total_cost - solver.Add(total_cost == solver.Sum( - [solver.ScalProd(x_row, cost_row) for (x_row, cost_row) in zip(x, cost)])) + solver.Add( + total_cost + == solver.Sum([ + solver.ScalProd(x_row, cost_row) for (x_row, cost_row) in zip(x, cost) + ]) + ) # exacly one assignment per row, all rows must be assigned [ - solver.Add(solver.Sum([x[row][j] - for j in range(cols)]) == 1) + solver.Add(solver.Sum([x[row][j] for j in range(cols)]) == 1) for row in range(rows) ] # zero or one assignments per column [ - solver.Add(solver.Sum([x[i][col] - for i in range(rows)]) <= 1) + solver.Add(solver.Sum([x[i][col] for i in range(rows)]) <= 1) for col in range(cols) ] diff --git a/examples/contrib/assignment6_mip.py b/examples/contrib/assignment6_mip.py index 061e18dd53d..f193d5c7f98 100644 --- a/examples/contrib/assignment6_mip.py +++ b/examples/contrib/assignment6_mip.py @@ -13,31 +13,31 @@ # limitations under the License. """ - Assignment problem using MIP in Google or-tools. +Assignment problem using MIP in Google or-tools. - From GLPK:s example assign.mod: - ''' - The assignment problem is one of the fundamental combinatorial - optimization problems. +From GLPK:s example assign.mod: +''' +The assignment problem is one of the fundamental combinatorial +optimization problems. - In its most general form, the problem is as follows: +In its most general form, the problem is as follows: - There are a number of agents and a number of tasks. Any agent can be - assigned to perform any task, incurring some cost that may vary - depending on the agent-task assignment. It is required to perform all - tasks by assigning exactly one agent to each task in such a way that - the total cost of the assignment is minimized. +There are a number of agents and a number of tasks. Any agent can be +assigned to perform any task, incurring some cost that may vary +depending on the agent-task assignment. It is required to perform all +tasks by assigning exactly one agent to each task in such a way that +the total cost of the assignment is minimized. - (From Wikipedia, the free encyclopedia.) - ''' +(From Wikipedia, the free encyclopedia.) +''' - Compare with the Comet model: - http://www.hakank.org/comet/assignment6.co +Compare with the Comet model: + http://www.hakank.org/comet/assignment6.co - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp @@ -77,10 +77,16 @@ def main(sol='CBC'): # # Optimal solution is 76 # """ - c = [[13, 21, 20, 12, 8, 26, 22, 11], [12, 36, 25, 41, 40, 11, 4, 8], - [35, 32, 13, 36, 26, 21, 13, 37], [34, 54, 7, 8, 12, 22, 11, 40], - [21, 6, 45, 18, 24, 34, 12, 48], [42, 19, 39, 15, 14, 16, 28, 46], - [16, 34, 38, 3, 34, 40, 22, 24], [26, 20, 5, 17, 45, 31, 37, 43]] + c = [ + [13, 21, 20, 12, 8, 26, 22, 11], + [12, 36, 25, 41, 40, 11, 4, 8], + [35, 32, 13, 36, 26, 21, 13, 37], + [34, 54, 7, 8, 12, 22, 11, 40], + [21, 6, 45, 18, 24, 34, 12, 48], + [42, 19, 39, 15, 14, 16, 28, 46], + [16, 34, 38, 3, 34, 40, 22, 24], + [26, 20, 5, 17, 45, 31, 37, 43], + ] # # variables diff --git a/examples/contrib/bacp.py b/examples/contrib/bacp.py index 7698696d907..edf084f9e4b 100644 --- a/examples/contrib/bacp.py +++ b/examples/contrib/bacp.py @@ -18,9 +18,10 @@ parser = argparse.ArgumentParser() parser.add_argument( - '--data', default='examples/contrib/bacp.txt', help='path to data file') + '--data', default='examples/contrib/bacp.txt', help='path to data file' +) -#----------------helper for binpacking posting---------------- +# ----------------helper for binpacking posting---------------- def BinPacking(solver, binvars, weights, loadvars): @@ -34,14 +35,15 @@ def BinPacking(solver, binvars, weights, loadvars): solver.Add(solver.SumEquality(loadvars, sum(weights))) -#------------------------------data reading------------------- +# ------------------------------data reading------------------- def ReadData(filename): """Read data from .""" f = open(filename) - nb_courses, nb_periods, min_credit, max_credit, nb_prereqs =\ - [int(nb) for nb in f.readline().split()] + nb_courses, nb_periods, min_credit, max_credit, nb_prereqs = [ + int(nb) for nb in f.readline().split() + ] credits = [int(nb) for nb in f.readline().split()] prereq = [int(nb) for nb in f.readline().split()] prereq = [(prereq[i * 2], prereq[i * 2 + 1]) for i in range(nb_prereqs)] @@ -49,7 +51,7 @@ def ReadData(filename): def main(args): - #------------------solver and variable declaration------------- + # ------------------solver and variable declaration------------- credits, nb_periods, prereq = ReadData(args.data) nb_courses = len(credits) @@ -64,7 +66,7 @@ def main(args): for i in range(nb_periods) ] - #-------------------post of the constraints-------------- + # -------------------post of the constraints-------------- # Bin Packing. BinPacking(solver, x, credits, load_vars) @@ -72,15 +74,16 @@ def main(args): for i, j in prereq: solver.Add(x[i] < x[j]) - #----------------Objective------------------------------- + # ----------------Objective------------------------------- objective_var = solver.Max(load_vars) objective = solver.Minimize(objective_var, 1) - #------------start the search and optimization----------- + # ------------start the search and optimization----------- - db = solver.Phase(x, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, - solver.INT_VALUE_DEFAULT) + db = solver.Phase( + x, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, solver.INT_VALUE_DEFAULT + ) search_log = solver.SearchLog(100000, objective_var) solver.Solve(db, [objective, search_log]) diff --git a/examples/contrib/blending.py b/examples/contrib/blending.py index 6334ad9c6df..225bbdd2875 100644 --- a/examples/contrib/blending.py +++ b/examples/contrib/blending.py @@ -13,13 +13,13 @@ # limitations under the License. """ - Blending problem in Google or-tools. +Blending problem in Google or-tools. - From the OPL model blending.mod. +From the OPL model blending.mod. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp @@ -79,16 +79,22 @@ def main(sol='CBC'): # constraints # - solver.Add(z == solver.Sum([CostMetal[i] * p[i] for i in Metals]) + - solver.Sum([CostRaw[i] * r[i] for i in Raws]) + - solver.Sum([CostScrap[i] * s[i] for i in Scraps]) + - solver.Sum([CostIngo[i] * ii[i] for i in Ingos])) + solver.Add( + z + == solver.Sum([CostMetal[i] * p[i] for i in Metals]) + + solver.Sum([CostRaw[i] * r[i] for i in Raws]) + + solver.Sum([CostScrap[i] * s[i] for i in Scraps]) + + solver.Sum([CostIngo[i] * ii[i] for i in Ingos]) + ) for j in Metals: solver.Add( - metal[j] == p[j] + solver.Sum([PercRaw[j][k] * r[k] for k in Raws]) + - solver.Sum([PercScrap[j][k] * s[k] for k in Scraps]) + - solver.Sum([PercIngo[j][k] * ii[k] for k in Ingos])) + metal[j] + == p[j] + + solver.Sum([PercRaw[j][k] * r[k] for k in Raws]) + + solver.Sum([PercScrap[j][k] * s[k] for k in Scraps]) + + solver.Sum([PercIngo[j][k] * ii[k] for k in Ingos]) + ) solver.Add(solver.Sum(metal) == Alloy) diff --git a/examples/contrib/broken_weights.py b/examples/contrib/broken_weights.py index 961750b6ad7..af6319389a9 100644 --- a/examples/contrib/broken_weights.py +++ b/examples/contrib/broken_weights.py @@ -13,38 +13,38 @@ # limitations under the License. """ - Broken weights problem in Google CP Solver. - - From http://www.mathlesstraveled.com/?p=701 - ''' - Here's a fantastic problem I recently heard. Apparently it was first - posed by Claude Gaspard Bachet de Meziriac in a book of arithmetic problems - published in 1612, and can also be found in Heinrich Dorrie's 100 - Great Problems of Elementary Mathematics. - - A merchant had a forty pound measuring weight that broke - into four pieces as the result of a fall. When the pieces were - subsequently weighed, it was found that the weight of each piece - was a whole number of pounds and that the four pieces could be - used to weigh every integral weight between 1 and 40 pounds. What - were the weights of the pieces? - - Note that since this was a 17th-century merchant, he of course used a - balance scale to weigh things. So, for example, he could use a 1-pound - weight and a 4-pound weight to weigh a 3-pound object, by placing the - 3-pound object and 1-pound weight on one side of the scale, and - the 4-pound weight on the other side. - ''' - - Compare with the following problems: - * MiniZinc: http://www.hakank.org/minizinc/broken_weights.mzn - * ECLiPSE: http://www.hakank.org/eclipse/broken_weights.ecl - * Gecode: http://www.hakank.org/gecode/broken_weights.cpp - * Comet: http://hakank.org/comet/broken_weights.co - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Broken weights problem in Google CP Solver. + +From http://www.mathlesstraveled.com/?p=701 +''' +Here's a fantastic problem I recently heard. Apparently it was first +posed by Claude Gaspard Bachet de Meziriac in a book of arithmetic problems +published in 1612, and can also be found in Heinrich Dorrie's 100 +Great Problems of Elementary Mathematics. + + A merchant had a forty pound measuring weight that broke + into four pieces as the result of a fall. When the pieces were + subsequently weighed, it was found that the weight of each piece + was a whole number of pounds and that the four pieces could be + used to weigh every integral weight between 1 and 40 pounds. What + were the weights of the pieces? + +Note that since this was a 17th-century merchant, he of course used a +balance scale to weigh things. So, for example, he could use a 1-pound +weight and a 4-pound weight to weigh a 3-pound object, by placing the +3-pound object and 1-pound weight on one side of the scale, and +the 4-pound weight on the other side. +''' + +Compare with the following problems: +* MiniZinc: http://www.hakank.org/minizinc/broken_weights.mzn +* ECLiPSE: http://www.hakank.org/eclipse/broken_weights.ecl +* Gecode: http://www.hakank.org/gecode/broken_weights.cpp +* Comet: http://hakank.org/comet/broken_weights.co + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -99,8 +99,9 @@ def main(m=40, n=4): # # search and result # - db = solver.Phase(weights + x_flat, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + weights + x_flat, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) search_log = solver.SearchLog(1) diff --git a/examples/contrib/bus_schedule.py b/examples/contrib/bus_schedule.py index d4be72f6a75..e25692058f5 100644 --- a/examples/contrib/bus_schedule.py +++ b/examples/contrib/bus_schedule.py @@ -13,24 +13,24 @@ # limitations under the License. """ - Bus scheduling in Google CP Solver. +Bus scheduling in Google CP Solver. - Problem from Taha "Introduction to Operations Research", page 58. +Problem from Taha "Introduction to Operations Research", page 58. - This is a slightly more general model than Taha's. +This is a slightly more general model than Taha's. - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/bus_scheduling.mzn - * Comet : http://www.hakank.org/comet/bus_schedule.co - * ECLiPSe : http://www.hakank.org/eclipse/bus_schedule.ecl - * Gecode : http://www.hakank.org/gecode/bus_schedule.cpp - * Tailor/Essence' : http://www.hakank.org/tailor/bus_schedule.eprime - * SICStus: http://hakank.org/sicstus/bus_schedule.pl +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/bus_scheduling.mzn +* Comet : http://www.hakank.org/comet/bus_schedule.co +* ECLiPSe : http://www.hakank.org/eclipse/bus_schedule.ecl +* Gecode : http://www.hakank.org/gecode/bus_schedule.cpp +* Tailor/Essence' : http://www.hakank.org/tailor/bus_schedule.eprime +* SICStus: http://hakank.org/sicstus/bus_schedule.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -83,7 +83,8 @@ def main(num_buses_check=0): solver.Solve( solver.Phase(x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE), - cargs) + cargs, + ) num_solutions = collector.SolutionCount() num_buses_check_value = 0 diff --git a/examples/contrib/car.py b/examples/contrib/car.py index 2a729854e59..1436a5c25f5 100644 --- a/examples/contrib/car.py +++ b/examples/contrib/car.py @@ -13,20 +13,20 @@ # limitations under the License. """ - Car sequencing in Google CP Solver. +Car sequencing in Google CP Solver. - This model is based on the car sequencing model in - Pascal Van Hentenryck - 'The OPL Optimization Programming Language', page 184ff. +This model is based on the car sequencing model in +Pascal Van Hentenryck +'The OPL Optimization Programming Language', page 184ff. - Compare with the following models: - * MiniZinc: http://hakank.org/minizinc/car.mzn - * Comet: http://hakank.org/comet/car.co +Compare with the following models: +* MiniZinc: http://hakank.org/minizinc/car.mzn +* Comet: http://hakank.org/comet/car.co - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -58,7 +58,7 @@ def main(num_sol=3): [0, 0, 1, 1, 0, 1], # option 2 [1, 0, 0, 0, 1, 0], # option 3 [1, 1, 0, 1, 0, 0], # option 4 - [0, 0, 1, 0, 0, 0] # option 5 + [0, 0, 1, 0, 0, 0], # option 5 ] capacity = [(1, 2), (2, 3), (1, 3), (2, 5), (1, 5)] @@ -104,8 +104,9 @@ def main(num_sol=3): # # search and result # - db = solver.Phase(slot + setup_flat, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + slot + setup_flat, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/contrib/check_dependencies.py b/examples/contrib/check_dependencies.py index 258f9db7c92..8ddd020cd8f 100644 --- a/examples/contrib/check_dependencies.py +++ b/examples/contrib/check_dependencies.py @@ -8,29 +8,41 @@ def log_error_and_exit(error_message): raise SystemExit -#try to import setuptools +# try to import setuptools try: from setuptools import setup, Extension from setuptools.command import easy_install except ImportError: - log_error_and_exit("""setuptools is not installed for \"""" + sys.executable + - """\" + log_error_and_exit( + """setuptools is not installed for \"""" + sys.executable + """\" Follow this link for installing instructions : https://pypi.python.org/pypi/setuptools -make sure you use \"""" + sys.executable + """\" during the installation""") +make sure you use \"""" + sys.executable + """\" during the installation""" + ) from pkg_resources import parse_version def notinstalled(modulename): - return modulename + """ could not be imported for \"""" + sys.executable + """\" + return ( + modulename + + """ could not be imported for \"""" + + sys.executable + + """\" Set PYTHONPATH to the output of this command \"make print-OR_TOOLS_PYTHONPATH\" before running the examples""" + ) def wrong_module(module_file, modulename): - return """ -The python examples are not importing the """ + modulename + """ module from the sources. -Remove the site-package that contains \"""" + module_file + """\", either manually or by using pip, and rerun this script again.""" + return ( + """ +The python examples are not importing the """ + + modulename + + """ module from the sources. +Remove the site-package that contains \"""" + + module_file + + """\", either manually or by using pip, and rerun this script again.""" + ) # Returns the n_th parent of file @@ -47,9 +59,12 @@ def n_dirname(n, file): "-l", "--log", type="string", - help= - "Available levels are CRITICAL (3), ERROR (2), WARNING (1), INFO (0), DEBUG (-1)", - default="INFO") + help=( + "Available levels are CRITICAL (3), ERROR (2), WARNING (1), INFO (0)," + " DEBUG (-1)" + ), + default="INFO", + ) options, args = parser.parse_args() try: @@ -64,25 +79,27 @@ def n_dirname(n, file): }[int(options.log)] logging.basicConfig( - format="[%(levelname)s] %(message)s", stream=sys.stdout, level=loglevel) + format="[%(levelname)s] %(message)s", stream=sys.stdout, level=loglevel + ) logging.info("Python path : " + sys.executable) logging.info("Python version : " + sys.version) logging.info("sys.path : " + str(sys.path)) ortools_project_path = n_dirname( - 3, abspath(inspect.getfile(inspect.currentframe()))) + 3, abspath(inspect.getfile(inspect.currentframe())) + ) - #try to import ortools + # try to import ortools try: import ortools except ImportError: logging.error(notinstalled("ortools")) raise SystemExit - #check if we're using ortools from the sources or it's binded by pypi's module + # check if we're using ortools from the sources or it's binded by pypi's module ortools_module_file = inspect.getfile(ortools) ortools_module_path = n_dirname(3, ortools_module_file) - if (ortools_module_path == ortools_project_path): + if ortools_module_path == ortools_project_path: logging.info("Or-tools is imported from : " + ortools_module_file) else: log_error_and_exit(wrong_module(ortools_module_file, "ortools")) @@ -95,20 +112,20 @@ def n_dirname(n, file): from ortools.algorithms import _pywrapknapsack_solver from ortools.graph import _pywrapgraph - #try to import protobuf + # try to import protobuf try: import google.protobuf except ImportError: log_error_and_exit(notinstalled("protobuf")) - #check if we're using protobuf from the sources or it's binded by pypi's module + # check if we're using protobuf from the sources or it's binded by pypi's module protobuf_module_file = inspect.getfile(google.protobuf) protobuf_module_path = n_dirname(7, protobuf_module_file) - if (protobuf_module_path == ortools_project_path): + if protobuf_module_path == ortools_project_path: logging.info("Protobuf is imported from : " + protobuf_module_file) else: log_error_and_exit(wrong_module(protobuf_module_file, "protobuf")) - #Check if the protobuf modules were successfully generated + # Check if the protobuf modules were successfully generated from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pb2 diff --git a/examples/contrib/circuit.py b/examples/contrib/circuit.py index d1680b770e7..df1fa49f93d 100644 --- a/examples/contrib/circuit.py +++ b/examples/contrib/circuit.py @@ -13,31 +13,31 @@ # limitations under the License. """ - Decomposition of the circuit constraint in Google CP Solver. +Decomposition of the circuit constraint in Google CP Solver. - Cf Global constraint catalog: - http://www.emn.fr/x-info/sdemasse/gccat/Ccircuit.html +Cf Global constraint catalog: +http://www.emn.fr/x-info/sdemasse/gccat/Ccircuit.html - Solution of n=4: - x: [2, 0, 3, 1] - x: [3, 0, 1, 2] - x: [1, 3, 0, 2] - x: [3, 2, 0, 1] - x: [1, 2, 3, 0] - x: [2, 3, 1, 0] +Solution of n=4: +x: [2, 0, 3, 1] +x: [3, 0, 1, 2] +x: [1, 3, 0, 2] +x: [3, 2, 0, 1] +x: [1, 2, 3, 0] +x: [2, 3, 1, 0] - The 'orbit' method that is used here is based on some - observations on permutation orbits. +The 'orbit' method that is used here is based on some +observations on permutation orbits. - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/circuit_test.mzn - * Gecode: http://www.hakank.org/gecode/circuit_orbit.mzn +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/circuit_test.mzn +* Gecode: http://www.hakank.org/gecode/circuit_orbit.mzn - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ @@ -109,7 +109,8 @@ def main(n=5): solver.Solve( solver.Phase(x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE), - [collector]) + [collector], + ) num_solutions = collector.SolutionCount() for s in range(num_solutions): diff --git a/examples/contrib/coins3.py b/examples/contrib/coins3.py index 70957d1ca2d..7584d88627c 100644 --- a/examples/contrib/coins3.py +++ b/examples/contrib/coins3.py @@ -13,28 +13,28 @@ # limitations under the License. """ - Coin application in Google CP Solver. +Coin application in Google CP Solver. - From 'Constraint Logic Programming using ECLiPSe' - pages 99f and 234 ff. - The solution in ECLiPSe is at page 236. +From 'Constraint Logic Programming using ECLiPSe' +pages 99f and 234 ff. +The solution in ECLiPSe is at page 236. - ''' - What is the minimum number of coins that allows one to pay _exactly_ - any amount smaller than one Euro? Recall that there are six different - euro cents, of denomination 1, 2, 5, 10, 20, 50 - ''' +''' +What is the minimum number of coins that allows one to pay _exactly_ +any amount smaller than one Euro? Recall that there are six different +euro cents, of denomination 1, 2, 5, 10, 20, 50 +''' - Compare with the following models: - * MiniZinc: http://hakank.org/minizinc/coins3.mzn - * Comet : http://www.hakank.org/comet/coins3.co - * Gecode : http://hakank.org/gecode/coins3.cpp - * SICStus : http://hakank.org/sicstus/coins3.pl +Compare with the following models: +* MiniZinc: http://hakank.org/minizinc/coins3.mzn +* Comet : http://www.hakank.org/comet/coins3.co +* Gecode : http://hakank.org/gecode/coins3.cpp +* SICStus : http://hakank.org/sicstus/coins3.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ @@ -80,8 +80,9 @@ def main(): solution.Add(num_coins) solution.AddObjective(num_coins) - db = solver.Phase(x, solver.CHOOSE_MIN_SIZE_LOWEST_MAX, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + x, solver.CHOOSE_MIN_SIZE_LOWEST_MAX, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db, [objective]) num_solutions = 0 diff --git a/examples/contrib/coins_grid.py b/examples/contrib/coins_grid.py index 115ac6f80be..5a3af6cf573 100644 --- a/examples/contrib/coins_grid.py +++ b/examples/contrib/coins_grid.py @@ -12,43 +12,43 @@ # See the License for the specific language governing permissions and # limitations under the License. """ - Coins grid problem in Google CP Solver. - - Problem from - Tony Hurlimann: "A coin puzzle - SVOR-contest 2007" - http://www.svor.ch/competitions/competition2007/AsroContestSolution.pdf - ''' - In a quadratic grid (or a larger chessboard) with 31x31 cells, one should - place coins in such a way that the following conditions are fulfilled: - 1. In each row exactly 14 coins must be placed. - 2. In each column exactly 14 coins must be placed. - 3. The sum of the quadratic horizontal distance from the main diagonal - of all cells containing a coin must be as small as possible. - 4. In each cell at most one coin can be placed. - The description says to place 14x31 = 434 coins on the chessboard each row - containing 14 coins and each column also containing 14 coins. - ''' - - Cf the LPL model: - http://diuflx71.unifr.ch/lpl/GetModel?name=/puzzles/coin - - Note: Laurent Perron helped me to improve this model. - - Compare with the following models: - * Tailor/Essence': http://hakank.org/tailor/coins_grid.eprime - * MiniZinc: http://hakank.org/minizinc/coins_grid.mzn - * SICStus: http://hakank.org/sicstus/coins_grid.pl - * Zinc: http://hakank.org/minizinc/coins_grid.zinc - * Choco: http://hakank.org/choco/CoinsGrid.java - * Comet: http://hakank.org/comet/coins_grid.co - * ECLiPSe: http://hakank.org/eclipse/coins_grid.ecl - * Gecode: http://hakank.org/gecode/coins_grid.cpp - * Gecode/R: http://hakank.org/gecode_r/coins_grid.rb - * JaCoP: http://hakank.org/JaCoP/CoinsGrid.java - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Coins grid problem in Google CP Solver. + +Problem from +Tony Hurlimann: "A coin puzzle - SVOR-contest 2007" +http://www.svor.ch/competitions/competition2007/AsroContestSolution.pdf +''' +In a quadratic grid (or a larger chessboard) with 31x31 cells, one should +place coins in such a way that the following conditions are fulfilled: + 1. In each row exactly 14 coins must be placed. + 2. In each column exactly 14 coins must be placed. + 3. The sum of the quadratic horizontal distance from the main diagonal + of all cells containing a coin must be as small as possible. + 4. In each cell at most one coin can be placed. +The description says to place 14x31 = 434 coins on the chessboard each row +containing 14 coins and each column also containing 14 coins. +''' + +Cf the LPL model: +http://diuflx71.unifr.ch/lpl/GetModel?name=/puzzles/coin + +Note: Laurent Perron helped me to improve this model. + +Compare with the following models: +* Tailor/Essence': http://hakank.org/tailor/coins_grid.eprime +* MiniZinc: http://hakank.org/minizinc/coins_grid.mzn +* SICStus: http://hakank.org/sicstus/coins_grid.pl +* Zinc: http://hakank.org/minizinc/coins_grid.zinc +* Choco: http://hakank.org/choco/CoinsGrid.java +* Comet: http://hakank.org/comet/coins_grid.co +* ECLiPSe: http://hakank.org/eclipse/coins_grid.ecl +* Gecode: http://hakank.org/gecode/coins_grid.cpp +* Gecode/R: http://hakank.org/gecode_r/coins_grid.rb +* JaCoP: http://hakank.org/JaCoP/CoinsGrid.java + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -80,7 +80,8 @@ def main(n, c): # quadratic horizonal distance var objective_var = solver.Sum( - [x[(i, j)] * (i - j) * (i - j) for i in range(n) for j in range(n)]) + [x[(i, j)] * (i - j) * (i - j) for i in range(n) for j in range(n)] + ) # objective objective = solver.Minimize(objective_var, 1) @@ -97,9 +98,13 @@ def main(n, c): search_log = solver.SearchLog(1000000, objective_var) restart = solver.ConstantRestart(300) solver.Solve( - solver.Phase([x[(i, j)] for i in range(n) for j in range(n)], - solver.CHOOSE_RANDOM, solver.ASSIGN_MAX_VALUE), - [collector, search_log, objective]) + solver.Phase( + [x[(i, j)] for i in range(n) for j in range(n)], + solver.CHOOSE_RANDOM, + solver.ASSIGN_MAX_VALUE, + ), + [collector, search_log, objective], + ) print("objective:", collector.ObjectiveValue(0)) for i in range(n): diff --git a/examples/contrib/coins_grid_mip.py b/examples/contrib/coins_grid_mip.py index afa35e81c40..9eb2d4c3aff 100644 --- a/examples/contrib/coins_grid_mip.py +++ b/examples/contrib/coins_grid_mip.py @@ -13,31 +13,31 @@ # limitations under the License. """ - Coins grid problem in Google CP Solver. - - - Problem from - Tony Hurlimann: "A coin puzzle - SVOR-contest 2007" - http://www.svor.ch/competitions/competition2007/AsroContestSolution.pdf - ''' - In a quadratic grid (or a larger chessboard) with 31x31 cells, one should - place coins in such a way that the following conditions are fulfilled: - 1. In each row exactly 14 coins must be placed. - 2. In each column exactly 14 coins must be placed. - 3. The sum of the quadratic horizontal distance from the main diagonal - of all cells containing a coin must be as small as possible. - 4. In each cell at most one coin can be placed. - The description says to place 14x31 = 434 coins on the chessboard each row - containing 14 coins and each column also containing 14 coins. - ''' - - This is a MIP version of - http://www.hakank.org/google_or_tools/coins_grid.py - and use - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Coins grid problem in Google CP Solver. + + +Problem from +Tony Hurlimann: "A coin puzzle - SVOR-contest 2007" +http://www.svor.ch/competitions/competition2007/AsroContestSolution.pdf +''' +In a quadratic grid (or a larger chessboard) with 31x31 cells, one should +place coins in such a way that the following conditions are fulfilled: + 1. In each row exactly 14 coins must be placed. + 2. In each column exactly 14 coins must be placed. + 3. The sum of the quadratic horizontal distance from the main diagonal + of all cells containing a coin must be as small as possible. + 4. In each cell at most one coin can be placed. +The description says to place 14x31 = 434 coins on the chessboard each row +containing 14 coins and each column also containing 14 coins. +''' + +This is a MIP version of + http://www.hakank.org/google_or_tools/coins_grid.py +and use + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.linear_solver import pywraplp @@ -72,7 +72,8 @@ def main(unused_argv): # quadratic horizonal distance var objective_var = solver.Sum( - [x[(i, j)] * (i - j) * (i - j) for i in range(n) for j in range(n)]) + [x[(i, j)] * (i - j) * (i - j) for i in range(n) for j in range(n)] + ) # objective objective = solver.Minimize(objective_var) diff --git a/examples/contrib/coloring_ip.py b/examples/contrib/coloring_ip.py index 1aeb3243197..08f7dc2e08b 100644 --- a/examples/contrib/coloring_ip.py +++ b/examples/contrib/coloring_ip.py @@ -13,28 +13,28 @@ # limitations under the License. """ - Simple coloring problem using MIP in Google CP Solver. +Simple coloring problem using MIP in Google CP Solver. - Inspired by the GLPK:s model color.mod - ''' - COLOR, Graph Coloring Problem +Inspired by the GLPK:s model color.mod +''' +COLOR, Graph Coloring Problem - Written in GNU MathProg by Andrew Makhorin +Written in GNU MathProg by Andrew Makhorin - Given an undirected loopless graph G = (V, E), where V is a set of - nodes, E <= V x V is a set of arcs, the Graph Coloring Problem is to - find a mapping (coloring) F: V -> C, where C = {1, 2, ... } is a set - of colors whose cardinality is as small as possible, such that - F(i) != F(j) for every arc (i,j) in E, that is adjacent nodes must - be assigned different colors. - ''' +Given an undirected loopless graph G = (V, E), where V is a set of +nodes, E <= V x V is a set of arcs, the Graph Coloring Problem is to +find a mapping (coloring) F: V -> C, where C = {1, 2, ... } is a set +of colors whose cardinality is as small as possible, such that +F(i) != F(j) for every arc (i,j) in E, that is adjacent nodes must +be assigned different colors. +''' - Compare with the MiniZinc model: - http://www.hakank.org/minizinc/coloring_ip.mzn +Compare with the MiniZinc model: + http://www.hakank.org/minizinc/coloring_ip.mzn - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp @@ -70,9 +70,28 @@ def main(sol='CBC'): # http://mat.gsia.cmu.edu/COLOR/instances.html # # Note: 1-based (adjusted below) - E = [[1, 2], [1, 4], [1, 7], [1, 9], [2, 3], [2, 6], [2, 8], [3, 5], [3, 7], - [3, 10], [4, 5], [4, 6], [4, 10], [5, 8], [5, 9], [6, 11], [7, 11], - [8, 11], [9, 11], [10, 11]] + E = [ + [1, 2], + [1, 4], + [1, 7], + [1, 9], + [2, 3], + [2, 6], + [2, 8], + [3, 5], + [3, 7], + [3, 10], + [4, 5], + [4, 6], + [4, 10], + [5, 8], + [5, 9], + [6, 11], + [7, 11], + [8, 11], + [9, 11], + [10, 11], + ] # # declare variables diff --git a/examples/contrib/combinatorial_auction2.py b/examples/contrib/combinatorial_auction2.py index 7a9a1a88a2d..a6d82eda56d 100644 --- a/examples/contrib/combinatorial_auction2.py +++ b/examples/contrib/combinatorial_auction2.py @@ -13,20 +13,20 @@ # limitations under the License. """Combinatorial auction in Google CP Solver. - This is a more general model for the combinatorial example - in the Numberjack Tutorial, pages 9 and 24 (slides 19/175 and - 51/175). +This is a more general model for the combinatorial example +in the Numberjack Tutorial, pages 9 and 24 (slides 19/175 and +51/175). - The original and more talkative model is here: - http://www.hakank.org/numberjack/combinatorial_auction.py +The original and more talkative model is here: +http://www.hakank.org/numberjack/combinatorial_auction.py - Compare with the following models: - * MiniZinc: http://hakank.org/minizinc/combinatorial_auction.mzn - * Gecode: http://hakank.org/gecode/combinatorial_auction.cpp +Compare with the following models: +* MiniZinc: http://hakank.org/minizinc/combinatorial_auction.mzn +* Gecode: http://hakank.org/gecode/combinatorial_auction.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from collections import * @@ -49,7 +49,7 @@ def main(): [0, 2], # A, C [1, 3], # B,D [1, 2, 3], # B,C,D - [0] # A + [0], # A ] # collect the bids for each item items_t = defaultdict(list) diff --git a/examples/contrib/contiguity_regular.py b/examples/contrib/contiguity_regular.py index 91700f20382..034850fcb8e 100644 --- a/examples/contrib/contiguity_regular.py +++ b/examples/contrib/contiguity_regular.py @@ -13,30 +13,30 @@ # limitations under the License. """ - Global constraint contiguity using regularin Google CP Solver. +Global constraint contiguity using regularin Google CP Solver. - This is a decomposition of the global constraint - global contiguity. +This is a decomposition of the global constraint +global contiguity. - From Global Constraint Catalogue - http://www.emn.fr/x-info/sdemasse/gccat/Cglobal_contiguity.html - ''' - Enforce all variables of the VARIABLES collection to be assigned to 0 or 1. - In addition, all variables assigned to value 1 appear contiguously. +From Global Constraint Catalogue +http://www.emn.fr/x-info/sdemasse/gccat/Cglobal_contiguity.html +''' +Enforce all variables of the VARIABLES collection to be assigned to 0 or 1. +In addition, all variables assigned to value 1 appear contiguously. - Example: - (<0, 1, 1, 0>) +Example: +(<0, 1, 1, 0>) - The global_contiguity constraint holds since the sequence 0 1 1 0 contains - no more than one group of contiguous 1. - ''' +The global_contiguity constraint holds since the sequence 0 1 1 0 contains +no more than one group of contiguous 1. +''' - Compare with the following model: - * MiniZinc: http://www.hakank.org/minizinc/contiguity_regular.mzn +Compare with the following model: +* MiniZinc: http://www.hakank.org/minizinc/contiguity_regular.mzn - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -108,7 +108,8 @@ def regular(x, Q, S, d, q0, F): # Determine a[i+1]: a[i+1] == d2[a[i], x[i]] solver.Add( - a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1))) + a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1)) + ) def main(): @@ -146,14 +147,21 @@ def main(): # # constraints # - regular(reg_input, n_states, input_max, transition_fn, initial_state, - accepting_states) + regular( + reg_input, + n_states, + input_max, + transition_fn, + initial_state, + accepting_states, + ) # # solution and search # - db = solver.Phase(reg_input, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + reg_input, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) diff --git a/examples/contrib/costas_array.py b/examples/contrib/costas_array.py index b6a2d38380e..455f9dd3e54 100644 --- a/examples/contrib/costas_array.py +++ b/examples/contrib/costas_array.py @@ -13,51 +13,51 @@ # limitations under the License. """ - Costas array in Google CP Solver. - - From http://mathworld.wolfram.com/CostasArray.html: - ''' - An order-n Costas array is a permutation on {1,...,n} such - that the distances in each row of the triangular difference - table are distinct. For example, the permutation {1,3,4,2,5} - has triangular difference table {2,1,-2,3}, {3,-1,1}, {1,2}, - and {4}. Since each row contains no duplications, the permutation - is therefore a Costas array. - ''' - - Also see - http://en.wikipedia.org/wiki/Costas_array - - About this model: - This model is based on Barry O'Sullivan's model: - http://www.g12.cs.mu.oz.au/mzn/costas_array/CostasArray.mzn - - and my small changes in - http://hakank.org/minizinc/costas_array.mzn - - Since there is no symmetry breaking of the order of the Costas - array it gives all the solutions for a specific length of - the array, e.g. those listed in - http://mathworld.wolfram.com/CostasArray.html - - 1 1 (1) - 2 2 (1, 2), (2,1) - 3 4 (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2) - 4 12 (1, 2, 4, 3), (1, 3, 4, 2), (1, 4, 2, 3), (2, 1, 3, 4), - (2, 3, 1, 4), (2, 4, 3, 1), (3, 1, 2, 4), (3, 2, 4, 1), - (3, 4, 2, 1), (4, 1, 3, 2), (4, 2, 1, 3), (4, 3, 1, 2) - .... - - See http://www.research.att.com/~njas/sequences/A008404 - for the number of solutions for n=1.. - 1, 2, 4, 12, 40, 116, 200, 444, 760, 2160, 4368, 7852, 12828, - 17252, 19612, 21104, 18276, 15096, 10240, 6464, 3536, 2052, - 872, 200, 88, 56, 204,... - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Costas array in Google CP Solver. + +From http://mathworld.wolfram.com/CostasArray.html: +''' +An order-n Costas array is a permutation on {1,...,n} such +that the distances in each row of the triangular difference +table are distinct. For example, the permutation {1,3,4,2,5} +has triangular difference table {2,1,-2,3}, {3,-1,1}, {1,2}, +and {4}. Since each row contains no duplications, the permutation +is therefore a Costas array. +''' + +Also see +http://en.wikipedia.org/wiki/Costas_array + +About this model: +This model is based on Barry O'Sullivan's model: +http://www.g12.cs.mu.oz.au/mzn/costas_array/CostasArray.mzn + +and my small changes in +http://hakank.org/minizinc/costas_array.mzn + +Since there is no symmetry breaking of the order of the Costas +array it gives all the solutions for a specific length of +the array, e.g. those listed in +http://mathworld.wolfram.com/CostasArray.html + +1 1 (1) +2 2 (1, 2), (2,1) +3 4 (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2) +4 12 (1, 2, 4, 3), (1, 3, 4, 2), (1, 4, 2, 3), (2, 1, 3, 4), + (2, 3, 1, 4), (2, 4, 3, 1), (3, 1, 2, 4), (3, 2, 4, 1), + (3, 4, 2, 1), (4, 1, 3, 2), (4, 2, 1, 3), (4, 3, 1, 2) +.... + +See http://www.research.att.com/~njas/sequences/A008404 +for the number of solutions for n=1.. +1, 2, 4, 12, 40, 116, 200, 444, 760, 2160, 4368, 7852, 12828, +17252, 19612, 21104, 18276, 15096, 10240, 6464, 3536, 2052, +872, 200, 88, 56, 204,... + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -81,8 +81,9 @@ def main(n=6): differences = {} for i in range(n): for j in range(n): - differences[(i, j)] = solver.IntVar(-n + 1, n - 1, - "differences[%i,%i]" % (i, j)) + differences[(i, j)] = solver.IntVar( + -n + 1, n - 1, "differences[%i,%i]" % (i, j) + ) differences_flat = [differences[i, j] for i in range(n) for j in range(n)] # @@ -112,7 +113,8 @@ def main(n=6): # triangle must be distint." for i in range(n - 2): solver.Add( - solver.AllDifferent([differences[i, j] for j in range(n) if j > i])) + solver.AllDifferent([differences[i, j] for j in range(n) if j > i]) + ) # # "All the following are redundant - only here to speed up search." @@ -127,14 +129,19 @@ def main(n=6): for k in range(2, n): for l in range(2, n): if k < l: - solver.Add(differences[k - 2, l - 1] + differences[k, l] == - differences[k - 1, l - 1] + differences[k - 1, l]) + solver.Add( + differences[k - 2, l - 1] + differences[k, l] + == differences[k - 1, l - 1] + differences[k - 1, l] + ) # # search and result # - db = solver.Phase(costas + differences_flat, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + costas + differences_flat, + solver.CHOOSE_FIRST_UNBOUND, + solver.ASSIGN_MIN_VALUE, + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/contrib/covering_opl.py b/examples/contrib/covering_opl.py index bcdf574c0fa..9a7c164038a 100644 --- a/examples/contrib/covering_opl.py +++ b/examples/contrib/covering_opl.py @@ -13,47 +13,47 @@ # limitations under the License. """ - Set covering problem in Google CP Solver. - - This example is from the OPL example covering.mod - ''' - Consider selecting workers to build a house. The construction of a - house can be divided into a number of tasks, each requiring a number of - skills (e.g., plumbing or masonry). A worker may or may not perform a - task, depending on skills. In addition, each worker can be hired for a - cost that also depends on his qualifications. The problem consists of - selecting a set of workers to perform all the tasks, while minimizing the - cost. This is known as a set-covering problem. The key idea in modeling - a set-covering problem as an integer program is to associate a 0/1 - variable with each worker to represent whether the worker is hired. - To make sure that all the tasks are performed, it is sufficient to - choose at least one worker by task. This constraint can be expressed by a - simple linear inequality. - ''' - - Solution from the OPL model (1-based) - ''' - Optimal solution found with objective: 14 - crew= {23 25 26} - ''' - - Solution from this model (0-based): - ''' - Total cost 14 - We should hire these workers: 22 24 25 - ''' - - - Compare with the following models: - * Comet: http://hakank.org/comet/covering_opl.co - * MiniZinc: http://hakank.org/minizinc/covering_opl.mzn - * ECLiPSe: http://hakank.org/eclipse/covering_opl.ecl - * Gecode: http://hakank.org/gecode/covering_opl.cpp - * SICStus: http://hakank.org/sicstus/covering_opl.pl - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Set covering problem in Google CP Solver. + +This example is from the OPL example covering.mod +''' +Consider selecting workers to build a house. The construction of a +house can be divided into a number of tasks, each requiring a number of +skills (e.g., plumbing or masonry). A worker may or may not perform a +task, depending on skills. In addition, each worker can be hired for a +cost that also depends on his qualifications. The problem consists of +selecting a set of workers to perform all the tasks, while minimizing the +cost. This is known as a set-covering problem. The key idea in modeling +a set-covering problem as an integer program is to associate a 0/1 +variable with each worker to represent whether the worker is hired. +To make sure that all the tasks are performed, it is sufficient to +choose at least one worker by task. This constraint can be expressed by a +simple linear inequality. +''' + +Solution from the OPL model (1-based) +''' +Optimal solution found with objective: 14 +crew= {23 25 26} +''' + +Solution from this model (0-based): +''' +Total cost 14 +We should hire these workers: 22 24 25 +''' + + +Compare with the following models: +* Comet: http://hakank.org/comet/covering_opl.co +* MiniZinc: http://hakank.org/minizinc/covering_opl.mzn +* ECLiPSe: http://hakank.org/eclipse/covering_opl.ecl +* Gecode: http://hakank.org/gecode/covering_opl.cpp +* SICStus: http://hakank.org/sicstus/covering_opl.pl + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -75,19 +75,57 @@ def main(): # Which worker is qualified for each task. # Note: This is 1-based and will be made 0-base below. - Qualified = [[1, 9, 19, 22, 25, 28, 31], - [2, 12, 15, 19, 21, 23, 27, 29, 30, 31, 32], - [3, 10, 19, 24, 26, 30, 32], [4, 21, 25, 28, 32], - [5, 11, 16, 22, 23, 27, 31], [6, 20, 24, 26, 30, 32], - [7, 12, 17, 25, 30, 31], [8, 17, 20, 22, 23], - [9, 13, 14, 26, 29, 30, 31], [10, 21, 25, 31, 32], - [14, 15, 18, 23, 24, 27, 30, 32], [18, 19, 22, 24, 26, 29, 31], - [11, 20, 25, 28, 30, 32], [16, 19, 23, 31], - [9, 18, 26, 28, 31, 32]] + Qualified = [ + [1, 9, 19, 22, 25, 28, 31], + [2, 12, 15, 19, 21, 23, 27, 29, 30, 31, 32], + [3, 10, 19, 24, 26, 30, 32], + [4, 21, 25, 28, 32], + [5, 11, 16, 22, 23, 27, 31], + [6, 20, 24, 26, 30, 32], + [7, 12, 17, 25, 30, 31], + [8, 17, 20, 22, 23], + [9, 13, 14, 26, 29, 30, 31], + [10, 21, 25, 31, 32], + [14, 15, 18, 23, 24, 27, 30, 32], + [18, 19, 22, 24, 26, 29, 31], + [11, 20, 25, 28, 30, 32], + [16, 19, 23, 31], + [9, 18, 26, 28, 31, 32], + ] Cost = [ - 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, - 5, 6, 6, 6, 7, 8, 9 + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 6, + 6, + 6, + 7, + 8, + 9, ] # diff --git a/examples/contrib/crew.py b/examples/contrib/crew.py index 7382ce36888..0ff057d3191 100644 --- a/examples/contrib/crew.py +++ b/examples/contrib/crew.py @@ -13,29 +13,29 @@ # limitations under the License. """ - Crew allocation problem in Google CP Solver. - - From Gecode example crew - examples/crew.cc - ''' - * Example: Airline crew allocation - * - * Assign 20 flight attendants to 10 flights. Each flight needs a certain - * number of cabin crew, and they have to speak certain languages. - * Every cabin crew member has two flights off after an attended flight. - * - ''' - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/crew.mzn - * Comet : http://www.hakank.org/comet/crew.co - * ECLiPSe : http://hakank.org/eclipse/crew.ecl - * SICStus : http://hakank.org/sicstus/crew.pl - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Crew allocation problem in Google CP Solver. + +From Gecode example crew +examples/crew.cc +''' +* Example: Airline crew allocation +* +* Assign 20 flight attendants to 10 flights. Each flight needs a certain +* number of cabin crew, and they have to speak certain languages. +* Every cabin crew member has two flights off after an attended flight. +* +''' + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/crew.mzn +* Comet : http://www.hakank.org/comet/crew.co +* ECLiPSe : http://hakank.org/eclipse/crew.ecl +* SICStus : http://hakank.org/sicstus/crew.pl + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -50,9 +50,26 @@ def main(sols=1): # data # names = [ - "Tom", "David", "Jeremy", "Ron", "Joe", "Bill", "Fred", "Bob", "Mario", - "Ed", "Carol", "Janet", "Tracy", "Marilyn", "Carolyn", "Cathy", "Inez", - "Jean", "Heather", "Juliet" + "Tom", + "David", + "Jeremy", + "Ron", + "Joe", + "Bill", + "Fred", + "Bob", + "Mario", + "Ed", + "Carol", + "Janet", + "Tracy", + "Marilyn", + "Carolyn", + "Cathy", + "Inez", + "Jean", + "Heather", + "Juliet", ] num_persons = len(names) # number of persons @@ -78,7 +95,7 @@ def main(sols=1): [0, 1, 1, 1, 1], # Inez = 17 [0, 1, 1, 0, 0], # Jean = 18 [0, 1, 0, 1, 1], # Heather = 19 - [0, 1, 1, 0, 0] # Juliet = 20 + [0, 1, 1, 0, 0], # Juliet = 20 ] # The columns are in the following order: @@ -98,7 +115,7 @@ def main(sols=1): [5, 1, 1, 1, 1, 1], [6, 1, 1, 1, 1, 1], [6, 2, 2, 1, 1, 1], # ... - [7, 3, 3, 1, 1, 1] # Flight 10 + [7, 3, 3, 1, 1, 1], # Flight 10 ] num_flights = len(required_crew) # number of flights @@ -122,12 +139,15 @@ def main(sols=1): # # number of working persons - solver.Add(num_working == solver.Sum([ - solver.IsGreaterOrEqualCstVar( - solver.Sum([crew[(f, p)] - for f in range(num_flights)]), 1) - for p in range(num_persons) - ])) + solver.Add( + num_working + == solver.Sum([ + solver.IsGreaterOrEqualCstVar( + solver.Sum([crew[(f, p)] for f in range(num_flights)]), 1 + ) + for p in range(num_persons) + ]) + ) for f in range(num_flights): # size of crew @@ -155,8 +175,9 @@ def main(sols=1): solution.Add(crew_flat) solution.Add(num_working) - db = solver.Phase(crew_flat, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + crew_flat, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) # # result @@ -204,7 +225,7 @@ def main(sols=1): num_solutions_to_show = 1 if __name__ == "__main__": - if (len(sys.argv) > 1): + if len(sys.argv) > 1: num_solutions_to_show = int(sys.argv[1]) main(num_solutions_to_show) diff --git a/examples/contrib/crossword2.py b/examples/contrib/crossword2.py index a86e2a761b9..e33d50ba2c1 100644 --- a/examples/contrib/crossword2.py +++ b/examples/contrib/crossword2.py @@ -13,48 +13,48 @@ # limitations under the License. """ - Crosswords in Google CP Solver. - - This is a standard example for constraint logic programming. See e.g. - - http://www.cis.temple.edu/~ingargio/cis587/readings/constraints.html - ''' - We are to complete the puzzle - - 1 2 3 4 5 - +---+---+---+---+---+ Given the list of words: - 1 | 1 | | 2 | | 3 | AFT LASER - +---+---+---+---+---+ ALE LEE - 2 | # | # | | # | | EEL LINE - +---+---+---+---+---+ HEEL SAILS - 3 | # | 4 | | 5 | | HIKE SHEET - +---+---+---+---+---+ HOSES STEER - 4 | 6 | # | 7 | | | KEEL TIE - +---+---+---+---+---+ KNOT - 5 | 8 | | | | | - +---+---+---+---+---+ - 6 | | # | # | | # | The numbers 1,2,3,4,5,6,7,8 in the crossword - +---+---+---+---+---+ puzzle correspond to the words - that will start at those locations. - ''' - - The model was inspired by Sebastian Brand's Array Constraint cross word - example - http://www.cs.mu.oz.au/~sbrand/project/ac/ - http://www.cs.mu.oz.au/~sbrand/project/ac/examples.pl - - - Also, see the following models: - * MiniZinc: http://www.hakank.org/minizinc/crossword.mzn - * Comet: http://www.hakank.org/comet/crossword.co - * ECLiPSe: http://hakank.org/eclipse/crossword2.ecl - * Gecode: http://hakank.org/gecode/crossword2.cpp - * SICStus: http://hakank.org/sicstus/crossword2.pl - * Zinc: http://hakank.org/minizinc/crossword2.zinc - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ + Crosswords in Google CP Solver. + + This is a standard example for constraint logic programming. See e.g. + + http://www.cis.temple.edu/~ingargio/cis587/readings/constraints.html + ''' + We are to complete the puzzle + + 1 2 3 4 5 + +---+---+---+---+---+ Given the list of words: +1 | 1 | | 2 | | 3 | AFT LASER + +---+---+---+---+---+ ALE LEE +2 | # | # | | # | | EEL LINE + +---+---+---+---+---+ HEEL SAILS +3 | # | 4 | | 5 | | HIKE SHEET + +---+---+---+---+---+ HOSES STEER +4 | 6 | # | 7 | | | KEEL TIE + +---+---+---+---+---+ KNOT +5 | 8 | | | | | + +---+---+---+---+---+ +6 | | # | # | | # | The numbers 1,2,3,4,5,6,7,8 in the crossword + +---+---+---+---+---+ puzzle correspond to the words + that will start at those locations. + ''' + + The model was inspired by Sebastian Brand's Array Constraint cross word + example + http://www.cs.mu.oz.au/~sbrand/project/ac/ + http://www.cs.mu.oz.au/~sbrand/project/ac/examples.pl + + + Also, see the following models: + * MiniZinc: http://www.hakank.org/minizinc/crossword.mzn + * Comet: http://www.hakank.org/comet/crossword.co + * ECLiPSe: http://hakank.org/eclipse/crossword2.ecl + * Gecode: http://hakank.org/gecode/crossword2.cpp + * SICStus: http://hakank.org/sicstus/crossword2.pl + * Zinc: http://hakank.org/minizinc/crossword2.zinc + + This model was created by Hakan Kjellerstrand (hakank@gmail.com) + Also see my other Google CP Solver models: + http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -111,7 +111,7 @@ def main(): [a, l, e, 0, 0], # ALE [e, e, l, 0, 0], # EEL [l, e, e, 0, 0], # LEE - [t, i, e, 0, 0] # TIE + [t, i, e, 0, 0], # TIE ] num_overlapping = 12 @@ -127,7 +127,7 @@ def main(): [7, 0, 5, 1], # l [7, 2, 1, 4], # s [7, 3, 4, 2], # e - [7, 4, 2, 4] # r + [7, 4, 2, 4], # r ] n = 8 @@ -156,9 +156,13 @@ def main(): # But we must use Element explicitly solver.Add( - solver.Element(A_flat, E[overlapping[I][0]] * word_len + - overlapping[I][1]) == solver - .Element(A_flat, E[overlapping[I][2]] * word_len + overlapping[I][3])) + solver.Element( + A_flat, E[overlapping[I][0]] * word_len + overlapping[I][1] + ) + == solver.Element( + A_flat, E[overlapping[I][2]] * word_len + overlapping[I][3] + ) + ) # # solution and search @@ -187,8 +191,9 @@ def main(): def print_solution(A, E, alpha, n, word_len): for ee in range(n): print("%i: (%2i)" % (ee, E[ee].Value()), end=" ") - print("".join( - ["%s" % (alpha[A[ee, ii].Value()]) for ii in range(word_len)])) + print( + "".join(["%s" % (alpha[A[ee, ii].Value()]) for ii in range(word_len)]) + ) if __name__ == "__main__": diff --git a/examples/contrib/crypta.py b/examples/contrib/crypta.py index f8414840f01..13c3394aaaf 100644 --- a/examples/contrib/crypta.py +++ b/examples/contrib/crypta.py @@ -13,35 +13,35 @@ # limitations under the License. """ - Cryptarithmetic puzzle in Google CP Solver. - - Prolog benchmark problem GNU Prolog (crypta.pl) - ''' - Name : crypta.pl - Title : crypt-arithmetic - Original Source: P. Van Hentenryck's book - Adapted by : Daniel Diaz - INRIA France - Date : September 1992 - - Solve the operation: - - B A I J J A J I I A H F C F E B B J E A - + D H F G A B C D I D B I F F A G F E J E - ----------------------------------------- - = G J E G A C D D H F A F J B F I H E E F - ''' - - - Compare with the following models: - * Comet: http://hakank.org/comet/crypta.co - * MiniZinc: http://hakank.org/minizinc/crypta.mzn - * ECLiPSe: http://hakank.org/eclipse/crypta.ecl - * Gecode: http://hakank.org/gecode/crypta.cpp - * SICStus: http://hakank.org/sicstus/crypta.pl - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Cryptarithmetic puzzle in Google CP Solver. + +Prolog benchmark problem GNU Prolog (crypta.pl) +''' +Name : crypta.pl +Title : crypt-arithmetic +Original Source: P. Van Hentenryck's book +Adapted by : Daniel Diaz - INRIA France +Date : September 1992 + +Solve the operation: + + B A I J J A J I I A H F C F E B B J E A + + D H F G A B C D I D B I F F A G F E J E + ----------------------------------------- + = G J E G A C D D H F A F J B F I H E E F +''' + + +Compare with the following models: +* Comet: http://hakank.org/comet/crypta.co +* MiniZinc: http://hakank.org/minizinc/crypta.mzn +* ECLiPSe: http://hakank.org/eclipse/crypta.ecl +* Gecode: http://hakank.org/gecode/crypta.cpp +* SICStus: http://hakank.org/sicstus/crypta.pl + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -72,19 +72,73 @@ def main(): solver.Add(D >= 1) solver.Add(G >= 1) - solver.Add(A + 10 * E + 100 * J + 1000 * B + 10000 * B + 100000 * E + - 1000000 * F + E + 10 * J + 100 * E + 1000 * F + 10000 * G + - 100000 * A + 1000000 * F == F + 10 * E + 100 * E + 1000 * H + - 10000 * I + 100000 * F + 1000000 * B + 10000000 * Sr1) - - solver.Add(C + 10 * F + 100 * H + 1000 * A + 10000 * I + 100000 * I + - 1000000 * J + F + 10 * I + 100 * B + 1000 * D + 10000 * I + - 100000 * D + 1000000 * C + Sr1 == J + 10 * F + 100 * A + 1000 * F + - 10000 * H + 100000 * D + 1000000 * D + 10000000 * Sr2) - - solver.Add(A + 10 * J + 100 * J + 1000 * I + 10000 * A + 100000 * B + B + - 10 * A + 100 * G + 1000 * F + 10000 * H + 100000 * D + Sr2 == C + - 10 * A + 100 * G + 1000 * E + 10000 * J + 100000 * G) + solver.Add( + A + + 10 * E + + 100 * J + + 1000 * B + + 10000 * B + + 100000 * E + + 1000000 * F + + E + + 10 * J + + 100 * E + + 1000 * F + + 10000 * G + + 100000 * A + + 1000000 * F + == F + + 10 * E + + 100 * E + + 1000 * H + + 10000 * I + + 100000 * F + + 1000000 * B + + 10000000 * Sr1 + ) + + solver.Add( + C + + 10 * F + + 100 * H + + 1000 * A + + 10000 * I + + 100000 * I + + 1000000 * J + + F + + 10 * I + + 100 * B + + 1000 * D + + 10000 * I + + 100000 * D + + 1000000 * C + + Sr1 + == J + + 10 * F + + 100 * A + + 1000 * F + + 10000 * H + + 100000 * D + + 1000000 * D + + 10000000 * Sr2 + ) + + solver.Add( + A + + 10 * J + + 100 * J + + 1000 * I + + 10000 * A + + 100000 * B + + B + + 10 * A + + 100 * G + + 1000 * F + + 10000 * H + + 100000 * D + + Sr2 + == C + 10 * A + 100 * G + 1000 * E + 10000 * J + 100000 * G + ) # # search and result @@ -97,7 +151,7 @@ def main(): str = "ABCDEFGHIJ" while solver.NextSolution(): num_solutions += 1 - for (letter, val) in [(str[i], LD[i].Value()) for i in range(len(LD))]: + for letter, val in [(str[i], LD[i].Value()) for i in range(len(LD))]: print("%s: %i" % (letter, val)) print() diff --git a/examples/contrib/crypto.py b/examples/contrib/crypto.py index 2ee765148f0..03518c7c5c5 100644 --- a/examples/contrib/crypto.py +++ b/examples/contrib/crypto.py @@ -13,34 +13,34 @@ # limitations under the License. """ - Crypto problem in Google CP Solver. - - Prolog benchmark problem GNU Prolog (crypta.pl) - ''' - Name : crypta.pl - Title : crypt-arithmetic - Original Source: P. Van Hentenryck's book - Adapted by : Daniel Diaz - INRIA France - Date : September 1992 - - Solve the operation: - - B A I J J A J I I A H F C F E B B J E A - + D H F G A B C D I D B I F F A G F E J E - ----------------------------------------- - = G J E G A C D D H F A F J B F I H E E F - ''' - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/crypta.mzn - * Comet : http://www.hakank.org/comet/crypta.co - * ECLiPSe : http://www.hakank.org/eclipse/crypta.ecl - * SICStus : http://hakank.org/sicstus/crypta.pl - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Crypto problem in Google CP Solver. + +Prolog benchmark problem GNU Prolog (crypta.pl) +''' +Name : crypta.pl +Title : crypt-arithmetic +Original Source: P. Van Hentenryck's book +Adapted by : Daniel Diaz - INRIA France +Date : September 1992 + +Solve the operation: + + B A I J J A J I I A H F C F E B B J E A + + D H F G A B C D I D B I F F A G F E J E + ----------------------------------------- + = G J E G A C D D H F A F J B F I H E E F +''' + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/crypta.mzn +* Comet : http://www.hakank.org/comet/crypta.co +* ECLiPSe : http://www.hakank.org/eclipse/crypta.ecl +* SICStus : http://hakank.org/sicstus/crypta.pl + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -80,7 +80,34 @@ def main(): # variables # LD = [solver.IntVar(1, num_letters, "LD[%i]" % i) for i in range(num_letters)] - A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z = LD + ( + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + ) = LD # # constraints @@ -110,8 +137,9 @@ def main(): # # search and result # - db = solver.Phase(LD, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, - solver.ASSIGN_CENTER_VALUE) + db = solver.Phase( + LD, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, solver.ASSIGN_CENTER_VALUE + ) solver.NewSearch(db) @@ -119,7 +147,7 @@ def main(): str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" while solver.NextSolution(): num_solutions += 1 - for (letter, val) in [(str[i], LD[i].Value()) for i in range(num_letters)]: + for letter, val in [(str[i], LD[i].Value()) for i in range(num_letters)]: print("%s: %i" % (letter, val)) print() diff --git a/examples/contrib/curious_set_of_integers.py b/examples/contrib/curious_set_of_integers.py index f4cc2b23b4c..febd1d66f12 100644 --- a/examples/contrib/curious_set_of_integers.py +++ b/examples/contrib/curious_set_of_integers.py @@ -13,50 +13,50 @@ # limitations under the License. """ - Crypto problem in Google CP Solver. - - Martin Gardner (February 1967): - ''' - The integers 1,3,8, and 120 form a set with a remarkable property: the - product of any two integers is one less than a perfect square. Find - a fifth number that can be added to the set without destroying - this property. - ''' - - Solution: The number is 0. - - There are however other sets of five numbers with this property. - Here are the one in the range of 0.10000: - [0, 1, 3, 8, 120] - [0, 1, 3, 120, 1680] - [0, 1, 8, 15, 528] - [0, 1, 8, 120, 4095] - [0, 1, 15, 24, 1520] - [0, 1, 24, 35, 3480] - [0, 1, 35, 48, 6888] - [0, 2, 4, 12, 420] - [0, 2, 12, 24, 2380] - [0, 2, 24, 40, 7812] - [0, 3, 5, 16, 1008] - [0, 3, 8, 21, 2080] - [0, 3, 16, 33, 6440] - [0, 4, 6, 20, 1980] - [0, 4, 12, 30, 5852] - [0, 5, 7, 24, 3432] - [0, 6, 8, 28, 5460] - [0, 7, 9, 32, 8160] - - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/crypta.mzn - * Comet : http://www.hakank.org/comet/crypta.co - * ECLiPSe : http://www.hakank.org/eclipse/crypta.ecl - * SICStus : http://hakank.org/sicstus/crypta.pl - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Crypto problem in Google CP Solver. + +Martin Gardner (February 1967): +''' +The integers 1,3,8, and 120 form a set with a remarkable property: the +product of any two integers is one less than a perfect square. Find +a fifth number that can be added to the set without destroying +this property. +''' + +Solution: The number is 0. + +There are however other sets of five numbers with this property. +Here are the one in the range of 0.10000: +[0, 1, 3, 8, 120] +[0, 1, 3, 120, 1680] +[0, 1, 8, 15, 528] +[0, 1, 8, 120, 4095] +[0, 1, 15, 24, 1520] +[0, 1, 24, 35, 3480] +[0, 1, 35, 48, 6888] +[0, 2, 4, 12, 420] +[0, 2, 12, 24, 2380] +[0, 2, 24, 40, 7812] +[0, 3, 5, 16, 1008] +[0, 3, 8, 21, 2080] +[0, 3, 16, 33, 6440] +[0, 4, 6, 20, 1980] +[0, 4, 12, 30, 5852] +[0, 5, 7, 24, 3432] +[0, 6, 8, 28, 5460] +[0, 7, 9, 32, 8160] + + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/crypta.mzn +* Comet : http://www.hakank.org/comet/crypta.co +* ECLiPSe : http://www.hakank.org/eclipse/crypta.ecl +* SICStus : http://hakank.org/sicstus/crypta.pl + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -103,8 +103,9 @@ def main(): # # search and result # - db = solver.Phase(x, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + x, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) diff --git a/examples/contrib/debruijn_binary.py b/examples/contrib/debruijn_binary.py index c90fc3d0c45..9b70e0a526b 100644 --- a/examples/contrib/debruijn_binary.py +++ b/examples/contrib/debruijn_binary.py @@ -13,33 +13,33 @@ # limitations under the License. """ - de Bruijn sequences in Google CP Solver. - - Implementation of de Bruijn sequences in Minizinc, both 'classical' and - 'arbitrary'. - The 'arbitrary' version is when the length of the sequence (m here) is < - base**n. - - - Compare with the web based programs: - http://www.hakank.org/comb/debruijn.cgi - http://www.hakank.org/comb/debruijn_arb.cgi - - Compare with the following models: - * Tailor/Essence': http://hakank.org/tailor/debruijn.eprime - * MiniZinc: http://hakank.org/minizinc/debruijn_binary.mzn - * SICStus: http://hakank.org/sicstus/debruijn.pl - * Zinc: http://hakank.org/minizinc/debruijn_binary.zinc - * Choco: http://hakank.org/choco/DeBruijn.java - * Comet: http://hakank.org/comet/debruijn.co - * ECLiPSe: http://hakank.org/eclipse/debruijn.ecl - * Gecode: http://hakank.org/gecode/debruijn.cpp - * Gecode/R: http://hakank.org/gecode_r/debruijn_binary.rb - * JaCoP: http://hakank.org/JaCoP/DeBruijn.java - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +de Bruijn sequences in Google CP Solver. + +Implementation of de Bruijn sequences in Minizinc, both 'classical' and +'arbitrary'. +The 'arbitrary' version is when the length of the sequence (m here) is < +base**n. + + +Compare with the web based programs: + http://www.hakank.org/comb/debruijn.cgi + http://www.hakank.org/comb/debruijn_arb.cgi + +Compare with the following models: +* Tailor/Essence': http://hakank.org/tailor/debruijn.eprime +* MiniZinc: http://hakank.org/minizinc/debruijn_binary.mzn +* SICStus: http://hakank.org/sicstus/debruijn.pl +* Zinc: http://hakank.org/minizinc/debruijn_binary.zinc +* Choco: http://hakank.org/choco/DeBruijn.java +* Comet: http://hakank.org/comet/debruijn.co +* ECLiPSe: http://hakank.org/eclipse/debruijn.ecl +* Gecode: http://hakank.org/gecode/debruijn.cpp +* Gecode/R: http://hakank.org/gecode_r/debruijn_binary.rb +* JaCoP: http://hakank.org/JaCoP/DeBruijn.java + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -50,7 +50,8 @@ def toNum(solver, t, s, base): tlen = len(t) solver.Add( - s == solver.Sum([(base**(tlen - i - 1)) * t[i] for i in range(tlen)])) + s == solver.Sum([(base ** (tlen - i - 1)) * t[i] for i in range(tlen)]) + ) def main(base=2, n=3, m=8): @@ -70,9 +71,9 @@ def main(base=2, n=3, m=8): # m = base**n # harder problem - #base = 13 - #n = 4 - #m = 52 + # base = 13 + # n = 4 + # m = 52 # for n = 4 with different value of base # base = 2 0.030 seconds 16 failures @@ -108,7 +109,7 @@ def main(base=2, n=3, m=8): # # constraints # - #solver.Add(solver.AllDifferent([x[i] for i in range(m)])) + # solver.Add(solver.AllDifferent([x[i] for i in range(m)])) solver.Add(solver.AllDifferent(x)) # converts x <-> binary @@ -151,8 +152,11 @@ def main(base=2, n=3, m=8): # solution.Add([binary[(i,j)] for i in range(m) for j in range(n)]) solution.Add([gcc[i] for i in range(base)]) - db = solver.Phase([x[i] for i in range(m)] + [bin_code[i] for i in range(m)], - solver.CHOOSE_MIN_SIZE_LOWEST_MAX, solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + [x[i] for i in range(m)] + [bin_code[i] for i in range(m)], + solver.CHOOSE_MIN_SIZE_LOWEST_MAX, + solver.ASSIGN_MIN_VALUE, + ) num_solutions = 0 solver.NewSearch(db) diff --git a/examples/contrib/diet1.py b/examples/contrib/diet1.py index 9b8b0357f44..a87ec7f30f0 100644 --- a/examples/contrib/diet1.py +++ b/examples/contrib/diet1.py @@ -13,35 +13,35 @@ # limitations under the License. """ - Simple diet problem in Google CP Solver. - - Standard Operations Research example in Minizinc - - - Minimize the cost for the products: - Type of Calories Chocolate Sugar Fat - Food (ounces) (ounces) (ounces) - Chocolate Cake (1 slice) 400 3 2 2 - Chocolate ice cream (1 scoop) 200 2 2 4 - Cola (1 bottle) 150 0 4 1 - Pineapple cheesecake (1 piece) 500 0 4 5 - - Compare with the following models: - * Tailor/Essence': http://hakank.org/tailor/diet1.eprime - * MiniZinc: http://hakank.org/minizinc/diet1.mzn - * SICStus: http://hakank.org/sicstus/diet1.pl - * Zinc: http://hakank.org/minizinc/diet1.zinc - * Choco: http://hakank.org/choco/Diet.java - * Comet: http://hakank.org/comet/diet.co - * ECLiPSe: http://hakank.org/eclipse/diet.ecl - * Gecode: http://hakank.org/gecode/diet.cpp - * Gecode/R: http://hakank.org/gecode_r/diet.rb - * JaCoP: http://hakank.org/JaCoP/Diet.java - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Simple diet problem in Google CP Solver. + +Standard Operations Research example in Minizinc + + +Minimize the cost for the products: +Type of Calories Chocolate Sugar Fat +Food (ounces) (ounces) (ounces) +Chocolate Cake (1 slice) 400 3 2 2 +Chocolate ice cream (1 scoop) 200 2 2 4 +Cola (1 bottle) 150 0 4 1 +Pineapple cheesecake (1 piece) 500 0 4 5 + +Compare with the following models: +* Tailor/Essence': http://hakank.org/tailor/diet1.eprime +* MiniZinc: http://hakank.org/minizinc/diet1.mzn +* SICStus: http://hakank.org/sicstus/diet1.pl +* Zinc: http://hakank.org/minizinc/diet1.zinc +* Choco: http://hakank.org/choco/Diet.java +* Comet: http://hakank.org/comet/diet.co +* ECLiPSe: http://hakank.org/eclipse/diet.ecl +* Gecode: http://hakank.org/gecode/diet.cpp +* Gecode/R: http://hakank.org/gecode_r/diet.rb +* JaCoP: http://hakank.org/JaCoP/Diet.java + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.sat.python import cp_model @@ -87,12 +87,12 @@ def main(unused_argv): # Output solution. if status == cp_model.OPTIMAL: print("cost:", solver.ObjectiveValue()) - print([("abcdefghij" [i], solver.Value(x[i])) for i in range(n)]) + print([("abcdefghij"[i], solver.Value(x[i])) for i in range(n)]) print() - print(' - status : %s' % solver.StatusName(status)) - print(' - conflicts : %i' % solver.NumConflicts()) - print(' - branches : %i' % solver.NumBranches()) - print(' - wall time : %f ms' % solver.WallTime()) + print(" - status : %s" % solver.StatusName(status)) + print(" - conflicts : %i" % solver.NumConflicts()) + print(" - branches : %i" % solver.NumBranches()) + print(" - wall time : %f ms" % solver.WallTime()) print() diff --git a/examples/contrib/diet1_b.py b/examples/contrib/diet1_b.py index 76db2931065..47dd3a52b47 100644 --- a/examples/contrib/diet1_b.py +++ b/examples/contrib/diet1_b.py @@ -13,37 +13,37 @@ # limitations under the License. """ - Simple diet problem in Google CP Solver. +Simple diet problem in Google CP Solver. - Standard Operations Research example in Minizinc +Standard Operations Research example in Minizinc - Minimize the cost for the products: - Type of Calories Chocolate Sugar Fat - Food (ounces) (ounces) (ounces) - Chocolate Cake (1 slice) 400 3 2 2 - Chocolate ice cream (1 scoop) 200 2 2 4 - Cola (1 bottle) 150 0 4 1 - Pineapple cheesecake (1 piece) 500 0 4 5 +Minimize the cost for the products: +Type of Calories Chocolate Sugar Fat +Food (ounces) (ounces) (ounces) +Chocolate Cake (1 slice) 400 3 2 2 +Chocolate ice cream (1 scoop) 200 2 2 4 +Cola (1 bottle) 150 0 4 1 +Pineapple cheesecake (1 piece) 500 0 4 5 - Compare with the following models: - * Tailor/Essence': http://hakank.org/tailor/diet1.eprime - * MiniZinc: http://hakank.org/minizinc/diet1.mzn - * SICStus: http://hakank.org/sicstus/diet1.pl - * Zinc: http://hakank.org/minizinc/diet1.zinc - * Choco: http://hakank.org/choco/Diet.java - * Comet: http://hakank.org/comet/diet.co - * ECLiPSe: http://hakank.org/eclipse/diet.ecl - * Gecode: http://hakank.org/gecode/diet.cpp - * Gecode/R: http://hakank.org/gecode_r/diet.rb - * JaCoP: http://hakank.org/JaCoP/Diet.java +Compare with the following models: +* Tailor/Essence': http://hakank.org/tailor/diet1.eprime +* MiniZinc: http://hakank.org/minizinc/diet1.mzn +* SICStus: http://hakank.org/sicstus/diet1.pl +* Zinc: http://hakank.org/minizinc/diet1.zinc +* Choco: http://hakank.org/choco/Diet.java +* Comet: http://hakank.org/comet/diet.co +* ECLiPSe: http://hakank.org/eclipse/diet.ecl +* Gecode: http://hakank.org/gecode/diet.cpp +* Gecode/R: http://hakank.org/gecode_r/diet.rb +* JaCoP: http://hakank.org/JaCoP/Diet.java - This version use ScalProd() instead of Sum(). +This version use ScalProd() instead of Sum(). - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -94,11 +94,12 @@ def main(unused_argv): search_log = solver.SearchLog(100, cost) solver.Solve( solver.Phase(x + [cost], solver.INT_VAR_SIMPLE, solver.ASSIGN_MIN_VALUE), - [objective, search_log, collector]) + [objective, search_log, collector], + ) # get the first (and only) solution print("cost:", collector.ObjectiveValue(0)) - print([("abcdefghij" [i], collector.Value(0, x[i])) for i in range(n)]) + print([("abcdefghij"[i], collector.Value(0, x[i])) for i in range(n)]) print() print("failures:", solver.Failures()) print("branches:", solver.Branches()) diff --git a/examples/contrib/diet1_mip.py b/examples/contrib/diet1_mip.py index decb8dc4459..a995a3b5fa5 100644 --- a/examples/contrib/diet1_mip.py +++ b/examples/contrib/diet1_mip.py @@ -13,26 +13,26 @@ # limitations under the License. """ - Simple diet problem using MIP in Google CP Solver. +Simple diet problem using MIP in Google CP Solver. - Standard Operations Research example. +Standard Operations Research example. - Minimize the cost for the products: - Type of Calories Chocolate Sugar Fat - Food (ounces) (ounces) (ounces) - Chocolate Cake (1 slice) 400 3 2 2 - Chocolate ice cream (1 scoop) 200 2 2 4 - Cola (1 bottle) 150 0 4 1 - Pineapple cheesecake (1 piece) 500 0 4 5 +Minimize the cost for the products: +Type of Calories Chocolate Sugar Fat +Food (ounces) (ounces) (ounces) +Chocolate Cake (1 slice) 400 3 2 2 +Chocolate ice cream (1 scoop) 200 2 2 4 +Cola (1 bottle) 150 0 4 1 +Pineapple cheesecake (1 piece) 500 0 4 5 - Compare with the CP model: - http://www.hakank.org/google_or_tools/diet1.py +Compare with the CP model: + http://www.hakank.org/google_or_tools/diet1.py - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp diff --git a/examples/contrib/discrete_tomography.py b/examples/contrib/discrete_tomography.py index e22a520dfd8..9c0d7b58b72 100644 --- a/examples/contrib/discrete_tomography.py +++ b/examples/contrib/discrete_tomography.py @@ -13,45 +13,45 @@ # limitations under the License. """ - Discrete tomography in Google CP Solver. - - Problem from http://eclipse.crosscoreop.com/examples/tomo.ecl.txt - ''' - This is a little 'tomography' problem, taken from an old issue - of Scientific American. - - A matrix which contains zeroes and ones gets "x-rayed" vertically and - horizontally, giving the total number of ones in each row and column. - The problem is to reconstruct the contents of the matrix from this - information. Sample run: - - ?- go. - 0 0 7 1 6 3 4 5 2 7 0 0 - 0 - 0 - 8 * * * * * * * * - 2 * * - 6 * * * * * * - 4 * * * * - 5 * * * * * - 3 * * * - 7 * * * * * * * - 0 - 0 - - Eclipse solution by Joachim Schimpf, IC-Parc - ''' - - Compare with the following models: - * Comet: http://www.hakank.org/comet/discrete_tomography.co - * Gecode: http://www.hakank.org/gecode/discrete_tomography.cpp - * MiniZinc: http://www.hakank.org/minizinc/tomography.mzn - * Tailor/Essence': http://www.hakank.org/tailor/tomography.eprime - * SICStus: http://hakank.org/sicstus/discrete_tomography.pl + Discrete tomography in Google CP Solver. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ + Problem from http://eclipse.crosscoreop.com/examples/tomo.ecl.txt + ''' + This is a little 'tomography' problem, taken from an old issue + of Scientific American. + + A matrix which contains zeroes and ones gets "x-rayed" vertically and + horizontally, giving the total number of ones in each row and column. + The problem is to reconstruct the contents of the matrix from this + information. Sample run: + + ?- go. + 0 0 7 1 6 3 4 5 2 7 0 0 +0 +0 +8 * * * * * * * * +2 * * +6 * * * * * * +4 * * * * +5 * * * * * +3 * * * +7 * * * * * * * +0 +0 + +Eclipse solution by Joachim Schimpf, IC-Parc +''' + +Compare with the following models: +* Comet: http://www.hakank.org/comet/discrete_tomography.co +* Gecode: http://www.hakank.org/gecode/discrete_tomography.cpp +* MiniZinc: http://www.hakank.org/minizinc/tomography.mzn +* Tailor/Essence': http://www.hakank.org/tailor/tomography.eprime +* SICStus: http://hakank.org/sicstus/discrete_tomography.pl + + This model was created by Hakan Kjellerstrand (hakank@gmail.com) + Also see my other Google CP Solver models: + http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -86,13 +86,11 @@ def main(row_sums="", col_sums=""): # constraints # [ - solver.Add(solver.Sum([x[i][j] - for j in range(c)]) == row_sums[i]) + solver.Add(solver.Sum([x[i][j] for j in range(c)]) == row_sums[i]) for i in range(r) ] [ - solver.Add(solver.Sum([x[i][j] - for i in range(r)]) == col_sums[j]) + solver.Add(solver.Sum([x[i][j] for i in range(r)]) == col_sums[j]) for j in range(c) ] diff --git a/examples/contrib/divisible_by_9_through_1.py b/examples/contrib/divisible_by_9_through_1.py index 7edff81a008..c7825108bc3 100644 --- a/examples/contrib/divisible_by_9_through_1.py +++ b/examples/contrib/divisible_by_9_through_1.py @@ -13,42 +13,42 @@ # limitations under the License. """ - Divisible by 9 through 1 puzzle in Google CP Solver. +Divisible by 9 through 1 puzzle in Google CP Solver. - From http://msdn.microsoft.com/en-us/vcsharp/ee957404.aspx - ' Solving Combinatory Problems with LINQ' - ''' - Find a number consisting of 9 digits in which each of the digits - from 1 to 9 appears only once. This number must also satisfy these - divisibility requirements: +From http://msdn.microsoft.com/en-us/vcsharp/ee957404.aspx +' Solving Combinatory Problems with LINQ' +''' +Find a number consisting of 9 digits in which each of the digits +from 1 to 9 appears only once. This number must also satisfy these +divisibility requirements: - 1. The number should be divisible by 9. - 2. If the rightmost digit is removed, the remaining number should - be divisible by 8. - 3. If the rightmost digit of the new number is removed, the remaining - number should be divisible by 7. - 4. And so on, until there's only one digit (which will necessarily - be divisible by 1). - ''' + 1. The number should be divisible by 9. + 2. If the rightmost digit is removed, the remaining number should + be divisible by 8. + 3. If the rightmost digit of the new number is removed, the remaining + number should be divisible by 7. + 4. And so on, until there's only one digit (which will necessarily + be divisible by 1). +''' - Also, see - 'Intel Parallel Studio: Great for Serial Code Too (Episode 1)' - http://software.intel.com/en-us/blogs/2009/12/07/intel-parallel-studio-great-for-serial-code-too-episode-1/ +Also, see +'Intel Parallel Studio: Great for Serial Code Too (Episode 1)' +http://software.intel.com/en-us/blogs/2009/12/07/intel-parallel-studio-great-for-serial-code-too-episode-1/ - This model is however generalized to handle any base, for reasonable limits. - The 'reasonable limit' for this model is that base must be between 2..16. +This model is however generalized to handle any base, for reasonable limits. +The 'reasonable limit' for this model is that base must be between 2..16. - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/divisible_by_9_through_1.mzn - * Comet : http://www.hakank.org/comet/divisible_by_9_through_1.co - * ECLiPSe : http://www.hakank.org/eclipse/divisible_by_9_through_1.ecl - * Gecode : http://www.hakank.org/gecode/divisible_by_9_through_1.cpp +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/divisible_by_9_through_1.mzn +* Comet : http://www.hakank.org/comet/divisible_by_9_through_1.co +* ECLiPSe : http://www.hakank.org/eclipse/divisible_by_9_through_1.ecl +* Gecode : http://www.hakank.org/gecode/divisible_by_9_through_1.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -100,7 +100,8 @@ def my_mod(solver, x, y, r): def toNum(solver, t, s, base): tlen = len(t) solver.Add( - s == solver.Sum([(base**(tlen - i - 1)) * t[i] for i in range(tlen)])) + s == solver.Sum([(base ** (tlen - i - 1)) * t[i] for i in range(tlen)]) + ) def main(base=10): @@ -109,7 +110,7 @@ def main(base=10): solver = pywrapcp.Solver("Divisible by 9 through 1") # data - m = base**(base - 1) - 1 + m = base ** (base - 1) - 1 n = base - 1 digits_str = "_0123456789ABCDEFGH" @@ -148,8 +149,14 @@ def main(base=10): while solver.NextSolution(): print("x: ", [x[i].Value() for i in range(n)]) print("t: ", [t[i].Value() for i in range(n)]) - print("number base 10: %i base %i: %s" % (t[0].Value(), base, "".join( - [digits_str[x[i].Value() + 1] for i in range(n)]))) + print( + "number base 10: %i base %i: %s" + % ( + t[0].Value(), + base, + "".join([digits_str[x[i].Value() + 1] for i in range(n)]), + ) + ) print() num_solutions += 1 solver.EndSearch() @@ -167,8 +174,10 @@ def main(base=10): if len(sys.argv) > 1: base = int(sys.argv[1]) if base > max_base: - print("Sorry, max allowed base is %i. Setting base to %i..." % - (max_base, default_base)) + print( + "Sorry, max allowed base is %i. Setting base to %i..." + % (max_base, default_base) + ) base = default_base main(base) diff --git a/examples/contrib/dudeney.py b/examples/contrib/dudeney.py index 2e4a0e0657e..ac6ddfb5783 100644 --- a/examples/contrib/dudeney.py +++ b/examples/contrib/dudeney.py @@ -20,7 +20,7 @@ def dudeney(n): s = solver.IntVar(list(range(1, 9 * n + 1)), 's') solver.Add(nb == s * s * s) - solver.Add(sum([10**(n - i - 1) * x[i] for i in range(n)]) == nb) + solver.Add(sum([10 ** (n - i - 1) * x[i] for i in range(n)]) == nb) solver.Add(sum([x[i] for i in range(n)]) == s) solution = solver.Assignment() @@ -29,7 +29,8 @@ def dudeney(n): solver.Solve( solver.Phase(x, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT), - [collector]) + [collector], + ) for i in range(collector.SolutionCount()): nbsol = collector.Value(i, nb) diff --git a/examples/contrib/einav_puzzle.py b/examples/contrib/einav_puzzle.py index b66cc1c8e8a..dbdbb055167 100644 --- a/examples/contrib/einav_puzzle.py +++ b/examples/contrib/einav_puzzle.py @@ -13,54 +13,54 @@ # limitations under the License. """ - A programming puzzle from Einav in Google CP Solver. - - From - 'A programming puzzle from Einav' - http://gcanyon.wordpress.com/2009/10/28/a-programming-puzzle-from-einav/ - ''' - My friend Einav gave me this programming puzzle to work on. Given - this array of positive and negative numbers: - 33 30 -10 -6 18 7 -11 -23 6 - ... - -25 4 16 30 33 -23 -4 4 -23 - - You can flip the sign of entire rows and columns, as many of them - as you like. The goal is to make all the rows and columns sum to positive - numbers (or zero), and then to find the solution (there are more than one) - that has the smallest overall sum. So for example, for this array: - 33 30 -10 - -16 19 9 - -17 -12 -14 - You could flip the sign for the bottom row to get this array: - 33 30 -10 - -16 19 9 - 17 12 14 - Now all the rows and columns have positive sums, and the overall total is - 108. - But you could instead flip the second and third columns, and the second - row, to get this array: - 33 -30 10 - 16 19 9 - -17 12 14 - All the rows and columns still total positive, and the overall sum is just - 66. So this solution is better (I don't know if it's the best) - A pure brute force solution would have to try over 30 billion solutions. - I wrote code to solve this in J. I'll post that separately. - ''' - - Compare with the following models: - * MiniZinc http://www.hakank.org/minizinc/einav_puzzle.mzn - * SICStus: http://hakank.org/sicstus/einav_puzzle.pl - - Note: - einav_puzzle2.py is Laurent Perron version, which don't use as many - decision variables as this version. - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +A programming puzzle from Einav in Google CP Solver. + +From +'A programming puzzle from Einav' +http://gcanyon.wordpress.com/2009/10/28/a-programming-puzzle-from-einav/ +''' +My friend Einav gave me this programming puzzle to work on. Given +this array of positive and negative numbers: +33 30 -10 -6 18 7 -11 -23 6 +... +-25 4 16 30 33 -23 -4 4 -23 + +You can flip the sign of entire rows and columns, as many of them +as you like. The goal is to make all the rows and columns sum to positive +numbers (or zero), and then to find the solution (there are more than one) +that has the smallest overall sum. So for example, for this array: +33 30 -10 +-16 19 9 +-17 -12 -14 +You could flip the sign for the bottom row to get this array: +33 30 -10 +-16 19 9 +17 12 14 +Now all the rows and columns have positive sums, and the overall total is +108. +But you could instead flip the second and third columns, and the second +row, to get this array: +33 -30 10 +16 19 9 +-17 12 14 +All the rows and columns still total positive, and the overall sum is just +66. So this solution is better (I don't know if it's the best) +A pure brute force solution would have to try over 30 billion solutions. +I wrote code to solve this in J. I'll post that separately. +''' + +Compare with the following models: +* MiniZinc http://www.hakank.org/minizinc/einav_puzzle.mzn +* SICStus: http://hakank.org/sicstus/einav_puzzle.pl + +Note: +einav_puzzle2.py is Laurent Perron version, which don't use as many +decision variables as this version. + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -86,33 +86,35 @@ def main(): # Full problem rows = 27 cols = 9 - data = [[33, 30, 10, -6, 18, -7, -11, 23, -6], - [16, -19, 9, -26, -8, -19, -8, -21, -14], - [17, 12, -14, 31, -30, 13, -13, 19, 16], - [-6, -11, 1, 17, -12, -4, -7, 14, -21], - [18, -31, 34, -22, 17, -19, 20, 24, 6], - [33, -18, 17, -15, 31, -5, 3, 27, -3], - [-18, -20, -18, 31, 6, 4, -2, -12, 24], - [27, 14, 4, -29, -3, 5, -29, 8, -12], - [-15, -7, -23, 23, -9, -8, 6, 8, -12], - [33, -23, -19, -4, -8, -7, 11, -12, 31], - [-20, 19, -15, -30, 11, 32, 7, 14, -5], - [-23, 18, -32, -2, -31, -7, 8, 24, 16], - [32, -4, -10, -14, -6, -1, 0, 23, 23], - [25, 0, -23, 22, 12, 28, -27, 15, 4], - [-30, -13, -16, -3, -3, -32, -3, 27, -31], - [22, 1, 26, 4, -2, -13, 26, 17, 14], - [-9, -18, 3, -20, -27, -32, -11, 27, 13], - [-17, 33, -7, 19, -32, 13, -31, -2, -24], - [-31, 27, -31, -29, 15, 2, 29, -15, 33], - [-18, -23, 15, 28, 0, 30, -4, 12, -32], - [-3, 34, 27, -25, -18, 26, 1, 34, 26], - [-21, -31, -10, -13, -30, -17, -12, -26, 31], - [23, -31, -19, 21, -17, -10, 2, -23, 23], - [-3, 6, 0, -3, -32, 0, -10, -25, 14], - [-19, 9, 14, -27, 20, 15, -5, -27, 18], - [11, -6, 24, 7, -17, 26, 20, -31, -25], - [-25, 4, -16, 30, 33, 23, -4, -4, 23]] + data = [ + [33, 30, 10, -6, 18, -7, -11, 23, -6], + [16, -19, 9, -26, -8, -19, -8, -21, -14], + [17, 12, -14, 31, -30, 13, -13, 19, 16], + [-6, -11, 1, 17, -12, -4, -7, 14, -21], + [18, -31, 34, -22, 17, -19, 20, 24, 6], + [33, -18, 17, -15, 31, -5, 3, 27, -3], + [-18, -20, -18, 31, 6, 4, -2, -12, 24], + [27, 14, 4, -29, -3, 5, -29, 8, -12], + [-15, -7, -23, 23, -9, -8, 6, 8, -12], + [33, -23, -19, -4, -8, -7, 11, -12, 31], + [-20, 19, -15, -30, 11, 32, 7, 14, -5], + [-23, 18, -32, -2, -31, -7, 8, 24, 16], + [32, -4, -10, -14, -6, -1, 0, 23, 23], + [25, 0, -23, 22, 12, 28, -27, 15, 4], + [-30, -13, -16, -3, -3, -32, -3, 27, -31], + [22, 1, 26, 4, -2, -13, 26, 17, 14], + [-9, -18, 3, -20, -27, -32, -11, 27, 13], + [-17, 33, -7, 19, -32, 13, -31, -2, -24], + [-31, 27, -31, -29, 15, 2, 29, -15, 33], + [-18, -23, 15, 28, 0, 30, -4, 12, -32], + [-3, 34, 27, -25, -18, 26, 1, 34, 26], + [-21, -31, -10, -13, -30, -17, -12, -26, 31], + [23, -31, -19, 21, -17, -10, 2, -23, 23], + [-3, 6, 0, -3, -32, 0, -10, -25, 14], + [-19, 9, 14, -27, 20, 15, -5, -27, 18], + [11, -6, 24, 7, -17, 26, 20, -31, -25], + [-25, 4, -16, 30, 33, 23, -4, -4, 23], + ] # # variables @@ -165,8 +167,11 @@ def main(): # # Note: The order of the variables makes a big difference. # If row_signs are before col_sign it is much slower. - db = solver.Phase(col_signs + row_signs, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, - solver.ASSIGN_MAX_VALUE) + db = solver.Phase( + col_signs + row_signs, + solver.CHOOSE_MIN_SIZE_LOWEST_MIN, + solver.ASSIGN_MAX_VALUE, + ) solver.NewSearch(db, [objective]) diff --git a/examples/contrib/einav_puzzle2.py b/examples/contrib/einav_puzzle2.py index cb57073a59c..a73104b50ef 100644 --- a/examples/contrib/einav_puzzle2.py +++ b/examples/contrib/einav_puzzle2.py @@ -13,55 +13,55 @@ # limitations under the License. """ - A programming puzzle from Einav in Google CP Solver. - - From - 'A programming puzzle from Einav' - http://gcanyon.wordpress.com/2009/10/28/a-programming-puzzle-from-einav/ - ''' - My friend Einav gave me this programming puzzle to work on. Given - this array of positive and negative numbers: - 33 30 -10 -6 18 7 -11 -23 6 - ... - -25 4 16 30 33 -23 -4 4 -23 - - You can flip the sign of entire rows and columns, as many of them - as you like. The goal is to make all the rows and columns sum to positive - numbers (or zero), and then to find the solution (there are more than one) - that has the smallest overall sum. So for example, for this array: - 33 30 -10 - -16 19 9 - -17 -12 -14 - You could flip the sign for the bottom row to get this array: - 33 30 -10 - -16 19 9 - 17 12 14 - Now all the rows and columns have positive sums, and the overall total is - 108. - But you could instead flip the second and third columns, and the second - row, to get this array: - 33 -30 10 - 16 19 9 - -17 12 14 - All the rows and columns still total positive, and the overall sum is just - 66. So this solution is better (I don't know if it's the best) - A pure brute force solution would have to try over 30 billion solutions. - I wrote code to solve this in J. I'll post that separately. - ''' - - Compare with the following models: - * MiniZinc http://www.hakank.org/minizinc/einav_puzzle.mzn - * SICStus: http://hakank.org/sicstus/einav_puzzle.pl - - Note: - This is a Larent Perrons's variant of einav_puzzle.py. - He removed some of the decision variables and made it more efficient. - Thanks! - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +A programming puzzle from Einav in Google CP Solver. + +From +'A programming puzzle from Einav' +http://gcanyon.wordpress.com/2009/10/28/a-programming-puzzle-from-einav/ +''' +My friend Einav gave me this programming puzzle to work on. Given +this array of positive and negative numbers: +33 30 -10 -6 18 7 -11 -23 6 +... +-25 4 16 30 33 -23 -4 4 -23 + +You can flip the sign of entire rows and columns, as many of them +as you like. The goal is to make all the rows and columns sum to positive +numbers (or zero), and then to find the solution (there are more than one) +that has the smallest overall sum. So for example, for this array: +33 30 -10 +-16 19 9 +-17 -12 -14 +You could flip the sign for the bottom row to get this array: +33 30 -10 +-16 19 9 +17 12 14 +Now all the rows and columns have positive sums, and the overall total is +108. +But you could instead flip the second and third columns, and the second +row, to get this array: +33 -30 10 +16 19 9 +-17 12 14 +All the rows and columns still total positive, and the overall sum is just +66. So this solution is better (I don't know if it's the best) +A pure brute force solution would have to try over 30 billion solutions. +I wrote code to solve this in J. I'll post that separately. +''' + +Compare with the following models: +* MiniZinc http://www.hakank.org/minizinc/einav_puzzle.mzn +* SICStus: http://hakank.org/sicstus/einav_puzzle.pl + +Note: +This is a Larent Perrons's variant of einav_puzzle.py. +He removed some of the decision variables and made it more efficient. +Thanks! + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -87,33 +87,35 @@ def main(): # Full problem rows = 27 cols = 9 - data = [[33, 30, 10, -6, 18, -7, -11, 23, -6], - [16, -19, 9, -26, -8, -19, -8, -21, -14], - [17, 12, -14, 31, -30, 13, -13, 19, 16], - [-6, -11, 1, 17, -12, -4, -7, 14, -21], - [18, -31, 34, -22, 17, -19, 20, 24, 6], - [33, -18, 17, -15, 31, -5, 3, 27, -3], - [-18, -20, -18, 31, 6, 4, -2, -12, 24], - [27, 14, 4, -29, -3, 5, -29, 8, -12], - [-15, -7, -23, 23, -9, -8, 6, 8, -12], - [33, -23, -19, -4, -8, -7, 11, -12, 31], - [-20, 19, -15, -30, 11, 32, 7, 14, -5], - [-23, 18, -32, -2, -31, -7, 8, 24, 16], - [32, -4, -10, -14, -6, -1, 0, 23, 23], - [25, 0, -23, 22, 12, 28, -27, 15, 4], - [-30, -13, -16, -3, -3, -32, -3, 27, -31], - [22, 1, 26, 4, -2, -13, 26, 17, 14], - [-9, -18, 3, -20, -27, -32, -11, 27, 13], - [-17, 33, -7, 19, -32, 13, -31, -2, -24], - [-31, 27, -31, -29, 15, 2, 29, -15, 33], - [-18, -23, 15, 28, 0, 30, -4, 12, -32], - [-3, 34, 27, -25, -18, 26, 1, 34, 26], - [-21, -31, -10, -13, -30, -17, -12, -26, 31], - [23, -31, -19, 21, -17, -10, 2, -23, 23], - [-3, 6, 0, -3, -32, 0, -10, -25, 14], - [-19, 9, 14, -27, 20, 15, -5, -27, 18], - [11, -6, 24, 7, -17, 26, 20, -31, -25], - [-25, 4, -16, 30, 33, 23, -4, -4, 23]] + data = [ + [33, 30, 10, -6, 18, -7, -11, 23, -6], + [16, -19, 9, -26, -8, -19, -8, -21, -14], + [17, 12, -14, 31, -30, 13, -13, 19, 16], + [-6, -11, 1, 17, -12, -4, -7, 14, -21], + [18, -31, 34, -22, 17, -19, 20, 24, 6], + [33, -18, 17, -15, 31, -5, 3, 27, -3], + [-18, -20, -18, 31, 6, 4, -2, -12, 24], + [27, 14, 4, -29, -3, 5, -29, 8, -12], + [-15, -7, -23, 23, -9, -8, 6, 8, -12], + [33, -23, -19, -4, -8, -7, 11, -12, 31], + [-20, 19, -15, -30, 11, 32, 7, 14, -5], + [-23, 18, -32, -2, -31, -7, 8, 24, 16], + [32, -4, -10, -14, -6, -1, 0, 23, 23], + [25, 0, -23, 22, 12, 28, -27, 15, 4], + [-30, -13, -16, -3, -3, -32, -3, 27, -31], + [22, 1, 26, 4, -2, -13, 26, 17, 14], + [-9, -18, 3, -20, -27, -32, -11, 27, 13], + [-17, 33, -7, 19, -32, 13, -31, -2, -24], + [-31, 27, -31, -29, 15, 2, 29, -15, 33], + [-18, -23, 15, 28, 0, 30, -4, 12, -32], + [-3, 34, 27, -25, -18, 26, 1, 34, 26], + [-21, -31, -10, -13, -30, -17, -12, -26, 31], + [23, -31, -19, 21, -17, -10, 2, -23, 23], + [-3, 6, 0, -3, -32, 0, -10, -25, 14], + [-19, 9, 14, -27, 20, 15, -5, -27, 18], + [11, -6, 24, 7, -17, 26, 20, -31, -25], + [-25, 4, -16, 30, 33, 23, -4, -4, 23], + ] # # variables @@ -162,8 +164,11 @@ def main(): # # search and result # - db = solver.Phase(col_signs + row_signs, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, - solver.ASSIGN_MAX_VALUE) + db = solver.Phase( + col_signs + row_signs, + solver.CHOOSE_MIN_SIZE_LOWEST_MIN, + solver.ASSIGN_MAX_VALUE, + ) solver.NewSearch(db, [objective]) diff --git a/examples/contrib/eq10.py b/examples/contrib/eq10.py index 7cc807de667..410fafa1371 100644 --- a/examples/contrib/eq10.py +++ b/examples/contrib/eq10.py @@ -13,18 +13,18 @@ # limitations under the License. """ - Eq 10 in Google CP Solver. +Eq 10 in Google CP Solver. - Standard benchmark problem. +Standard benchmark problem. - Compare with the following models: - * MiniZinc: http://hakank.org/minizinc/eq10.mzn - * ECLiPSe: http://hakank.org/eclipse/eq10.ecl - * SICStus: http://hakank.org/sicstus/eq10.pl +Compare with the following models: +* MiniZinc: http://hakank.org/minizinc/eq10.mzn +* ECLiPSe: http://hakank.org/eclipse/eq10.ecl +* SICStus: http://hakank.org/sicstus/eq10.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -48,35 +48,61 @@ def main(): # # constraints # - solver.Add(0 + 98527 * X1 + 34588 * X2 + 5872 * X3 + 59422 * X5 + - 65159 * X7 == 1547604 + 30704 * X4 + 29649 * X6) - - solver.Add(0 + 98957 * X2 + 83634 * X3 + 69966 * X4 + 62038 * X5 + - 37164 * X6 + 85413 * X7 == 1823553 + 93989 * X1) - - solver.Add(900032 + 10949 * X1 + 77761 * X2 + 67052 * X5 == 0 + 80197 * X3 + - 61944 * X4 + 92964 * X6 + 44550 * X7) - - solver.Add(0 + 73947 * X1 + 84391 * X3 + 81310 * X5 == 1164380 + 96253 * X2 + - 44247 * X4 + 70582 * X6 + 33054 * X7) - - solver.Add(0 + 13057 * X3 + 42253 * X4 + 77527 * X5 + 96552 * X7 == 1185471 + - 60152 * X1 + 21103 * X2 + 97932 * X6) - - solver.Add(1394152 + 66920 * X1 + 55679 * X4 == 0 + 64234 * X2 + 65337 * X3 + - 45581 * X5 + 67707 * X6 + 98038 * X7) - - solver.Add(0 + 68550 * X1 + 27886 * X2 + 31716 * X3 + 73597 * X4 + - 38835 * X7 == 279091 + 88963 * X5 + 76391 * X6) - - solver.Add(0 + 76132 * X2 + 71860 * X3 + 22770 * X4 + 68211 * X5 + - 78587 * X6 == 480923 + 48224 * X1 + 82817 * X7) - - solver.Add(519878 + 94198 * X2 + 87234 * X3 + 37498 * X4 == 0 + 71583 * X1 + - 25728 * X5 + 25495 * X6 + 70023 * X7) - - solver.Add(361921 + 78693 * X1 + 38592 * X5 + 38478 * X6 == 0 + 94129 * X2 + - 43188 * X3 + 82528 * X4 + 69025 * X7) + solver.Add( + 0 + 98527 * X1 + 34588 * X2 + 5872 * X3 + 59422 * X5 + 65159 * X7 + == 1547604 + 30704 * X4 + 29649 * X6 + ) + + solver.Add( + 0 + + 98957 * X2 + + 83634 * X3 + + 69966 * X4 + + 62038 * X5 + + 37164 * X6 + + 85413 * X7 + == 1823553 + 93989 * X1 + ) + + solver.Add( + 900032 + 10949 * X1 + 77761 * X2 + 67052 * X5 + == 0 + 80197 * X3 + 61944 * X4 + 92964 * X6 + 44550 * X7 + ) + + solver.Add( + 0 + 73947 * X1 + 84391 * X3 + 81310 * X5 + == 1164380 + 96253 * X2 + 44247 * X4 + 70582 * X6 + 33054 * X7 + ) + + solver.Add( + 0 + 13057 * X3 + 42253 * X4 + 77527 * X5 + 96552 * X7 + == 1185471 + 60152 * X1 + 21103 * X2 + 97932 * X6 + ) + + solver.Add( + 1394152 + 66920 * X1 + 55679 * X4 + == 0 + 64234 * X2 + 65337 * X3 + 45581 * X5 + 67707 * X6 + 98038 * X7 + ) + + solver.Add( + 0 + 68550 * X1 + 27886 * X2 + 31716 * X3 + 73597 * X4 + 38835 * X7 + == 279091 + 88963 * X5 + 76391 * X6 + ) + + solver.Add( + 0 + 76132 * X2 + 71860 * X3 + 22770 * X4 + 68211 * X5 + 78587 * X6 + == 480923 + 48224 * X1 + 82817 * X7 + ) + + solver.Add( + 519878 + 94198 * X2 + 87234 * X3 + 37498 * X4 + == 0 + 71583 * X1 + 25728 * X5 + 25495 * X6 + 70023 * X7 + ) + + solver.Add( + 361921 + 78693 * X1 + 38592 * X5 + 38478 * X6 + == 0 + 94129 * X2 + 43188 * X3 + 82528 * X4 + 69025 * X7 + ) # # search and result diff --git a/examples/contrib/eq20.py b/examples/contrib/eq20.py index fb5dd124788..67b330eab3a 100644 --- a/examples/contrib/eq20.py +++ b/examples/contrib/eq20.py @@ -13,18 +13,18 @@ # limitations under the License. """ - Eq 20 in Google CP Solver. +Eq 20 in Google CP Solver. - Standard benchmark problem. +Standard benchmark problem. - Compare with the following models: - * Gecode/R: http://hakank.org/gecode_r/eq20.rb - * ECLiPSe: http://hakank.org/eclipse/eq20.ecl - * SICStus: http://hakank.org/sicstus/eq20.pl +Compare with the following models: +* Gecode/R: http://hakank.org/gecode_r/eq20.rb +* ECLiPSe: http://hakank.org/eclipse/eq20.ecl +* SICStus: http://hakank.org/sicstus/eq20.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -48,46 +48,206 @@ def main(): # # constraints # - solver.Add(-76706 * X0 + 98205 * X1 + 23445 * X2 + 67921 * X3 + 24111 * X4 + - -48614 * X5 + -41906 * X6 == 821228) - solver.Add(87059 * X0 + -29101 * X1 + -5513 * X2 + -21219 * X3 + 22128 * X4 + - 7276 * X5 + 57308 * X6 == 22167) - solver.Add(-60113 * X0 + 29475 * X1 + 34421 * X2 + -76870 * X3 + 62646 * X4 + - 29278 * X5 + -15212 * X6 == 251591) - solver.Add(49149 * X0 + 52871 * X1 + -7132 * X2 + 56728 * X3 + -33576 * X4 + - -49530 * X5 + -62089 * X6 == 146074) - solver.Add(-10343 * X0 + 87758 * X1 + -11782 * X2 + 19346 * X3 + 70072 * X4 + - -36991 * X5 + 44529 * X6 == 740061) - solver.Add(85176 * X0 + -95332 * X1 + -1268 * X2 + 57898 * X3 + 15883 * X4 + - 50547 * X5 + 83287 * X6 == 373854) - solver.Add(-85698 * X0 + 29958 * X1 + 57308 * X2 + 48789 * X3 + -78219 * X4 + - 4657 * X5 + 34539 * X6 == 249912) - solver.Add(-67456 * X0 + 84750 * X1 + -51553 * X2 + 21239 * X3 + 81675 * X4 + - -99395 * X5 + -4254 * X6 == 277271) - solver.Add(94016 * X0 + -82071 * X1 + 35961 * X2 + 66597 * X3 + -30705 * X4 + - -44404 * X5 + -38304 * X6 == 25334) - solver.Add(-60301 * X0 + 31227 * X1 + 93951 * X2 + 73889 * X3 + 81526 * X4 + - -72702 * X5 + 68026 * X6 == 1410723) - solver.Add(-16835 * X0 + 47385 * X1 + 97715 * X2 + -12640 * X3 + 69028 * X4 + - 76212 * X5 + -81102 * X6 == 1244857) - solver.Add(-43277 * X0 + 43525 * X1 + 92298 * X2 + 58630 * X3 + 92590 * X4 + - -9372 * X5 + -60227 * X6 == 1503588) - solver.Add(-64919 * X0 + 80460 * X1 + 90840 * X2 + -59624 * X3 + -75542 * X4 + - 25145 * X5 + -47935 * X6 == 18465) - solver.Add(-45086 * X0 + 51830 * X1 + -4578 * X2 + 96120 * X3 + 21231 * X4 + - 97919 * X5 + 65651 * X6 == 1198280) - solver.Add(85268 * X0 + 54180 * X1 + -18810 * X2 + -48219 * X3 + 6013 * X4 + - 78169 * X5 + -79785 * X6 == 90614) - solver.Add(8874 * X0 + -58412 * X1 + 73947 * X2 + 17147 * X3 + 62335 * X4 + - 16005 * X5 + 8632 * X6 == 752447) - solver.Add(71202 * X0 + -11119 * X1 + 73017 * X2 + -38875 * X3 + -14413 * X4 + - -29234 * X5 + 72370 * X6 == 129768) - solver.Add(1671 * X0 + -34121 * X1 + 10763 * X2 + 80609 * X3 + 42532 * X4 + - 93520 * X5 + -33488 * X6 == 915683) - solver.Add(51637 * X0 + 67761 * X1 + 95951 * X2 + 3834 * X3 + -96722 * X4 + - 59190 * X5 + 15280 * X6 == 533909) - solver.Add(-16105 * X0 + 62397 * X1 + -6704 * X2 + 43340 * X3 + 95100 * X4 + - -68610 * X5 + 58301 * X6 == 876370) + solver.Add( + -76706 * X0 + + 98205 * X1 + + 23445 * X2 + + 67921 * X3 + + 24111 * X4 + + -48614 * X5 + + -41906 * X6 + == 821228 + ) + solver.Add( + 87059 * X0 + + -29101 * X1 + + -5513 * X2 + + -21219 * X3 + + 22128 * X4 + + 7276 * X5 + + 57308 * X6 + == 22167 + ) + solver.Add( + -60113 * X0 + + 29475 * X1 + + 34421 * X2 + + -76870 * X3 + + 62646 * X4 + + 29278 * X5 + + -15212 * X6 + == 251591 + ) + solver.Add( + 49149 * X0 + + 52871 * X1 + + -7132 * X2 + + 56728 * X3 + + -33576 * X4 + + -49530 * X5 + + -62089 * X6 + == 146074 + ) + solver.Add( + -10343 * X0 + + 87758 * X1 + + -11782 * X2 + + 19346 * X3 + + 70072 * X4 + + -36991 * X5 + + 44529 * X6 + == 740061 + ) + solver.Add( + 85176 * X0 + + -95332 * X1 + + -1268 * X2 + + 57898 * X3 + + 15883 * X4 + + 50547 * X5 + + 83287 * X6 + == 373854 + ) + solver.Add( + -85698 * X0 + + 29958 * X1 + + 57308 * X2 + + 48789 * X3 + + -78219 * X4 + + 4657 * X5 + + 34539 * X6 + == 249912 + ) + solver.Add( + -67456 * X0 + + 84750 * X1 + + -51553 * X2 + + 21239 * X3 + + 81675 * X4 + + -99395 * X5 + + -4254 * X6 + == 277271 + ) + solver.Add( + 94016 * X0 + + -82071 * X1 + + 35961 * X2 + + 66597 * X3 + + -30705 * X4 + + -44404 * X5 + + -38304 * X6 + == 25334 + ) + solver.Add( + -60301 * X0 + + 31227 * X1 + + 93951 * X2 + + 73889 * X3 + + 81526 * X4 + + -72702 * X5 + + 68026 * X6 + == 1410723 + ) + solver.Add( + -16835 * X0 + + 47385 * X1 + + 97715 * X2 + + -12640 * X3 + + 69028 * X4 + + 76212 * X5 + + -81102 * X6 + == 1244857 + ) + solver.Add( + -43277 * X0 + + 43525 * X1 + + 92298 * X2 + + 58630 * X3 + + 92590 * X4 + + -9372 * X5 + + -60227 * X6 + == 1503588 + ) + solver.Add( + -64919 * X0 + + 80460 * X1 + + 90840 * X2 + + -59624 * X3 + + -75542 * X4 + + 25145 * X5 + + -47935 * X6 + == 18465 + ) + solver.Add( + -45086 * X0 + + 51830 * X1 + + -4578 * X2 + + 96120 * X3 + + 21231 * X4 + + 97919 * X5 + + 65651 * X6 + == 1198280 + ) + solver.Add( + 85268 * X0 + + 54180 * X1 + + -18810 * X2 + + -48219 * X3 + + 6013 * X4 + + 78169 * X5 + + -79785 * X6 + == 90614 + ) + solver.Add( + 8874 * X0 + + -58412 * X1 + + 73947 * X2 + + 17147 * X3 + + 62335 * X4 + + 16005 * X5 + + 8632 * X6 + == 752447 + ) + solver.Add( + 71202 * X0 + + -11119 * X1 + + 73017 * X2 + + -38875 * X3 + + -14413 * X4 + + -29234 * X5 + + 72370 * X6 + == 129768 + ) + solver.Add( + 1671 * X0 + + -34121 * X1 + + 10763 * X2 + + 80609 * X3 + + 42532 * X4 + + 93520 * X5 + + -33488 * X6 + == 915683 + ) + solver.Add( + 51637 * X0 + + 67761 * X1 + + 95951 * X2 + + 3834 * X3 + + -96722 * X4 + + 59190 * X5 + + 15280 * X6 + == 533909 + ) + solver.Add( + -16105 * X0 + + 62397 * X1 + + -6704 * X2 + + 43340 * X3 + + 95100 * X4 + + -68610 * X5 + + 58301 * X6 + == 876370 + ) # # search and result diff --git a/examples/contrib/fill_a_pix.py b/examples/contrib/fill_a_pix.py index 4210701d2d5..82ec5d8b5f3 100644 --- a/examples/contrib/fill_a_pix.py +++ b/examples/contrib/fill_a_pix.py @@ -13,42 +13,42 @@ # limitations under the License. """ - Fill-a-Pix problem in Google CP Solver. - - From - http://www.conceptispuzzles.com/index.aspx?uri=puzzle/fill-a-pix/basiclogic - ''' - Each puzzle consists of a grid containing clues in various places. The - object is to reveal a hidden picture by painting the squares around each - clue so that the number of painted squares, including the square with - the clue, matches the value of the clue. - ''' - - http://www.conceptispuzzles.com/index.aspx?uri=puzzle/fill-a-pix/rules - ''' - Fill-a-Pix is a Minesweeper-like puzzle based on a grid with a pixilated - picture hidden inside. Using logic alone, the solver determines which - squares are painted and which should remain empty until the hidden picture - is completely exposed. - ''' - - Fill-a-pix History: - http://www.conceptispuzzles.com/index.aspx?uri=puzzle/fill-a-pix/history - - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/fill_a_pix.mzn - * SICStus Prolog: http://www.hakank.org/sicstus/fill_a_pix.pl - * ECLiPSe: http://hakank.org/eclipse/fill_a_pix.ecl - * Gecode: http://hakank.org/gecode/fill_a_pix.cpp - - And see the Minesweeper model: - * http://www.hakank.org/google_or_tools/minesweeper.py - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Fill-a-Pix problem in Google CP Solver. + +From +http://www.conceptispuzzles.com/index.aspx?uri=puzzle/fill-a-pix/basiclogic +''' +Each puzzle consists of a grid containing clues in various places. The +object is to reveal a hidden picture by painting the squares around each +clue so that the number of painted squares, including the square with +the clue, matches the value of the clue. +''' + +http://www.conceptispuzzles.com/index.aspx?uri=puzzle/fill-a-pix/rules +''' +Fill-a-Pix is a Minesweeper-like puzzle based on a grid with a pixilated +picture hidden inside. Using logic alone, the solver determines which +squares are painted and which should remain empty until the hidden picture +is completely exposed. +''' + +Fill-a-pix History: +http://www.conceptispuzzles.com/index.aspx?uri=puzzle/fill-a-pix/history + + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/fill_a_pix.mzn +* SICStus Prolog: http://www.hakank.org/sicstus/fill_a_pix.pl +* ECLiPSe: http://hakank.org/eclipse/fill_a_pix.ecl +* Gecode: http://hakank.org/gecode/fill_a_pix.cpp + +And see the Minesweeper model: +* http://www.hakank.org/google_or_tools/minesweeper.py + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -58,11 +58,16 @@ default_n = 10 X = -1 default_puzzle = [ - [X, X, X, X, X, X, X, X, 0, X], [X, 8, 8, X, 2, X, 0, X, X, X], - [5, X, 8, X, X, X, X, X, X, X], [X, X, X, X, X, 2, X, X, X, 2], - [1, X, X, X, 4, 5, 6, X, X, X], [X, 0, X, X, X, 7, 9, X, X, 6], - [X, X, X, 6, X, X, 9, X, X, 6], [X, X, 6, 6, 8, 7, 8, 7, X, 5], - [X, 4, X, 6, 6, 6, X, 6, X, 4], [X, X, X, X, X, X, 3, X, X, X] + [X, X, X, X, X, X, X, X, 0, X], + [X, 8, 8, X, 2, X, 0, X, X, X], + [5, X, 8, X, X, X, X, X, X, X], + [X, X, X, X, X, 2, X, X, X, 2], + [1, X, X, X, 4, 5, 6, X, X, X], + [X, 0, X, X, X, 7, 9, X, X, 6], + [X, X, X, 6, X, X, 9, X, X, 6], + [X, X, 6, 6, 8, 7, 8, 7, X, 5], + [X, 4, X, 6, 6, 6, X, 6, X, 4], + [X, X, X, X, X, X, 3, X, X, X], ] @@ -113,12 +118,15 @@ def main(puzzle='', n=''): for j in range(n): if puzzle[i][j] > X: # this cell is the sum of all the surrounding cells - solver.Add(puzzle[i][j] == solver.Sum([ - pict[i + a, j + b] - for a in S - for b in S - if i + a >= 0 and j + b >= 0 and i + a < n and j + b < n - ])) + solver.Add( + puzzle[i][j] + == solver.Sum([ + pict[i + a, j + b] + for a in S + for b in S + if i + a >= 0 and j + b >= 0 and i + a < n and j + b < n + ]) + ) # # solution and search diff --git a/examples/contrib/furniture_moving.py b/examples/contrib/furniture_moving.py index b8186ff6ab2..b4c19979fb1 100644 --- a/examples/contrib/furniture_moving.py +++ b/examples/contrib/furniture_moving.py @@ -13,27 +13,27 @@ # limitations under the License. """ - Moving furnitures (scheduling) problem in Google CP Solver. +Moving furnitures (scheduling) problem in Google CP Solver. - Marriott & Stukey: 'Programming with constraints', page 112f +Marriott & Stukey: 'Programming with constraints', page 112f - The model implements an experimental decomposition of the - global constraint cumulative. +The model implements an experimental decomposition of the +global constraint cumulative. - Compare with the following models: - * ECLiPSE: http://www.hakank.org/eclipse/furniture_moving.ecl - * MiniZinc: http://www.hakank.org/minizinc/furniture_moving.mzn - * Comet: http://www.hakank.org/comet/furniture_moving.co - * Choco: http://www.hakank.org/choco/FurnitureMoving.java - * Gecode: http://www.hakank.org/gecode/furniture_moving.cpp - * JaCoP: http://www.hakank.org/JaCoP/FurnitureMoving.java - * SICStus: http://hakank.org/sicstus/furniture_moving.pl - * Zinc: http://hakank.org/minizinc/furniture_moving.zinc +Compare with the following models: +* ECLiPSE: http://www.hakank.org/eclipse/furniture_moving.ecl +* MiniZinc: http://www.hakank.org/minizinc/furniture_moving.mzn +* Comet: http://www.hakank.org/comet/furniture_moving.co +* Choco: http://www.hakank.org/choco/FurnitureMoving.java +* Gecode: http://www.hakank.org/gecode/furniture_moving.cpp +* JaCoP: http://www.hakank.org/JaCoP/FurnitureMoving.java +* SICStus: http://hakank.org/sicstus/furniture_moving.pl +* Zinc: http://hakank.org/minizinc/furniture_moving.zinc - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -145,8 +145,9 @@ def main(): solution.Add(end_time) solution.Add(num_resources) - db = solver.Phase(start_times, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + start_times, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) # # result diff --git a/examples/contrib/futoshiki.py b/examples/contrib/futoshiki.py index 945cb6d5726..4202d34df58 100644 --- a/examples/contrib/futoshiki.py +++ b/examples/contrib/futoshiki.py @@ -13,36 +13,36 @@ # limitations under the License. """ - Futoshiki problem in Google CP Solver. +Futoshiki problem in Google CP Solver. - From http://en.wikipedia.org/wiki/Futoshiki - ''' - The puzzle is played on a square grid, such as 5 x 5. The objective - is to place the numbers 1 to 5 (or whatever the dimensions are) - such that each row, and column contains each of the digits 1 to 5. - Some digits may be given at the start. In addition, inequality - constraints are also initially specifed between some of the squares, - such that one must be higher or lower than its neighbour. These - constraints must be honoured as the grid is filled out. - ''' +From http://en.wikipedia.org/wiki/Futoshiki +''' +The puzzle is played on a square grid, such as 5 x 5. The objective +is to place the numbers 1 to 5 (or whatever the dimensions are) +such that each row, and column contains each of the digits 1 to 5. +Some digits may be given at the start. In addition, inequality +constraints are also initially specifed between some of the squares, +such that one must be higher or lower than its neighbour. These +constraints must be honoured as the grid is filled out. +''' - Also see - http://www.guardian.co.uk/world/2006/sep/30/japan.estheraddley +Also see +http://www.guardian.co.uk/world/2006/sep/30/japan.estheraddley - This Google CP Solver model is inspired by the Minion/Tailor - example futoshiki.eprime. +This Google CP Solver model is inspired by the Minion/Tailor +example futoshiki.eprime. - Compare with the following models: - * MiniZinc: http://hakank.org/minizinc/futoshiki.mzn - * ECLiPSe: http://hakank.org/eclipse/futoshiki.ecl - * Gecode: http://hakank.org/gecode/futoshiki.cpp - * SICStus: http://hakank.org/sicstus/futoshiki.pl +Compare with the following models: +* MiniZinc: http://hakank.org/minizinc/futoshiki.mzn +* ECLiPSe: http://hakank.org/eclipse/futoshiki.ecl +* Gecode: http://hakank.org/gecode/futoshiki.cpp +* SICStus: http://hakank.org/sicstus/futoshiki.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -89,13 +89,15 @@ def main(values, lt): # Also: make 0-based for i in NUMQD: solver.Add( - field[lt[i][0] - 1, lt[i][1] - 1] < field[lt[i][2] - 1, lt[i][3] - 1]) + field[lt[i][0] - 1, lt[i][1] - 1] < field[lt[i][2] - 1, lt[i][3] - 1] + ) # # search and result # - db = solver.Phase(field_flat, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + field_flat, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) @@ -128,14 +130,29 @@ def main(values, lt): # Futoshiki instance, by Andras Salamon # specify the numbers in the grid # -values1 = [[0, 0, 3, 2, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], - [0, 0, 0, 0, 0]] +values1 = [ + [0, 0, 3, 2, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], +] # [i1,j1, i2,j2] requires that values[i1,j1] < values[i2,j2] # Note: 1-based -lt1 = [[1, 2, 1, 1], [1, 4, 1, 5], [2, 3, 1, 3], [3, 3, 2, 3], [3, 4, 2, 4], - [2, 5, 3, 5], [3, 2, 4, 2], [4, 4, 4, 3], [5, 2, 5, 1], [5, 4, 5, 3], - [5, 5, 4, 5]] +lt1 = [ + [1, 2, 1, 1], + [1, 4, 1, 5], + [2, 3, 1, 3], + [3, 3, 2, 3], + [3, 4, 2, 4], + [2, 5, 3, 5], + [3, 2, 4, 2], + [4, 4, 4, 3], + [5, 2, 5, 1], + [5, 4, 5, 3], + [5, 5, 4, 5], +] # # Example from http://en.wikipedia.org/wiki/Futoshiki @@ -146,12 +163,23 @@ def main(values, lt): # 3 5 2 1 4 # 1 2 5 4 3 # -values2 = [[0, 0, 0, 0, 0], [4, 0, 0, 0, 2], [0, 0, 4, 0, 0], [0, 0, 0, 0, 4], - [0, 0, 0, 0, 0]] +values2 = [ + [0, 0, 0, 0, 0], + [4, 0, 0, 0, 2], + [0, 0, 4, 0, 0], + [0, 0, 0, 0, 4], + [0, 0, 0, 0, 0], +] # Note: 1-based -lt2 = [[1, 2, 1, 1], [1, 4, 1, 3], [1, 5, 1, 4], [4, 4, 4, 5], [5, 1, 5, 2], - [5, 2, 5, 3]] +lt2 = [ + [1, 2, 1, 1], + [1, 4, 1, 3], + [1, 5, 1, 4], + [4, 4, 4, 5], + [5, 1, 5, 2], + [5, 2, 5, 3], +] if __name__ == "__main__": print("Problem 1") diff --git a/examples/contrib/game_theory_taha.py b/examples/contrib/game_theory_taha.py index 9f88819f914..361ec04fa6c 100644 --- a/examples/contrib/game_theory_taha.py +++ b/examples/contrib/game_theory_taha.py @@ -13,15 +13,15 @@ # limitations under the License. """ - Game theory in Google or-tools. +Game theory in Google or-tools. - 2 player zero sum game. +2 player zero sum game. - From Taha, Operations Research (8'th edition), page 528. +From Taha, Operations Research (8'th edition), page 528. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp diff --git a/examples/contrib/grocery.py b/examples/contrib/grocery.py index 89ebe7faa04..d55a28dab0d 100644 --- a/examples/contrib/grocery.py +++ b/examples/contrib/grocery.py @@ -13,27 +13,27 @@ # limitations under the License. """ - Grocery problem in Google CP Solver. - - From Christian Schulte, Gert Smolka, Finite Domain - http://www.mozart-oz.org/documentation/fdt/ - Constraint Programming in Oz. A Tutorial. 2001. - ''' - A kid goes into a grocery store and buys four items. The cashier - charges $7.11, the kid pays and is about to leave when the cashier - calls the kid back, and says 'Hold on, I multiplied the four items - instead of adding them; I'll try again; Hah, with adding them the - price still comes to $7.11'. What were the prices of the four items? - ''' - - Compare with the following models: - * MiniZinc: http://hakank.org/minizinc/grocery.mzn - * Comet: http://hakank.org/comet/grocery.co - * Zinc: http://hakank.org/minizinc/grocery.zinc - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Grocery problem in Google CP Solver. + +From Christian Schulte, Gert Smolka, Finite Domain +http://www.mozart-oz.org/documentation/fdt/ +Constraint Programming in Oz. A Tutorial. 2001. +''' +A kid goes into a grocery store and buys four items. The cashier +charges $7.11, the kid pays and is about to leave when the cashier +calls the kid back, and says 'Hold on, I multiplied the four items +instead of adding them; I'll try again; Hah, with adding them the +price still comes to $7.11'. What were the prices of the four items? +''' + +Compare with the following models: +* MiniZinc: http://hakank.org/minizinc/grocery.mzn +* Comet: http://hakank.org/comet/grocery.co +* Zinc: http://hakank.org/minizinc/grocery.zinc + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys diff --git a/examples/contrib/hidato.py b/examples/contrib/hidato.py index faeeb56c542..618292e7e57 100644 --- a/examples/contrib/hidato.py +++ b/examples/contrib/hidato.py @@ -12,32 +12,32 @@ # See the License for the specific language governing permissions and # limitations under the License. """ - Hidato puzzle in Google CP Solver. - - http://www.shockwave.com/gamelanding/hidato.jsp - http://www.hidato.com/ - ''' - Puzzles start semi-filled with numbered tiles. - The first and last numbers are circled. - Connect the numbers together to win. Consecutive - number must touch horizontally, vertically, or - diagonally. - ''' - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/hidato.mzn - * Gecode : http://www.hakank.org/gecode/hidato.cpp - * Comet : http://www.hakank.org/comet/hidato.co - * Tailopr/Essence': http://hakank.org/tailor/hidato.eprime - * ECLiPSe: http://hakank.org/eclipse/hidato.ecl - * SICStus: http://hakank.org/sicstus/hidato.pl - - Note: This model is very slow. Please see Laurent Perron's much faster - (and more elegant) model: hidato_table.py . - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Hidato puzzle in Google CP Solver. + +http://www.shockwave.com/gamelanding/hidato.jsp +http://www.hidato.com/ +''' +Puzzles start semi-filled with numbered tiles. +The first and last numbers are circled. +Connect the numbers together to win. Consecutive +number must touch horizontally, vertically, or +diagonally. +''' + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/hidato.mzn +* Gecode : http://www.hakank.org/gecode/hidato.cpp +* Comet : http://www.hakank.org/comet/hidato.co +* Tailopr/Essence': http://hakank.org/tailor/hidato.eprime +* ECLiPSe: http://hakank.org/eclipse/hidato.ecl +* SICStus: http://hakank.org/sicstus/hidato.pl + +Note: This model is very slow. Please see Laurent Perron's much faster + (and more elegant) model: hidato_table.py . + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -53,10 +53,15 @@ def main(r, c): puzzle = [[6, 0, 9], [0, 2, 8], [1, 0, 0]] if r == 7 and c == 7: - puzzle = [[0, 44, 41, 0, 0, 0, 0], [0, 43, 0, 28, 29, 0, 0], - [0, 1, 0, 0, 0, 33, 0], [0, 2, 25, 4, 34, 0, 36], - [49, 16, 0, 23, 0, 0, 0], [0, 19, 0, 0, 12, 7, 0], - [0, 0, 0, 14, 0, 0, 0]] + puzzle = [ + [0, 44, 41, 0, 0, 0, 0], + [0, 43, 0, 28, 29, 0, 0], + [0, 1, 0, 0, 0, 33, 0], + [0, 2, 25, 4, 34, 0, 36], + [49, 16, 0, 23, 0, 0, 0], + [0, 19, 0, 0, 12, 7, 0], + [0, 0, 0, 14, 0, 0, 0], + ] # Problems from the book: # Gyora Bededek: "Hidato: 2000 Pure Logic Puzzles" @@ -84,16 +89,28 @@ def main(r, c): # Problem 3 (Beginner) if r == 6 and c == 6: - puzzle = [[0, 26, 0, 0, 0, 18], [0, 0, 27, 0, 0, 19], [31, 23, 0, 0, 14, 0], - [0, 33, 8, 0, 15, 1], [0, 0, 0, 5, 0, 0], [35, 36, 0, 10, 0, 0]] + puzzle = [ + [0, 26, 0, 0, 0, 18], + [0, 0, 27, 0, 0, 19], + [31, 23, 0, 0, 14, 0], + [0, 33, 8, 0, 15, 1], + [0, 0, 0, 5, 0, 0], + [35, 36, 0, 10, 0, 0], + ] # Problem 15 (Intermediate) # Note: This takes very long time to solve... if r == 8 and c == 8: - puzzle = [[64, 0, 0, 0, 0, 0, 0, 0], [1, 63, 0, 59, 15, 57, 53, 0], - [0, 4, 0, 14, 0, 0, 0, 0], [3, 0, 11, 0, 20, 19, 0, 50], - [0, 0, 0, 0, 22, 0, 48, 40], [9, 0, 0, 32, 23, 0, 0, 41], - [27, 0, 0, 0, 36, 0, 46, 0], [28, 30, 0, 35, 0, 0, 0, 0]] + puzzle = [ + [64, 0, 0, 0, 0, 0, 0, 0], + [1, 63, 0, 59, 15, 57, 53, 0], + [0, 4, 0, 14, 0, 0, 0, 0], + [3, 0, 11, 0, 20, 19, 0, 50], + [0, 0, 0, 0, 22, 0, 48, 40], + [9, 0, 0, 32, 23, 0, 0, 41], + [27, 0, 0, 0, 36, 0, 46, 0], + [28, 30, 0, 35, 0, 0, 0, 0], + ] print_game(puzzle, r, c) @@ -169,7 +186,8 @@ def main(r, c): # solver.ASSIGN_MAX_VALUE # solver.ASSIGN_RANDOM_VALUE # solver.ASSIGN_CENTER_VALUE - solver.ASSIGN_MIN_VALUE) + solver.ASSIGN_MIN_VALUE, + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/contrib/just_forgotten.py b/examples/contrib/just_forgotten.py index fc87c7508ff..31e13000491 100644 --- a/examples/contrib/just_forgotten.py +++ b/examples/contrib/just_forgotten.py @@ -13,36 +13,36 @@ # limitations under the License. """ - Just forgotten puzzle (Enigma 1517) in Google CP Solver. - - From http://www.f1compiler.com/samples/Enigma 201517.f1.html - ''' - Enigma 1517 Bob Walker, New Scientist magazine, October 25, 2008. - - Joe was furious when he forgot one of his bank account numbers. - He remembered that it had all the digits 0 to 9 in some order, - so he tried the following four sets without success: - - 9 4 6 2 1 5 7 8 3 0 - 8 6 0 4 3 9 1 2 5 7 - 1 6 4 0 2 9 7 8 5 3 - 6 8 2 4 3 1 9 0 7 5 - - When Joe finally remembered his account number, he realised that - in each set just four of the digits were in their correct position - and that, if one knew that, it was possible to work out his - account number. What was it? - ''' - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/just_forgotten.mzn - * SICStus Prolog: http://www.hakank.org/sicstis/just_forgotten.pl - * ECLiPSe: http://hakank.org/eclipse/just_forgotten.ecl - * Gecpde: http://hakank.org/gecode/just_forgotten.cpp - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Just forgotten puzzle (Enigma 1517) in Google CP Solver. + +From http://www.f1compiler.com/samples/Enigma 201517.f1.html +''' +Enigma 1517 Bob Walker, New Scientist magazine, October 25, 2008. + +Joe was furious when he forgot one of his bank account numbers. +He remembered that it had all the digits 0 to 9 in some order, +so he tried the following four sets without success: + + 9 4 6 2 1 5 7 8 3 0 + 8 6 0 4 3 9 1 2 5 7 + 1 6 4 0 2 9 7 8 5 3 + 6 8 2 4 3 1 9 0 7 5 + +When Joe finally remembered his account number, he realised that +in each set just four of the digits were in their correct position +and that, if one knew that, it was possible to work out his +account number. What was it? +''' + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/just_forgotten.mzn +* SICStus Prolog: http://www.hakank.org/sicstis/just_forgotten.pl +* ECLiPSe: http://hakank.org/eclipse/just_forgotten.ecl +* Gecpde: http://hakank.org/gecode/just_forgotten.cpp + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -58,8 +58,12 @@ def main(): rows = 4 cols = 10 # The four tries - a = [[9, 4, 6, 2, 1, 5, 7, 8, 3, 0], [8, 6, 0, 4, 3, 9, 1, 2, 5, 7], - [1, 6, 4, 0, 2, 9, 7, 8, 5, 3], [6, 8, 2, 4, 3, 1, 9, 0, 7, 5]] + a = [ + [9, 4, 6, 2, 1, 5, 7, 8, 3, 0], + [8, 6, 0, 4, 3, 9, 1, 2, 5, 7], + [1, 6, 4, 0, 2, 9, 7, 8, 5, 3], + [6, 8, 2, 4, 3, 1, 9, 0, 7, 5], + ] # # variables diff --git a/examples/contrib/kakuro.py b/examples/contrib/kakuro.py index 8ab80e53d9e..58681d6fe7e 100644 --- a/examples/contrib/kakuro.py +++ b/examples/contrib/kakuro.py @@ -13,46 +13,46 @@ # limitations under the License. """ - Kakuru puzzle in Google CP Solver. - - http://en.wikipedia.org/wiki/Kakuro - ''' - The object of the puzzle is to insert a digit from 1 to 9 inclusive - into each white cell such that the sum of the numbers in each entry - matches the clue associated with it and that no digit is duplicated in - any entry. It is that lack of duplication that makes creating Kakuro - puzzles with unique solutions possible, and which means solving a Kakuro - puzzle involves investigating combinations more, compared to Sudoku in - which the focus is on permutations. There is an unwritten rule for - making Kakuro puzzles that each clue must have at least two numbers - that add up to it. This is because including one number is mathematically - trivial when solving Kakuro puzzles; one can simply disregard the - number entirely and subtract it from the clue it indicates. - ''' - - This model solves the problem at the Wikipedia page. - For a larger picture, see - http://en.wikipedia.org/wiki/File:Kakuro_black_box.svg - - The solution: - 9 7 0 0 8 7 9 - 8 9 0 8 9 5 7 - 6 8 5 9 7 0 0 - 0 6 1 0 2 6 0 - 0 0 4 6 1 3 2 - 8 9 3 1 0 1 4 - 3 1 2 0 0 2 1 - - Compare with the following models: - * Comet : http://www.hakank.org/comet/kakuro.co - * MiniZinc: http://www.hakank.org/minizinc/kakuro.mzn - * SICStus : http://www.hakank.org/sicstus/kakuro.pl - * ECLiPSe: http://www.hakank.org/eclipse/kakuro.ecl - * Gecode: http://www.hakank.org/gecode/kenken2.cpp - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Kakuru puzzle in Google CP Solver. + +http://en.wikipedia.org/wiki/Kakuro +''' +The object of the puzzle is to insert a digit from 1 to 9 inclusive +into each white cell such that the sum of the numbers in each entry +matches the clue associated with it and that no digit is duplicated in +any entry. It is that lack of duplication that makes creating Kakuro +puzzles with unique solutions possible, and which means solving a Kakuro +puzzle involves investigating combinations more, compared to Sudoku in +which the focus is on permutations. There is an unwritten rule for +making Kakuro puzzles that each clue must have at least two numbers +that add up to it. This is because including one number is mathematically +trivial when solving Kakuro puzzles; one can simply disregard the +number entirely and subtract it from the clue it indicates. +''' + +This model solves the problem at the Wikipedia page. +For a larger picture, see +http://en.wikipedia.org/wiki/File:Kakuro_black_box.svg + +The solution: + 9 7 0 0 8 7 9 + 8 9 0 8 9 5 7 + 6 8 5 9 7 0 0 + 0 6 1 0 2 6 0 + 0 0 4 6 1 3 2 + 8 9 3 1 0 1 4 + 3 1 2 0 0 2 1 + +Compare with the following models: +* Comet : http://www.hakank.org/comet/kakuro.co +* MiniZinc: http://www.hakank.org/minizinc/kakuro.mzn +* SICStus : http://www.hakank.org/sicstus/kakuro.pl +* ECLiPSe: http://www.hakank.org/eclipse/kakuro.ecl +* Gecode: http://www.hakank.org/gecode/kenken2.cpp + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -91,26 +91,52 @@ def main(): # segments # [sum, [segments]] # Note: 1-based - problem = [[16, [1, 1], [1, 2]], [24, [1, 5], [1, 6], [1, 7]], - [17, [2, 1], [2, 2]], [29, [2, 4], [2, 5], [2, 6], [2, 7]], - [35, [3, 1], [3, 2], [3, 3], [3, 4], [3, 5]], [7, [4, 2], [4, 3]], - [8, [4, 5], [4, 6]], [16, [5, 3], [5, 4], [5, 5], [5, 6], [5, 7]], - [21, [6, 1], [6, 2], [6, 3], [6, 4]], [5, [6, 6], [6, 7]], - [6, [7, 1], [7, 2], [7, 3]], [3, [7, 6], [7, 7]], - [23, [1, 1], [2, 1], [3, 1]], [30, [1, 2], [2, 2], [3, 2], [4, 2]], - [27, [1, 5], [2, 5], [3, 5], [4, 5], [5, 5]], [12, [1, 6], [2, 6]], - [16, [1, 7], [2, 7]], [17, [2, 4], [3, 4]], - [15, [3, 3], [4, 3], [5, 3], [6, 3], [7, 3]], - [12, [4, 6], [5, 6], [6, 6], [7, 6]], [7, [5, 4], [6, 4]], - [7, [5, 7], [6, 7], [7, 7]], [11, [6, 1], [7, 1]], - [10, [6, 2], [7, 2]]] + problem = [ + [16, [1, 1], [1, 2]], + [24, [1, 5], [1, 6], [1, 7]], + [17, [2, 1], [2, 2]], + [29, [2, 4], [2, 5], [2, 6], [2, 7]], + [35, [3, 1], [3, 2], [3, 3], [3, 4], [3, 5]], + [7, [4, 2], [4, 3]], + [8, [4, 5], [4, 6]], + [16, [5, 3], [5, 4], [5, 5], [5, 6], [5, 7]], + [21, [6, 1], [6, 2], [6, 3], [6, 4]], + [5, [6, 6], [6, 7]], + [6, [7, 1], [7, 2], [7, 3]], + [3, [7, 6], [7, 7]], + [23, [1, 1], [2, 1], [3, 1]], + [30, [1, 2], [2, 2], [3, 2], [4, 2]], + [27, [1, 5], [2, 5], [3, 5], [4, 5], [5, 5]], + [12, [1, 6], [2, 6]], + [16, [1, 7], [2, 7]], + [17, [2, 4], [3, 4]], + [15, [3, 3], [4, 3], [5, 3], [6, 3], [7, 3]], + [12, [4, 6], [5, 6], [6, 6], [7, 6]], + [7, [5, 4], [6, 4]], + [7, [5, 7], [6, 7], [7, 7]], + [11, [6, 1], [7, 1]], + [10, [6, 2], [7, 2]], + ] num_p = len(problem) # The blanks # Note: 1-based - blanks = [[1, 3], [1, 4], [2, 3], [3, 6], [3, 7], [4, 1], [4, 4], [4, 7], - [5, 1], [5, 2], [6, 5], [7, 4], [7, 5]] + blanks = [ + [1, 3], + [1, 4], + [2, 3], + [3, 6], + [3, 7], + [4, 1], + [4, 4], + [4, 7], + [5, 1], + [5, 2], + [6, 5], + [7, 4], + [7, 5], + ] num_blanks = len(blanks) # diff --git a/examples/contrib/kenken2.py b/examples/contrib/kenken2.py index 454f20eb314..a97c0db17b1 100644 --- a/examples/contrib/kenken2.py +++ b/examples/contrib/kenken2.py @@ -13,47 +13,47 @@ # limitations under the License. """ - KenKen puzzle in Google CP Solver. - - http://en.wikipedia.org/wiki/KenKen - ''' - KenKen or KEN-KEN is a style of arithmetic and logical puzzle sharing - several characteristics with sudoku. The name comes from Japanese and - is translated as 'square wisdom' or 'cleverness squared'. - ... - The objective is to fill the grid in with the digits 1 through 6 such that: - - * Each row contains exactly one of each digit - * Each column contains exactly one of each digit - * Each bold-outlined group of cells is a cage containing digits which - achieve the specified result using the specified mathematical operation: - addition (+), - subtraction (-), - multiplication (x), - and division (/). - (Unlike in Killer sudoku, digits may repeat within a group.) - - ... - More complex KenKen problems are formed using the principles described - above but omitting the symbols +, -, x and /, thus leaving them as - yet another unknown to be determined. - ''' - - - The solution is: - - 5 6 3 4 1 2 - 6 1 4 5 2 3 - 4 5 2 3 6 1 - 3 4 1 2 5 6 - 2 3 6 1 4 5 - 1 2 5 6 3 4 - - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +KenKen puzzle in Google CP Solver. + +http://en.wikipedia.org/wiki/KenKen +''' +KenKen or KEN-KEN is a style of arithmetic and logical puzzle sharing +several characteristics with sudoku. The name comes from Japanese and +is translated as 'square wisdom' or 'cleverness squared'. +... +The objective is to fill the grid in with the digits 1 through 6 such that: + + * Each row contains exactly one of each digit + * Each column contains exactly one of each digit + * Each bold-outlined group of cells is a cage containing digits which + achieve the specified result using the specified mathematical operation: + addition (+), + subtraction (-), + multiplication (x), + and division (/). + (Unlike in Killer sudoku, digits may repeat within a group.) + +... +More complex KenKen problems are formed using the principles described +above but omitting the symbols +, -, x and /, thus leaving them as +yet another unknown to be determined. +''' + + +The solution is: + + 5 6 3 4 1 2 + 6 1 4 5 2 3 + 4 5 2 3 6 1 + 3 4 1 2 5 6 + 2 3 6 1 4 5 + 1 2 5 6 3 4 + + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -124,14 +124,23 @@ def main(): # hints # [sum, [segments]] # Note: 1-based - problem = [[11, [[1, 1], [2, 1]]], [2, [[1, 2], [1, 3]]], - [20, [[1, 4], [2, 4]]], [6, [[1, 5], [1, 6], [2, 6], [3, 6]]], - [3, [[2, 2], [2, 3]]], [3, [[2, 5], [3, 5]]], - [240, [[3, 1], [3, 2], [4, 1], [4, 2]]], [6, [[3, 3], [3, 4]]], - [6, [[4, 3], [5, 3]]], [7, [[4, 4], [5, 4], [5, 5]]], - [30, [[4, 5], [4, 6]]], [6, [[5, 1], [5, 2]]], - [9, [[5, 6], [6, 6]]], [8, [[6, 1], [6, 2], [6, 3]]], - [2, [[6, 4], [6, 5]]]] + problem = [ + [11, [[1, 1], [2, 1]]], + [2, [[1, 2], [1, 3]]], + [20, [[1, 4], [2, 4]]], + [6, [[1, 5], [1, 6], [2, 6], [3, 6]]], + [3, [[2, 2], [2, 3]]], + [3, [[2, 5], [3, 5]]], + [240, [[3, 1], [3, 2], [4, 1], [4, 2]]], + [6, [[3, 3], [3, 4]]], + [6, [[4, 3], [5, 3]]], + [7, [[4, 4], [5, 4], [5, 5]]], + [30, [[4, 5], [4, 6]]], + [6, [[5, 1], [5, 2]]], + [9, [[5, 6], [6, 6]]], + [8, [[6, 1], [6, 2], [6, 3]]], + [2, [[6, 4], [6, 5]]], + ] num_p = len(problem) @@ -160,7 +169,7 @@ def main(): solver.Add(solver.AllDifferent(col)) # calculate the segments - for (res, segment) in problem: + for res, segment in problem: calc(segment, x, res) # diff --git a/examples/contrib/killer_sudoku.py b/examples/contrib/killer_sudoku.py index 71e713e4a3a..77d6eae9257 100644 --- a/examples/contrib/killer_sudoku.py +++ b/examples/contrib/killer_sudoku.py @@ -13,58 +13,58 @@ # limitations under the License. """ - Killer Sudoku in Google CP Solver. - - http://en.wikipedia.org/wiki/Killer_Sudoku - ''' - Killer sudoku (also killer su doku, sumdoku, sum doku, addoku, or - samunamupure) is a puzzle that combines elements of sudoku and kakuro. - Despite the name, the simpler killer sudokus can be easier to solve - than regular sudokus, depending on the solver's skill at mental arithmetic; - the hardest ones, however, can take hours to crack. - - ... - - The objective is to fill the grid with numbers from 1 to 9 in a way that - the following conditions are met: - - * Each row, column, and nonet contains each number exactly once. - * The sum of all numbers in a cage must match the small number printed - in its corner. - * No number appears more than once in a cage. (This is the standard rule - for killer sudokus, and implies that no cage can include more - than 9 cells.) - - In 'Killer X', an additional rule is that each of the long diagonals - contains each number once. - ''' - - Here we solve the problem from the Wikipedia page, also shown here - http://en.wikipedia.org/wiki/File:Killersudoku_color.svg - - The output is: - 2 1 5 6 4 7 3 9 8 - 3 6 8 9 5 2 1 7 4 - 7 9 4 3 8 1 6 5 2 - 5 8 6 2 7 4 9 3 1 - 1 4 2 5 9 3 8 6 7 - 9 7 3 8 1 6 4 2 5 - 8 2 1 7 3 9 5 4 6 - 6 5 9 4 2 8 7 1 3 - 4 3 7 1 6 5 2 8 9 - - - Compare with the following models: - * Comet : http://www.hakank.org/comet/killer_sudoku.co - * MiniZinc: http://www.hakank.org/minizinc/killer_sudoku.mzn - * SICStus: http://www.hakank.org/sicstus/killer_sudoku.pl - * ECLiPSE: http://www.hakank.org/eclipse/killer_sudoku.ecl - * Gecode: http://www.hakank.org/gecode/killer_sudoku.cpp - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Killer Sudoku in Google CP Solver. + +http://en.wikipedia.org/wiki/Killer_Sudoku +''' +Killer sudoku (also killer su doku, sumdoku, sum doku, addoku, or +samunamupure) is a puzzle that combines elements of sudoku and kakuro. +Despite the name, the simpler killer sudokus can be easier to solve +than regular sudokus, depending on the solver's skill at mental arithmetic; +the hardest ones, however, can take hours to crack. + +... + +The objective is to fill the grid with numbers from 1 to 9 in a way that +the following conditions are met: + + * Each row, column, and nonet contains each number exactly once. + * The sum of all numbers in a cage must match the small number printed + in its corner. + * No number appears more than once in a cage. (This is the standard rule + for killer sudokus, and implies that no cage can include more + than 9 cells.) + +In 'Killer X', an additional rule is that each of the long diagonals +contains each number once. +''' + +Here we solve the problem from the Wikipedia page, also shown here +http://en.wikipedia.org/wiki/File:Killersudoku_color.svg + +The output is: + 2 1 5 6 4 7 3 9 8 + 3 6 8 9 5 2 1 7 4 + 7 9 4 3 8 1 6 5 2 + 5 8 6 2 7 4 9 3 1 + 1 4 2 5 9 3 8 6 7 + 9 7 3 8 1 6 4 2 5 + 8 2 1 7 3 9 5 4 6 + 6 5 9 4 2 8 7 1 3 + 4 3 7 1 6 5 2 8 9 + + +Compare with the following models: +* Comet : http://www.hakank.org/comet/killer_sudoku.co +* MiniZinc: http://www.hakank.org/minizinc/killer_sudoku.mzn +* SICStus: http://www.hakank.org/sicstus/killer_sudoku.pl +* ECLiPSE: http://www.hakank.org/eclipse/killer_sudoku.ecl +* Gecode: http://www.hakank.org/gecode/killer_sudoku.cpp + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -104,21 +104,37 @@ def main(): # hints # [sum, [segments]] # Note: 1-based - problem = [[3, [[1, 1], [1, 2]]], [15, [[1, 3], [1, 4], [1, 5]]], - [22, [[1, 6], [2, 5], [2, 6], [3, 5]]], [4, [[1, 7], [2, 7]]], - [16, [[1, 8], [2, 8]]], [15, [[1, 9], [2, 9], [3, 9], [4, 9]]], - [25, [[2, 1], [2, 2], [3, 1], [3, 2]]], [17, [[2, 3], [2, 4]]], - [9, [[3, 3], [3, 4], [4, 4]]], [8, [[3, 6], [4, 6], [5, 6]]], - [20, [[3, 7], [3, 8], [4, 7]]], [6, [[4, 1], [5, 1]]], - [14, [[4, 2], [4, 3]]], [17, [[4, 5], [5, 5], [6, 5]]], - [17, [[4, 8], [5, 7], [5, 8]]], [13, [[5, 2], [5, 3], [6, 2]]], - [20, [[5, 4], [6, 4], [7, 4]]], [12, [[5, 9], [6, 9]]], - [27, [[6, 1], [7, 1], [8, 1], [9, 1]]], - [6, [[6, 3], [7, 2], [7, 3]]], [20, [[6, 6], [7, 6], [7, 7]]], - [6, [[6, 7], [6, 8]]], [10, [[7, 5], [8, 4], [8, 5], [9, 4]]], - [14, [[7, 8], [7, 9], [8, 8], [8, 9]]], [8, [[8, 2], [9, 2]]], - [16, [[8, 3], [9, 3]]], [15, [[8, 6], [8, 7]]], - [13, [[9, 5], [9, 6], [9, 7]]], [17, [[9, 8], [9, 9]]]] + problem = [ + [3, [[1, 1], [1, 2]]], + [15, [[1, 3], [1, 4], [1, 5]]], + [22, [[1, 6], [2, 5], [2, 6], [3, 5]]], + [4, [[1, 7], [2, 7]]], + [16, [[1, 8], [2, 8]]], + [15, [[1, 9], [2, 9], [3, 9], [4, 9]]], + [25, [[2, 1], [2, 2], [3, 1], [3, 2]]], + [17, [[2, 3], [2, 4]]], + [9, [[3, 3], [3, 4], [4, 4]]], + [8, [[3, 6], [4, 6], [5, 6]]], + [20, [[3, 7], [3, 8], [4, 7]]], + [6, [[4, 1], [5, 1]]], + [14, [[4, 2], [4, 3]]], + [17, [[4, 5], [5, 5], [6, 5]]], + [17, [[4, 8], [5, 7], [5, 8]]], + [13, [[5, 2], [5, 3], [6, 2]]], + [20, [[5, 4], [6, 4], [7, 4]]], + [12, [[5, 9], [6, 9]]], + [27, [[6, 1], [7, 1], [8, 1], [9, 1]]], + [6, [[6, 3], [7, 2], [7, 3]]], + [20, [[6, 6], [7, 6], [7, 7]]], + [6, [[6, 7], [6, 8]]], + [10, [[7, 5], [8, 4], [8, 5], [9, 4]]], + [14, [[7, 8], [7, 9], [8, 8], [8, 9]]], + [8, [[8, 2], [9, 2]]], + [16, [[8, 3], [9, 3]]], + [15, [[8, 6], [8, 7]]], + [13, [[9, 5], [9, 6], [9, 7]]], + [17, [[9, 8], [9, 9]]], + ] # # variables @@ -155,7 +171,7 @@ def main(): solver.Add(solver.AllDifferent(cell)) # calculate the segments - for (res, segment) in problem: + for res, segment in problem: calc(segment, x, res) # diff --git a/examples/contrib/knapsack_cp.py b/examples/contrib/knapsack_cp.py index b6208c6b0d6..c4b0fbc6336 100644 --- a/examples/contrib/knapsack_cp.py +++ b/examples/contrib/knapsack_cp.py @@ -13,13 +13,13 @@ # limitations under the License. """ - Knapsack problem in Google CP Solver. +Knapsack problem in Google CP Solver. - Simple knapsack problem. +Simple knapsack problem. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp diff --git a/examples/contrib/knapsack_mip.py b/examples/contrib/knapsack_mip.py index d1c08e24c9e..ba478f71175 100644 --- a/examples/contrib/knapsack_mip.py +++ b/examples/contrib/knapsack_mip.py @@ -13,13 +13,13 @@ # limitations under the License. """ - Knapsack problem using MIP in Google or-tools. +Knapsack problem using MIP in Google or-tools. - From the OPL model knapsack.mod +From the OPL model knapsack.mod - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp @@ -43,13 +43,15 @@ def main(sol='CBC'): capacity = [18209, 7692, 1333, 924, 26638, 61188, 13360] value = [96, 76, 56, 11, 86, 10, 66, 86, 83, 12, 9, 81] - use = [[19, 1, 10, 1, 1, 14, 152, 11, 1, 1, 1, 1], - [0, 4, 53, 0, 0, 80, 0, 4, 5, 0, 0, 0], - [4, 660, 3, 0, 30, 0, 3, 0, 4, 90, 0, 0], - [7, 0, 18, 6, 770, 330, 7, 0, 0, 6, 0, 0], - [0, 20, 0, 4, 52, 3, 0, 0, 0, 5, 4, 0], - [0, 0, 40, 70, 4, 63, 0, 0, 60, 0, 4, 0], - [0, 32, 0, 0, 0, 5, 0, 3, 0, 660, 0, 9]] + use = [ + [19, 1, 10, 1, 1, 14, 152, 11, 1, 1, 1, 1], + [0, 4, 53, 0, 0, 80, 0, 4, 5, 0, 0, 0], + [4, 660, 3, 0, 30, 0, 3, 0, 4, 90, 0, 0], + [7, 0, 18, 6, 770, 330, 7, 0, 0, 6, 0, 0], + [0, 20, 0, 4, 52, 3, 0, 0, 0, 5, 4, 0], + [0, 0, 40, 70, 4, 63, 0, 0, 60, 0, 4, 0], + [0, 32, 0, 0, 0, 5, 0, 3, 0, 660, 0, 9], + ] max_value = max(capacity) diff --git a/examples/contrib/labeled_dice.py b/examples/contrib/labeled_dice.py index 45b7157b889..ea0b2dd394d 100644 --- a/examples/contrib/labeled_dice.py +++ b/examples/contrib/labeled_dice.py @@ -13,38 +13,38 @@ # limitations under the License. """ - Labeled dice problem in Google CP Solver. - - From Jim Orlin 'Colored letters, labeled dice: a logic puzzle' - http://jimorlin.wordpress.com/2009/02/17/colored-letters-labeled-dice-a-logic-puzzle/ - ''' - My daughter Jenn bough a puzzle book, and showed me a cute puzzle. There - are 13 words as follows: BUOY, CAVE, CELT, FLUB, FORK, HEMP, JUDY, - JUNK, LIMN, QUIP, SWAG, VISA, WISH. - - There are 24 different letters that appear in the 13 words. The question - is: can one assign the 24 letters to 4 different cubes so that the - four letters of each word appears on different cubes. (There is one - letter from each word on each cube.) It might be fun for you to try - it. I'll give a small hint at the end of this post. The puzzle was - created by Humphrey Dudley. - ''' - - Jim Orlin's followup 'Update on Logic Puzzle': - http://jimorlin.wordpress.com/2009/02/21/update-on-logic-puzzle/ - - - Compare with the following models: - * ECLiPSe: http://hakank.org/eclipse/labeled_dice.ecl - * Comet : http://www.hakank.org/comet/labeled_dice.co - * Gecode : http://hakank.org/gecode/labeled_dice.cpp - * SICStus: http://hakank.org/sicstus/labeled_dice.pl - * Zinc : http://hakank.org/minizinc/labeled_dice.zinc - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Labeled dice problem in Google CP Solver. + +From Jim Orlin 'Colored letters, labeled dice: a logic puzzle' +http://jimorlin.wordpress.com/2009/02/17/colored-letters-labeled-dice-a-logic-puzzle/ +''' +My daughter Jenn bough a puzzle book, and showed me a cute puzzle. There +are 13 words as follows: BUOY, CAVE, CELT, FLUB, FORK, HEMP, JUDY, +JUNK, LIMN, QUIP, SWAG, VISA, WISH. + +There are 24 different letters that appear in the 13 words. The question +is: can one assign the 24 letters to 4 different cubes so that the +four letters of each word appears on different cubes. (There is one +letter from each word on each cube.) It might be fun for you to try +it. I'll give a small hint at the end of this post. The puzzle was +created by Humphrey Dudley. +''' + +Jim Orlin's followup 'Update on Logic Puzzle': +http://jimorlin.wordpress.com/2009/02/21/update-on-logic-puzzle/ + + +Compare with the following models: +* ECLiPSe: http://hakank.org/eclipse/labeled_dice.ecl +* Comet : http://www.hakank.org/comet/labeled_dice.co +* Gecode : http://hakank.org/gecode/labeled_dice.cpp +* SICStus: http://hakank.org/sicstus/labeled_dice.pl +* Zinc : http://hakank.org/minizinc/labeled_dice.zinc + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -59,17 +59,52 @@ def main(): # n = 4 m = 24 - A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, Y = ( - list(range(m))) + A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, Y = list( + range(m) + ) letters = [ - "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", - "P", "Q", "R", "S", "T", "U", "V", "W", "Y" + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "Y", ] num_words = 13 - words = [[B, U, O, Y], [C, A, V, E], [C, E, L, T], [F, L, U, B], [F, O, R, K], - [H, E, M, P], [J, U, D, Y], [J, U, N, K], [L, I, M, N], [Q, U, I, P], - [S, W, A, G], [V, I, S, A], [W, I, S, H]] + words = [ + [B, U, O, Y], + [C, A, V, E], + [C, E, L, T], + [F, L, U, B], + [F, O, R, K], + [H, E, M, P], + [J, U, D, Y], + [J, U, N, K], + [L, I, M, N], + [Q, U, I, P], + [S, W, A, G], + [V, I, S, A], + [W, I, S, H], + ] # # declare variables @@ -117,7 +152,8 @@ def main(): for j in range(n): print( "%s (%i)" % (letters[words[i][j]], dice[words[i][j]].Value()), - end=" ") + end=" ", + ) print() print() diff --git a/examples/contrib/langford.py b/examples/contrib/langford.py index aeb14957161..43e4369111f 100644 --- a/examples/contrib/langford.py +++ b/examples/contrib/langford.py @@ -13,35 +13,35 @@ # limitations under the License. """ - Langford's number problem in Google CP Solver. +Langford's number problem in Google CP Solver. - Langford's number problem (CSP lib problem 24) - http://www.csplib.org/prob/prob024/ - ''' - Arrange 2 sets of positive integers 1..k to a sequence, - such that, following the first occurence of an integer i, - each subsequent occurrence of i, appears i+1 indices later - than the last. - For example, for k=4, a solution would be 41312432 - ''' +Langford's number problem (CSP lib problem 24) +http://www.csplib.org/prob/prob024/ +''' +Arrange 2 sets of positive integers 1..k to a sequence, +such that, following the first occurence of an integer i, +each subsequent occurrence of i, appears i+1 indices later +than the last. +For example, for k=4, a solution would be 41312432 +''' - * John E. Miller: Langford's Problem - http://www.lclark.edu/~miller/langford.html +* John E. Miller: Langford's Problem + http://www.lclark.edu/~miller/langford.html - * Encyclopedia of Integer Sequences for the number of solutions for each k - http://www.research.att.com/cgi-bin/access.cgi/as/njas/sequences/eisA.cgi?Anum=014552 +* Encyclopedia of Integer Sequences for the number of solutions for each k + http://www.research.att.com/cgi-bin/access.cgi/as/njas/sequences/eisA.cgi?Anum=014552 - Also, see the following models: - * MiniZinc: http://www.hakank.org/minizinc/langford2.mzn - * Gecode/R: http://www.hakank.org/gecode_r/langford.rb - * ECLiPSe: http://hakank.org/eclipse/langford.ecl - * SICStus: http://hakank.org/sicstus/langford.pl +Also, see the following models: +* MiniZinc: http://www.hakank.org/minizinc/langford2.mzn +* Gecode/R: http://www.hakank.org/gecode_r/langford.rb +* ECLiPSe: http://hakank.org/eclipse/langford.ecl +* SICStus: http://hakank.org/sicstus/langford.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -81,8 +81,9 @@ def main(k=8, num_sol=0): # # search and result # - db = solver.Phase(position, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + position, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/contrib/least_diff.py b/examples/contrib/least_diff.py index 3451090e154..2fee980f29e 100644 --- a/examples/contrib/least_diff.py +++ b/examples/contrib/least_diff.py @@ -13,28 +13,28 @@ # limitations under the License. """ - Least diff problem in Google CP Solver. - - This model solves the following problem: - - What is the smallest difference between two numbers X - Y - if you must use all the digits (0..9) exactly once. - - Compare with the following models: - * Choco : http://www.hakank.org/choco/LeastDiff2.java - * ECLiPSE : http://www.hakank.org/eclipse/least_diff2.ecl - * Comet : http://www.hakank.org/comet/least_diff.co - * Tailor/Essence': http://www.hakank.org/tailor/leastDiff.eprime - * Gecode : http://www.hakank.org/gecode/least_diff.cpp - * Gecode/R: http://www.hakank.org/gecode_r/least_diff.rb - * JaCoP : http://www.hakank.org/JaCoP/LeastDiff.java - * MiniZinc: http://www.hakank.org/minizinc/least_diff.mzn - * SICStus : http://www.hakank.org/sicstus/least_diff.pl - * Zinc : http://hakank.org/minizinc/least_diff.zinc - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_cp_solver/ +Least diff problem in Google CP Solver. + +This model solves the following problem: + +What is the smallest difference between two numbers X - Y +if you must use all the digits (0..9) exactly once. + +Compare with the following models: +* Choco : http://www.hakank.org/choco/LeastDiff2.java +* ECLiPSE : http://www.hakank.org/eclipse/least_diff2.ecl +* Comet : http://www.hakank.org/comet/least_diff.co +* Tailor/Essence': http://www.hakank.org/tailor/leastDiff.eprime +* Gecode : http://www.hakank.org/gecode/least_diff.cpp +* Gecode/R: http://www.hakank.org/gecode_r/least_diff.rb +* JaCoP : http://www.hakank.org/JaCoP/LeastDiff.java +* MiniZinc: http://www.hakank.org/minizinc/least_diff.mzn +* SICStus : http://www.hakank.org/sicstus/least_diff.pl +* Zinc : http://hakank.org/minizinc/least_diff.zinc + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_cp_solver/ """ from ortools.constraint_solver import pywrapcp @@ -91,7 +91,8 @@ def main(unused_argv): # find the solution in just 4 steps solver.Solve( solver.Phase(letters, solver.CHOOSE_PATH, solver.ASSIGN_MIN_VALUE), - [objective, search_log, collector]) + [objective, search_log, collector], + ) # get the first (and only) solution @@ -102,7 +103,7 @@ def main(unused_argv): print("y:", yval) print("diff:", diffval) print(xval, "-", yval, "=", diffval) - print([("abcdefghij" [i], collector.Value(0, letters[i])) for i in range(10)]) + print([("abcdefghij"[i], collector.Value(0, letters[i])) for i in range(10)]) print() print("failures:", solver.Failures()) print("branches:", solver.Branches()) diff --git a/examples/contrib/least_square.py b/examples/contrib/least_square.py index 04b8dd4b6cd..cf5eb968dca 100644 --- a/examples/contrib/least_square.py +++ b/examples/contrib/least_square.py @@ -13,16 +13,16 @@ # limitations under the License. """ - Least square optimization problem in Google or-tools. +Least square optimization problem in Google or-tools. - Solving a fourth grade least square equation. +Solving a fourth grade least square equation. - From the Swedish book 'Optimeringslara' [Optimization Theory], - page 286f. +From the Swedish book 'Optimeringslara' [Optimization Theory], +page 286f. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp @@ -44,8 +44,20 @@ def main(sol='CBC'): # percentage gas F = [ - 0.0, 5.8, 14.7, 31.6, 43.2, 58.3, 78.4, 89.4, 96.4, 99.1, 99.5, 99.9, - 100.0, 100.0 + 0.0, + 5.8, + 14.7, + 31.6, + 43.2, + 58.3, + 78.4, + 89.4, + 96.4, + 99.1, + 99.5, + 99.9, + 100.0, + 100.0, ] p = 4 @@ -57,7 +69,8 @@ def main(sol='CBC'): # to minimize z = solver.Sum([ - (F[i] - (sum([a[j] * t[i]**j for j in range(p + 1)]))) for i in range(num) + (F[i] - (sum([a[j] * t[i] ** j for j in range(p + 1)]))) + for i in range(num) ]) # @@ -69,7 +82,8 @@ def main(sol='CBC'): for i in range(num): solver.Add( - solver.Sum([j * a[j] * t[i]**(j - 1) for j in range(p + 1)]) >= 0) + solver.Sum([j * a[j] * t[i] ** (j - 1) for j in range(p + 1)]) >= 0 + ) objective = solver.Minimize(z) diff --git a/examples/contrib/lectures.py b/examples/contrib/lectures.py index cfa75d7bcea..85f72ef16db 100644 --- a/examples/contrib/lectures.py +++ b/examples/contrib/lectures.py @@ -13,35 +13,35 @@ # limitations under the License. """ - Lectures problem in Google CP Solver. - - Biggs: Discrete Mathematics (2nd ed), page 187. - ''' - Suppose we wish to schedule six one-hour lectures, v1, v2, v3, v4, v5, v6. - Among the potential audience there are people who wish to hear both - - - v1 and v2 - - v1 and v4 - - v3 and v5 - - v2 and v6 - - v4 and v5 - - v5 and v6 - - v1 and v6 - - How many hours are necessary in order that the lectures can be given - without clashes? - ''' - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/lectures.mzn - * SICstus: http://hakank.org/sicstus/lectures.pl - * ECLiPSe: http://hakank.org/eclipse/lectures.ecl - * Gecode: http://hakank.org/gecode/lectures.cpp - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ + Lectures problem in Google CP Solver. + + Biggs: Discrete Mathematics (2nd ed), page 187. + ''' + Suppose we wish to schedule six one-hour lectures, v1, v2, v3, v4, v5, v6. + Among the potential audience there are people who wish to hear both + + - v1 and v2 + - v1 and v4 + - v3 and v5 + - v2 and v6 + - v4 and v5 + - v5 and v6 + - v1 and v6 + + How many hours are necessary in order that the lectures can be given + without clashes? + ''' + + Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/lectures.mzn +* SICstus: http://hakank.org/sicstus/lectures.pl +* ECLiPSe: http://hakank.org/eclipse/lectures.ecl +* Gecode: http://hakank.org/gecode/lectures.cpp + + + This model was created by Hakan Kjellerstrand (hakank@gmail.com) + Also see my other Google CP Solver models: + http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -100,8 +100,9 @@ def main(): # # solution and search # - db = solver.Phase(v, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, - solver.ASSIGN_CENTER_VALUE) + db = solver.Phase( + v, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, solver.ASSIGN_CENTER_VALUE + ) solver.NewSearch(db, [objective]) diff --git a/examples/contrib/magic_square.py b/examples/contrib/magic_square.py index d3057268228..a34ca7e060c 100644 --- a/examples/contrib/magic_square.py +++ b/examples/contrib/magic_square.py @@ -13,13 +13,13 @@ # limitations under the License. """ - Magic squares in Google CP Solver. +Magic squares in Google CP Solver. - Magic square problem. +Magic square problem. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -77,9 +77,9 @@ def main(n, limit): # solver.INT_VAR_DEFAULT, solver.CHOOSE_FIRST_UNBOUND, # solver.CHOOSE_MIN_SIZE_LOWEST_MAX, - # solver.ASSIGN_MIN_VALUE - solver.ASSIGN_CENTER_VALUE) + solver.ASSIGN_CENTER_VALUE, + ) solver.NewSearch(db) num_solutions = 0 @@ -104,7 +104,7 @@ def main(n, limit): n = 4 -limit=100 +limit = 100 if __name__ == "__main__": if len(sys.argv) > 1: n = int(sys.argv[1]) diff --git a/examples/contrib/magic_square_and_cards.py b/examples/contrib/magic_square_and_cards.py index 045d68b54ca..547bccd85d9 100644 --- a/examples/contrib/magic_square_and_cards.py +++ b/examples/contrib/magic_square_and_cards.py @@ -13,19 +13,19 @@ # limitations under the License. """ - Magic squares and cards problem in Google CP Solver. +Magic squares and cards problem in Google CP Solver. - Martin Gardner (July 1971) - ''' - Allowing duplicates values, what is the largest constant sum for an order-3 - magic square that can be formed with nine cards from the deck. - ''' +Martin Gardner (July 1971) +''' +Allowing duplicates values, what is the largest constant sum for an order-3 +magic square that can be formed with nine cards from the deck. +''' - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -79,8 +79,9 @@ def main(n=3): solution.Add(counts) # db: DecisionBuilder - db = solver.Phase(x_flat, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MAX_VALUE) + db = solver.Phase( + x_flat, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MAX_VALUE + ) solver.NewSearch(db, [objective]) num_solutions = 0 diff --git a/examples/contrib/magic_square_mip.py b/examples/contrib/magic_square_mip.py index c050389f8d3..0d3c10c290b 100644 --- a/examples/contrib/magic_square_mip.py +++ b/examples/contrib/magic_square_mip.py @@ -13,33 +13,33 @@ # limitations under the License. """ - Magic square (integer programming) in Google or-tools. +Magic square (integer programming) in Google or-tools. - Translated from GLPK:s example magic.mod - ''' - MAGIC, Magic Square +Translated from GLPK:s example magic.mod +''' +MAGIC, Magic Square - Written in GNU MathProg by Andrew Makhorin +Written in GNU MathProg by Andrew Makhorin - In recreational mathematics, a magic square of order n is an - arrangement of n^2 numbers, usually distinct integers, in a square, - such that n numbers in all rows, all columns, and both diagonals sum - to the same constant. A normal magic square contains the integers - from 1 to n^2. +In recreational mathematics, a magic square of order n is an +arrangement of n^2 numbers, usually distinct integers, in a square, +such that n numbers in all rows, all columns, and both diagonals sum +to the same constant. A normal magic square contains the integers +from 1 to n^2. - (From Wikipedia, the free encyclopedia.) - ''' +(From Wikipedia, the free encyclopedia.) +''' - Compare to the CP version: - http://www.hakank.org/google_or_tools/magic_square.py +Compare to the CP version: + http://www.hakank.org/google_or_tools/magic_square.py - Here we also experiment with how long it takes when - using an output_matrix (much longer). +Here we also experiment with how long it takes when +using an output_matrix (much longer). - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp @@ -107,37 +107,41 @@ def main(n=3, sol='CBC', use_output_matrix=0): # # the sum in each row must be the magic sum for i in range_n: solver.Add( - solver.Sum([k * x[i, j, k] for j in range_n for k in range_N]) == s) + solver.Sum([k * x[i, j, k] for j in range_n for k in range_N]) == s + ) # # the sum in each column must be the magic sum for j in range_n: solver.Add( - solver.Sum([k * x[i, j, k] for i in range_n for k in range_N]) == s) + solver.Sum([k * x[i, j, k] for i in range_n for k in range_N]) == s + ) # # the sum in the diagonal must be the magic sum solver.Add( - solver.Sum([k * x[i, i, k] for i in range_n for k in range_N]) == s) + solver.Sum([k * x[i, i, k] for i in range_n for k in range_N]) == s + ) # # the sum in the co-diagonal must be the magic sum if range_n[0] == 1: # for range_n = 1..n solver.Add( - solver.Sum([k * x[i, n - i + 1, k] - for i in range_n - for k in range_N]) == s) + solver.Sum([k * x[i, n - i + 1, k] for i in range_n for k in range_N]) + == s + ) else: # for range_n = 0..n-1 solver.Add( - solver.Sum([k * x[i, n - i - 1, k] - for i in range_n - for k in range_N]) == s) + solver.Sum([k * x[i, n - i - 1, k] for i in range_n for k in range_N]) + == s + ) # for output if use_output_matrix == 1: for i in range_n: for j in range_n: solver.Add( - square[i, j] == solver.Sum([k * x[i, j, k] for k in range_N])) + square[i, j] == solver.Sum([k * x[i, j, k] for k in range_N]) + ) # # solution and search @@ -159,7 +163,8 @@ def main(n=3, sol='CBC', use_output_matrix=0): print( sum([int(k * x[i, j, k].SolutionValue()) for k in range_N]), ' ', - end=' ') + end=' ', + ) print() print('\nx:') diff --git a/examples/contrib/map.py b/examples/contrib/map.py index 8a88ac133ad..bf23d244253 100644 --- a/examples/contrib/map.py +++ b/examples/contrib/map.py @@ -13,24 +13,24 @@ # limitations under the License. """ - Map coloring problem in Google CP Solver. +Map coloring problem in Google CP Solver. - From Pascal Van Hentenryck 'The OPL Optimization Programming Language', - page 7, 42. +From Pascal Van Hentenryck 'The OPL Optimization Programming Language', +page 7, 42. - Compare with the following models: - * Comet: http://www.hakank.org/comet/map.co - * Tailor/Essence': http://hakank.org/tailor/map_coloring.eprime - * SICStus: http://hakank.org/sicstus/map_coloring.pl - * ECLiPSe: http://hakank.org/eclipse/map.ecl - * Gecode: http://hakank.org/gecode/map.cpp - * MiniZinc: http://hakank.org/minizinc/map.mzn - * Zinc: http://hakank.org/minizinc/map.zinc +Compare with the following models: +* Comet: http://www.hakank.org/comet/map.co +* Tailor/Essence': http://hakank.org/tailor/map_coloring.eprime +* SICStus: http://hakank.org/sicstus/map_coloring.pl +* ECLiPSe: http://hakank.org/eclipse/map.ecl +* Gecode: http://hakank.org/gecode/map.cpp +* MiniZinc: http://hakank.org/minizinc/map.mzn +* Zinc: http://hakank.org/minizinc/map.zinc - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -79,8 +79,13 @@ def main(): # collector = solver.FirstSolutionCollector(solution) # search_log = solver.SearchLog(100, x[0]) solver.Solve( - solver.Phase([color[i] for i in range(n)], solver.INT_VAR_SIMPLE, - solver.ASSIGN_MIN_VALUE), [collector]) + solver.Phase( + [color[i] for i in range(n)], + solver.INT_VAR_SIMPLE, + solver.ASSIGN_MIN_VALUE, + ), + [collector], + ) num_solutions = collector.SolutionCount() print("num_solutions: ", num_solutions) diff --git a/examples/contrib/marathon2.py b/examples/contrib/marathon2.py index 15507eda658..0aa23400865 100644 --- a/examples/contrib/marathon2.py +++ b/examples/contrib/marathon2.py @@ -13,38 +13,38 @@ # limitations under the License. """ - Marathon puzzle in Google CP Solver. - - From Xpress example - http://www.dashoptimization.com/home/cgi-bin/example.pl?id=mosel_puzzle_5_3 - ''' - Dominique, Ignace, Naren, Olivier, Philippe, and Pascal - have arrived as the first six at the Paris marathon. - Reconstruct their arrival order from the following - information: - a) Olivier has not arrived last - b) Dominique, Pascal and Ignace have arrived before Naren - and Olivier - c) Dominique who was third last year has improved this year. - d) Philippe is among the first four. - e) Ignace has arrived neither in second nor third position. - f) Pascal has beaten Naren by three positions. - g) Neither Ignace nor Dominique are on the fourth position. - - (c) 2002 Dash Associates - author: S. Heipcke, Mar. 2002 - ''' - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/marathon2.mzn - * SICStus Prolog: http://www.hakank.org/sicstus/marathon2.pl - * ECLiPSe: http://hakank.org/eclipse/marathon2.ecl - * Gecode: http://hakank.org/gecode/marathon2.cpp - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Marathon puzzle in Google CP Solver. + +From Xpress example +http://www.dashoptimization.com/home/cgi-bin/example.pl?id=mosel_puzzle_5_3 +''' +Dominique, Ignace, Naren, Olivier, Philippe, and Pascal +have arrived as the first six at the Paris marathon. +Reconstruct their arrival order from the following +information: +a) Olivier has not arrived last +b) Dominique, Pascal and Ignace have arrived before Naren + and Olivier +c) Dominique who was third last year has improved this year. +d) Philippe is among the first four. +e) Ignace has arrived neither in second nor third position. +f) Pascal has beaten Naren by three positions. +g) Neither Ignace nor Dominique are on the fourth position. + + (c) 2002 Dash Associates + author: S. Heipcke, Mar. 2002 +''' + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/marathon2.mzn +* SICStus Prolog: http://www.hakank.org/sicstus/marathon2.pl +* ECLiPSe: http://hakank.org/eclipse/marathon2.ecl +* Gecode: http://hakank.org/gecode/marathon2.cpp + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -61,7 +61,12 @@ def main(): n = 6 runners_str = [ - 'Dominique', 'Ignace', 'Naren', 'Olivier', 'Philippe', 'Pascal' + 'Dominique', + 'Ignace', + 'Naren', + 'Olivier', + 'Philippe', + 'Pascal', ] # @@ -106,8 +111,9 @@ def main(): # # solution and search # - db = solver.Phase(runners, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, - solver.ASSIGN_CENTER_VALUE) + db = solver.Phase( + runners, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, solver.ASSIGN_CENTER_VALUE + ) solver.NewSearch(db) diff --git a/examples/contrib/max_flow_taha.py b/examples/contrib/max_flow_taha.py index 6eb0e97958a..b67017c38cc 100644 --- a/examples/contrib/max_flow_taha.py +++ b/examples/contrib/max_flow_taha.py @@ -13,19 +13,19 @@ # limitations under the License. """ - Max flow problem in Google CP Solver. +Max flow problem in Google CP Solver. - From Taha 'Introduction to Operations Research', Example 6.4-2 +From Taha 'Introduction to Operations Research', Example 6.4-2 - Translated from the AMPL code at - http://taha.ineg.uark.edu/maxflo.txt +Translated from the AMPL code at +http://taha.ineg.uark.edu/maxflo.txt - Compare with the following model: - * MiniZinc: http://www.hakank.org/minizinc/max_flow_taha.mzn +Compare with the following model: +* MiniZinc: http://www.hakank.org/minizinc/max_flow_taha.mzn - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -45,8 +45,13 @@ def main(): nodes = list(range(n)) # cost matrix - c = [[0, 20, 30, 10, 0], [0, 0, 40, 0, 30], [0, 0, 0, 10, 20], - [0, 0, 5, 0, 20], [0, 0, 0, 0, 0]] + c = [ + [0, 20, 30, 10, 0], + [0, 0, 40, 0, 30], + [0, 0, 0, 10, 20], + [0, 0, 5, 0, 20], + [0, 0, 0, 0, 0], + ] # # declare variables diff --git a/examples/contrib/max_flow_winston1.py b/examples/contrib/max_flow_winston1.py index 895ca5c3d17..038313adb65 100644 --- a/examples/contrib/max_flow_winston1.py +++ b/examples/contrib/max_flow_winston1.py @@ -13,19 +13,19 @@ # limitations under the License. """ - Max flow problem in Google CP Solver. +Max flow problem in Google CP Solver. - From Winston 'Operations Research', page 420f, 423f - Sunco Oil example. +From Winston 'Operations Research', page 420f, 423f +Sunco Oil example. - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/max_flow_winston1.mzn - * Comet: http://hakank.org/comet/max_flow_winston1.co +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/max_flow_winston1.mzn +* Comet: http://hakank.org/comet/max_flow_winston1.co - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -50,7 +50,7 @@ def main(): # convert arcs to 0-based arcs = [] - for (a_from, a_to) in arcs1: + for a_from, a_to in arcs1: a_from -= 1 a_to -= 1 arcs.append([a_from, a_to]) diff --git a/examples/contrib/minesweeper.py b/examples/contrib/minesweeper.py index 50690a72dc8..7cd4d2768ec 100644 --- a/examples/contrib/minesweeper.py +++ b/examples/contrib/minesweeper.py @@ -13,54 +13,54 @@ # limitations under the License. """ - Minesweeper in Google CP Solver. - - From gecode/examples/minesweeper.cc: - ''' - A specification is a square matrix of characters. Alphanumeric - characters represent the number of mines adjacent to that field. - Dots represent fields with an unknown number of mines adjacent to - it (or an actual mine). - ''' - - E.g. - '..2.3.' - '2.....' - '..24.3' - '1.34..' - '.....3' - '.3.3..' - - - Also see: - * http://www.janko.at/Raetsel/Minesweeper/index.htm - - * http://en.wikipedia.org/wiki/Minesweeper_(computer_game) - - * Ian Stewart on Minesweeper: - http://www.claymath.org/Popular_Lectures/Minesweeper/ - - * Richard Kaye's Minesweeper Pages - http://web.mat.bham.ac.uk/R.W.Kaye/minesw/minesw.htm - - * Some Minesweeper Configurations - http://web.mat.bham.ac.uk/R.W.Kaye/minesw/minesw.pdf - - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/minesweeper.mzn - * Choco : http://www.hakank.org/choco/MineSweeper.java - * JaCoP : http://www.hakank.org/JaCoP/MineSweeper.java - * Gecode/R: http://www.hakank.org/gecode_r/minesweeper.rb - * Comet : http://www.hakank.org/comet/minesweeper.co - * ECLiPSe : http://www.hakank.org/eclipse/minesweeper.ecl - * SICStus : http://www.hakank.org/sicstus/minesweeper.pl - * Tailor/Essence': http://www.hakank.org/tailor/minesweeper.eprime - * Zinc: http://www.hakank.org/minizinc/minesweeper.zinc - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Minesweeper in Google CP Solver. + +From gecode/examples/minesweeper.cc: +''' +A specification is a square matrix of characters. Alphanumeric +characters represent the number of mines adjacent to that field. +Dots represent fields with an unknown number of mines adjacent to +it (or an actual mine). +''' + +E.g. + '..2.3.' + '2.....' + '..24.3' + '1.34..' + '.....3' + '.3.3..' + + +Also see: +* http://www.janko.at/Raetsel/Minesweeper/index.htm + +* http://en.wikipedia.org/wiki/Minesweeper_(computer_game) + +* Ian Stewart on Minesweeper: + http://www.claymath.org/Popular_Lectures/Minesweeper/ + +* Richard Kaye's Minesweeper Pages + http://web.mat.bham.ac.uk/R.W.Kaye/minesw/minesw.htm + +* Some Minesweeper Configurations + http://web.mat.bham.ac.uk/R.W.Kaye/minesw/minesw.pdf + + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/minesweeper.mzn +* Choco : http://www.hakank.org/choco/MineSweeper.java +* JaCoP : http://www.hakank.org/JaCoP/MineSweeper.java +* Gecode/R: http://www.hakank.org/gecode_r/minesweeper.rb +* Comet : http://www.hakank.org/comet/minesweeper.co +* ECLiPSe : http://www.hakank.org/eclipse/minesweeper.ecl +* SICStus : http://www.hakank.org/sicstus/minesweeper.pl +* Tailor/Essence': http://www.hakank.org/tailor/minesweeper.eprime +* Zinc: http://www.hakank.org/minizinc/minesweeper.zinc + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -68,10 +68,16 @@ default_r = 8 default_c = 8 X = -1 -default_game = [[2, 3, X, 2, 2, X, 2, 1], [X, X, 4, X, X, 4, X, 2], - [X, X, X, X, X, X, 4, X], [X, 5, X, 6, X, X, X, 2], - [2, X, X, X, 5, 5, X, 2], [1, 3, 4, X, X, X, 4, X], - [0, 1, X, 4, X, X, X, 3], [0, 1, 2, X, 2, 3, X, 2]] +default_game = [ + [2, 3, X, 2, 2, X, 2, 1], + [X, X, 4, X, X, 4, X, 2], + [X, X, X, X, X, X, 4, X], + [X, 5, X, 6, X, X, X, 2], + [2, X, X, X, 5, 5, X, 2], + [1, 3, 4, X, X, X, 4, X], + [0, 1, X, 4, X, X, X, 3], + [0, 1, 2, X, 2, 3, X, 2], +] def main(game="", r="", c=""): @@ -137,12 +143,15 @@ def main(game="", r="", c=""): if game[i][j] >= 0: solver.Add(mines[i, j] == 0) # this cell is the sum of all the surrounding cells - solver.Add(game[i][j] == solver.Sum([ - mines[i + a, j + b] - for a in S - for b in S - if i + a >= 0 and j + b >= 0 and i + a < r and j + b < c - ])) + solver.Add( + game[i][j] + == solver.Sum([ + mines[i + a, j + b] + for a in S + for b in S + if i + a >= 0 and j + b >= 0 and i + a < r and j + b < c + ]) + ) if game[i][j] > X: # This cell cannot be a mine solver.Add(mines[i, j] == 0) @@ -155,8 +164,13 @@ def main(game="", r="", c=""): collector = solver.AllSolutionCollector(solution) solver.Solve( - solver.Phase([mines[(i, j)] for i in range(r) for j in range(c)], - solver.INT_VAR_SIMPLE, solver.ASSIGN_MIN_VALUE), [collector]) + solver.Phase( + [mines[(i, j)] for i in range(r) for j in range(c)], + solver.INT_VAR_SIMPLE, + solver.ASSIGN_MIN_VALUE, + ), + [collector], + ) num_solutions = collector.SolutionCount() print("num_solutions: ", num_solutions) diff --git a/examples/contrib/mr_smith.py b/examples/contrib/mr_smith.py index db86addf31b..fd394eae163 100644 --- a/examples/contrib/mr_smith.py +++ b/examples/contrib/mr_smith.py @@ -13,38 +13,38 @@ # limitations under the License. """ - Mr Smith in Google CP Solver. - - From an IF Prolog example (http://www.ifcomputer.de/) - ''' - The Smith family and their three children want to pay a visit but they - do not all have the time to do so. Following are few hints who will go - and who will not: - o If Mr Smith comes, his wife will come too. - o At least one of their two sons Matt and John will come. - o Either Mrs Smith or Tim will come, but not both. - o Either Tim and John will come, or neither will come. - o If Matt comes, then John and his father will - also come. - ''' - - The answer should be: - Mr_Smith_comes = 0 - Mrs_Smith_comes = 0 - Matt_comes = 0 - John_comes = 1 - Tim_comes = 1 - - Compare with the following models: - * ECLiPSe: http://www.hakank.org/eclipse/mr_smith.ecl - * SICStus Prolog: http://www.hakank.org/sicstus/mr_smith.pl - * Gecode: http://www.hakank.org/gecode/mr_smith.cpp - * MiniZinc: http://www.hakank.org/minizinc/mr_smith.mzn - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Mr Smith in Google CP Solver. + +From an IF Prolog example (http://www.ifcomputer.de/) +''' +The Smith family and their three children want to pay a visit but they +do not all have the time to do so. Following are few hints who will go +and who will not: + o If Mr Smith comes, his wife will come too. + o At least one of their two sons Matt and John will come. + o Either Mrs Smith or Tim will come, but not both. + o Either Tim and John will come, or neither will come. + o If Matt comes, then John and his father will + also come. +''' + +The answer should be: + Mr_Smith_comes = 0 + Mrs_Smith_comes = 0 + Matt_comes = 0 + John_comes = 1 + Tim_comes = 1 + +Compare with the following models: +* ECLiPSe: http://www.hakank.org/eclipse/mr_smith.ecl +* SICStus Prolog: http://www.hakank.org/sicstus/mr_smith.pl +* Gecode: http://www.hakank.org/gecode/mr_smith.cpp +* MiniZinc: http://www.hakank.org/minizinc/mr_smith.mzn + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp diff --git a/examples/contrib/nonogram_default_search.py b/examples/contrib/nonogram_default_search.py index 2219b2787be..59dafc00532 100644 --- a/examples/contrib/nonogram_default_search.py +++ b/examples/contrib/nonogram_default_search.py @@ -13,30 +13,30 @@ # limitations under the License. """ - Nonogram (Painting by numbers) in Google CP Solver. +Nonogram (Painting by numbers) in Google CP Solver. - http://en.wikipedia.org/wiki/Nonogram - ''' - Nonograms or Paint by Numbers are picture logic puzzles in which cells in a - grid have to be colored or left blank according to numbers given at the - side of the grid to reveal a hidden picture. In this puzzle type, the - numbers measure how many unbroken lines of filled-in squares there are - in any given row or column. For example, a clue of '4 8 3' would mean - there are sets of four, eight, and three filled squares, in that order, - with at least one blank square between successive groups. +http://en.wikipedia.org/wiki/Nonogram +''' +Nonograms or Paint by Numbers are picture logic puzzles in which cells in a +grid have to be colored or left blank according to numbers given at the +side of the grid to reveal a hidden picture. In this puzzle type, the +numbers measure how many unbroken lines of filled-in squares there are +in any given row or column. For example, a clue of '4 8 3' would mean +there are sets of four, eight, and three filled squares, in that order, +with at least one blank square between successive groups. - ''' +''' - See problem 12 at http://www.csplib.org/. +See problem 12 at http://www.csplib.org/. - http://www.puzzlemuseum.com/nonogram.htm +http://www.puzzlemuseum.com/nonogram.htm - Haskell solution: - http://twan.home.fmf.nl/blog/haskell/Nonograms.details +Haskell solution: +http://twan.home.fmf.nl/blog/haskell/Nonograms.details - Brunetti, Sara & Daurat, Alain (2003) - 'An algorithm reconstructing convex lattice sets' - http://geodisi.u-strasbg.fr/~daurat/papiers/tomoqconv.pdf +Brunetti, Sara & Daurat, Alain (2003) +'An algorithm reconstructing convex lattice sets' +http://geodisi.u-strasbg.fr/~daurat/papiers/tomoqconv.pdf """ import sys @@ -95,8 +95,10 @@ def check_rule(rules, y): solver = y[0].solver() solver.Add( - solver.TransitionConstraint(y, transition_tuples, initial_state, - accepting_states)) + solver.TransitionConstraint( + y, transition_tuples, initial_state, accepting_states + ) + ) def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): @@ -185,13 +187,35 @@ def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): # rows = 12 row_rule_len = 3 -row_rules = [[0, 0, 2], [0, 1, 2], [0, 1, 1], [0, 0, 2], [0, 0, 1], [0, 0, 3], - [0, 0, 3], [0, 2, 2], [0, 2, 1], [2, 2, 1], [0, 2, 3], [0, 2, 2]] +row_rules = [ + [0, 0, 2], + [0, 1, 2], + [0, 1, 1], + [0, 0, 2], + [0, 0, 1], + [0, 0, 3], + [0, 0, 3], + [0, 2, 2], + [0, 2, 1], + [2, 2, 1], + [0, 2, 3], + [0, 2, 2], +] cols = 10 col_rule_len = 2 -col_rules = [[2, 1], [1, 3], [2, 4], [3, 4], [0, 4], [0, 3], [0, 3], [0, 3], - [0, 2], [0, 2]] +col_rules = [ + [2, 1], + [1, 3], + [2, 4], + [3, 4], + [0, 4], + [0, 3], + [0, 3], + [0, 3], + [0, 2], + [0, 2], +] if __name__ == '__main__': if len(sys.argv) > 1: diff --git a/examples/contrib/nonogram_regular.py b/examples/contrib/nonogram_regular.py index 9de08fdbeb2..8ac04a55364 100644 --- a/examples/contrib/nonogram_regular.py +++ b/examples/contrib/nonogram_regular.py @@ -13,56 +13,56 @@ # limitations under the License. """ - Nonogram (Painting by numbers) in Google CP Solver. +Nonogram (Painting by numbers) in Google CP Solver. - http://en.wikipedia.org/wiki/Nonogram - ''' - Nonograms or Paint by Numbers are picture logic puzzles in which cells in a - grid have to be colored or left blank according to numbers given at the - side of the grid to reveal a hidden picture. In this puzzle type, the - numbers measure how many unbroken lines of filled-in squares there are - in any given row or column. For example, a clue of '4 8 3' would mean - there are sets of four, eight, and three filled squares, in that order, - with at least one blank square between successive groups. +http://en.wikipedia.org/wiki/Nonogram +''' +Nonograms or Paint by Numbers are picture logic puzzles in which cells in a +grid have to be colored or left blank according to numbers given at the +side of the grid to reveal a hidden picture. In this puzzle type, the +numbers measure how many unbroken lines of filled-in squares there are +in any given row or column. For example, a clue of '4 8 3' would mean +there are sets of four, eight, and three filled squares, in that order, +with at least one blank square between successive groups. - ''' +''' - See problem 12 at http://www.csplib.org/. +See problem 12 at http://www.csplib.org/. - http://www.puzzlemuseum.com/nonogram.htm +http://www.puzzlemuseum.com/nonogram.htm - Haskell solution: - http://twan.home.fmf.nl/blog/haskell/Nonograms.details +Haskell solution: +http://twan.home.fmf.nl/blog/haskell/Nonograms.details - Brunetti, Sara & Daurat, Alain (2003) - 'An algorithm reconstructing convex lattice sets' - http://geodisi.u-strasbg.fr/~daurat/papiers/tomoqconv.pdf +Brunetti, Sara & Daurat, Alain (2003) +'An algorithm reconstructing convex lattice sets' +http://geodisi.u-strasbg.fr/~daurat/papiers/tomoqconv.pdf - The Comet model (http://www.hakank.org/comet/nonogram_regular.co) - was a major influence when writing this Google CP solver model. +The Comet model (http://www.hakank.org/comet/nonogram_regular.co) +was a major influence when writing this Google CP solver model. - I have also blogged about the development of a Nonogram solver in Comet - using the regular constraint. - * 'Comet: Nonogram improved: solving problem P200 from 1:30 minutes - to about 1 second' - http://www.hakank.org/constraint_programming_blog/2009/03/comet_nonogram_improved_solvin_1.html +I have also blogged about the development of a Nonogram solver in Comet +using the regular constraint. +* 'Comet: Nonogram improved: solving problem P200 from 1:30 minutes + to about 1 second' + http://www.hakank.org/constraint_programming_blog/2009/03/comet_nonogram_improved_solvin_1.html - * 'Comet: regular constraint, a much faster Nonogram with the regular - constraint, - some OPL models, and more' - http://www.hakank.org/constraint_programming_blog/2009/02/comet_regular_constraint_a_muc_1.html +* 'Comet: regular constraint, a much faster Nonogram with the regular +constraint, + some OPL models, and more' + http://www.hakank.org/constraint_programming_blog/2009/02/comet_regular_constraint_a_muc_1.html - Compare with the other models: - * Gecode/R: http://www.hakank.org/gecode_r/nonogram.rb (using 'regexps') - * MiniZinc: http://www.hakank.org/minizinc/nonogram_regular.mzn - * MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton.mzn - * MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton2.mzn - Note: nonogram_create_automaton2.mzn is the preferred model +Compare with the other models: +* Gecode/R: http://www.hakank.org/gecode_r/nonogram.rb (using 'regexps') +* MiniZinc: http://www.hakank.org/minizinc/nonogram_regular.mzn +* MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton.mzn +* MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton2.mzn + Note: nonogram_create_automaton2.mzn is the preferred model - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -134,7 +134,8 @@ def regular(x, Q, S, d, q0, F): solver.Add(x[i] <= S) # Determine a[i+1]: a[i+1] == d2[a[i], x[i]] solver.Add( - a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1))) + a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1)) + ) # @@ -220,8 +221,9 @@ def check_rule(rules, y): initial_state = 1 accepting_states = [n_states] # This is the last state - regular(y, n_states, input_max, transition_fn, initial_state, - accepting_states) + regular( + y, n_states, input_max, transition_fn, initial_state, accepting_states + ) def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): @@ -259,18 +261,23 @@ def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): # constraints # for i in range(rows): - check_rule([row_rules[i][j] for j in range(row_rule_len)], - [board[i, j] for j in range(cols)]) + check_rule( + [row_rules[i][j] for j in range(row_rule_len)], + [board[i, j] for j in range(cols)], + ) for j in range(cols): - check_rule([col_rules[j][k] for k in range(col_rule_len)], - [board[i, j] for i in range(rows)]) + check_rule( + [col_rules[j][k] for k in range(col_rule_len)], + [board[i, j] for i in range(rows)], + ) # # solution and search # - db = solver.Phase(board_label, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + board_label, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) @@ -311,13 +318,35 @@ def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): # rows = 12 row_rule_len = 3 -row_rules = [[0, 0, 2], [0, 1, 2], [0, 1, 1], [0, 0, 2], [0, 0, 1], [0, 0, 3], - [0, 0, 3], [0, 2, 2], [0, 2, 1], [2, 2, 1], [0, 2, 3], [0, 2, 2]] +row_rules = [ + [0, 0, 2], + [0, 1, 2], + [0, 1, 1], + [0, 0, 2], + [0, 0, 1], + [0, 0, 3], + [0, 0, 3], + [0, 2, 2], + [0, 2, 1], + [2, 2, 1], + [0, 2, 3], + [0, 2, 2], +] cols = 10 col_rule_len = 2 -col_rules = [[2, 1], [1, 3], [2, 4], [3, 4], [0, 4], [0, 3], [0, 3], [0, 3], - [0, 2], [0, 2]] +col_rules = [ + [2, 1], + [1, 3], + [2, 4], + [3, 4], + [0, 4], + [0, 3], + [0, 3], + [0, 3], + [0, 2], + [0, 2], +] if __name__ == '__main__': if len(sys.argv) > 1: diff --git a/examples/contrib/nonogram_table.py b/examples/contrib/nonogram_table.py index d539a35aafd..e3dcced30ad 100644 --- a/examples/contrib/nonogram_table.py +++ b/examples/contrib/nonogram_table.py @@ -13,56 +13,56 @@ # limitations under the License. """ - Nonogram (Painting by numbers) in Google CP Solver. +Nonogram (Painting by numbers) in Google CP Solver. - http://en.wikipedia.org/wiki/Nonogram - ''' - Nonograms or Paint by Numbers are picture logic puzzles in which cells in a - grid have to be colored or left blank according to numbers given at the - side of the grid to reveal a hidden picture. In this puzzle type, the - numbers measure how many unbroken lines of filled-in squares there are - in any given row or column. For example, a clue of '4 8 3' would mean - there are sets of four, eight, and three filled squares, in that order, - with at least one blank square between successive groups. +http://en.wikipedia.org/wiki/Nonogram +''' +Nonograms or Paint by Numbers are picture logic puzzles in which cells in a +grid have to be colored or left blank according to numbers given at the +side of the grid to reveal a hidden picture. In this puzzle type, the +numbers measure how many unbroken lines of filled-in squares there are +in any given row or column. For example, a clue of '4 8 3' would mean +there are sets of four, eight, and three filled squares, in that order, +with at least one blank square between successive groups. - ''' +''' - See problem 12 at http://www.csplib.org/. +See problem 12 at http://www.csplib.org/. - http://www.puzzlemuseum.com/nonogram.htm +http://www.puzzlemuseum.com/nonogram.htm - Haskell solution: - http://twan.home.fmf.nl/blog/haskell/Nonograms.details +Haskell solution: +http://twan.home.fmf.nl/blog/haskell/Nonograms.details - Brunetti, Sara & Daurat, Alain (2003) - 'An algorithm reconstructing convex lattice sets' - http://geodisi.u-strasbg.fr/~daurat/papiers/tomoqconv.pdf +Brunetti, Sara & Daurat, Alain (2003) +'An algorithm reconstructing convex lattice sets' +http://geodisi.u-strasbg.fr/~daurat/papiers/tomoqconv.pdf - The Comet model (http://www.hakank.org/comet/nonogram_regular.co) - was a major influence when writing this Google CP solver model. +The Comet model (http://www.hakank.org/comet/nonogram_regular.co) +was a major influence when writing this Google CP solver model. - I have also blogged about the development of a Nonogram solver in Comet - using the regular constraint. - * 'Comet: Nonogram improved: solving problem P200 from 1:30 minutes - to about 1 second' - http://www.hakank.org/constraint_programming_blog/2009/03/comet_nonogram_improved_solvin_1.html +I have also blogged about the development of a Nonogram solver in Comet +using the regular constraint. +* 'Comet: Nonogram improved: solving problem P200 from 1:30 minutes + to about 1 second' + http://www.hakank.org/constraint_programming_blog/2009/03/comet_nonogram_improved_solvin_1.html - * 'Comet: regular constraint, a much faster Nonogram with the regular - constraint, - some OPL models, and more' - http://www.hakank.org/constraint_programming_blog/2009/02/comet_regular_constraint_a_muc_1.html +* 'Comet: regular constraint, a much faster Nonogram with the regular +constraint, + some OPL models, and more' + http://www.hakank.org/constraint_programming_blog/2009/02/comet_regular_constraint_a_muc_1.html - Compare with the other models: - * Gecode/R: http://www.hakank.org/gecode_r/nonogram.rb (using 'regexps') - * MiniZinc: http://www.hakank.org/minizinc/nonogram_regular.mzn - * MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton.mzn - * MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton2.mzn - Note: nonogram_create_automaton2.mzn is the preferred model +Compare with the other models: +* Gecode/R: http://www.hakank.org/gecode_r/nonogram.rb (using 'regexps') +* MiniZinc: http://www.hakank.org/minizinc/nonogram_regular.mzn +* MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton.mzn +* MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton2.mzn + Note: nonogram_create_automaton2.mzn is the preferred model - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -216,8 +216,9 @@ def check_rule(rules, y): initial_state = 1 accepting_states = [n_states] # This is the last state - regular(y, n_states, input_max, transition_fn, initial_state, - accepting_states) + regular( + y, n_states, input_max, transition_fn, initial_state, accepting_states + ) def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): @@ -255,18 +256,23 @@ def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): # constraints # for i in range(rows): - check_rule([row_rules[i][j] for j in range(row_rule_len)], - [board[i, j] for j in range(cols)]) + check_rule( + [row_rules[i][j] for j in range(row_rule_len)], + [board[i, j] for j in range(cols)], + ) for j in range(cols): - check_rule([col_rules[j][k] for k in range(col_rule_len)], - [board[i, j] for i in range(rows)]) + check_rule( + [col_rules[j][k] for k in range(col_rule_len)], + [board[i, j] for i in range(rows)], + ) # # solution and search # - db = solver.Phase(board_label, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + board_label, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) @@ -307,13 +313,35 @@ def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): # rows = 12 row_rule_len = 3 -row_rules = [[0, 0, 2], [0, 1, 2], [0, 1, 1], [0, 0, 2], [0, 0, 1], [0, 0, 3], - [0, 0, 3], [0, 2, 2], [0, 2, 1], [2, 2, 1], [0, 2, 3], [0, 2, 2]] +row_rules = [ + [0, 0, 2], + [0, 1, 2], + [0, 1, 1], + [0, 0, 2], + [0, 0, 1], + [0, 0, 3], + [0, 0, 3], + [0, 2, 2], + [0, 2, 1], + [2, 2, 1], + [0, 2, 3], + [0, 2, 2], +] cols = 10 col_rule_len = 2 -col_rules = [[2, 1], [1, 3], [2, 4], [3, 4], [0, 4], [0, 3], [0, 3], [0, 3], - [0, 2], [0, 2]] +col_rules = [ + [2, 1], + [1, 3], + [2, 4], + [3, 4], + [0, 4], + [0, 3], + [0, 3], + [0, 3], + [0, 2], + [0, 2], +] if __name__ == '__main__': if len(sys.argv) > 1: diff --git a/examples/contrib/nonogram_table2.py b/examples/contrib/nonogram_table2.py index 6e643c2dfcb..afbc21e8f07 100644 --- a/examples/contrib/nonogram_table2.py +++ b/examples/contrib/nonogram_table2.py @@ -13,56 +13,56 @@ # limitations under the License. """ - Nonogram (Painting by numbers) in Google CP Solver. +Nonogram (Painting by numbers) in Google CP Solver. - http://en.wikipedia.org/wiki/Nonogram - ''' - Nonograms or Paint by Numbers are picture logic puzzles in which cells in a - grid have to be colored or left blank according to numbers given at the - side of the grid to reveal a hidden picture. In this puzzle type, the - numbers measure how many unbroken lines of filled-in squares there are - in any given row or column. For example, a clue of '4 8 3' would mean - there are sets of four, eight, and three filled squares, in that order, - with at least one blank square between successive groups. +http://en.wikipedia.org/wiki/Nonogram +''' +Nonograms or Paint by Numbers are picture logic puzzles in which cells in a +grid have to be colored or left blank according to numbers given at the +side of the grid to reveal a hidden picture. In this puzzle type, the +numbers measure how many unbroken lines of filled-in squares there are +in any given row or column. For example, a clue of '4 8 3' would mean +there are sets of four, eight, and three filled squares, in that order, +with at least one blank square between successive groups. - ''' +''' - See problem 12 at http://www.csplib.org/. +See problem 12 at http://www.csplib.org/. - http://www.puzzlemuseum.com/nonogram.htm +http://www.puzzlemuseum.com/nonogram.htm - Haskell solution: - http://twan.home.fmf.nl/blog/haskell/Nonograms.details +Haskell solution: +http://twan.home.fmf.nl/blog/haskell/Nonograms.details - Brunetti, Sara & Daurat, Alain (2003) - 'An algorithm reconstructing convex lattice sets' - http://geodisi.u-strasbg.fr/~daurat/papiers/tomoqconv.pdf +Brunetti, Sara & Daurat, Alain (2003) +'An algorithm reconstructing convex lattice sets' +http://geodisi.u-strasbg.fr/~daurat/papiers/tomoqconv.pdf - The Comet model (http://www.hakank.org/comet/nonogram_regular.co) - was a major influence when writing this Google CP solver model. +The Comet model (http://www.hakank.org/comet/nonogram_regular.co) +was a major influence when writing this Google CP solver model. - I have also blogged about the development of a Nonogram solver in Comet - using the regular constraint. - * 'Comet: Nonogram improved: solving problem P200 from 1:30 minutes - to about 1 second' - http://www.hakank.org/constraint_programming_blog/2009/03/comet_nonogram_improved_solvin_1.html +I have also blogged about the development of a Nonogram solver in Comet +using the regular constraint. +* 'Comet: Nonogram improved: solving problem P200 from 1:30 minutes + to about 1 second' + http://www.hakank.org/constraint_programming_blog/2009/03/comet_nonogram_improved_solvin_1.html - * 'Comet: regular constraint, a much faster Nonogram with the regular - constraint, - some OPL models, and more' - http://www.hakank.org/constraint_programming_blog/2009/02/comet_regular_constraint_a_muc_1.html +* 'Comet: regular constraint, a much faster Nonogram with the regular +constraint, + some OPL models, and more' + http://www.hakank.org/constraint_programming_blog/2009/02/comet_regular_constraint_a_muc_1.html - Compare with the other models: - * Gecode/R: http://www.hakank.org/gecode_r/nonogram.rb (using 'regexps') - * MiniZinc: http://www.hakank.org/minizinc/nonogram_regular.mzn - * MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton.mzn - * MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton2.mzn - Note: nonogram_create_automaton2.mzn is the preferred model +Compare with the other models: +* Gecode/R: http://www.hakank.org/gecode_r/nonogram.rb (using 'regexps') +* MiniZinc: http://www.hakank.org/minizinc/nonogram_regular.mzn +* MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton.mzn +* MiniZinc: http://www.hakank.org/minizinc/nonogram_create_automaton2.mzn + Note: nonogram_create_automaton2.mzn is the preferred model - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -121,8 +121,10 @@ def check_rule(rules, y): solver = y[0].solver() solver.Add( - solver.TransitionConstraint(y, transition_tuples, initial_state, - accepting_states)) + solver.TransitionConstraint( + y, transition_tuples, initial_state, accepting_states + ) + ) def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): @@ -164,8 +166,9 @@ def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): # # solution and search # - db = solver.Phase(board_label, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + board_label, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) print('before solver, wall time = ', solver.WallTime(), 'ms') solver.NewSearch(db) @@ -207,13 +210,35 @@ def main(rows, row_rule_len, row_rules, cols, col_rule_len, col_rules): # rows = 12 row_rule_len = 3 -row_rules = [[0, 0, 2], [0, 1, 2], [0, 1, 1], [0, 0, 2], [0, 0, 1], [0, 0, 3], - [0, 0, 3], [0, 2, 2], [0, 2, 1], [2, 2, 1], [0, 2, 3], [0, 2, 2]] +row_rules = [ + [0, 0, 2], + [0, 1, 2], + [0, 1, 1], + [0, 0, 2], + [0, 0, 1], + [0, 0, 3], + [0, 0, 3], + [0, 2, 2], + [0, 2, 1], + [2, 2, 1], + [0, 2, 3], + [0, 2, 2], +] cols = 10 col_rule_len = 2 -col_rules = [[2, 1], [1, 3], [2, 4], [3, 4], [0, 4], [0, 3], [0, 3], [0, 3], - [0, 2], [0, 2]] +col_rules = [ + [2, 1], + [1, 3], + [2, 4], + [3, 4], + [0, 4], + [0, 3], + [0, 3], + [0, 3], + [0, 2], + [0, 2], +] if __name__ == '__main__': if len(sys.argv) > 1: diff --git a/examples/contrib/nontransitive_dice.py b/examples/contrib/nontransitive_dice.py index 3604842ac66..2957358bc6c 100644 --- a/examples/contrib/nontransitive_dice.py +++ b/examples/contrib/nontransitive_dice.py @@ -13,58 +13,58 @@ # limitations under the License. """ - Nontransitive dice in Google CP Solver. - - From - http://en.wikipedia.org/wiki/Nontransitive_dice - ''' - A set of nontransitive dice is a set of dice for which the relation - 'is more likely to roll a higher number' is not transitive. See also - intransitivity. - - This situation is similar to that in the game Rock, Paper, Scissors, - in which each element has an advantage over one choice and a - disadvantage to the other. - ''' - - I start with the 3 dice version - ''' - * die A has sides {2,2,4,4,9,9}, - * die B has sides {1,1,6,6,8,8}, and - * die C has sides {3,3,5,5,7,7}. - ''' - - 3 dice: - Maximum winning: 27 - comp: [19, 27, 19] + Nontransitive dice in Google CP Solver. + + From + http://en.wikipedia.org/wiki/Nontransitive_dice + ''' + A set of nontransitive dice is a set of dice for which the relation + 'is more likely to roll a higher number' is not transitive. See also + intransitivity. + + This situation is similar to that in the game Rock, Paper, Scissors, + in which each element has an advantage over one choice and a + disadvantage to the other. + ''' + + I start with the 3 dice version + ''' + * die A has sides {2,2,4,4,9,9}, + * die B has sides {1,1,6,6,8,8}, and + * die C has sides {3,3,5,5,7,7}. + ''' + + 3 dice: + Maximum winning: 27 + comp: [19, 27, 19] + dice: + [[0, 0, 3, 6, 6, 6], + [2, 5, 5, 5, 5, 5], + [1, 1, 4, 4, 4, 7]] + max_win: 27 + + Number of solutions: 1 + Nodes: 1649873 Time: 25.94 + getFailures: 1649853 + getBacktracks: 1649873 + getPropags: 98105090 + +Max winnings where they are the same: 21 + comp: [21, 21, 21] dice: - [[0, 0, 3, 6, 6, 6], - [2, 5, 5, 5, 5, 5], - [1, 1, 4, 4, 4, 7]] - max_win: 27 - - Number of solutions: 1 - Nodes: 1649873 Time: 25.94 - getFailures: 1649853 - getBacktracks: 1649873 - getPropags: 98105090 - - Max winnings where they are the same: 21 - comp: [21, 21, 21] - dice: - [[0, 0, 3, 3, 3, 6], - [2, 2, 2, 2, 2, 5], - [1, 1, 1, 4, 4, 4]] - max_win: 21 - - Compare with these models: - * MiniZinc: http://hakank.org/minizinc/nontransitive_dice.mzn - * Comet: http://hakank.org/comet/nontransitive_dice.co - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ + [[0, 0, 3, 3, 3, 6], + [2, 2, 2, 2, 2, 5], + [1, 1, 1, 4, 4, 4]] + max_win: 21 + + Compare with these models: + * MiniZinc: http://hakank.org/minizinc/nontransitive_dice.mzn + * Comet: http://hakank.org/comet/nontransitive_dice.co + + + This model was created by Hakan Kjellerstrand (hakank@gmail.com) + Also see my other Google CP Solver models: + http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -160,8 +160,9 @@ def main(m=3, n=6, minimize_val=0): # # solution and search # - db = solver.Phase(dice_flat + comp_flat, solver.INT_VAR_DEFAULT, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + dice_flat + comp_flat, solver.INT_VAR_DEFAULT, solver.ASSIGN_MIN_VALUE + ) if minimize_val: solver.NewSearch(db, [objective]) diff --git a/examples/contrib/nqueens.py b/examples/contrib/nqueens.py index 1a8e7cd79fe..f54d8e21c34 100644 --- a/examples/contrib/nqueens.py +++ b/examples/contrib/nqueens.py @@ -13,13 +13,13 @@ # limitations under the License. """ - n-queens problem in Google CP Solver. +n-queens problem in Google CP Solver. - N queens problem. +N queens problem. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -63,8 +63,13 @@ def main(n=8): # collector = solver.FirstSolutionCollector(solution) # search_log = solver.SearchLog(100, x[0]) solver.Solve( - solver.Phase([q[i] for i in range(n)], solver.INT_VAR_SIMPLE, - solver.ASSIGN_MIN_VALUE), [collector]) + solver.Phase( + [q[i] for i in range(n)], + solver.INT_VAR_SIMPLE, + solver.ASSIGN_MIN_VALUE, + ), + [collector], + ) num_solutions = collector.SolutionCount() print("num_solutions: ", num_solutions) diff --git a/examples/contrib/nqueens2.py b/examples/contrib/nqueens2.py index aa6f6eff451..1a570bf9249 100644 --- a/examples/contrib/nqueens2.py +++ b/examples/contrib/nqueens2.py @@ -13,16 +13,16 @@ # limitations under the License. """ - n-queens problem in Google CP Solver. +n-queens problem in Google CP Solver. - N queens problem. +N queens problem. - This version use NewSearch()/NextSolution() for looping through - the solutions. +This version use NewSearch()/NextSolution() for looping through +the solutions. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -68,7 +68,8 @@ def main(n=8): [q[i] for i in range(n)], # solver.CHOOSE_FIRST_UNBOUND, solver.CHOOSE_MIN_SIZE_LOWEST_MAX, - solver.ASSIGN_CENTER_VALUE) + solver.ASSIGN_CENTER_VALUE, + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/contrib/nqueens3.py b/examples/contrib/nqueens3.py index d14db010d54..2cad3d73618 100644 --- a/examples/contrib/nqueens3.py +++ b/examples/contrib/nqueens3.py @@ -13,18 +13,18 @@ # limitations under the License. """ - n-queens problem in Google CP Solver. +n-queens problem in Google CP Solver. - N queens problem. +N queens problem. - Faster than the previous versions: - - http://www.hakank.org/gogle_cp_solver/nqueens.py - - http://www.hakank.org/gogle_cp_solver/nqueens2.py +Faster than the previous versions: +- http://www.hakank.org/gogle_cp_solver/nqueens.py +- http://www.hakank.org/gogle_cp_solver/nqueens2.py - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -58,8 +58,9 @@ def main(n=8, num_sol=0, print_sol=1): # search # - db = solver.Phase(q, solver.CHOOSE_MIN_SIZE_LOWEST_MAX, - solver.ASSIGN_CENTER_VALUE) + db = solver.Phase( + q, solver.CHOOSE_MIN_SIZE_LOWEST_MAX, solver.ASSIGN_CENTER_VALUE + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/contrib/nurse_rostering.py b/examples/contrib/nurse_rostering.py index 48b54b8ed67..d793b0bb5cf 100644 --- a/examples/contrib/nurse_rostering.py +++ b/examples/contrib/nurse_rostering.py @@ -13,19 +13,19 @@ # limitations under the License. """ - Nurse rostering in Google CP Solver. +Nurse rostering in Google CP Solver. - This is a simple nurse rostering model using a DFA and - my decomposition of regular constraint. +This is a simple nurse rostering model using a DFA and +my decomposition of regular constraint. - The DFA is from MiniZinc Tutorial, Nurse Rostering example: - - one day off every 4 days - - no 3 nights in a row. +The DFA is from MiniZinc Tutorial, Nurse Rostering example: +- one day off every 4 days +- no 3 nights in a row. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -98,7 +98,8 @@ def regular(x, Q, S, d, q0, F): # Determine a[i+1]: a[i+1] == d2[a[i], x[i]] solver.Add( - a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1))) + a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1)) + ) def main(): @@ -134,7 +135,7 @@ def main(): [4, 5, 1], # state 3 [6, 6, 1], # state 4 [6, 0, 1], # state 5 - [0, 0, 1] # state 6 + [0, 0, 1], # state 6 ] days = ['d', 'n', 'o'] # for presentation @@ -168,8 +169,14 @@ def main(): # for i in range(num_nurses): reg_input = [x[i, j] for j in range(num_days)] - regular(reg_input, n_states, input_max, transition_fn, initial_state, - accepting_states) + regular( + reg_input, + n_states, + input_max, + transition_fn, + initial_state, + accepting_states, + ) # # Statistics and constraints for each nurse @@ -177,8 +184,9 @@ def main(): for i in range(num_nurses): # number of worked days (day or night shift) b = [ - solver.IsEqualCstVar(x[i, j], day_shift) + solver.IsEqualCstVar( - x[i, j], night_shift) for j in range(num_days) + solver.IsEqualCstVar(x[i, j], day_shift) + + solver.IsEqualCstVar(x[i, j], night_shift) + for j in range(num_days) ] solver.Add(nurse_stat[i] == solver.Sum(b)) @@ -221,8 +229,11 @@ def main(): # # solution and search # - db = solver.Phase(day_stat_flat + x_flat + nurse_stat, - solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + day_stat_flat + x_flat + nurse_stat, + solver.CHOOSE_FIRST_UNBOUND, + solver.ASSIGN_MIN_VALUE, + ) solver.NewSearch(db) @@ -238,7 +249,8 @@ def main(): this_day_stat[d] += 1 print(d, end=' ') print( - ' day_stat:', [(d, this_day_stat[d]) for d in this_day_stat], end=' ') + ' day_stat:', [(d, this_day_stat[d]) for d in this_day_stat], end=' ' + ) print('total:', nurse_stat[i].Value(), 'workdays') print() diff --git a/examples/contrib/nurses_cp.py b/examples/contrib/nurses_cp.py index c7d7ed19368..7245f1aac29 100644 --- a/examples/contrib/nurses_cp.py +++ b/examples/contrib/nurses_cp.py @@ -15,8 +15,9 @@ def main(): for j in range(num_nurses): for i in range(num_days): - shifts[(j, i)] = solver.IntVar(0, num_shifts - 1, - "shifts(%i,%i)" % (j, i)) + shifts[(j, i)] = solver.IntVar( + 0, num_shifts - 1, "shifts(%i,%i)" % (j, i) + ) shifts_flat = [ shifts[(j, i)] for j in range(num_nurses) for i in range(num_days) ] @@ -26,8 +27,9 @@ def main(): for j in range(num_shifts): for i in range(num_days): - nurses[(j, i)] = solver.IntVar(0, num_nurses - 1, - "shift%d day%d" % (j, i)) + nurses[(j, i)] = solver.IntVar( + 0, num_nurses - 1, "shift%d day%d" % (j, i) + ) # Set relationships between shifts and nurses. for day in range(num_days): nurses_for_day = [nurses[(j, day)] for j in range(num_shifts)] @@ -53,104 +55,108 @@ def main(): for i in range(num_nurses): for j in range(num_shifts): - solver.Add(works_shift[( - i, j)] == solver.Max([shifts[(i, k)] == j for k in range(num_days)])) + solver.Add( + works_shift[(i, j)] + == solver.Max([shifts[(i, k)] == j for k in range(num_days)]) + ) # For each shift (other than 0), at most 2 nurses are assigned to that shift # during the week. for j in range(1, num_shifts): solver.Add( - solver.Sum([works_shift[(i, j)] for i in range(num_nurses)]) <= 2) + solver.Sum([works_shift[(i, j)] for i in range(num_nurses)]) <= 2 + ) # If s nurses works shifts 2 or 3 on, he must also work that shift the previous # day or the following day. solver.Add( - solver.Max(nurses[(2, - 0)] == nurses[(2, - 1)], nurses[(2, - 1)] == nurses[(2, - 2)]) == 1) + solver.Max( + nurses[(2, 0)] == nurses[(2, 1)], nurses[(2, 1)] == nurses[(2, 2)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(2, - 1)] == nurses[(2, - 2)], nurses[(2, - 2)] == nurses[(2, - 3)]) == 1) + solver.Max( + nurses[(2, 1)] == nurses[(2, 2)], nurses[(2, 2)] == nurses[(2, 3)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(2, - 2)] == nurses[(2, - 3)], nurses[(2, - 3)] == nurses[(2, - 4)]) == 1) + solver.Max( + nurses[(2, 2)] == nurses[(2, 3)], nurses[(2, 3)] == nurses[(2, 4)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(2, - 3)] == nurses[(2, - 4)], nurses[(2, - 4)] == nurses[(2, - 5)]) == 1) + solver.Max( + nurses[(2, 3)] == nurses[(2, 4)], nurses[(2, 4)] == nurses[(2, 5)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(2, - 4)] == nurses[(2, - 5)], nurses[(2, - 5)] == nurses[(2, - 6)]) == 1) + solver.Max( + nurses[(2, 4)] == nurses[(2, 5)], nurses[(2, 5)] == nurses[(2, 6)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(2, - 5)] == nurses[(2, - 6)], nurses[(2, - 6)] == nurses[(2, - 0)]) == 1) + solver.Max( + nurses[(2, 5)] == nurses[(2, 6)], nurses[(2, 6)] == nurses[(2, 0)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(2, - 6)] == nurses[(2, - 0)], nurses[(2, - 0)] == nurses[(2, - 1)]) == 1) + solver.Max( + nurses[(2, 6)] == nurses[(2, 0)], nurses[(2, 0)] == nurses[(2, 1)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(3, - 0)] == nurses[(3, - 1)], nurses[(3, - 1)] == nurses[(3, - 2)]) == 1) + solver.Max( + nurses[(3, 0)] == nurses[(3, 1)], nurses[(3, 1)] == nurses[(3, 2)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(3, - 1)] == nurses[(3, - 2)], nurses[(3, - 2)] == nurses[(3, - 3)]) == 1) + solver.Max( + nurses[(3, 1)] == nurses[(3, 2)], nurses[(3, 2)] == nurses[(3, 3)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(3, - 2)] == nurses[(3, - 3)], nurses[(3, - 3)] == nurses[(3, - 4)]) == 1) + solver.Max( + nurses[(3, 2)] == nurses[(3, 3)], nurses[(3, 3)] == nurses[(3, 4)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(3, - 3)] == nurses[(3, - 4)], nurses[(3, - 4)] == nurses[(3, - 5)]) == 1) + solver.Max( + nurses[(3, 3)] == nurses[(3, 4)], nurses[(3, 4)] == nurses[(3, 5)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(3, - 4)] == nurses[(3, - 5)], nurses[(3, - 5)] == nurses[(3, - 6)]) == 1) + solver.Max( + nurses[(3, 4)] == nurses[(3, 5)], nurses[(3, 5)] == nurses[(3, 6)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(3, - 5)] == nurses[(3, - 6)], nurses[(3, - 6)] == nurses[(3, - 0)]) == 1) + solver.Max( + nurses[(3, 5)] == nurses[(3, 6)], nurses[(3, 6)] == nurses[(3, 0)] + ) + == 1 + ) solver.Add( - solver.Max(nurses[(3, - 6)] == nurses[(3, - 0)], nurses[(3, - 0)] == nurses[(3, - 1)]) == 1) + solver.Max( + nurses[(3, 6)] == nurses[(3, 0)], nurses[(3, 0)] == nurses[(3, 1)] + ) + == 1 + ) # Create the decision builder. - db = solver.Phase(shifts_flat, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + shifts_flat, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) # Create the solution collector. solution = solver.Assignment() solution.Add(shifts_flat) @@ -169,8 +175,9 @@ def main(): for i in range(num_days): print("Day", i) for j in range(num_nurses): - print("Nurse", j, "assigned to task", - collector.Value(sol, shifts[(j, i)])) + print( + "Nurse", j, "assigned to task", collector.Value(sol, shifts[(j, i)]) + ) print() diff --git a/examples/contrib/olympic.py b/examples/contrib/olympic.py index 9a6cc978afb..510d665a0d1 100644 --- a/examples/contrib/olympic.py +++ b/examples/contrib/olympic.py @@ -13,47 +13,47 @@ # limitations under the License. """ - Olympic puzzle in Google CP Solver. +Olympic puzzle in Google CP Solver. - Benchmark for Prolog (BProlog) - ''' - File : olympic.pl - Author : Neng-Fa ZHOU - Date : 1993 +Benchmark for Prolog (BProlog) +''' +File : olympic.pl +Author : Neng-Fa ZHOU +Date : 1993 - Purpose: solve a puzzle taken from Olympic Arithmetic Contest +Purpose: solve a puzzle taken from Olympic Arithmetic Contest - Given ten variables with the following configuration: +Given ten variables with the following configuration: - X7 X8 X9 X10 + X7 X8 X9 X10 - X4 X5 X6 + X4 X5 X6 - X2 X3 + X2 X3 - X1 + X1 - We already know that X1 is equal to 3 and want to assign each variable - with a different integer from {1,2,...,10} such that for any three - variables - Xi Xj +We already know that X1 is equal to 3 and want to assign each variable +with a different integer from {1,2,...,10} such that for any three +variables + Xi Xj - Xk - the following constraint is satisfied: + Xk +the following constraint is satisfied: - |Xi-Xj| = Xk - ''' + |Xi-Xj| = Xk +''' - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/olympic.mzn - * SICStus Prolog: http://www.hakank.org/sicstus/olympic.pl - * ECLiPSe: http://hakank.org/eclipse/olympic.ecl - * Gecode: http://hakank.org/gecode/olympic.cpp +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/olympic.mzn +* SICStus Prolog: http://www.hakank.org/sicstus/olympic.pl +* ECLiPSe: http://hakank.org/eclipse/olympic.ecl +* Gecode: http://hakank.org/gecode/olympic.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp diff --git a/examples/contrib/organize_day.py b/examples/contrib/organize_day.py index 92c3776e881..bbdd511e9e6 100644 --- a/examples/contrib/organize_day.py +++ b/examples/contrib/organize_day.py @@ -13,23 +13,23 @@ # limitations under the License. """ - Organizing a day in Google CP Solver. +Organizing a day in Google CP Solver. - Simple scheduling problem. +Simple scheduling problem. - Problem formulation from ECLiPSe: - Slides on (Finite Domain) Constraint Logic Programming, page 38f - http://eclipse-clp.org/reports/eclipse.ppt +Problem formulation from ECLiPSe: +Slides on (Finite Domain) Constraint Logic Programming, page 38f +http://eclipse-clp.org/reports/eclipse.ppt - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/organize_day.mzn - * Comet: http://www.hakank.org/comet/organize_day.co - * Gecode: http://hakank.org/gecode/organize_day.cpp +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/organize_day.mzn +* Comet: http://www.hakank.org/comet/organize_day.co +* Gecode: http://hakank.org/gecode/organize_day.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -84,7 +84,7 @@ def main(): no_overlap(solver, begins[i], durations[i], begins[j], durations[j]) # specific constraints - for (before, after) in before_tasks: + for before, after in before_tasks: solver.Add(ends[before] <= begins[after]) solver.Add(begins[work] >= 11) @@ -92,8 +92,9 @@ def main(): # # solution and search # - db = solver.Phase(begins + ends, solver.INT_VAR_DEFAULT, - solver.INT_VALUE_DEFAULT) + db = solver.Phase( + begins + ends, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT + ) solver.NewSearch(db) diff --git a/examples/contrib/p_median.py b/examples/contrib/p_median.py index 433e24230aa..86c2a9c7efe 100644 --- a/examples/contrib/p_median.py +++ b/examples/contrib/p_median.py @@ -13,25 +13,25 @@ # limitations under the License. """ - P-median problem in Google CP Solver. - - Model and data from the OPL Manual, which describes the problem: - ''' - The P-Median problem is a well known problem in Operations Research. - The problem can be stated very simply, like this: given a set of customers - with known amounts of demand, a set of candidate locations for warehouses, - and the distance between each pair of customer-warehouse, choose P - warehouses to open that minimize the demand-weighted distance of serving - all customers from those P warehouses. - ''' - - Compare with the following models: - * MiniZinc: http://hakank.org/minizinc/p_median.mzn - * Comet: http://hakank.org/comet/p_median.co - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +P-median problem in Google CP Solver. + +Model and data from the OPL Manual, which describes the problem: +''' +The P-Median problem is a well known problem in Operations Research. +The problem can be stated very simply, like this: given a set of customers +with known amounts of demand, a set of candidate locations for warehouses, +and the distance between each pair of customer-warehouse, choose P +warehouses to open that minimize the demand-weighted distance of serving +all customers from those P warehouses. +''' + +Compare with the following models: +* MiniZinc: http://hakank.org/minizinc/p_median.mzn +* Comet: http://hakank.org/comet/p_median.co + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -95,8 +95,9 @@ def main(): # # solution and search # - db = solver.Phase(open + ship_flat, solver.INT_VAR_DEFAULT, - solver.INT_VALUE_DEFAULT) + db = solver.Phase( + open + ship_flat, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT + ) solver.NewSearch(db, [objective]) diff --git a/examples/contrib/pandigital_numbers.py b/examples/contrib/pandigital_numbers.py index d180cf3d019..a112d4b4bc8 100644 --- a/examples/contrib/pandigital_numbers.py +++ b/examples/contrib/pandigital_numbers.py @@ -13,51 +13,51 @@ # limitations under the License. """ - Pandigital numbers in Google CP Solver. - - From Albert H. Beiler 'Recreations in the Theory of Numbers', - quoted from http://www.worldofnumbers.com/ninedig1.htm - ''' - Chapter VIII : Digits - and the magic of 9 - - The following curious table shows how to arrange the 9 digits so that - the product of 2 groups is equal to a number represented by the - remaining digits. - - 12 x 483 = 5796 - 42 x 138 = 5796 - 18 x 297 = 5346 - 27 x 198 = 5346 - 39 x 186 = 7254 - 48 x 159 = 7632 - 28 x 157 = 4396 - 4 x 1738 = 6952 - 4 x 1963 = 7852 - ''' - - See also MathWorld http://mathworld.wolfram.com/PandigitalNumber.html - ''' - A number is said to be pandigital if it contains each of the digits - from 0 to 9 (and whose leading digit must be nonzero). However, - 'zeroless' pandigital quantities contain the digits 1 through 9. - Sometimes exclusivity is also required so that each digit is - restricted to appear exactly once. - ''' - - * Wikipedia http://en.wikipedia.org/wiki/Pandigital_number - - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/pandigital_numbers.mzn - * Comet : http://www.hakank.org/comet/pandigital_numbers.co - * ECLiPSe : http://www.hakank.org/eclipse/pandigital_numbers.ecl - * Gecode/R: http://www.hakank.org/gecoder/pandigital_numbers.rb - * ECLiPSe : http://hakank.org/eclipse/pandigital_numbers.ecl - * SICStus : http://hakank.org/sicstus/pandigital_numbers.pl - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Pandigital numbers in Google CP Solver. + +From Albert H. Beiler 'Recreations in the Theory of Numbers', +quoted from http://www.worldofnumbers.com/ninedig1.htm +''' +Chapter VIII : Digits - and the magic of 9 + +The following curious table shows how to arrange the 9 digits so that +the product of 2 groups is equal to a number represented by the +remaining digits. + + 12 x 483 = 5796 + 42 x 138 = 5796 + 18 x 297 = 5346 + 27 x 198 = 5346 + 39 x 186 = 7254 + 48 x 159 = 7632 + 28 x 157 = 4396 + 4 x 1738 = 6952 + 4 x 1963 = 7852 +''' + +See also MathWorld http://mathworld.wolfram.com/PandigitalNumber.html +''' +A number is said to be pandigital if it contains each of the digits +from 0 to 9 (and whose leading digit must be nonzero). However, +'zeroless' pandigital quantities contain the digits 1 through 9. +Sometimes exclusivity is also required so that each digit is +restricted to appear exactly once. +''' + +* Wikipedia http://en.wikipedia.org/wiki/Pandigital_number + + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/pandigital_numbers.mzn +* Comet : http://www.hakank.org/comet/pandigital_numbers.co +* ECLiPSe : http://www.hakank.org/eclipse/pandigital_numbers.ecl +* Gecode/R: http://www.hakank.org/gecoder/pandigital_numbers.rb +* ECLiPSe : http://hakank.org/eclipse/pandigital_numbers.ecl +* SICStus : http://hakank.org/sicstus/pandigital_numbers.pl + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -72,7 +72,8 @@ def toNum(solver, t, s, base): tlen = len(t) solver.Add( - s == solver.Sum([(base**(tlen - i - 1)) * t[i] for i in range(tlen)])) + s == solver.Sum([(base ** (tlen - i - 1)) * t[i] for i in range(tlen)]) + ) def main(base=10, start=1, len1=1, len2=4): diff --git a/examples/contrib/permutation_flow_shop.py b/examples/contrib/permutation_flow_shop.py index 505ce7dbfdf..3a6aaf72738 100644 --- a/examples/contrib/permutation_flow_shop.py +++ b/examples/contrib/permutation_flow_shop.py @@ -51,141 +51,138 @@ @dataclass class TaskType: - """ - Small wrapper to hold the start, end, and interval variables of a task. - """ + """ + Small wrapper to hold the start, end, and interval variables of a task. + """ - start: cp_model.IntVar - end: cp_model.IntVar - interval: cp_model.IntervalVar + start: cp_model.IntVar + end: cp_model.IntVar + interval: cp_model.IntervalVar def permutation_flow_shop( - processing_times: np.ndarray, - time_limit: float, - log: bool, - params: str + processing_times: np.ndarray, time_limit: float, log: bool, params: str ): - """ - Solves the given permutation flow shop problem instance with OR-Tools. - - Parameters - ---------- - processing_times - An n-by-m matrix of processing times of the jobs on the machines. - time_limit - The time limit in seconds. If not set, the solver runs until an - optimal solution is found. - log - Whether to log the solver output. Default is False. - - Raises - ------ - ValueError - If the number of lines is greater than 1, i.e., the instance is a - distributed permutation flow shop problem. - """ - m = cp_model.CpModel() - num_jobs, num_machines = processing_times.shape - horizon = processing_times.sum() - - # Create interval variables for all tasks (each job/machine pair). - tasks = {} - for job, machine in product(range(num_jobs), range(num_machines)): - start = m.new_int_var(0, horizon, "") - end = m.new_int_var(0, horizon, "") - duration = processing_times[job][machine] - interval = m.new_interval_var(start, duration, end, "") - tasks[job, machine] = TaskType(start, end, interval) - - # No overlap for all job intervals on this machine. - for machine in range(num_machines): - intervals = [tasks[job, machine].interval for job in range(num_jobs)] - m.add_no_overlap(intervals) - - # Add precedence constraints between tasks of the same job. - for job, machine in product(range(num_jobs), range(num_machines - 1)): - pred = tasks[job, machine] - succ = tasks[job, machine + 1] - m.add(pred.end <= succ.start) - - # Create arcs for circuit constraints. - arcs = [] - for idx1 in range(num_jobs): - arcs.append((0, idx1 + 1, m.new_bool_var("start"))) - arcs.append((idx1 + 1, 0, m.new_bool_var("end"))) - - lits = {} + """ + Solves the given permutation flow shop problem instance with OR-Tools. + + Parameters + ---------- + processing_times + An n-by-m matrix of processing times of the jobs on the machines. + time_limit + The time limit in seconds. If not set, the solver runs until an + optimal solution is found. + log + Whether to log the solver output. Default is False. + + Raises + ------ + ValueError + If the number of lines is greater than 1, i.e., the instance is a + distributed permutation flow shop problem. + """ + m = cp_model.CpModel() + num_jobs, num_machines = processing_times.shape + horizon = processing_times.sum() + + # Create interval variables for all tasks (each job/machine pair). + tasks = {} + for job, machine in product(range(num_jobs), range(num_machines)): + start = m.new_int_var(0, horizon, "") + end = m.new_int_var(0, horizon, "") + duration = processing_times[job][machine] + interval = m.new_interval_var(start, duration, end, "") + tasks[job, machine] = TaskType(start, end, interval) + + # No overlap for all job intervals on this machine. + for machine in range(num_machines): + intervals = [tasks[job, machine].interval for job in range(num_jobs)] + m.add_no_overlap(intervals) + + # Add precedence constraints between tasks of the same job. + for job, machine in product(range(num_jobs), range(num_machines - 1)): + pred = tasks[job, machine] + succ = tasks[job, machine + 1] + m.add(pred.end <= succ.start) + + # Create arcs for circuit constraints. + arcs = [] + for idx1 in range(num_jobs): + arcs.append((0, idx1 + 1, m.new_bool_var("start"))) + arcs.append((idx1 + 1, 0, m.new_bool_var("end"))) + + lits = {} + for idx1, idx2 in product(range(num_jobs), repeat=2): + if idx1 != idx2: + lit = m.new_bool_var(f"{idx1} -> {idx2}") + lits[idx1, idx2] = lit + arcs.append((idx1 + 1, idx2 + 1, lit)) + + m.add_circuit(arcs) + + # Enforce that the permutation of jobs is the same on all machines. + for machine in range(num_machines): + starts = [tasks[job, machine].start for job in range(num_jobs)] + ends = [tasks[job, machine].end for job in range(num_jobs)] + for idx1, idx2 in product(range(num_jobs), repeat=2): - if idx1 != idx2: - lit = m.new_bool_var(f"{idx1} -> {idx2}") - lits[idx1, idx2] = lit - arcs.append((idx1 + 1, idx2 + 1, lit)) - - m.add_circuit(arcs) - - # Enforce that the permutation of jobs is the same on all machines. - for machine in range(num_machines): - starts = [tasks[job, machine].start for job in range(num_jobs)] - ends = [tasks[job, machine].end for job in range(num_jobs)] - - for idx1, idx2 in product(range(num_jobs), repeat=2): - if idx1 == idx2: - continue - - # Since all machines share the same arc literals, if the literal - # i -> j is True, this enforces that job i is always scheduled - # before job j on all machines. - lit = lits[idx1, idx2] - m.add(ends[idx1] <= starts[idx2]).only_enforce_if(lit) - - # Set minimizing makespan as objective. - obj_var = m.new_int_var(0, horizon, "makespan") - completion_times = [ - tasks[(job, num_machines - 1)].end for job in range(num_jobs) - ] - m.add_max_equality(obj_var, completion_times) - m.minimize(obj_var) - - solver = cp_model.CpSolver() - if params: - text_format.Parse(params, solver.parameters) - solver.parameters.log_search_progress = log - solver.parameters.max_time_in_seconds = time_limit - - status_code = solver.Solve(m) - status = solver.StatusName(status_code) - - print(f"Status: {status}") - print(f"Makespan: {solver.ObjectiveValue()}") - - if status in ["OPTIMAL", "FEASIBLE"]: - start = [solver.Value(tasks[job, 0].start) for job in range(num_jobs)] - solution = np.argsort(start) + 1 - print(f"Solution: {solution}") + if idx1 == idx2: + continue + + # Since all machines share the same arc literals, if the literal + # i -> j is True, this enforces that job i is always scheduled + # before job j on all machines. + lit = lits[idx1, idx2] + m.add(ends[idx1] <= starts[idx2]).only_enforce_if(lit) + + # Set minimizing makespan as objective. + obj_var = m.new_int_var(0, horizon, "makespan") + completion_times = [ + tasks[(job, num_machines - 1)].end for job in range(num_jobs) + ] + m.add_max_equality(obj_var, completion_times) + m.minimize(obj_var) + + solver = cp_model.CpSolver() + if params: + text_format.Parse(params, solver.parameters) + solver.parameters.log_search_progress = log + solver.parameters.max_time_in_seconds = time_limit + + status_code = solver.Solve(m) + status = solver.StatusName(status_code) + + print(f"Status: {status}") + print(f"Makespan: {solver.ObjectiveValue()}") + + if status in ["OPTIMAL", "FEASIBLE"]: + start = [solver.Value(tasks[job, 0].start) for job in range(num_jobs)] + solution = np.argsort(start) + 1 + print(f"Solution: {solution}") def main(argv: Sequence[str]) -> None: - """Creates the data and calls the solving procedure.""" - # VRF_10_5_2 instance from http://soa.iti.es/problem-instances. - # Optimal makespan is 698. - processing_times = [ - [79, 67, 10, 48, 52], - [40, 40, 57, 21, 54], - [48, 93, 49, 11, 79], - [16, 23, 19, 2, 38], - [38, 90, 57, 73, 3], - [76, 13, 99, 98, 55], - [73, 85, 40, 20, 85], - [34, 6, 27, 53, 21], - [38, 6, 35, 28, 44], - [32, 11, 11, 34, 27], - ] - - permutation_flow_shop( - np.array(processing_times), _TIME_LIMIT.value, _LOG.value, _PARAMS.value - ) + """Creates the data and calls the solving procedure.""" + # VRF_10_5_2 instance from http://soa.iti.es/problem-instances. + # Optimal makespan is 698. + processing_times = [ + [79, 67, 10, 48, 52], + [40, 40, 57, 21, 54], + [48, 93, 49, 11, 79], + [16, 23, 19, 2, 38], + [38, 90, 57, 73, 3], + [76, 13, 99, 98, 55], + [73, 85, 40, 20, 85], + [34, 6, 27, 53, 21], + [38, 6, 35, 28, 44], + [32, 11, 11, 34, 27], + ] + + permutation_flow_shop( + np.array(processing_times), _TIME_LIMIT.value, _LOG.value, _PARAMS.value + ) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/contrib/photo_problem.py b/examples/contrib/photo_problem.py index 6a513d84801..7ce6c809b13 100644 --- a/examples/contrib/photo_problem.py +++ b/examples/contrib/photo_problem.py @@ -13,37 +13,37 @@ # limitations under the License. """ - Photo problem in Google CP Solver. +Photo problem in Google CP Solver. - Problem statement from Mozart/Oz tutorial: - http://www.mozart-oz.org/home/doc/fdt/node37.html#section.reified.photo - ''' - Betty, Chris, Donald, Fred, Gary, Mary, and Paul want to align in one - row for taking a photo. Some of them have preferences next to whom - they want to stand: +Problem statement from Mozart/Oz tutorial: +http://www.mozart-oz.org/home/doc/fdt/node37.html#section.reified.photo +''' +Betty, Chris, Donald, Fred, Gary, Mary, and Paul want to align in one +row for taking a photo. Some of them have preferences next to whom +they want to stand: - 1. Betty wants to stand next to Gary and Mary. - 2. Chris wants to stand next to Betty and Gary. - 3. Fred wants to stand next to Mary and Donald. - 4. Paul wants to stand next to Fred and Donald. + 1. Betty wants to stand next to Gary and Mary. + 2. Chris wants to stand next to Betty and Gary. + 3. Fred wants to stand next to Mary and Donald. + 4. Paul wants to stand next to Fred and Donald. - Obviously, it is impossible to satisfy all preferences. Can you find - an alignment that maximizes the number of satisfied preferences? - ''' +Obviously, it is impossible to satisfy all preferences. Can you find +an alignment that maximizes the number of satisfied preferences? +''' - Oz solution: - 6 # alignment(betty:5 chris:6 donald:1 fred:3 gary:7 mary:4 paul:2) - [5, 6, 1, 3, 7, 4, 2] +Oz solution: + 6 # alignment(betty:5 chris:6 donald:1 fred:3 gary:7 mary:4 paul:2) +[5, 6, 1, 3, 7, 4, 2] - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/photo_hkj.mzn - * Comet: http://hakank.org/comet/photo_problem.co - * SICStus: http://hakank.org/sicstus/photo_problem.pl +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/photo_hkj.mzn +* Comet: http://hakank.org/comet/photo_problem.co +* SICStus: http://hakank.org/sicstus/photo_problem.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -69,7 +69,7 @@ def main(show_all_max=0): [0, 0, 1, 0, 0, 1, 0], # Fred 3 [0, 0, 0, 0, 0, 0, 0], # Gary 4 [0, 0, 0, 0, 0, 0, 0], # Mary 5 - [0, 0, 1, 1, 0, 0, 0] # Paul 6 + [0, 0, 1, 1, 0, 0, 0], # Paul 6 ] print("""Preferences: @@ -115,8 +115,9 @@ def main(show_all_max=0): # # search and result # - db = solver.Phase(positions, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MAX_VALUE) + db = solver.Phase( + positions, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MAX_VALUE + ) if show_all_max == 0: solver.NewSearch(db, [objective]) @@ -128,8 +129,9 @@ def main(show_all_max=0): print("z:", z.Value()) p = [positions[i].Value() for i in range(n)] - print(" ".join( - [persons[j] for i in range(n) for j in range(n) if p[j] == i])) + print( + " ".join([persons[j] for i in range(n) for j in range(n) if p[j] == i]) + ) print("Successful preferences:") for i in range(n): for j in range(n): diff --git a/examples/contrib/place_number_puzzle.py b/examples/contrib/place_number_puzzle.py index 40162e59cef..ba8ea59a7db 100644 --- a/examples/contrib/place_number_puzzle.py +++ b/examples/contrib/place_number_puzzle.py @@ -52,10 +52,40 @@ def main(): m = 32 n = 8 # Note: this is 1-based for compatibility (and lazyness) - graph = [[1, 2], [1, 3], [1, 4], [2, 1], [2, 3], [2, 5], [2, 6], [3, 2], - [3, 4], [3, 6], [3, 7], [4, 1], [4, 3], [4, 6], [4, 7], [5, 2], - [5, 3], [5, 6], [5, 8], [6, 2], [6, 3], [6, 4], [6, 5], [6, 7], - [6, 8], [7, 3], [7, 4], [7, 6], [7, 8], [8, 5], [8, 6], [8, 7]] + graph = [ + [1, 2], + [1, 3], + [1, 4], + [2, 1], + [2, 3], + [2, 5], + [2, 6], + [3, 2], + [3, 4], + [3, 6], + [3, 7], + [4, 1], + [4, 3], + [4, 6], + [4, 7], + [5, 2], + [5, 3], + [5, 6], + [5, 8], + [6, 2], + [6, 3], + [6, 4], + [6, 5], + [6, 7], + [6, 8], + [7, 3], + [7, 4], + [7, 6], + [7, 8], + [8, 5], + [8, 6], + [8, 7], + ] # declare variables x = [solver.IntVar(1, n, "x%i" % i) for i in range(n)] @@ -81,7 +111,8 @@ def main(): solver.Solve( solver.Phase(x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE), - [collector]) + [collector], + ) num_solutions = collector.SolutionCount() for s in range(num_solutions): diff --git a/examples/contrib/post_office_problem2.py b/examples/contrib/post_office_problem2.py index bfe43a10c56..8c237344873 100644 --- a/examples/contrib/post_office_problem2.py +++ b/examples/contrib/post_office_problem2.py @@ -13,45 +13,45 @@ # limitations under the License. """ - Post office problem in Google CP Solver. - - Problem statement: - http://www-128.ibm.com/developerworks/linux/library/l-glpk2/ - - From Winston 'Operations Research: Applications and Algorithms': - ''' - A post office requires a different number of full-time employees working - on different days of the week [summarized below]. Union rules state that - each full-time employee must work for 5 consecutive days and then receive - two days off. For example, an employee who works on Monday to Friday - must be off on Saturday and Sunday. The post office wants to meet its - daily requirements using only full-time employees. Minimize the number - of employees that must be hired. - - To summarize the important information about the problem: - - * Every full-time worker works for 5 consecutive days and takes 2 days off - * Day 1 (Monday): 17 workers needed - * Day 2 : 13 workers needed - * Day 3 : 15 workers needed - * Day 4 : 19 workers needed - * Day 5 : 14 workers needed - * Day 6 : 16 workers needed - * Day 7 (Sunday) : 11 workers needed - - The post office needs to minimize the number of employees it needs - to hire to meet its demand. - ''' - - * MiniZinc: http://www.hakank.org/minizinc/post_office_problem2.mzn - * SICStus: http://www.hakank.org/sicstus/post_office_problem2.pl - * ECLiPSe: http://www.hakank.org/eclipse/post_office_problem2.ecl - * Gecode: http://www.hakank.org/gecode/post_office_problem2.cpp - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Post office problem in Google CP Solver. + +Problem statement: +http://www-128.ibm.com/developerworks/linux/library/l-glpk2/ + +From Winston 'Operations Research: Applications and Algorithms': +''' +A post office requires a different number of full-time employees working +on different days of the week [summarized below]. Union rules state that +each full-time employee must work for 5 consecutive days and then receive +two days off. For example, an employee who works on Monday to Friday +must be off on Saturday and Sunday. The post office wants to meet its +daily requirements using only full-time employees. Minimize the number +of employees that must be hired. + +To summarize the important information about the problem: + + * Every full-time worker works for 5 consecutive days and takes 2 days off + * Day 1 (Monday): 17 workers needed + * Day 2 : 13 workers needed + * Day 3 : 15 workers needed + * Day 4 : 19 workers needed + * Day 5 : 14 workers needed + * Day 6 : 16 workers needed + * Day 7 (Sunday) : 11 workers needed + +The post office needs to minimize the number of employees it needs +to hire to meet its demand. +''' + +* MiniZinc: http://www.hakank.org/minizinc/post_office_problem2.mzn +* SICStus: http://www.hakank.org/sicstus/post_office_problem2.pl +* ECLiPSe: http://www.hakank.org/eclipse/post_office_problem2.ecl +* Gecode: http://www.hakank.org/gecode/post_office_problem2.cpp + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -94,7 +94,8 @@ def main(): for i in days: s = solver.Sum( - [x[j] for j in days if j != (i + 5) % n and j != (i + 6) % n]) + [x[j] for j in days if j != (i + 5) % n and j != (i + 6) % n] + ) solver.Add(s >= need[i]) # objective @@ -103,8 +104,9 @@ def main(): # # search and result # - db = solver.Phase(x, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + x, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db, [objective]) diff --git a/examples/contrib/production.py b/examples/contrib/production.py index df43b82613b..8ec75b7e543 100644 --- a/examples/contrib/production.py +++ b/examples/contrib/production.py @@ -13,13 +13,13 @@ # limitations under the License. """ - Production planning problem in Google or-tools. +Production planning problem in Google or-tools. - From the OPL model production.mod. +From the OPL model production.mod. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp @@ -73,8 +73,9 @@ def main(): # for r in range(num_resources): solver.Add( - solver.Sum([consumption[p][r] * inside[p] - for p in range(num_products)]) <= capacity[r]) + solver.Sum([consumption[p][r] * inside[p] for p in range(num_products)]) + <= capacity[r] + ) for p in range(num_products): solver.Add(inside[p] + outside[p] >= demand[p]) @@ -94,9 +95,15 @@ def main(): '(ReducedCost:', inside[p].ReducedCost(), ')', - end=' ') - print('outside:', outside[p].SolutionValue(), ' (ReducedCost:', - outside[p].ReducedCost(), ')') + end=' ', + ) + print( + 'outside:', + outside[p].SolutionValue(), + ' (ReducedCost:', + outside[p].ReducedCost(), + ')', + ) print() diff --git a/examples/contrib/project_scheduling_sat.py b/examples/contrib/project_scheduling_sat.py index a88e657a85e..6614690c5ca 100644 --- a/examples/contrib/project_scheduling_sat.py +++ b/examples/contrib/project_scheduling_sat.py @@ -1,23 +1,22 @@ - from ortools.sat.python import cp_model -#project name, duration, starts earliest, ends latest, demand role A, demand role S, demand role J +# project name, duration, starts earliest, ends latest, demand role A, demand role S, demand role J projects = [ - ['P1',3, 0, 9, 1, 0, 1], - ['P2',8, 0, 9, 1, 1, 0], - ['P3',3, 0, 9, 1, 0, 2], - ['P4',4, 0, 9, 1, 0, 1] - ] + ['P1', 3, 0, 9, 1, 0, 1], + ['P2', 8, 0, 9, 1, 1, 0], + ['P3', 3, 0, 9, 1, 0, 2], + ['P4', 4, 0, 9, 1, 0, 1], +] num_projects = len(projects) -roles = ['A','S','J'] +roles = ['A', 'S', 'J'] -#Roles available at each time step +# Roles available at each time step available_roles = [ - [2,2,2,2,2,2,2,2,2,2], #Role A - [1,1,1,1,1,1,1,1,1,1], #Role S - [1,1,1,1,1,1,1,2,2,2] #Role J + [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], # Role A + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], # Role S + [1, 1, 1, 1, 1, 1, 1, 2, 2, 2], # Role J ] all_projects = range(num_projects) @@ -26,12 +25,21 @@ # Creates the model. model = cp_model.CpModel() -#Creating decision variables +# Creating decision variables -#starts and ends of the projects -starts = [model.NewIntVar(projects[j][2], projects[j][3] + 1 , 'start_%i' % j) for j in all_projects] -ends = [model.NewIntVar(projects[j][2], projects[j][3] + 1, 'end_%i' % j) for j in all_projects] -intervals = [model.NewIntervalVar(starts[j], projects[j][1], ends[j], 'interval_%i' % j) for j in all_projects] +# starts and ends of the projects +starts = [ + model.NewIntVar(projects[j][2], projects[j][3] + 1, 'start_%i' % j) + for j in all_projects +] +ends = [ + model.NewIntVar(projects[j][2], projects[j][3] + 1, 'end_%i' % j) + for j in all_projects +] +intervals = [ + model.NewIntervalVar(starts[j], projects[j][1], ends[j], 'interval_%i' % j) + for j in all_projects +] # Role A has a capacity 2. Every project uses it. demands = [1 for _ in all_projects] @@ -46,13 +54,13 @@ demands_for_project_j = [projects[j][6] for j in all_projects] + [1] model.AddCumulative(intervals_for_project_j, demands_for_project_j, 2) -#We want the projects to start as early as possible +# We want the projects to start as early as possible model.Minimize(sum(starts)) # Solve model. solver = cp_model.CpSolver() solver.parameters.log_search_progress = True -status=solver.Solve(model) +status = solver.Solve(model) for it in zip(starts, ends): - print("[%i, %i]" % (solver.Value(it[0]), solver.Value(it[1]))) + print('[%i, %i]' % (solver.Value(it[0]), solver.Value(it[1]))) diff --git a/examples/contrib/pyls_api.py b/examples/contrib/pyls_api.py index 0d30153647c..b0798a9b2e8 100755 --- a/examples/contrib/pyls_api.py +++ b/examples/contrib/pyls_api.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 from ortools.constraint_solver import pywrapcp + class OneVarLns(pywrapcp.BaseLns): """One Var LNS.""" @@ -55,8 +56,13 @@ def __init__(self, vars): def OnSynchronize(self, delta): self.__sum = sum(self.Value(index) for index in range(self.Size())) - def Accept(self, delta, unused_delta_delta, unused_objective_min, - unused_objective_max): + def Accept( + self, + delta, + unused_delta_delta, + unused_objective_min, + unused_objective_max, + ): solution_delta = delta.IntVarContainer() solution_delta_size = solution_delta.Size() for i in range(solution_delta_size): @@ -101,8 +107,9 @@ def Solve(type): move_one_var = MoveOneVar(vars) sum_filter = SumFilter(vars) filter_manager = pywrapcp.LocalSearchFilterManager([sum_filter]) - ls_params = solver.LocalSearchPhaseParameters(sum_var, move_one_var, db, None, - filter_manager) + ls_params = solver.LocalSearchPhaseParameters( + sum_var, move_one_var, db, None, filter_manager + ) ls = solver.LocalSearchPhase(vars, db, ls_params) collector = solver.LastSolutionCollector() diff --git a/examples/contrib/quasigroup_completion.py b/examples/contrib/quasigroup_completion.py index b675eac9bca..df35fa51af7 100644 --- a/examples/contrib/quasigroup_completion.py +++ b/examples/contrib/quasigroup_completion.py @@ -13,44 +13,44 @@ # limitations under the License. """ - Quasigroup completion Google CP Solver. - - - See Carla P. Gomes and David Shmoys: - "Completing Quasigroups or Latin Squares: Structured Graph Coloring Problem" - - See also - Ivars Peterson "Completing Latin Squares" - http://www.maa.org/mathland/mathtrek_5_8_00.html +Quasigroup completion Google CP Solver. + + +See Carla P. Gomes and David Shmoys: +"Completing Quasigroups or Latin Squares: Structured Graph Coloring Problem" + +See also +Ivars Peterson "Completing Latin Squares" +http://www.maa.org/mathland/mathtrek_5_8_00.html +''' + Using only the numbers 1, 2, 3, and 4, arrange four sets of these numbers + into + a four-by-four array so that no column or row contains the same two numbers. + The result is known as a Latin square. + ... + The so-called quasigroup completion problem concerns a table that is + correctly + but only partially filled in. The question is whether the remaining blanks + in + the table can be filled in to obtain a complete Latin square (or a proper + quasigroup multiplication table). ''' - Using only the numbers 1, 2, 3, and 4, arrange four sets of these numbers - into - a four-by-four array so that no column or row contains the same two numbers. - The result is known as a Latin square. - ... - The so-called quasigroup completion problem concerns a table that is - correctly - but only partially filled in. The question is whether the remaining blanks - in - the table can be filled in to obtain a complete Latin square (or a proper - quasigroup multiplication table). - ''' - - Compare with the following models: - * Choco: http://www.hakank.org/choco/QuasigroupCompletion.java - * Comet: http://www.hakank.org/comet/quasigroup_completion.co - * ECLiPSE: http://www.hakank.org/eclipse/quasigroup_completion.ecl - * Gecode: http://www.hakank.org/gecode/quasigroup_completion.cpp - * Gecode/R: http://www.hakank.org/gecode_r/quasigroup_completion.rb - * JaCoP: http://www.hakank.org/JaCoP/QuasigroupCompletion.java - * MiniZinc: http://www.hakank.org/minizinc/quasigroup_completion.mzn - * Tailor/Essence': http://www.hakank.org/tailor/quasigroup_completion.eprime - * SICStus: http://hakank.org/sicstus/quasigroup_completion.pl - * Zinc: http://hakank.org/minizinc/quasigroup_completion.zinc - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ + + Compare with the following models: + * Choco: http://www.hakank.org/choco/QuasigroupCompletion.java + * Comet: http://www.hakank.org/comet/quasigroup_completion.co + * ECLiPSE: http://www.hakank.org/eclipse/quasigroup_completion.ecl + * Gecode: http://www.hakank.org/gecode/quasigroup_completion.cpp + * Gecode/R: http://www.hakank.org/gecode_r/quasigroup_completion.rb + * JaCoP: http://www.hakank.org/JaCoP/QuasigroupCompletion.java + * MiniZinc: http://www.hakank.org/minizinc/quasigroup_completion.mzn + * Tailor/Essence': http://www.hakank.org/tailor/quasigroup_completion.eprime + * SICStus: http://hakank.org/sicstus/quasigroup_completion.pl + * Zinc: http://hakank.org/minizinc/quasigroup_completion.zinc + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -59,8 +59,13 @@ X = 0 # default problem # (This is the same as quasigroup1.txt) -default_puzzle = [[1, X, X, X, 4], [X, 5, X, X, X], [4, X, X, 2, X], - [X, 4, X, X, X], [X, X, 5, X, 1]] +default_puzzle = [ + [1, X, X, X, 4], + [X, 5, X, X, X], + [4, X, X, 2, X], + [X, 4, X, X, X], + [X, X, 5, X, 1], +] def main(puzzle="", n=0): diff --git a/examples/contrib/regular.py b/examples/contrib/regular.py index 9c7a094c800..3937c871069 100644 --- a/examples/contrib/regular.py +++ b/examples/contrib/regular.py @@ -13,28 +13,28 @@ # limitations under the License. """ - Global constraint regular in Google CP Solver. +Global constraint regular in Google CP Solver. - This is a translation of MiniZinc's regular constraint (defined in - lib/zinc/globals.mzn). All comments are from the MiniZinc code. - ''' - The sequence of values in array 'x' (which must all be in the range 1..S) - is accepted by the DFA of 'Q' states with input 1..S and transition - function 'd' (which maps (1..Q, 1..S) -> 0..Q)) and initial state 'q0' - (which must be in 1..Q) and accepting states 'F' (which all must be in - 1..Q). We reserve state 0 to be an always failing state. - ''' +This is a translation of MiniZinc's regular constraint (defined in +lib/zinc/globals.mzn). All comments are from the MiniZinc code. +''' +The sequence of values in array 'x' (which must all be in the range 1..S) +is accepted by the DFA of 'Q' states with input 1..S and transition +function 'd' (which maps (1..Q, 1..S) -> 0..Q)) and initial state 'q0' +(which must be in 1..Q) and accepting states 'F' (which all must be in +1..Q). We reserve state 0 to be an always failing state. +''' - It is, however, translated from the Comet model: - * Comet: http://www.hakank.org/comet/regular.co +It is, however, translated from the Comet model: +* Comet: http://www.hakank.org/comet/regular.co - Here we test with the following regular expression: - 0*1{3}0+1{2}0+1{1}0* - using an array of size 10. +Here we test with the following regular expression: + 0*1{3}0+1{2}0+1{1}0* +using an array of size 10. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -105,7 +105,8 @@ def regular(x, Q, S, d, q0, F): # Determine a[i+1]: a[i+1] == d2[a[i], x[i]] solver.Add( - a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1))) + a[i + 1] == solver.Element(d2_flatten, ((a[i]) * S) + (x[i] - 1)) + ) # @@ -195,14 +196,21 @@ def main(): # # constraints # - regular(reg_input, n_states, input_max, transition_fn, initial_state, - accepting_states) + regular( + reg_input, + n_states, + input_max, + transition_fn, + initial_state, + accepting_states, + ) # # solution and search # - db = solver.Phase(reg_input, solver.CHOOSE_MIN_SIZE_HIGHEST_MAX, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + reg_input, solver.CHOOSE_MIN_SIZE_HIGHEST_MAX, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) diff --git a/examples/contrib/regular_table.py b/examples/contrib/regular_table.py index fcfe6f4201d..1c1bbb683e1 100644 --- a/examples/contrib/regular_table.py +++ b/examples/contrib/regular_table.py @@ -13,28 +13,28 @@ # limitations under the License. """ - Global constraint regular in Google CP Solver. +Global constraint regular in Google CP Solver. - This is a translation of MiniZinc's regular constraint (defined in - lib/zinc/globals.mzn). All comments are from the MiniZinc code. - ''' - The sequence of values in array 'x' (which must all be in the range 1..S) - is accepted by the DFA of 'Q' states with input 1..S and transition - function 'd' (which maps (1..Q, 1..S) -> 0..Q)) and initial state 'q0' - (which must be in 1..Q) and accepting states 'F' (which all must be in - 1..Q). We reserve state 0 to be an always failing state. - ''' +This is a translation of MiniZinc's regular constraint (defined in +lib/zinc/globals.mzn). All comments are from the MiniZinc code. +''' +The sequence of values in array 'x' (which must all be in the range 1..S) +is accepted by the DFA of 'Q' states with input 1..S and transition +function 'd' (which maps (1..Q, 1..S) -> 0..Q)) and initial state 'q0' +(which must be in 1..Q) and accepting states 'F' (which all must be in +1..Q). We reserve state 0 to be an always failing state. +''' - It is, however, translated from the Comet model: - * Comet: http://www.hakank.org/comet/regular.co +It is, however, translated from the Comet model: +* Comet: http://www.hakank.org/comet/regular.co - Here we test with the following regular expression: - 0*1{3}0+1{2}0+1{1}0* - using an array of size 10. +Here we test with the following regular expression: + 0*1{3}0+1{2}0+1{1}0* +using an array of size 10. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -188,14 +188,21 @@ def main(): # # constraints # - regular(reg_input, n_states, input_max, transition_fn, initial_state, - accepting_states) + regular( + reg_input, + n_states, + input_max, + transition_fn, + initial_state, + accepting_states, + ) # # solution and search # - db = solver.Phase(reg_input, solver.CHOOSE_MIN_SIZE_HIGHEST_MAX, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + reg_input, solver.CHOOSE_MIN_SIZE_HIGHEST_MAX, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) diff --git a/examples/contrib/regular_table2.py b/examples/contrib/regular_table2.py index 95f51545512..0fb51701142 100644 --- a/examples/contrib/regular_table2.py +++ b/examples/contrib/regular_table2.py @@ -13,28 +13,28 @@ # limitations under the License. """ - Global constraint regular in Google CP Solver. +Global constraint regular in Google CP Solver. - This is a translation of MiniZinc's regular constraint (defined in - lib/zinc/globals.mzn). All comments are from the MiniZinc code. - ''' - The sequence of values in array 'x' (which must all be in the range 1..S) - is accepted by the DFA of 'Q' states with input 1..S and transition - function 'd' (which maps (1..Q, 1..S) -> 0..Q)) and initial state 'q0' - (which must be in 1..Q) and accepting states 'F' (which all must be in - 1..Q). We reserve state 0 to be an always failing state. - ''' +This is a translation of MiniZinc's regular constraint (defined in +lib/zinc/globals.mzn). All comments are from the MiniZinc code. +''' +The sequence of values in array 'x' (which must all be in the range 1..S) +is accepted by the DFA of 'Q' states with input 1..S and transition +function 'd' (which maps (1..Q, 1..S) -> 0..Q)) and initial state 'q0' +(which must be in 1..Q) and accepting states 'F' (which all must be in +1..Q). We reserve state 0 to be an always failing state. +''' - It is, however, translated from the Comet model: - * Comet: http://www.hakank.org/comet/regular.co +It is, however, translated from the Comet model: +* Comet: http://www.hakank.org/comet/regular.co - Here we test with the following regular expression: - 0*1{3}0+1{2}0+1{1}0* - using an array of size 10. +Here we test with the following regular expression: + 0*1{3}0+1{2}0+1{1}0* +using an array of size 10. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -172,14 +172,21 @@ def main(): # # constraints # - regular(reg_input, n_states, input_max, transition_fn, initial_state, - accepting_states) + regular( + reg_input, + n_states, + input_max, + transition_fn, + initial_state, + accepting_states, + ) # # solution and search # - db = solver.Phase(reg_input, solver.CHOOSE_MIN_SIZE_HIGHEST_MAX, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + reg_input, solver.CHOOSE_MIN_SIZE_HIGHEST_MAX, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) diff --git a/examples/contrib/rogo2.py b/examples/contrib/rogo2.py index 914c90384d0..c5a02bba32c 100644 --- a/examples/contrib/rogo2.py +++ b/examples/contrib/rogo2.py @@ -13,35 +13,35 @@ # limitations under the License. """ - Rogo puzzle solver in Google CP Solver. +Rogo puzzle solver in Google CP Solver. - From http://www.rogopuzzle.co.nz/ - ''' - The object is to collect the biggest score possible using a given - number of steps in a loop around a grid. The best possible score - for a puzzle is given with it, so you can easily check that you have - solved the puzzle. Rogo puzzles can also include forbidden squares, - which must be avoided in your loop. - ''' +From http://www.rogopuzzle.co.nz/ +''' +The object is to collect the biggest score possible using a given +number of steps in a loop around a grid. The best possible score +for a puzzle is given with it, so you can easily check that you have +solved the puzzle. Rogo puzzles can also include forbidden squares, +which must be avoided in your loop. +''' - Also see Mike Trick: - 'Operations Research, Sudoko, Rogo, and Puzzles' - http://mat.tepper.cmu.edu/blog/?p=1302 +Also see Mike Trick: +'Operations Research, Sudoko, Rogo, and Puzzles' +http://mat.tepper.cmu.edu/blog/?p=1302 - Problem instances: - * http://www.hakank.org/google_or_tools/rogo_mike_trick.py - * http://www.hakank.org/google_or_tools/rogo_20110106.py - * http://www.hakank.org/google_or_tools/rogo_20110107.py +Problem instances: +* http://www.hakank.org/google_or_tools/rogo_mike_trick.py +* http://www.hakank.org/google_or_tools/rogo_20110106.py +* http://www.hakank.org/google_or_tools/rogo_20110107.py - Compare with the following models: - * Answer Set Programming: - http://www.hakank.org/answer_set_programming/rogo2.lp - * MiniZinc: http://www.hakank.org/minizinc/rogo2.mzn +Compare with the following models: +* Answer Set Programming: + http://www.hakank.org/answer_set_programming/rogo2.lp +* MiniZinc: http://www.hakank.org/minizinc/rogo2.mzn - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -180,9 +180,13 @@ def main(problem, rows, cols, max_steps): max_steps = 12 W = 0 B = -1 -problem = [[2, W, W, W, W, W, W, W, W], [W, 3, W, W, 1, W, W, 2, W], - [W, W, W, W, W, W, B, W, 2], [W, W, 2, B, W, W, W, W, W], - [W, W, W, W, 2, W, W, 1, W]] +problem = [ + [2, W, W, W, W, W, W, W, W], + [W, 3, W, W, 1, W, W, 2, W], + [W, W, W, W, W, W, B, W, 2], + [W, W, 2, B, W, W, W, W, W], + [W, W, W, W, 2, W, W, 1, W], +] if __name__ == "__main__": if len(sys.argv) > 1: exec(compile(open(sys.argv[1]).read(), sys.argv[1], "exec")) diff --git a/examples/contrib/rostering_with_travel.py b/examples/contrib/rostering_with_travel.py index 4004e77d991..7a67796318d 100644 --- a/examples/contrib/rostering_with_travel.py +++ b/examples/contrib/rostering_with_travel.py @@ -5,8 +5,14 @@ def SolveRosteringWithTravel(): model = cp_model.CpModel() # [duration, start, end, location] - jobs = [[3, 0, 6, 1], [5, 0, 6, 0], [1, 3, 7, 1], [1, 3, 5, 0], [3, 0, 3, 0], - [3, 0, 8, 0]] + jobs = [ + [3, 0, 6, 1], + [5, 0, 6, 0], + [1, 3, 7, 1], + [1, 3, 5, 0], + [3, 0, 3, 0], + [3, 0, 8, 0], + ] max_length = 20 @@ -45,12 +51,14 @@ def SolveRosteringWithTravel(): job_performed.append(performed_on_m) # Create an optional copy of interval to be executed on a machine - location0 = model.NewIntVar(jobs[i][3], jobs[i][3], - 'location_%i_on_m%i' % (i, m)) + location0 = model.NewIntVar( + jobs[i][3], jobs[i][3], 'location_%i_on_m%i' % (i, m) + ) start0 = model.NewIntVar(jobs[i][1], horizon, 'start_%i_on_m%i' % (i, m)) end0 = model.NewIntVar(0, jobs[i][2], 'end_%i_on_m%i' % (i, m)) interval0 = model.NewOptionalIntervalVar( - start0, duration, end0, performed_on_m, 'interval_%i_on_m%i' % (i, m)) + start0, duration, end0, performed_on_m, 'interval_%i_on_m%i' % (i, m) + ) optional_intervals[m].append(interval0) # We only propagate the constraint if the tasks is performed on the machine. @@ -60,8 +68,12 @@ def SolveRosteringWithTravel(): startT = model.NewIntVar(0, horizon, 'start_%i_on_m%i' % (i, m)) endT = model.NewIntVar(0, horizon, 'end_%i_on_m%i' % (i, m)) intervalT = model.NewOptionalIntervalVar( - startT, travel_time, endT, travel, - 'travel_interval_%i_on_m%i' % (i, m)) + startT, + travel_time, + endT, + travel, + 'travel_interval_%i_on_m%i' % (i, m), + ) optional_intervals[m].append(intervalT) job_travels.append(travel) @@ -81,7 +93,8 @@ def SolveRosteringWithTravel(): is_job_earlier = model.NewBoolVar('is_j%i_earlier_j%i' % (i, c)) model.Add(starts[i] < starts[c]).OnlyEnforceIf(is_job_earlier) model.Add(starts[i] >= starts[c]).OnlyEnforceIf( - is_job_earlier.Not()) + is_job_earlier.Not() + ) # Max Length constraint (modeled as a cumulative) # model.AddCumulative(intervals, demands, max_length) @@ -92,8 +105,14 @@ def SolveRosteringWithTravel(): # Objective variable. total_cost = model.NewIntVar(0, 1000, 'cost') - model.Add(total_cost == sum( - performed[j][m] * (10 * (m + 1)) for j in all_jobs for m in all_machines)) + model.Add( + total_cost + == sum( + performed[j][m] * (10 * (m + 1)) + for j in all_jobs + for m in all_machines + ) + ) model.Minimize(total_cost) # Solve model. diff --git a/examples/contrib/safe_cracking.py b/examples/contrib/safe_cracking.py index bf994944690..04355ae2d27 100644 --- a/examples/contrib/safe_cracking.py +++ b/examples/contrib/safe_cracking.py @@ -13,36 +13,36 @@ # limitations under the License. """ - Safe cracking puzzle in Google CP Solver. +Safe cracking puzzle in Google CP Solver. - From the Oz Primer: - http://www.comp.nus.edu.sg/~henz/projects/puzzles/digits/index.html - ''' - The code of Professor Smart's safe is a sequence of 9 distinct - nonzero digits C1 .. C9 such that the following equations and - inequations are satisfied: +From the Oz Primer: +http://www.comp.nus.edu.sg/~henz/projects/puzzles/digits/index.html +''' +The code of Professor Smart's safe is a sequence of 9 distinct +nonzero digits C1 .. C9 such that the following equations and +inequations are satisfied: - C4 - C6 = C7 - C1 * C2 * C3 = C8 + C9 - C2 + C3 + C6 < C8 - C9 < C8 + C4 - C6 = C7 + C1 * C2 * C3 = C8 + C9 + C2 + C3 + C6 < C8 + C9 < C8 - and + and - C1 <> 1, C2 <> 2, ..., C9 <> 9 + C1 <> 1, C2 <> 2, ..., C9 <> 9 - can you find the correct combination? - ''' +can you find the correct combination? +''' - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/safe_cracking.mzn - * ECLiPSe : http://www.hakank.org/eclipse/safe_cracking.ecl - * SICStus : http://www.hakank.org/sicstus/safe_cracking.pl - * Gecode: http://hakank.org/gecode/safe_cracking.cpp +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/safe_cracking.mzn +* ECLiPSe : http://www.hakank.org/eclipse/safe_cracking.ecl +* SICStus : http://www.hakank.org/sicstus/safe_cracking.pl +* Gecode: http://hakank.org/gecode/safe_cracking.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp diff --git a/examples/contrib/scheduling_speakers.py b/examples/contrib/scheduling_speakers.py index 41544410b12..8c22dfec645 100644 --- a/examples/contrib/scheduling_speakers.py +++ b/examples/contrib/scheduling_speakers.py @@ -13,20 +13,20 @@ # limitations under the License. """ - Scheduling speakers problem in Google CP Solver. +Scheduling speakers problem in Google CP Solver. - From Rina Dechter, Constraint Processing, page 72 - Scheduling of 6 speakers in 6 slots. +From Rina Dechter, Constraint Processing, page 72 +Scheduling of 6 speakers in 6 slots. - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/scheduling_speakers.mzn - * SICStus Prolog: http://www.hakank.org/sicstus/scheduling_speakers.pl - * ECLiPSe: http://hakank.org/eclipse/scheduling_speakers.ecl - * Gecode: http://hakank.org/gecode/scheduling_speakers.cpp +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/scheduling_speakers.mzn +* SICStus Prolog: http://www.hakank.org/sicstus/scheduling_speakers.pl +* ECLiPSe: http://hakank.org/eclipse/scheduling_speakers.ecl +* Gecode: http://hakank.org/gecode/scheduling_speakers.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -49,7 +49,7 @@ def main(): [2, 3, 4, 5], # 3) only with 5 after F -> 1 and A -> 6 [2, 3, 4], # 4) only with 2 after C -> 5 and F -> 1 [3, 4], # 5) 3 or 4 - [1, 2, 3, 4, 5, 6] # 1) the only with 1 + [1, 2, 3, 4, 5, 6], # 1) the only with 1 ] # diff --git a/examples/contrib/scheduling_with_transitions_sat.py b/examples/contrib/scheduling_with_transitions_sat.py index cbaaf78b94f..28655f8c996 100644 --- a/examples/contrib/scheduling_with_transitions_sat.py +++ b/examples/contrib/scheduling_with_transitions_sat.py @@ -11,19 +11,21 @@ from ortools.sat.python import cp_model from google.protobuf import text_format -#---------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Command line arguments. PARSER = argparse.ArgumentParser() PARSER.add_argument( - '--problem_instance', default=0, type=int, help='Problem instance.') + '--problem_instance', default=0, type=int, help='Problem instance.' +) PARSER.add_argument( '--output_proto', default='', - help='Output file to write the cp_model proto to.') + help='Output file to write the cp_model proto to.', +) PARSER.add_argument('--params', default='', help='Sat solver parameters.') -#---------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Intermediate solution printer class SolutionPrinter(cp_model.CpSolverSolutionCallback): """Print intermediate solutions.""" @@ -34,9 +36,15 @@ def __init__(self, makespan): self.__makespan = makespan def OnSolutionCallback(self): - print('Solution %i, time = %f s, objective = %i, makespan = %i' % - (self.__solution_count, self.WallTime(), self.ObjectiveValue(), - self.Value(self.__makespan))) + print( + 'Solution %i, time = %f s, objective = %i, makespan = %i' + % ( + self.__solution_count, + self.WallTime(), + self.ObjectiveValue(), + self.Value(self.__makespan), + ) + ) self.__solution_count += 1 @@ -47,72 +55,108 @@ def main(args): parameters = args.params output_proto = args.output_proto - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Data. - small_jobs = [[[(100, 0, 'R6'), (2, 1, 'R6')]], - [[(2, 0, 'R3'), (100, 1, 'R3')]], - [[(100, 0, 'R1'), (16, 1, 'R1')]], - [[(1, 0, 'R1'), (38, 1, 'R1')]], [[(14, 0, 'R1'), (10, 1, - 'R1')]], - [[(16, 0, 'R3'), (17, 1, 'R3')]], - [[(14, 0, 'R3'), (14, 1, 'R3')]], - [[(14, 0, 'R3'), (15, 1, 'R3')]], - [[(14, 0, 'R3'), (13, 1, 'R3')]], - [[(100, 0, 'R1'), (38, 1, 'R1')]]] + small_jobs = [ + [[(100, 0, 'R6'), (2, 1, 'R6')]], + [[(2, 0, 'R3'), (100, 1, 'R3')]], + [[(100, 0, 'R1'), (16, 1, 'R1')]], + [[(1, 0, 'R1'), (38, 1, 'R1')]], + [[(14, 0, 'R1'), (10, 1, 'R1')]], + [[(16, 0, 'R3'), (17, 1, 'R3')]], + [[(14, 0, 'R3'), (14, 1, 'R3')]], + [[(14, 0, 'R3'), (15, 1, 'R3')]], + [[(14, 0, 'R3'), (13, 1, 'R3')]], + [[(100, 0, 'R1'), (38, 1, 'R1')]], + ] large_jobs = [ - [[(-1, 0, 'R1'), (10, 1, 'R1')]], [[(9, 0, 'R3'), - (22, 1, 'R3')]], - [[(-1, 0, 'R3'), (13, 1, 'R3')]], [[(-1, 0, 'R3'), (38, 1, 'R3')]], - [[(-1, 0, 'R3'), (38, 1, 'R3')]], [[(-1, 0, 'R3'), (16, 1, 'R3')]], - [[(-1, 0, 'R3'), (11, 1, 'R3')]], [[(-1, 0, 'R3'), (13, 1, 'R3')]], - [[(13, 0, 'R3'), (-1, 1, 'R3')]], [[(13, 0, 'R3'), (-1, 1, 'R3')]], - [[(-1, 0, 'R3'), (15, 1, 'R3')]], [[(12, 0, 'R1'), (-1, 1, 'R1')]], - [[(14, 0, 'R1'), (-1, 1, 'R1')]], [[(13, 0, 'R3'), (-1, 1, 'R3')]], - [[(-1, 0, 'R3'), (15, 1, 'R3')]], [[(14, 0, 'R1'), (-1, 1, 'R1')]], - [[(13, 0, 'R3'), (-1, 1, 'R3')]], [[(14, 0, 'R3'), (-1, 1, 'R3')]], - [[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(15, 0, 'R1'), (-1, 1, 'R1')]], - [[(-1, 0, 'R2'), (16, 1, 'R2')]], [[(-1, 0, 'R2'), (16, 1, 'R2')]], - [[(-1, 0, 'R5'), (27, 1, 'R5')]], [[(-1, 0, 'R5'), (13, 1, 'R5')]], - [[(-1, 0, 'R5'), (13, 1, 'R5')]], [[(-1, 0, 'R5'), (13, 1, 'R5')]], - [[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(-1, 0, 'R1'), (17, 1, 'R1')]], - [[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]], - [[(16, 0, 'R4'), (-1, 1, 'R4')]], [[(16, 0, 'R4'), (-1, 1, 'R4')]], - [[(16, 0, 'R4'), (-1, 1, 'R4')]], [[(16, 0, 'R4'), (-1, 1, 'R4')]], - [[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]], - [[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]], - [[(12, 0, 'R1'), (-1, 1, 'R1')]], [[(14, 0, 'R4'), (-1, 1, 'R4')]], - [[(-1, 0, 'R5'), (14, 1, 'R5')]], [[(14, 0, 'R4'), (-1, 1, 'R4')]], - [[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(14, 0, 'R4'), (-1, 1, 'R4')]], - [[(14, 0, 'R4'), (-1, 1, 'R4')]], [[(-1, 0, 'R1'), (21, 1, 'R1')]], - [[(-1, 0, 'R1'), (21, 1, 'R1')]], [[(-1, 0, 'R1'), (21, 1, 'R1')]], - [[(13, 0, 'R6'), (-1, 1, 'R6')]], [[(13, 0, 'R2'), (-1, 1, 'R2')]], - [[(-1, 0, 'R6'), (12, 1, 'R6')]], [[(13, 0, 'R1'), (-1, 1, 'R1')]], - [[(13, 0, 'R1'), (-1, 1, 'R1')]], [[(-1, 0, 'R6'), (14, 1, 'R6')]], - [[(-1, 0, 'R5'), (5, 1, 'R5')]], [[(3, 0, 'R2'), (-1, 1, 'R2')]], - [[(-1, 0, 'R1'), (21, 1, 'R1')]], [[(-1, 0, 'R1'), (21, 1, 'R1')]], - [[(-1, 0, 'R1'), (21, 1, 'R1')]], [[(-1, 0, 'R5'), (1, 1, 'R5')]], - [[(1, 0, 'R2'), (-1, 1, 'R2')]], [[(-1, 0, 'R2'), (19, 1, 'R2')]], - [[(13, 0, 'R4'), (-1, 1, 'R4')]], [[(12, 0, 'R4'), (-1, 1, 'R4')]], - [[(-1, 0, 'R3'), (2, 1, 'R3')]], [[(11, 0, 'R4'), (-1, 1, 'R4')]], - [[(6, 0, 'R6'), (-1, 1, 'R6')]], [[(6, 0, 'R6'), (-1, 1, 'R6')]], - [[(1, 0, 'R2'), (-1, 1, 'R2')]], [[(12, 0, 'R4'), (-1, 1, 'R4')]] + [[(-1, 0, 'R1'), (10, 1, 'R1')]], + [[(9, 0, 'R3'), (22, 1, 'R3')]], + [[(-1, 0, 'R3'), (13, 1, 'R3')]], + [[(-1, 0, 'R3'), (38, 1, 'R3')]], + [[(-1, 0, 'R3'), (38, 1, 'R3')]], + [[(-1, 0, 'R3'), (16, 1, 'R3')]], + [[(-1, 0, 'R3'), (11, 1, 'R3')]], + [[(-1, 0, 'R3'), (13, 1, 'R3')]], + [[(13, 0, 'R3'), (-1, 1, 'R3')]], + [[(13, 0, 'R3'), (-1, 1, 'R3')]], + [[(-1, 0, 'R3'), (15, 1, 'R3')]], + [[(12, 0, 'R1'), (-1, 1, 'R1')]], + [[(14, 0, 'R1'), (-1, 1, 'R1')]], + [[(13, 0, 'R3'), (-1, 1, 'R3')]], + [[(-1, 0, 'R3'), (15, 1, 'R3')]], + [[(14, 0, 'R1'), (-1, 1, 'R1')]], + [[(13, 0, 'R3'), (-1, 1, 'R3')]], + [[(14, 0, 'R3'), (-1, 1, 'R3')]], + [[(13, 0, 'R1'), (-1, 1, 'R1')]], + [[(15, 0, 'R1'), (-1, 1, 'R1')]], + [[(-1, 0, 'R2'), (16, 1, 'R2')]], + [[(-1, 0, 'R2'), (16, 1, 'R2')]], + [[(-1, 0, 'R5'), (27, 1, 'R5')]], + [[(-1, 0, 'R5'), (13, 1, 'R5')]], + [[(-1, 0, 'R5'), (13, 1, 'R5')]], + [[(-1, 0, 'R5'), (13, 1, 'R5')]], + [[(13, 0, 'R1'), (-1, 1, 'R1')]], + [[(-1, 0, 'R1'), (17, 1, 'R1')]], + [[(14, 0, 'R4'), (-1, 1, 'R4')]], + [[(13, 0, 'R1'), (-1, 1, 'R1')]], + [[(16, 0, 'R4'), (-1, 1, 'R4')]], + [[(16, 0, 'R4'), (-1, 1, 'R4')]], + [[(16, 0, 'R4'), (-1, 1, 'R4')]], + [[(16, 0, 'R4'), (-1, 1, 'R4')]], + [[(13, 0, 'R1'), (-1, 1, 'R1')]], + [[(13, 0, 'R1'), (-1, 1, 'R1')]], + [[(14, 0, 'R4'), (-1, 1, 'R4')]], + [[(13, 0, 'R1'), (-1, 1, 'R1')]], + [[(12, 0, 'R1'), (-1, 1, 'R1')]], + [[(14, 0, 'R4'), (-1, 1, 'R4')]], + [[(-1, 0, 'R5'), (14, 1, 'R5')]], + [[(14, 0, 'R4'), (-1, 1, 'R4')]], + [[(14, 0, 'R4'), (-1, 1, 'R4')]], + [[(14, 0, 'R4'), (-1, 1, 'R4')]], + [[(14, 0, 'R4'), (-1, 1, 'R4')]], + [[(-1, 0, 'R1'), (21, 1, 'R1')]], + [[(-1, 0, 'R1'), (21, 1, 'R1')]], + [[(-1, 0, 'R1'), (21, 1, 'R1')]], + [[(13, 0, 'R6'), (-1, 1, 'R6')]], + [[(13, 0, 'R2'), (-1, 1, 'R2')]], + [[(-1, 0, 'R6'), (12, 1, 'R6')]], + [[(13, 0, 'R1'), (-1, 1, 'R1')]], + [[(13, 0, 'R1'), (-1, 1, 'R1')]], + [[(-1, 0, 'R6'), (14, 1, 'R6')]], + [[(-1, 0, 'R5'), (5, 1, 'R5')]], + [[(3, 0, 'R2'), (-1, 1, 'R2')]], + [[(-1, 0, 'R1'), (21, 1, 'R1')]], + [[(-1, 0, 'R1'), (21, 1, 'R1')]], + [[(-1, 0, 'R1'), (21, 1, 'R1')]], + [[(-1, 0, 'R5'), (1, 1, 'R5')]], + [[(1, 0, 'R2'), (-1, 1, 'R2')]], + [[(-1, 0, 'R2'), (19, 1, 'R2')]], + [[(13, 0, 'R4'), (-1, 1, 'R4')]], + [[(12, 0, 'R4'), (-1, 1, 'R4')]], + [[(-1, 0, 'R3'), (2, 1, 'R3')]], + [[(11, 0, 'R4'), (-1, 1, 'R4')]], + [[(6, 0, 'R6'), (-1, 1, 'R6')]], + [[(6, 0, 'R6'), (-1, 1, 'R6')]], + [[(1, 0, 'R2'), (-1, 1, 'R2')]], + [[(12, 0, 'R4'), (-1, 1, 'R4')]], ] jobs = small_jobs if instance == 0 else large_jobs - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Helper data. num_jobs = len(jobs) all_jobs = range(num_jobs) num_machines = 2 all_machines = range(num_machines) - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Model. model = cp_model.CpModel() - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Compute a maximum makespan greedily. horizon = 0 for job in jobs: @@ -124,7 +168,7 @@ def main(args): print('Horizon = %i' % horizon) - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Global storage of variables. intervals_per_machines = collections.defaultdict(list) presences_per_machines = collections.defaultdict(list) @@ -138,7 +182,7 @@ def main(args): job_ranks = {} # indexed by (job_id, task_id, alt_id). job_ends = [] # indexed by job_id - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Scan the jobs and create the relevant variables and intervals. for job_id in all_jobs: job = jobs[job_id] @@ -161,8 +205,9 @@ def main(args): # Create main interval for the task. suffix_name = '_j%i_t%i' % (job_id, task_id) start = model.NewIntVar(0, horizon, 'start' + suffix_name) - duration = model.NewIntVar(min_duration, max_duration, - 'duration' + suffix_name) + duration = model.NewIntVar( + min_duration, max_duration, 'duration' + suffix_name + ) end = model.NewIntVar(0, horizon, 'end' + suffix_name) # Store the start for the solution. @@ -186,7 +231,8 @@ def main(args): l_duration = task[alt_id][0] l_end = model.NewIntVar(0, horizon, 'end' + alt_suffix) l_interval = model.NewOptionalIntervalVar( - l_start, l_duration, l_end, l_presence, 'interval' + alt_suffix) + l_start, l_duration, l_end, l_presence, 'interval' + alt_suffix + ) l_rank = model.NewIntVar(-1, num_jobs, 'rank' + alt_suffix) l_presences.append(l_presence) l_machine = task[alt_id][1] @@ -214,14 +260,14 @@ def main(args): job_ends.append(previous_end) - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Create machines constraints nonoverlap process for machine_id in all_machines: intervals = intervals_per_machines[machine_id] if len(intervals) > 1: model.AddNoOverlap(intervals) - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Transition times and transition costs using a circuit constraint. switch_literals = [] for machine_id in all_machines: @@ -247,7 +293,8 @@ def main(args): # Self arc if the task is not performed. arcs.append([i + 1, i + 1, machine_presences[i].Not()]) model.Add(machine_ranks[i] == -1).OnlyEnforceIf( - machine_presences[i].Not()) + machine_presences[i].Not() + ) for j in all_machine_tasks: if i == j: @@ -269,28 +316,30 @@ def main(args): transition_time = 0 # We add the reified transition to link the literals with the times # of the tasks. - model.Add(machine_starts[j] == machine_ends[i] + - transition_time).OnlyEnforceIf(lit) + model.Add( + machine_starts[j] == machine_ends[i] + transition_time + ).OnlyEnforceIf(lit) if arcs: - model.AddCircuit(arcs) + model.AddCircuit(arcs) - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Objective. makespan = model.NewIntVar(0, horizon, 'makespan') model.AddMaxEquality(makespan, job_ends) makespan_weight = 1 transition_weight = 5 - model.Minimize(makespan * makespan_weight + - sum(switch_literals) * transition_weight) + model.Minimize( + makespan * makespan_weight + sum(switch_literals) * transition_weight + ) - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Write problem to file. if output_proto: print('Writing proto to %s' % output_proto) with open(output_proto, 'w') as text_file: text_file.write(str(model)) - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Solve. solver = cp_model.CpSolver() solver.parameters.max_time_in_seconds = 60 * 60 * 2 @@ -299,7 +348,7 @@ def main(args): solution_printer = SolutionPrinter(makespan) status = solver.Solve(model, solution_printer) - #---------------------------------------------------------------------------- + # ---------------------------------------------------------------------------- # Print solution. if status == cp_model.FEASIBLE or status == cp_model.OPTIMAL: for job_id in all_jobs: @@ -321,8 +370,10 @@ def main(args): rank = solver.Value(job_ranks[(job_id, task_id, alt_id)]) print( - ' Job %i starts at %i (alt %i, duration %i) with rank %i on machine %i' - % (job_id, start_value, select, duration, rank, machine)) + ' Job %i starts at %i (alt %i, duration %i) with rank %i on' + ' machine %i' + % (job_id, start_value, select, duration, rank, machine) + ) print('Solve status: %s' % solver.StatusName(status)) print('Objective value: %i' % solver.ObjectiveValue()) diff --git a/examples/contrib/school_scheduling_sat.py b/examples/contrib/school_scheduling_sat.py index 41cf80cc71f..c3e1420d889 100755 --- a/examples/contrib/school_scheduling_sat.py +++ b/examples/contrib/school_scheduling_sat.py @@ -1,347 +1,368 @@ #!/usr/bin/env python3 -'''Solve a School Scheduling Problem''' +"""Solve a School Scheduling Problem""" from ortools.sat.python import cp_model -class SchoolSchedulingProblem(): - '''Data of the problem.''' - - def __init__(self, levels, sections, subjects, curriculum, teachers, - specialties, time_slots): - self._levels = levels - self._sections = sections - self._subjects = subjects - self._curriculum = curriculum - assert len(self._curriculum) == len(self._levels) * len( - self._subjects), 'Some curriculum are missing' - for (lvl, sub) in self._curriculum.keys(): - assert lvl in self._levels, f'{lvl} not in LEVELS' - assert sub in self._subjects, f'{sub} not in SUBJECTS' - - self._teachers = teachers - self._specialties = specialties - assert len(self._specialties) == len( - self._subjects), 'Missing some rows' - for s, ts in self._specialties.items(): - assert s in self._subjects, f'{s} is not in SUBJECTS' - for t in ts: - assert t in self._teachers, f'{t} is not in TEACHERS' - - self._time_slots = time_slots - - @property - def levels(self): - return self._levels - - @property - def sections(self): - return self._sections - - @property - def subjects(self): - return self._subjects - - @property - def curriculum(self): - return self._curriculum - - @property - def teachers(self): - return self._teachers - - def teacher_name(self, teacher_idx): - assert 0 <= teacher_idx < len(self._teachers) - return list(self._teachers.keys())[teacher_idx] - - def teacher_max_hours(self, teacher_idx): - assert 0 <= teacher_idx < len(self._teachers) - return list(self._teachers.values())[teacher_idx] - - @property - def specialties(self): - return self._specialties - - def specialtie_teachers(self, subject): - assert subject in self._subjects, f'{subject} not in SUBJECTS' - return self._specialties[subject] - - @property - def time_slots(self): - return self._time_slots - - def slot_duration(self, slot_idx): - assert 0 <= slot_idx < len(self._time_slots) - return list(self._time_slots.values())[slot_idx] - - -class SchoolSchedulingSatSolver(): - '''Solver instance.''' - - def __init__(self, problem: SchoolSchedulingProblem): - # Problem - self._problem = problem - - # Utilities - num_levels = len(self._problem.levels) - self._all_levels = range(num_levels) - num_sections = len(self._problem.sections) - self._all_sections = range(num_sections) - num_subjects = len(self._problem.subjects) - self._all_subjects = range(num_subjects) - num_teachers = len(self._problem.teachers) - self._all_teachers = range(num_teachers) - num_slots = len(self._problem.time_slots) - self._all_slots = range(num_slots) - - # Create Model - self._model = cp_model.CpModel() - - # Create Variables - self._assignment = {} - for lvl_idx, level in enumerate(self._problem.levels): - for sec_idx, section in enumerate(self._problem.sections): - for sub_idx, subject in enumerate(self._problem.subjects): - for tch_idx, teacher in enumerate(self._problem.teachers): - for slt_idx, slot in enumerate(self._problem.time_slots): - key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx) - name = f'{level}-{section} S:{subject} T:{teacher} Slot:{slot}' - #print(name) - if teacher in self._problem.specialtie_teachers(subject): - self._assignment[key] = self._model.NewBoolVar(name) - else: - name = 'NO DISP ' + name - self._assignment[key] = self._model.NewIntVar(0, 0, name) - - # Constraints - # Each Level-Section must have the quantity of classes per Subject specified in the Curriculum - for lvl_idx, level in enumerate(self._problem.levels): - for sec_idx in self._all_sections: - for sub_idx, subject in enumerate(self._problem.subjects): - required_duration = self._problem.curriculum[level, subject] - #print(f'L:{level} S:{subject} duration:{required_duration}h') - self._model.Add( - sum(self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] * - int(self._problem.slot_duration(slt_idx) * 10) - for tch_idx in self._all_teachers - for slt_idx in self._all_slots) == int(required_duration * 10)) - - # Each Level-Section can do at most one class at a time - for lvl_idx in self._all_levels: - for sec_idx in self._all_sections: - for slt_idx in self._all_slots: - self._model.Add( - sum([ - self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] - for sub_idx in self._all_subjects - for tch_idx in self._all_teachers - ]) <= 1) - - # Teacher can do at most one class at a time - for tch_idx in self._all_teachers: - for slt_idx in self._all_slots: - self._model.Add( - sum([ - self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] - for lvl_idx in self._all_levels - for sec_idx in self._all_sections - for sub_idx in self._all_subjects - ]) <= 1) - - # Maximum work hours for each teacher - for tch_idx in self._all_teachers: - self._model.Add( - sum([ - self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] * - int(self._problem.slot_duration(slt_idx) * 10) - for lvl_idx in self._all_levels - for sec_idx in self._all_sections - for sub_idx in self._all_subjects - for slt_idx in self._all_slots - ]) <= int(self._problem.teacher_max_hours(tch_idx) * 10)) - - # Teacher makes all the classes of a subject's course - teacher_courses = {} - for lvl_idx, level in enumerate(self._problem.levels): - for sec_idx, section in enumerate(self._problem.sections): - for sub_idx, subject in enumerate(self._problem.subjects): - for tch_idx, teacher in enumerate(self._problem.teachers): - name = f'{level}-{section} S:{subject} T:{teacher}' - teacher_courses[lvl_idx, sec_idx, sub_idx, tch_idx] = self._model.NewBoolVar(name) - temp_array = [ - self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] - for slt_idx in self._all_slots - ] - self._model.AddMaxEquality( - teacher_courses[lvl_idx, sec_idx, sub_idx, tch_idx], temp_array) - self._model.Add( - sum([teacher_courses[lvl_idx, sec_idx, sub_idx, tch_idx] - for tch_idx in self._all_teachers - ]) == 1) - - - def print_teacher_schedule(self, tch_idx): - teacher_name = self._problem.teacher_name(tch_idx) - print(f'Teacher: {teacher_name}') - total_working_hours = 0 - for slt_idx, slot in enumerate(self._problem.time_slots): - for lvl_idx, level in enumerate(self._problem.levels): - for sec_idx, section in enumerate(self._problem.sections): - for sub_idx, subject in enumerate(self._problem.subjects): - key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx) - if self._solver.BooleanValue(self._assignment[key]): - total_working_hours += self._problem.slot_duration(slt_idx) - print(f'{slot}: C:{level}-{section} S:{subject}') - print(f'Total working hours: {total_working_hours}h') - - - def print_class_schedule(self, lvl_idx, sec_idx): - level = self._problem.levels[lvl_idx] - section = self._problem.sections[sec_idx] - print(f'Class: {level}-{section}') - total_working_hours = {} - for sub in self._problem.subjects: - total_working_hours[sub] = 0 - for slt_idx, slot in enumerate(self._problem.time_slots): +class SchoolSchedulingProblem: + """Data of the problem.""" + + def __init__( + self, + levels, + sections, + subjects, + curriculum, + teachers, + specialties, + time_slots, + ): + self._levels = levels + self._sections = sections + self._subjects = subjects + self._curriculum = curriculum + assert len(self._curriculum) == len(self._levels) * len( + self._subjects + ), 'Some curriculum are missing' + for lvl, sub in self._curriculum.keys(): + assert lvl in self._levels, f'{lvl} not in LEVELS' + assert sub in self._subjects, f'{sub} not in SUBJECTS' + + self._teachers = teachers + self._specialties = specialties + assert len(self._specialties) == len(self._subjects), 'Missing some rows' + for s, ts in self._specialties.items(): + assert s in self._subjects, f'{s} is not in SUBJECTS' + for t in ts: + assert t in self._teachers, f'{t} is not in TEACHERS' + + self._time_slots = time_slots + + @property + def levels(self): + return self._levels + + @property + def sections(self): + return self._sections + + @property + def subjects(self): + return self._subjects + + @property + def curriculum(self): + return self._curriculum + + @property + def teachers(self): + return self._teachers + + def teacher_name(self, teacher_idx): + assert 0 <= teacher_idx < len(self._teachers) + return list(self._teachers.keys())[teacher_idx] + + def teacher_max_hours(self, teacher_idx): + assert 0 <= teacher_idx < len(self._teachers) + return list(self._teachers.values())[teacher_idx] + + @property + def specialties(self): + return self._specialties + + def specialtie_teachers(self, subject): + assert subject in self._subjects, f'{subject} not in SUBJECTS' + return self._specialties[subject] + + @property + def time_slots(self): + return self._time_slots + + def slot_duration(self, slot_idx): + assert 0 <= slot_idx < len(self._time_slots) + return list(self._time_slots.values())[slot_idx] + + +class SchoolSchedulingSatSolver: + """Solver instance.""" + + def __init__(self, problem: SchoolSchedulingProblem): + # Problem + self._problem = problem + + # Utilities + num_levels = len(self._problem.levels) + self._all_levels = range(num_levels) + num_sections = len(self._problem.sections) + self._all_sections = range(num_sections) + num_subjects = len(self._problem.subjects) + self._all_subjects = range(num_subjects) + num_teachers = len(self._problem.teachers) + self._all_teachers = range(num_teachers) + num_slots = len(self._problem.time_slots) + self._all_slots = range(num_slots) + + # Create Model + self._model = cp_model.CpModel() + + # Create Variables + self._assignment = {} + for lvl_idx, level in enumerate(self._problem.levels): + for sec_idx, section in enumerate(self._problem.sections): + for sub_idx, subject in enumerate(self._problem.subjects): + for tch_idx, teacher in enumerate(self._problem.teachers): + for slt_idx, slot in enumerate(self._problem.time_slots): + key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx) + name = f'{level}-{section} S:{subject} T:{teacher} Slot:{slot}' + # print(name) + if teacher in self._problem.specialtie_teachers(subject): + self._assignment[key] = self._model.NewBoolVar(name) + else: + name = 'NO DISP ' + name + self._assignment[key] = self._model.NewIntVar(0, 0, name) + + # Constraints + # Each Level-Section must have the quantity of classes per Subject specified in the Curriculum + for lvl_idx, level in enumerate(self._problem.levels): + for sec_idx in self._all_sections: + for sub_idx, subject in enumerate(self._problem.subjects): + required_duration = self._problem.curriculum[level, subject] + # print(f'L:{level} S:{subject} duration:{required_duration}h') + self._model.Add( + sum( + self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] + * int(self._problem.slot_duration(slt_idx) * 10) + for tch_idx in self._all_teachers + for slt_idx in self._all_slots + ) + == int(required_duration * 10) + ) + + # Each Level-Section can do at most one class at a time + for lvl_idx in self._all_levels: + for sec_idx in self._all_sections: + for slt_idx in self._all_slots: + self._model.Add( + sum([ + self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] + for sub_idx in self._all_subjects + for tch_idx in self._all_teachers + ]) + <= 1 + ) + + # Teacher can do at most one class at a time + for tch_idx in self._all_teachers: + for slt_idx in self._all_slots: + self._model.Add( + sum([ + self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] + for lvl_idx in self._all_levels + for sec_idx in self._all_sections + for sub_idx in self._all_subjects + ]) + <= 1 + ) + + # Maximum work hours for each teacher + for tch_idx in self._all_teachers: + self._model.Add( + sum([ + self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] + * int(self._problem.slot_duration(slt_idx) * 10) + for lvl_idx in self._all_levels + for sec_idx in self._all_sections + for sub_idx in self._all_subjects + for slt_idx in self._all_slots + ]) + <= int(self._problem.teacher_max_hours(tch_idx) * 10) + ) + + # Teacher makes all the classes of a subject's course + teacher_courses = {} + for lvl_idx, level in enumerate(self._problem.levels): + for sec_idx, section in enumerate(self._problem.sections): + for sub_idx, subject in enumerate(self._problem.subjects): + for tch_idx, teacher in enumerate(self._problem.teachers): + name = f'{level}-{section} S:{subject} T:{teacher}' + teacher_courses[lvl_idx, sec_idx, sub_idx, tch_idx] = ( + self._model.NewBoolVar(name) + ) + temp_array = [ + self._assignment[lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx] + for slt_idx in self._all_slots + ] + self._model.AddMaxEquality( + teacher_courses[lvl_idx, sec_idx, sub_idx, tch_idx], temp_array + ) + self._model.Add( + sum([ + teacher_courses[lvl_idx, sec_idx, sub_idx, tch_idx] + for tch_idx in self._all_teachers + ]) + == 1 + ) + + def print_teacher_schedule(self, tch_idx): + teacher_name = self._problem.teacher_name(tch_idx) + print(f'Teacher: {teacher_name}') + total_working_hours = 0 + for slt_idx, slot in enumerate(self._problem.time_slots): + for lvl_idx, level in enumerate(self._problem.levels): + for sec_idx, section in enumerate(self._problem.sections): + for sub_idx, subject in enumerate(self._problem.subjects): + key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx) + if self._solver.BooleanValue(self._assignment[key]): + total_working_hours += self._problem.slot_duration(slt_idx) + print(f'{slot}: C:{level}-{section} S:{subject}') + print(f'Total working hours: {total_working_hours}h') + + def print_class_schedule(self, lvl_idx, sec_idx): + level = self._problem.levels[lvl_idx] + section = self._problem.sections[sec_idx] + print(f'Class: {level}-{section}') + total_working_hours = {} + for sub in self._problem.subjects: + total_working_hours[sub] = 0 + for slt_idx, slot in enumerate(self._problem.time_slots): + for tch_idx, teacher in enumerate(self._problem.teachers): + for sub_idx, subject in enumerate(self._problem.subjects): + key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx) + if self._solver.BooleanValue(self._assignment[key]): + total_working_hours[subject] += self._problem.slot_duration(slt_idx) + print(f'{slot}: S:{subject} T:{teacher}') + for subject, hours in total_working_hours.items(): + print(f'Total working hours for {subject}: {hours}h') + + def print_school_schedule(self): + print('School:') + for slt_idx, slot in enumerate(self._problem.time_slots): + tmp = f'{slot}:' + for lvl_idx, level in enumerate(self._problem.levels): + for sec_idx, section in enumerate(self._problem.sections): + for sub_idx, subject in enumerate(self._problem.subjects): for tch_idx, teacher in enumerate(self._problem.teachers): - for sub_idx, subject in enumerate(self._problem.subjects): - key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx) - if self._solver.BooleanValue(self._assignment[key]): - total_working_hours[subject] += self._problem.slot_duration(slt_idx) - print(f'{slot}: S:{subject} T:{teacher}') - for (subject, hours) in total_working_hours.items(): - print(f'Total working hours for {subject}: {hours}h') - - - def print_school_schedule(self): - print('School:') - for slt_idx, slot in enumerate(self._problem.time_slots): - tmp = f'{slot}:' - for lvl_idx, level in enumerate(self._problem.levels): - for sec_idx, section in enumerate(self._problem.sections): - for sub_idx, subject in enumerate(self._problem.subjects): - for tch_idx, teacher in enumerate(self._problem.teachers): - key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx) - if self._solver.BooleanValue(self._assignment[key]): - tmp += f' {level}-{section}:({subject},{teacher})' - print(tmp) - - - def solve(self): - print('Solving') - # Create Solver - self._solver = cp_model.CpSolver() - - solution_printer = SchoolSchedulingSatSolutionPrinter() - status = self._solver.Solve(self._model, solution_printer) - print('Status: ', self._solver.StatusName(status)) - - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print('\n# Teachers') - for teacher_idx in self._all_teachers: - self.print_teacher_schedule(teacher_idx) - - print('\n# Classes') - for level_idx in self._all_levels: - for section_idx in self._all_sections: - self.print_class_schedule(level_idx, section_idx) - - print('\n# School') - self.print_school_schedule() - - print('Branches: ', self._solver.NumBranches()) - print('Conflicts: ', self._solver.NumConflicts()) - print('WallTime: ', self._solver.WallTime()) + key = (lvl_idx, sec_idx, sub_idx, tch_idx, slt_idx) + if self._solver.BooleanValue(self._assignment[key]): + tmp += f' {level}-{section}:({subject},{teacher})' + print(tmp) + + def solve(self): + print('Solving') + # Create Solver + self._solver = cp_model.CpSolver() + + solution_printer = SchoolSchedulingSatSolutionPrinter() + status = self._solver.Solve(self._model, solution_printer) + print('Status: ', self._solver.StatusName(status)) + + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print('\n# Teachers') + for teacher_idx in self._all_teachers: + self.print_teacher_schedule(teacher_idx) + + print('\n# Classes') + for level_idx in self._all_levels: + for section_idx in self._all_sections: + self.print_class_schedule(level_idx, section_idx) + + print('\n# School') + self.print_school_schedule() + + print('Branches: ', self._solver.NumBranches()) + print('Conflicts: ', self._solver.NumConflicts()) + print('WallTime: ', self._solver.WallTime()) class SchoolSchedulingSatSolutionPrinter(cp_model.CpSolverSolutionCallback): - def __init__(self): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 + def __init__(self): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 - def OnSolutionCallback(self): - print( - f'Solution #{self.__solution_count}, objective: {self.ObjectiveValue()}' - ) - self.__solution_count += 1 + def OnSolutionCallback(self): + print( + f'Solution #{self.__solution_count}, objective: {self.ObjectiveValue()}' + ) + self.__solution_count += 1 def main(): - # DATA - ## Classes - LEVELS = [ - '1', - '2', - '3', - ] - SECTIONS = [ - 'A', - 'B', - ] - SUBJECTS = [ - 'English', - 'Math', - #'Science', - 'History', - ] - CURRICULUM = { - ('1', 'English'): 3, - ('1', 'Math'): 3, - ('1', 'History'): 2, - ('2', 'English'): 4, - ('2', 'Math'): 2, - ('2', 'History'): 2, - ('3', 'English'): 2, - ('3', 'Math'): 4, - ('3', 'History'): 2, - } - - ## Teachers - TEACHERS = { # name, max_work_hours - 'Mario': 14, - 'Elvis': 12, - 'Harry': 12, - 'Ian': 14, - } - # Subject -> List of teachers who can teach it - SPECIALTIES = { - 'English': ['Elvis', 'Ian'], - 'Math': ['Mario', 'Ian'], - 'History': ['Harry', 'Ian'], - } - - ## Schedule - TIME_SLOTS = { - 'Monday:08:00-09:30': 1.5, - 'Monday:09:45-11:15': 1.5, - 'Monday:11:30-12:30': 1, - 'Monday:13:30-15:30': 2, - 'Monday:15:45-17:15': 1.5, - 'Tuesday:08:00-09:30': 1.5, - 'Tuesday:09:45-11:15': 1.5, - 'Tuesday:11:30-12:30': 1, - 'Tuesday:13:30-15:30': 2, - 'Tuesday:15:45-17:15': 1.5, - 'Wednesday:08:00-09:30': 1.5, - 'Wednesday:09:45-11:15': 1.5, - 'Wednesday:11:30-12:30': 1, - 'Thursday:08:00-09:30': 1.5, - 'Thursday:09:45-11:15': 1.5, - 'Thursday:11:30-12:30': 1, - 'Thursday:13:30-15:30': 2, - 'Thursday:15:45-17:15': 1.5, - 'Friday:08:00-09:30': 1.5, - 'Friday:09:45-11:15': 1.5, - 'Friday:11:30-12:30': 1, - 'Friday:13:30-15:30': 2, - 'Friday:15:45-17:15': 1.5, - } - - problem = SchoolSchedulingProblem(LEVELS, SECTIONS, SUBJECTS, CURRICULUM, - TEACHERS, SPECIALTIES, TIME_SLOTS) - solver = SchoolSchedulingSatSolver(problem) - solver.solve() + # DATA + ## Classes + LEVELS = [ + '1', + '2', + '3', + ] + SECTIONS = [ + 'A', + 'B', + ] + SUBJECTS = [ + 'English', + 'Math', + #'Science', + 'History', + ] + CURRICULUM = { + ('1', 'English'): 3, + ('1', 'Math'): 3, + ('1', 'History'): 2, + ('2', 'English'): 4, + ('2', 'Math'): 2, + ('2', 'History'): 2, + ('3', 'English'): 2, + ('3', 'Math'): 4, + ('3', 'History'): 2, + } + + ## Teachers + TEACHERS = { # name, max_work_hours + 'Mario': 14, + 'Elvis': 12, + 'Harry': 12, + 'Ian': 14, + } + # Subject -> List of teachers who can teach it + SPECIALTIES = { + 'English': ['Elvis', 'Ian'], + 'Math': ['Mario', 'Ian'], + 'History': ['Harry', 'Ian'], + } + + ## Schedule + TIME_SLOTS = { + 'Monday:08:00-09:30': 1.5, + 'Monday:09:45-11:15': 1.5, + 'Monday:11:30-12:30': 1, + 'Monday:13:30-15:30': 2, + 'Monday:15:45-17:15': 1.5, + 'Tuesday:08:00-09:30': 1.5, + 'Tuesday:09:45-11:15': 1.5, + 'Tuesday:11:30-12:30': 1, + 'Tuesday:13:30-15:30': 2, + 'Tuesday:15:45-17:15': 1.5, + 'Wednesday:08:00-09:30': 1.5, + 'Wednesday:09:45-11:15': 1.5, + 'Wednesday:11:30-12:30': 1, + 'Thursday:08:00-09:30': 1.5, + 'Thursday:09:45-11:15': 1.5, + 'Thursday:11:30-12:30': 1, + 'Thursday:13:30-15:30': 2, + 'Thursday:15:45-17:15': 1.5, + 'Friday:08:00-09:30': 1.5, + 'Friday:09:45-11:15': 1.5, + 'Friday:11:30-12:30': 1, + 'Friday:13:30-15:30': 2, + 'Friday:15:45-17:15': 1.5, + } + + problem = SchoolSchedulingProblem( + LEVELS, SECTIONS, SUBJECTS, CURRICULUM, TEACHERS, SPECIALTIES, TIME_SLOTS + ) + solver = SchoolSchedulingSatSolver(problem) + solver.solve() if __name__ == '__main__': - main() + main() diff --git a/examples/contrib/secret_santa.py b/examples/contrib/secret_santa.py index b5bb4afe9f9..9346de25bc3 100644 --- a/examples/contrib/secret_santa.py +++ b/examples/contrib/secret_santa.py @@ -13,51 +13,51 @@ # limitations under the License. """ - Secret Santa problem in Google CP Solver. - - From Ruby Quiz Secret Santa - http://www.rubyquiz.com/quiz2.html - ''' - Honoring a long standing tradition started by my wife's dad, my friends - all play a Secret Santa game around Christmas time. We draw names and - spend a week sneaking that person gifts and clues to our identity. On the - last night of the game, we get together, have dinner, share stories, and, - most importantly, try to guess who our Secret Santa was. It's a crazily - fun way to enjoy each other's company during the holidays. - - To choose Santas, we use to draw names out of a hat. This system was - tedious, prone to many 'Wait, I got myself...' problems. This year, we - made a change to the rules that further complicated picking and we knew - the hat draw would not stand up to the challenge. Naturally, to solve - this problem, I scripted the process. Since that turned out to be more - interesting than I had expected, I decided to share. - - This weeks Ruby Quiz is to implement a Secret Santa selection script. - - Your script will be fed a list of names on STDIN. - ... - Your script should then choose a Secret Santa for every name in the list. - Obviously, a person cannot be their own Secret Santa. In addition, my friends - no longer allow people in the same family to be Santas for each other and your - script should take this into account. - ''' - - Comment: This model skips the file input and mail parts. We - assume that the friends are identified with a number from 1..n, - and the families is identified with a number 1..num_families. - - Compare with the following model: - * MiniZinc: http://www.hakank.org/minizinc/secret_santa.mzn - - - This model gives 4089600 solutions and the following statistics: - - failures: 31264 - - branches: 8241726 - - WallTime: 23735 ms (note: without any printing of the solutions) - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Secret Santa problem in Google CP Solver. + +From Ruby Quiz Secret Santa +http://www.rubyquiz.com/quiz2.html +''' +Honoring a long standing tradition started by my wife's dad, my friends +all play a Secret Santa game around Christmas time. We draw names and +spend a week sneaking that person gifts and clues to our identity. On the +last night of the game, we get together, have dinner, share stories, and, +most importantly, try to guess who our Secret Santa was. It's a crazily +fun way to enjoy each other's company during the holidays. + +To choose Santas, we use to draw names out of a hat. This system was +tedious, prone to many 'Wait, I got myself...' problems. This year, we +made a change to the rules that further complicated picking and we knew +the hat draw would not stand up to the challenge. Naturally, to solve +this problem, I scripted the process. Since that turned out to be more +interesting than I had expected, I decided to share. + +This weeks Ruby Quiz is to implement a Secret Santa selection script. + +Your script will be fed a list of names on STDIN. +... +Your script should then choose a Secret Santa for every name in the list. +Obviously, a person cannot be their own Secret Santa. In addition, my friends +no longer allow people in the same family to be Santas for each other and your +script should take this into account. +''' + +Comment: This model skips the file input and mail parts. We + assume that the friends are identified with a number from 1..n, + and the families is identified with a number 1..num_families. + +Compare with the following model: +* MiniZinc: http://www.hakank.org/minizinc/secret_santa.mzn + + +This model gives 4089600 solutions and the following statistics: +- failures: 31264 +- branches: 8241726 +- WallTime: 23735 ms (note: without any printing of the solutions) + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp diff --git a/examples/contrib/secret_santa2.py b/examples/contrib/secret_santa2.py index 4eae4493d0e..aa219fb17c1 100644 --- a/examples/contrib/secret_santa2.py +++ b/examples/contrib/secret_santa2.py @@ -13,52 +13,52 @@ # limitations under the License. """ - Secret Santa problem II in Google CP Solver. - - From Maple Primes: 'Secret Santa Graph Theory' - http://www.mapleprimes.com/blog/jpmay/secretsantagraphtheory - ''' - Every year my extended family does a 'secret santa' gift exchange. - Each person draws another person at random and then gets a gift for - them. At first, none of my siblings were married, and so the draw was - completely random. Then, as people got married, we added the restriction - that spouses should not draw each others names. This restriction meant - that we moved from using slips of paper on a hat to using a simple - computer program to choose names. Then people began to complain when - they would get the same person two years in a row, so the program was - modified to keep some history and avoid giving anyone a name in their - recent history. This year, not everyone was participating, and so after - removing names, and limiting the number of exclusions to four per person, - I had data something like this: - - Name: Spouse, Recent Picks - - Noah: Ava. Ella, Evan, Ryan, John - Ava: Noah, Evan, Mia, John, Ryan - Ryan: Mia, Ella, Ava, Lily, Evan - Mia: Ryan, Ava, Ella, Lily, Evan - Ella: John, Lily, Evan, Mia, Ava - John: Ella, Noah, Lily, Ryan, Ava - Lily: Evan, John, Mia, Ava, Ella - Evan: Lily, Mia, John, Ryan, Noah - ''' - - Note: I interpret this as the following three constraints: - 1) One cannot be a Secret Santa of one's spouse - 2) One cannot be a Secret Santa for somebody two years in a row - 3) Optimization: maximize the time since the last time - - This model also handle single persons, something the original - problem don't mention. - - Compare with the following models: - * Google CP Solver: http://www.hakank.org/google_or_tools/secret_santa.py - * MiniZinc: http://www.hakank.org/minizinc/secret_santa2.mzn - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Secret Santa problem II in Google CP Solver. + +From Maple Primes: 'Secret Santa Graph Theory' +http://www.mapleprimes.com/blog/jpmay/secretsantagraphtheory +''' +Every year my extended family does a 'secret santa' gift exchange. +Each person draws another person at random and then gets a gift for +them. At first, none of my siblings were married, and so the draw was +completely random. Then, as people got married, we added the restriction +that spouses should not draw each others names. This restriction meant +that we moved from using slips of paper on a hat to using a simple +computer program to choose names. Then people began to complain when +they would get the same person two years in a row, so the program was +modified to keep some history and avoid giving anyone a name in their +recent history. This year, not everyone was participating, and so after +removing names, and limiting the number of exclusions to four per person, +I had data something like this: + +Name: Spouse, Recent Picks + +Noah: Ava. Ella, Evan, Ryan, John +Ava: Noah, Evan, Mia, John, Ryan +Ryan: Mia, Ella, Ava, Lily, Evan +Mia: Ryan, Ava, Ella, Lily, Evan +Ella: John, Lily, Evan, Mia, Ava +John: Ella, Noah, Lily, Ryan, Ava +Lily: Evan, John, Mia, Ava, Ella +Evan: Lily, Mia, John, Ryan, Noah +''' + +Note: I interpret this as the following three constraints: + 1) One cannot be a Secret Santa of one's spouse + 2) One cannot be a Secret Santa for somebody two years in a row + 3) Optimization: maximize the time since the last time + +This model also handle single persons, something the original +problem don't mention. + +Compare with the following models: +* Google CP Solver: http://www.hakank.org/google_or_tools/secret_santa.py +* MiniZinc: http://www.hakank.org/minizinc/secret_santa2.mzn + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -92,7 +92,7 @@ def main(singe=0): [M, 4, M, 3, 0, M, 1, 2], # Ella [1, 4, 3, M, M, 0, 2, M], # John [M, 3, M, 2, 4, 1, 0, M], # Lily - [4, M, 3, 1, M, 2, M, 0] # Evan + [4, M, 3, 1, M, 2, M, 0], # Evan ] # @@ -110,7 +110,7 @@ def main(singe=0): [1, 4, 3, M, M, 0, 2, M, M], # John [M, 3, M, 2, 4, 1, 0, M, M], # Lily [4, M, 3, 1, M, 2, M, 0, M], # Evan - [1, 2, 3, 4, M, 2, M, M, 0] # Single + [1, 2, 3, 4, M, 2, M, M, 0], # Single ] if single == 1: @@ -125,7 +125,15 @@ def main(singe=0): M = n + 1 persons = [ - 'Noah', 'Ava', 'Ryan', 'Mia', 'Ella', 'John', 'Lily', 'Evan', 'Single' + 'Noah', + 'Ava', + 'Ryan', + 'Mia', + 'Ella', + 'John', + 'Lily', + 'Evan', + 'Single', ] spouses = [ @@ -137,7 +145,7 @@ def main(singe=0): Ella, # John Evan, # Lily Lily, # Evan - -1 # Single has no spouse + -1, # Single has no spouse ] # @@ -185,8 +193,9 @@ def main(singe=0): # # solution and search # - db = solver.Phase(santas, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, - solver.ASSIGN_CENTER_VALUE) + db = solver.Phase( + santas, solver.CHOOSE_MIN_SIZE_LOWEST_MIN, solver.ASSIGN_CENTER_VALUE + ) solver.NewSearch(db, [objective]) @@ -196,10 +205,10 @@ def main(singe=0): print('total distances:', z.Value()) print('santas:', [santas[i].Value() for i in range(n)]) for i in range(n): - print('%s\tis a Santa to %s (distance %i)' % \ - (persons[i], - persons[santas[i].Value()], - santa_distance[i].Value())) + print( + '%s\tis a Santa to %s (distance %i)' + % (persons[i], persons[santas[i].Value()], santa_distance[i].Value()) + ) # print 'distance:', [santa_distance[i].Value() # for i in range(n)] print() diff --git a/examples/contrib/send_more_money_any_base.py b/examples/contrib/send_more_money_any_base.py index 795363355d9..ab066b3cd61 100644 --- a/examples/contrib/send_more_money_any_base.py +++ b/examples/contrib/send_more_money_any_base.py @@ -13,32 +13,32 @@ # limitations under the License. """ - SEND+MORE=MONEY in 'any' base in Google CP Solver. +SEND+MORE=MONEY in 'any' base in Google CP Solver. - Alphametic problem SEND+MORE=MONEY in any base. +Alphametic problem SEND+MORE=MONEY in any base. - Examples: - Base 10 has one solution: - {9, 5, 6, 7, 1, 0, 8, 2} - Base 11 has three soltutions: - {10, 5, 6, 8, 1, 0, 9, 2} - {10, 6, 7, 8, 1, 0, 9, 3} - {10, 7, 8, 6, 1, 0, 9, 2} +Examples: +Base 10 has one solution: + {9, 5, 6, 7, 1, 0, 8, 2} +Base 11 has three soltutions: + {10, 5, 6, 8, 1, 0, 9, 2} + {10, 6, 7, 8, 1, 0, 9, 3} + {10, 7, 8, 6, 1, 0, 9, 2} - Also, compare with the following models: - * Comet : http://www.hakank.org/comet/send_more_money_any_base.co - * ECLiPSE : http://www.hakank.org/eclipse/send_more_money_any_base.ecl - * Essence : http://www.hakank.org/tailor/send_more_money_any_base.eprime - * Gecode : http://www.hakank.org/gecode/send_more_money_any_base.cpp - * Gecode/R: http://www.hakank.org/gecode_r/send_more_money_any_base.rb - * MiniZinc: http://www.hakank.org/minizinc/send_more_money_any_base.mzn - * Zinc: http://www.hakank.org/minizinc/send_more_money_any_base.zinc - * SICStus: http://www.hakank.org/sicstus/send_more_money_any_base.pl +Also, compare with the following models: +* Comet : http://www.hakank.org/comet/send_more_money_any_base.co +* ECLiPSE : http://www.hakank.org/eclipse/send_more_money_any_base.ecl +* Essence : http://www.hakank.org/tailor/send_more_money_any_base.eprime +* Gecode : http://www.hakank.org/gecode/send_more_money_any_base.cpp +* Gecode/R: http://www.hakank.org/gecode_r/send_more_money_any_base.rb +* MiniZinc: http://www.hakank.org/minizinc/send_more_money_any_base.mzn +* Zinc: http://www.hakank.org/minizinc/send_more_money_any_base.zinc +* SICStus: http://www.hakank.org/sicstus/send_more_money_any_base.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -70,8 +70,16 @@ def main(base=10): # solver.Add(solver.AllDifferent(x)) solver.Add( - s * base**3 + e * base**2 + n * base + d + m * base**3 + o * base**2 + - r * base + e == m * base**4 + o * base**3 + n * base**2 + e * base + y,) + s * base**3 + + e * base**2 + + n * base + + d + + m * base**3 + + o * base**2 + + r * base + + e + == m * base**4 + o * base**3 + n * base**2 + e * base + y, + ) solver.Add(s > 0) solver.Add(m > 0) @@ -85,7 +93,8 @@ def main(base=10): solver.Solve( solver.Phase(x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MAX_VALUE), - [collector]) + [collector], + ) num_solutions = collector.SolutionCount() money_val = 0 diff --git a/examples/contrib/send_most_money.py b/examples/contrib/send_most_money.py index ab35ad62ac0..3954a022736 100644 --- a/examples/contrib/send_most_money.py +++ b/examples/contrib/send_most_money.py @@ -13,28 +13,28 @@ # limitations under the License. """ - SEND+MOST=MONEY in Google CP Solver. +SEND+MOST=MONEY in Google CP Solver. - Alphametic problem were we maximize MONEY. +Alphametic problem were we maximize MONEY. - Problem from the lecture notes: - http://www.ict.kth.se/courses/ID2204/notes/L01.pdf +Problem from the lecture notes: +http://www.ict.kth.se/courses/ID2204/notes/L01.pdf - Compare with the following models: - * Comet : http://www.hakank.org/comet/send_most_money.co - * Comet : http://www.hakank.org/comet/send_most_money2.co - * ECLiPSE : http://www.hakank.org/eclipse/send_most_money.ecl - * SICStus: http://hakank.org/sicstus/send_most_money.pl - * MiniZinc: http://www.hakank.org/minizinc/send_most_money.mzn - * Gecode/R: http://www.hakank.org/gecode_r/send_most_money2.rb - * Tailor/Essence': http://www.hakank.org/tailor/send_most_money.eprime - * Zinc: http://www.hakank.org/minizinc/send_most_money.zinc +Compare with the following models: +* Comet : http://www.hakank.org/comet/send_most_money.co +* Comet : http://www.hakank.org/comet/send_most_money2.co +* ECLiPSE : http://www.hakank.org/eclipse/send_most_money.ecl +* SICStus: http://hakank.org/sicstus/send_most_money.pl +* MiniZinc: http://www.hakank.org/minizinc/send_most_money.mzn +* Gecode/R: http://www.hakank.org/gecode_r/send_most_money2.rb +* Tailor/Essence': http://www.hakank.org/tailor/send_most_money.eprime +* Zinc: http://www.hakank.org/minizinc/send_most_money.zinc - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ @@ -70,8 +70,9 @@ def main(MONEY=0): solver.Add(solver.AllDifferent(x)) solver.Add(money == m * 10000 + o * 1000 + n * 100 + e * 10 + y) solver.Add(money > 0) - solver.Add(1000 * s + 100 * e + 10 * n + d + 1000 * m + 100 * o + 10 * s + - t == money) + solver.Add( + 1000 * s + 100 * e + 10 * n + d + 1000 * m + 100 * o + 10 * s + t == money + ) solver.Add(s > 0) solver.Add(m > 0) @@ -91,7 +92,8 @@ def main(MONEY=0): solver.Solve( solver.Phase(x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MAX_VALUE), - cargs) + cargs, + ) num_solutions = collector.SolutionCount() money_val = 0 diff --git a/examples/contrib/seseman.py b/examples/contrib/seseman.py index f60a185c942..007504f0f14 100644 --- a/examples/contrib/seseman.py +++ b/examples/contrib/seseman.py @@ -13,45 +13,45 @@ # limitations under the License. """ - Seseman Convent problem in Google CP Solver. +Seseman Convent problem in Google CP Solver. - n is the length of a border - There are (n-2)^2 "holes", i.e. - there are n^2 - (n-2)^2 variables to find out. +n is the length of a border +There are (n-2)^2 "holes", i.e. +there are n^2 - (n-2)^2 variables to find out. - The simplest problem, n = 3 (n x n matrix) - which is represented by the following matrix: +The simplest problem, n = 3 (n x n matrix) +which is represented by the following matrix: - a b c - d e - f g h + a b c + d e + f g h - Where the following constraints must hold: +Where the following constraints must hold: - a + b + c = border_sum - a + d + f = border_sum - c + e + h = border_sum - f + g + h = border_sum - a + b + c + d + e + f = total_sum + a + b + c = border_sum + a + d + f = border_sum + c + e + h = border_sum + f + g + h = border_sum + a + b + c + d + e + f = total_sum - Compare with the following models: - * Tailor/Essence': http://hakank.org/tailor/seseman.eprime - * MiniZinc: http://hakank.org/minizinc/seseman.mzn - * SICStus: http://hakank.org/sicstus/seseman.pl - * Zinc: http://hakank.org/minizinc/seseman.zinc - * Choco: http://hakank.org/choco/Seseman.java - * Comet: http://hakank.org/comet/seseman.co - * ECLiPSe: http://hakank.org/eclipse/seseman.ecl - * Gecode: http://hakank.org/gecode/seseman.cpp - * Gecode/R: http://hakank.org/gecode_r/seseman.rb - * JaCoP: http://hakank.org/JaCoP/Seseman.java +Compare with the following models: +* Tailor/Essence': http://hakank.org/tailor/seseman.eprime +* MiniZinc: http://hakank.org/minizinc/seseman.mzn +* SICStus: http://hakank.org/sicstus/seseman.pl +* Zinc: http://hakank.org/minizinc/seseman.zinc +* Choco: http://hakank.org/choco/Seseman.java +* Comet: http://hakank.org/comet/seseman.co +* ECLiPSe: http://hakank.org/eclipse/seseman.ecl +* Gecode: http://hakank.org/gecode/seseman.cpp +* Gecode/R: http://hakank.org/gecode_r/seseman.rb +* JaCoP: http://hakank.org/JaCoP/Seseman.java - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -94,7 +94,8 @@ def main(unused_argv): # total solver.Add( - solver.Sum([x[(i, j)] for i in range(n) for j in range(n)]) == total_sum) + solver.Sum([x[(i, j)] for i in range(n) for j in range(n)]) == total_sum + ) # # solution and search @@ -107,9 +108,14 @@ def main(unused_argv): collector = solver.AllSolutionCollector(solution) # search_log = solver.SearchLog(100, total_sum) solver.Solve( - solver.Phase([x[(i, j)] for i in range(n) for j in range(n)], - solver.CHOOSE_PATH, solver.ASSIGN_MIN_VALUE), [collector]) - #[collector, search_log]) + solver.Phase( + [x[(i, j)] for i in range(n) for j in range(n)], + solver.CHOOSE_PATH, + solver.ASSIGN_MIN_VALUE, + ), + [collector], + ) + # [collector, search_log]) num_solutions = collector.SolutionCount() # print "x:", x diff --git a/examples/contrib/seseman_b.py b/examples/contrib/seseman_b.py index c96505d615c..183f2a1eddd 100644 --- a/examples/contrib/seseman_b.py +++ b/examples/contrib/seseman_b.py @@ -13,47 +13,47 @@ # limitations under the License. """ - Seseman Convent problem in Google CP Solver. +Seseman Convent problem in Google CP Solver. - n is the length of a border - There are (n-2)^2 "holes", i.e. - there are n^2 - (n-2)^2 variables to find out. +n is the length of a border +There are (n-2)^2 "holes", i.e. +there are n^2 - (n-2)^2 variables to find out. - The simplest problem, n = 3 (n x n matrix) - which is represented by the following matrix: +The simplest problem, n = 3 (n x n matrix) +which is represented by the following matrix: - a b c - d e - f g h + a b c + d e + f g h - Where the following constraints must hold: +Where the following constraints must hold: - a + b + c = border_sum - a + d + f = border_sum - c + e + h = border_sum - f + g + h = border_sum - a + b + c + d + e + f = total_sum + a + b + c = border_sum + a + d + f = border_sum + c + e + h = border_sum + f + g + h = border_sum + a + b + c + d + e + f = total_sum - Compare with the following models: - * Tailor/Essence': http://hakank.org/tailor/seseman.eprime - * MiniZinc: http://hakank.org/minizinc/seseman.mzn - * SICStus: http://hakank.org/sicstus/seseman.pl - * Zinc: http://hakank.org/minizinc/seseman.zinc - * Choco: http://hakank.org/choco/Seseman.java - * Comet: http://hakank.org/comet/seseman.co - * ECLiPSe: http://hakank.org/eclipse/seseman.ecl - * Gecode: http://hakank.org/gecode/seseman.cpp - * Gecode/R: http://hakank.org/gecode_r/seseman.rb - * JaCoP: http://hakank.org/JaCoP/Seseman.java +Compare with the following models: +* Tailor/Essence': http://hakank.org/tailor/seseman.eprime +* MiniZinc: http://hakank.org/minizinc/seseman.mzn +* SICStus: http://hakank.org/sicstus/seseman.pl +* Zinc: http://hakank.org/minizinc/seseman.zinc +* Choco: http://hakank.org/choco/Seseman.java +* Comet: http://hakank.org/comet/seseman.co +* ECLiPSe: http://hakank.org/eclipse/seseman.ecl +* Gecode: http://hakank.org/gecode/seseman.cpp +* Gecode/R: http://hakank.org/gecode_r/seseman.rb +* JaCoP: http://hakank.org/JaCoP/Seseman.java - This version use a better way of looping through all solutions. +This version use a better way of looping through all solutions. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -96,7 +96,8 @@ def main(unused_argv): # total solver.Add( - solver.Sum([x[(i, j)] for i in range(n) for j in range(n)]) == total_sum) + solver.Sum([x[(i, j)] for i in range(n) for j in range(n)]) == total_sum + ) # # solution and search @@ -105,8 +106,11 @@ def main(unused_argv): solution.Add([x[(i, j)] for i in range(n) for j in range(n)]) solution.Add(total_sum) - db = solver.Phase([x[(i, j)] for i in range(n) for j in range(n)], - solver.CHOOSE_PATH, solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + [x[(i, j)] for i in range(n) for j in range(n)], + solver.CHOOSE_PATH, + solver.ASSIGN_MIN_VALUE, + ) solver.NewSearch(db) diff --git a/examples/contrib/set_covering.py b/examples/contrib/set_covering.py index 875ca4817a7..1a55a085a7e 100644 --- a/examples/contrib/set_covering.py +++ b/examples/contrib/set_covering.py @@ -13,21 +13,21 @@ # limitations under the License. """ - Set covering in Google CP Solver. +Set covering in Google CP Solver. - Placing of firestations, from Winston 'Operations Research', page 486. +Placing of firestations, from Winston 'Operations Research', page 486. - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/set_covering.mzn - * ECLiPSe : http://www.hakank.org/eclipse/set_covering.ecl - * Comet : http://www.hakank.org/comet/set_covering.co - * Gecode : http://www.hakank.org/gecode/set_covering.cpp - * SICStus : http://www.hakank.org/sicstus/set_covering.pl +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/set_covering.mzn +* ECLiPSe : http://www.hakank.org/eclipse/set_covering.ecl +* Comet : http://www.hakank.org/comet/set_covering.co +* Gecode : http://www.hakank.org/gecode/set_covering.cpp +* SICStus : http://www.hakank.org/sicstus/set_covering.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -44,9 +44,14 @@ def main(unused_argv): min_distance = 15 num_cities = 6 - distance = [[0, 10, 20, 30, 30, 20], [10, 0, 25, 35, 20, 10], - [20, 25, 0, 15, 30, 20], [30, 35, 15, 0, 15, 25], - [30, 20, 30, 15, 0, 14], [20, 10, 20, 25, 14, 0]] + distance = [ + [0, 10, 20, 30, 30, 20], + [10, 0, 25, 35, 20, 10], + [20, 25, 0, 15, 30, 20], + [30, 35, 15, 0, 15, 25], + [30, 20, 30, 15, 0, 14], + [20, 10, 20, 25, 14, 0], + ] # # declare variables @@ -77,7 +82,8 @@ def main(unused_argv): collector = solver.LastSolutionCollector(solution) solver.Solve( solver.Phase(x + [z], solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT), - [collector, objective]) + [collector, objective], + ) print("z:", collector.ObjectiveValue(0)) print("x:", [collector.Value(0, x[i]) for i in range(num_cities)]) diff --git a/examples/contrib/set_covering2.py b/examples/contrib/set_covering2.py index 6ae299b1bd3..49a2c378826 100644 --- a/examples/contrib/set_covering2.py +++ b/examples/contrib/set_covering2.py @@ -13,23 +13,23 @@ # limitations under the License. """ - Set covering in Google CP Solver. +Set covering in Google CP Solver. - Example 9.1-2, page 354ff, from - Taha 'Operations Research - An Introduction' - Minimize the number of security telephones in street - corners on a campus. +Example 9.1-2, page 354ff, from +Taha 'Operations Research - An Introduction' +Minimize the number of security telephones in street +corners on a campus. - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/set_covering2.mzn - * Comet : http://www.hakank.org/comet/set_covering2.co - * ECLiPSe : http://www.hakank.org/eclipse/set_covering2.ecl - * SICStus: http://hakank.org/sicstus/set_covering2.pl - * Gecode: http://hakank.org/gecode/set_covering2.cpp +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/set_covering2.mzn +* Comet : http://www.hakank.org/comet/set_covering2.co +* ECLiPSe : http://www.hakank.org/eclipse/set_covering2.ecl +* SICStus: http://hakank.org/sicstus/set_covering2.pl +* Gecode: http://hakank.org/gecode/set_covering2.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -48,8 +48,19 @@ def main(unused_argv): # corners of each street # Note: 1-based (handled below) - corner = [[1, 2], [2, 3], [4, 5], [7, 8], [6, 7], [2, 6], [1, 6], [4, 7], - [2, 4], [5, 8], [3, 5]] + corner = [ + [1, 2], + [2, 3], + [4, 5], + [7, 8], + [6, 7], + [2, 6], + [1, 6], + [4, 7], + [2, 4], + [5, 8], + [3, 5], + ] # # declare variables @@ -80,7 +91,8 @@ def main(unused_argv): collector = solver.LastSolutionCollector(solution) solver.Solve( solver.Phase(x, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT), - [collector, objective]) + [collector, objective], + ) print("z:", collector.ObjectiveValue(0)) print("x:", [collector.Value(0, x[i]) for i in range(n)]) diff --git a/examples/contrib/set_covering3.py b/examples/contrib/set_covering3.py index 7fa1e01b291..20ce75048c1 100644 --- a/examples/contrib/set_covering3.py +++ b/examples/contrib/set_covering3.py @@ -13,36 +13,36 @@ # limitations under the License. """ - Set covering in Google CP Solver. +Set covering in Google CP Solver. - Problem from - Katta G. Murty: 'Optimization Models for Decision Making', page 302f - http://ioe.engin.umich.edu/people/fac/books/murty/opti_model/junior-7.pdf +Problem from +Katta G. Murty: 'Optimization Models for Decision Making', page 302f +http://ioe.engin.umich.edu/people/fac/books/murty/opti_model/junior-7.pdf - 10 senators making a committee, where there must at least be one - representative from each group: - group: senators: - southern 1 2 3 4 5 - northern 6 7 8 9 10 - liberals 2 3 8 9 10 - conservative 1 5 6 7 - democrats 3 4 5 6 7 9 - republicans 1 2 8 10 +10 senators making a committee, where there must at least be one +representative from each group: +group: senators: +southern 1 2 3 4 5 +northern 6 7 8 9 10 +liberals 2 3 8 9 10 +conservative 1 5 6 7 +democrats 3 4 5 6 7 9 +republicans 1 2 8 10 - The objective is to minimize the number of senators. +The objective is to minimize the number of senators. - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/set_covering3_model.mzn (model) - http://www.hakank.org/minizinc/set_covering3.mzn (data) - * Comet : http://www.hakank.org/comet/set_covering3.co - * ECLiPSe : http://www.hakank.org/eclipse/set_covering3.ecl - * SICStus : http://hakank.org/sicstus/set_covering3.pl - * Gecode : http://hakank.org/gecode/set_covering3.cpp +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/set_covering3_model.mzn (model) + http://www.hakank.org/minizinc/set_covering3.mzn (data) +* Comet : http://www.hakank.org/comet/set_covering3.co +* ECLiPSe : http://www.hakank.org/eclipse/set_covering3.ecl +* SICStus : http://hakank.org/sicstus/set_covering3.pl +* Gecode : http://hakank.org/gecode/set_covering3.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -66,7 +66,7 @@ def main(unused_argv): [0, 1, 1, 0, 0, 0, 0, 1, 1, 1], # 3 liberals [1, 0, 0, 0, 1, 1, 1, 0, 0, 0], # 4 conservative [0, 0, 1, 1, 1, 1, 1, 0, 1, 0], # 5 democrats - [1, 1, 0, 0, 0, 0, 0, 1, 0, 1] # 6 republicans + [1, 1, 0, 0, 0, 0, 0, 1, 0, 1], # 6 republicans ] # @@ -86,7 +86,9 @@ def main(unused_argv): for i in range(num_groups): solver.Add( solver.SumGreaterOrEqual( - [x[j] * belongs[i][j] for j in range(num_senators)], 1)) + [x[j] * belongs[i][j] for j in range(num_senators)], 1 + ) + ) objective = solver.Minimize(z, 1) @@ -100,7 +102,8 @@ def main(unused_argv): collector = solver.LastSolutionCollector(solution) solver.Solve( solver.Phase(x, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT), - [collector, objective]) + [collector, objective], + ) print("z:", collector.ObjectiveValue(0)) print("x:", [collector.Value(0, x[i]) for i in range(num_senators)]) diff --git a/examples/contrib/set_covering4.py b/examples/contrib/set_covering4.py index ba217cc42d3..dbe1e22ee10 100644 --- a/examples/contrib/set_covering4.py +++ b/examples/contrib/set_covering4.py @@ -13,56 +13,56 @@ # limitations under the License. """ - Set partition and set covering in Google CP Solver. - - Example from the Swedish book - Lundgren, Roennqvist, Vaebrand - 'Optimeringslaera' (translation: 'Optimization theory'), - page 408. - - * Set partition: - We want to minimize the cost of the alternatives which covers all the - objects, i.e. all objects must be choosen. The requirement is than an - object may be selected _exactly_ once. - - Note: This is 1-based representation - - Alternative Cost Object - 1 19 1,6 - 2 16 2,6,8 - 3 18 1,4,7 - 4 13 2,3,5 - 5 15 2,5 - 6 19 2,3 - 7 15 2,3,4 - 8 17 4,5,8 - 9 16 3,6,8 - 10 15 1,6,7 - - The problem has a unique solution of z = 49 where alternatives - 3, 5, and 9 - is selected. - - * Set covering: - If we, however, allow that an object is selected _more than one time_, - then the solution is z = 45 (i.e. less cost than the first problem), - and the alternatives - 4, 8, and 10 - is selected, where object 5 is selected twice (alt. 4 and 8). - It's an unique solution as well. - - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/set_covering4.mzn - * Comet : http://www.hakank.org/comet/set_covering4.co - * ECLiPSe : http://www.hakank.org/eclipse/set_covering4.ecl - * SICStus : http://www.hakank.org/sicstus/set_covering4.pl - * Gecode : http://www.hakank.org/gecode/set_covering4.cpp - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ + Set partition and set covering in Google CP Solver. + + Example from the Swedish book + Lundgren, Roennqvist, Vaebrand + 'Optimeringslaera' (translation: 'Optimization theory'), + page 408. + + * Set partition: + We want to minimize the cost of the alternatives which covers all the + objects, i.e. all objects must be choosen. The requirement is than an + object may be selected _exactly_ once. + + Note: This is 1-based representation + + Alternative Cost Object + 1 19 1,6 + 2 16 2,6,8 + 3 18 1,4,7 + 4 13 2,3,5 + 5 15 2,5 + 6 19 2,3 + 7 15 2,3,4 + 8 17 4,5,8 + 9 16 3,6,8 + 10 15 1,6,7 + + The problem has a unique solution of z = 49 where alternatives + 3, 5, and 9 + is selected. + + * Set covering: + If we, however, allow that an object is selected _more than one time_, + then the solution is z = 45 (i.e. less cost than the first problem), + and the alternatives + 4, 8, and 10 + is selected, where object 5 is selected twice (alt. 4 and 8). + It's an unique solution as well. + + +Compare with the following models: + * MiniZinc: http://www.hakank.org/minizinc/set_covering4.mzn + * Comet : http://www.hakank.org/comet/set_covering4.co + * ECLiPSe : http://www.hakank.org/eclipse/set_covering4.ecl + * SICStus : http://www.hakank.org/sicstus/set_covering4.pl + * Gecode : http://www.hakank.org/gecode/set_covering4.cpp + + + This model was created by Hakan Kjellerstrand (hakank@gmail.com) + Also see my other Google CP Solver models: + http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -94,7 +94,7 @@ def main(set_partition=1): [0, 1, 1, 1, 0, 0, 0, 0], # alternative 7 [0, 0, 0, 1, 1, 0, 0, 1], # alternative 8 [0, 0, 1, 0, 0, 1, 0, 1], # alternative 9 - [1, 0, 0, 0, 0, 1, 1, 0] # alternative 10 + [1, 0, 0, 0, 0, 1, 1, 0], # alternative 10 ] # @@ -114,12 +114,16 @@ def main(set_partition=1): for j in range(num_objects): if set_partition == 1: solver.Add( - solver.SumEquality([x[i] * a[i][j] for i in range(num_alternatives)], - 1)) + solver.SumEquality( + [x[i] * a[i][j] for i in range(num_alternatives)], 1 + ) + ) else: solver.Add( solver.SumGreaterOrEqual( - [x[i] * a[i][j] for i in range(num_alternatives)], 1)) + [x[i] * a[i][j] for i in range(num_alternatives)], 1 + ) + ) objective = solver.Minimize(z, 1) @@ -132,14 +136,19 @@ def main(set_partition=1): collector = solver.LastSolutionCollector(solution) solver.Solve( - solver.Phase([x[i] for i in range(num_alternatives)], - solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT), - [collector, objective]) + solver.Phase( + [x[i] for i in range(num_alternatives)], + solver.INT_VAR_DEFAULT, + solver.INT_VALUE_DEFAULT, + ), + [collector, objective], + ) print("z:", collector.ObjectiveValue(0)) print( "selected alternatives:", - [i + 1 for i in range(num_alternatives) if collector.Value(0, x[i]) == 1]) + [i + 1 for i in range(num_alternatives) if collector.Value(0, x[i]) == 1], + ) print("failures:", solver.Failures()) print("branches:", solver.Branches()) diff --git a/examples/contrib/set_covering_deployment.py b/examples/contrib/set_covering_deployment.py index 92440fc4bdc..3842dbd9695 100644 --- a/examples/contrib/set_covering_deployment.py +++ b/examples/contrib/set_covering_deployment.py @@ -13,29 +13,29 @@ # limitations under the License. """ - Set covering deployment in Google CP Solver - - From http://mathworld.wolfram.com/SetCoveringDeployment.html - ''' - Set covering deployment (sometimes written 'set-covering deployment' - and abbreviated SCDP for 'set covering deployment problem') seeks - an optimal stationing of troops in a set of regions so that a - relatively small number of troop units can control a large - geographic region. ReVelle and Rosing (2000) first described - this in a study of Emperor Constantine the Great's mobile field - army placements to secure the Roman Empire. - ''' - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/set_covering_deployment.mzn - * Comet : http://www.hakank.org/comet/set_covering_deployment.co - * Gecode : http://www.hakank.org/gecode/set_covering_deployment.cpp - * ECLiPSe : http://www.hakank.org/eclipse/set_covering_deployment.ecl - * SICStus : http://hakank.org/sicstus/set_covering_deployment.pl - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Set covering deployment in Google CP Solver + +From http://mathworld.wolfram.com/SetCoveringDeployment.html +''' +Set covering deployment (sometimes written 'set-covering deployment' +and abbreviated SCDP for 'set covering deployment problem') seeks +an optimal stationing of troops in a set of regions so that a +relatively small number of troop units can control a large +geographic region. ReVelle and Rosing (2000) first described +this in a study of Emperor Constantine the Great's mobile field +army placements to secure the Roman Empire. +''' + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/set_covering_deployment.mzn +* Comet : http://www.hakank.org/comet/set_covering_deployment.co +* Gecode : http://www.hakank.org/gecode/set_covering_deployment.cpp +* ECLiPSe : http://www.hakank.org/eclipse/set_covering_deployment.ecl +* SICStus : http://hakank.org/sicstus/set_covering_deployment.pl + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -51,16 +51,28 @@ def main(): # countries = [ - "Alexandria", "Asia Minor", "Britain", "Byzantium", "Gaul", "Iberia", - "Rome", "Tunis" + "Alexandria", + "Asia Minor", + "Britain", + "Byzantium", + "Gaul", + "Iberia", + "Rome", + "Tunis", ] n = len(countries) # the incidence matrix (neighbours) - mat = [[0, 1, 0, 1, 0, 0, 1, 1], [1, 0, 0, 1, 0, 0, 0, 0], - [0, 0, 0, 0, 1, 1, 0, 0], [1, 1, 0, 0, 0, 0, 1, 0], - [0, 0, 1, 0, 0, 1, 1, 0], [0, 0, 1, 0, 1, 0, 1, 1], - [1, 0, 0, 1, 1, 1, 0, 1], [1, 0, 0, 0, 0, 1, 1, 0]] + mat = [ + [0, 1, 0, 1, 0, 0, 1, 1], + [1, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 1, 0, 0], + [1, 1, 0, 0, 0, 0, 1, 0], + [0, 0, 1, 0, 0, 1, 1, 0], + [0, 0, 1, 0, 1, 0, 1, 1], + [1, 0, 0, 1, 1, 1, 0, 1], + [1, 0, 0, 0, 0, 1, 1, 0], + ] # # declare variables @@ -108,7 +120,8 @@ def main(): collector = solver.LastSolutionCollector(solution) solver.Solve( solver.Phase(X + Y, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT), - [collector, objective]) + [collector, objective], + ) print("num_armies:", collector.ObjectiveValue(0)) print("X:", [collector.Value(0, X[i]) for i in range(n)]) diff --git a/examples/contrib/set_covering_skiena.py b/examples/contrib/set_covering_skiena.py index 21f02b66608..a6c72f65137 100644 --- a/examples/contrib/set_covering_skiena.py +++ b/examples/contrib/set_covering_skiena.py @@ -13,30 +13,30 @@ # limitations under the License. """ - Set covering in Google CP Solver. - - Example from Steven Skiena, The Stony Brook Algorithm Repository - http://www.cs.sunysb.edu/~algorith/files/set-cover.shtml - ''' - Input Description: A set of subsets S_1, ..., S_m of the - universal set U = {1,...,n}. - - Problem: What is the smallest subset of subsets T subset S such - that \cup_{t_i in T} t_i = U? - ''' - Data is from the pictures INPUT/OUTPUT. - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/set_covering_skiena.mzn - * Comet: http://www.hakank.org/comet/set_covering_skiena.co - * ECLiPSe: http://www.hakank.org/eclipse/set_covering_skiena.ecl - * SICStus Prolog: http://www.hakank.org/sicstus/set_covering_skiena.pl - * Gecode: http://hakank.org/gecode/set_covering_skiena.cpp - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Set covering in Google CP Solver. + +Example from Steven Skiena, The Stony Brook Algorithm Repository +http://www.cs.sunysb.edu/~algorith/files/set-cover.shtml +''' +Input Description: A set of subsets S_1, ..., S_m of the +universal set U = {1,...,n}. + +Problem: What is the smallest subset of subsets T subset S such +that \cup_{t_i in T} t_i = U? +''' +Data is from the pictures INPUT/OUTPUT. + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/set_covering_skiena.mzn +* Comet: http://www.hakank.org/comet/set_covering_skiena.co +* ECLiPSe: http://www.hakank.org/eclipse/set_covering_skiena.ecl +* SICStus Prolog: http://www.hakank.org/sicstus/set_covering_skiena.pl +* Gecode: http://hakank.org/gecode/set_covering_skiena.cpp + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -59,7 +59,7 @@ def main(): [0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0], # 4 [0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0], # 5 [1, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 0], # 6 - [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1] # 7 + [0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1], # 7 ] # @@ -84,9 +84,14 @@ def main(): solver.Add(s >= 1) # number of used elements - solver.Add(tot_elements == solver.Sum([ - x[i] * belongs[i][j] for i in range(num_sets) for j in range(num_elements) - ])) + solver.Add( + tot_elements + == solver.Sum([ + x[i] * belongs[i][j] + for i in range(num_sets) + for j in range(num_elements) + ]) + ) # objective objective = solver.Minimize(z, 1) diff --git a/examples/contrib/set_partition.py b/examples/contrib/set_partition.py index 9227af09440..f5a72308ac9 100644 --- a/examples/contrib/set_partition.py +++ b/examples/contrib/set_partition.py @@ -13,36 +13,36 @@ # limitations under the License. """ - Set partition problem in Google CP Solver. +Set partition problem in Google CP Solver. - Problem formulation from - http://www.koalog.com/resources/samples/PartitionProblem.java.html - ''' - This is a partition problem. - Given the set S = {1, 2, ..., n}, - it consists in finding two sets A and B such that: +Problem formulation from +http://www.koalog.com/resources/samples/PartitionProblem.java.html +''' + This is a partition problem. + Given the set S = {1, 2, ..., n}, + it consists in finding two sets A and B such that: - A U B = S, - |A| = |B|, - sum(A) = sum(B), - sum_squares(A) = sum_squares(B) + A U B = S, + |A| = |B|, + sum(A) = sum(B), + sum_squares(A) = sum_squares(B) - ''' +''' - This model uses a binary matrix to represent the sets. +This model uses a binary matrix to represent the sets. - Also, compare with other models which uses var sets: - * MiniZinc: http://www.hakank.org/minizinc/set_partition.mzn - * Gecode/R: http://www.hakank.org/gecode_r/set_partition.rb - * Comet: http://hakank.org/comet/set_partition.co - * Gecode: http://hakank.org/gecode/set_partition.cpp - * ECLiPSe: http://hakank.org/eclipse/set_partition.ecl - * SICStus: http://hakank.org/sicstus/set_partition.pl +Also, compare with other models which uses var sets: +* MiniZinc: http://www.hakank.org/minizinc/set_partition.mzn +* Gecode/R: http://www.hakank.org/gecode_r/set_partition.rb +* Comet: http://hakank.org/comet/set_partition.co +* Gecode: http://hakank.org/gecode/set_partition.cpp +* ECLiPSe: http://hakank.org/eclipse/set_partition.ecl +* SICStus: http://hakank.org/sicstus/set_partition.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -106,18 +106,21 @@ def main(n=16, num_sets=2): # same cardinality solver.Add( - solver.Sum([a[i, k] for k in range(n)]) == solver.Sum( - [a[j, k] for k in range(n)])) + solver.Sum([a[i, k] for k in range(n)]) + == solver.Sum([a[j, k] for k in range(n)]) + ) # same sum solver.Add( - solver.Sum([k * a[i, k] for k in range(n)]) == solver.Sum( - [k * a[j, k] for k in range(n)])) + solver.Sum([k * a[i, k] for k in range(n)]) + == solver.Sum([k * a[j, k] for k in range(n)]) + ) # same sum squared solver.Add( - solver.Sum([(k * a[i, k]) * (k * a[i, k]) for k in range(n)]) == - solver.Sum([(k * a[j, k]) * (k * a[j, k]) for k in range(n)])) + solver.Sum([(k * a[i, k]) * (k * a[i, k]) for k in range(n)]) + == solver.Sum([(k * a[j, k]) * (k * a[j, k]) for k in range(n)]) + ) # symmetry breaking for num_sets == 2 if num_sets == 2: @@ -139,7 +142,7 @@ def main(n=16, num_sets=2): sq = sum([(j + 1) * a_val[0, j] for j in range(n)]) print("sums:", sq) - sq2 = sum([((j + 1) * a_val[0, j])**2 for j in range(n)]) + sq2 = sum([((j + 1) * a_val[0, j]) ** 2 for j in range(n)]) print("sums squared:", sq2) for i in range(num_sets): diff --git a/examples/contrib/sicherman_dice.py b/examples/contrib/sicherman_dice.py index 342e8e5b589..47b99cf5e6b 100644 --- a/examples/contrib/sicherman_dice.py +++ b/examples/contrib/sicherman_dice.py @@ -13,50 +13,50 @@ # limitations under the License. """ - Sicherman Dice in Google CP Solver. +Sicherman Dice in Google CP Solver. - From http://en.wikipedia.org/wiki/Sicherman_dice - "" - Sicherman dice are the only pair of 6-sided dice which are not normal dice, - bear only positive integers, and have the same probability distribution for - the sum as normal dice. +From http://en.wikipedia.org/wiki/Sicherman_dice +"" +Sicherman dice are the only pair of 6-sided dice which are not normal dice, +bear only positive integers, and have the same probability distribution for +the sum as normal dice. - The faces on the dice are numbered 1, 2, 2, 3, 3, 4 and 1, 3, 4, 5, 6, 8. - "" +The faces on the dice are numbered 1, 2, 2, 3, 3, 4 and 1, 3, 4, 5, 6, 8. +"" - I read about this problem in a book/column by Martin Gardner long - time ago, and got inspired to model it now by the WolframBlog post - "Sicherman Dice": http://blog.wolfram.com/2010/07/13/sicherman-dice/ +I read about this problem in a book/column by Martin Gardner long +time ago, and got inspired to model it now by the WolframBlog post +"Sicherman Dice": http://blog.wolfram.com/2010/07/13/sicherman-dice/ - This model gets the two different ways, first the standard way and - then the Sicherman dice: +This model gets the two different ways, first the standard way and +then the Sicherman dice: - x1 = [1, 2, 3, 4, 5, 6] - x2 = [1, 2, 3, 4, 5, 6] - ---------- - x1 = [1, 2, 2, 3, 3, 4] - x2 = [1, 3, 4, 5, 6, 8] +x1 = [1, 2, 3, 4, 5, 6] +x2 = [1, 2, 3, 4, 5, 6] +---------- +x1 = [1, 2, 2, 3, 3, 4] +x2 = [1, 3, 4, 5, 6, 8] - Extra: If we also allow 0 (zero) as a valid value then the - following two solutions are also valid: +Extra: If we also allow 0 (zero) as a valid value then the +following two solutions are also valid: - x1 = [0, 1, 1, 2, 2, 3] - x2 = [2, 4, 5, 6, 7, 9] - ---------- - x1 = [0, 1, 2, 3, 4, 5] - x2 = [2, 3, 4, 5, 6, 7] +x1 = [0, 1, 1, 2, 2, 3] +x2 = [2, 4, 5, 6, 7, 9] +---------- +x1 = [0, 1, 2, 3, 4, 5] +x2 = [2, 3, 4, 5, 6, 7] - These two extra cases are mentioned here: - http://mathworld.wolfram.com/SichermanDice.html +These two extra cases are mentioned here: +http://mathworld.wolfram.com/SichermanDice.html - Compare with these models: - * MiniZinc: http://hakank.org/minizinc/sicherman_dice.mzn - * Gecode: http://hakank.org/gecode/sicherman_dice.cpp +Compare with these models: +* MiniZinc: http://hakank.org/minizinc/sicherman_dice.mzn +* Gecode: http://hakank.org/gecode/sicherman_dice.cpp - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -122,8 +122,9 @@ def main(): solver.EndSearch() print() - print("num_solutions:", num_solutions, "solver.solutions:", - solver.Solutions()) + print( + "num_solutions:", num_solutions, "solver.solutions:", solver.Solutions() + ) print("failures:", solver.Failures()) print("branches:", solver.Branches()) print("WallTime:", solver.WallTime()) diff --git a/examples/contrib/ski_assignment.py b/examples/contrib/ski_assignment.py index 063c00df658..5955c1156f7 100644 --- a/examples/contrib/ski_assignment.py +++ b/examples/contrib/ski_assignment.py @@ -13,39 +13,39 @@ # limitations under the License. """ - Ski assignment in Google CP Solver. - - From Jeffrey Lee Hellrung, Jr.: - PIC 60, Fall 2008 Final Review, December 12, 2008 - http://www.math.ucla.edu/~jhellrun/course_files/Fall%25202008/PIC%252060%2520-%2520Data%2520Structures%2520and%2520Algorithms/final_review.pdf - ''' - 5. Ski Optimization! Your job at Snapple is pleasant but in the winter - you've decided to become a ski bum. You've hooked up with the Mount - Baldy Ski Resort. They'll let you ski all winter for free in exchange - for helping their ski rental shop with an algorithm to assign skis to - skiers. Ideally, each skier should obtain a pair of skis whose height - matches his or her own height exactly. Unfortunately, this is generally - not possible. We define the disparity between a skier and his or her - skis to be the absolute value of the difference between the height of - the skier and the pair of skis. Our objective is to find an assignment - of skis to skiers that minimizes the sum of the disparities. - ... - Illustrate your algorithm by explicitly filling out the A[i, j] table - for the following sample data: - * Ski heights: 1, 2, 5, 7, 13, 21. - * Skier heights: 3, 4, 7, 11, 18. - ''' - - Compare with the following models: - * Comet : http://www.hakank.org/comet/ski_assignment.co - * MiniZinc: http://hakank.org/minizinc/ski_assignment.mzn - * ECLiPSe : http://www.hakank.org/eclipse/ski_assignment.ecl - * SICStus: http://hakank.org/sicstus/ski_assignment.pl - * Gecode: http://hakank.org/gecode/ski_assignment.cpp - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Ski assignment in Google CP Solver. + +From Jeffrey Lee Hellrung, Jr.: +PIC 60, Fall 2008 Final Review, December 12, 2008 +http://www.math.ucla.edu/~jhellrun/course_files/Fall%25202008/PIC%252060%2520-%2520Data%2520Structures%2520and%2520Algorithms/final_review.pdf +''' +5. Ski Optimization! Your job at Snapple is pleasant but in the winter +you've decided to become a ski bum. You've hooked up with the Mount +Baldy Ski Resort. They'll let you ski all winter for free in exchange +for helping their ski rental shop with an algorithm to assign skis to +skiers. Ideally, each skier should obtain a pair of skis whose height +matches his or her own height exactly. Unfortunately, this is generally +not possible. We define the disparity between a skier and his or her +skis to be the absolute value of the difference between the height of +the skier and the pair of skis. Our objective is to find an assignment +of skis to skiers that minimizes the sum of the disparities. +... +Illustrate your algorithm by explicitly filling out the A[i, j] table +for the following sample data: + * Ski heights: 1, 2, 5, 7, 13, 21. + * Skier heights: 3, 4, 7, 11, 18. +''' + +Compare with the following models: +* Comet : http://www.hakank.org/comet/ski_assignment.co +* MiniZinc: http://hakank.org/minizinc/ski_assignment.mzn +* ECLiPSe : http://www.hakank.org/eclipse/ski_assignment.ecl +* SICStus: http://hakank.org/sicstus/ski_assignment.pl +* Gecode: http://hakank.org/gecode/ski_assignment.cpp + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -102,8 +102,10 @@ def main(): x_val = x[i].Value() ski_height = ski_heights[x[i].Value()] diff = ski_height - skier_heights[i] - print('Skier %i: Ski %i with length %2i (diff: %2i)' %\ - (i, x_val, ski_height, diff)) + print( + 'Skier %i: Ski %i with length %2i (diff: %2i)' + % (i, x_val, ski_height, diff) + ) print() solver.EndSearch() diff --git a/examples/contrib/slitherlink.py b/examples/contrib/slitherlink.py index af17f471e2e..64486349de9 100644 --- a/examples/contrib/slitherlink.py +++ b/examples/contrib/slitherlink.py @@ -3,17 +3,29 @@ small = [[3, 2, -1, 3], [-1, -1, -1, 2], [3, -1, -1, -1], [3, -1, 3, 1]] -medium = [[-1, 0, -1, 1, -1, -1, 1, -1], [-1, 3, -1, -1, 2, 3, -1, 2], - [-1, -1, 0, -1, -1, -1, -1, 0], [-1, 3, -1, -1, 0, -1, -1, -1], - [-1, -1, -1, 3, -1, -1, 0, -1], [1, -1, -1, -1, -1, 3, -1, -1], - [3, -1, 1, 3, -1, -1, 3, -1], [-1, 0, -1, -1, 3, -1, 3, -1]] - -big = [[3, -1, -1, -1, 2, -1, 1, -1, 1, 2], [1, -1, 0, -1, 3, -1, 2, 0, -1, -1], - [-1, 3, -1, -1, -1, -1, -1, -1, 3, -1], - [2, 0, -1, 3, -1, 2, 3, -1, -1, -1], [-1, -1, -1, 1, 1, 1, -1, -1, 3, 3], - [2, 3, -1, -1, 2, 2, 3, -1, -1, -1], [-1, -1, -1, 1, 2, -1, 2, -1, 3, 3], - [-1, 2, -1, -1, -1, -1, -1, -1, 2, -1], - [-1, -1, 1, 1, -1, 2, -1, 1, -1, 3], [3, 3, -1, 1, -1, 2, -1, -1, -1, 2]] +medium = [ + [-1, 0, -1, 1, -1, -1, 1, -1], + [-1, 3, -1, -1, 2, 3, -1, 2], + [-1, -1, 0, -1, -1, -1, -1, 0], + [-1, 3, -1, -1, 0, -1, -1, -1], + [-1, -1, -1, 3, -1, -1, 0, -1], + [1, -1, -1, -1, -1, 3, -1, -1], + [3, -1, 1, 3, -1, -1, 3, -1], + [-1, 0, -1, -1, 3, -1, 3, -1], +] + +big = [ + [3, -1, -1, -1, 2, -1, 1, -1, 1, 2], + [1, -1, 0, -1, 3, -1, 2, 0, -1, -1], + [-1, 3, -1, -1, -1, -1, -1, -1, 3, -1], + [2, 0, -1, 3, -1, 2, 3, -1, -1, -1], + [-1, -1, -1, 1, 1, 1, -1, -1, 3, 3], + [2, 3, -1, -1, 2, 2, 3, -1, -1, -1], + [-1, -1, -1, 1, 2, -1, 2, -1, 3, 3], + [-1, 2, -1, -1, -1, -1, -1, -1, 2, -1], + [-1, -1, 1, 1, -1, 2, -1, 1, -1, 3], + [3, 3, -1, 1, -1, 2, -1, -1, -1, 2], +] def NeighboringArcs(i, j, h_arcs, v_arcs): @@ -216,13 +228,15 @@ def SlitherLink(data): num_columns = len(data[0]) solver = pywrapcp.Solver('slitherlink') - h_arcs = [[ - solver.BoolVar('h_arcs[%i][%i]' % (i, j)) for j in range(num_columns) - ] for i in range(num_rows + 1)] + h_arcs = [ + [solver.BoolVar('h_arcs[%i][%i]' % (i, j)) for j in range(num_columns)] + for i in range(num_rows + 1) + ] - v_arcs = [[ - solver.BoolVar('v_arcs[%i][%i]' % (i, j)) for j in range(num_rows) - ] for i in range(num_columns + 1)] + v_arcs = [ + [solver.BoolVar('v_arcs[%i][%i]' % (i, j)) for j in range(num_rows)] + for i in range(num_columns + 1) + ] # Constraint on the sum or arcs for i in range(num_rows): @@ -271,8 +285,9 @@ def SlitherLink(data): for column in v_arcs: all_vars.extend(column) - db = solver.Phase(all_vars, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MAX_VALUE) + db = solver.Phase( + all_vars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MAX_VALUE + ) log = solver.SearchLog(1000000) diff --git a/examples/contrib/sports_schedule_sat.py b/examples/contrib/sports_schedule_sat.py index 70624a51b77..525fe23989b 100644 --- a/examples/contrib/sports_schedule_sat.py +++ b/examples/contrib/sports_schedule_sat.py @@ -59,510 +59,561 @@ def csv_dump_results(solver, fixtures, num_teams, num_matchdays, csv_basename): - matchdays = range(num_matchdays) - teams = range(num_teams) - - vcsv = [] - for d in matchdays: - game = 0 - for home in range(num_teams): - for away in range(num_teams): - if solver.Value(fixtures[d][home][away]): - game += 1 - # each row: day,game,home,away - row = { - 'day': d + 1, - 'game': game, - 'home': home + 1, - 'away': away + 1 - } - vcsv.append(row) - - # check for any existing file - idx = 1 - checkname = csv_basename - match = re.search(r"\.csv", checkname) - if not match: - print( - 'looking for a .csv ending in passed in CSV file name. Did not find it, so appending .csv to', - csv_basename) - csv_basename += ".csv" - - checkname = csv_basename - while os.path.exists(checkname): - checkname = re.sub(r"\.csv", "_{}.csv".format(idx), csv_basename) - idx += 1 - # or just get rid of it, but that is often undesireable - # os.unlink(csv_basename) - - with open(checkname, 'w', newline='') as csvfile: - fieldnames = ['day', 'game', 'home', 'away'] - writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - - writer.writeheader() - for row in vcsv: - writer.writerow(row) - - -def screen_dump_results(solver, fixtures, num_teams, num_matchdays): - matchdays = range(num_matchdays) - teams = range(num_teams) - - total_games = 0 - for d in matchdays: - game = 0 - for home in teams: - for away in teams: - match_on = solver.Value(fixtures[d][home][away]) - if match_on: - game += 1 - print('day %i game %i home %i away %i' % - (d + 1, game, home + 1, away + 1)) - total_games += game - - -def assign_matches(num_teams, - num_matchdays, - num_matches_per_day, - max_home_stand, - time_limit=None, - num_cpus=None, - csv=None, - debug=None): - """Assign matches between teams in a league. - - Keyword arguments: - num_teams -- the number of teams - num_matchdays -- the number of match days to play. Should be greater than one day. Note that if num_matchdays is exactly some multipe (`n`) of `num_teams - 1` then each team with play every other team exactly `n` times. If the number of match days is less than or greater than a perfect multiple, then some teams will not play each other `n` times. - num_matches_per_day -- how many matches can be played in a day. The assumption is one match per day, and really this code was not tested with different values. - max_home_stand -- how many home games are allowed to be in a row. - time_limit -- the time in minutes to allow the solver to work on the problem. - num_cpus -- the number of processors to use for the solution - csv -- a file name to save the output to a CSV file - debug -- boolean value stating whether to ask the solver to show its progress or not - - """ - - model = cp_model.CpModel() - - print('num_teams', num_teams, 'num_matchdays', num_matchdays, - 'num_matches_per_day', num_matches_per_day, 'max_home_stand', - max_home_stand) - - matchdays = range(num_matchdays) - matches = range(num_matches_per_day) - teams = range(num_teams) - # how many possible unique games? - unique_games = (num_teams) * (num_teams - 1) / 2 - - # how many games are possible to play - total_games = num_matchdays * num_matches_per_day - - # maximum possible games versus an opponent. example, if 20 - # possible total games, and 28 unique combinations, then 20 // 28 - # +1 = 1. If 90 total games (say 5 per day for 18 match days) and - # 10 teams for 45 possible unique combinations of teams, then 90 - # // 45 + 1 = 3. Hmm. Should be 2 - matchups = int((total_games // unique_games) + 1) - # print(matchups) - # there is a special case, if total games / unique games == total - # games // unique games, then the constraint can be ==, not <= - matchups_exact = False - if (total_games % unique_games == 0): - matchups_exact = True - matchups = int(total_games // unique_games) - - print('expected matchups per pair', matchups, 'exact?', matchups_exact) - - days_to_play = int(unique_games // num_matches_per_day) - print('unique_games', unique_games, '\nnum matches per day', - num_matches_per_day, '\ndays to play', days_to_play, - '\ntotal games possible', total_games) - - fixtures = [ - ] # all possible games, list of lists of lists: fixture[day][iteam][jteam] - at_home = [ - ] # whether or not a team plays at home on matchday, list of lists - - # Does team i receive team j at home on day d? - for d in matchdays: - # hackity hack, append a new list for all possible fixtures for a team on day d - fixtures.append([]) - for i in teams: - # hackity hack, append a list of possible fixtures for team i - fixtures[d].append([]) - for j in teams: - # for each possible away opponent for home team i, add a fixture - # - # note that the fixture is not only that team i plays - # team j, but also that team i is the home team and - # team j is the away team. - fixtures[d][i].append( - model.NewBoolVar( - 'fixture: home team %i, opponent %i, matchday %i' % - (i, j, d))) - if i == j: - # It is not possible for team i to play itself, - # but it is cleaner to add the fixture than it is - # to skip it---the list stays the length of the - # number of teams. The C++ version adds a "FalseVar" instead - model.Add(fixtures[d][i][j] == 0) # forbid playing self - - # Is team t at home on day d? - for d in matchdays: - # hackity hack, append a new list for whether or not a team is at home on day d - at_home.append([]) - for i in teams: - # is team i playing at home on day d? - at_home[d].append( - model.NewBoolVar('team %i is home on matchday %i' % (i, d))) - - # each day, team t plays either home or away, but only once - for d in matchdays: - for t in teams: - # for each team, each day, list possible opponents - possible_opponents = [] - for opponent in teams: - if t == opponent: - continue - # t is home possibility - possible_opponents.append(fixtures[d][t][opponent]) - # t is away possibility - possible_opponents.append(fixtures[d][opponent][t]) - model.Add( - sum(possible_opponents) == 1) # can only play one game per day - - # "Each fixture happens once per season" is not a valid constraint - # in this formulation. in the C++ program, there are exactly a - # certain number of games such that every team plays every other - # team once at home, and once away. In this case, this is not the - # case, because there are a variable number of games. Instead, - # the constraint is that each fixture happens Matchups/2 times per - # season, where Matchups is the number of times each team plays every - # other team. - fixture_repeats = int(math.ceil(matchups / 2)) - print('fixture repeats expected is', fixture_repeats) - - for t in teams: - for opponent in teams: - if t == opponent: - continue - possible_days = [] - for d in matchdays: - possible_days.append(fixtures[d][t][opponent]) - if matchups % 2 == 0 and matchups_exact: - model.Add(sum(possible_days) == fixture_repeats) - else: - # not a hard constraint, because not exactly the right - # number of matches to be played - model.Add(sum(possible_days) <= fixture_repeats) - - # Next, each matchup between teams happens at most "matchups" - # times per season. Again this is different that C++ version, in - # which the number of games is such that each team plays every - # other team exactly two times. Here this is not the case, - # because the number of games in the season is not fixed. - # - # in C++ version, the season is split into two halves. In this - # case, splitting the season into "Matchups" sections, with each - # team playing every other team once per section. - # - # The very last section of games is special, as there might not be - # enough games for every team to play every other team once. - # - for t in teams: - for opponent in teams: - if t == opponent: - continue - prior_home = [] - for m in range(matchups): - current_home = [] - pairings = [] - # if m = matchups - 1, then last time through - days = int(days_to_play) - if m == matchups - 1: - days = int( - min(days_to_play, num_matchdays - m * days_to_play)) - # print('days',days) - for d in range(days): - theday = int(d + m * days_to_play) - # print('theday',theday) - pairings.append(fixtures[theday][t][opponent]) - pairings.append(fixtures[theday][opponent][t]) - # current_home.append(fixtures[theday][t][opponent]) - if m == matchups - 1 and not matchups_exact: - # in the last group of games, if the number of - # games left to play isn't quite right, then it - # will not be possible for each team to play every - # other team, and so the sum will be <= 1, rather - # than == 1 - # - # print('last matchup',m,'relaxed pairings constraint') - model.Add(sum(pairings) <= 1) - else: - # if it is not the last group of games, then every - # team must play every other team exactly once. - # - # print('matchup',m,'hard pairings constraint') - model.Add(sum(pairings) == 1) - - # maintain consistency between fixtures and at_home[day][team] - for d in matchdays: - for t in teams: - for opponent in teams: - if t == opponent: - continue - # if the [t][opp] fixture is true, then at home is true for t - model.AddImplication(fixtures[d][t][opponent], at_home[d][t]) - # if the [t][opp] fixture is true, then at home false for opponent - model.AddImplication(fixtures[d][t][opponent], - at_home[d][opponent].Not()) - - # balance home and away games via the following "breaks" logic - # forbid sequence of "max_home_stand" home games or away games in a row - # In sports like baseball, homestands can be quite long. - for t in teams: - for d in range(num_matchdays - max_home_stand): - model.AddBoolOr([ - at_home[d + offset][t] for offset in range(max_home_stand + 1) - ]) - model.AddBoolOr([ - at_home[d + offset][t].Not() - for offset in range(max_home_stand + 1) - ]) - # note, this works because AddBoolOr means at least one - # element must be true. if it was just AddBoolOr([home0, - # home1, ..., homeN]), then that would mean that one or - # all of these could be true, and you could have an - # infinite sequence of home games. However, that home - # constraint is matched with an away constraint. So the - # combination says: - # - # AddBoolOr([home0, ... homeN]) at least one of these is true - # AddBoolOr([away0, ... awayN]) at least one of these is true - # - # taken together, at least one home from 0 to N is true, - # which means at least one away0 to awayN is false. At - # the same time, at least one away is true, which means - # that the corresponding home is false. So together, this - # prevents a sequence of one more than max_home_stand to - # take place. - - # objective using breaks concept - breaks = [] - for t in teams: - for d in range(num_matchdays - 1): - breaks.append( - model.NewBoolVar( - 'two home or two away for team %i, starting on matchday %i' - % (t, d))) - - model.AddBoolOr([at_home[d][t], at_home[d + 1][t], breaks[-1]]) - model.AddBoolOr( - [at_home[d][t].Not(), at_home[d + 1][t].Not(), breaks[-1]]) - - model.AddBoolOr( - [at_home[d][t].Not(), at_home[d + 1][t], breaks[-1].Not()]) - model.AddBoolOr( - [at_home[d][t], at_home[d + 1][t].Not(), breaks[-1].Not()]) - - # I couldn't figure this out, so I wrote a little program - # and proved it. These effectively are identical to - # - # model.Add(at_home[d][t] == at_home[d+1][t]).OnlyEnforceIf(breaks[-1]) - # model.Add(at_home[d][t] != at_home[d+1][t]).OnlyEnforceIf(breaks[-1].Not()) - # - # except they are a little more efficient, I believe. Wrote it up in a blog post - # - # my write-up is at https://activimetrics.com/blog/ortools/cp_sat/addboolor/ - - # constrain breaks - # - # Another deviation from the C++ code. In the C++, the breaks sum is - # required to be >= (2 * num_teams - 4), which is exactly the number - # of match days. - # - # I said on the mailing list that I didn't know why this was, and - # Laurent pointed out that there was a known bound. I looked it - # up and found some references (see thread at - # https://groups.google.com/d/msg/or-tools-discuss/ITdlPs6oRaY/FvwgB5LgAQAJ) - # - # The reference states that "schedules with n teams with even n - # have at least n − 2 breaks", and further that "for odd n - # schedules without any breaks are constructed." - # - # That research doesn't *quite* apply here, as the authors were - # assuming a single round-robin tournament - #. - # Here there is not an exact round-robin tournament multiple, but - # still the implication is that the number of breaks cannot be - # less than the number of matchdays. - # - # Literature aside, I'm finding in practice that if you don't have - # an exact round robin multiple, and if num_matchdays is odd, the - # best you can do is num_matchdays + 1. If you have even days, - # then you can do num_matchdays. This can be tested for small - # numbers of teams and days, in which the solver is able to search - # all possible combinations in a reasonable time limit. - - optimal_value = matchups * (num_teams - 2) - if not matchups_exact: - # fiddle a bit, based on experiments with low values of N and D - if num_matchdays % 2: - # odd number of days - optimal_value = min(num_matchdays + 1, optimal_value) - else: - optimal_value = min(num_matchdays, optimal_value) - - print('expected optimal value is', optimal_value) - model.Add(sum(breaks) >= optimal_value) - - model.Minimize(sum(breaks)) - # run the solver - solver = cp_model.CpSolver() - solver.parameters.max_time_in_seconds = time_limit - solver.parameters.log_search_progress = debug - solver.parameters.num_search_workers = num_cpus - - # solution_printer = SolutionPrinter() # since we stop at first - # solution, this isn't really - # necessary I think - status = solver.Solve(model) - print('Solve status: %s' % solver.StatusName(status)) - print('Statistics') - print(' - conflicts : %i' % solver.NumConflicts()) - print(' - branches : %i' % solver.NumBranches()) - print(' - wall time : %f s' % solver.WallTime()) + matchdays = range(num_matchdays) + teams = range(num_teams) + + vcsv = [] + for d in matchdays: + game = 0 + for home in range(num_teams): + for away in range(num_teams): + if solver.Value(fixtures[d][home][away]): + game += 1 + # each row: day,game,home,away + row = {'day': d + 1, 'game': game, 'home': home + 1, 'away': away + 1} + vcsv.append(row) + + # check for any existing file + idx = 1 + checkname = csv_basename + match = re.search(r'\.csv', checkname) + if not match: + print( + 'looking for a .csv ending in passed in CSV file name. Did not find' + ' it, so appending .csv to', + csv_basename, + ) + csv_basename += '.csv' - if status == cp_model.INFEASIBLE: - return status + checkname = csv_basename + while os.path.exists(checkname): + checkname = re.sub(r'\.csv', '_{}.csv'.format(idx), csv_basename) + idx += 1 + # or just get rid of it, but that is often undesireable + # os.unlink(csv_basename) - if status == cp_model.UNKNOWN: - print('Not enough time allowed to compute a solution') - print('Add more time using the --timelimit command line option') - return status + with open(checkname, 'w', newline='') as csvfile: + fieldnames = ['day', 'game', 'home', 'away'] + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) - print('Optimal objective value: %i' % solver.ObjectiveValue()) + writer.writeheader() + for row in vcsv: + writer.writerow(row) - screen_dump_results(solver, fixtures, num_teams, num_matchdays) - if status != cp_model.OPTIMAL and solver.WallTime() >= time_limit: - print('Please note that solver reached maximum time allowed %i.' % - time_limit) - print( - 'A better solution than %i might be found by adding more time using the --timelimit command line option' - % solver.ObjectiveValue()) +def screen_dump_results(solver, fixtures, num_teams, num_matchdays): + matchdays = range(num_matchdays) + teams = range(num_teams) + + total_games = 0 + for d in matchdays: + game = 0 + for home in teams: + for away in teams: + match_on = solver.Value(fixtures[d][home][away]) + if match_on: + game += 1 + print( + 'day %i game %i home %i away %i' + % (d + 1, game, home + 1, away + 1) + ) + total_games += game + + +def assign_matches( + num_teams, + num_matchdays, + num_matches_per_day, + max_home_stand, + time_limit=None, + num_cpus=None, + csv=None, + debug=None, +): + """Assign matches between teams in a league. + + Keyword arguments: + num_teams -- the number of teams + num_matchdays -- the number of match days to play. Should be greater than one day. Note that if num_matchdays is exactly some multipe (`n`) of `num_teams - 1` then each team with play every other team exactly `n` times. If the number of match days is less than or greater than a perfect multiple, then some teams will not play each other `n` times. + num_matches_per_day -- how many matches can be played in a day. The assumption is one match per day, and really this code was not tested with different values. + max_home_stand -- how many home games are allowed to be in a row. + time_limit -- the time in minutes to allow the solver to work on the problem. + num_cpus -- the number of processors to use for the solution + csv -- a file name to save the output to a CSV file + debug -- boolean value stating whether to ask the solver to show its progress or not + + """ + + model = cp_model.CpModel() + + print( + 'num_teams', + num_teams, + 'num_matchdays', + num_matchdays, + 'num_matches_per_day', + num_matches_per_day, + 'max_home_stand', + max_home_stand, + ) + + matchdays = range(num_matchdays) + matches = range(num_matches_per_day) + teams = range(num_teams) + # how many possible unique games? + unique_games = (num_teams) * (num_teams - 1) / 2 + + # how many games are possible to play + total_games = num_matchdays * num_matches_per_day + + # maximum possible games versus an opponent. example, if 20 + # possible total games, and 28 unique combinations, then 20 // 28 + # +1 = 1. If 90 total games (say 5 per day for 18 match days) and + # 10 teams for 45 possible unique combinations of teams, then 90 + # // 45 + 1 = 3. Hmm. Should be 2 + matchups = int((total_games // unique_games) + 1) + # print(matchups) + # there is a special case, if total games / unique games == total + # games // unique games, then the constraint can be ==, not <= + matchups_exact = False + if total_games % unique_games == 0: + matchups_exact = True + matchups = int(total_games // unique_games) + + print('expected matchups per pair', matchups, 'exact?', matchups_exact) + + days_to_play = int(unique_games // num_matches_per_day) + print( + 'unique_games', + unique_games, + '\nnum matches per day', + num_matches_per_day, + '\ndays to play', + days_to_play, + '\ntotal games possible', + total_games, + ) + + fixtures = ( + [] + ) # all possible games, list of lists of lists: fixture[day][iteam][jteam] + at_home = [] # whether or not a team plays at home on matchday, list of lists + + # Does team i receive team j at home on day d? + for d in matchdays: + # hackity hack, append a new list for all possible fixtures for a team on day d + fixtures.append([]) + for i in teams: + # hackity hack, append a list of possible fixtures for team i + fixtures[d].append([]) + for j in teams: + # for each possible away opponent for home team i, add a fixture + # + # note that the fixture is not only that team i plays + # team j, but also that team i is the home team and + # team j is the away team. + fixtures[d][i].append( + model.NewBoolVar( + 'fixture: home team %i, opponent %i, matchday %i' % (i, j, d) + ) + ) + if i == j: + # It is not possible for team i to play itself, + # but it is cleaner to add the fixture than it is + # to skip it---the list stays the length of the + # number of teams. The C++ version adds a "FalseVar" instead + model.Add(fixtures[d][i][j] == 0) # forbid playing self + + # Is team t at home on day d? + for d in matchdays: + # hackity hack, append a new list for whether or not a team is at home on day d + at_home.append([]) + for i in teams: + # is team i playing at home on day d? + at_home[d].append( + model.NewBoolVar('team %i is home on matchday %i' % (i, d)) + ) + + # each day, team t plays either home or away, but only once + for d in matchdays: + for t in teams: + # for each team, each day, list possible opponents + possible_opponents = [] + for opponent in teams: + if t == opponent: + continue + # t is home possibility + possible_opponents.append(fixtures[d][t][opponent]) + # t is away possibility + possible_opponents.append(fixtures[d][opponent][t]) + model.Add(sum(possible_opponents) == 1) # can only play one game per day + + # "Each fixture happens once per season" is not a valid constraint + # in this formulation. in the C++ program, there are exactly a + # certain number of games such that every team plays every other + # team once at home, and once away. In this case, this is not the + # case, because there are a variable number of games. Instead, + # the constraint is that each fixture happens Matchups/2 times per + # season, where Matchups is the number of times each team plays every + # other team. + fixture_repeats = int(math.ceil(matchups / 2)) + print('fixture repeats expected is', fixture_repeats) + + for t in teams: + for opponent in teams: + if t == opponent: + continue + possible_days = [] + for d in matchdays: + possible_days.append(fixtures[d][t][opponent]) + if matchups % 2 == 0 and matchups_exact: + model.Add(sum(possible_days) == fixture_repeats) + else: + # not a hard constraint, because not exactly the right + # number of matches to be played + model.Add(sum(possible_days) <= fixture_repeats) + + # Next, each matchup between teams happens at most "matchups" + # times per season. Again this is different that C++ version, in + # which the number of games is such that each team plays every + # other team exactly two times. Here this is not the case, + # because the number of games in the season is not fixed. + # + # in C++ version, the season is split into two halves. In this + # case, splitting the season into "Matchups" sections, with each + # team playing every other team once per section. + # + # The very last section of games is special, as there might not be + # enough games for every team to play every other team once. + # + for t in teams: + for opponent in teams: + if t == opponent: + continue + prior_home = [] + for m in range(matchups): + current_home = [] + pairings = [] + # if m = matchups - 1, then last time through + days = int(days_to_play) + if m == matchups - 1: + days = int(min(days_to_play, num_matchdays - m * days_to_play)) + # print('days',days) + for d in range(days): + theday = int(d + m * days_to_play) + # print('theday',theday) + pairings.append(fixtures[theday][t][opponent]) + pairings.append(fixtures[theday][opponent][t]) + # current_home.append(fixtures[theday][t][opponent]) + if m == matchups - 1 and not matchups_exact: + # in the last group of games, if the number of + # games left to play isn't quite right, then it + # will not be possible for each team to play every + # other team, and so the sum will be <= 1, rather + # than == 1 + # + # print('last matchup',m,'relaxed pairings constraint') + model.Add(sum(pairings) <= 1) + else: + # if it is not the last group of games, then every + # team must play every other team exactly once. + # + # print('matchup',m,'hard pairings constraint') + model.Add(sum(pairings) == 1) + + # maintain consistency between fixtures and at_home[day][team] + for d in matchdays: + for t in teams: + for opponent in teams: + if t == opponent: + continue + # if the [t][opp] fixture is true, then at home is true for t + model.AddImplication(fixtures[d][t][opponent], at_home[d][t]) + # if the [t][opp] fixture is true, then at home false for opponent + model.AddImplication( + fixtures[d][t][opponent], at_home[d][opponent].Not() + ) + + # balance home and away games via the following "breaks" logic + # forbid sequence of "max_home_stand" home games or away games in a row + # In sports like baseball, homestands can be quite long. + for t in teams: + for d in range(num_matchdays - max_home_stand): + model.AddBoolOr( + [at_home[d + offset][t] for offset in range(max_home_stand + 1)] + ) + model.AddBoolOr( + [at_home[d + offset][t].Not() for offset in range(max_home_stand + 1)] + ) + # note, this works because AddBoolOr means at least one + # element must be true. if it was just AddBoolOr([home0, + # home1, ..., homeN]), then that would mean that one or + # all of these could be true, and you could have an + # infinite sequence of home games. However, that home + # constraint is matched with an away constraint. So the + # combination says: + # + # AddBoolOr([home0, ... homeN]) at least one of these is true + # AddBoolOr([away0, ... awayN]) at least one of these is true + # + # taken together, at least one home from 0 to N is true, + # which means at least one away0 to awayN is false. At + # the same time, at least one away is true, which means + # that the corresponding home is false. So together, this + # prevents a sequence of one more than max_home_stand to + # take place. + + # objective using breaks concept + breaks = [] + for t in teams: + for d in range(num_matchdays - 1): + breaks.append( + model.NewBoolVar( + 'two home or two away for team %i, starting on matchday %i' + % (t, d) + ) + ) + + model.AddBoolOr([at_home[d][t], at_home[d + 1][t], breaks[-1]]) + model.AddBoolOr( + [at_home[d][t].Not(), at_home[d + 1][t].Not(), breaks[-1]] + ) + + model.AddBoolOr( + [at_home[d][t].Not(), at_home[d + 1][t], breaks[-1].Not()] + ) + model.AddBoolOr( + [at_home[d][t], at_home[d + 1][t].Not(), breaks[-1].Not()] + ) + + # I couldn't figure this out, so I wrote a little program + # and proved it. These effectively are identical to + # + # model.Add(at_home[d][t] == at_home[d+1][t]).OnlyEnforceIf(breaks[-1]) + # model.Add(at_home[d][t] != at_home[d+1][t]).OnlyEnforceIf(breaks[-1].Not()) + # + # except they are a little more efficient, I believe. Wrote it up in a blog post + # + # my write-up is at https://activimetrics.com/blog/ortools/cp_sat/addboolor/ + + # constrain breaks + # + # Another deviation from the C++ code. In the C++, the breaks sum is + # required to be >= (2 * num_teams - 4), which is exactly the number + # of match days. + # + # I said on the mailing list that I didn't know why this was, and + # Laurent pointed out that there was a known bound. I looked it + # up and found some references (see thread at + # https://groups.google.com/d/msg/or-tools-discuss/ITdlPs6oRaY/FvwgB5LgAQAJ) + # + # The reference states that "schedules with n teams with even n + # have at least n − 2 breaks", and further that "for odd n + # schedules without any breaks are constructed." + # + # That research doesn't *quite* apply here, as the authors were + # assuming a single round-robin tournament + # . + # Here there is not an exact round-robin tournament multiple, but + # still the implication is that the number of breaks cannot be + # less than the number of matchdays. + # + # Literature aside, I'm finding in practice that if you don't have + # an exact round robin multiple, and if num_matchdays is odd, the + # best you can do is num_matchdays + 1. If you have even days, + # then you can do num_matchdays. This can be tested for small + # numbers of teams and days, in which the solver is able to search + # all possible combinations in a reasonable time limit. + + optimal_value = matchups * (num_teams - 2) + if not matchups_exact: + # fiddle a bit, based on experiments with low values of N and D + if num_matchdays % 2: + # odd number of days + optimal_value = min(num_matchdays + 1, optimal_value) + else: + optimal_value = min(num_matchdays, optimal_value) + + print('expected optimal value is', optimal_value) + model.Add(sum(breaks) >= optimal_value) + + model.Minimize(sum(breaks)) + # run the solver + solver = cp_model.CpSolver() + solver.parameters.max_time_in_seconds = time_limit + solver.parameters.log_search_progress = debug + solver.parameters.num_search_workers = num_cpus + + # solution_printer = SolutionPrinter() # since we stop at first + # solution, this isn't really + # necessary I think + status = solver.Solve(model) + print('Solve status: %s' % solver.StatusName(status)) + print('Statistics') + print(' - conflicts : %i' % solver.NumConflicts()) + print(' - branches : %i' % solver.NumBranches()) + print(' - wall time : %f s' % solver.WallTime()) + + if status == cp_model.INFEASIBLE: + return status + + if status == cp_model.UNKNOWN: + print('Not enough time allowed to compute a solution') + print('Add more time using the --timelimit command line option') + return status + + print('Optimal objective value: %i' % solver.ObjectiveValue()) + + screen_dump_results(solver, fixtures, num_teams, num_matchdays) + + if status != cp_model.OPTIMAL and solver.WallTime() >= time_limit: + print( + 'Please note that solver reached maximum time allowed %i.' % time_limit + ) + print( + 'A better solution than %i might be found by adding more time using the' + ' --timelimit command line option' + % solver.ObjectiveValue() + ) - if csv: - csv_dump_results(solver, fixtures, num_teams, num_matchdays, csv) + if csv: + csv_dump_results(solver, fixtures, num_teams, num_matchdays, csv) - # # print break results, to get a clue what they are doing - # print('Breaks') - # for b in breaks: - # print(' %s is %i' % (b.Name(), solver.Value(b))) + # # print break results, to get a clue what they are doing + # print('Breaks') + # for b in breaks: + # print(' %s is %i' % (b.Name(), solver.Value(b))) def main(): - """Entry point of the program.""" - parser = argparse.ArgumentParser( - description='Solve sports league match play assignment problem') - parser.add_argument('-t,--teams', - type=int, - dest='num_teams', - default=10, - help='Number of teams in the league') - - parser.add_argument( - '-d,--days', - type=int, - dest='num_matchdays', - default=2 * 10 - 2, - help= - 'Number of days on which matches are played. Default is enough days such that every team can play every other team, or (number of teams - 1)' - ) - - parser.add_argument( - '--matches_per_day', - type=int, - dest='num_matches_per_day', - default=10 - 1, - help= - 'Number of matches played per day. Default is number of teams divided by 2. If greater than the number of teams, then this implies some teams will play each other more than once. In that case, home and away should alternate between the teams in repeated matchups.' - ) - - parser.add_argument( - '--csv', - type=str, - dest='csv', - default='output.csv', - help='A file to dump the team assignments. Default is output.csv') - - parser.add_argument( - '--timelimit', - type=int, - dest='time_limit', - default=60, - help='Maximum run time for solver, in seconds. Default is 60 seconds.') - - parser.add_argument( - '--cpu', - type=int, - dest='cpu', - help= - 'Number of workers (CPUs) to use for solver. Default is 6 or number of CPUs available, whichever is lower' - ) - - parser.add_argument('--debug', - action='store_true', - help="Turn on some print statements.") - - parser.add_argument( - '--max_home_stand', - type=int, - dest='max_home_stand', - default=2, - help= - "Maximum consecutive home or away games. Default to 2, which means three home or away games in a row is forbidden." + """Entry point of the program.""" + parser = argparse.ArgumentParser( + description='Solve sports league match play assignment problem' + ) + parser.add_argument( + '-t,--teams', + type=int, + dest='num_teams', + default=10, + help='Number of teams in the league', + ) + + parser.add_argument( + '-d,--days', + type=int, + dest='num_matchdays', + default=2 * 10 - 2, + help=( + 'Number of days on which matches are played. Default is enough days' + ' such that every team can play every other team, or (number of teams' + ' - 1)' + ), + ) + + parser.add_argument( + '--matches_per_day', + type=int, + dest='num_matches_per_day', + default=10 - 1, + help=( + 'Number of matches played per day. Default is number of teams' + ' divided by 2. If greater than the number of teams, then this' + ' implies some teams will play each other more than once. In that' + ' case, home and away should alternate between the teams in repeated' + ' matchups.' + ), + ) + + parser.add_argument( + '--csv', + type=str, + dest='csv', + default='output.csv', + help='A file to dump the team assignments. Default is output.csv', + ) + + parser.add_argument( + '--timelimit', + type=int, + dest='time_limit', + default=60, + help='Maximum run time for solver, in seconds. Default is 60 seconds.', + ) + + parser.add_argument( + '--cpu', + type=int, + dest='cpu', + help=( + 'Number of workers (CPUs) to use for solver. Default is 6 or number' + ' of CPUs available, whichever is lower' + ), + ) + + parser.add_argument( + '--debug', action='store_true', help='Turn on some print statements.' + ) + + parser.add_argument( + '--max_home_stand', + type=int, + dest='max_home_stand', + default=2, + help=( + 'Maximum consecutive home or away games. Default to 2, which means' + ' three home or away games in a row is forbidden.' + ), + ) + + args = parser.parse_args() + + # set default for num_matchdays + num_matches_per_day = args.num_matches_per_day + if not num_matches_per_day: + num_matches_per_day = args.num_teams // 2 + ncpu = 8 + try: + ncpu = len(os.sched_getaffinity(0)) + except AttributeError: + pass + cpu = args.cpu + if not cpu: + cpu = min(6, ncpu) + print('Setting number of search workers to %i' % cpu) + + if cpu > ncpu: + print( + 'You asked for %i workers to be used, but the os only reports %i CPUs' + ' available. This might slow down processing' % (cpu, ncpu) ) - args = parser.parse_args() - - # set default for num_matchdays - num_matches_per_day = args.num_matches_per_day - if not num_matches_per_day: - num_matches_per_day = args.num_teams // 2 - ncpu = 8 - try: - ncpu = len(os.sched_getaffinity(0)) - except AttributeError: - pass - cpu = args.cpu - if not cpu: - cpu = min(6, ncpu) - print('Setting number of search workers to %i' % cpu) - - if cpu > ncpu: - print( - 'You asked for %i workers to be used, but the os only reports %i CPUs available. This might slow down processing' - % (cpu, ncpu)) - - if cpu != 6: - # don't whinge at user if cpu is set to 6 - if cpu < ncpu: - print( - 'Using %i workers, but there are %i CPUs available. You might get faster results by using the command line option --cpu %i, but be aware ORTools CP-SAT solver is tuned to 6 CPUs' - % (cpu, ncpu, ncpu)) - - if cpu > 6: - print( - 'Using %i workers. Be aware ORTools CP-SAT solver is tuned to 6 CPUs' - % cpu) - - # assign_matches() - assign_matches(args.num_teams, args.num_matchdays, num_matches_per_day, - args.max_home_stand, args.time_limit, cpu, args.csv, - args.debug) + if cpu != 6: + # don't whinge at user if cpu is set to 6 + if cpu < ncpu: + print( + 'Using %i workers, but there are %i CPUs available. You might get' + ' faster results by using the command line option --cpu %i, but be' + ' aware ORTools CP-SAT solver is tuned to 6 CPUs' % (cpu, ncpu, ncpu) + ) + + if cpu > 6: + print( + 'Using %i workers. Be aware ORTools CP-SAT solver is tuned to 6 CPUs' + % cpu + ) + + # assign_matches() + assign_matches( + args.num_teams, + args.num_matchdays, + num_matches_per_day, + args.max_home_stand, + args.time_limit, + cpu, + args.csv, + args.debug, + ) if __name__ == '__main__': - main() + main() diff --git a/examples/contrib/stable_marriage.py b/examples/contrib/stable_marriage.py index 1831c470ae8..4c274e626da 100644 --- a/examples/contrib/stable_marriage.py +++ b/examples/contrib/stable_marriage.py @@ -13,28 +13,28 @@ # limitations under the License. """ - Stable marriage problem in Google CP Solver. +Stable marriage problem in Google CP Solver. - Problem and OPL model from Pascal Van Hentenryck - 'The OPL Optimization Programming Language', page 43ff. +Problem and OPL model from Pascal Van Hentenryck +'The OPL Optimization Programming Language', page 43ff. - Also, see - http://www.comp.rgu.ac.uk/staff/ha/ZCSP/additional_problems/stable_marriage/stable_marriage.pdf +Also, see +http://www.comp.rgu.ac.uk/staff/ha/ZCSP/additional_problems/stable_marriage/stable_marriage.pdf - Note: This model is translated from my Comet model - http://www.hakank.org/comet/stable_marriage.co - I have kept some of the constraint from that code. +Note: This model is translated from my Comet model + http://www.hakank.org/comet/stable_marriage.co +I have kept some of the constraint from that code. - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/stable_marriage.mzn - * Comet : http://www.hakank.org/comet/stable_marriage.co - * ECLiPSe : http://www.hakank.org/eclipse/stable_marriage.ecl - * Gecode : http://hakank.org/gecode/stable_marriage.cpp - * SICStus : http://hakank.org/sicstus/stable_marriage.pl +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/stable_marriage.mzn +* Comet : http://www.hakank.org/comet/stable_marriage.co +* ECLiPSe : http://www.hakank.org/eclipse/stable_marriage.ecl +* Gecode : http://hakank.org/gecode/stable_marriage.cpp +* SICStus : http://hakank.org/sicstus/stable_marriage.pl - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys @@ -83,10 +83,11 @@ def main(ranks, problem_name): for m in range(n): for o in range(n): b1 = solver.IsGreaterCstVar( - solver.Element(rankMen[m], wife[m]), rankMen[m][o]) - b2 = ( - solver.IsLessCstVar( - solver.Element(rankWomen[o], husband[o]), rankWomen[o][m])) + solver.Element(rankMen[m], wife[m]), rankMen[m][o] + ) + b2 = solver.IsLessCstVar( + solver.Element(rankWomen[o], husband[o]), rankWomen[o][m] + ) solver.Add(b1 - b2 <= 0) # forall(w in Women, o in Men) @@ -95,9 +96,11 @@ def main(ranks, problem_name): for w in range(n): for o in range(n): b1 = solver.IsGreaterCstVar( - solver.Element(rankWomen[w], husband[w]), rankWomen[w][o]) + solver.Element(rankWomen[w], husband[w]), rankWomen[w][o] + ) b2 = solver.IsLessCstVar( - solver.Element(rankMen[o], wife[o]), rankMen[o][w]) + solver.Element(rankMen[o], wife[o]), rankMen[o][w] + ) solver.Add(b1 - b2 <= 0) # @@ -107,8 +110,9 @@ def main(ranks, problem_name): solution.Add(wife) solution.Add(husband) - db = solver.Phase(wife + husband, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + wife + husband, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) num_solutions = 0 @@ -135,10 +139,20 @@ def main(ranks, problem_name): # From Van Hentenryck's OPL book # van_hentenryck = { - "rankWomen": [[1, 2, 4, 3, 5], [3, 5, 1, 2, 4], [5, 4, 2, 1, 3], - [1, 3, 5, 4, 2], [4, 2, 3, 5, 1]], - "rankMen": [[5, 1, 2, 4, 3], [4, 1, 3, 2, 5], [5, 3, 2, 4, 1], - [1, 5, 4, 3, 2], [4, 3, 2, 1, 5]] + "rankWomen": [ + [1, 2, 4, 3, 5], + [3, 5, 1, 2, 4], + [5, 4, 2, 1, 3], + [1, 3, 5, 4, 2], + [4, 2, 3, 5, 1], + ], + "rankMen": [ + [5, 1, 2, 4, 3], + [4, 1, 3, 2, 5], + [5, 3, 2, 4, 1], + [1, 5, 4, 3, 2], + [4, 3, 2, 1, 5], + ], } # @@ -146,16 +160,28 @@ def main(ranks, problem_name): # http://mathworld.wolfram.com/StableMarriageProblem.html # mathworld = { - "rankWomen": [[3, 1, 5, 2, 8, 7, 6, 9, 4], [9, 4, 8, 1, 7, 6, 3, 2, 5], - [3, 1, 8, 9, 5, 4, 2, 6, 7], [8, 7, 5, 3, 2, 6, 4, 9, 1], - [6, 9, 2, 5, 1, 4, 7, 3, 8], [2, 4, 5, 1, 6, 8, 3, 9, 7], - [9, 3, 8, 2, 7, 5, 4, 6, 1], [6, 3, 2, 1, 8, 4, 5, 9, 7], - [8, 2, 6, 4, 9, 1, 3, 7, 5]], - "rankMen": [[7, 3, 8, 9, 6, 4, 2, 1, 5], [5, 4, 8, 3, 1, 2, 6, 7, 9], - [4, 8, 3, 9, 7, 5, 6, 1, 2], [9, 7, 4, 2, 5, 8, 3, 1, 6], - [2, 6, 4, 9, 8, 7, 5, 1, 3], [2, 7, 8, 6, 5, 3, 4, 1, 9], - [1, 6, 2, 3, 8, 5, 4, 9, 7], [5, 6, 9, 1, 2, 8, 4, 3, 7], - [6, 1, 4, 7, 5, 8, 3, 9, 2]] + "rankWomen": [ + [3, 1, 5, 2, 8, 7, 6, 9, 4], + [9, 4, 8, 1, 7, 6, 3, 2, 5], + [3, 1, 8, 9, 5, 4, 2, 6, 7], + [8, 7, 5, 3, 2, 6, 4, 9, 1], + [6, 9, 2, 5, 1, 4, 7, 3, 8], + [2, 4, 5, 1, 6, 8, 3, 9, 7], + [9, 3, 8, 2, 7, 5, 4, 6, 1], + [6, 3, 2, 1, 8, 4, 5, 9, 7], + [8, 2, 6, 4, 9, 1, 3, 7, 5], + ], + "rankMen": [ + [7, 3, 8, 9, 6, 4, 2, 1, 5], + [5, 4, 8, 3, 1, 2, 6, 7, 9], + [4, 8, 3, 9, 7, 5, 6, 1, 2], + [9, 7, 4, 2, 5, 8, 3, 1, 6], + [2, 6, 4, 9, 8, 7, 5, 1, 3], + [2, 7, 8, 6, 5, 3, 4, 1, 9], + [1, 6, 2, 3, 8, 5, 4, 9, 7], + [5, 6, 9, 1, 2, 8, 4, 3, 7], + [6, 1, 4, 7, 5, 8, 3, 9, 2], + ], } # @@ -164,7 +190,7 @@ def main(ranks, problem_name): # problem3 = { "rankWomen": [[1, 2, 3, 4], [4, 3, 2, 1], [1, 2, 3, 4], [3, 4, 1, 2]], - "rankMen": [[1, 2, 3, 4], [2, 1, 3, 4], [1, 4, 3, 2], [4, 3, 1, 2]] + "rankMen": [[1, 2, 3, 4], [2, 1, 3, 4], [1, 4, 3, 2], [4, 3, 1, 2]], } # @@ -173,10 +199,22 @@ def main(ranks, problem_name): # page 4 # problem4 = { - "rankWomen": [[1, 5, 4, 6, 2, 3], [4, 1, 5, 2, 6, 3], [6, 4, 2, 1, 5, 3], - [1, 5, 2, 4, 3, 6], [4, 2, 1, 5, 6, 3], [2, 6, 3, 5, 1, 4]], - "rankMen": [[1, 4, 2, 5, 6, 3], [3, 4, 6, 1, 5, 2], [1, 6, 4, 2, 3, 5], - [6, 5, 3, 4, 2, 1], [3, 1, 2, 4, 5, 6], [2, 3, 1, 6, 5, 4]] + "rankWomen": [ + [1, 5, 4, 6, 2, 3], + [4, 1, 5, 2, 6, 3], + [6, 4, 2, 1, 5, 3], + [1, 5, 2, 4, 3, 6], + [4, 2, 1, 5, 6, 3], + [2, 6, 3, 5, 1, 4], + ], + "rankMen": [ + [1, 4, 2, 5, 6, 3], + [3, 4, 6, 1, 5, 2], + [1, 6, 4, 2, 3, 5], + [6, 5, 3, 4, 2, 1], + [3, 1, 2, 4, 5, 6], + [2, 3, 1, 6, 5, 4], + ], } if __name__ == "__main__": diff --git a/examples/contrib/stable_marriage_sat.py b/examples/contrib/stable_marriage_sat.py index 59742fdae0a..7b7648c392b 100644 --- a/examples/contrib/stable_marriage_sat.py +++ b/examples/contrib/stable_marriage_sat.py @@ -52,7 +52,7 @@ def main(ranks, pair_num): for w in range(n): model.AddElement(husband[w], wife, w) - #mrank[w][m] < mrank[w][husband[w]] => wrank[m][wife[m]] < wrank[m][w] + # mrank[w][m] < mrank[w][husband[w]] => wrank[m][wife[m]] < wrank[m][w] for w in range(n): for m in range(n): husband_rank = model.NewIntVar(1, n, "") @@ -64,7 +64,8 @@ def main(ranks, pair_num): husband_dominated = model.NewBoolVar("") model.Add(mrank[w][m] < husband_rank).OnlyEnforceIf(husband_dominated) model.Add(mrank[w][m] >= husband_rank).OnlyEnforceIf( - husband_dominated.Not()) + husband_dominated.Not() + ) wife_dominates = model.NewBoolVar("") model.Add(wife_rank < wrank[m][w]).OnlyEnforceIf(wife_dominates) @@ -72,7 +73,7 @@ def main(ranks, pair_num): model.AddImplication(husband_dominated, wife_dominates) - #wrank[m][w] < wrank[m][wife[m]] => mrank[w][husband[w]] < mrank[w][m] + # wrank[m][w] < wrank[m][wife[m]] => mrank[w][husband[w]] < mrank[w][m] for m in range(n): for w in range(n): wife_rank = model.NewIntVar(1, n, "") @@ -88,7 +89,8 @@ def main(ranks, pair_num): husband_dominates = model.NewBoolVar("") model.Add(husband_rank < mrank[w][m]).OnlyEnforceIf(husband_dominates) model.Add(husband_rank >= mrank[w][m]).OnlyEnforceIf( - husband_dominates.Not()) + husband_dominates.Not() + ) model.AddImplication(wife_dominated, husband_dominates) @@ -101,14 +103,26 @@ def main(ranks, pair_num): if __name__ == "__main__": rankings1 = { "rankMen": [[1, 2, 3, 4], [4, 3, 2, 1], [1, 2, 3, 4], [3, 4, 1, 2]], - "rankWomen": [[1, 2, 3, 4], [2, 1, 3, 4], [1, 4, 3, 2], [4, 3, 1, 2]] + "rankWomen": [[1, 2, 3, 4], [2, 1, 3, 4], [1, 4, 3, 2], [4, 3, 1, 2]], } rankings2 = { - "rankMen": [[1, 5, 4, 6, 2, 3], [4, 1, 5, 2, 6, 3], [6, 4, 2, 1, 5, 3], - [1, 5, 2, 4, 3, 6], [4, 2, 1, 5, 6, 3], [2, 6, 3, 5, 1, 4]], - "rankWomen": [[1, 4, 2, 5, 6, 3], [3, 4, 6, 1, 5, 2], [1, 6, 4, 2, 3, 5], - [6, 5, 3, 4, 2, 1], [3, 1, 2, 4, 5, 6], [2, 3, 1, 6, 5, 4]] + "rankMen": [ + [1, 5, 4, 6, 2, 3], + [4, 1, 5, 2, 6, 3], + [6, 4, 2, 1, 5, 3], + [1, 5, 2, 4, 3, 6], + [4, 2, 1, 5, 6, 3], + [2, 6, 3, 5, 1, 4], + ], + "rankWomen": [ + [1, 4, 2, 5, 6, 3], + [3, 4, 6, 1, 5, 2], + [1, 6, 4, 2, 3, 5], + [6, 5, 3, 4, 2, 1], + [3, 1, 2, 4, 5, 6], + [2, 3, 1, 6, 5, 4], + ], } problem = rankings2 diff --git a/examples/contrib/steel.py b/examples/contrib/steel.py index 3915f6e91b1..e46c888306f 100644 --- a/examples/contrib/steel.py +++ b/examples/contrib/steel.py @@ -17,13 +17,13 @@ parser = argparse.ArgumentParser() parser.add_argument( - '--data', - default='examples/contrib/steel.txt', - help='path to data file') + '--data', default='examples/contrib/steel.txt', help='path to data file' +) parser.add_argument( - '--time_limit', default=20000, type=int, help='global time limit') + '--time_limit', default=20000, type=int, help='global time limit' +) -#----------------helper for binpacking posting---------------- +# ----------------helper for binpacking posting---------------- def BinPacking(solver, binvars, weights, loadvars): @@ -37,7 +37,7 @@ def BinPacking(solver, binvars, weights, loadvars): solver.Add(solver.SumEquality(loadvars, sum(weights))) -#------------------------------data reading------------------- +# ------------------------------data reading------------------- def ReadData(filename): @@ -55,15 +55,15 @@ def ReadData(filename): loss = [ min([x for x in capacity if x >= c]) - c for c in range(max_capacity + 1) ] - color_orders = [[o - for o in range(nb_slabs) - if colors[o] == c] - for c in range(1, nb_colors + 1)] + color_orders = [ + [o for o in range(nb_slabs) if colors[o] == c] + for c in range(1, nb_colors + 1) + ] print('Solving steel mill with', nb_slabs, 'slabs') return (nb_slabs, capacity, max_capacity, weights, colors, loss, color_orders) -#------------------dedicated search for this problem----------- +# ------------------dedicated search for this problem----------- class SteelDecisionBuilder(pywrapcp.PyDecisionBuilder): @@ -102,10 +102,11 @@ def Next(self, solver): # try first to place the order in the slab that will induce # the least increase of the loss loads = self.getLoads() - l, v = min((self.__losstab[loads[i] + weight], i) - for i in range(var.Min(), - var.Max() + 1) - if var.Contains(i) and loads[i] + weight <= self.__maxcapa) + l, v = min( + (self.__losstab[loads[i] + weight], i) + for i in range(var.Min(), var.Max() + 1) + if var.Contains(i) and loads[i] + weight <= self.__maxcapa + ) decision = solver.AssignVariableValue(var, v) return decision else: @@ -119,18 +120,23 @@ def getLoads(self): return load def MaxBound(self): - """ returns the max value bound to a variable, -1 if no variables bound""" - return max([-1] + [ - self.__x[o].Min() - for o in range(self.__nb_slabs) - if self.__x[o].Bound() - ]) + """returns the max value bound to a variable, -1 if no variables bound""" + return max( + [-1] + + [ + self.__x[o].Min() + for o in range(self.__nb_slabs) + if self.__x[o].Bound() + ] + ) def NextVar(self): - """ mindom size heuristic with tie break on the weights of orders """ - res = [(self.__x[o].Size(), -self.__weights[o], self.__x[o]) - for o in range(self.__nb_slabs) - if self.__x[o].Size() > 1] + """mindom size heuristic with tie break on the weights of orders""" + res = [ + (self.__x[o].Size(), -self.__weights[o], self.__x[o]) + for o in range(self.__nb_slabs) + if self.__x[o].Size() > 1 + ] if res: res.sort() return res[0][2], -res[0][1] # returns the order var and its weight @@ -142,9 +148,10 @@ def DebugString(self): def main(args): - #------------------solver and variable declaration------------- - (nb_slabs, capacity, max_capacity, weights, colors, loss, color_orders) =\ + # ------------------solver and variable declaration------------- + (nb_slabs, capacity, max_capacity, weights, colors, loss, color_orders) = ( ReadData(args.data) + ) nb_colors = len(color_orders) solver = pywrapcp.Solver('Steel Mill Slab') x = [solver.IntVar(0, nb_slabs - 1, 'x' + str(i)) for i in range(nb_slabs)] @@ -153,34 +160,42 @@ def main(args): for i in range(nb_slabs) ] - #-------------------post of the constraints-------------- + # -------------------post of the constraints-------------- # Bin Packing. BinPacking(solver, x, weights, load_vars) # At most two colors per slab. for s in range(nb_slabs): solver.Add( - solver.SumLessOrEqual([ - solver.Max([solver.IsEqualCstVar(x[c], s) - for c in o]) - for o in color_orders - ], 2)) - - #----------------Objective------------------------------- - - objective_var = \ - solver.Sum([load_vars[s].IndexOf(loss) for s in range(nb_slabs)]).Var() + solver.SumLessOrEqual( + [ + solver.Max([solver.IsEqualCstVar(x[c], s) for c in o]) + for o in color_orders + ], + 2, + ) + ) + + # ----------------Objective------------------------------- + + objective_var = solver.Sum( + [load_vars[s].IndexOf(loss) for s in range(nb_slabs)] + ).Var() objective = solver.Minimize(objective_var, 1) - #------------start the search and optimization----------- + # ------------start the search and optimization----------- db = SteelDecisionBuilder(x, nb_slabs, weights, loss, load_vars) search_log = solver.SearchLog(100000, objective_var) global_limit = solver.TimeLimit(args.time_limit) solver.NewSearch(db, [objective, search_log, global_limit]) while solver.NextSolution(): - print('Objective:', objective_var.Value(),\ - 'check:', sum(loss[load_vars[s].Min()] for s in range(nb_slabs))) + print( + 'Objective:', + objective_var.Value(), + 'check:', + sum(loss[load_vars[s].Min()] for s in range(nb_slabs)), + ) solver.EndSearch() diff --git a/examples/contrib/steel_lns.py b/examples/contrib/steel_lns.py index 86e926520d5..cca575d1de9 100644 --- a/examples/contrib/steel_lns.py +++ b/examples/contrib/steel_lns.py @@ -18,26 +18,29 @@ parser = argparse.ArgumentParser() parser.add_argument( - '--data', - default='examples/contrib/steel.txt', - help='path to data file') + '--data', default='examples/contrib/steel.txt', help='path to data file' +) parser.add_argument( - '--time_limit', default=20000, type=int, help='global time limit') + '--time_limit', default=20000, type=int, help='global time limit' +) parser.add_argument( '--lns_fragment_size', default=10, type=int, - help='size of the random lns fragment') + help='size of the random lns fragment', +) parser.add_argument( '--lns_random_seed', default=0, type=int, - help='seed for the lns random generator') + help='seed for the lns random generator', +) parser.add_argument( '--lns_fail_limit', default=30, type=int, - help='fail limit when exploring fragments') + help='fail limit when exploring fragments', +) # ---------- helper for binpacking posting ---------- @@ -71,10 +74,10 @@ def ReadData(filename): loss = [ min([x for x in capacity if x >= c]) - c for c in range(max_capacity + 1) ] - color_orders = [[o - for o in range(nb_slabs) - if colors[o] == c] - for c in range(1, nb_colors + 1)] + color_orders = [ + [o for o in range(nb_slabs) if colors[o] == c] + for c in range(1, nb_colors + 1) + ] print('Solving steel mill with', nb_slabs, 'slabs') return (nb_slabs, capacity, max_capacity, weights, colors, loss, color_orders) @@ -120,9 +123,9 @@ def Next(self, solver): loads = self.getLoads() l, v = min( (self.__loss_array[loads[i] + weight], i) - for i in range(var.Min(), - var.Max() + 1) - if var.Contains(i) and loads[i] + weight <= self.__max_capacity) + for i in range(var.Min(), var.Max() + 1) + if var.Contains(i) and loads[i] + weight <= self.__max_capacity + ) decision = solver.AssignVariableValue(var, v) return decision else: @@ -130,24 +133,29 @@ def Next(self, solver): def getLoads(self): load = [0] * len(self.__loads) - for (w, x) in zip(self.__weights, self.__x): + for w, x in zip(self.__weights, self.__x): if x.Bound(): load[x.Min()] += w return load def MaxBound(self): - """ returns the max value bound to a variable, -1 if no variables bound""" - return max([-1] + [ - self.__x[o].Min() - for o in range(self.__nb_slabs) - if self.__x[o].Bound() - ]) + """returns the max value bound to a variable, -1 if no variables bound""" + return max( + [-1] + + [ + self.__x[o].Min() + for o in range(self.__nb_slabs) + if self.__x[o].Bound() + ] + ) def NextVar(self): - """ mindom size heuristic with tie break on the weights of orders """ - res = [(self.__x[o].Size(), -self.__weights[o], self.__x[o]) - for o in range(self.__nb_slabs) - if self.__x[o].Size() > 1] + """mindom size heuristic with tie break on the weights of orders""" + res = [ + (self.__x[o].Size(), -self.__weights[o], self.__x[o]) + for o in range(self.__nb_slabs) + if self.__x[o].Size() > 1 + ] if res: res.sort() return (res[0][2], -res[0][1]) # returns the order var and its weight @@ -184,8 +192,9 @@ def NextFragment(self): def main(args): # ----- solver and variable declaration ----- - (nb_slabs, capacity, max_capacity, weights, colors, loss, color_orders) =\ + (nb_slabs, capacity, max_capacity, weights, colors, loss, color_orders) = ( ReadData(args.data) + ) nb_colors = len(color_orders) solver = pywrapcp.Solver('Steel Mill Slab') x = [solver.IntVar(0, nb_slabs - 1, 'x' + str(i)) for i in range(nb_slabs)] @@ -201,16 +210,20 @@ def main(args): # At most two colors per slab. for s in range(nb_slabs): solver.Add( - solver.SumLessOrEqual([ - solver.Max([solver.IsEqualCstVar(x[c], s) - for c in o]) - for o in color_orders - ], 2)) + solver.SumLessOrEqual( + [ + solver.Max([solver.IsEqualCstVar(x[c], s) for c in o]) + for o in color_orders + ], + 2, + ) + ) # ----- Objective ----- - objective_var = \ - solver.Sum([load_vars[s].IndexOf(loss) for s in range(nb_slabs)]).Var() + objective_var = solver.Sum( + [load_vars[s].IndexOf(loss) for s in range(nb_slabs)] + ).Var() objective = solver.Minimize(objective_var, 1) # ----- start the search and optimization ----- @@ -241,9 +254,11 @@ def main(args): # args.lns_fragment_size, # args.lns_random_seed) local_search_parameters = solver.LocalSearchPhaseParameters( - objective_var, local_search_operator, continuation_db) - local_search_db = solver.LocalSearchPhase(first_solution, - local_search_parameters) + objective_var, local_search_operator, continuation_db + ) + local_search_db = solver.LocalSearchPhase( + first_solution, local_search_parameters + ) global_limit = solver.TimeLimit(args.time_limit) print('using LNS to improve the initial solution') @@ -251,8 +266,12 @@ def main(args): search_log = solver.SearchLog(100000, objective_var) solver.NewSearch(local_search_db, [objective, search_log, global_limit]) while solver.NextSolution(): - print('Objective:', objective_var.Value(),\ - 'check:', sum(loss[load_vars[s].Min()] for s in range(nb_slabs))) + print( + 'Objective:', + objective_var.Value(), + 'check:', + sum(loss[load_vars[s].Min()] for s in range(nb_slabs)), + ) solver.EndSearch() diff --git a/examples/contrib/stigler_contrib.py b/examples/contrib/stigler_contrib.py index f56900dd490..5e4563929aa 100644 --- a/examples/contrib/stigler_contrib.py +++ b/examples/contrib/stigler_contrib.py @@ -13,109 +13,109 @@ # limitations under the License. """ - Original Stigler's 1939 diet problem Google or-tools. - - From GLPK:s example stigler.mod - ''' - STIGLER, original Stigler's 1939 diet problem - - The Stigler Diet is an optimization problem named for George Stigler, - a 1982 Nobel Laureate in economics, who posed the following problem: - For a moderately active man weighing 154 pounds, how much of each of - 77 foods should be eaten on a daily basis so that the man's intake of - nine nutrients will be at least equal to the recommended dietary - allowances (RDSs) suggested by the National Research Council in 1943, - with the cost of the diet being minimal? - - The nutrient RDAs required to be met in Stigler's experiment were - calories, protein, calcium, iron, vitamin A, thiamine, riboflavin, - niacin, and ascorbic acid. The result was an annual budget allocated - to foods such as evaporated milk, cabbage, dried navy beans, and beef - liver at a cost of approximately $0.11 a day in 1939 U.S. dollars. - - While the name 'Stigler Diet' was applied after the experiment by - outsiders, according to Stigler, 'No one recommends these diets for - anyone, let alone everyone.' The Stigler diet has been much ridiculed - for its lack of variety and palatability, however his methodology has - received praise and is considered to be some of the earliest work in - linear programming. - - The Stigler diet question is a linear programming problem. Lacking - any sophisticated method of solving such a problem, Stigler was - forced to utilize heuristic methods in order to find a solution. The - diet question originally asked in which quantities a 154 pound male - would have to consume 77 different foods in order to fulfill the - recommended intake of 9 different nutrients while keeping expense at - a minimum. Through 'trial and error, mathematical insight and - agility,' Stigler was able to eliminate 62 of the foods from the - original 77 (these foods were removed based because they lacked - nutrients in comparison to the remaining 15). From the reduced list, - Stigler calculated the required amounts of each of the remaining 15 - foods to arrive at a cost-minimizing solution to his question. - According to Stigler's calculations, the annual cost of his solution - was $39.93 in 1939 dollars. When corrected for inflation using the - consumer price index, the cost of the diet in 2005 dollars is - $561.43. The specific combination of foods and quantities is as - follows: - - Stigler's 1939 Diet - - Food Annual Quantities Annual Cost - ---------------- ----------------- ----------- - Wheat Flour 370 lb. $13.33 - Evaporated Milk 57 cans 3.84 - Cabbage 111 lb. 4.11 - Spinach 23 lb. 1.85 - Dried Navy Beans 285 lb. 16.80 - ---------------------------------------------- - Total Annual Cost $39.93 - - The 9 nutrients that Stigler's diet took into consideration and their - respective recommended daily amounts were: - - Table of nutrients considered in Stigler's diet - - Nutrient Daily Recommended Intake - ------------------------- ------------------------ - Calories 3,000 Calories - Protein 70 grams - Calcium .8 grams - Iron 12 milligrams - Vitamin A 5,000 IU - Thiamine (Vitamin B1) 1.8 milligrams - Riboflavin (Vitamin B2) 2.7 milligrams - Niacin 18 milligrams - Ascorbic Acid (Vitamin C) 75 milligrams - - Seven years after Stigler made his initial estimates, the development - of George Dantzig's Simplex algorithm made it possible to solve the - problem without relying on heuristic methods. The exact value was - determined to be $39.69 (using the original 1939 data). Dantzig's - algorithm describes a method of traversing the vertices of a polytope - of N+1 dimensions in order to find the optimal solution to a specific - situation. - - (From Wikipedia, the free encyclopedia.) - - Translated from GAMS by Andrew Makhorin . - - For the original GAMS model stigler1939.gms see [3]. - - References: - - 1. George J. Stigler, 'The Cost of Subsistence,' J. Farm Econ. 27, - 1945, pp. 303-14. - - 2. National Research Council, 'Recommended Daily Allowances,' Reprint - and Circular Series No. 115, January, 1943. - - 3. Erwin Kalvelagen, 'Model building with GAMS,' Chapter 2, 'Building - linear programming models,' pp. 128-34. - ''' - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Original Stigler's 1939 diet problem Google or-tools. + +From GLPK:s example stigler.mod +''' +STIGLER, original Stigler's 1939 diet problem + +The Stigler Diet is an optimization problem named for George Stigler, +a 1982 Nobel Laureate in economics, who posed the following problem: +For a moderately active man weighing 154 pounds, how much of each of +77 foods should be eaten on a daily basis so that the man's intake of +nine nutrients will be at least equal to the recommended dietary +allowances (RDSs) suggested by the National Research Council in 1943, +with the cost of the diet being minimal? + +The nutrient RDAs required to be met in Stigler's experiment were +calories, protein, calcium, iron, vitamin A, thiamine, riboflavin, +niacin, and ascorbic acid. The result was an annual budget allocated +to foods such as evaporated milk, cabbage, dried navy beans, and beef +liver at a cost of approximately $0.11 a day in 1939 U.S. dollars. + +While the name 'Stigler Diet' was applied after the experiment by +outsiders, according to Stigler, 'No one recommends these diets for +anyone, let alone everyone.' The Stigler diet has been much ridiculed +for its lack of variety and palatability, however his methodology has +received praise and is considered to be some of the earliest work in +linear programming. + +The Stigler diet question is a linear programming problem. Lacking +any sophisticated method of solving such a problem, Stigler was +forced to utilize heuristic methods in order to find a solution. The +diet question originally asked in which quantities a 154 pound male +would have to consume 77 different foods in order to fulfill the +recommended intake of 9 different nutrients while keeping expense at +a minimum. Through 'trial and error, mathematical insight and +agility,' Stigler was able to eliminate 62 of the foods from the +original 77 (these foods were removed based because they lacked +nutrients in comparison to the remaining 15). From the reduced list, +Stigler calculated the required amounts of each of the remaining 15 +foods to arrive at a cost-minimizing solution to his question. +According to Stigler's calculations, the annual cost of his solution +was $39.93 in 1939 dollars. When corrected for inflation using the +consumer price index, the cost of the diet in 2005 dollars is +$561.43. The specific combination of foods and quantities is as +follows: + +Stigler's 1939 Diet + +Food Annual Quantities Annual Cost +---------------- ----------------- ----------- +Wheat Flour 370 lb. $13.33 +Evaporated Milk 57 cans 3.84 +Cabbage 111 lb. 4.11 +Spinach 23 lb. 1.85 +Dried Navy Beans 285 lb. 16.80 +---------------------------------------------- +Total Annual Cost $39.93 + +The 9 nutrients that Stigler's diet took into consideration and their +respective recommended daily amounts were: + +Table of nutrients considered in Stigler's diet + +Nutrient Daily Recommended Intake +------------------------- ------------------------ +Calories 3,000 Calories +Protein 70 grams +Calcium .8 grams +Iron 12 milligrams +Vitamin A 5,000 IU +Thiamine (Vitamin B1) 1.8 milligrams +Riboflavin (Vitamin B2) 2.7 milligrams +Niacin 18 milligrams +Ascorbic Acid (Vitamin C) 75 milligrams + +Seven years after Stigler made his initial estimates, the development +of George Dantzig's Simplex algorithm made it possible to solve the +problem without relying on heuristic methods. The exact value was +determined to be $39.69 (using the original 1939 data). Dantzig's +algorithm describes a method of traversing the vertices of a polytope +of N+1 dimensions in order to find the optimal solution to a specific +situation. + +(From Wikipedia, the free encyclopedia.) + +Translated from GAMS by Andrew Makhorin . + +For the original GAMS model stigler1939.gms see [3]. + +References: + +1. George J. Stigler, 'The Cost of Subsistence,' J. Farm Econ. 27, + 1945, pp. 303-14. + +2. National Research Council, 'Recommended Daily Allowances,' Reprint + and Circular Series No. 115, January, 1943. + +3. Erwin Kalvelagen, 'Model building with GAMS,' Chapter 2, 'Building + linear programming models,' pp. 128-34. +''' + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.linear_solver import pywraplp @@ -151,49 +151,88 @@ def main(sol="CBC"): "thiamine", # Thiamine, Vit. B1, unit = milligrams "riboflavin", # Riboflavin, Vit. B2, unit = milligrams "niacin", # Niacin (Nicotinic Acid), unit = milligrams - "ascorbicAcid" # Ascorbic Acid, Vit. C, unit = milligrams + "ascorbicAcid", # Ascorbic Acid, Vit. C, unit = milligrams ] - commodities = [["Wheat Flour (Enriched)", "10 lb."], ["Macaroni", "1 lb."], - ["Wheat Cereal (Enriched)", - "28 oz."], ["Corn Flakes", "8 oz."], ["Corn Meal", "1 lb."], - ["Hominy Grits", "24 oz."], ["Rice", "1 lb."], - ["Rolled Oats", "1 lb."], ["White Bread (Enriched)", "1 lb."], - ["Whole Wheat Bread", "1 lb."], ["Rye Bread", "1 lb."], - ["Pound Cake", "1 lb."], ["Soda Crackers", "1 lb."], - ["Milk", "1 qt."], ["Evaporated Milk (can)", "14.5 oz."], - ["Butter", "1 lb."], ["Oleomargarine", "1 lb."], - ["Eggs", "1 doz."], ["Cheese (Cheddar)", "1 lb."], - ["Cream", "1/2 pt."], ["Peanut Butter", "1 lb."], - ["Mayonnaise", "1/2 pt."], ["Crisco", "1 lb."], - ["Lard", "1 lb."], ["Sirloin Steak", "1 lb."], - ["Round Steak", "1 lb."], ["Rib Roast", "1 lb."], - ["Chuck Roast", "1 lb."], ["Plate", "1 lb."], - ["Liver (Beef)", "1 lb."], ["Leg of Lamb", "1 lb."], - ["Lamb Chops (Rib)", "1 lb."], ["Pork Chops", "1 lb."], - ["Pork Loin Roast", "1 lb."], ["Bacon", "1 lb."], - ["Ham - smoked", "1 lb."], ["Salt Pork", "1 lb."], - ["Roasting Chicken", "1 lb."], ["Veal Cutlets", "1 lb."], - ["Salmon, Pink (can)", "16 oz."], ["Apples", "1 lb."], - ["Bananas", "1 lb."], ["Lemons", "1 doz."], - ["Oranges", "1 doz."], ["Green Beans", "1 lb."], - ["Cabbage", "1 lb."], ["Carrots", "1 bunch"], - ["Celery", "1 stalk"], ["Lettuce", "1 head"], - ["Onions", "1 lb."], ["Potatoes", "15 lb."], - ["Spinach", "1 lb."], ["Sweet Potatoes", "1 lb."], - ["Peaches (can)", "No. 2 1/2"], ["Pears (can)", "No. 2 1/2,"], - ["Pineapple (can)", "No. 2 1/2"], ["Asparagus (can)", "No. 2"], - ["Grean Beans (can)", "No. 2"], - ["Pork and Beans (can)", "16 oz."], ["Corn (can)", "No. 2"], - ["Peas (can)", "No. 2"], ["Tomatoes (can)", "No. 2"], - ["Tomato Soup (can)", "10 1/2 oz."], - ["Peaches, Dried", "1 lb."], ["Prunes, Dried", "1 lb."], - ["Raisins, Dried", "15 oz."], ["Peas, Dried", "1 lb."], - ["Lima Beans, Dried", "1 lb."], ["Navy Beans, Dried", "1 lb."], - ["Coffee", "1 lb."], ["Tea", "1/4 lb."], ["Cocoa", "8 oz."], - ["Chocolate", "8 oz."], ["Sugar", "10 lb."], - ["Corn Sirup", "24 oz."], ["Molasses", "18 oz."], - ["Strawberry Preserve", "1 lb."]] + commodities = [ + ["Wheat Flour (Enriched)", "10 lb."], + ["Macaroni", "1 lb."], + ["Wheat Cereal (Enriched)", "28 oz."], + ["Corn Flakes", "8 oz."], + ["Corn Meal", "1 lb."], + ["Hominy Grits", "24 oz."], + ["Rice", "1 lb."], + ["Rolled Oats", "1 lb."], + ["White Bread (Enriched)", "1 lb."], + ["Whole Wheat Bread", "1 lb."], + ["Rye Bread", "1 lb."], + ["Pound Cake", "1 lb."], + ["Soda Crackers", "1 lb."], + ["Milk", "1 qt."], + ["Evaporated Milk (can)", "14.5 oz."], + ["Butter", "1 lb."], + ["Oleomargarine", "1 lb."], + ["Eggs", "1 doz."], + ["Cheese (Cheddar)", "1 lb."], + ["Cream", "1/2 pt."], + ["Peanut Butter", "1 lb."], + ["Mayonnaise", "1/2 pt."], + ["Crisco", "1 lb."], + ["Lard", "1 lb."], + ["Sirloin Steak", "1 lb."], + ["Round Steak", "1 lb."], + ["Rib Roast", "1 lb."], + ["Chuck Roast", "1 lb."], + ["Plate", "1 lb."], + ["Liver (Beef)", "1 lb."], + ["Leg of Lamb", "1 lb."], + ["Lamb Chops (Rib)", "1 lb."], + ["Pork Chops", "1 lb."], + ["Pork Loin Roast", "1 lb."], + ["Bacon", "1 lb."], + ["Ham - smoked", "1 lb."], + ["Salt Pork", "1 lb."], + ["Roasting Chicken", "1 lb."], + ["Veal Cutlets", "1 lb."], + ["Salmon, Pink (can)", "16 oz."], + ["Apples", "1 lb."], + ["Bananas", "1 lb."], + ["Lemons", "1 doz."], + ["Oranges", "1 doz."], + ["Green Beans", "1 lb."], + ["Cabbage", "1 lb."], + ["Carrots", "1 bunch"], + ["Celery", "1 stalk"], + ["Lettuce", "1 head"], + ["Onions", "1 lb."], + ["Potatoes", "15 lb."], + ["Spinach", "1 lb."], + ["Sweet Potatoes", "1 lb."], + ["Peaches (can)", "No. 2 1/2"], + ["Pears (can)", "No. 2 1/2,"], + ["Pineapple (can)", "No. 2 1/2"], + ["Asparagus (can)", "No. 2"], + ["Grean Beans (can)", "No. 2"], + ["Pork and Beans (can)", "16 oz."], + ["Corn (can)", "No. 2"], + ["Peas (can)", "No. 2"], + ["Tomatoes (can)", "No. 2"], + ["Tomato Soup (can)", "10 1/2 oz."], + ["Peaches, Dried", "1 lb."], + ["Prunes, Dried", "1 lb."], + ["Raisins, Dried", "15 oz."], + ["Peas, Dried", "1 lb."], + ["Lima Beans, Dried", "1 lb."], + ["Navy Beans, Dried", "1 lb."], + ["Coffee", "1 lb."], + ["Tea", "1/4 lb."], + ["Cocoa", "8 oz."], + ["Chocolate", "8 oz."], + ["Sugar", "10 lb."], + ["Corn Sirup", "24 oz."], + ["Molasses", "18 oz."], + ["Strawberry Preserve", "1 lb."], + ] # price and weight are the two first columns data = [ @@ -273,7 +312,7 @@ def main(sol="CBC"): [51.7, 8773.0, 34.9, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], [13.7, 4996.0, 14.7, 0.0, 0.5, 74.0, 0.0, 0.0, 0.0, 5.0, 0.0], [13.6, 3752.0, 9.0, 0.0, 10.3, 244.0, 0.0, 1.9, 7.5, 146.0, 0.0], - [20.5, 2213.0, 6.4, 11.0, 0.4, 7.0, 0.2, 0.2, 0.4, 3.0, 0.0] + [20.5, 2213.0, 6.4, 11.0, 0.4, 7.0, 0.2, 0.2, 0.4, 3.0, 0.0], ] # recommended daily allowance for a moderately active man @@ -320,9 +359,15 @@ def main(sol="CBC"): print() for i in C: if x[i].SolutionValue() > 0: - print("%-21s %-11s %0.2f %0.2f" % - (commodities[i][0], commodities[i][1], x_cost[i].SolutionValue(), - quant[i].SolutionValue())) + print( + "%-21s %-11s %0.2f %0.2f" + % ( + commodities[i][0], + commodities[i][1], + x_cost[i].SolutionValue(), + quant[i].SolutionValue(), + ) + ) print() diff --git a/examples/contrib/strimko2.py b/examples/contrib/strimko2.py index 883ef0f97ac..a660bba523e 100644 --- a/examples/contrib/strimko2.py +++ b/examples/contrib/strimko2.py @@ -13,34 +13,34 @@ # limitations under the License. """Strimko problem in Google CP Solver. - From - 360: A New Twist on Latin Squares - http://threesixty360.wordpress.com/2009/08/04/a-new-twist-on-latin-squares/ - ''' - The idea is simple: each row and column of an nxn grid must contain - the number 1, 2, ... n exactly once (that is, the grid must form a - Latin square), and each "stream" (connected path in the grid) must - also contain the numbers 1, 2, ..., n exactly once. - ''' - - For more information, see: - * http://www.strimko.com/ - * http://www.strimko.com/rules.htm - * http://www.strimko.com/about.htm - * http://www.puzzlersparadise.com/Strimko.htm - - I have blogged about this (using MiniZinc model) in - 'Strimko - Latin squares puzzle with "streams"' - http://www.hakank.org/constraint_programming_blog/2009/08/strimko_latin_squares_puzzle_w_1.html - - Compare with the following models: - * MiniZinc: http://hakank.org/minizinc/strimko2.mzn - * ECLiPSe: http://hakank.org/eclipse/strimko2.ecl - * SICStus: http://hakank.org/sicstus/strimko2.pl - * Gecode: http://hakank.org/gecode/strimko2.cpp - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - See my other Google CP Solver models: http://www.hakank.org/google_or_tools/ +From +360: A New Twist on Latin Squares +http://threesixty360.wordpress.com/2009/08/04/a-new-twist-on-latin-squares/ +''' +The idea is simple: each row and column of an nxn grid must contain +the number 1, 2, ... n exactly once (that is, the grid must form a +Latin square), and each "stream" (connected path in the grid) must +also contain the numbers 1, 2, ..., n exactly once. +''' + +For more information, see: +* http://www.strimko.com/ +* http://www.strimko.com/rules.htm +* http://www.strimko.com/about.htm +* http://www.puzzlersparadise.com/Strimko.htm + +I have blogged about this (using MiniZinc model) in +'Strimko - Latin squares puzzle with "streams"' +http://www.hakank.org/constraint_programming_blog/2009/08/strimko_latin_squares_puzzle_w_1.html + +Compare with the following models: +* MiniZinc: http://hakank.org/minizinc/strimko2.mzn +* ECLiPSe: http://hakank.org/eclipse/strimko2.ecl +* SICStus: http://hakank.org/sicstus/strimko2.pl +* Gecode: http://hakank.org/gecode/strimko2.cpp + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +See my other Google CP Solver models: http://www.hakank.org/google_or_tools/ """ import sys @@ -56,14 +56,29 @@ def main(streams='', placed=''): # default problem # if streams == '': - streams = [[1, 1, 2, 2, 2, 2, 2], [1, 1, 2, 3, 3, 3, 2], - [1, 4, 1, 3, 3, 5, 5], [4, 4, 3, 1, 3, 5, 5], - [4, 6, 6, 6, 7, 7, 5], [6, 4, 6, 4, 5, 5, 7], - [6, 6, 4, 7, 7, 7, 7]] + streams = [ + [1, 1, 2, 2, 2, 2, 2], + [1, 1, 2, 3, 3, 3, 2], + [1, 4, 1, 3, 3, 5, 5], + [4, 4, 3, 1, 3, 5, 5], + [4, 6, 6, 6, 7, 7, 5], + [6, 4, 6, 4, 5, 5, 7], + [6, 6, 4, 7, 7, 7, 7], + ] # Note: This is 1-based - placed = [[2, 1, 1], [2, 3, 7], [2, 5, 6], [2, 7, 4], [3, 2, 7], [3, 6, 1], - [4, 1, 4], [4, 7, 5], [5, 2, 2], [5, 6, 6]] + placed = [ + [2, 1, 1], + [2, 3, 7], + [2, 5, 6], + [2, 7, 4], + [3, 2, 7], + [3, 6, 1], + [4, 1, 4], + [4, 7, 5], + [5, 2, 2], + [5, 6, 6], + ] n = len(streams) num_placed = len(placed) diff --git a/examples/contrib/subset_sum.py b/examples/contrib/subset_sum.py index cdf308834bc..ddb0a67c823 100644 --- a/examples/contrib/subset_sum.py +++ b/examples/contrib/subset_sum.py @@ -13,30 +13,30 @@ # limitations under the License. """ - Subset sum problem in Google CP Solver. - - From Katta G. Murty: 'Optimization Models for Decision Making', page 340 - http://ioe.engin.umich.edu/people/fac/books/murty/opti_model/junior-7.pdf - ''' - Example 7.8.1 - - A bank van had several bags of coins, each containing either - 16, 17, 23, 24, 39, or 40 coins. While the van was parked on the - street, thieves stole some bags. A total of 100 coins were lost. - It is required to find how many bags were stolen. - ''' - - Compare with the following models: - * Comet: http://www.hakank.org/comet/subset_sum.co - * ECLiPSE: http://www.hakank.org/eclipse/subset_sum.ecl - * Gecode: http://www.hakank.org/gecode/subset_sum.cpp - * MiniZinc: http://www.hakank.org/minizinc/subset_sum.mzn - * Tailor/Essence': http://www.hakank.org/tailor/subset_sum.py - * SICStus: http://hakank.org/sicstus/subset_sum.pl - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Subset sum problem in Google CP Solver. + +From Katta G. Murty: 'Optimization Models for Decision Making', page 340 +http://ioe.engin.umich.edu/people/fac/books/murty/opti_model/junior-7.pdf +''' +Example 7.8.1 + +A bank van had several bags of coins, each containing either +16, 17, 23, 24, 39, or 40 coins. While the van was parked on the +street, thieves stole some bags. A total of 100 coins were lost. +It is required to find how many bags were stolen. +''' + +Compare with the following models: +* Comet: http://www.hakank.org/comet/subset_sum.co +* ECLiPSE: http://www.hakank.org/eclipse/subset_sum.ecl +* Gecode: http://www.hakank.org/gecode/subset_sum.cpp +* MiniZinc: http://www.hakank.org/minizinc/subset_sum.mzn +* Tailor/Essence': http://www.hakank.org/tailor/subset_sum.py +* SICStus: http://hakank.org/sicstus/subset_sum.pl + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp diff --git a/examples/contrib/survo_puzzle.py b/examples/contrib/survo_puzzle.py index 8a31e0fa35c..d47cbe7cb0d 100644 --- a/examples/contrib/survo_puzzle.py +++ b/examples/contrib/survo_puzzle.py @@ -13,51 +13,51 @@ # limitations under the License. """ - Survo puzzle Google CP Solver. - - http://en.wikipedia.org/wiki/Survo_Puzzle - ''' - Survo puzzle is a kind of logic puzzle presented (in April 2006) and studied - by Seppo Mustonen. The name of the puzzle is associated to Mustonen's - Survo system which is a general environment for statistical computing and - related areas. - - In a Survo puzzle the task is to fill an m * n table by integers 1,2,...,m*n - so - that each of these numbers appears only once and their row and column sums are - equal to integers given on the bottom and the right side of the table. - Often some of the integers are given readily in the table in order to - guarantee uniqueness of the solution and/or for making the task easier. - ''' - - See also - http://www.survo.fi/english/index.html - http://www.survo.fi/puzzles/index.html - - References: - Mustonen, S. (2006b). "On certain cross sum puzzles" - http://www.survo.fi/papers/puzzles.pdf - Mustonen, S. (2007b). "Enumeration of uniquely solvable open Survo puzzles." - http://www.survo.fi/papers/enum_survo_puzzles.pdf - Kimmo Vehkalahti: "Some comments on magic squares and Survo puzzles" - http://www.helsinki.fi/~kvehkala/Kimmo_Vehkalahti_Windsor.pdf - R code: http://koti.mbnet.fi/tuimala/tiedostot/survo.R - - Compare with the following models: - * Choco : http://www.hakank.org/choco/SurvoPuzzle.java - * Comet : http://www.hakank.org/comet/survo_puzzle.co - * ECLiPSE : http://www.hakank.org/eclipse/survo_puzzle.ecl - * Gecode : http://www.hakank.org/gecode/survo_puzzle.cpp - * Gecode/R: http://www.hakank.org/gecode_r/survo_puzzle.rb - * JaCoP : http://www.hakank.org/JaCoP/SurvoPuzzle.java - * MiniZinc: http://www.hakank.org/minizinc/survo_puzzle.mzn - * Tailor/Essence': http://www.hakank.org/tailor/survo_puzzle.eprime - * Zinc: http://www.hakank.org/minizinc/survo_puzzle.zinc - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Survo puzzle Google CP Solver. + +http://en.wikipedia.org/wiki/Survo_Puzzle +''' +Survo puzzle is a kind of logic puzzle presented (in April 2006) and studied +by Seppo Mustonen. The name of the puzzle is associated to Mustonen's +Survo system which is a general environment for statistical computing and +related areas. + +In a Survo puzzle the task is to fill an m * n table by integers 1,2,...,m*n +so +that each of these numbers appears only once and their row and column sums are +equal to integers given on the bottom and the right side of the table. +Often some of the integers are given readily in the table in order to +guarantee uniqueness of the solution and/or for making the task easier. +''' + +See also +http://www.survo.fi/english/index.html +http://www.survo.fi/puzzles/index.html + +References: +Mustonen, S. (2006b). "On certain cross sum puzzles" +http://www.survo.fi/papers/puzzles.pdf +Mustonen, S. (2007b). "Enumeration of uniquely solvable open Survo puzzles." +http://www.survo.fi/papers/enum_survo_puzzles.pdf +Kimmo Vehkalahti: "Some comments on magic squares and Survo puzzles" +http://www.helsinki.fi/~kvehkala/Kimmo_Vehkalahti_Windsor.pdf +R code: http://koti.mbnet.fi/tuimala/tiedostot/survo.R + +Compare with the following models: +* Choco : http://www.hakank.org/choco/SurvoPuzzle.java +* Comet : http://www.hakank.org/comet/survo_puzzle.co +* ECLiPSE : http://www.hakank.org/eclipse/survo_puzzle.ecl +* Gecode : http://www.hakank.org/gecode/survo_puzzle.cpp +* Gecode/R: http://www.hakank.org/gecode_r/survo_puzzle.rb +* JaCoP : http://www.hakank.org/JaCoP/SurvoPuzzle.java +* MiniZinc: http://www.hakank.org/minizinc/survo_puzzle.mzn +* Tailor/Essence': http://www.hakank.org/tailor/survo_puzzle.eprime +* Zinc: http://www.hakank.org/minizinc/survo_puzzle.zinc + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys from ortools.constraint_solver import pywrapcp @@ -118,7 +118,8 @@ def main(r=0, c=0, rowsums=[], colsums=[], game=[]): collector = solver.AllSolutionCollector(solution) solver.Solve( solver.Phase(xflat, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE), - [collector]) + [collector], + ) num_solutions = collector.SolutionCount() print("\nnum_solutions: ", num_solutions) diff --git a/examples/contrib/toNum.py b/examples/contrib/toNum.py index a985041ac94..c3283eaea26 100644 --- a/examples/contrib/toNum.py +++ b/examples/contrib/toNum.py @@ -13,13 +13,13 @@ # limitations under the License. """ - toNum in Google CP Solver. +toNum in Google CP Solver. - Convert a number <-> array of int in a specific base. +Convert a number <-> array of int in a specific base. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.constraint_solver import pywrapcp @@ -32,7 +32,8 @@ def toNum(solver, t, s, base): tlen = len(t) solver.Add( - s == solver.Sum([(base**(tlen - i - 1)) * t[i] for i in range(tlen)])) + s == solver.Sum([(base ** (tlen - i - 1)) * t[i] for i in range(tlen)]) + ) def main(unused_argv): @@ -65,8 +66,13 @@ def main(unused_argv): collector = solver.AllSolutionCollector(solution) solver.Solve( - solver.Phase([x[i] for i in range(n)], solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE), [collector]) + solver.Phase( + [x[i] for i in range(n)], + solver.CHOOSE_FIRST_UNBOUND, + solver.ASSIGN_MIN_VALUE, + ), + [collector], + ) num_solutions = collector.SolutionCount() for s in range(num_solutions): diff --git a/examples/contrib/traffic_lights.py b/examples/contrib/traffic_lights.py index ed8bef72848..40598ab0c8d 100644 --- a/examples/contrib/traffic_lights.py +++ b/examples/contrib/traffic_lights.py @@ -13,60 +13,60 @@ # limitations under the License. """ - Traffic lights problem in Google CP Solver. - - CSPLib problem 16 - http://www.cs.st-andrews.ac.uk/~ianm/CSPLib/prob/prob016/index.html - ''' - Specification: - Consider a four way traffic junction with eight traffic lights. Four of the - traffic - lights are for the vehicles and can be represented by the variables V1 to V4 - with domains - {r,ry,g,y} (for red, red-yellow, green and yellow). The other four traffic - lights are - for the pedestrians and can be represented by the variables P1 to P4 with - domains {r,g}. - - The constraints on these variables can be modelled by quaternary constraints - on - (Vi, Pi, Vj, Pj ) for 1<=i<=4, j=(1+i)mod 4 which allow just the tuples - {(r,r,g,g), (ry,r,y,r), (g,g,r,r), (y,r,ry,r)}. - - It would be interesting to consider other types of junction (e.g. five roads - intersecting) as well as modelling the evolution over time of the traffic - light sequence. - ... - - Results - Only 2^2 out of the 2^12 possible assignments are solutions. - - (V1,P1,V2,P2,V3,P3,V4,P4) = - {(r,r,g,g,r,r,g,g), (ry,r,y,r,ry,r,y,r), (g,g,r,r,g,g,r,r), - (y,r,ry,r,y,r,ry,r)} - [(1,1,3,3,1,1,3,3), ( 2,1,4,1, 2,1,4,1), (3,3,1,1,3,3,1,1), (4,1, 2,1,4,1, - 2,1)} - - The problem has relative few constraints, but each is very tight. Local - propagation - appears to be rather ineffective on this problem. - - ''' - - Note: In this model we use only the constraint solver.AllowedAssignments(). - - - Compare with these models: - * MiniZinc: http://www.hakank.org/minizinc/traffic_lights.mzn - * Comet : http://www.hakank.org/comet/traffic_lights.co - * ECLiPSe : http://www.hakank.org/eclipse/traffic_lights.ecl - * Gecode : http://hakank.org/gecode/traffic_lights.cpp - * SICStus : http://hakank.org/sicstus/traffic_lights.pl - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Traffic lights problem in Google CP Solver. + +CSPLib problem 16 +http://www.cs.st-andrews.ac.uk/~ianm/CSPLib/prob/prob016/index.html +''' +Specification: +Consider a four way traffic junction with eight traffic lights. Four of the +traffic +lights are for the vehicles and can be represented by the variables V1 to V4 +with domains +{r,ry,g,y} (for red, red-yellow, green and yellow). The other four traffic +lights are +for the pedestrians and can be represented by the variables P1 to P4 with +domains {r,g}. + +The constraints on these variables can be modelled by quaternary constraints +on +(Vi, Pi, Vj, Pj ) for 1<=i<=4, j=(1+i)mod 4 which allow just the tuples +{(r,r,g,g), (ry,r,y,r), (g,g,r,r), (y,r,ry,r)}. + +It would be interesting to consider other types of junction (e.g. five roads +intersecting) as well as modelling the evolution over time of the traffic +light sequence. +... + +Results +Only 2^2 out of the 2^12 possible assignments are solutions. + +(V1,P1,V2,P2,V3,P3,V4,P4) = + {(r,r,g,g,r,r,g,g), (ry,r,y,r,ry,r,y,r), (g,g,r,r,g,g,r,r), + (y,r,ry,r,y,r,ry,r)} + [(1,1,3,3,1,1,3,3), ( 2,1,4,1, 2,1,4,1), (3,3,1,1,3,3,1,1), (4,1, 2,1,4,1, + 2,1)} + + The problem has relative few constraints, but each is very tight. Local + propagation +appears to be rather ineffective on this problem. + +''' + +Note: In this model we use only the constraint solver.AllowedAssignments(). + + +Compare with these models: +* MiniZinc: http://www.hakank.org/minizinc/traffic_lights.mzn +* Comet : http://www.hakank.org/comet/traffic_lights.co +* ECLiPSe : http://www.hakank.org/eclipse/traffic_lights.ecl +* Gecode : http://hakank.org/gecode/traffic_lights.cpp +* SICStus : http://hakank.org/sicstus/traffic_lights.pl + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys diff --git a/examples/contrib/vendor_scheduling.py b/examples/contrib/vendor_scheduling.py index e681d8b70b7..32c7c2856bc 100644 --- a/examples/contrib/vendor_scheduling.py +++ b/examples/contrib/vendor_scheduling.py @@ -18,12 +18,14 @@ def main(): # Last columns are : # index_of_the_schedule, sum of worked hours (per work type). # The index is useful for branching. - possible_schedules = [[1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 8], - [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 4], - [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 2, 5], - [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 3, 4], - [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 4, 3], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0]] + possible_schedules = [ + [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 8], + [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 4], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 2, 5], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 3, 4], + [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 4, 3], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0], + ] num_possible_schedules = len(possible_schedules) selected_schedules = [] @@ -40,8 +42,9 @@ def main(): for j in range(num_hours): x[i, j] = solver.IntVar(0, num_work_types, 'x[%i,%i]' % (i, j)) tmp.append(x[i, j]) - selected_schedule = solver.IntVar(0, num_possible_schedules - 1, - 's[%i]' % i) + selected_schedule = solver.IntVar( + 0, num_possible_schedules - 1, 's[%i]' % i + ) hours = solver.IntVar(0, num_hours, 'h[%i]' % i) selected_schedules.append(selected_schedule) vendors_stat.append(hours) @@ -67,8 +70,9 @@ def main(): # # Search # - db = solver.Phase(selected_schedules, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + selected_schedules, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) @@ -77,8 +81,9 @@ def main(): num_solutions += 1 for i in range(num_vendors): - print('Vendor %i: ' % i, - possible_schedules[selected_schedules[i].Value()]) + print( + 'Vendor %i: ' % i, possible_schedules[selected_schedules[i].Value()] + ) print() print('Statistics per day:') diff --git a/examples/contrib/volsay.py b/examples/contrib/volsay.py index af1efe9f6a5..e3038477cbc 100644 --- a/examples/contrib/volsay.py +++ b/examples/contrib/volsay.py @@ -13,13 +13,13 @@ # limitations under the License. """ - Volsay problem in Google or-tools. +Volsay problem in Google or-tools. - From the OPL model volsay.mod +From the OPL model volsay.mod - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.linear_solver import pywraplp @@ -29,7 +29,7 @@ def main(unused_argv): # Create the solver. # using GLPK - #solver = pywraplp.Solver('CoinsGridGLPK', + # solver = pywraplp.Solver('CoinsGridGLPK', # pywraplp.Solver.GLPK_LINEAR_PROGRAMMING) # Using CLP @@ -62,8 +62,12 @@ def main(unused_argv): print() print('objective = ', solver.Objective().Value()) print('Gas = ', Gas.SolutionValue(), 'ReducedCost =', Gas.ReducedCost()) - print('Chloride:', Chloride.SolutionValue(), 'ReducedCost =', - Chloride.ReducedCost()) + print( + 'Chloride:', + Chloride.SolutionValue(), + 'ReducedCost =', + Chloride.ReducedCost(), + ) if __name__ == '__main__': diff --git a/examples/contrib/volsay2.py b/examples/contrib/volsay2.py index ba3a3a58474..48ba4065b9a 100644 --- a/examples/contrib/volsay2.py +++ b/examples/contrib/volsay2.py @@ -13,14 +13,14 @@ # limitations under the License. """ - Volsay problem in Google or-tools. +Volsay problem in Google or-tools. - From the OPL model volsay.mod - Using arrays. +From the OPL model volsay.mod +Using arrays. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.linear_solver import pywraplp diff --git a/examples/contrib/volsay3.py b/examples/contrib/volsay3.py index df389eace7f..7c12d859641 100644 --- a/examples/contrib/volsay3.py +++ b/examples/contrib/volsay3.py @@ -13,14 +13,14 @@ # limitations under the License. """ - Volsay problem in Google or-tools. +Volsay problem in Google or-tools. - From the OPL model volsay.mod - Using arrays. +From the OPL model volsay.mod +Using arrays. - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from ortools.linear_solver import pywraplp @@ -59,13 +59,15 @@ def main(unused_argv): # for c in range(len(components)): solver.Add( - solver.Sum([demand[p][c] * production[p] - for p in range(len(products))]) <= stock[c]) + solver.Sum([demand[p][c] * production[p] for p in range(len(products))]) + <= stock[c] + ) # objective # Note: there is no support for solver.ScalProd in the LP/IP interface objective = solver.Maximize( - solver.Sum([production[p] * profit[p] for p in range(num_products)])) + solver.Sum([production[p] * profit[p] for p in range(num_products)]) + ) print('NumConstraints:', solver.NumConstraints()) print('NumVariables:', solver.NumVariables()) diff --git a/examples/contrib/vrptw_fixed_penalty.cs b/examples/contrib/vrptw_fixed_penalty.cs index 2d97d4b0dbe..52e74b5ff5b 100644 --- a/examples/contrib/vrptw_fixed_penalty.cs +++ b/examples/contrib/vrptw_fixed_penalty.cs @@ -14,6 +14,7 @@ using System; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; /// /// Vehicles Routing Problem (VRP) with Time Windows, with the difference that we'll add a fixed penalty for lateness, @@ -158,7 +159,7 @@ public static void Main(String[] args) // Setting first solution heuristic. RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // Solve the problem. diff --git a/examples/contrib/wedding_optimal_chart.py b/examples/contrib/wedding_optimal_chart.py index 76ab9bde51f..2879c91ee67 100644 --- a/examples/contrib/wedding_optimal_chart.py +++ b/examples/contrib/wedding_optimal_chart.py @@ -15,6 +15,7 @@ from ortools.constraint_solver import pywrapcp from ortools.constraint_solver import solver_parameters_pb2 + """Finding an optimal wedding seating chart. From @@ -61,30 +62,45 @@ def main(): # Connection matrix: who knows who, and how strong # is the relation - C = [[1, 50, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [50, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 50, 1, 1, 1, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 50, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 1, 50, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 50, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 1, 1, 1, 50, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 1, 1, 50, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 10, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 50, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1]] + C = [ + [1, 50, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [50, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 50, 1, 1, 1, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 50, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 50, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 50, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 50, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 50, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 10, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 50, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + ] # Names of the guests. B: Bride side, G: Groom side names = [ - "Deb (B)", "John (B)", "Martha (B)", "Travis (B)", "Allan (B)", - "Lois (B)", "Jayne (B)", "Brad (B)", "Abby (B)", "Mary Helen (G)", - "Lee (G)", "Annika (G)", "Carl (G)", "Colin (G)", "Shirley (G)", - "DeAnn (G)", "Lori (G)" + "Deb (B)", + "John (B)", + "Martha (B)", + "Travis (B)", + "Allan (B)", + "Lois (B)", + "Jayne (B)", + "Brad (B)", + "Abby (B)", + "Mary Helen (G)", + "Lee (G)", + "Annika (G)", + "Carl (G)", + "Colin (G)", + "Shirley (G)", + "DeAnn (G)", + "Lori (G)", ] m = len(C) @@ -108,10 +124,12 @@ def main(): # Constraints # for i in NRANGE: - minGuests = [(tables[j] == i) * (tables[k] == i) - for j in MRANGE - for k in MRANGE - if j < k and C[j][k] > 0] + minGuests = [ + (tables[j] == i) * (tables[k] == i) + for j in MRANGE + for k in MRANGE + if j < k and C[j][k] > 0 + ] solver.Add(solver.Sum(minGuests) >= b) maxGuests = [tables[j] == i for j in MRANGE] diff --git a/examples/contrib/who_killed_agatha.py b/examples/contrib/who_killed_agatha.py index 619263f32b7..d80f00dbccc 100644 --- a/examples/contrib/who_killed_agatha.py +++ b/examples/contrib/who_killed_agatha.py @@ -13,51 +13,51 @@ # limitations under the License. """ - Who killed agatha? (The Dreadsbury Mansion Murder Mystery) in Google CP - Solver. - - This is a standard benchmark for theorem proving. - - http://www.lsv.ens-cachan.fr/~goubault/H1.dist/H1.1/Doc/h1003.html - ''' - Someone in Dreadsbury Mansion killed Aunt Agatha. - Agatha, the butler, and Charles live in Dreadsbury Mansion, and - are the only ones to live there. A killer always hates, and is no - richer than his victim. Charles hates noone that Agatha hates. Agatha - hates everybody except the butler. The butler hates everyone not richer - than Aunt Agatha. The butler hates everyone whom Agatha hates. - Noone hates everyone. Who killed Agatha? - ''' - - Originally from F. J. Pelletier: - Seventy-five problems for testing automatic theorem provers. - Journal of Automated Reasoning, 2: 216, 1986. - - Note1: Since Google CP Solver/Pythons (currently) don't have - special support for logical operations on decision - variables (i.e. ->, <->, and, or, etc), this model - use some IP modeling tricks. - - Note2: There are 8 different solutions, all stating that Agatha - killed herself - - Compare with the following models: - * Choco : http://www.hakank.org/choco/WhoKilledAgatha.java - * Choco : http://www.hakank.org/choco/WhoKilledAgatha_element.java - * Comet : http://www.hakank.org/comet/who_killed_agatha.co - * ECLiPSE : http://www.hakank.org/eclipse/who_killed_agatha.ecl - * Gecode : http://www.hakank.org/gecode/who_killed_agatha.cpp - * JaCoP : http://www.hakank.org/JaCoP/WhoKilledAgatha.java - * JaCoP : http://www.hakank.org/JaCoP/WhoKilledAgatha_element.java - * MiniZinc: http://www.hakank.org/minizinc/who_killed_agatha.mzn - * Tailor/Essence': http://www.hakank.org/tailor/who_killed_agatha.eprime - * SICStus : http://hakank.org/sicstus/who_killed_agatha.pl - * Zinc :http://hakank.org/minizinc/who_killed_agatha.zinc - - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Who killed agatha? (The Dreadsbury Mansion Murder Mystery) in Google CP +Solver. + +This is a standard benchmark for theorem proving. + +http://www.lsv.ens-cachan.fr/~goubault/H1.dist/H1.1/Doc/h1003.html +''' +Someone in Dreadsbury Mansion killed Aunt Agatha. +Agatha, the butler, and Charles live in Dreadsbury Mansion, and +are the only ones to live there. A killer always hates, and is no +richer than his victim. Charles hates noone that Agatha hates. Agatha +hates everybody except the butler. The butler hates everyone not richer +than Aunt Agatha. The butler hates everyone whom Agatha hates. +Noone hates everyone. Who killed Agatha? +''' + +Originally from F. J. Pelletier: +Seventy-five problems for testing automatic theorem provers. +Journal of Automated Reasoning, 2: 216, 1986. + +Note1: Since Google CP Solver/Pythons (currently) don't have + special support for logical operations on decision + variables (i.e. ->, <->, and, or, etc), this model + use some IP modeling tricks. + +Note2: There are 8 different solutions, all stating that Agatha + killed herself + +Compare with the following models: +* Choco : http://www.hakank.org/choco/WhoKilledAgatha.java +* Choco : http://www.hakank.org/choco/WhoKilledAgatha_element.java +* Comet : http://www.hakank.org/comet/who_killed_agatha.co +* ECLiPSE : http://www.hakank.org/eclipse/who_killed_agatha.ecl +* Gecode : http://www.hakank.org/gecode/who_killed_agatha.cpp +* JaCoP : http://www.hakank.org/JaCoP/WhoKilledAgatha.java +* JaCoP : http://www.hakank.org/JaCoP/WhoKilledAgatha_element.java +* MiniZinc: http://www.hakank.org/minizinc/who_killed_agatha.mzn +* Tailor/Essence': http://www.hakank.org/tailor/who_killed_agatha.eprime +* SICStus : http://hakank.org/sicstus/who_killed_agatha.pl +* Zinc :http://hakank.org/minizinc/who_killed_agatha.zinc + + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ from collections import defaultdict @@ -178,8 +178,11 @@ def main(the_killers): solution.Add(richer_flat) # db: DecisionBuilder - db = solver.Phase(hates_flat + richer_flat, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + hates_flat + richer_flat, + solver.CHOOSE_FIRST_UNBOUND, + solver.ASSIGN_MIN_VALUE, + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/contrib/word_square.py b/examples/contrib/word_square.py index 9912a0e7c65..0fdd224dd06 100755 --- a/examples/contrib/word_square.py +++ b/examples/contrib/word_square.py @@ -13,27 +13,27 @@ # See the License for the specific language governing permissions and # limitations under the License. """ - Word square in Google CP Solver. - From http://en.wikipedia.org/wiki/Word_square - ''' - A word square is a special case of acrostic. It consists of a set of words, - all having the same number of letters as the total number of words (the - 'order' of the square); when the words are written out in a square grid - horizontally, the same set of words can be read vertically. - ''' - - Compare with the following models: - * MiniZinc: http://www.hakank.org/minizinc/word_square.mzn - * Comet : http://www.hakank.org/comet/word_square.co - * Choco : http://www.hakank.org/choco/WordSquare.java - * Gecode : http://www.hakank.org/gecode/word_square.cpp - * Gecode : http://www.hakank.org/gecode/word_square2.cpp - * JaCoP : http://www.hakank.org/JaCoP/WordSquare.java - * Zinc: http://hakank.org/minizinc/word_square.zinc - - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ +Word square in Google CP Solver. +From http://en.wikipedia.org/wiki/Word_square +''' +A word square is a special case of acrostic. It consists of a set of words, +all having the same number of letters as the total number of words (the +'order' of the square); when the words are written out in a square grid +horizontally, the same set of words can be read vertically. +''' + +Compare with the following models: +* MiniZinc: http://www.hakank.org/minizinc/word_square.mzn +* Comet : http://www.hakank.org/comet/word_square.co +* Choco : http://www.hakank.org/choco/WordSquare.java +* Gecode : http://www.hakank.org/gecode/word_square.cpp +* Gecode : http://www.hakank.org/gecode/word_square2.cpp +* JaCoP : http://www.hakank.org/JaCoP/WordSquare.java +* Zinc: http://hakank.org/minizinc/word_square.zinc + +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ """ import sys import re @@ -80,8 +80,9 @@ def main(words, word_len, num_answers=20): # We must use Element explicitly solver.Add( - solver.Element(A_flat, E[i] * word_len + - j) == solver.Element(A_flat, E[j] * word_len + i)) + solver.Element(A_flat, E[i] * word_len + j) + == solver.Element(A_flat, E[j] * word_len + i) + ) # # solution and search @@ -90,8 +91,9 @@ def main(words, word_len, num_answers=20): solution.Add(E) # db: DecisionBuilder - db = solver.Phase(E + A_flat, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + E + A_flat, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/contrib/xkcd.py b/examples/contrib/xkcd.py index 8eef7f6f167..e4df4ff9d1e 100644 --- a/examples/contrib/xkcd.py +++ b/examples/contrib/xkcd.py @@ -13,28 +13,28 @@ # limitations under the License. """ - xkcd problem (Knapsack) in Google CP Solver. +xkcd problem (Knapsack) in Google CP Solver. - http://xkcd.com/287/ +http://xkcd.com/287/ - Some amount (or none) of each dish should be ordered to give a total - of exact 15.05 +Some amount (or none) of each dish should be ordered to give a total +of exact 15.05 - Compare with the following models: - * Comet: http://www.hakank.org/comet/xkcd.co - * ECLiPSE: http://www.hakank.org/eclipse/xkcd.ecl - * Gecode: http://www.hakank.org/gecode/xkcd.cpp - * Gecode/R: http://www.hakank.org/gecode_r/xkcd.rb - * MiniZinc: http://www.hakank.org/minizinc/xkcd.mzn - * Tailor: http://www.hakank.org/minizinc/xkcd.mzn - * SICtus: http://www.hakank.org/sicstus/xkcd.pl - * Zinc: http://www.hakank.org/minizinc/xkcd.zinc +Compare with the following models: +* Comet: http://www.hakank.org/comet/xkcd.co +* ECLiPSE: http://www.hakank.org/eclipse/xkcd.ecl +* Gecode: http://www.hakank.org/gecode/xkcd.cpp +* Gecode/R: http://www.hakank.org/gecode_r/xkcd.rb +* MiniZinc: http://www.hakank.org/minizinc/xkcd.mzn +* Tailor: http://www.hakank.org/minizinc/xkcd.mzn +* SICtus: http://www.hakank.org/sicstus/xkcd.pl +* Zinc: http://www.hakank.org/minizinc/xkcd.zinc - This model was created by Hakan Kjellerstrand (hakank@gmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_cp_solver/ +This model was created by Hakan Kjellerstrand (hakank@gmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_cp_solver/ """ from ortools.constraint_solver import pywrapcp @@ -53,8 +53,12 @@ def main(): total = 1505 products = [ - "mixed fruit", "french fries", "side salad", "host wings", - "mozzarella sticks", "samples place" + "mixed fruit", + "french fries", + "side salad", + "host wings", + "mozzarella sticks", + "samples place", ] # declare variables @@ -80,8 +84,13 @@ def main(): # collector = solver.FirstSolutionCollector(solution) # search_log = solver.SearchLog(100, x[0]) solver.Solve( - solver.Phase([x[i] for i in range(num_prices)], solver.INT_VAR_SIMPLE, - solver.ASSIGN_MIN_VALUE), [collector]) + solver.Phase( + [x[i] for i in range(num_prices)], + solver.INT_VAR_SIMPLE, + solver.ASSIGN_MIN_VALUE, + ), + [collector], + ) num_solutions = collector.SolutionCount() print("num_solutions: ", num_solutions) diff --git a/examples/contrib/young_tableaux.py b/examples/contrib/young_tableaux.py index f47186ced03..f9ce3cfe781 100644 --- a/examples/contrib/young_tableaux.py +++ b/examples/contrib/young_tableaux.py @@ -128,8 +128,9 @@ def main(n=5): solution.Add(p) # db: DecisionBuilder - db = solver.Phase(x_flat + p, solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + x_flat + p, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) solver.NewSearch(db) num_solutions = 0 diff --git a/examples/cpp/BUILD.bazel b/examples/cpp/BUILD.bazel index a31911a5372..20d51c78f7e 100644 --- a/examples/cpp/BUILD.bazel +++ b/examples/cpp/BUILD.bazel @@ -604,7 +604,7 @@ cc_binary( srcs = ["random_tsp.cc"], deps = [ "//ortools/base", - "//ortools/constraint_solver:routing", + "//ortools/routing", "//ortools/util:random_engine", "@abseil-cpp//absl/strings", "@protobuf", @@ -618,7 +618,7 @@ cc_binary( "//ortools/base", "//ortools/base:file", "//ortools/base:mathutil", - "//ortools/constraint_solver:routing", + "//ortools/routing", "//ortools/routing/parsers:lilim_parser", "@abseil-cpp//absl/flags:flag", "@abseil-cpp//absl/strings", diff --git a/examples/cpp/pdptw.cc b/examples/cpp/pdptw.cc index 8dfeecf2583..7482286dc40 100644 --- a/examples/cpp/pdptw.cc +++ b/examples/cpp/pdptw.cc @@ -58,13 +58,15 @@ #include "ortools/base/init_google.h" #include "ortools/base/mathutil.h" #include "ortools/base/timer.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/lilim_parser.h" #include "ortools/routing/parsers/simple_graph.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" ABSL_FLAG(std::string, pdp_file, "", "File containing the Pickup and Delivery Problem to solve."); @@ -81,7 +83,8 @@ ABSL_FLAG(std::string, routing_model_parameters, "", "Text proto RoutingModelParameters (possibly partial) that will " "override the DefaultRoutingModelParameters()"); -namespace operations_research { +namespace operations_research::routing { +namespace { // Returns the list of variables to use for the Tabu metaheuristic. // The current list is: @@ -125,18 +128,17 @@ double ComputeScalingFactorFromCallback(const C& callback, int size) { return max_scaled_distance / max_value; } -void SetupModel(const routing::LiLimParser& parser, - const RoutingIndexManager& manager, RoutingModel* model, +void SetupModel(const LiLimParser& parser, const RoutingIndexManager& manager, + RoutingModel* model, RoutingSearchParameters* search_parameters) { const int64_t kPenalty = 100000000; const int64_t kFixedCost = 100000; const int num_nodes = parser.NumberOfNodes(); const int64_t horizon = - absl::c_max_element(parser.time_windows(), - [](const routing::SimpleTimeWindow& a, - const routing::SimpleTimeWindow& b) { - return a.end < b.end; - }) + absl::c_max_element( + parser.time_windows(), + [](const SimpleTimeWindow& a, + const SimpleTimeWindow& b) { return a.end < b.end; }) ->end; const double scaling_factor = ComputeScalingFactorFromCallback( [&parser](int64_t i, int64_t j) -> double { @@ -198,8 +200,7 @@ void SetupModel(const routing::LiLimParser& parser, model->AddPickupAndDelivery(index, delivery_index); } IntVar* const cumul = time_dimension.CumulVar(index); - const routing::SimpleTimeWindow& window = - parser.time_windows()[node]; + const SimpleTimeWindow& window = parser.time_windows()[node]; cumul->SetMin(MathUtil::Round(scaling_factor * window.start)); cumul->SetMax(MathUtil::Round(scaling_factor * window.end)); } @@ -244,8 +245,7 @@ void SetupModel(const routing::LiLimParser& parser, std::string VerboseOutput(const RoutingModel& model, const RoutingIndexManager& manager, const Assignment& assignment, - const routing::LiLimParser& parser, - double scaling_factor) { + const LiLimParser& parser, double scaling_factor) { std::string output; const RoutingDimension& time_dimension = model.GetDimensionOrDie("time"); const RoutingDimension& load_dimension = model.GetDimensionOrDie("demand"); @@ -292,13 +292,14 @@ std::string VerboseOutput(const RoutingModel& model, } return output; } +} // namespace // Builds and solves a model from a file in the format defined by Li & Lim // (https://www.sintef.no/projectweb/top/pdptw/li-lim-benchmark/documentation/). bool LoadAndSolve(absl::string_view pdp_file, const RoutingModelParameters& model_parameters, RoutingSearchParameters& search_parameters) { - routing::LiLimParser parser; + LiLimParser parser; if (!parser.LoadFile(pdp_file)) { return false; } @@ -355,23 +356,25 @@ bool LoadAndSolve(absl::string_view pdp_file, return false; } -} // namespace operations_research +} // namespace operations_research::routing + +namespace o_r = ::operations_research::routing; int main(int argc, char** argv) { absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); InitGoogle(argv[0], &argc, &argv, true); - operations_research::RoutingModelParameters model_parameters = - operations_research::DefaultRoutingModelParameters(); + o_r::RoutingModelParameters model_parameters = + o_r::DefaultRoutingModelParameters(); model_parameters.set_reduce_vehicle_cost_model( absl::GetFlag(FLAGS_reduce_vehicle_cost_model)); CHECK(google::protobuf::TextFormat::MergeFromString( absl::GetFlag(FLAGS_routing_model_parameters), &model_parameters)); - operations_research::RoutingSearchParameters search_parameters = - operations_research::DefaultRoutingSearchParameters(); + o_r::RoutingSearchParameters search_parameters = + o_r::DefaultRoutingSearchParameters(); CHECK(google::protobuf::TextFormat::MergeFromString( absl::GetFlag(FLAGS_routing_search_parameters), &search_parameters)); - if (!operations_research::LoadAndSolve(absl::GetFlag(FLAGS_pdp_file), - model_parameters, search_parameters)) { + if (!o_r::LoadAndSolve(absl::GetFlag(FLAGS_pdp_file), model_parameters, + search_parameters)) { LOG(INFO) << "Error solving " << absl::GetFlag(FLAGS_pdp_file); } return EXIT_SUCCESS; diff --git a/examples/cpp/random_tsp.cc b/examples/cpp/random_tsp.cc index ca044f4bf84..d6f4e05c8eb 100644 --- a/examples/cpp/random_tsp.cc +++ b/examples/cpp/random_tsp.cc @@ -41,10 +41,10 @@ #include "absl/strings/str_cat.h" #include "google/protobuf/text_format.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" #include "ortools/util/random_engine.h" ABSL_FLAG(int, tsp_size, 10, "Size of Traveling Salesman Problem instance."); @@ -61,7 +61,7 @@ ABSL_FLAG(std::string, routing_search_parameters, "Text proto RoutingSearchParameters (possibly partial) that will " "override the DefaultRoutingSearchParameters()"); -namespace operations_research { +namespace operations_research::routing { // Random seed generator. int32_t GetSeed() { @@ -187,13 +187,13 @@ void Tsp() { LOG(INFO) << "Specify an instance size greater than 0."; } } -} // namespace operations_research +} // namespace operations_research::routing int main(int argc, char** argv) { absl::InitializeLog(); absl::EnableLogPrefix(false); absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); absl::ParseCommandLine(argc, argv); - operations_research::Tsp(); + operations_research::routing::Tsp(); return EXIT_SUCCESS; } diff --git a/examples/dotnet/cscvrptw.cs b/examples/dotnet/cscvrptw.cs index 61f13c836ef..5ce35d8ed19 100644 --- a/examples/dotnet/cscvrptw.cs +++ b/examples/dotnet/cscvrptw.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; /// /// Sample showing how to model and solve a capacitated vehicle routing @@ -271,8 +272,7 @@ private void Solve(int number_of_orders, int number_of_vehicles) } // Solving - RoutingSearchParameters search_parameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters search_parameters = RoutingGlobals.DefaultRoutingSearchParameters(); search_parameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.AllUnperformed; Console.WriteLine("Search..."); diff --git a/examples/dotnet/cstsp.cs b/examples/dotnet/cstsp.cs index e5eb55daab0..f42a3ecf4a1 100644 --- a/examples/dotnet/cstsp.cs +++ b/examples/dotnet/cstsp.cs @@ -14,6 +14,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; class Tsp { @@ -77,8 +78,7 @@ static void Solve(int size, int forbidden, int seed) size + 1, size + 1, true, "dummy"); // Solve, returns a solution if any (owned by RoutingModel). - RoutingSearchParameters search_parameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters search_parameters = RoutingGlobals.DefaultRoutingSearchParameters(); // Setting first solution heuristic (cheapest addition). search_parameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; diff --git a/examples/java/CapacitatedVehicleRoutingProblemWithTimeWindows.java b/examples/java/CapacitatedVehicleRoutingProblemWithTimeWindows.java index 7f3dae049c4..0344fb8c73b 100644 --- a/examples/java/CapacitatedVehicleRoutingProblemWithTimeWindows.java +++ b/examples/java/CapacitatedVehicleRoutingProblemWithTimeWindows.java @@ -15,14 +15,14 @@ import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; import com.google.ortools.constraintsolver.IntVar; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.RoutingSearchStatus; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; +import com.google.ortools.routing.RoutingSearchStatus; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -281,7 +281,7 @@ public long applyAsLong(long fromIndex) { // Solving RoutingSearchParameters parameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.ALL_UNPERFORMED) .build(); diff --git a/examples/java/RandomTsp.java b/examples/java/RandomTsp.java index 29bf624e3c7..e7c1e279135 100644 --- a/examples/java/RandomTsp.java +++ b/examples/java/RandomTsp.java @@ -16,11 +16,11 @@ import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; +import com.google.ortools.routing.Globals; // import java.io.*; // import java.text.*; // import java.util.*; @@ -92,7 +92,7 @@ static void solve(int size, int forbidden, int seed) { // Solve, returns a solution if any (owned by RoutingModel). RoutingSearchParameters search_parameters = RoutingSearchParameters.newBuilder() - .mergeFrom(main.defaultRoutingSearchParameters()) + .mergeFrom(Globals.defaultRoutingSearchParameters()) .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/examples/notebook/examples/cvrptw_plot.ipynb b/examples/notebook/examples/cvrptw_plot.ipynb index 5f7928f82fd..fff0ae13400 100644 --- a/examples/notebook/examples/cvrptw_plot.ipynb +++ b/examples/notebook/examples/cvrptw_plot.ipynb @@ -108,7 +108,7 @@ "from matplotlib import pyplot as plt\n", "from collections import namedtuple\n", "from ortools.constraint_solver import pywrapcp\n", - "from ortools.constraint_solver import routing_enums_pb2\n", + "from ortools.routing import enums_pb2\n", "from datetime import datetime, timedelta\n", "\n", "\n", @@ -693,7 +693,7 @@ " parameters = pywrapcp.DefaultRoutingSearchParameters()\n", " # Setting first solution heuristic (cheapest addition).\n", " parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n", " # Routing: forbids use of TSPOpt neighborhood, (this is the default behaviour)\n", " parameters.local_search_operators.use_tsp_opt = pywrapcp.BOOL_FALSE\n", " # Disabling Large Neighborhood Search, (this is the default behaviour)\n", diff --git a/examples/notebook/examples/prize_collecting_tsp.ipynb b/examples/notebook/examples/prize_collecting_tsp.ipynb index 1f673e1e5b1..2a7956340e6 100644 --- a/examples/notebook/examples/prize_collecting_tsp.ipynb +++ b/examples/notebook/examples/prize_collecting_tsp.ipynb @@ -82,8 +82,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "DISTANCE_MATRIX = [\n", " [0, 10938, 4542, 2835, 29441, 2171, 1611, 9208, 9528, 11111, 16120, 22606, 22127, 20627, 21246, 23387, 16697, 33609, 26184, 24772, 22644, 20655, 30492, 23296, 32979, 18141, 19248, 17129, 17192, 15645, 12658, 11210, 12094, 13175, 18162, 4968, 12308, 10084, 13026, 15056],\n", @@ -175,13 +175,13 @@ " all_nodes = range(num_nodes)\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " num_nodes,\n", " num_vehicles,\n", " depot)\n", "\n", " # Create routing model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -214,11 +214,11 @@ " VISIT_VALUES[node])\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)\n", " search_parameters.time_limit.FromSeconds(15)\n", " #search_parameters.log_search = True\n", "\n", diff --git a/examples/notebook/examples/prize_collecting_vrp.ipynb b/examples/notebook/examples/prize_collecting_vrp.ipynb index 1d0da49a882..2fb39f49093 100644 --- a/examples/notebook/examples/prize_collecting_vrp.ipynb +++ b/examples/notebook/examples/prize_collecting_vrp.ipynb @@ -82,8 +82,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "DISTANCE_MATRIX = [\n", " [0, 10938, 4542, 2835, 29441, 2171, 1611, 9208, 9528, 11111, 16120, 22606, 22127, 20627, 21246, 23387, 16697, 33609, 26184, 24772, 22644, 20655, 30492, 23296, 32979, 18141, 19248, 17129, 17192, 15645, 12658, 11210, 12094, 13175, 18162, 4968, 12308, 10084, 13026, 15056],\n", @@ -151,6 +151,8 @@ " total_distance = 0\n", " total_value_collected = 0\n", " for v in range(manager.GetNumberOfVehicles()):\n", + " if not routing.IsVehicleUsed(assignment, v):\n", + " continue\n", " index = routing.Start(v)\n", " plan_output = f'Route for vehicle {v}:\\n'\n", " route_distance = 0\n", @@ -181,13 +183,13 @@ " all_nodes = range(num_nodes)\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " num_nodes,\n", " num_vehicles,\n", " depot)\n", "\n", " # Create routing model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -220,11 +222,11 @@ " VISIT_VALUES[node])\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)\n", " search_parameters.time_limit.FromSeconds(15)\n", " #search_parameters.log_search = True\n", "\n", diff --git a/examples/notebook/examples/random_tsp.ipynb b/examples/notebook/examples/random_tsp.ipynb index b07e34c39cf..62e29e9e189 100644 --- a/examples/notebook/examples/random_tsp.ipynb +++ b/examples/notebook/examples/random_tsp.ipynb @@ -96,8 +96,8 @@ "from functools import partial\n", "import random\n", "\n", - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "parser = argparse.ArgumentParser()\n", "\n", @@ -161,12 +161,12 @@ " # Second argument = 1 to build a single tour (it's a TSP).\n", " # Nodes are indexed from 0 to args_tsp_size - 1, by default the start of\n", " # the route is node 0.\n", - " manager = pywrapcp.RoutingIndexManager(args.tsp_size, 1, 0)\n", - " routing = pywrapcp.RoutingModel(manager)\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " manager = pywraprouting.RoutingIndexManager(args.tsp_size, 1, 0)\n", + " routing = pywraprouting.RoutingModel(manager)\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " # Setting first solution heuristic (cheapest addition).\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)\n", "\n", " # Setting the cost function.\n", " # Put a callback to the distance accessor here. The callback takes two\n", diff --git a/examples/notebook/examples/transit_time.ipynb b/examples/notebook/examples/transit_time.ipynb index 8f75f9028ef..f8fa14e7939 100644 --- a/examples/notebook/examples/transit_time.ipynb +++ b/examples/notebook/examples/transit_time.ipynb @@ -89,7 +89,6 @@ "outputs": [], "source": [ "from ortools.constraint_solver import pywrapcp\n", - "from ortools.constraint_solver import routing_enums_pb2\n", "\n", "\n", "###########################\n", diff --git a/examples/notebook/constraint_solver/cvrp_reload.ipynb b/examples/notebook/routing/cvrp_reload.ipynb similarity index 96% rename from examples/notebook/constraint_solver/cvrp_reload.ipynb rename to examples/notebook/routing/cvrp_reload.ipynb index 2e70d31c2e0..3d999c4ce1c 100644 --- a/examples/notebook/constraint_solver/cvrp_reload.ipynb +++ b/examples/notebook/routing/cvrp_reload.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -111,8 +111,8 @@ "source": [ "from functools import partial\n", "\n", - "from ortools.constraint_solver import pywrapcp\n", - "from ortools.constraint_solver import routing_enums_pb2\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "###########################\n", @@ -446,12 +446,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " data[\"num_locations\"], data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Define weight of each edge\n", " distance_evaluator_index = routing.RegisterTransitCallback(\n", @@ -475,12 +475,12 @@ " add_time_window_constraints(routing, manager, data, time_evaluator_index)\n", "\n", " # Setting first solution heuristic (cheapest addition).\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " ) # pylint: disable=no-member\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " search_parameters.time_limit.FromSeconds(3)\n", "\n", diff --git a/examples/notebook/routing/cvrptw.ipynb b/examples/notebook/routing/cvrptw.ipynb new file mode 100644 index 00000000000..421bd3000dd --- /dev/null +++ b/examples/notebook/routing/cvrptw.ipynb @@ -0,0 +1,340 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "google", + "metadata": {}, + "source": [ + "##### Copyright 2025 Google LLC." + ] + }, + { + "cell_type": "markdown", + "id": "apache", + "metadata": {}, + "source": [ + "Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "you may not use this file except in compliance with the License.\n", + "You may obtain a copy of the License at\n", + "\n", + " http://www.apache.org/licenses/LICENSE-2.0\n", + "\n", + "Unless required by applicable law or agreed to in writing, software\n", + "distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "See the License for the specific language governing permissions and\n", + "limitations under the License.\n" + ] + }, + { + "cell_type": "markdown", + "id": "basename", + "metadata": {}, + "source": [ + "# cvrptw" + ] + }, + { + "cell_type": "markdown", + "id": "link", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "
\n", + "Run in Google Colab\n", + "\n", + "View source on GitHub\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "doc", + "metadata": {}, + "source": [ + "First, you must install [ortools](https://pypi.org/project/ortools/) package in this colab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "install", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install ortools" + ] + }, + { + "cell_type": "markdown", + "id": "description", + "metadata": {}, + "source": [ + "\n", + "Capacited Vehicles Routing Problem with Time Windows (CVRPTW).\n", + "\n", + "This is a sample using the routing library python wrapper to solve a VRP\n", + "problem.\n", + "A description of the problem can be found here:\n", + "http://en.wikipedia.org/wiki/Vehicle_routing_problem.\n", + "\n", + "Distances are in meters.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "code", + "metadata": {}, + "outputs": [], + "source": [ + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", + "\n", + "FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy\n", + "LocalSearchMetaheuristic = enums_pb2.LocalSearchMetaheuristic\n", + "RoutingSearchStatus = enums_pb2.RoutingSearchStatus\n", + "\n", + "\n", + "def create_data_model():\n", + " \"\"\"Stores the data for the problem.\"\"\"\n", + " data = {}\n", + " data[\"distance_matrix\"] = [\n", + " # fmt: off\n", + " [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662],\n", + " [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210],\n", + " [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754],\n", + " [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358],\n", + " [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244],\n", + " [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708],\n", + " [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480],\n", + " [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856],\n", + " [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514],\n", + " [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468],\n", + " [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354],\n", + " [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844],\n", + " [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730],\n", + " [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536],\n", + " [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194],\n", + " [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798],\n", + " [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0],\n", + " # fmt: on\n", + " ]\n", + " data[\"time_matrix\"] = [\n", + " [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7],\n", + " [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14],\n", + " [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9],\n", + " [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16],\n", + " [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14],\n", + " [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8],\n", + " [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5],\n", + " [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10],\n", + " [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6],\n", + " [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5],\n", + " [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4],\n", + " [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10],\n", + " [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8],\n", + " [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6],\n", + " [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2],\n", + " [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9],\n", + " [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0],\n", + " ]\n", + " data[\"time_windows\"] = [\n", + " (0, 30), # depot\n", + " (7, 12), # 1\n", + " (10, 15), # 2\n", + " (16, 18), # 3\n", + " (10, 13), # 4\n", + " (0, 5), # 5\n", + " (5, 10), # 6\n", + " (0, 4), # 7\n", + " (5, 10), # 8\n", + " (0, 3), # 9\n", + " (10, 16), # 10\n", + " (10, 15), # 11\n", + " (0, 5), # 12\n", + " (5, 10), # 13\n", + " (7, 8), # 14\n", + " (10, 15), # 15\n", + " (11, 15), # 16\n", + " ]\n", + " assert len(data[\"distance_matrix\"]) == len(data[\"time_matrix\"])\n", + " assert len(data[\"time_matrix\"]) == len(data[\"time_windows\"])\n", + " data[\"demands\"] = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8]\n", + " assert len(data[\"distance_matrix\"]) == len(data[\"demands\"])\n", + " data[\"vehicle_capacities\"] = [15, 15, 15, 15]\n", + " data[\"num_vehicles\"] = len(data[\"vehicle_capacities\"])\n", + " data[\"depot\"] = 0\n", + " return data\n", + "\n", + "\n", + "def print_solution(manager, routing, solution):\n", + " \"\"\"Prints solution on console.\"\"\"\n", + " status = routing.status()\n", + " print(f\"Status: {RoutingSearchStatus.Value.Name(status)}\")\n", + " if (\n", + " status != RoutingSearchStatus.ROUTING_OPTIMAL\n", + " and status != RoutingSearchStatus.ROUTING_SUCCESS\n", + " ):\n", + " print(\"No solution found!\")\n", + " return\n", + " print(f\"Objective: {solution.ObjectiveValue()}\")\n", + " time_dimension = routing.GetDimensionOrDie(\"Time\")\n", + " capacity_dimension = routing.GetDimensionOrDie(\"Capacity\")\n", + " total_distance = 0\n", + " total_time = 0\n", + " total_load = 0\n", + " for vehicle_id in range(manager.GetNumberOfVehicles()):\n", + " if not routing.IsVehicleUsed(solution, vehicle_id):\n", + " continue\n", + " index = routing.Start(vehicle_id)\n", + " plan_output = f\"Route for vehicle {vehicle_id}:\\n\"\n", + " route_distance = 0\n", + " while not routing.IsEnd(index):\n", + " time_var = time_dimension.CumulVar(index)\n", + " capacity_var = capacity_dimension.CumulVar(index)\n", + " plan_output += (\n", + " f\"Node_{manager.IndexToNode(index)}\"\n", + " f\" TW:[{time_var.Min()},{time_var.Max()}]\"\n", + " f\" Time({solution.Min(time_var)},{solution.Max(time_var)})\"\n", + " f\" Load({solution.Value(capacity_var)}/{capacity_var.Max()})\"\n", + " \" -> \"\n", + " )\n", + " previous_index = index\n", + " index = solution.Value(routing.NextVar(index))\n", + " route_distance += routing.GetArcCostForVehicle(\n", + " previous_index, index, vehicle_id\n", + " )\n", + " time_var = time_dimension.CumulVar(index)\n", + " capacity_var = capacity_dimension.CumulVar(index)\n", + " plan_output += (\n", + " f\"Node_{manager.IndexToNode(index)}\"\n", + " f\" Time({solution.Min(time_var)},{solution.Max(time_var)})\"\n", + " f\" Load({solution.Value(capacity_var)}/{capacity_var.Max()})\"\n", + " \"\\n\"\n", + " )\n", + " plan_output += f\"Distance of the route: {route_distance}m\\n\"\n", + " plan_output += f\"Time of the route: {solution.Min(time_var)}min\\n\"\n", + " plan_output += f\"Load of the route: {solution.Value(capacity_var)}\\n\"\n", + " print(plan_output)\n", + " total_distance += route_distance\n", + " total_time += solution.Min(time_var)\n", + " total_load += solution.Value(capacity_var)\n", + " print(f\"Total distance of all routes: {total_distance}m\")\n", + " print(f\"Total time of all routes: {total_time}min\")\n", + " print(f\"Total load of all routes: {total_load}\")\n", + "\n", + "\n", + "def main():\n", + " \"\"\"Entry point of the program.\"\"\"\n", + " # Instantiate the data problem.\n", + " data = create_data_model()\n", + "\n", + " # Create the routing index manager.\n", + " manager = pywraprouting.RoutingIndexManager(\n", + " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", + " )\n", + "\n", + " # Create Routing Model.\n", + " routing = pywraprouting.RoutingModel(manager)\n", + "\n", + " # Create and register a distance transit callback.\n", + " def distance_callback(from_index, to_index):\n", + " \"\"\"Returns the distance between the two nodes.\"\"\"\n", + " # Convert from routing variable Index to distance matrix NodeIndex.\n", + " from_node = manager.IndexToNode(from_index)\n", + " to_node = manager.IndexToNode(to_index)\n", + " return data[\"distance_matrix\"][from_node][to_node]\n", + "\n", + " distance_callback_index = routing.RegisterTransitCallback(distance_callback)\n", + "\n", + " # Define cost of each arc.\n", + " routing.SetArcCostEvaluatorOfAllVehicles(distance_callback_index)\n", + "\n", + " # Add Time Windows constraint.\n", + " def time_callback(from_index, to_index):\n", + " \"\"\"Returns the travel time between the two nodes.\"\"\"\n", + " # Convert from routing variable Index to time matrix NodeIndex.\n", + " from_node = manager.IndexToNode(from_index)\n", + " to_node = manager.IndexToNode(to_index)\n", + " return data[\"time_matrix\"][from_node][to_node]\n", + "\n", + " time_callback_index = routing.RegisterTransitCallback(time_callback)\n", + " routing.AddDimension(\n", + " time_callback_index,\n", + " 30, # allow waiting time\n", + " 30, # maximum time per vehicle\n", + " False, # Don't force start cumul to zero.\n", + " \"Time\",\n", + " )\n", + " time_dimension = routing.GetDimensionOrDie(\"Time\")\n", + " # Add time window constraints for each location except depot.\n", + " for location_idx, time_window in enumerate(data[\"time_windows\"]):\n", + " if location_idx == data[\"depot\"]:\n", + " continue\n", + " index = manager.NodeToIndex(location_idx)\n", + " time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1])\n", + " # Add time window constraints for each vehicle start node.\n", + " depot_idx = data[\"depot\"]\n", + " for vehicle_id in range(data[\"num_vehicles\"]):\n", + " index = routing.Start(vehicle_id)\n", + " time_dimension.CumulVar(index).SetRange(\n", + " data[\"time_windows\"][depot_idx][0], data[\"time_windows\"][depot_idx][1]\n", + " )\n", + "\n", + " # Instantiate route start and end times to produce feasible times.\n", + " for i in range(data[\"num_vehicles\"]):\n", + " routing.AddVariableMinimizedByFinalizer(\n", + " time_dimension.CumulVar(routing.Start(i))\n", + " )\n", + " routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(i)))\n", + "\n", + " # Add Capacity constraint.\n", + " def demand_callback(from_index):\n", + " \"\"\"Returns the demand of the node.\"\"\"\n", + " # Convert from routing variable Index to demands NodeIndex.\n", + " from_node = manager.IndexToNode(from_index)\n", + " return data[\"demands\"][from_node]\n", + "\n", + " demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)\n", + " routing.AddDimensionWithVehicleCapacity(\n", + " demand_callback_index,\n", + " 0, # null capacity slack\n", + " data[\"vehicle_capacities\"], # vehicle maximum capacities\n", + " True, # start cumul to zero\n", + " \"Capacity\",\n", + " )\n", + "\n", + " # Setting first solution heuristic.\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", + " search_parameters.first_solution_strategy = (\n", + " FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION\n", + " )\n", + " search_parameters.local_search_metaheuristic = (\n", + " LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " )\n", + " search_parameters.time_limit.FromSeconds(3)\n", + "\n", + " # Solve the problem.\n", + " solution = routing.SolveWithParameters(search_parameters)\n", + "\n", + " # Print solution on console.\n", + " print_solution(manager, routing, solution)\n", + "\n", + "\n", + "main()\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebook/constraint_solver/cvrptw_break.ipynb b/examples/notebook/routing/cvrptw_break.ipynb similarity index 95% rename from examples/notebook/constraint_solver/cvrptw_break.ipynb rename to examples/notebook/routing/cvrptw_break.ipynb index ad4a747083e..cf0f75be8ca 100644 --- a/examples/notebook/constraint_solver/cvrptw_break.ipynb +++ b/examples/notebook/routing/cvrptw_break.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -92,9 +92,8 @@ "outputs": [], "source": [ "import functools\n", - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -350,12 +349,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " data[\"numlocations_\"], data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Define weight of each edge\n", " distance_evaluator_index = routing.RegisterTransitCallback(\n", @@ -401,9 +400,9 @@ " )\n", "\n", " # Setting first solution heuristic (cheapest addition).\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " ) # pylint: disable=no-member\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/simple_routing_program.ipynb b/examples/notebook/routing/simple_routing_program.ipynb similarity index 83% rename from examples/notebook/constraint_solver/simple_routing_program.ipynb rename to examples/notebook/routing/simple_routing_program.ipynb index 57d54942cd5..d00e47206a0 100644 --- a/examples/notebook/constraint_solver/simple_routing_program.ipynb +++ b/examples/notebook/routing/simple_routing_program.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def main():\n", @@ -96,10 +95,10 @@ " depot = 0\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(num_locations, num_vehicles, depot)\n", + " manager = pywraprouting.RoutingIndexManager(num_locations, num_vehicles, depot)\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -115,9 +114,9 @@ " routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " ) # pylint: disable=no-member\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/routing/tsp.ipynb b/examples/notebook/routing/tsp.ipynb new file mode 100644 index 00000000000..21356a6db6c --- /dev/null +++ b/examples/notebook/routing/tsp.ipynb @@ -0,0 +1,217 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "google", + "metadata": {}, + "source": [ + "##### Copyright 2025 Google LLC." + ] + }, + { + "cell_type": "markdown", + "id": "apache", + "metadata": {}, + "source": [ + "Licensed under the Apache License, Version 2.0 (the \"License\");\n", + "you may not use this file except in compliance with the License.\n", + "You may obtain a copy of the License at\n", + "\n", + " http://www.apache.org/licenses/LICENSE-2.0\n", + "\n", + "Unless required by applicable law or agreed to in writing, software\n", + "distributed under the License is distributed on an \"AS IS\" BASIS,\n", + "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n", + "See the License for the specific language governing permissions and\n", + "limitations under the License.\n" + ] + }, + { + "cell_type": "markdown", + "id": "basename", + "metadata": {}, + "source": [ + "# tsp" + ] + }, + { + "cell_type": "markdown", + "id": "link", + "metadata": {}, + "source": [ + "\n", + "\n", + "\n", + "
\n", + "Run in Google Colab\n", + "\n", + "View source on GitHub\n", + "
" + ] + }, + { + "cell_type": "markdown", + "id": "doc", + "metadata": {}, + "source": [ + "First, you must install [ortools](https://pypi.org/project/ortools/) package in this colab." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "install", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install ortools" + ] + }, + { + "cell_type": "markdown", + "id": "description", + "metadata": {}, + "source": [ + "\n", + "Simple Travelling Salesman Problem.\n", + "\n", + "A description of the problem can be found here:\n", + "http://en.wikipedia.org/wiki/Travelling_salesperson_problem.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "code", + "metadata": {}, + "outputs": [], + "source": [ + "from ortools.routing import enums_pb2\n", + "from ortools.routing import parameters_pb2\n", + "from ortools.routing.python import model\n", + "\n", + "FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy\n", + "RoutingSearchStatus = enums_pb2.RoutingSearchStatus\n", + "\n", + "\n", + "def create_data_model():\n", + " \"\"\"Stores the data for the problem.\"\"\"\n", + " data = {}\n", + " # Locations in block units\n", + " locations = [\n", + " # fmt:off\n", + " (4, 4), # depot\n", + " (2, 0), (8, 0), # locations to visit\n", + " (0, 1), (1, 1),\n", + " (5, 2), (7, 2),\n", + " (3, 3), (6, 3),\n", + " (5, 5), (8, 5),\n", + " (1, 6), (2, 6),\n", + " (3, 7), (6, 7),\n", + " (0, 8), (7, 8)\n", + " # fmt:on\n", + " ]\n", + " # Convert locations in meters using a city block dimension of 114m x 80m.\n", + " data[\"locations\"] = [(l[0] * 114, l[1] * 80) for l in locations]\n", + " data[\"num_vehicles\"] = 1\n", + " data[\"depot\"] = 0\n", + " return data\n", + "\n", + "\n", + "def create_distance_callback(data, manager):\n", + " \"\"\"Creates callback to return distance between points.\"\"\"\n", + " distances_ = {}\n", + " index_manager_ = manager\n", + " # precompute distance between location to have distance callback in O(1)\n", + " for from_counter, from_node in enumerate(data[\"locations\"]):\n", + " distances_[from_counter] = {}\n", + " for to_counter, to_node in enumerate(data[\"locations\"]):\n", + " if from_counter == to_counter:\n", + " distances_[from_counter][to_counter] = 0\n", + " else:\n", + " distances_[from_counter][to_counter] = abs(\n", + " from_node[0] - to_node[0]\n", + " ) + abs(from_node[1] - to_node[1])\n", + "\n", + " def distance_callback(from_index, to_index):\n", + " \"\"\"Returns the manhattan distance between the two nodes.\"\"\"\n", + " # Convert from routing variable Index to distance matrix NodeIndex.\n", + " from_node = index_manager_.index_to_node(from_index)\n", + " to_node = index_manager_.index_to_node(to_index)\n", + " return distances_[from_node][to_node]\n", + "\n", + " return distance_callback\n", + "\n", + "\n", + "def print_solution(manager, routing, solution):\n", + " \"\"\"Prints assignment on console.\"\"\"\n", + " status = routing.status()\n", + " print(f\"Status: {RoutingSearchStatus.Value.Name(status)}\")\n", + " if (\n", + " status != RoutingSearchStatus.ROUTING_OPTIMAL\n", + " and status != RoutingSearchStatus.ROUTING_SUCCESS\n", + " ):\n", + " print(\"No solution found!\")\n", + " return\n", + " print(f\"Objective: {solution.objective_value()}\")\n", + " index = routing.start(0)\n", + " plan_output = \"Route for vehicle 0:\\n\"\n", + " route_distance = 0\n", + " while not routing.is_end(index):\n", + " plan_output += f\" {manager.index_to_node(index)} ->\"\n", + " previous_index = index\n", + " index = solution.value(routing.next_var(index))\n", + " route_distance += routing.get_arc_cost_for_vehicle(previous_index, index, 0)\n", + " plan_output += f\" {manager.index_to_node(index)}\\n\"\n", + " plan_output += f\"Distance of the route: {route_distance}m\\n\"\n", + " print(plan_output)\n", + "\n", + "\n", + "def main():\n", + " \"\"\"Entry point of the program.\"\"\"\n", + " # Instantiate the data problem.\n", + " data = create_data_model()\n", + "\n", + " # Create the routing index manager.\n", + " manager = model.RoutingIndexManager(\n", + " len(data[\"locations\"]), data[\"num_vehicles\"], data[\"depot\"]\n", + " )\n", + "\n", + " # Create Routing Model.\n", + " routing = model.RoutingModel(manager)\n", + "\n", + " # Create and register a transit callback.\n", + " distance_callback = create_distance_callback(data, manager)\n", + " transit_callback_index = routing.register_transit_callback(distance_callback)\n", + "\n", + " # Define cost of each arc.\n", + " routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index)\n", + "\n", + " # Setting first solution heuristic.\n", + " search_parameters: parameters_pb2.RoutingSearchParameters = (\n", + " model.default_routing_search_parameters()\n", + " )\n", + " search_parameters.first_solution_strategy = FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + "\n", + " # Solve the problem.\n", + " solution = routing.solve()\n", + " # solution = routing.solve_with_parameters(search_parameters)\n", + "\n", + " # Print solution on console.\n", + " print_solution(manager, routing, solution)\n", + "\n", + "\n", + "main()\n", + "\n" + ] + } + ], + "metadata": { + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebook/constraint_solver/tsp_circuit_board.ipynb b/examples/notebook/routing/tsp_circuit_board.ipynb similarity index 91% rename from examples/notebook/constraint_solver/tsp_circuit_board.ipynb rename to examples/notebook/routing/tsp_circuit_board.ipynb index 7cadec642c2..1fe1bf4bfa8 100644 --- a/examples/notebook/constraint_solver/tsp_circuit_board.ipynb +++ b/examples/notebook/routing/tsp_circuit_board.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -84,9 +84,8 @@ "outputs": [], "source": [ "import math\n", - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -177,8 +176,8 @@ " index = solution.Value(routing.NextVar(index))\n", " route_distance += routing.GetArcCostForVehicle(previous_index, index, 0)\n", " plan_output += f\" {manager.IndexToNode(index)}\\n\"\n", + " plan_output += f\"Route distance: {route_distance}mm\\n\"\n", " print(plan_output)\n", - " plan_output += f\"Objective: {route_distance}m\\n\"\n", "\n", "\n", "def main():\n", @@ -187,12 +186,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"locations\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " distance_matrix = compute_euclidean_distance_matrix(data[\"locations\"])\n", "\n", @@ -209,9 +208,9 @@ " routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/tsp_cities.ipynb b/examples/notebook/routing/tsp_cities.ipynb similarity index 88% rename from examples/notebook/constraint_solver/tsp_cities.ipynb rename to examples/notebook/routing/tsp_cities.ipynb index 1f05f969441..b9cac5110a1 100644 --- a/examples/notebook/constraint_solver/tsp_cities.ipynb +++ b/examples/notebook/routing/tsp_cities.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -133,12 +132,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", "\n", " def distance_callback(from_index, to_index):\n", @@ -154,9 +153,9 @@ " routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/tsp_distance_matrix.ipynb b/examples/notebook/routing/tsp_distance_matrix.ipynb similarity index 89% rename from examples/notebook/constraint_solver/tsp_distance_matrix.ipynb rename to examples/notebook/routing/tsp_distance_matrix.ipynb index da4b7f4e199..1fc7907caf3 100644 --- a/examples/notebook/constraint_solver/tsp_distance_matrix.ipynb +++ b/examples/notebook/routing/tsp_distance_matrix.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -139,12 +138,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -160,9 +159,9 @@ " routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/tsp.ipynb b/examples/notebook/routing/tsp_legacy.ipynb similarity index 79% rename from examples/notebook/constraint_solver/tsp.ipynb rename to examples/notebook/routing/tsp_legacy.ipynb index 6021ea54fe2..3b35500736b 100644 --- a/examples/notebook/constraint_solver/tsp.ipynb +++ b/examples/notebook/routing/tsp_legacy.ipynb @@ -31,7 +31,7 @@ "id": "basename", "metadata": {}, "source": [ - "# tsp" + "# tsp_legacy" ] }, { @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -87,9 +87,11 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", + "FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy\n", + "RoutingSearchStatus = enums_pb2.RoutingSearchStatus\n", "\n", "\n", "def create_data_model():\n", @@ -141,16 +143,24 @@ " return distance_callback\n", "\n", "\n", - "def print_solution(manager, routing, assignment):\n", + "def print_solution(manager, routing, solution):\n", " \"\"\"Prints assignment on console.\"\"\"\n", - " print(f\"Objective: {assignment.ObjectiveValue()}\")\n", + " status = routing.status()\n", + " print(f\"Status: {RoutingSearchStatus.Value.Name(status)}\")\n", + " if (\n", + " status != RoutingSearchStatus.ROUTING_OPTIMAL\n", + " and status != RoutingSearchStatus.ROUTING_SUCCESS\n", + " ):\n", + " print(\"No solution found!\")\n", + " return\n", + " print(f\"Objective: {solution.ObjectiveValue()}\")\n", " index = routing.Start(0)\n", " plan_output = \"Route for vehicle 0:\\n\"\n", " route_distance = 0\n", " while not routing.IsEnd(index):\n", " plan_output += f\" {manager.IndexToNode(index)} ->\"\n", " previous_index = index\n", - " index = assignment.Value(routing.NextVar(index))\n", + " index = solution.Value(routing.NextVar(index))\n", " route_distance += routing.GetArcCostForVehicle(previous_index, index, 0)\n", " plan_output += f\" {manager.IndexToNode(index)}\\n\"\n", " plan_output += f\"Distance of the route: {route_distance}m\\n\"\n", @@ -163,12 +173,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"locations\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " distance_callback = create_distance_callback(data, manager)\n", @@ -178,17 +188,14 @@ " routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", - " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", - " )\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", + " search_parameters.first_solution_strategy = FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", "\n", " # Solve the problem.\n", - " assignment = routing.SolveWithParameters(search_parameters)\n", + " solution = routing.SolveWithParameters(search_parameters)\n", "\n", " # Print solution on console.\n", - " if assignment:\n", - " print_solution(manager, routing, assignment)\n", + " print_solution(manager, routing, solution)\n", "\n", "\n", "main()\n", diff --git a/examples/notebook/constraint_solver/vrp.ipynb b/examples/notebook/routing/vrp.ipynb similarity index 83% rename from examples/notebook/constraint_solver/vrp.ipynb rename to examples/notebook/routing/vrp.ipynb index 8f3808c22a3..9f644488a2a 100644 --- a/examples/notebook/constraint_solver/vrp.ipynb +++ b/examples/notebook/routing/vrp.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -91,9 +91,11 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", + "FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy\n", + "RoutingSearchStatus = enums_pb2.RoutingSearchStatus\n", "\n", "\n", "def create_data_model():\n", @@ -125,8 +127,16 @@ " return data\n", "\n", "\n", - "def print_solution(data, manager, routing, solution):\n", - " \"\"\"Prints solution on console.\"\"\"\n", + "def print_solution(manager, routing, solution):\n", + " \"\"\"Prints assignment on console.\"\"\"\n", + " status = routing.status()\n", + " print(f\"Status: {RoutingSearchStatus.Value.Name(status)}\")\n", + " if (\n", + " status != RoutingSearchStatus.ROUTING_OPTIMAL\n", + " and status != RoutingSearchStatus.ROUTING_SUCCESS\n", + " ):\n", + " print(\"No solution found!\")\n", + " return\n", " print(f\"Objective: {solution.ObjectiveValue()}\")\n", " total_distance = 0\n", " for vehicle_index in range(manager.GetNumberOfVehicles()):\n", @@ -156,12 +166,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -177,19 +187,14 @@ " routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", - " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", - " )\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", + " search_parameters.first_solution_strategy = FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", "\n", " # Solve the problem.\n", " solution = routing.SolveWithParameters(search_parameters)\n", "\n", " # Print solution on console.\n", - " if solution:\n", - " print_solution(data, manager, routing, solution)\n", - " else:\n", - " print(\"No solution found !\")\n", + " print_solution(manager, routing, solution)\n", "\n", "\n", "main()\n", diff --git a/examples/notebook/constraint_solver/vrp_breaks.ipynb b/examples/notebook/routing/vrp_breaks.ipynb similarity index 89% rename from examples/notebook/constraint_solver/vrp_breaks.ipynb rename to examples/notebook/routing/vrp_breaks.ipynb index 607fd6aaaef..99ce9691934 100644 --- a/examples/notebook/constraint_solver/vrp_breaks.ipynb +++ b/examples/notebook/routing/vrp_breaks.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -75,12 +75,12 @@ "\n", "Vehicle Routing Problem (VRP) with breaks.\n", "\n", - " This is a sample using the routing library python wrapper to solve a VRP\n", - " problem.\n", - " A description of the problem can be found here:\n", - " http://en.wikipedia.org/wiki/Vehicle_routing_problem.\n", + "This is a sample using the routing library python wrapper to solve a VRP\n", + "problem.\n", + "A description of the problem can be found here:\n", + "http://en.wikipedia.org/wiki/Vehicle_routing_problem.\n", "\n", - " Durations are in minutes.\n", + "Durations are in minutes.\n", "\n" ] }, @@ -91,9 +91,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -170,12 +169,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"time_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def time_callback(from_index, to_index):\n", @@ -225,12 +224,12 @@ " )\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " # search_parameters.log_search = True\n", " search_parameters.time_limit.FromSeconds(2)\n", diff --git a/examples/notebook/constraint_solver/vrp_breaks_from_start.ipynb b/examples/notebook/routing/vrp_breaks_from_start.ipynb similarity index 92% rename from examples/notebook/constraint_solver/vrp_breaks_from_start.ipynb rename to examples/notebook/routing/vrp_breaks_from_start.ipynb index 354ee332d2f..c4e5aa129a1 100644 --- a/examples/notebook/constraint_solver/vrp_breaks_from_start.ipynb +++ b/examples/notebook/routing/vrp_breaks_from_start.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -92,8 +92,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "\n", @@ -174,12 +174,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"time_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def time_callback(from_index, to_index):\n", @@ -233,12 +233,12 @@ " )\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " # search_parameters.log_search = True\n", " search_parameters.time_limit.FromSeconds(2)\n", diff --git a/examples/notebook/constraint_solver/vrp_capacity.ipynb b/examples/notebook/routing/vrp_capacity.ipynb similarity index 90% rename from examples/notebook/constraint_solver/vrp_capacity.ipynb rename to examples/notebook/routing/vrp_capacity.ipynb index 66bfc3158bf..82106b3fc07 100644 --- a/examples/notebook/constraint_solver/vrp_capacity.ipynb +++ b/examples/notebook/routing/vrp_capacity.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -156,12 +155,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -193,12 +192,12 @@ " )\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " search_parameters.time_limit.FromSeconds(1)\n", "\n", diff --git a/examples/notebook/constraint_solver/vrp_drop_nodes.ipynb b/examples/notebook/routing/vrp_drop_nodes.ipynb similarity index 91% rename from examples/notebook/constraint_solver/vrp_drop_nodes.ipynb rename to examples/notebook/routing/vrp_drop_nodes.ipynb index 19f5d019b5f..f7dfee1d5df 100644 --- a/examples/notebook/constraint_solver/vrp_drop_nodes.ipynb +++ b/examples/notebook/routing/vrp_drop_nodes.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -165,12 +164,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -206,12 +205,12 @@ " routing.AddDisjunction([manager.NodeToIndex(node)], penalty)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " search_parameters.time_limit.FromSeconds(1)\n", "\n", diff --git a/examples/notebook/constraint_solver/vrp_global_span.ipynb b/examples/notebook/routing/vrp_global_span.ipynb similarity index 91% rename from examples/notebook/constraint_solver/vrp_global_span.ipynb rename to examples/notebook/routing/vrp_global_span.ipynb index a652e64d266..0b844c935be 100644 --- a/examples/notebook/constraint_solver/vrp_global_span.ipynb +++ b/examples/notebook/routing/vrp_global_span.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -91,9 +91,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -156,12 +155,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -189,9 +188,9 @@ " distance_dimension.SetGlobalSpanCostCoefficient(100)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/vrp_initial_routes.ipynb b/examples/notebook/routing/vrp_initial_routes.ipynb similarity index 91% rename from examples/notebook/constraint_solver/vrp_initial_routes.ipynb rename to examples/notebook/routing/vrp_initial_routes.ipynb index 1a81fc82464..4433aa8d3ed 100644 --- a/examples/notebook/constraint_solver/vrp_initial_routes.ipynb +++ b/examples/notebook/routing/vrp_initial_routes.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -156,12 +155,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -189,12 +188,12 @@ " distance_dimension.SetGlobalSpanCostCoefficient(100)\n", "\n", " # Close model with the custom search parameters.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " search_parameters.time_limit.FromSeconds(5)\n", " # When an initial solution is given for search, the model will be closed with\n", diff --git a/examples/notebook/constraint_solver/vrp_items_to_deliver.ipynb b/examples/notebook/routing/vrp_items_to_deliver.ipynb similarity index 96% rename from examples/notebook/constraint_solver/vrp_items_to_deliver.ipynb rename to examples/notebook/routing/vrp_items_to_deliver.ipynb index a75bf948937..7f20d5d50b6 100644 --- a/examples/notebook/constraint_solver/vrp_items_to_deliver.ipynb +++ b/examples/notebook/routing/vrp_items_to_deliver.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -93,9 +93,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -532,7 +531,7 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]),\n", " data[\"num_vehicles\"],\n", " data[\"starts\"],\n", @@ -540,7 +539,7 @@ " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", "\n", " # Create and register a transit callback.\n", @@ -627,12 +626,12 @@ " routing.AddDisjunction([manager.NodeToIndex(node)], penalty)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " # Sets a time limit; default is 100 milliseconds.\n", " # search_parameters.log_search = True\n", diff --git a/examples/notebook/constraint_solver/vrp_node_max.ipynb b/examples/notebook/routing/vrp_node_max.ipynb similarity index 93% rename from examples/notebook/constraint_solver/vrp_node_max.ipynb rename to examples/notebook/routing/vrp_node_max.ipynb index c5f2e9b4538..ec8f59d502a 100644 --- a/examples/notebook/constraint_solver/vrp_node_max.ipynb +++ b/examples/notebook/routing/vrp_node_max.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -87,9 +87,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -191,13 +190,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", - "\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -293,12 +291,12 @@ " dim_two.SetCumulVarSoftUpperBound(end, 0, 4200)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " # search_parameters.log_search = True\n", " search_parameters.time_limit.FromSeconds(5)\n", diff --git a/examples/notebook/constraint_solver/vrp_nodes_indices.ipynb b/examples/notebook/routing/vrp_nodes_indices.ipynb similarity index 90% rename from examples/notebook/constraint_solver/vrp_nodes_indices.ipynb rename to examples/notebook/routing/vrp_nodes_indices.ipynb index a795ca98bbb..2228f39731e 100644 --- a/examples/notebook/constraint_solver/vrp_nodes_indices.ipynb +++ b/examples/notebook/routing/vrp_nodes_indices.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -110,8 +110,7 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def main():\n", @@ -122,8 +121,8 @@ " vehicles = len(starts)\n", " assert len(starts) == len(ends)\n", "\n", - " manager = pywrapcp.RoutingIndexManager(locations, vehicles, starts, ends)\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " manager = pywraprouting.RoutingIndexManager(locations, vehicles, starts, ends)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " print(\"Starts/Ends:\")\n", " header = \"| |\"\n", diff --git a/examples/notebook/constraint_solver/vrp_pickup_delivery.ipynb b/examples/notebook/routing/vrp_pickup_delivery.ipynb similarity index 91% rename from examples/notebook/constraint_solver/vrp_pickup_delivery.ipynb rename to examples/notebook/routing/vrp_pickup_delivery.ipynb index 8bf90a41ab7..3ad66bcaf37 100644 --- a/examples/notebook/constraint_solver/vrp_pickup_delivery.ipynb +++ b/examples/notebook/routing/vrp_pickup_delivery.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -157,12 +156,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", "\n", " # Define cost of each arc.\n", @@ -202,9 +201,9 @@ " )\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION\n", + " enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/vrp_pickup_delivery_fifo.ipynb b/examples/notebook/routing/vrp_pickup_delivery_fifo.ipynb similarity index 90% rename from examples/notebook/constraint_solver/vrp_pickup_delivery_fifo.ipynb rename to examples/notebook/routing/vrp_pickup_delivery_fifo.ipynb index e783169c6be..e07ccf7ff88 100644 --- a/examples/notebook/constraint_solver/vrp_pickup_delivery_fifo.ipynb +++ b/examples/notebook/routing/vrp_pickup_delivery_fifo.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -157,12 +156,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", "\n", " # Define cost of each arc.\n", @@ -201,13 +200,13 @@ " <= distance_dimension.CumulVar(delivery_index)\n", " )\n", " routing.SetPickupAndDeliveryPolicyOfAllVehicles(\n", - " pywrapcp.RoutingModel.PICKUP_AND_DELIVERY_FIFO\n", + " pywraprouting.RoutingModel.PICKUP_AND_DELIVERY_FIFO\n", " )\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION\n", + " enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/vrp_pickup_delivery_lifo.ipynb b/examples/notebook/routing/vrp_pickup_delivery_lifo.ipynb similarity index 90% rename from examples/notebook/constraint_solver/vrp_pickup_delivery_lifo.ipynb rename to examples/notebook/routing/vrp_pickup_delivery_lifo.ipynb index 7ec8bfee398..e66c68c509e 100644 --- a/examples/notebook/constraint_solver/vrp_pickup_delivery_lifo.ipynb +++ b/examples/notebook/routing/vrp_pickup_delivery_lifo.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -157,12 +156,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", "\n", " # Define cost of each arc.\n", @@ -201,13 +200,13 @@ " <= distance_dimension.CumulVar(delivery_index)\n", " )\n", " routing.SetPickupAndDeliveryPolicyOfAllVehicles(\n", - " pywrapcp.RoutingModel.PICKUP_AND_DELIVERY_LIFO\n", + " pywraprouting.RoutingModel.PICKUP_AND_DELIVERY_LIFO\n", " )\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION\n", + " enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/vrp_resources.ipynb b/examples/notebook/routing/vrp_resources.ipynb similarity index 93% rename from examples/notebook/constraint_solver/vrp_resources.ipynb rename to examples/notebook/routing/vrp_resources.ipynb index c861e6a3a81..b8b57a1f764 100644 --- a/examples/notebook/constraint_solver/vrp_resources.ipynb +++ b/examples/notebook/routing/vrp_resources.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -172,12 +171,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"time_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def time_callback(from_index, to_index):\n", @@ -249,9 +248,9 @@ " routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(i)))\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/vrp_solution_callback.ipynb b/examples/notebook/routing/vrp_solution_callback.ipynb similarity index 87% rename from examples/notebook/constraint_solver/vrp_solution_callback.ipynb rename to examples/notebook/routing/vrp_solution_callback.ipynb index b66d471977f..95aedc93987 100644 --- a/examples/notebook/constraint_solver/vrp_solution_callback.ipynb +++ b/examples/notebook/routing/vrp_solution_callback.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -93,9 +93,8 @@ "source": [ "import weakref\n", "\n", - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -128,7 +127,8 @@ "\n", "\n", "def print_solution(\n", - " routing_manager: pywrapcp.RoutingIndexManager, routing_model: pywrapcp.RoutingModel\n", + " routing_manager: pywraprouting.RoutingIndexManager,\n", + " routing_model: pywraprouting.RoutingModel,\n", "):\n", " \"\"\"Prints solution on console.\"\"\"\n", " print(\"################\")\n", @@ -160,8 +160,8 @@ "\n", " def __init__(\n", " self,\n", - " manager: pywrapcp.RoutingIndexManager,\n", - " model: pywrapcp.RoutingModel,\n", + " manager: pywraprouting.RoutingIndexManager,\n", + " model: pywraprouting.RoutingModel,\n", " limit: int,\n", " ):\n", " # We need a weak ref on the routing model to avoid a cycle.\n", @@ -177,11 +177,12 @@ " ) # pytype: disable=attribute-error\n", " if not self.objectives or objective < self.objectives[-1]:\n", " self.objectives.append(objective)\n", - " print_solution(self._routing_manager_ref(), self._routing_model_ref())\n", + " print_solution(\n", + " self._routing_manager_ref(), self._routing_model_ref()\n", + " ) # pytype: disable=attribute-error\n", " self._counter += 1\n", " if self._counter > self._counter_limit:\n", - " self._routing_model_ref().solver().FinishCurrentSearch()\n", - "\n", + " self._routing_model_ref().solver().FinishCurrentSearch() # pytype: disable=attribute-error\n", "\n", "\n", "\n", @@ -191,12 +192,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " routing_manager = pywrapcp.RoutingIndexManager(\n", + " routing_manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing_model = pywrapcp.RoutingModel(routing_manager)\n", + " routing_model = pywraprouting.RoutingModel(routing_manager)\n", "\n", "\n", " # Create and register a transit callback.\n", @@ -229,12 +230,12 @@ " routing_model.AddAtSolutionCallback(solution_callback)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " search_parameters.time_limit.FromSeconds(5)\n", "\n", diff --git a/examples/notebook/constraint_solver/vrp_starts_ends.ipynb b/examples/notebook/routing/vrp_starts_ends.ipynb similarity index 89% rename from examples/notebook/constraint_solver/vrp_starts_ends.ipynb rename to examples/notebook/routing/vrp_starts_ends.ipynb index 1f3a3f3e0db..8450579d020 100644 --- a/examples/notebook/constraint_solver/vrp_starts_ends.ipynb +++ b/examples/notebook/routing/vrp_starts_ends.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -148,12 +147,15 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", - " len(data[\"distance_matrix\"]), data[\"num_vehicles\"], data[\"starts\"], data[\"ends\"]\n", + " manager = pywraprouting.RoutingIndexManager(\n", + " len(data[\"distance_matrix\"]),\n", + " data[\"num_vehicles\"],\n", + " data[\"starts\"],\n", + " data[\"ends\"],\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -181,9 +183,9 @@ " distance_dimension.SetGlobalSpanCostCoefficient(100)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/vrp_time_windows.ipynb b/examples/notebook/routing/vrp_time_windows.ipynb similarity index 92% rename from examples/notebook/constraint_solver/vrp_time_windows.ipynb rename to examples/notebook/routing/vrp_time_windows.ipynb index 516707633bd..460da3387be 100644 --- a/examples/notebook/constraint_solver/vrp_time_windows.ipynb +++ b/examples/notebook/routing/vrp_time_windows.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -169,12 +168,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"time_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def time_callback(from_index, to_index):\n", @@ -221,9 +220,9 @@ " routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(i)))\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/notebook/constraint_solver/vrp_time_windows_per_vehicles.ipynb b/examples/notebook/routing/vrp_time_windows_per_vehicles.ipynb similarity index 93% rename from examples/notebook/constraint_solver/vrp_time_windows_per_vehicles.ipynb rename to examples/notebook/routing/vrp_time_windows_per_vehicles.ipynb index 205d5d7b19f..8240bda85c9 100644 --- a/examples/notebook/constraint_solver/vrp_time_windows_per_vehicles.ipynb +++ b/examples/notebook/routing/vrp_time_windows_per_vehicles.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -97,9 +97,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -187,12 +186,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " 1 + 16 * 4, data[\"num_vehicles\"], data[\"depot\"] # number of locations\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", "\n", " # Create and register a transit callback.\n", @@ -276,12 +275,12 @@ " routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(i)))\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " search_parameters.time_limit.FromSeconds(1)\n", "\n", diff --git a/examples/notebook/constraint_solver/vrp_tokens.ipynb b/examples/notebook/routing/vrp_tokens.ipynb similarity index 91% rename from examples/notebook/constraint_solver/vrp_tokens.ipynb rename to examples/notebook/routing/vrp_tokens.ipynb index cea5aa12cfd..776ecdb4aa3 100644 --- a/examples/notebook/constraint_solver/vrp_tokens.ipynb +++ b/examples/notebook/routing/vrp_tokens.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -161,12 +160,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"tokens\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def distance_callback(from_index, to_index):\n", @@ -221,12 +220,12 @@ " )\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " search_parameters.time_limit.FromSeconds(1)\n", "\n", diff --git a/examples/notebook/constraint_solver/vrp_with_time_limit.ipynb b/examples/notebook/routing/vrp_with_time_limit.ipynb similarity index 85% rename from examples/notebook/constraint_solver/vrp_with_time_limit.ipynb rename to examples/notebook/routing/vrp_with_time_limit.ipynb index cbafba12e8b..8c0410f1b83 100644 --- a/examples/notebook/constraint_solver/vrp_with_time_limit.ipynb +++ b/examples/notebook/routing/vrp_with_time_limit.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def print_solution(manager, routing, solution):\n", @@ -120,10 +119,10 @@ " depot = 0\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(num_locations, num_vehicles, depot)\n", + " manager = pywraprouting.RoutingIndexManager(num_locations, num_vehicles, depot)\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", "\n", " # Create and register a transit callback.\n", @@ -150,12 +149,12 @@ " distance_dimension.SetGlobalSpanCostCoefficient(100)\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", " search_parameters.local_search_metaheuristic = (\n", - " routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", + " enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH\n", " )\n", " search_parameters.log_search = True\n", " search_parameters.time_limit.FromSeconds(5)\n", diff --git a/examples/notebook/constraint_solver/vrptw_store_solution_data.ipynb b/examples/notebook/routing/vrptw_store_solution_data.ipynb similarity index 93% rename from examples/notebook/constraint_solver/vrptw_store_solution_data.ipynb rename to examples/notebook/routing/vrptw_store_solution_data.ipynb index 558847e5fc3..0389628ffa6 100644 --- a/examples/notebook/constraint_solver/vrptw_store_solution_data.ipynb +++ b/examples/notebook/routing/vrptw_store_solution_data.ipynb @@ -41,10 +41,10 @@ "source": [ "\n", "\n", "\n", "
\n", - "Run in Google Colab\n", + "Run in Google Colab\n", "\n", - "View source on GitHub\n", + "View source on GitHub\n", "
" ] @@ -83,9 +83,8 @@ "metadata": {}, "outputs": [], "source": [ - "from ortools.constraint_solver import routing_enums_pb2\n", - "from ortools.constraint_solver import pywrapcp\n", - "\n", + "from ortools.routing import enums_pb2\n", + "from ortools.routing import pywraprouting\n", "\n", "\n", "def create_data_model():\n", @@ -217,12 +216,12 @@ " data = create_data_model()\n", "\n", " # Create the routing index manager.\n", - " manager = pywrapcp.RoutingIndexManager(\n", + " manager = pywraprouting.RoutingIndexManager(\n", " len(data[\"time_matrix\"]), data[\"num_vehicles\"], data[\"depot\"]\n", " )\n", "\n", " # Create Routing Model.\n", - " routing = pywrapcp.RoutingModel(manager)\n", + " routing = pywraprouting.RoutingModel(manager)\n", "\n", " # Create and register a transit callback.\n", " def time_callback(from_index, to_index):\n", @@ -269,9 +268,9 @@ " routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(i)))\n", "\n", " # Setting first solution heuristic.\n", - " search_parameters = pywrapcp.DefaultRoutingSearchParameters()\n", + " search_parameters = pywraprouting.DefaultRoutingSearchParameters()\n", " search_parameters.first_solution_strategy = (\n", - " routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", + " enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC\n", " )\n", "\n", " # Solve the problem.\n", diff --git a/examples/python/appointments.py b/examples/python/appointments.py index 328ac691f07..7282e43cf3d 100644 --- a/examples/python/appointments.py +++ b/examples/python/appointments.py @@ -32,57 +32,61 @@ _LOAD_MIN = flags.DEFINE_integer("load_min", 480, "Minimum load in minutes.") _LOAD_MAX = flags.DEFINE_integer("load_max", 540, "Maximum load in minutes.") -_COMMUTE_TIME = flags.DEFINE_integer("commute_time", 30, "Commute time in minutes.") -_NUM_WORKERS = flags.DEFINE_integer("num_workers", 98, "Maximum number of workers.") +_COMMUTE_TIME = flags.DEFINE_integer( + "commute_time", 30, "Commute time in minutes." +) +_NUM_WORKERS = flags.DEFINE_integer( + "num_workers", 98, "Maximum number of workers." +) class AllSolutionCollector(cp_model.CpSolverSolutionCallback): - """Stores all solutions.""" + """Stores all solutions.""" - def __init__(self, variables): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables - self.__collect = [] + def __init__(self, variables): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables + self.__collect = [] - def on_solution_callback(self) -> None: - """Collect a new combination.""" - combination = [self.value(v) for v in self.__variables] - self.__collect.append(combination) + def on_solution_callback(self) -> None: + """Collect a new combination.""" + combination = [self.value(v) for v in self.__variables] + self.__collect.append(combination) - def combinations(self) -> list[list[int]]: - """Returns all collected combinations.""" - return self.__collect + def combinations(self) -> list[list[int]]: + """Returns all collected combinations.""" + return self.__collect def enumerate_all_knapsacks_with_repetition( item_sizes: list[int], total_size_min: int, total_size_max: int ) -> list[list[int]]: - """Enumerate all possible knapsacks with total size in the given range. - - Args: - item_sizes: a list of integers. item_sizes[i] is the size of item #i. - total_size_min: an integer, the minimum total size. - total_size_max: an integer, the maximum total size. - - Returns: - The list of all the knapsacks whose total size is in the given inclusive - range. Each knapsack is a list [#item0, #item1, ... ], where #itemK is an - nonnegative integer: the number of times we put item #K in the knapsack. - """ - model = cp_model.CpModel() - variables = [ - model.new_int_var(0, total_size_max // size, "") for size in item_sizes - ] - load = sum(variables[i] * size for i, size in enumerate(item_sizes)) - model.add_linear_constraint(load, total_size_min, total_size_max) - - solver = cp_model.CpSolver() - solution_collector = AllSolutionCollector(variables) - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - # solve - solver.solve(model, solution_collector) - return solution_collector.combinations() + """Enumerate all possible knapsacks with total size in the given range. + + Args: + item_sizes: a list of integers. item_sizes[i] is the size of item #i. + total_size_min: an integer, the minimum total size. + total_size_max: an integer, the maximum total size. + + Returns: + The list of all the knapsacks whose total size is in the given inclusive + range. Each knapsack is a list [#item0, #item1, ... ], where #itemK is an + nonnegative integer: the number of times we put item #K in the knapsack. + """ + model = cp_model.CpModel() + variables = [ + model.new_int_var(0, total_size_max // size, "") for size in item_sizes + ] + load = sum(variables[i] * size for i, size in enumerate(item_sizes)) + model.add_linear_constraint(load, total_size_min, total_size_max) + + solver = cp_model.CpSolver() + solution_collector = AllSolutionCollector(variables) + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + # solve + solver.solve(model, solution_collector) + return solution_collector.combinations() def aggregate_item_collections_optimally( @@ -90,186 +94,185 @@ def aggregate_item_collections_optimally( max_num_collections: int, ideal_item_ratios: list[float], ) -> list[int]: - """Selects a set (with repetition) of combination of items optimally. - - Given a set of collections of N possible items (in each collection, an item - may appear multiple times), a given "ideal breakdown of items", and a - maximum number of collections, this method finds the optimal way to - aggregate the collections in order to: - - maximize the overall number of items - - while keeping the ratio of each item, among the overall selection, as close - as possible to a given input ratio (which depends on the item). - Each collection may be selected more than one time. - - Args: - item_collections: a list of item collections. Each item collection is a list - of integers [#item0, ..., #itemN-1], where #itemK is the number of times - item #K appears in the collection, and N is the number of distinct items. - max_num_collections: an integer, the maximum number of item collections that - may be selected (counting repetitions of the same collection). - ideal_item_ratios: A list of N float which sums to 1.0: the K-th element is - the ideal ratio of item #K in the whole aggregated selection. - - Returns: - A pair (objective value, list of pairs (item collection, num_selections)), - where: - - "objective value" is the value of the internal objective function used - by the MIP Solver - - Each "item collection" is an element of the input item_collections - - and its associated "num_selections" is the number of times it was - selected. - """ - solver = pywraplp.Solver.CreateSolver("SCIP") - if not solver: - return [] - n = len(ideal_item_ratios) - num_distinct_collections = len(item_collections) - max_num_items_per_collection = 0 - for template in item_collections: - max_num_items_per_collection = max(max_num_items_per_collection, sum(template)) - upper_bound = max_num_items_per_collection * max_num_collections - - # num_selections_of_collection[i] is an IntVar that represents the number - # of times that we will use collection #i in our global selection. - num_selections_of_collection = [ - solver.IntVar(0, max_num_collections, "s[%d]" % i) - for i in range(num_distinct_collections) - ] - - # num_overall_item[i] is an IntVar that represents the total count of item #i, - # aggregated over all selected collections. This is enforced with dedicated - # constraints that bind them with the num_selections_of_collection vars. - num_overall_item = [ - solver.IntVar(0, upper_bound, "num_overall_item[%d]" % i) for i in range(n) - ] - for i in range(n): - ct = solver.Constraint(0.0, 0.0) - ct.SetCoefficient(num_overall_item[i], -1) - for j in range(num_distinct_collections): - ct.SetCoefficient(num_selections_of_collection[j], item_collections[j][i]) - - # Maintain the num_all_item variable as the sum of all num_overall_item - # variables. - num_all_items = solver.IntVar(0, upper_bound, "num_all_items") - solver.Add(solver.Sum(num_overall_item) == num_all_items) - - # Sets the total number of workers. - solver.Add(solver.Sum(num_selections_of_collection) == max_num_collections) - - # Objective variables. - deviation_vars = [ - solver.NumVar(0, upper_bound, "deviation_vars[%d]" % i) for i in range(n) - ] - for i in range(n): - deviation = deviation_vars[i] - solver.Add( - deviation >= num_overall_item[i] - ideal_item_ratios[i] * num_all_items - ) - solver.Add( - deviation >= ideal_item_ratios[i] * num_all_items - num_overall_item[i] - ) - - solver.Maximize(num_all_items - solver.Sum(deviation_vars)) - - result_status = solver.Solve() - - if result_status == pywraplp.Solver.OPTIMAL: - # The problem has an optimal solution. - return [int(v.solution_value()) for v in num_selections_of_collection] + """Selects a set (with repetition) of combination of items optimally. + + Given a set of collections of N possible items (in each collection, an item + may appear multiple times), a given "ideal breakdown of items", and a + maximum number of collections, this method finds the optimal way to + aggregate the collections in order to: + - maximize the overall number of items + - while keeping the ratio of each item, among the overall selection, as close + as possible to a given input ratio (which depends on the item). + Each collection may be selected more than one time. + + Args: + item_collections: a list of item collections. Each item collection is a list + of integers [#item0, ..., #itemN-1], where #itemK is the number of times + item #K appears in the collection, and N is the number of distinct items. + max_num_collections: an integer, the maximum number of item collections that + may be selected (counting repetitions of the same collection). + ideal_item_ratios: A list of N float which sums to 1.0: the K-th element is + the ideal ratio of item #K in the whole aggregated selection. + + Returns: + A pair (objective value, list of pairs (item collection, num_selections)), + where: + - "objective value" is the value of the internal objective function used + by the MIP Solver + - Each "item collection" is an element of the input item_collections + - and its associated "num_selections" is the number of times it was + selected. + """ + solver = pywraplp.Solver.CreateSolver("SCIP") + if not solver: return [] + n = len(ideal_item_ratios) + num_distinct_collections = len(item_collections) + max_num_items_per_collection = 0 + for template in item_collections: + max_num_items_per_collection = max( + max_num_items_per_collection, sum(template) + ) + upper_bound = max_num_items_per_collection * max_num_collections + + # num_selections_of_collection[i] is an IntVar that represents the number + # of times that we will use collection #i in our global selection. + num_selections_of_collection = [ + solver.IntVar(0, max_num_collections, "s[%d]" % i) + for i in range(num_distinct_collections) + ] + + # num_overall_item[i] is an IntVar that represents the total count of item #i, + # aggregated over all selected collections. This is enforced with dedicated + # constraints that bind them with the num_selections_of_collection vars. + num_overall_item = [ + solver.IntVar(0, upper_bound, "num_overall_item[%d]" % i) + for i in range(n) + ] + for i in range(n): + ct = solver.Constraint(0.0, 0.0) + ct.SetCoefficient(num_overall_item[i], -1) + for j in range(num_distinct_collections): + ct.SetCoefficient(num_selections_of_collection[j], item_collections[j][i]) + + # Maintain the num_all_item variable as the sum of all num_overall_item + # variables. + num_all_items = solver.IntVar(0, upper_bound, "num_all_items") + solver.Add(solver.Sum(num_overall_item) == num_all_items) + + # Sets the total number of workers. + solver.Add(solver.Sum(num_selections_of_collection) == max_num_collections) + + # Objective variables. + deviation_vars = [ + solver.NumVar(0, upper_bound, "deviation_vars[%d]" % i) for i in range(n) + ] + for i in range(n): + deviation = deviation_vars[i] + solver.Add( + deviation >= num_overall_item[i] - ideal_item_ratios[i] * num_all_items + ) + solver.Add( + deviation >= ideal_item_ratios[i] * num_all_items - num_overall_item[i] + ) + + solver.Maximize(num_all_items - solver.Sum(deviation_vars)) + + result_status = solver.Solve() + + if result_status == pywraplp.Solver.OPTIMAL: + # The problem has an optimal solution. + return [int(v.solution_value()) for v in num_selections_of_collection] + return [] def get_optimal_schedule( - demand: list[tuple[float, str, int]] + demand: list[tuple[float, str, int]], ) -> list[tuple[int, list[tuple[int, str]]]]: - """Computes the optimal schedule for the installation input. - - Args: - demand: a list of "appointment types". Each "appointment type" is a triple - (ideal_ratio_pct, name, duration_minutes), where ideal_ratio_pct is the - ideal percentage (in [0..100.0]) of that type of appointment among all - appointments scheduled. - - Returns: - The same output type as EnumerateAllKnapsacksWithRepetition. - """ - combinations = enumerate_all_knapsacks_with_repetition( - [a[2] + _COMMUTE_TIME.value for a in demand], - _LOAD_MIN.value, - _LOAD_MAX.value, - ) - print( - ( - "Found %d possible day schedules " % len(combinations) - + "(i.e. combination of appointments filling up one worker's day)" - ) - ) - - selection = aggregate_item_collections_optimally( - combinations, _NUM_WORKERS.value, [a[0] / 100.0 for a in demand] - ) - output = [] - for i, s in enumerate(selection): - if s != 0: - output.append( - ( - s, - [ - (combinations[i][t], d[1]) - for t, d in enumerate(demand) - if combinations[i][t] != 0 - ], - ) - ) - - return output + """Computes the optimal schedule for the installation input. + + Args: + demand: a list of "appointment types". Each "appointment type" is a triple + (ideal_ratio_pct, name, duration_minutes), where ideal_ratio_pct is the + ideal percentage (in [0..100.0]) of that type of appointment among all + appointments scheduled. + + Returns: + The same output type as EnumerateAllKnapsacksWithRepetition. + """ + combinations = enumerate_all_knapsacks_with_repetition( + [a[2] + _COMMUTE_TIME.value for a in demand], + _LOAD_MIN.value, + _LOAD_MAX.value, + ) + print(( + "Found %d possible day schedules " % len(combinations) + + "(i.e. combination of appointments filling up one worker's day)" + )) + + selection = aggregate_item_collections_optimally( + combinations, _NUM_WORKERS.value, [a[0] / 100.0 for a in demand] + ) + output = [] + for i, s in enumerate(selection): + if s != 0: + output.append(( + s, + [ + (combinations[i][t], d[1]) + for t, d in enumerate(demand) + if combinations[i][t] != 0 + ], + )) + + return output def main(_): - demand = [(45.0, "Type1", 90), (30.0, "Type2", 120), (25.0, "Type3", 180)] - print("*** input problem ***") - print("Appointments: ") - for a in demand: - print(" %.2f%% of %s : %d min" % (a[0], a[1], a[2])) - print("Commute time = %d" % _COMMUTE_TIME.value) - print( - "Acceptable duration of a work day = [%d..%d]" - % (_LOAD_MIN.value, _LOAD_MAX.value) - ) - print("%d workers" % _NUM_WORKERS.value) - selection = get_optimal_schedule(demand) - print() - installed = 0 - installed_per_type = {} - for a in demand: - installed_per_type[a[1]] = 0 - - # [START print_solution] - print("*** output solution ***") - for template in selection: - num_instances = template[0] - print("%d schedules with " % num_instances) - for t in template[1]: - mult = t[0] - print(" %d installation of type %s" % (mult, t[1])) - installed += num_instances * mult - installed_per_type[t[1]] += num_instances * mult - - print() - print("%d installations planned" % installed) - for a in demand: - name = a[1] - per_type = installed_per_type[name] - if installed != 0: - print( - f" {per_type} ({per_type * 100.0 / installed}%) installations of" - f" type {name} planned" - ) - else: - print(f" {per_type} installations of type {name} planned") - # [END print_solution] + demand = [(45.0, "Type1", 90), (30.0, "Type2", 120), (25.0, "Type3", 180)] + print("*** input problem ***") + print("Appointments: ") + for a in demand: + print(" %.2f%% of %s : %d min" % (a[0], a[1], a[2])) + print("Commute time = %d" % _COMMUTE_TIME.value) + print( + "Acceptable duration of a work day = [%d..%d]" + % (_LOAD_MIN.value, _LOAD_MAX.value) + ) + print("%d workers" % _NUM_WORKERS.value) + selection = get_optimal_schedule(demand) + print() + installed = 0 + installed_per_type = {} + for a in demand: + installed_per_type[a[1]] = 0 + + # [START print_solution] + print("*** output solution ***") + for template in selection: + num_instances = template[0] + print("%d schedules with " % num_instances) + for t in template[1]: + mult = t[0] + print(" %d installation of type %s" % (mult, t[1])) + installed += num_instances * mult + installed_per_type[t[1]] += num_instances * mult + + print() + print("%d installations planned" % installed) + for a in demand: + name = a[1] + per_type = installed_per_type[name] + if installed != 0: + print( + f" {per_type} ({per_type * 100.0 / installed}%) installations of" + f" type {name} planned" + ) + else: + print(f" {per_type} installations of type {name} planned") + # [END print_solution] if __name__ == "__main__": - app.run(main) + app.run(main) # [END program] diff --git a/examples/python/arc_flow_cutting_stock_sat.py b/examples/python/arc_flow_cutting_stock_sat.py index 9b70f2c2cac..eb73bcc2795 100644 --- a/examples/python/arc_flow_cutting_stock_sat.py +++ b/examples/python/arc_flow_cutting_stock_sat.py @@ -34,7 +34,9 @@ "num_search_workers:8,log_search_progress:true,max_time_in_seconds:10", "Sat solver parameters.", ) -_SOLVER = flags.DEFINE_string("solver", "sat", "Method used to solve: sat, mip.") +_SOLVER = flags.DEFINE_string( + "solver", "sat", "Method used to solve: sat, mip." +) DESIRED_LENGTHS = [ @@ -184,247 +186,256 @@ def regroup_and_count(raw_input): - """Regroup all equal capacities in a multiset.""" - grouped = collections.defaultdict(int) - for i in raw_input: - grouped[i] += 1 - output = [] - for size, count in grouped.items(): - output.append([size, count]) - output.sort(reverse=False) - return output + """Regroup all equal capacities in a multiset.""" + grouped = collections.defaultdict(int) + for i in raw_input: + grouped[i] += 1 + output = [] + for size, count in grouped.items(): + output.append([size, count]) + output.sort(reverse=False) + return output def price_usage(usage, capacities): - """Compute the best price for a given usage and possible capacities.""" - price = max(capacities) - for capacity in capacities: - if capacity < usage: - continue - price = min(capacity - usage, price) - return price + """Compute the best price for a given usage and possible capacities.""" + price = max(capacities) + for capacity in capacities: + if capacity < usage: + continue + price = min(capacity - usage, price) + return price def create_state_graph(items, max_capacity): - """Create a state graph from a multiset of items, and a maximum capacity.""" - states = [] - state_to_index = {} - states.append(0) - state_to_index[0] = 0 - transitions = [] - - for item_index, size_and_count in enumerate(items): - size, count = size_and_count - num_states = len(states) - for state_index in range(num_states): - current_state = states[state_index] - current_state_index = state_index - - for card in range(count): - new_state = current_state + size * (card + 1) - if new_state > max_capacity: - break - if new_state in state_to_index: - new_state_index = state_to_index[new_state] - else: - new_state_index = len(states) - states.append(new_state) - state_to_index[new_state] = new_state_index - # Add the transition - transitions.append( - [current_state_index, new_state_index, item_index, card + 1] - ) - - return states, transitions - - -def solve_cutting_stock_with_arc_flow_and_sat(output_proto_file: str, params: str): - """Solve the cutting stock with arc-flow and the CP-SAT solver.""" - items = regroup_and_count(DESIRED_LENGTHS) - print("Items:", items) - num_items = len(DESIRED_LENGTHS) - - max_capacity = max(POSSIBLE_CAPACITIES) - states, transitions = create_state_graph(items, max_capacity) + """Create a state graph from a multiset of items, and a maximum capacity.""" + states = [] + state_to_index = {} + states.append(0) + state_to_index[0] = 0 + transitions = [] + + for item_index, size_and_count in enumerate(items): + size, count = size_and_count + num_states = len(states) + for state_index in range(num_states): + current_state = states[state_index] + current_state_index = state_index + + for card in range(count): + new_state = current_state + size * (card + 1) + if new_state > max_capacity: + break + if new_state in state_to_index: + new_state_index = state_to_index[new_state] + else: + new_state_index = len(states) + states.append(new_state) + state_to_index[new_state] = new_state_index + # Add the transition + transitions.append( + [current_state_index, new_state_index, item_index, card + 1] + ) - print( - "Dynamic programming has generated", - len(states), - "states and", - len(transitions), - "transitions", + return states, transitions + + +def solve_cutting_stock_with_arc_flow_and_sat( + output_proto_file: str, params: str +): + """Solve the cutting stock with arc-flow and the CP-SAT solver.""" + items = regroup_and_count(DESIRED_LENGTHS) + print("Items:", items) + num_items = len(DESIRED_LENGTHS) + + max_capacity = max(POSSIBLE_CAPACITIES) + states, transitions = create_state_graph(items, max_capacity) + + print( + "Dynamic programming has generated", + len(states), + "states and", + len(transitions), + "transitions", + ) + + incoming_vars = collections.defaultdict(list) + outgoing_vars = collections.defaultdict(list) + incoming_sink_vars = [] + item_vars = collections.defaultdict(list) + item_coeffs = collections.defaultdict(list) + transition_vars = [] + + model = cp_model.CpModel() + + objective_vars = [] + objective_coeffs = [] + + for outgoing, incoming, item_index, card in transitions: + count = items[item_index][1] + max_count = count // card + count_var = model.NewIntVar( + 0, max_count, "i%i_f%i_t%i_C%s" % (item_index, incoming, outgoing, card) + ) + incoming_vars[incoming].append(count_var) + outgoing_vars[outgoing].append(count_var) + item_vars[item_index].append(count_var) + item_coeffs[item_index].append(card) + transition_vars.append(count_var) + + for state_index, state in enumerate(states): + if state_index == 0: + continue + exit_var = model.NewIntVar(0, num_items, "e%i" % state_index) + outgoing_vars[state_index].append(exit_var) + incoming_sink_vars.append(exit_var) + price = price_usage(state, POSSIBLE_CAPACITIES) + objective_vars.append(exit_var) + objective_coeffs.append(price) + + # Flow conservation + for state_index in range(1, len(states)): + model.Add( + sum(incoming_vars[state_index]) == sum(outgoing_vars[state_index]) ) - incoming_vars = collections.defaultdict(list) - outgoing_vars = collections.defaultdict(list) - incoming_sink_vars = [] - item_vars = collections.defaultdict(list) - item_coeffs = collections.defaultdict(list) - transition_vars = [] - - model = cp_model.CpModel() - - objective_vars = [] - objective_coeffs = [] + # Flow going out of the source must go in the sink + model.Add(sum(outgoing_vars[0]) == sum(incoming_sink_vars)) - for outgoing, incoming, item_index, card in transitions: - count = items[item_index][1] - max_count = count // card - count_var = model.NewIntVar( - 0, max_count, "i%i_f%i_t%i_C%s" % (item_index, incoming, outgoing, card) + # Items must be placed + for item_index, size_and_count in enumerate(items): + num_arcs = len(item_vars[item_index]) + model.Add( + sum( + item_vars[item_index][i] * item_coeffs[item_index][i] + for i in range(num_arcs) ) - incoming_vars[incoming].append(count_var) - outgoing_vars[outgoing].append(count_var) - item_vars[item_index].append(count_var) - item_coeffs[item_index].append(card) - transition_vars.append(count_var) - - for state_index, state in enumerate(states): - if state_index == 0: - continue - exit_var = model.NewIntVar(0, num_items, "e%i" % state_index) - outgoing_vars[state_index].append(exit_var) - incoming_sink_vars.append(exit_var) - price = price_usage(state, POSSIBLE_CAPACITIES) - objective_vars.append(exit_var) - objective_coeffs.append(price) - - # Flow conservation - for state_index in range(1, len(states)): - model.Add(sum(incoming_vars[state_index]) == sum(outgoing_vars[state_index])) - - # Flow going out of the source must go in the sink - model.Add(sum(outgoing_vars[0]) == sum(incoming_sink_vars)) - - # Items must be placed - for item_index, size_and_count in enumerate(items): - num_arcs = len(item_vars[item_index]) - model.Add( - sum( - item_vars[item_index][i] * item_coeffs[item_index][i] - for i in range(num_arcs) - ) - == size_and_count[1] - ) - - # Objective is the sum of waste - model.Minimize( - sum(objective_vars[i] * objective_coeffs[i] for i in range(len(objective_vars))) + == size_and_count[1] ) - # Output model proto to file. - if output_proto_file: - model.ExportToFile(output_proto_file) + # Objective is the sum of waste + model.Minimize( + sum( + objective_vars[i] * objective_coeffs[i] + for i in range(len(objective_vars)) + ) + ) - # Solve model. - solver = cp_model.CpSolver() - if params: - text_format.Parse(params, solver.parameters) - solver.parameters.log_search_progress = True - solver.Solve(model) + # Output model proto to file. + if output_proto_file: + model.ExportToFile(output_proto_file) + # Solve model. + solver = cp_model.CpSolver() + if params: + text_format.Parse(params, solver.parameters) + solver.parameters.log_search_progress = True + solver.Solve(model) -def solve_cutting_stock_with_arc_flow_and_mip(): - """Solve the cutting stock with arc-flow and a MIP solver.""" - items = regroup_and_count(DESIRED_LENGTHS) - print("Items:", items) - num_items = len(DESIRED_LENGTHS) - max_capacity = max(POSSIBLE_CAPACITIES) - states, transitions = create_state_graph(items, max_capacity) - print( - "Dynamic programming has generated", - len(states), - "states and", - len(transitions), - "transitions", +def solve_cutting_stock_with_arc_flow_and_mip(): + """Solve the cutting stock with arc-flow and a MIP solver.""" + items = regroup_and_count(DESIRED_LENGTHS) + print("Items:", items) + num_items = len(DESIRED_LENGTHS) + max_capacity = max(POSSIBLE_CAPACITIES) + states, transitions = create_state_graph(items, max_capacity) + + print( + "Dynamic programming has generated", + len(states), + "states and", + len(transitions), + "transitions", + ) + + incoming_vars = collections.defaultdict(list) + outgoing_vars = collections.defaultdict(list) + incoming_sink_vars = [] + item_vars = collections.defaultdict(list) + item_coeffs = collections.defaultdict(list) + + start_time = time.time() + model = mb.ModelBuilder() + + objective_vars = [] + objective_coeffs = [] + + var_index = 0 + for outgoing, incoming, item_index, card in transitions: + count = items[item_index][1] + count_var = model.new_int_var( + 0, + count, + "a%i_i%i_f%i_t%i_c%i" + % (var_index, item_index, incoming, outgoing, card), + ) + var_index += 1 + incoming_vars[incoming].append(count_var) + outgoing_vars[outgoing].append(count_var) + item_vars[item_index].append(count_var) + item_coeffs[item_index].append(card) + + for state_index, state in enumerate(states): + if state_index == 0: + continue + exit_var = model.new_int_var(0, num_items, "e%i" % state_index) + outgoing_vars[state_index].append(exit_var) + incoming_sink_vars.append(exit_var) + price = price_usage(state, POSSIBLE_CAPACITIES) + objective_vars.append(exit_var) + objective_coeffs.append(price) + + # Flow conservation + for state_index in range(1, len(states)): + model.add( + mb.LinearExpr.sum(incoming_vars[state_index]) + == mb.LinearExpr.sum(outgoing_vars[state_index]) ) - incoming_vars = collections.defaultdict(list) - outgoing_vars = collections.defaultdict(list) - incoming_sink_vars = [] - item_vars = collections.defaultdict(list) - item_coeffs = collections.defaultdict(list) - - start_time = time.time() - model = mb.ModelBuilder() - - objective_vars = [] - objective_coeffs = [] - - var_index = 0 - for outgoing, incoming, item_index, card in transitions: - count = items[item_index][1] - count_var = model.new_int_var( - 0, - count, - "a%i_i%i_f%i_t%i_c%i" % (var_index, item_index, incoming, outgoing, card), - ) - var_index += 1 - incoming_vars[incoming].append(count_var) - outgoing_vars[outgoing].append(count_var) - item_vars[item_index].append(count_var) - item_coeffs[item_index].append(card) - - for state_index, state in enumerate(states): - if state_index == 0: - continue - exit_var = model.new_int_var(0, num_items, "e%i" % state_index) - outgoing_vars[state_index].append(exit_var) - incoming_sink_vars.append(exit_var) - price = price_usage(state, POSSIBLE_CAPACITIES) - objective_vars.append(exit_var) - objective_coeffs.append(price) - - # Flow conservation - for state_index in range(1, len(states)): - model.add( - mb.LinearExpr.sum(incoming_vars[state_index]) - == mb.LinearExpr.sum(outgoing_vars[state_index]) - ) + # Flow going out of the source must go in the sink + model.add( + mb.LinearExpr.sum(outgoing_vars[0]) + == mb.LinearExpr.sum(incoming_sink_vars) + ) - # Flow going out of the source must go in the sink + # Items must be placed + for item_index, size_and_count in enumerate(items): + num_arcs = len(item_vars[item_index]) model.add( - mb.LinearExpr.sum(outgoing_vars[0]) == mb.LinearExpr.sum(incoming_sink_vars) + mb.LinearExpr.sum([ + item_vars[item_index][i] * item_coeffs[item_index][i] + for i in range(num_arcs) + ]) + == size_and_count[1] ) - # Items must be placed - for item_index, size_and_count in enumerate(items): - num_arcs = len(item_vars[item_index]) - model.add( - mb.LinearExpr.sum( - [ - item_vars[item_index][i] * item_coeffs[item_index][i] - for i in range(num_arcs) - ] - ) - == size_and_count[1] - ) - - # Objective is the sum of waste - model.minimize(np.dot(objective_vars, objective_coeffs)) + # Objective is the sum of waste + model.minimize(np.dot(objective_vars, objective_coeffs)) - solver = mb.ModelSolver("scip") - solver.enable_output(True) - status = solver.solve(model) + solver = mb.ModelSolver("scip") + solver.enable_output(True) + status = solver.solve(model) - ### Output the solution. - if status == mb.SolveStatus.OPTIMAL or status == mb.SolveStatus.FEASIBLE: - print( - "Objective value = %f found in %.2f s" - % (solver.objective_value, time.time() - start_time) - ) - else: - print("No solution") + ### Output the solution. + if status == mb.SolveStatus.OPTIMAL or status == mb.SolveStatus.FEASIBLE: + print( + "Objective value = %f found in %.2f s" + % (solver.objective_value, time.time() - start_time) + ) + else: + print("No solution") def main(_): - """Main function.""" - if _SOLVER.value == "sat": - solve_cutting_stock_with_arc_flow_and_sat(_OUTPUT_PROTO.value, _PARAMS.value) - else: # 'mip' - solve_cutting_stock_with_arc_flow_and_mip() + """Main function.""" + if _SOLVER.value == "sat": + solve_cutting_stock_with_arc_flow_and_sat( + _OUTPUT_PROTO.value, _PARAMS.value + ) + else: # 'mip' + solve_cutting_stock_with_arc_flow_and_mip() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/assignment_with_constraints_sat.py b/examples/python/assignment_with_constraints_sat.py index f32d6299432..68761917309 100644 --- a/examples/python/assignment_with_constraints_sat.py +++ b/examples/python/assignment_with_constraints_sat.py @@ -20,109 +20,118 @@ def solve_assignment(): - """solve the assignment problem.""" - # Data. - cost = [ - [90, 76, 75, 70, 50, 74], - [35, 85, 55, 65, 48, 101], - [125, 95, 90, 105, 59, 120], - [45, 110, 95, 115, 104, 83], - [60, 105, 80, 75, 59, 62], - [45, 65, 110, 95, 47, 31], - [38, 51, 107, 41, 69, 99], - [47, 85, 57, 71, 92, 77], - [39, 63, 97, 49, 118, 56], - [47, 101, 71, 60, 88, 109], - [17, 39, 103, 64, 61, 92], - [101, 45, 83, 59, 92, 27], - ] - - group1 = [ - [0, 0, 1, 1], # Workers 2, 3 - [0, 1, 0, 1], # Workers 1, 3 - [0, 1, 1, 0], # Workers 1, 2 - [1, 1, 0, 0], # Workers 0, 1 - [1, 0, 1, 0], # Workers 0, 2 - ] - - group2 = [ - [0, 0, 1, 1], # Workers 6, 7 - [0, 1, 0, 1], # Workers 5, 7 - [0, 1, 1, 0], # Workers 5, 6 - [1, 1, 0, 0], # Workers 4, 5 - [1, 0, 0, 1], # Workers 4, 7 - ] - - group3 = [ - [0, 0, 1, 1], # Workers 10, 11 - [0, 1, 0, 1], # Workers 9, 11 - [0, 1, 1, 0], # Workers 9, 10 - [1, 0, 1, 0], # Workers 8, 10 - [1, 0, 0, 1], # Workers 8, 11 - ] - - sizes = [10, 7, 3, 12, 15, 4, 11, 5] - total_size_max = 15 - num_workers = len(cost) - num_tasks = len(cost[1]) - all_workers = range(num_workers) - all_tasks = range(num_tasks) - - # Model. - - model = cp_model.CpModel() - # Variables - selected = [ - [model.new_bool_var(f"x[{i},{j}]") for j in all_tasks] for i in all_workers - ] - works = [model.new_bool_var(f"works[{i}]") for i in all_workers] - - # Constraints - - # Link selected and workers. - for i in range(num_workers): - model.add_max_equality(works[i], selected[i]) - - # Each task is assigned to at least one worker. - for j in all_tasks: - model.add(sum(selected[i][j] for i in all_workers) >= 1) - - # Total task size for each worker is at most total_size_max - for i in all_workers: - model.add(sum(sizes[j] * selected[i][j] for j in all_tasks) <= total_size_max) - - # Group constraints. - model.add_allowed_assignments([works[0], works[1], works[2], works[3]], group1) - model.add_allowed_assignments([works[4], works[5], works[6], works[7]], group2) - model.add_allowed_assignments([works[8], works[9], works[10], works[11]], group3) - - # Objective - model.minimize( - sum(selected[i][j] * cost[i][j] for j in all_tasks for i in all_workers) + """solve the assignment problem.""" + # Data. + cost = [ + [90, 76, 75, 70, 50, 74], + [35, 85, 55, 65, 48, 101], + [125, 95, 90, 105, 59, 120], + [45, 110, 95, 115, 104, 83], + [60, 105, 80, 75, 59, 62], + [45, 65, 110, 95, 47, 31], + [38, 51, 107, 41, 69, 99], + [47, 85, 57, 71, 92, 77], + [39, 63, 97, 49, 118, 56], + [47, 101, 71, 60, 88, 109], + [17, 39, 103, 64, 61, 92], + [101, 45, 83, 59, 92, 27], + ] + + group1 = [ + [0, 0, 1, 1], # Workers 2, 3 + [0, 1, 0, 1], # Workers 1, 3 + [0, 1, 1, 0], # Workers 1, 2 + [1, 1, 0, 0], # Workers 0, 1 + [1, 0, 1, 0], # Workers 0, 2 + ] + + group2 = [ + [0, 0, 1, 1], # Workers 6, 7 + [0, 1, 0, 1], # Workers 5, 7 + [0, 1, 1, 0], # Workers 5, 6 + [1, 1, 0, 0], # Workers 4, 5 + [1, 0, 0, 1], # Workers 4, 7 + ] + + group3 = [ + [0, 0, 1, 1], # Workers 10, 11 + [0, 1, 0, 1], # Workers 9, 11 + [0, 1, 1, 0], # Workers 9, 10 + [1, 0, 1, 0], # Workers 8, 10 + [1, 0, 0, 1], # Workers 8, 11 + ] + + sizes = [10, 7, 3, 12, 15, 4, 11, 5] + total_size_max = 15 + num_workers = len(cost) + num_tasks = len(cost[1]) + all_workers = range(num_workers) + all_tasks = range(num_tasks) + + # Model. + + model = cp_model.CpModel() + # Variables + selected = [ + [model.new_bool_var(f"x[{i},{j}]") for j in all_tasks] + for i in all_workers + ] + works = [model.new_bool_var(f"works[{i}]") for i in all_workers] + + # Constraints + + # Link selected and workers. + for i in range(num_workers): + model.add_max_equality(works[i], selected[i]) + + # Each task is assigned to at least one worker. + for j in all_tasks: + model.add(sum(selected[i][j] for i in all_workers) >= 1) + + # Total task size for each worker is at most total_size_max + for i in all_workers: + model.add( + sum(sizes[j] * selected[i][j] for j in all_tasks) <= total_size_max ) - # Solve and output solution. - solver = cp_model.CpSolver() - status = solver.solve(model) - - if status == cp_model.OPTIMAL: - print(f"Total cost = {solver.objective_value}") - print() - for i in all_workers: - for j in all_tasks: - if solver.boolean_value(selected[i][j]): - print(f"Worker {i} assigned to task {j} with Cost = {cost[i][j]}") + # Group constraints. + model.add_allowed_assignments( + [works[0], works[1], works[2], works[3]], group1 + ) + model.add_allowed_assignments( + [works[4], works[5], works[6], works[7]], group2 + ) + model.add_allowed_assignments( + [works[8], works[9], works[10], works[11]], group3 + ) + + # Objective + model.minimize( + sum(selected[i][j] * cost[i][j] for j in all_tasks for i in all_workers) + ) + + # Solve and output solution. + solver = cp_model.CpSolver() + status = solver.solve(model) + + if status == cp_model.OPTIMAL: + print(f"Total cost = {solver.objective_value}") + print() + for i in all_workers: + for j in all_tasks: + if solver.boolean_value(selected[i][j]): + print(f"Worker {i} assigned to task {j} with Cost = {cost[i][j]}") - print() + print() - print(solver.response_stats()) + print(solver.response_stats()) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - solve_assignment() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + solve_assignment() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/balance_group_sat.py b/examples/python/balance_group_sat.py index 5f37f95605f..1c617c7101d 100644 --- a/examples/python/balance_group_sat.py +++ b/examples/python/balance_group_sat.py @@ -28,157 +28,162 @@ # Create a solution printer. class SolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" - - def __init__(self, values, colors, all_groups, all_items, item_in_group): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 - self.__values = values - self.__colors = colors - self.__all_groups = all_groups - self.__all_items = all_items - self.__item_in_group = item_in_group - - def on_solution_callback(self): - print(f"Solution {self.__solution_count}") - self.__solution_count += 1 - - print(f" objective value = {self.objective_value}") - groups = {} - sums = {} - for g in self.__all_groups: - groups[g] = [] - sums[g] = 0 - for item in self.__all_items: - if self.boolean_value(self.__item_in_group[(item, g)]): - groups[g].append(item) - sums[g] += self.__values[item] - - for g in self.__all_groups: - group = groups[g] - print(f"group {g}: sum = {sums[g]:0.2f} [", end="") - for item in group: - value = self.__values[item] - color = self.__colors[item] - print(f" ({item}, {value}, {color})", end="") - print("]") + """Print intermediate solutions.""" + + def __init__(self, values, colors, all_groups, all_items, item_in_group): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 + self.__values = values + self.__colors = colors + self.__all_groups = all_groups + self.__all_items = all_items + self.__item_in_group = item_in_group + + def on_solution_callback(self): + print(f"Solution {self.__solution_count}") + self.__solution_count += 1 + + print(f" objective value = {self.objective_value}") + groups = {} + sums = {} + for g in self.__all_groups: + groups[g] = [] + sums[g] = 0 + for item in self.__all_items: + if self.boolean_value(self.__item_in_group[(item, g)]): + groups[g].append(item) + sums[g] += self.__values[item] + + for g in self.__all_groups: + group = groups[g] + print(f"group {g}: sum = {sums[g]:0.2f} [", end="") + for item in group: + value = self.__values[item] + color = self.__colors[item] + print(f" ({item}, {value}, {color})", end="") + print("]") def main(argv: Sequence[str]) -> None: - """Solves a group balancing problem.""" - - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - # Data. - num_groups = 10 - num_items = 100 - num_colors = 3 - min_items_of_same_color_per_group = 4 - - all_groups = range(num_groups) - all_items = range(num_items) - all_colors = range(num_colors) - - # values for each items. - values = [1 + i + (i * i // 200) for i in all_items] - # Color for each item (simple modulo). - colors = [i % num_colors for i in all_items] - - sum_of_values = sum(values) - average_sum_per_group = sum_of_values // num_groups - - num_items_per_group = num_items // num_groups - - # Collect all items in a given color. - items_per_color: Dict[int, list[int]] = {} - for color in all_colors: - items_per_color[color] = [] - for i in all_items: - if colors[i] == color: - items_per_color[color].append(i) - - print( - f"Model has {num_items} items, {num_groups} groups, and" f" {num_colors} colors" - ) - print(f" average sum per group = {average_sum_per_group}") + """Solves a group balancing problem.""" + + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + # Data. + num_groups = 10 + num_items = 100 + num_colors = 3 + min_items_of_same_color_per_group = 4 + + all_groups = range(num_groups) + all_items = range(num_items) + all_colors = range(num_colors) + + # values for each items. + values = [1 + i + (i * i // 200) for i in all_items] + # Color for each item (simple modulo). + colors = [i % num_colors for i in all_items] + + sum_of_values = sum(values) + average_sum_per_group = sum_of_values // num_groups + + num_items_per_group = num_items // num_groups + + # Collect all items in a given color. + items_per_color: Dict[int, list[int]] = {} + for color in all_colors: + items_per_color[color] = [] + for i in all_items: + if colors[i] == color: + items_per_color[color].append(i) - # Model. + print( + f"Model has {num_items} items, {num_groups} groups, and" + f" {num_colors} colors" + ) + print(f" average sum per group = {average_sum_per_group}") - model = cp_model.CpModel() + # Model. - item_in_group = {} - for i in all_items: - for g in all_groups: - item_in_group[(i, g)] = model.new_bool_var(f"item {i} in group {g}") + model = cp_model.CpModel() - # Each group must have the same size. + item_in_group = {} + for i in all_items: for g in all_groups: - model.add(sum(item_in_group[(i, g)] for i in all_items) == num_items_per_group) + item_in_group[(i, g)] = model.new_bool_var(f"item {i} in group {g}") - # One item must belong to exactly one group. - for i in all_items: - model.add(sum(item_in_group[(i, g)] for g in all_groups) == 1) + # Each group must have the same size. + for g in all_groups: + model.add( + sum(item_in_group[(i, g)] for i in all_items) == num_items_per_group + ) + + # One item must belong to exactly one group. + for i in all_items: + model.add(sum(item_in_group[(i, g)] for g in all_groups) == 1) + + # The deviation of the sum of each items in a group against the average. + e = model.new_int_var(0, 550, "epsilon") + + # Constrain the sum of values in one group around the average sum per group. + for g in all_groups: + model.add( + sum(item_in_group[(i, g)] * values[i] for i in all_items) + <= average_sum_per_group + e + ) + model.add( + sum(item_in_group[(i, g)] * values[i] for i in all_items) + >= average_sum_per_group - e + ) - # The deviation of the sum of each items in a group against the average. - e = model.new_int_var(0, 550, "epsilon") + # color_in_group variables. + color_in_group = {} + for g in all_groups: + for c in all_colors: + color_in_group[(c, g)] = model.new_bool_var(f"color {c} is in group {g}") - # Constrain the sum of values in one group around the average sum per group. + # Item is in a group implies its color is in that group. + for i in all_items: for g in all_groups: - model.add( - sum(item_in_group[(i, g)] * values[i] for i in all_items) - <= average_sum_per_group + e - ) - model.add( - sum(item_in_group[(i, g)] * values[i] for i in all_items) - >= average_sum_per_group - e - ) - - # color_in_group variables. - color_in_group = {} + model.add_implication( + item_in_group[(i, g)], color_in_group[(colors[i], g)] + ) + + # If a color is in a group, it must contains at least + # min_items_of_same_color_per_group items from that color. + for c in all_colors: for g in all_groups: - for c in all_colors: - color_in_group[(c, g)] = model.new_bool_var(f"color {c} is in group {g}") + literal = color_in_group[(c, g)] + model.add( + sum(item_in_group[(i, g)] for i in items_per_color[c]) + >= min_items_of_same_color_per_group + ).only_enforce_if(literal) - # Item is in a group implies its color is in that group. - for i in all_items: - for g in all_groups: - model.add_implication(item_in_group[(i, g)], color_in_group[(colors[i], g)]) + # Compute the maximum number of colors in a group. + max_color = num_items_per_group // min_items_of_same_color_per_group - # If a color is in a group, it must contains at least - # min_items_of_same_color_per_group items from that color. - for c in all_colors: - for g in all_groups: - literal = color_in_group[(c, g)] - model.add( - sum(item_in_group[(i, g)] for i in items_per_color[c]) - >= min_items_of_same_color_per_group - ).only_enforce_if(literal) - - # Compute the maximum number of colors in a group. - max_color = num_items_per_group // min_items_of_same_color_per_group - - # Redundant constraint, it helps with solving time. - if max_color < num_colors: - for g in all_groups: - model.add(sum(color_in_group[(c, g)] for c in all_colors) <= max_color) - - # minimize epsilon - model.minimize(e) - - solver = cp_model.CpSolver() - # solver.parameters.log_search_progress = True - solver.parameters.num_workers = 16 - solution_printer = SolutionPrinter( - values, colors, all_groups, all_items, item_in_group - ) - status = solver.solve(model, solution_printer) + # Redundant constraint, it helps with solving time. + if max_color < num_colors: + for g in all_groups: + model.add(sum(color_in_group[(c, g)] for c in all_colors) <= max_color) + + # minimize epsilon + model.minimize(e) + + solver = cp_model.CpSolver() + # solver.parameters.log_search_progress = True + solver.parameters.num_workers = 16 + solution_printer = SolutionPrinter( + values, colors, all_groups, all_items, item_in_group + ) + status = solver.solve(model, solution_printer) - if status == cp_model.OPTIMAL: - print(f"Optimal epsilon: {solver.objective_value}") - print(solver.response_stats()) - else: - print("No solution found") + if status == cp_model.OPTIMAL: + print(f"Optimal epsilon: {solver.objective_value}") + print(solver.response_stats()) + else: + print("No solution found") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/bus_driver_scheduling_flow_sat.py b/examples/python/bus_driver_scheduling_flow_sat.py index 275a8b41310..aa8368d2f52 100644 --- a/examples/python/bus_driver_scheduling_flow_sat.py +++ b/examples/python/bus_driver_scheduling_flow_sat.py @@ -29,11 +29,13 @@ from ortools.sat.python import cp_model PARSER = argparse.ArgumentParser() -PARSER.add_argument("--instance", default=1, type=int, help="Instance number (1..3).") +PARSER.add_argument( + "--instance", default=1, type=int, help="Instance number (1..3)." +) PARSER.add_argument( "--output_proto_file", default="", - help="Output file to write the cp_model" "proto to.", + help="Output file to write the cp_modelproto to.", ) PARSER.add_argument("--params", default="", help="Sat solver parameters.") @@ -1663,165 +1665,169 @@ def find_minimum_number_of_drivers(shifts, params): - """Minimize the number of needed drivers.""" + """Minimize the number of needed drivers.""" - num_shifts = len(shifts) + num_shifts = len(shifts) - # All durations are in minutes. - max_driving_time = 540 # 8 hours. - max_driving_time_without_pauses = 240 # 4 hours - min_pause_after_4h = 30 - min_delay_between_shifts = 2 - max_working_time = 720 - min_working_time = 390 # 6.5 hours - extra_time = 10 + 25 - max_break = 180 + # All durations are in minutes. + max_driving_time = 540 # 8 hours. + max_driving_time_without_pauses = 240 # 4 hours + min_pause_after_4h = 30 + min_delay_between_shifts = 2 + max_working_time = 720 + min_working_time = 390 # 6.5 hours + extra_time = 10 + 25 + max_break = 180 - # Computed data. - total_driving_time = sum(shift[5] for shift in shifts) - min_num_drivers = int(math.ceil(total_driving_time * 1.0 / max_driving_time)) - min_start_time = min(shift[3] for shift in shifts) - max_end_time = max(shift[4] for shift in shifts) + # Computed data. + total_driving_time = sum(shift[5] for shift in shifts) + min_num_drivers = int(math.ceil(total_driving_time * 1.0 / max_driving_time)) + min_start_time = min(shift[3] for shift in shifts) + max_end_time = max(shift[4] for shift in shifts) - print("Bus driver scheduling") - print(" num shifts =", num_shifts) - print(" total driving time =", total_driving_time, "minutes") - print(" min num drivers =", min_num_drivers) - print(" min start time =", min_start_time) - print(" max end time =", max_end_time) + print("Bus driver scheduling") + print(" num shifts =", num_shifts) + print(" total driving time =", total_driving_time, "minutes") + print(" min num drivers =", min_num_drivers) + print(" min start time =", min_start_time) + print(" max end time =", max_end_time) - # We are going to build a flow from a the start of the day to the end - # of the day. - # - # Along the path, we will accumulate driving time, accrued time since the - # last break, and total working time. + # We are going to build a flow from a the start of the day to the end + # of the day. + # + # Along the path, we will accumulate driving time, accrued time since the + # last break, and total working time. - model = cp_model.CpModel() + model = cp_model.CpModel() - # Per node info - driving_time = {} - working_time = {} - no_break_driving_time = {} + # Per node info + driving_time = {} + working_time = {} + no_break_driving_time = {} - incoming_literals = collections.defaultdict(list) - outgoing_literals = collections.defaultdict(list) - outgoing_source_literals = [] - incoming_sink_literals = [] + incoming_literals = collections.defaultdict(list) + outgoing_literals = collections.defaultdict(list) + outgoing_source_literals = [] + incoming_sink_literals = [] - all_literals = [] + all_literals = [] - # Create all the shift variables before iterating on the transitions - # between these shifts. - for shift in range(num_shifts): - driving_time[shift] = model.NewIntVar(0, max_driving_time, "dt_%i" % shift) - no_break_driving_time[shift] = model.NewIntVar( - 0, max_driving_time_without_pauses, "nbdt_%i" % shift - ) - working_time[shift] = model.NewIntVar(0, max_working_time, "wt_%i" % shift) + # Create all the shift variables before iterating on the transitions + # between these shifts. + for shift in range(num_shifts): + driving_time[shift] = model.NewIntVar(0, max_driving_time, "dt_%i" % shift) + no_break_driving_time[shift] = model.NewIntVar( + 0, max_driving_time_without_pauses, "nbdt_%i" % shift + ) + working_time[shift] = model.NewIntVar(0, max_working_time, "wt_%i" % shift) - for shift in range(num_shifts): - duration = shifts[shift][5] + for shift in range(num_shifts): + duration = shifts[shift][5] - # Arc from source to shift. - # - set the working time of the driver - # - increase driving time and driving time since the last break - source_lit = model.NewBoolVar("from source to %i" % shift) - all_literals.append(source_lit) - outgoing_source_literals.append(source_lit) - incoming_literals[shift].append(source_lit) - model.Add(driving_time[shift] == duration).OnlyEnforceIf(source_lit) - model.Add(no_break_driving_time[shift] == duration).OnlyEnforceIf(source_lit) - model.Add(working_time[shift] == duration + extra_time).OnlyEnforceIf( - source_lit - ) + # Arc from source to shift. + # - set the working time of the driver + # - increase driving time and driving time since the last break + source_lit = model.NewBoolVar("from source to %i" % shift) + all_literals.append(source_lit) + outgoing_source_literals.append(source_lit) + incoming_literals[shift].append(source_lit) + model.Add(driving_time[shift] == duration).OnlyEnforceIf(source_lit) + model.Add(no_break_driving_time[shift] == duration).OnlyEnforceIf( + source_lit + ) + model.Add(working_time[shift] == duration + extra_time).OnlyEnforceIf( + source_lit + ) - # Arc from shift to sink - # - checks that working time is greater than min_working_time - sink_lit = model.NewBoolVar("from %i to sink" % shift) - all_literals.append(sink_lit) - outgoing_literals[shift].append(sink_lit) - incoming_sink_literals.append(sink_lit) - model.Add(working_time[shift] >= min_working_time).OnlyEnforceIf(sink_lit) + # Arc from shift to sink + # - checks that working time is greater than min_working_time + sink_lit = model.NewBoolVar("from %i to sink" % shift) + all_literals.append(sink_lit) + outgoing_literals[shift].append(sink_lit) + incoming_sink_literals.append(sink_lit) + model.Add(working_time[shift] >= min_working_time).OnlyEnforceIf(sink_lit) - for other in range(num_shifts): - delay = shifts[other][3] - shifts[shift][4] - if delay < min_delay_between_shifts: - continue - if delay > max_break: - break # Assumes start times are sorted. - other_duration = shifts[other][5] - lit = model.NewBoolVar("from %i to %i" % (shift, other)) - all_literals.append(lit) + for other in range(num_shifts): + delay = shifts[other][3] - shifts[shift][4] + if delay < min_delay_between_shifts: + continue + if delay > max_break: + break # Assumes start times are sorted. + other_duration = shifts[other][5] + lit = model.NewBoolVar("from %i to %i" % (shift, other)) + all_literals.append(lit) - # Increase driving time - model.Add( - driving_time[other] == driving_time[shift] + other_duration - ).OnlyEnforceIf(lit) + # Increase driving time + model.Add( + driving_time[other] == driving_time[shift] + other_duration + ).OnlyEnforceIf(lit) - # Increase no_break_driving or reset it to 0 depending on the delay - if delay >= min_pause_after_4h: - model.Add(no_break_driving_time[other] == other_duration).OnlyEnforceIf( - lit - ) - else: - model.Add( - no_break_driving_time[other] - == no_break_driving_time[shift] + other_duration - ).OnlyEnforceIf(lit) + # Increase no_break_driving or reset it to 0 depending on the delay + if delay >= min_pause_after_4h: + model.Add(no_break_driving_time[other] == other_duration).OnlyEnforceIf( + lit + ) + else: + model.Add( + no_break_driving_time[other] + == no_break_driving_time[shift] + other_duration + ).OnlyEnforceIf(lit) - # Increase working time - model.Add( - working_time[other] == working_time[shift] + delay + other_duration - ).OnlyEnforceIf(lit) + # Increase working time + model.Add( + working_time[other] == working_time[shift] + delay + other_duration + ).OnlyEnforceIf(lit) - # Add arc - outgoing_literals[shift].append(lit) - incoming_literals[other].append(lit) + # Add arc + outgoing_literals[shift].append(lit) + incoming_literals[other].append(lit) - # Create dag constraint. - for shift in range(num_shifts): - model.Add(sum(outgoing_literals[shift]) == 1) - model.Add(sum(incoming_literals[shift]) == 1) + # Create dag constraint. + for shift in range(num_shifts): + model.Add(sum(outgoing_literals[shift]) == 1) + model.Add(sum(incoming_literals[shift]) == 1) - # Num drivers - num_drivers = model.NewIntVar(min_num_drivers, min_num_drivers * 3, "num_drivers") - model.Add(sum(incoming_sink_literals) == num_drivers) - model.Add(sum(outgoing_source_literals) == num_drivers) + # Num drivers + num_drivers = model.NewIntVar( + min_num_drivers, min_num_drivers * 3, "num_drivers" + ) + model.Add(sum(incoming_sink_literals) == num_drivers) + model.Add(sum(outgoing_source_literals) == num_drivers) - model.Minimize(num_drivers) + model.Minimize(num_drivers) - # Solve model. - solver = cp_model.CpSolver() - solver.parameters.log_search_progress = True - # solver.parameters.num_search_workers = 16 - # solver.parameters.boolean_encoding_level = 0 - # solver.parameters.lns_focus_on_decision_variables = True - status = solver.Solve(model) + # Solve model. + solver = cp_model.CpSolver() + solver.parameters.log_search_progress = True + # solver.parameters.num_search_workers = 16 + # solver.parameters.boolean_encoding_level = 0 + # solver.parameters.lns_focus_on_decision_variables = True + status = solver.Solve(model) - if status != cp_model.OPTIMAL and status != cp_model.FEASIBLE: - return -1 + if status != cp_model.OPTIMAL and status != cp_model.FEASIBLE: + return -1 - # Display solution - optimal_num_drivers = int(solver.ObjectiveValue()) - print("minimal number of drivers =", optimal_num_drivers) - return optimal_num_drivers + # Display solution + optimal_num_drivers = int(solver.ObjectiveValue()) + print("minimal number of drivers =", optimal_num_drivers) + return optimal_num_drivers def main(args): - """Optimize the bus driver allocation in two passes.""" - print("----------- first pass: minimize the number of drivers") - shifts = [] - if args.instance == 1: - shifts = SAMPLE_SHIFTS_SMALL - elif args.instance == 2: - shifts = SAMPLE_SHIFTS_MEDIUM - elif args.instance == 3: - shifts = SAMPLE_SHIFTS_LARGE - num_drivers = find_minimum_number_of_drivers(shifts, args.params) + """Optimize the bus driver allocation in two passes.""" + print("----------- first pass: minimize the number of drivers") + shifts = [] + if args.instance == 1: + shifts = SAMPLE_SHIFTS_SMALL + elif args.instance == 2: + shifts = SAMPLE_SHIFTS_MEDIUM + elif args.instance == 3: + shifts = SAMPLE_SHIFTS_LARGE + num_drivers = find_minimum_number_of_drivers(shifts, args.params) - print("----------- second pass: minimize the sum of working times") - # bus_driver_scheduling(False, num_drivers) + print("----------- second pass: minimize the sum of working times") + # bus_driver_scheduling(False, num_drivers) if __name__ == "__main__": - main(PARSER.parse_args()) + main(PARSER.parse_args()) diff --git a/examples/python/bus_driver_scheduling_sat.py b/examples/python/bus_driver_scheduling_sat.py index 0a957febd2e..6423fe06ffc 100644 --- a/examples/python/bus_driver_scheduling_sat.py +++ b/examples/python/bus_driver_scheduling_sat.py @@ -1709,327 +1709,339 @@ def bus_driver_scheduling(minimize_drivers: bool, max_num_drivers: int) -> int: - """Optimize the bus driver scheduling problem. + """Optimize the bus driver scheduling problem. - This model has two modes. + This model has two modes. - If minimize_drivers == True, the objective will be to find the minimal - number of drivers, independently of the working times of each drivers. + If minimize_drivers == True, the objective will be to find the minimal + number of drivers, independently of the working times of each drivers. - Otherwise, will will create max_num_drivers non optional drivers, and - minimize the sum of working times of these drivers. + Otherwise, will will create max_num_drivers non optional drivers, and + minimize the sum of working times of these drivers. - Args: - minimize_drivers: A Boolean parameter specifying the objective of the - problem. If True, it tries to minimize the number of used drivers. If - false, it minimizes the sum of working times per workers. - max_num_drivers: This number specifies the exact number of non optional - drivers to use. This is only used if 'minimize_drivers' is False. + Args: + minimize_drivers: A Boolean parameter specifying the objective of the + problem. If True, it tries to minimize the number of used drivers. If + false, it minimizes the sum of working times per workers. + max_num_drivers: This number specifies the exact number of non optional + drivers to use. This is only used if 'minimize_drivers' is False. - Returns: - The objective value of the model. - """ - shifts = None - if _INSTANCE.value == 0: - shifts = SAMPLE_SHIFTS_TINY - elif _INSTANCE.value == 1: - shifts = SAMPLE_SHIFTS_SMALL - elif _INSTANCE.value == 2: - shifts = SAMPLE_SHIFTS_MEDIUM - elif _INSTANCE.value == 3: - shifts = SAMPLE_SHIFTS_LARGE + Returns: + The objective value of the model. + """ + shifts = None + if _INSTANCE.value == 0: + shifts = SAMPLE_SHIFTS_TINY + elif _INSTANCE.value == 1: + shifts = SAMPLE_SHIFTS_SMALL + elif _INSTANCE.value == 2: + shifts = SAMPLE_SHIFTS_MEDIUM + elif _INSTANCE.value == 3: + shifts = SAMPLE_SHIFTS_LARGE - num_shifts = len(shifts) + num_shifts = len(shifts) - # All durations are in minutes. - max_driving_time = 540 # 8 hours. - max_driving_time_without_pauses = 240 # 4 hours - min_pause_after_4h = 30 - min_delay_between_shifts = 2 - max_working_time = 720 - min_working_time = 390 # 6.5 hours - setup_time = 10 - cleanup_time = 15 + # All durations are in minutes. + max_driving_time = 540 # 8 hours. + max_driving_time_without_pauses = 240 # 4 hours + min_pause_after_4h = 30 + min_delay_between_shifts = 2 + max_working_time = 720 + min_working_time = 390 # 6.5 hours + setup_time = 10 + cleanup_time = 15 - # Computed data. - total_driving_time = sum(shift[5] for shift in shifts) - min_num_drivers = int(math.ceil(total_driving_time * 1.0 / max_driving_time)) - num_drivers = 2 * min_num_drivers if minimize_drivers else max_num_drivers - min_start_time = min(shift[3] for shift in shifts) - max_end_time = max(shift[4] for shift in shifts) + # Computed data. + total_driving_time = sum(shift[5] for shift in shifts) + min_num_drivers = int(math.ceil(total_driving_time * 1.0 / max_driving_time)) + num_drivers = 2 * min_num_drivers if minimize_drivers else max_num_drivers + min_start_time = min(shift[3] for shift in shifts) + max_end_time = max(shift[4] for shift in shifts) - print("Bus driver scheduling") - print(" num shifts =", num_shifts) - print(" total driving time =", total_driving_time, "minutes") - print(" min num drivers =", min_num_drivers) - print(" num drivers =", num_drivers) - print(" min start time =", min_start_time) - print(" max end time =", max_end_time) + print("Bus driver scheduling") + print(" num shifts =", num_shifts) + print(" total driving time =", total_driving_time, "minutes") + print(" min num drivers =", min_num_drivers) + print(" num drivers =", num_drivers) + print(" min start time =", min_start_time) + print(" max end time =", max_end_time) - model = cp_model.CpModel() + model = cp_model.CpModel() - # For each driver and each shift, we store: - # - the total driving time including this shift - # - the acrued driving time since the last 30 minute break - # Special arcs have the following effect: - # - 'from source to shift' sets the starting time and accumulate the first - # shift - # - 'from shift to end' sets the ending time, and fill the driving_times - # variable - # Arcs between two shifts have the following impact - # - add the duration of the shift to the total driving time - # - reset the accumulated driving time if the distance between the two - # shifts is more than 30 minutes, add the duration of the shift to the - # accumulated driving time since the last break otherwise + # For each driver and each shift, we store: + # - the total driving time including this shift + # - the acrued driving time since the last 30 minute break + # Special arcs have the following effect: + # - 'from source to shift' sets the starting time and accumulate the first + # shift + # - 'from shift to end' sets the ending time, and fill the driving_times + # variable + # Arcs between two shifts have the following impact + # - add the duration of the shift to the total driving time + # - reset the accumulated driving time if the distance between the two + # shifts is more than 30 minutes, add the duration of the shift to the + # accumulated driving time since the last break otherwise - # Per (driver, node) info (driving time, performed, - # driving time since break) - total_driving = {} - no_break_driving = {} - performed = {} - starting_shifts = {} + # Per (driver, node) info (driving time, performed, + # driving time since break) + total_driving = {} + no_break_driving = {} + performed = {} + starting_shifts = {} - # Per driver info (start, end, driving times, is working) - start_times = [] - end_times = [] - driving_times = [] - working_drivers = [] - working_times = [] + # Per driver info (start, end, driving times, is working) + start_times = [] + end_times = [] + driving_times = [] + working_drivers = [] + working_times = [] - # Weighted objective - delay_literals = [] - delay_weights = [] + # Weighted objective + delay_literals = [] + delay_weights = [] - # Used to propagate more between drivers - shared_incoming_literals = collections.defaultdict(list) - shared_outgoing_literals = collections.defaultdict(list) + # Used to propagate more between drivers + shared_incoming_literals = collections.defaultdict(list) + shared_outgoing_literals = collections.defaultdict(list) - for d in range(num_drivers): - start_times.append( - model.new_int_var(min_start_time - setup_time, max_end_time, "start_%i" % d) + for d in range(num_drivers): + start_times.append( + model.new_int_var( + min_start_time - setup_time, max_end_time, "start_%i" % d ) - end_times.append( - model.new_int_var(min_start_time, max_end_time + cleanup_time, "end_%i" % d) + ) + end_times.append( + model.new_int_var( + min_start_time, max_end_time + cleanup_time, "end_%i" % d ) - driving_times.append(model.new_int_var(0, max_driving_time, "driving_%i" % d)) - working_times.append( - model.new_int_var(0, max_working_time, "working_times_%i" % d) - ) - - incoming_literals = collections.defaultdict(list) - outgoing_literals = collections.defaultdict(list) - outgoing_source_literals = [] - incoming_sink_literals = [] + ) + driving_times.append( + model.new_int_var(0, max_driving_time, "driving_%i" % d) + ) + working_times.append( + model.new_int_var(0, max_working_time, "working_times_%i" % d) + ) - # Create all the shift variables before iterating on the transitions - # between these shifts. - for s in range(num_shifts): - total_driving[d, s] = model.new_int_var( - 0, max_driving_time, "dr_%i_%i" % (d, s) - ) - no_break_driving[d, s] = model.new_int_var( - 0, max_driving_time_without_pauses, "mdr_%i_%i" % (d, s) - ) - performed[d, s] = model.new_bool_var("performed_%i_%i" % (d, s)) + incoming_literals = collections.defaultdict(list) + outgoing_literals = collections.defaultdict(list) + outgoing_source_literals = [] + incoming_sink_literals = [] - for s in range(num_shifts): - shift = shifts[s] - duration = shift[5] + # Create all the shift variables before iterating on the transitions + # between these shifts. + for s in range(num_shifts): + total_driving[d, s] = model.new_int_var( + 0, max_driving_time, "dr_%i_%i" % (d, s) + ) + no_break_driving[d, s] = model.new_int_var( + 0, max_driving_time_without_pauses, "mdr_%i_%i" % (d, s) + ) + performed[d, s] = model.new_bool_var("performed_%i_%i" % (d, s)) - # Arc from source to shift. - # - set the start time of the driver - # - increase driving time and driving time since break - source_lit = model.new_bool_var("%i from source to %i" % (d, s)) - outgoing_source_literals.append(source_lit) - incoming_literals[s].append(source_lit) - shared_incoming_literals[s].append(source_lit) - model.add(start_times[d] == shift[3] - setup_time).only_enforce_if( - source_lit - ) - model.add(total_driving[d, s] == duration).only_enforce_if(source_lit) - model.add(no_break_driving[d, s] == duration).only_enforce_if(source_lit) - starting_shifts[d, s] = source_lit + for s in range(num_shifts): + shift = shifts[s] + duration = shift[5] - # Arc from shift to sink - # - set the end time of the driver - # - set the driving times of the driver - sink_lit = model.new_bool_var("%i from %i to sink" % (d, s)) - outgoing_literals[s].append(sink_lit) - shared_outgoing_literals[s].append(sink_lit) - incoming_sink_literals.append(sink_lit) - model.add(end_times[d] == shift[4] + cleanup_time).only_enforce_if(sink_lit) - model.add(driving_times[d] == total_driving[d, s]).only_enforce_if(sink_lit) + # Arc from source to shift. + # - set the start time of the driver + # - increase driving time and driving time since break + source_lit = model.new_bool_var("%i from source to %i" % (d, s)) + outgoing_source_literals.append(source_lit) + incoming_literals[s].append(source_lit) + shared_incoming_literals[s].append(source_lit) + model.add(start_times[d] == shift[3] - setup_time).only_enforce_if( + source_lit + ) + model.add(total_driving[d, s] == duration).only_enforce_if(source_lit) + model.add(no_break_driving[d, s] == duration).only_enforce_if(source_lit) + starting_shifts[d, s] = source_lit - # Node not performed - # - set both driving times to 0 - # - add a looping arc on the node - model.add(total_driving[d, s] == 0).only_enforce_if(~performed[d, s]) - model.add(no_break_driving[d, s] == 0).only_enforce_if(~performed[d, s]) - incoming_literals[s].append(~performed[d, s]) - outgoing_literals[s].append(~performed[d, s]) - # negated adding to the shared lists, because, globally, each node will - # have one incoming literal, and one outgoing literal. + # Arc from shift to sink + # - set the end time of the driver + # - set the driving times of the driver + sink_lit = model.new_bool_var("%i from %i to sink" % (d, s)) + outgoing_literals[s].append(sink_lit) + shared_outgoing_literals[s].append(sink_lit) + incoming_sink_literals.append(sink_lit) + model.add(end_times[d] == shift[4] + cleanup_time).only_enforce_if( + sink_lit + ) + model.add(driving_times[d] == total_driving[d, s]).only_enforce_if( + sink_lit + ) - # Node performed: - # - add upper bound on start_time - # - add lower bound on end_times - model.add(start_times[d] <= shift[3] - setup_time).only_enforce_if( - performed[d, s] - ) - model.add(end_times[d] >= shift[4] + cleanup_time).only_enforce_if( - performed[d, s] - ) + # Node not performed + # - set both driving times to 0 + # - add a looping arc on the node + model.add(total_driving[d, s] == 0).only_enforce_if(~performed[d, s]) + model.add(no_break_driving[d, s] == 0).only_enforce_if(~performed[d, s]) + incoming_literals[s].append(~performed[d, s]) + outgoing_literals[s].append(~performed[d, s]) + # negated adding to the shared lists, because, globally, each node will + # have one incoming literal, and one outgoing literal. - for o in range(num_shifts): - other = shifts[o] - delay = other[3] - shift[4] - if delay < min_delay_between_shifts: - continue - lit = model.new_bool_var("%i from %i to %i" % (d, s, o)) + # Node performed: + # - add upper bound on start_time + # - add lower bound on end_times + model.add(start_times[d] <= shift[3] - setup_time).only_enforce_if( + performed[d, s] + ) + model.add(end_times[d] >= shift[4] + cleanup_time).only_enforce_if( + performed[d, s] + ) - # Increase driving time - model.add( - total_driving[d, o] == total_driving[d, s] + other[5] - ).only_enforce_if(lit) + for o in range(num_shifts): + other = shifts[o] + delay = other[3] - shift[4] + if delay < min_delay_between_shifts: + continue + lit = model.new_bool_var("%i from %i to %i" % (d, s, o)) - # Increase no_break_driving or reset it to 0 depending on the delay - if delay >= min_pause_after_4h: - model.add(no_break_driving[d, o] == other[5]).only_enforce_if(lit) - else: - model.add( - no_break_driving[d, o] == no_break_driving[d, s] + other[5] - ).only_enforce_if(lit) + # Increase driving time + model.add( + total_driving[d, o] == total_driving[d, s] + other[5] + ).only_enforce_if(lit) - # add arc - outgoing_literals[s].append(lit) - shared_outgoing_literals[s].append(lit) - incoming_literals[o].append(lit) - shared_incoming_literals[o].append(lit) + # Increase no_break_driving or reset it to 0 depending on the delay + if delay >= min_pause_after_4h: + model.add(no_break_driving[d, o] == other[5]).only_enforce_if(lit) + else: + model.add( + no_break_driving[d, o] == no_break_driving[d, s] + other[5] + ).only_enforce_if(lit) - # Cost part - delay_literals.append(lit) - delay_weights.append(delay) + # add arc + outgoing_literals[s].append(lit) + shared_outgoing_literals[s].append(lit) + incoming_literals[o].append(lit) + shared_incoming_literals[o].append(lit) - model.add(working_times[d] == end_times[d] - start_times[d]) + # Cost part + delay_literals.append(lit) + delay_weights.append(delay) - if minimize_drivers: - # Driver is not working. - working = model.new_bool_var("working_%i" % d) - model.add(start_times[d] == min_start_time).only_enforce_if(~working) - model.add(end_times[d] == min_start_time).only_enforce_if(~working) - model.add(driving_times[d] == 0).only_enforce_if(~working) - working_drivers.append(working) - outgoing_source_literals.append(~working) - incoming_sink_literals.append(~working) - # Conditional working time constraints - model.add(working_times[d] >= min_working_time).only_enforce_if(working) - model.add(working_times[d] == 0).only_enforce_if(~working) - else: - # Working time constraints - model.add(working_times[d] >= min_working_time) + model.add(working_times[d] == end_times[d] - start_times[d]) - # Create circuit constraint. - model.add_exactly_one(outgoing_source_literals) - for s in range(num_shifts): - model.add_exactly_one(outgoing_literals[s]) - model.add_exactly_one(incoming_literals[s]) - model.add_exactly_one(incoming_sink_literals) + if minimize_drivers: + # Driver is not working. + working = model.new_bool_var("working_%i" % d) + model.add(start_times[d] == min_start_time).only_enforce_if(~working) + model.add(end_times[d] == min_start_time).only_enforce_if(~working) + model.add(driving_times[d] == 0).only_enforce_if(~working) + working_drivers.append(working) + outgoing_source_literals.append(~working) + incoming_sink_literals.append(~working) + # Conditional working time constraints + model.add(working_times[d] >= min_working_time).only_enforce_if(working) + model.add(working_times[d] == 0).only_enforce_if(~working) + else: + # Working time constraints + model.add(working_times[d] >= min_working_time) - # Each shift is covered. + # Create circuit constraint. + model.add_exactly_one(outgoing_source_literals) for s in range(num_shifts): - model.add_exactly_one(performed[d, s] for d in range(num_drivers)) - # Globally, each node has one incoming and one outgoing literal - model.add_exactly_one(shared_incoming_literals[s]) - model.add_exactly_one(shared_outgoing_literals[s]) + model.add_exactly_one(outgoing_literals[s]) + model.add_exactly_one(incoming_literals[s]) + model.add_exactly_one(incoming_sink_literals) - # Symmetry breaking + # Each shift is covered. + for s in range(num_shifts): + model.add_exactly_one(performed[d, s] for d in range(num_drivers)) + # Globally, each node has one incoming and one outgoing literal + model.add_exactly_one(shared_incoming_literals[s]) + model.add_exactly_one(shared_outgoing_literals[s]) - # The first 3 shifts must be performed by 3 different drivers. - # Let's assign them to the first 3 drivers in sequence - model.add(starting_shifts[0, 0] == 1) - model.add(starting_shifts[1, 1] == 1) - model.add(starting_shifts[2, 2] == 1) + # Symmetry breaking - if minimize_drivers: - # Push non working drivers to the end - for d in range(num_drivers - 1): - model.add_implication(~working_drivers[d], ~working_drivers[d + 1]) + # The first 3 shifts must be performed by 3 different drivers. + # Let's assign them to the first 3 drivers in sequence + model.add(starting_shifts[0, 0] == 1) + model.add(starting_shifts[1, 1] == 1) + model.add(starting_shifts[2, 2] == 1) - # Redundant constraints: sum of driving times = sum of shift driving times - model.add(cp_model.LinearExpr.sum(driving_times) == total_driving_time) - if not minimize_drivers: - model.add( - cp_model.LinearExpr.sum(working_times) - == total_driving_time - + num_drivers * (setup_time + cleanup_time) - + cp_model.LinearExpr.weighted_sum(delay_literals, delay_weights) - ) + if minimize_drivers: + # Push non working drivers to the end + for d in range(num_drivers - 1): + model.add_implication(~working_drivers[d], ~working_drivers[d + 1]) - if minimize_drivers: - # minimize the number of working drivers - model.minimize(cp_model.LinearExpr.sum(working_drivers)) - else: - # minimize the sum of delays between tasks, which in turns minimize the - # sum of working times as the total driving time is fixed - model.minimize(cp_model.LinearExpr.weighted_sum(delay_literals, delay_weights)) + # Redundant constraints: sum of driving times = sum of shift driving times + model.add(cp_model.LinearExpr.sum(driving_times) == total_driving_time) + if not minimize_drivers: + model.add( + cp_model.LinearExpr.sum(working_times) + == total_driving_time + + num_drivers * (setup_time + cleanup_time) + + cp_model.LinearExpr.weighted_sum(delay_literals, delay_weights) + ) - if not minimize_drivers and _OUTPUT_PROTO.value: - print("Writing proto to %s" % _OUTPUT_PROTO.value) - with open(_OUTPUT_PROTO.value, "w") as text_file: - text_file.write(str(model)) + if minimize_drivers: + # minimize the number of working drivers + model.minimize(cp_model.LinearExpr.sum(working_drivers)) + else: + # minimize the sum of delays between tasks, which in turns minimize the + # sum of working times as the total driving time is fixed + model.minimize( + cp_model.LinearExpr.weighted_sum(delay_literals, delay_weights) + ) - # Solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) + if not minimize_drivers and _OUTPUT_PROTO.value: + print("Writing proto to %s" % _OUTPUT_PROTO.value) + with open(_OUTPUT_PROTO.value, "w") as text_file: + text_file.write(str(model)) - status = solver.solve(model) + # Solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) - if status != cp_model.OPTIMAL and status != cp_model.FEASIBLE: - return -1 + status = solver.solve(model) - # Display solution - if minimize_drivers: - max_num_drivers = int(solver.objective_value) - print("minimal number of drivers =", max_num_drivers) - return max_num_drivers + if status != cp_model.OPTIMAL and status != cp_model.FEASIBLE: + return -1 - for d in range(num_drivers): - print("Driver %i: " % (d + 1)) - print(" total driving time =", solver.value(driving_times[d])) - print( - " working time =", - solver.value(working_times[d]) + setup_time + cleanup_time, - ) + # Display solution + if minimize_drivers: + max_num_drivers = int(solver.objective_value) + print("minimal number of drivers =", max_num_drivers) + return max_num_drivers + + for d in range(num_drivers): + print("Driver %i: " % (d + 1)) + print(" total driving time =", solver.value(driving_times[d])) + print( + " working time =", + solver.value(working_times[d]) + setup_time + cleanup_time, + ) - first = True - for s in range(num_shifts): - shift = shifts[s] + first = True + for s in range(num_shifts): + shift = shifts[s] - if not solver.boolean_value(performed[d, s]): - continue + if not solver.boolean_value(performed[d, s]): + continue - # Hack to detect if the waiting time between the last shift and - # this one exceeds 30 minutes. For this, we look at the - # no_break_driving which was reinitialized in that case. - if solver.value(no_break_driving[d, s]) == shift[5] and not first: - print(" **break**") - print(" shift ", shift[0], ":", shift[1], "-", shift[2]) - first = False + # Hack to detect if the waiting time between the last shift and + # this one exceeds 30 minutes. For this, we look at the + # no_break_driving which was reinitialized in that case. + if solver.value(no_break_driving[d, s]) == shift[5] and not first: + print(" **break**") + print(" shift ", shift[0], ":", shift[1], "-", shift[2]) + first = False - return int(solver.objective_value) + return int(solver.objective_value) def main(_): - """Optimize the bus driver allocation in two passes.""" - print("----------- first pass: minimize the number of drivers") - num_drivers = bus_driver_scheduling(True, -1) - if num_drivers == -1: - print("no solution found, skipping the final step") - else: - print("----------- second pass: minimize the sum of working times") - bus_driver_scheduling(False, num_drivers) + """Optimize the bus driver allocation in two passes.""" + print("----------- first pass: minimize the number of drivers") + num_drivers = bus_driver_scheduling(True, -1) + if num_drivers == -1: + print("no solution found, skipping the final step") + else: + print("----------- second pass: minimize the sum of working times") + bus_driver_scheduling(False, num_drivers) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/chemical_balance_lp.py b/examples/python/chemical_balance_lp.py index 82f5b6b6836..ca684cb1299 100755 --- a/examples/python/chemical_balance_lp.py +++ b/examples/python/chemical_balance_lp.py @@ -48,24 +48,31 @@ # Model max_set = [ - min(max_quantities[q][1] / chemical_set[s][q + 1] for q in ALL_PRODUCTS - if chemical_set[s][q + 1] != 0.0) for s in ALL_SETS + min( + max_quantities[q][1] / chemical_set[s][q + 1] + for q in ALL_PRODUCTS + if chemical_set[s][q + 1] != 0.0 + ) + for s in ALL_SETS ] -solver = pywraplp.Solver("chemical_set_lp", - pywraplp.Solver.GLOP_LINEAR_PROGRAMMING) +solver = pywraplp.Solver( + "chemical_set_lp", pywraplp.Solver.GLOP_LINEAR_PROGRAMMING +) set_vars = [solver.NumVar(0, max_set[s], f"set_{s}") for s in ALL_SETS] epsilon = solver.NumVar(0, 1000, "epsilon") for p in ALL_PRODUCTS: - solver.Add( - sum(chemical_set[s][p + 1] * set_vars[s] - for s in ALL_SETS) <= max_quantities[p][1]) - solver.Add( - sum(chemical_set[s][p + 1] * set_vars[s] - for s in ALL_SETS) >= max_quantities[p][1] - epsilon) + solver.Add( + sum(chemical_set[s][p + 1] * set_vars[s] for s in ALL_SETS) + <= max_quantities[p][1] + ) + solver.Add( + sum(chemical_set[s][p + 1] * set_vars[s] for s in ALL_SETS) + >= max_quantities[p][1] - epsilon + ) solver.Minimize(epsilon) @@ -85,11 +92,12 @@ print(f"Optimal objective value = {solver.Objective().Value()}") for s in ALL_SETS: - print(f" {chemical_set[s][0]} = {set_vars[s].solution_value()}", end=" ") - print() + print(f" {chemical_set[s][0]} = {set_vars[s].solution_value()}", end=" ") + print() for p in ALL_PRODUCTS: - name = max_quantities[p][0] - max_quantity = max_quantities[p][1] - quantity = sum(set_vars[s].solution_value() * chemical_set[s][p + 1] - for s in ALL_SETS) - print(f"{name}: {quantity} out of {max_quantity}") + name = max_quantities[p][0] + max_quantity = max_quantities[p][1] + quantity = sum( + set_vars[s].solution_value() * chemical_set[s][p + 1] for s in ALL_SETS + ) + print(f"{name}: {quantity} out of {max_quantity}") diff --git a/examples/python/chemical_balance_sat.py b/examples/python/chemical_balance_sat.py index 9ea99e81e22..0fcbe682a69 100644 --- a/examples/python/chemical_balance_sat.py +++ b/examples/python/chemical_balance_sat.py @@ -26,94 +26,94 @@ def chemical_balance(): - """Solves the chemical balance problem.""" - # Data - max_quantities = [ - ["N_Total", 1944], - ["P2O5", 1166.4], - ["K2O", 1822.5], - ["CaO", 1458], - ["MgO", 486], - ["Fe", 9.7], - ["B", 2.4], - ] - - chemical_set = [ - ["A", 0, 0, 510, 540, 0, 0, 0], - ["B", 110, 0, 0, 0, 160, 0, 0], - ["C", 61, 149, 384, 0, 30, 1, 0.2], - ["D", 148, 70, 245, 0, 15, 1, 0.2], - ["E", 160, 158, 161, 0, 10, 1, 0.2], - ] - - num_products = len(max_quantities) - all_products = range(num_products) - - num_sets = len(chemical_set) - all_sets = range(num_sets) - - # Model - - model = cp_model.CpModel() - - # Scale quantities by 100. - max_set = [ - int( - math.ceil( - min( - max_quantities[q][1] * 1000 / chemical_set[s][q + 1] - for q in all_products - if chemical_set[s][q + 1] != 0 - ) - ) - ) - for s in all_sets - ] - - set_vars = [model.new_int_var(0, max_set[s], f"set_{s}") for s in all_sets] - - epsilon = model.new_int_var(0, 10000000, "epsilon") - + """Solves the chemical balance problem.""" + # Data + max_quantities = [ + ["N_Total", 1944], + ["P2O5", 1166.4], + ["K2O", 1822.5], + ["CaO", 1458], + ["MgO", 486], + ["Fe", 9.7], + ["B", 2.4], + ] + + chemical_set = [ + ["A", 0, 0, 510, 540, 0, 0, 0], + ["B", 110, 0, 0, 0, 160, 0, 0], + ["C", 61, 149, 384, 0, 30, 1, 0.2], + ["D", 148, 70, 245, 0, 15, 1, 0.2], + ["E", 160, 158, 161, 0, 10, 1, 0.2], + ] + + num_products = len(max_quantities) + all_products = range(num_products) + + num_sets = len(chemical_set) + all_sets = range(num_sets) + + # Model + + model = cp_model.CpModel() + + # Scale quantities by 100. + max_set = [ + int( + math.ceil( + min( + max_quantities[q][1] * 1000 / chemical_set[s][q + 1] + for q in all_products + if chemical_set[s][q + 1] != 0 + ) + ) + ) + for s in all_sets + ] + + set_vars = [model.new_int_var(0, max_set[s], f"set_{s}") for s in all_sets] + + epsilon = model.new_int_var(0, 10000000, "epsilon") + + for p in all_products: + model.add( + sum(int(chemical_set[s][p + 1] * 10) * set_vars[s] for s in all_sets) + <= int(max_quantities[p][1] * 10000) + ) + model.add( + sum(int(chemical_set[s][p + 1] * 10) * set_vars[s] for s in all_sets) + >= int(max_quantities[p][1] * 10000) - epsilon + ) + + model.minimize(epsilon) + + # Creates a solver and solves. + solver = cp_model.CpSolver() + status = solver.solve(model) + if status == cp_model.OPTIMAL: + # The objective value of the solution. + print(f"Optimal objective value = {solver.objective_value / 10000.0}") + + for s in all_sets: + print( + f" {chemical_set[s][0]} = {solver.value(set_vars[s]) / 1000.0}", + end=" ", + ) + print() for p in all_products: - model.add( - sum(int(chemical_set[s][p + 1] * 10) * set_vars[s] for s in all_sets) - <= int(max_quantities[p][1] * 10000) - ) - model.add( - sum(int(chemical_set[s][p + 1] * 10) * set_vars[s] for s in all_sets) - >= int(max_quantities[p][1] * 10000) - epsilon - ) - - model.minimize(epsilon) - - # Creates a solver and solves. - solver = cp_model.CpSolver() - status = solver.solve(model) - if status == cp_model.OPTIMAL: - # The objective value of the solution. - print(f"Optimal objective value = {solver.objective_value / 10000.0}") - - for s in all_sets: - print( - f" {chemical_set[s][0]} = {solver.value(set_vars[s]) / 1000.0}", - end=" ", - ) - print() - for p in all_products: - name = max_quantities[p][0] - max_quantity = max_quantities[p][1] - quantity = sum( - solver.value(set_vars[s]) / 1000.0 * chemical_set[s][p + 1] - for s in all_sets - ) - print(f"{name}: {quantity:.3f} out of {max_quantity}") + name = max_quantities[p][0] + max_quantity = max_quantities[p][1] + quantity = sum( + solver.value(set_vars[s]) / 1000.0 * chemical_set[s][p + 1] + for s in all_sets + ) + print(f"{name}: {quantity:.3f} out of {max_quantity}") def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - chemical_balance() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + chemical_balance() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/clustering_sat.py b/examples/python/clustering_sat.py index b53eddb87bc..76ebcf5ad62 100644 --- a/examples/python/clustering_sat.py +++ b/examples/python/clustering_sat.py @@ -66,78 +66,78 @@ def clustering_sat() -> None: - """Entry point of the program.""" - num_nodes = len(distance_matrix) - print("Num nodes =", num_nodes) - - # Number of groups to split the nodes, must divide num_nodes. - num_groups = 4 - group_size = num_nodes // num_groups - - # Model. - model = cp_model.CpModel() - - # Variables. - neighbors = {} - obj_vars = [] - obj_coeffs = [] - for n1 in range(num_nodes - 1): - for n2 in range(n1 + 1, num_nodes): - same = model.new_bool_var("neighbors_%i_%i" % (n1, n2)) - neighbors[n1, n2] = same - obj_vars.append(same) - obj_coeffs.append(distance_matrix[n1][n2] + distance_matrix[n2][n1]) - - # Number of neighborss: - for n in range(num_nodes): + """Entry point of the program.""" + num_nodes = len(distance_matrix) + print("Num nodes =", num_nodes) + + # Number of groups to split the nodes, must divide num_nodes. + num_groups = 4 + group_size = num_nodes // num_groups + + # Model. + model = cp_model.CpModel() + + # Variables. + neighbors = {} + obj_vars = [] + obj_coeffs = [] + for n1 in range(num_nodes - 1): + for n2 in range(n1 + 1, num_nodes): + same = model.new_bool_var("neighbors_%i_%i" % (n1, n2)) + neighbors[n1, n2] = same + obj_vars.append(same) + obj_coeffs.append(distance_matrix[n1][n2] + distance_matrix[n2][n1]) + + # Number of neighborss: + for n in range(num_nodes): + model.add( + sum(neighbors[m, n] for m in range(n)) + + sum(neighbors[n, m] for m in range(n + 1, num_nodes)) + == group_size - 1 + ) + + # Enforce transivity on all triplets. + for n1 in range(num_nodes - 2): + for n2 in range(n1 + 1, num_nodes - 1): + for n3 in range(n2 + 1, num_nodes): model.add( - sum(neighbors[m, n] for m in range(n)) - + sum(neighbors[n, m] for m in range(n + 1, num_nodes)) - == group_size - 1 + neighbors[n1, n3] + neighbors[n2, n3] + neighbors[n1, n2] != 2 ) - # Enforce transivity on all triplets. - for n1 in range(num_nodes - 2): - for n2 in range(n1 + 1, num_nodes - 1): - for n3 in range(n2 + 1, num_nodes): - model.add( - neighbors[n1, n3] + neighbors[n2, n3] + neighbors[n1, n2] != 2 - ) - - # Redundant constraints on total sum of neighborss. - model.add(sum(obj_vars) == num_groups * group_size * (group_size - 1) // 2) - - # Minimize weighted sum of arcs. - model.minimize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) - - # Solve and print out the solution. - solver = cp_model.CpSolver() - solver.parameters.log_search_progress = True - solver.parameters.num_search_workers = 8 - - status = solver.solve(model) - print(solver.response_stats()) - - if status == cp_model.FEASIBLE or status == cp_model.OPTIMAL: - visited = set() - for g in range(num_groups): - for n in range(num_nodes): - if n not in visited: - visited.add(n) - output = str(n) - for o in range(n + 1, num_nodes): - if solver.boolean_value(neighbors[n, o]): - visited.add(o) - output += " " + str(o) - print("Group", g, ":", output) - break + # Redundant constraints on total sum of neighborss. + model.add(sum(obj_vars) == num_groups * group_size * (group_size - 1) // 2) + + # Minimize weighted sum of arcs. + model.minimize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) + + # Solve and print out the solution. + solver = cp_model.CpSolver() + solver.parameters.log_search_progress = True + solver.parameters.num_search_workers = 8 + + status = solver.solve(model) + print(solver.response_stats()) + + if status == cp_model.FEASIBLE or status == cp_model.OPTIMAL: + visited = set() + for g in range(num_groups): + for n in range(num_nodes): + if n not in visited: + visited.add(n) + output = str(n) + for o in range(n + 1, num_nodes): + if solver.boolean_value(neighbors[n, o]): + visited.add(o) + output += " " + str(o) + print("Group", g, ":", output) + break def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - clustering_sat() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + clustering_sat() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/cover_rectangle_sat.py b/examples/python/cover_rectangle_sat.py index 81089f682af..32ebbc6cd68 100644 --- a/examples/python/cover_rectangle_sat.py +++ b/examples/python/cover_rectangle_sat.py @@ -20,104 +20,104 @@ def cover_rectangle(num_squares: int) -> bool: - """Try to fill the rectangle with a given number of squares.""" - size_x = 60 - size_y = 50 - - model = cp_model.CpModel() - - areas = [] - sizes = [] - x_intervals = [] - y_intervals = [] - x_starts = [] - y_starts = [] - - # Creates intervals for the NoOverlap2D and size variables. + """Try to fill the rectangle with a given number of squares.""" + size_x = 60 + size_y = 50 + + model = cp_model.CpModel() + + areas = [] + sizes = [] + x_intervals = [] + y_intervals = [] + x_starts = [] + y_starts = [] + + # Creates intervals for the NoOverlap2D and size variables. + for i in range(num_squares): + size = model.new_int_var(1, size_y, "size_%i" % i) + start_x = model.new_int_var(0, size_x, "sx_%i" % i) + end_x = model.new_int_var(0, size_x, "ex_%i" % i) + start_y = model.new_int_var(0, size_y, "sy_%i" % i) + end_y = model.new_int_var(0, size_y, "ey_%i" % i) + + interval_x = model.new_interval_var(start_x, size, end_x, "ix_%i" % i) + interval_y = model.new_interval_var(start_y, size, end_y, "iy_%i" % i) + + area = model.new_int_var(1, size_y * size_y, "area_%i" % i) + model.add_multiplication_equality(area, [size, size]) + + areas.append(area) + x_intervals.append(interval_x) + y_intervals.append(interval_y) + sizes.append(size) + x_starts.append(start_x) + y_starts.append(start_y) + + # Main constraint. + model.add_no_overlap_2d(x_intervals, y_intervals) + + # Redundant constraints. + model.add_cumulative(x_intervals, sizes, size_y) + model.add_cumulative(y_intervals, sizes, size_x) + + # Forces the rectangle to be exactly covered. + model.add(sum(areas) == size_x * size_y) + + # Symmetry breaking 1: sizes are ordered. + for i in range(num_squares - 1): + model.add(sizes[i] <= sizes[i + 1]) + + # Define same to be true iff sizes[i] == sizes[i + 1] + same = model.new_bool_var("") + model.add(sizes[i] == sizes[i + 1]).only_enforce_if(same) + model.add(sizes[i] < sizes[i + 1]).only_enforce_if(~same) + + # Tie break with starts. + model.add(x_starts[i] <= x_starts[i + 1]).only_enforce_if(same) + + # Symmetry breaking 2: first square in one quadrant. + model.add(x_starts[0] < (size_x + 1) // 2) + model.add(y_starts[0] < (size_y + 1) // 2) + + # Creates a solver and solves. + solver = cp_model.CpSolver() + solver.parameters.num_workers = 8 + solver.parameters.max_time_in_seconds = 10.0 + status = solver.solve(model) + print("%s found in %0.2fs" % (solver.status_name(status), solver.wall_time)) + + # Prints solution. + solution_found = status == cp_model.OPTIMAL or status == cp_model.FEASIBLE + if solution_found: + display = [[" " for _ in range(size_x)] for _ in range(size_y)] for i in range(num_squares): - size = model.new_int_var(1, size_y, "size_%i" % i) - start_x = model.new_int_var(0, size_x, "sx_%i" % i) - end_x = model.new_int_var(0, size_x, "ex_%i" % i) - start_y = model.new_int_var(0, size_y, "sy_%i" % i) - end_y = model.new_int_var(0, size_y, "ey_%i" % i) - - interval_x = model.new_interval_var(start_x, size, end_x, "ix_%i" % i) - interval_y = model.new_interval_var(start_y, size, end_y, "iy_%i" % i) - - area = model.new_int_var(1, size_y * size_y, "area_%i" % i) - model.add_multiplication_equality(area, [size, size]) - - areas.append(area) - x_intervals.append(interval_x) - y_intervals.append(interval_y) - sizes.append(size) - x_starts.append(start_x) - y_starts.append(start_y) - - # Main constraint. - model.add_no_overlap_2d(x_intervals, y_intervals) - - # Redundant constraints. - model.add_cumulative(x_intervals, sizes, size_y) - model.add_cumulative(y_intervals, sizes, size_x) - - # Forces the rectangle to be exactly covered. - model.add(sum(areas) == size_x * size_y) - - # Symmetry breaking 1: sizes are ordered. - for i in range(num_squares - 1): - model.add(sizes[i] <= sizes[i + 1]) - - # Define same to be true iff sizes[i] == sizes[i + 1] - same = model.new_bool_var("") - model.add(sizes[i] == sizes[i + 1]).only_enforce_if(same) - model.add(sizes[i] < sizes[i + 1]).only_enforce_if(~same) - - # Tie break with starts. - model.add(x_starts[i] <= x_starts[i + 1]).only_enforce_if(same) - - # Symmetry breaking 2: first square in one quadrant. - model.add(x_starts[0] < (size_x + 1) // 2) - model.add(y_starts[0] < (size_y + 1) // 2) - - # Creates a solver and solves. - solver = cp_model.CpSolver() - solver.parameters.num_workers = 8 - solver.parameters.max_time_in_seconds = 10.0 - status = solver.solve(model) - print("%s found in %0.2fs" % (solver.status_name(status), solver.wall_time)) - - # Prints solution. - solution_found = status == cp_model.OPTIMAL or status == cp_model.FEASIBLE - if solution_found: - display = [[" " for _ in range(size_x)] for _ in range(size_y)] - for i in range(num_squares): - sol_x = solver.value(x_starts[i]) - sol_y = solver.value(y_starts[i]) - sol_s = solver.value(sizes[i]) - char = format(i, "01x") - for j in range(sol_s): - for k in range(sol_s): - if display[sol_y + j][sol_x + k] != " ": - print( - "ERROR between %s and %s" - % (display[sol_y + j][sol_x + k], char) - ) - display[sol_y + j][sol_x + k] = char - - for line in range(size_y): - print(" ".join(display[line])) - return solution_found + sol_x = solver.value(x_starts[i]) + sol_y = solver.value(y_starts[i]) + sol_s = solver.value(sizes[i]) + char = format(i, "01x") + for j in range(sol_s): + for k in range(sol_s): + if display[sol_y + j][sol_x + k] != " ": + print( + "ERROR between %s and %s" + % (display[sol_y + j][sol_x + k], char) + ) + display[sol_y + j][sol_x + k] = char + + for line in range(size_y): + print(" ".join(display[line])) + return solution_found def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - for num_squares in range(1, 15): - print("Trying with size =", num_squares) - if cover_rectangle(num_squares): - break + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + for num_squares in range(1, 15): + print("Trying with size =", num_squares) + if cover_rectangle(num_squares): + break if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/cryptarithm_sat.py b/examples/python/cryptarithm_sat.py index c4e49e0873d..0732c8df1f7 100644 --- a/examples/python/cryptarithm_sat.py +++ b/examples/python/cryptarithm_sat.py @@ -12,71 +12,70 @@ # See the License for the specific language governing permissions and # limitations under the License. -"""Use CP-SAT to solve a simple cryptarithmetic problem: SEND+MORE=MONEY. -""" +"""Use CP-SAT to solve a simple cryptarithmetic problem: SEND+MORE=MONEY.""" from absl import app from ortools.sat.python import cp_model def send_more_money() -> None: - """solve the cryptarithmic puzzle SEND+MORE=MONEY.""" - model = cp_model.CpModel() - - # Create variables. - # Since s is a leading digit, it can't be 0. - s = model.new_int_var(1, 9, "s") - e = model.new_int_var(0, 9, "e") - n = model.new_int_var(0, 9, "n") - d = model.new_int_var(0, 9, "d") - # Since m is a leading digit, it can't be 0. - m = model.new_int_var(1, 9, "m") - o = model.new_int_var(0, 9, "o") - r = model.new_int_var(0, 9, "r") - y = model.new_int_var(0, 9, "y") - - # Create carry variables. c0 is true if the first column of addends carries - # a 1, c2 is true if the second column carries a 1, and so on. - c0 = model.new_bool_var("c0") - c1 = model.new_bool_var("c1") - c2 = model.new_bool_var("c2") - c3 = model.new_bool_var("c3") - - # Force all letters to take on different values. - model.add_all_different(s, e, n, d, m, o, r, y) - - # Column 0: - model.add(c0 == m) - - # Column 1: - model.add(c1 + s + m == o + 10 * c0) - - # Column 2: - model.add(c2 + e + o == n + 10 * c1) - - # Column 3: - model.add(c3 + n + r == e + 10 * c2) - - # Column 4: - model.add(d + e == y + 10 * c3) - - # solve model. - solver = cp_model.CpSolver() - if solver.solve(model) == cp_model.OPTIMAL: - print("Optimal solution found!") - print("s:", solver.value(s)) - print("e:", solver.value(e)) - print("n:", solver.value(n)) - print("d:", solver.value(d)) - print("m:", solver.value(m)) - print("o:", solver.value(o)) - print("r:", solver.value(r)) - print("y:", solver.value(y)) + """solve the cryptarithmic puzzle SEND+MORE=MONEY.""" + model = cp_model.CpModel() + + # Create variables. + # Since s is a leading digit, it can't be 0. + s = model.new_int_var(1, 9, "s") + e = model.new_int_var(0, 9, "e") + n = model.new_int_var(0, 9, "n") + d = model.new_int_var(0, 9, "d") + # Since m is a leading digit, it can't be 0. + m = model.new_int_var(1, 9, "m") + o = model.new_int_var(0, 9, "o") + r = model.new_int_var(0, 9, "r") + y = model.new_int_var(0, 9, "y") + + # Create carry variables. c0 is true if the first column of addends carries + # a 1, c2 is true if the second column carries a 1, and so on. + c0 = model.new_bool_var("c0") + c1 = model.new_bool_var("c1") + c2 = model.new_bool_var("c2") + c3 = model.new_bool_var("c3") + + # Force all letters to take on different values. + model.add_all_different(s, e, n, d, m, o, r, y) + + # Column 0: + model.add(c0 == m) + + # Column 1: + model.add(c1 + s + m == o + 10 * c0) + + # Column 2: + model.add(c2 + e + o == n + 10 * c1) + + # Column 3: + model.add(c3 + n + r == e + 10 * c2) + + # Column 4: + model.add(d + e == y + 10 * c3) + + # solve model. + solver = cp_model.CpSolver() + if solver.solve(model) == cp_model.OPTIMAL: + print("Optimal solution found!") + print("s:", solver.value(s)) + print("e:", solver.value(e)) + print("n:", solver.value(n)) + print("d:", solver.value(d)) + print("m:", solver.value(m)) + print("o:", solver.value(o)) + print("r:", solver.value(r)) + print("y:", solver.value(y)) def main(_) -> None: - send_more_money() + send_more_money() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/cvrptw_plot.py b/examples/python/cvrptw_plot.py index 074cd5ea346..7f199428f93 100644 --- a/examples/python/cvrptw_plot.py +++ b/examples/python/cvrptw_plot.py @@ -13,25 +13,25 @@ # limitations under the License. """Capacitated Vehicle Routing Problem with Time Windows (and optional orders). - This is a sample using the routing library python wrapper to solve a - CVRPTW problem. - A description of the problem can be found here: - http://en.wikipedia.org/wiki/Vehicle_routing_problem. - The variant which is tackled by this model includes a capacity dimension, - time windows and optional orders, with a penalty cost if orders are not - performed. - To help explore the problem, two classes are provided Customers() and - Vehicles(): used to randomly locate orders and depots, and to randomly - generate demands, time-window constraints and vehicles. - Distances are computed using the Great Circle distances. Distances are in km - and times in seconds. - - A function for the displaying of the vehicle plan - display_vehicle_output - - The optimization engine uses local search to improve solutions, first - solutions being generated using a cheapest addition heuristic. - Numpy and Matplotlib are required for the problem creation and display. +This is a sample using the routing library python wrapper to solve a +CVRPTW problem. +A description of the problem can be found here: +http://en.wikipedia.org/wiki/Vehicle_routing_problem. +The variant which is tackled by this model includes a capacity dimension, +time windows and optional orders, with a penalty cost if orders are not +performed. +To help explore the problem, two classes are provided Customers() and +Vehicles(): used to randomly locate orders and depots, and to randomly +generate demands, time-window constraints and vehicles. +Distances are computed using the Great Circle distances. Distances are in km +and times in seconds. + +A function for the displaying of the vehicle plan +display_vehicle_output + +The optimization engine uses local search to improve solutions, first +solutions being generated using a cheapest addition heuristic. +Numpy and Matplotlib are required for the problem creation and display. """ import os @@ -39,715 +39,748 @@ from matplotlib import pyplot as plt from collections import namedtuple from ortools.constraint_solver import pywrapcp -from ortools.constraint_solver import routing_enums_pb2 +from ortools.routing import enums_pb2 from datetime import datetime, timedelta -class Customers(): - """ - A class that generates and holds customers information. - - Randomly normally distribute a number of customers and locations within - a region described by a rectangle. Generate a random demand for each - customer. Generate a random time window for each customer. - May either be initiated with the extents, as a dictionary describing - two corners of a rectangle in latitude and longitude OR as a center - point (lat, lon), and box_size in km. The default arguments are for a - 10 x 10 km square centered in Sheffield). - - Args: extents (Optional[Dict]): A dictionary describing a rectangle in - latitude and longitude with the keys 'llcrnrlat', 'llcrnrlon' & - 'urcrnrlat' & 'urcrnrlat' center (Optional(Tuple): A tuple of - (latitude, longitude) describing the centre of the rectangle. box_size - (Optional float: The length in km of the box's sides. num_stops (int): - The number of customers, including the depots that are placed normally - distributed in the rectangle. min_demand (int): Lower limit on the - randomly generated demand at each customer. max_demand (int): Upper - limit on the randomly generated demand at each customer. - min_tw: shortest random time window for a customer, in hours. - max_tw: longest random time window for a customer, in hours. - Examples: To place 100 customers randomly within 100 km x 100 km - rectangle, centered in the default location, with a random demand of - between 5 and 10 units: >>> customers = Customers(num_stops=100, - box_size=100, ... min_demand=5, max_demand=10) - alternatively, to place 75 customers in the same area with default - arguments for demand: >>> extents = {'urcrnrlon': 0.03403, 'llcrnrlon': - -2.98325, ... 'urcrnrlat': 54.28127, 'llcrnrlat': 52.48150} >>> - customers = Customers(num_stops=75, extents=extents) +class Customers: + """ + A class that generates and holds customers information. + + Randomly normally distribute a number of customers and locations within + a region described by a rectangle. Generate a random demand for each + customer. Generate a random time window for each customer. + May either be initiated with the extents, as a dictionary describing + two corners of a rectangle in latitude and longitude OR as a center + point (lat, lon), and box_size in km. The default arguments are for a + 10 x 10 km square centered in Sheffield). + + Args: extents (Optional[Dict]): A dictionary describing a rectangle in + latitude and longitude with the keys 'llcrnrlat', 'llcrnrlon' & + 'urcrnrlat' & 'urcrnrlat' center (Optional(Tuple): A tuple of + (latitude, longitude) describing the centre of the rectangle. box_size + (Optional float: The length in km of the box's sides. num_stops (int): + The number of customers, including the depots that are placed normally + distributed in the rectangle. min_demand (int): Lower limit on the + randomly generated demand at each customer. max_demand (int): Upper + limit on the randomly generated demand at each customer. + min_tw: shortest random time window for a customer, in hours. + max_tw: longest random time window for a customer, in hours. + Examples: To place 100 customers randomly within 100 km x 100 km + rectangle, centered in the default location, with a random demand of + between 5 and 10 units: >>> customers = Customers(num_stops=100, + box_size=100, ... min_demand=5, max_demand=10) + alternatively, to place 75 customers in the same area with default + arguments for demand: >>> extents = {'urcrnrlon': 0.03403, 'llcrnrlon': + -2.98325, ... 'urcrnrlat': 54.28127, 'llcrnrlat': 52.48150} >>> + customers = Customers(num_stops=75, extents=extents) """ - def __init__(self, - extents=None, - center=(53.381393, -1.474611), - box_size=10, - num_stops=100, - min_demand=0, - max_demand=25, - min_tw=1, - max_tw=5): - self.number = num_stops #: The number of customers and depots - #: Location, a named tuple for locations. - Location = namedtuple('Location', ['lat', 'lon']) - if extents is not None: - self.extents = extents #: The lower left and upper right points - #: Location[lat,lon]: the centre point of the area. - self.center = Location( - extents['urcrnrlat'] - 0.5 * - (extents['urcrnrlat'] - extents['llcrnrlat']), - extents['urcrnrlon'] - 0.5 * - (extents['urcrnrlon'] - extents['llcrnrlon'])) - else: - #: Location[lat,lon]: the centre point of the area. - (clat, clon) = self.center = Location(center[0], center[1]) - rad_earth = 6367 # km - circ_earth = np.pi * rad_earth - #: The lower left and upper right points - self.extents = { - 'llcrnrlon': (clon - 180 * box_size / - (circ_earth * np.cos(np.deg2rad(clat)))), - 'llcrnrlat': - clat - 180 * box_size / circ_earth, - 'urcrnrlon': (clon + 180 * box_size / - (circ_earth * np.cos(np.deg2rad(clat)))), - 'urcrnrlat': - clat + 180 * box_size / circ_earth - } - # The 'name' of the stop, indexed from 0 to num_stops-1 - stops = np.array(range(0, num_stops)) - # normaly distributed random distribution of stops within the box - stdv = 6 # the number of standard deviations 99.9% will be within +-3 - lats = (self.extents['llcrnrlat'] + np.random.randn(num_stops) * - (self.extents['urcrnrlat'] - self.extents['llcrnrlat']) / stdv) - lons = (self.extents['llcrnrlon'] + np.random.randn(num_stops) * - (self.extents['urcrnrlon'] - self.extents['llcrnrlon']) / stdv) - # uniformly distributed integer demands. - demands = np.random.randint(min_demand, max_demand, num_stops) - - self.time_horizon = 24 * 60**2 # A 24 hour period. - - # The customers demand min_tw to max_tw hour time window for each - # delivery - time_windows = np.random.randint(min_tw * 3600, max_tw * 3600, - num_stops) - # The last time a delivery window can start - latest_time = self.time_horizon - time_windows - start_times = [None for o in time_windows] - stop_times = [None for o in time_windows] - # Make random timedeltas, nominally from the start of the day. - for idx in range(self.number): - stime = int(np.random.randint(0, latest_time[idx])) - start_times[idx] = timedelta(seconds=stime) - stop_times[idx] = ( - start_times[idx] + timedelta(seconds=int(time_windows[idx]))) - # A named tuple for the customer - Customer = namedtuple( - 'Customer', - [ - 'index', # the index of the stop - 'demand', # the demand for the stop - 'lat', # the latitude of the stop - 'lon', # the longitude of the stop - 'tw_open', # timedelta window open - 'tw_close' - ]) # timedelta window cls - - self.customers = [ - Customer(idx, dem, lat, lon, tw_open, tw_close) - for idx, dem, lat, lon, tw_open, tw_close in zip( - stops, demands, lats, lons, start_times, stop_times) - ] - - # The number of seconds needed to 'unload' 1 unit of goods. - self.service_time_per_dem = 300 # seconds - - def set_manager(self, manager): - self.manager = manager - - def central_start_node(self, invert=False): - """ - Return a random starting node, with probability weighted by distance - from the centre of the extents, so that a central starting node is - likely. - - Args: invert (Optional bool): When True, a peripheral starting node is - most likely. - - Returns: - int: a node index. - - Examples: - >>> customers.central_start_node(invert=True) - 42 - """ - num_nodes = len(self.customers) - dist = np.empty((num_nodes, 1)) - for idx_to in range(num_nodes): - dist[idx_to] = self._haversine(self.center.lon, self.center.lat, - self.customers[idx_to].lon, - self.customers[idx_to].lat) - furthest = np.max(dist) - - if invert: - prob = dist * 1.0 / sum(dist) - else: - prob = (furthest - dist * 1.0) / sum(furthest - dist) - indexes = np.array([range(num_nodes)]) - start_node = np.random.choice( - indexes.flatten(), size=1, replace=True, p=prob.flatten()) - return start_node[0] - - def make_distance_mat(self, method='haversine'): - """ - Return a distance matrix and make it a member of Customer, using the - method given in the call. Currently only Haversine (GC distance) is - implemented, but Manhattan, or using a maps API could be added here. - Raises an AssertionError for all other methods. - - Args: method (Optional[str]): method of distance calculation to use. The - Haversine formula is the only method implemented. - - Returns: - Numpy array of node to node distances. - - Examples: - >>> dist_mat = customers.make_distance_mat(method='haversine') - >>> dist_mat = customers.make_distance_mat(method='manhattan') - AssertionError - """ - self.distmat = np.zeros((self.number, self.number)) - methods = {'haversine': self._haversine} - assert (method in methods) - for frm_idx in range(self.number): - for to_idx in range(self.number): - if frm_idx != to_idx: - frm_c = self.customers[frm_idx] - to_c = self.customers[to_idx] - self.distmat[frm_idx, to_idx] = self._haversine( - frm_c.lon, frm_c.lat, to_c.lon, to_c.lat) - return (self.distmat) - - def _haversine(self, lon1, lat1, lon2, lat2): - """ - Calculate the great circle distance between two points - on the earth specified in decimal degrees of latitude and longitude. - https://en.wikipedia.org/wiki/Haversine_formula - - Args: - lon1: longitude of pt 1, - lat1: latitude of pt 1, - lon2: longitude of pt 2, - lat2: latitude of pt 2 - - Returns: - the distace in km between pt1 and pt2 - """ - # convert decimal degrees to radians - lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2]) - - # haversine formula - dlon = lon2 - lon1 - dlat = lat2 - lat1 - a = (np.sin(dlat / 2)**2 + - np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2)**2) - c = 2 * np.arcsin(np.sqrt(a)) - - # 6367 km is the radius of the Earth - km = 6367 * c - return km - - def get_total_demand(self): - """ - Return the total demand of all customers. - """ - return (sum([c.demand for c in self.customers])) - - def return_dist_callback(self, **kwargs): - """ - Return a callback function for the distance matrix. - - Args: **kwargs: Arbitrary keyword arguments passed on to - make_distance_mat() - - Returns: - function: dist_return(a,b) A function that takes the 'from' node - index and the 'to' node index and returns the distance in km. - """ - self.make_distance_mat(**kwargs) - - def dist_return(from_index, to_index): - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = self.manager.IndexToNode(from_index) - to_node = self.manager.IndexToNode(to_index) - return (self.distmat[from_node][to_node]) - - return dist_return - - def return_dem_callback(self): - """ - Return a callback function that gives the demands. - - Returns: - function: dem_return(a) A function that takes the 'from' node - index and returns the distance in km. - """ - - def dem_return(from_index): - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = self.manager.IndexToNode(from_index) - return (self.customers[from_node].demand) - - return dem_return - - def zero_depot_demands(self, depot): - """ - Zero out the demands and time windows of depot. The Depots do not have - demands or time windows so this function clears them. - - Args: depot (int): index of the stop to modify into a depot. - Examples: >>> customers.zero_depot_demands(5) >>> - customers.customers[5].demand == 0 True + def __init__( + self, + extents=None, + center=(53.381393, -1.474611), + box_size=10, + num_stops=100, + min_demand=0, + max_demand=25, + min_tw=1, + max_tw=5, + ): + self.number = num_stops #: The number of customers and depots + #: Location, a named tuple for locations. + Location = namedtuple('Location', ['lat', 'lon']) + if extents is not None: + self.extents = extents #: The lower left and upper right points + #: Location[lat,lon]: the centre point of the area. + self.center = Location( + extents['urcrnrlat'] + - 0.5 * (extents['urcrnrlat'] - extents['llcrnrlat']), + extents['urcrnrlon'] + - 0.5 * (extents['urcrnrlon'] - extents['llcrnrlon']), + ) + else: + #: Location[lat,lon]: the centre point of the area. + (clat, clon) = self.center = Location(center[0], center[1]) + rad_earth = 6367 # km + circ_earth = np.pi * rad_earth + #: The lower left and upper right points + self.extents = { + 'llcrnrlon': clon - 180 * box_size / ( + circ_earth * np.cos(np.deg2rad(clat)) + ), + 'llcrnrlat': clat - 180 * box_size / circ_earth, + 'urcrnrlon': clon + 180 * box_size / ( + circ_earth * np.cos(np.deg2rad(clat)) + ), + 'urcrnrlat': clat + 180 * box_size / circ_earth, + } + # The 'name' of the stop, indexed from 0 to num_stops-1 + stops = np.array(range(0, num_stops)) + # normaly distributed random distribution of stops within the box + stdv = 6 # the number of standard deviations 99.9% will be within +-3 + lats = ( + self.extents['llcrnrlat'] + + np.random.randn(num_stops) + * (self.extents['urcrnrlat'] - self.extents['llcrnrlat']) + / stdv + ) + lons = ( + self.extents['llcrnrlon'] + + np.random.randn(num_stops) + * (self.extents['urcrnrlon'] - self.extents['llcrnrlon']) + / stdv + ) + # uniformly distributed integer demands. + demands = np.random.randint(min_demand, max_demand, num_stops) + + self.time_horizon = 24 * 60**2 # A 24 hour period. + + # The customers demand min_tw to max_tw hour time window for each + # delivery + time_windows = np.random.randint(min_tw * 3600, max_tw * 3600, num_stops) + # The last time a delivery window can start + latest_time = self.time_horizon - time_windows + start_times = [None for o in time_windows] + stop_times = [None for o in time_windows] + # Make random timedeltas, nominally from the start of the day. + for idx in range(self.number): + stime = int(np.random.randint(0, latest_time[idx])) + start_times[idx] = timedelta(seconds=stime) + stop_times[idx] = start_times[idx] + timedelta( + seconds=int(time_windows[idx]) + ) + # A named tuple for the customer + Customer = namedtuple( + 'Customer', + [ + 'index', # the index of the stop + 'demand', # the demand for the stop + 'lat', # the latitude of the stop + 'lon', # the longitude of the stop + 'tw_open', # timedelta window open + 'tw_close', + ], + ) # timedelta window cls + + self.customers = [ + Customer(idx, dem, lat, lon, tw_open, tw_close) + for idx, dem, lat, lon, tw_open, tw_close in zip( + stops, demands, lats, lons, start_times, stop_times + ) + ] + + # The number of seconds needed to 'unload' 1 unit of goods. + self.service_time_per_dem = 300 # seconds + + def set_manager(self, manager): + self.manager = manager + + def central_start_node(self, invert=False): """ - start_depot = self.customers[depot] - self.customers[depot] = start_depot._replace( - demand=0, tw_open=None, tw_close=None) + Return a random starting node, with probability weighted by distance + from the centre of the extents, so that a central starting node is + likely. - def make_service_time_call_callback(self): - """ - Return a callback function that provides the time spent servicing the - customer. Here is it proportional to the demand given by - self.service_time_per_dem, default 300 seconds per unit demand. + Args: invert (Optional bool): When True, a peripheral starting node is + most likely. - Returns: - function [dem_return(a, b)]: A function that takes the from/a node - index and the to/b node index and returns the service time at a + Returns: + int: a node index. - """ + Examples: + >>> customers.central_start_node(invert=True) + 42 + """ + num_nodes = len(self.customers) + dist = np.empty((num_nodes, 1)) + for idx_to in range(num_nodes): + dist[idx_to] = self._haversine( + self.center.lon, + self.center.lat, + self.customers[idx_to].lon, + self.customers[idx_to].lat, + ) + furthest = np.max(dist) + + if invert: + prob = dist * 1.0 / sum(dist) + else: + prob = (furthest - dist * 1.0) / sum(furthest - dist) + indexes = np.array([range(num_nodes)]) + start_node = np.random.choice( + indexes.flatten(), size=1, replace=True, p=prob.flatten() + ) + return start_node[0] + + def make_distance_mat(self, method='haversine'): + """ + Return a distance matrix and make it a member of Customer, using the + method given in the call. Currently only Haversine (GC distance) is + implemented, but Manhattan, or using a maps API could be added here. + Raises an AssertionError for all other methods. - def service_time_return(a, b): - return (self.customers[a].demand * self.service_time_per_dem) + Args: method (Optional[str]): method of distance calculation to use. The + Haversine formula is the only method implemented. - return service_time_return + Returns: + Numpy array of node to node distances. - def make_transit_time_callback(self, speed_kmph=10): - """ - Creates a callback function for transit time. Assuming an average - speed of speed_kmph - Args: - speed_kmph: the average speed in km/h + Examples: + >>> dist_mat = customers.make_distance_mat(method='haversine') + >>> dist_mat = customers.make_distance_mat(method='manhattan') + AssertionError + """ + self.distmat = np.zeros((self.number, self.number)) + methods = {'haversine': self._haversine} + assert method in methods + for frm_idx in range(self.number): + for to_idx in range(self.number): + if frm_idx != to_idx: + frm_c = self.customers[frm_idx] + to_c = self.customers[to_idx] + self.distmat[frm_idx, to_idx] = self._haversine( + frm_c.lon, frm_c.lat, to_c.lon, to_c.lat + ) + return self.distmat + + def _haversine(self, lon1, lat1, lon2, lat2): + """ + Calculate the great circle distance between two points + on the earth specified in decimal degrees of latitude and longitude. + https://en.wikipedia.org/wiki/Haversine_formula - Returns: - function [transit_time_return(a, b)]: A function that takes the - from/a node index and the to/b node index and returns the - transit time from a to b. - """ + Args: + lon1: longitude of pt 1, + lat1: latitude of pt 1, + lon2: longitude of pt 2, + lat2: latitude of pt 2 - def transit_time_return(a, b): - return (self.distmat[a][b] / (speed_kmph * 1.0 / 60**2)) + Returns: + the distace in km between pt1 and pt2 + """ + # convert decimal degrees to radians + lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2]) + + # haversine formula + dlon = lon2 - lon1 + dlat = lat2 - lat1 + a = ( + np.sin(dlat / 2) ** 2 + + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2) ** 2 + ) + c = 2 * np.arcsin(np.sqrt(a)) + + # 6367 km is the radius of the Earth + km = 6367 * c + return km + + def get_total_demand(self): + """ + Return the total demand of all customers. + """ + return sum([c.demand for c in self.customers]) - return transit_time_return + def return_dist_callback(self, **kwargs): + """ + Return a callback function for the distance matrix. + Args: **kwargs: Arbitrary keyword arguments passed on to + make_distance_mat() -class Vehicles(): + Returns: + function: dist_return(a,b) A function that takes the 'from' node + index and the 'to' node index and returns the distance in km. """ - A Class to create and hold vehicle information. - - The Vehicles in a CVRPTW problem service the customers and belong to a - depot. The class Vehicles creates a list of named tuples describing the - Vehicles. The main characteristics are the vehicle capacity, fixed cost, - and cost per km. The fixed cost of using a certain type of vehicles can be - higher or lower than others. If a vehicle is used, i.e. this vehicle serves - at least one node, then this cost is added to the objective function. - - Note: - If numpy arrays are given for capacity and cost, then they must be of - the same length, and the number of vehicles are inferred from them. - If scalars are given, the fleet is homogeneous, and the number of - vehicles is determined by number. - - Args: capacity (scalar or numpy array): The integer capacity of demand - units. cost (scalar or numpy array): The fixed cost of the vehicle. number - (Optional [int]): The number of vehicles in a homogeneous fleet. - """ + self.make_distance_mat(**kwargs) - def __init__(self, capacity=100, cost=100, number=None): - - Vehicle = namedtuple('Vehicle', ['index', 'capacity', 'cost']) - - if number is None: - self.number = np.size(capacity) - else: - self.number = number - idxs = np.array(range(0, self.number)) - - if np.isscalar(capacity): - capacities = capacity * np.ones_like(idxs) - elif np.size(capacity) != self.number: - print('capacity is neither scalar, nor the same size as num!') - else: - capacities = capacity - - if np.isscalar(cost): - costs = cost * np.ones_like(idxs) - elif np.size(cost) != self.number: - print(np.size(cost)) - print('cost is neither scalar, nor the same size as num!') - else: - costs = cost - - self.vehicles = [ - Vehicle(idx, capacity, cost) - for idx, capacity, cost in zip(idxs, capacities, costs) - ] - - def get_total_capacity(self): - return (sum([c.capacity for c in self.vehicles])) - - def return_starting_callback(self, customers, sameStartFinish=False): - # create a different starting and finishing depot for each vehicle - self.starts = [ - int(customers.central_start_node()) for o in range(self.number) - ] - if sameStartFinish: - self.ends = self.starts - else: - self.ends = [ - int(customers.central_start_node(invert=True)) - for o in range(self.number) - ] - # the depots will not have demands, so zero them. - for depot in self.starts: - customers.zero_depot_demands(depot) - for depot in self.ends: - customers.zero_depot_demands(depot) - - def start_return(v): - return (self.starts[v]) - - return start_return + def dist_return(from_index, to_index): + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = self.manager.IndexToNode(from_index) + to_node = self.manager.IndexToNode(to_index) + return self.distmat[from_node][to_node] + return dist_return -def discrete_cmap(N, base_cmap=None): + def return_dem_callback(self): """ - Create an N-bin discrete colormap from the specified input map + Return a callback function that gives the demands. + + Returns: + function: dem_return(a) A function that takes the 'from' node + index and returns the distance in km. """ - # Note that if base_cmap is a string or None, you can simply do - # return plt.cm.get_cmap(base_cmap, N) - # The following works for string, None, or a colormap instance: - base = plt.cm.get_cmap(base_cmap) - color_list = base(np.linspace(0, 1, N)) - cmap_name = base.name + str(N) - return base.from_list(cmap_name, color_list, N) + def dem_return(from_index): + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = self.manager.IndexToNode(from_index) + return self.customers[from_node].demand + return dem_return -def vehicle_output_string(manager, routing, plan): + def zero_depot_demands(self, depot): + """ + Zero out the demands and time windows of depot. The Depots do not have + demands or time windows so this function clears them. + + Args: depot (int): index of the stop to modify into a depot. + Examples: >>> customers.zero_depot_demands(5) >>> + customers.customers[5].demand == 0 True """ - Return a string displaying the output of the routing instance and - assignment (plan). + start_depot = self.customers[depot] + self.customers[depot] = start_depot._replace( + demand=0, tw_open=None, tw_close=None + ) - Args: routing (ortools.constraint_solver.pywrapcp.RoutingModel): routing. - plan (ortools.constraint_solver.pywrapcp.Assignment): the assignment. + def make_service_time_call_callback(self): + """ + Return a callback function that provides the time spent servicing the + customer. Here is it proportional to the demand given by + self.service_time_per_dem, default 300 seconds per unit demand. Returns: - (string) plan_output: describing each vehicle's plan. - - (List) dropped: list of dropped orders. + function [dem_return(a, b)]: A function that takes the from/a node + index and the to/b node index and returns the service time at a """ - dropped = [] - for order in range(routing.Size()): - if (plan.Value(routing.NextVar(order)) == order): - dropped.append(str(order)) - - capacity_dimension = routing.GetDimensionOrDie('Capacity') - time_dimension = routing.GetDimensionOrDie('Time') - plan_output = '' - - for route_number in range(routing.vehicles()): - order = routing.Start(route_number) - plan_output += 'Route {0}:'.format(route_number) - if routing.IsEnd(plan.Value(routing.NextVar(order))): - plan_output += ' Empty \n' - else: - while True: - load_var = capacity_dimension.CumulVar(order) - time_var = time_dimension.CumulVar(order) - node = manager.IndexToNode(order) - plan_output += \ - ' {node} Load({load}) Time({tmin}, {tmax}) -> '.format( - node=node, - load=plan.Value(load_var), - tmin=str(timedelta(seconds=plan.Min(time_var))), - tmax=str(timedelta(seconds=plan.Max(time_var)))) - - if routing.IsEnd(order): - plan_output += ' EndRoute {0}. \n'.format(route_number) - break - order = plan.Value(routing.NextVar(order)) - plan_output += '\n' - - return (plan_output, dropped) + def service_time_return(a, b): + return self.customers[a].demand * self.service_time_per_dem -def build_vehicle_route(manager, routing, plan, customers, veh_number): - """ - Build a route for a vehicle by starting at the strat node and - continuing to the end node. + return service_time_return - Args: routing (ortools.constraint_solver.pywrapcp.RoutingModel): routing. - plan (ortools.constraint_solver.pywrapcp.Assignment): the assignment. - customers (Customers): the customers instance. veh_number (int): index of - the vehicle + def make_transit_time_callback(self, speed_kmph=10): + """ + Creates a callback function for transit time. Assuming an average + speed of speed_kmph + Args: + speed_kmph: the average speed in km/h Returns: - (List) route: indexes of the customers for vehicle veh_number + function [transit_time_return(a, b)]: A function that takes the + from/a node index and the to/b node index and returns the + transit time from a to b. """ - veh_used = routing.IsVehicleUsed(plan, veh_number) - print('Vehicle {0} is used {1}'.format(veh_number, veh_used)) - if veh_used: - route = [] - node = routing.Start(veh_number) # Get the starting node index - route.append(customers.customers[manager.IndexToNode(node)]) - while not routing.IsEnd(node): - route.append(customers.customers[manager.IndexToNode(node)]) - node = plan.Value(routing.NextVar(node)) - - route.append(customers.customers[manager.IndexToNode(node)]) - return route + + def transit_time_return(a, b): + return self.distmat[a][b] / (speed_kmph * 1.0 / 60**2) + + return transit_time_return + + +class Vehicles: + """ + A Class to create and hold vehicle information. + + The Vehicles in a CVRPTW problem service the customers and belong to a + depot. The class Vehicles creates a list of named tuples describing the + Vehicles. The main characteristics are the vehicle capacity, fixed cost, + and cost per km. The fixed cost of using a certain type of vehicles can be + higher or lower than others. If a vehicle is used, i.e. this vehicle serves + at least one node, then this cost is added to the objective function. + + Note: + If numpy arrays are given for capacity and cost, then they must be of + the same length, and the number of vehicles are inferred from them. + If scalars are given, the fleet is homogeneous, and the number of + vehicles is determined by number. + + Args: capacity (scalar or numpy array): The integer capacity of demand + units. cost (scalar or numpy array): The fixed cost of the vehicle. number + (Optional [int]): The number of vehicles in a homogeneous fleet. + """ + + def __init__(self, capacity=100, cost=100, number=None): + + Vehicle = namedtuple('Vehicle', ['index', 'capacity', 'cost']) + + if number is None: + self.number = np.size(capacity) else: - return None + self.number = number + idxs = np.array(range(0, self.number)) + if np.isscalar(capacity): + capacities = capacity * np.ones_like(idxs) + elif np.size(capacity) != self.number: + print('capacity is neither scalar, nor the same size as num!') + else: + capacities = capacity -def plot_vehicle_routes(veh_route, ax1, customers, vehicles): - """ - Plot the vehicle routes on matplotlib axis ax1. + if np.isscalar(cost): + costs = cost * np.ones_like(idxs) + elif np.size(cost) != self.number: + print(np.size(cost)) + print('cost is neither scalar, nor the same size as num!') + else: + costs = cost + + self.vehicles = [ + Vehicle(idx, capacity, cost) + for idx, capacity, cost in zip(idxs, capacities, costs) + ] + + def get_total_capacity(self): + return sum([c.capacity for c in self.vehicles]) + + def return_starting_callback(self, customers, sameStartFinish=False): + # create a different starting and finishing depot for each vehicle + self.starts = [ + int(customers.central_start_node()) for o in range(self.number) + ] + if sameStartFinish: + self.ends = self.starts + else: + self.ends = [ + int(customers.central_start_node(invert=True)) + for o in range(self.number) + ] + # the depots will not have demands, so zero them. + for depot in self.starts: + customers.zero_depot_demands(depot) + for depot in self.ends: + customers.zero_depot_demands(depot) + + def start_return(v): + return self.starts[v] - Args: veh_route (dict): a dictionary of routes keyed by vehicle idx. ax1 - (matplotlib.axes._subplots.AxesSubplot): Matplotlib axes customers - (Customers): the customers instance. vehicles (Vehicles): the vehicles - instance. + return start_return + + +def discrete_cmap(N, base_cmap=None): """ - veh_used = [v for v in veh_route if veh_route[v] is not None] - - cmap = discrete_cmap(vehicles.number + 2, 'nipy_spectral') - - for veh_number in veh_used: - - lats, lons = zip(*[(c.lat, c.lon) for c in veh_route[veh_number]]) - lats = np.array(lats) - lons = np.array(lons) - s_dep = customers.customers[vehicles.starts[veh_number]] - s_fin = customers.customers[vehicles.ends[veh_number]] - ax1.annotate( - 'v({veh}) S @ {node}'.format( - veh=veh_number, node=vehicles.starts[veh_number]), - xy=(s_dep.lon, s_dep.lat), - xytext=(10, 10), - xycoords='data', - textcoords='offset points', - arrowprops=dict( - arrowstyle='->', - connectionstyle='angle3,angleA=90,angleB=0', - shrinkA=0.05), - ) - ax1.annotate( - 'v({veh}) F @ {node}'.format( - veh=veh_number, node=vehicles.ends[veh_number]), - xy=(s_fin.lon, s_fin.lat), - xytext=(10, -20), - xycoords='data', - textcoords='offset points', - arrowprops=dict( - arrowstyle='->', - connectionstyle='angle3,angleA=-90,angleB=0', - shrinkA=0.05), + Create an N-bin discrete colormap from the specified input map + """ + # Note that if base_cmap is a string or None, you can simply do + # return plt.cm.get_cmap(base_cmap, N) + # The following works for string, None, or a colormap instance: + + base = plt.cm.get_cmap(base_cmap) + color_list = base(np.linspace(0, 1, N)) + cmap_name = base.name + str(N) + return base.from_list(cmap_name, color_list, N) + + +def vehicle_output_string(manager, routing, plan): + """ + Return a string displaying the output of the routing instance and + assignment (plan). + + Args: routing (ortools.constraint_solver.pywrapcp.RoutingModel): routing. + plan (ortools.constraint_solver.pywrapcp.Assignment): the assignment. + + Returns: + (string) plan_output: describing each vehicle's plan. + + (List) dropped: list of dropped orders. + + """ + dropped = [] + for order in range(routing.Size()): + if plan.Value(routing.NextVar(order)) == order: + dropped.append(str(order)) + + capacity_dimension = routing.GetDimensionOrDie('Capacity') + time_dimension = routing.GetDimensionOrDie('Time') + plan_output = '' + + for route_number in range(routing.vehicles()): + order = routing.Start(route_number) + plan_output += 'Route {0}:'.format(route_number) + if routing.IsEnd(plan.Value(routing.NextVar(order))): + plan_output += ' Empty \n' + else: + while True: + load_var = capacity_dimension.CumulVar(order) + time_var = time_dimension.CumulVar(order) + node = manager.IndexToNode(order) + plan_output += ' {node} Load({load}) Time({tmin}, {tmax}) -> '.format( + node=node, + load=plan.Value(load_var), + tmin=str(timedelta(seconds=plan.Min(time_var))), + tmax=str(timedelta(seconds=plan.Max(time_var))), ) - ax1.plot(lons, lats, 'o', mfc=cmap(veh_number + 1)) - ax1.quiver( - lons[:-1], - lats[:-1], - lons[1:] - lons[:-1], - lats[1:] - lats[:-1], - scale_units='xy', - angles='xy', - scale=1, - color=cmap(veh_number + 1)) + + if routing.IsEnd(order): + plan_output += ' EndRoute {0}. \n'.format(route_number) + break + order = plan.Value(routing.NextVar(order)) + plan_output += '\n' + + return (plan_output, dropped) + + +def build_vehicle_route(manager, routing, plan, customers, veh_number): + """ + Build a route for a vehicle by starting at the strat node and + continuing to the end node. + + Args: routing (ortools.constraint_solver.pywrapcp.RoutingModel): routing. + plan (ortools.constraint_solver.pywrapcp.Assignment): the assignment. + customers (Customers): the customers instance. veh_number (int): index of + the vehicle + + Returns: + (List) route: indexes of the customers for vehicle veh_number + """ + veh_used = routing.IsVehicleUsed(plan, veh_number) + print('Vehicle {0} is used {1}'.format(veh_number, veh_used)) + if veh_used: + route = [] + node = routing.Start(veh_number) # Get the starting node index + route.append(customers.customers[manager.IndexToNode(node)]) + while not routing.IsEnd(node): + route.append(customers.customers[manager.IndexToNode(node)]) + node = plan.Value(routing.NextVar(node)) + + route.append(customers.customers[manager.IndexToNode(node)]) + return route + else: + return None + + +def plot_vehicle_routes(veh_route, ax1, customers, vehicles): + """ + Plot the vehicle routes on matplotlib axis ax1. + + Args: veh_route (dict): a dictionary of routes keyed by vehicle idx. ax1 + (matplotlib.axes._subplots.AxesSubplot): Matplotlib axes customers + (Customers): the customers instance. vehicles (Vehicles): the vehicles + instance. + """ + veh_used = [v for v in veh_route if veh_route[v] is not None] + + cmap = discrete_cmap(vehicles.number + 2, 'nipy_spectral') + + for veh_number in veh_used: + + lats, lons = zip(*[(c.lat, c.lon) for c in veh_route[veh_number]]) + lats = np.array(lats) + lons = np.array(lons) + s_dep = customers.customers[vehicles.starts[veh_number]] + s_fin = customers.customers[vehicles.ends[veh_number]] + ax1.annotate( + 'v({veh}) S @ {node}'.format( + veh=veh_number, node=vehicles.starts[veh_number] + ), + xy=(s_dep.lon, s_dep.lat), + xytext=(10, 10), + xycoords='data', + textcoords='offset points', + arrowprops=dict( + arrowstyle='->', + connectionstyle='angle3,angleA=90,angleB=0', + shrinkA=0.05, + ), + ) + ax1.annotate( + 'v({veh}) F @ {node}'.format( + veh=veh_number, node=vehicles.ends[veh_number] + ), + xy=(s_fin.lon, s_fin.lat), + xytext=(10, -20), + xycoords='data', + textcoords='offset points', + arrowprops=dict( + arrowstyle='->', + connectionstyle='angle3,angleA=-90,angleB=0', + shrinkA=0.05, + ), + ) + ax1.plot(lons, lats, 'o', mfc=cmap(veh_number + 1)) + ax1.quiver( + lons[:-1], + lats[:-1], + lons[1:] - lons[:-1], + lats[1:] - lats[:-1], + scale_units='xy', + angles='xy', + scale=1, + color=cmap(veh_number + 1), + ) def main(): - # Create a set of customer, (and depot) stops. - customers = Customers( - num_stops=50, - min_demand=1, - max_demand=15, - box_size=40, - min_tw=3, - max_tw=6) - - # Create a list of inhomgenious vehicle capacities as integer units. - capacity = [50, 75, 100, 125, 150, 175, 200, 250] - - # Create a list of inhomogeneous fixed vehicle costs. - cost = [int(100 + 2 * np.sqrt(c)) for c in capacity] - - # Create a set of vehicles, the number set by the length of capacity. - vehicles = Vehicles(capacity=capacity, cost=cost) - - # check to see that the problem is feasible, if we don't have enough - # vehicles to cover the demand, there is no point in going further. - assert (customers.get_total_demand() < vehicles.get_total_capacity()) - - # Set the starting nodes, and create a callback fn for the starting node. - start_fn = vehicles.return_starting_callback( - customers, sameStartFinish=False) - - # Create the routing index manager. - manager = pywrapcp.RoutingIndexManager( - customers.number, # int number - vehicles.number, # int number - vehicles.starts, # List of int start depot - vehicles.ends) # List of int end depot - - customers.set_manager(manager) - - # Set model parameters - model_parameters = pywrapcp.DefaultRoutingModelParameters() - - # The solver parameters can be accessed from the model parameters. For example : - # model_parameters.solver_parameters.CopyFrom( - # pywrapcp.Solver.DefaultSolverParameters()) - # model_parameters.solver_parameters.trace_propagation = True - - # Make the routing model instance. - routing = pywrapcp.RoutingModel(manager, model_parameters) - - parameters = pywrapcp.DefaultRoutingSearchParameters() - # Setting first solution heuristic (cheapest addition). - parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC) - # Routing: forbids use of TSPOpt neighborhood, (this is the default behaviour) - parameters.local_search_operators.use_tsp_opt = pywrapcp.BOOL_FALSE - # Disabling Large Neighborhood Search, (this is the default behaviour) - parameters.local_search_operators.use_path_lns = pywrapcp.BOOL_FALSE - parameters.local_search_operators.use_inactive_lns = pywrapcp.BOOL_FALSE - - parameters.time_limit.seconds = 10 - parameters.use_full_propagation = True - #parameters.log_search = True - - # Create callback fns for distances, demands, service and transit-times. - dist_fn = customers.return_dist_callback() - dist_fn_index = routing.RegisterTransitCallback(dist_fn) - - dem_fn = customers.return_dem_callback() - dem_fn_index = routing.RegisterUnaryTransitCallback(dem_fn) - - # Create and register a transit callback. - serv_time_fn = customers.make_service_time_call_callback() - transit_time_fn = customers.make_transit_time_callback() - def tot_time_fn(from_index, to_index): - """ - The time function we want is both transit time and service time. - """ - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return serv_time_fn(from_node, to_node) + transit_time_fn(from_node, to_node) - - tot_time_fn_index = routing.RegisterTransitCallback(tot_time_fn) - - # Set the cost function (distance callback) for each arc, homogeneous for - # all vehicles. - routing.SetArcCostEvaluatorOfAllVehicles(dist_fn_index) - - # Set vehicle costs for each vehicle, not homogeneous. - for veh in vehicles.vehicles: - routing.SetFixedCostOfVehicle(veh.cost, int(veh.index)) - - # Add a dimension for vehicle capacities - null_capacity_slack = 0 - routing.AddDimensionWithVehicleCapacity( - dem_fn_index, # demand callback - null_capacity_slack, - capacity, # capacity array - True, - 'Capacity') - # Add a dimension for time and a limit on the total time_horizon - routing.AddDimension( - tot_time_fn_index, # total time function callback - customers.time_horizon, - customers.time_horizon, - True, - 'Time') - - time_dimension = routing.GetDimensionOrDie('Time') - for cust in customers.customers: - if cust.tw_open is not None: - time_dimension.CumulVar(manager.NodeToIndex(cust.index)).SetRange( - cust.tw_open.seconds, cust.tw_close.seconds) + # Create a set of customer, (and depot) stops. + customers = Customers( + num_stops=50, min_demand=1, max_demand=15, box_size=40, min_tw=3, max_tw=6 + ) + + # Create a list of inhomgenious vehicle capacities as integer units. + capacity = [50, 75, 100, 125, 150, 175, 200, 250] + + # Create a list of inhomogeneous fixed vehicle costs. + cost = [int(100 + 2 * np.sqrt(c)) for c in capacity] + + # Create a set of vehicles, the number set by the length of capacity. + vehicles = Vehicles(capacity=capacity, cost=cost) + + # check to see that the problem is feasible, if we don't have enough + # vehicles to cover the demand, there is no point in going further. + assert customers.get_total_demand() < vehicles.get_total_capacity() + + # Set the starting nodes, and create a callback fn for the starting node. + start_fn = vehicles.return_starting_callback(customers, sameStartFinish=False) + + # Create the routing index manager. + manager = pywrapcp.RoutingIndexManager( + customers.number, # int number + vehicles.number, # int number + vehicles.starts, # List of int start depot + vehicles.ends, + ) # List of int end depot + + customers.set_manager(manager) + + # Set model parameters + model_parameters = pywrapcp.DefaultRoutingModelParameters() + + # The solver parameters can be accessed from the model parameters. For example : + # model_parameters.solver_parameters.CopyFrom( + # pywrapcp.Solver.DefaultSolverParameters()) + # model_parameters.solver_parameters.trace_propagation = True + + # Make the routing model instance. + routing = pywrapcp.RoutingModel(manager, model_parameters) + + parameters = pywrapcp.DefaultRoutingSearchParameters() + # Setting first solution heuristic (cheapest addition). + parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # Routing: forbids use of TSPOpt neighborhood, (this is the default behaviour) + parameters.local_search_operators.use_tsp_opt = pywrapcp.BOOL_FALSE + # Disabling Large Neighborhood Search, (this is the default behaviour) + parameters.local_search_operators.use_path_lns = pywrapcp.BOOL_FALSE + parameters.local_search_operators.use_inactive_lns = pywrapcp.BOOL_FALSE + + parameters.time_limit.seconds = 10 + parameters.use_full_propagation = True + # parameters.log_search = True + + # Create callback fns for distances, demands, service and transit-times. + dist_fn = customers.return_dist_callback() + dist_fn_index = routing.RegisterTransitCallback(dist_fn) + + dem_fn = customers.return_dem_callback() + dem_fn_index = routing.RegisterUnaryTransitCallback(dem_fn) + + # Create and register a transit callback. + serv_time_fn = customers.make_service_time_call_callback() + transit_time_fn = customers.make_transit_time_callback() + + def tot_time_fn(from_index, to_index): + """ + The time function we want is both transit time and service time. """ + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return serv_time_fn(from_node, to_node) + transit_time_fn( + from_node, to_node + ) + + tot_time_fn_index = routing.RegisterTransitCallback(tot_time_fn) + + # Set the cost function (distance callback) for each arc, homogeneous for + # all vehicles. + routing.SetArcCostEvaluatorOfAllVehicles(dist_fn_index) + + # Set vehicle costs for each vehicle, not homogeneous. + for veh in vehicles.vehicles: + routing.SetFixedCostOfVehicle(veh.cost, int(veh.index)) + + # Add a dimension for vehicle capacities + null_capacity_slack = 0 + routing.AddDimensionWithVehicleCapacity( + dem_fn_index, # demand callback + null_capacity_slack, + capacity, # capacity array + True, + 'Capacity', + ) + # Add a dimension for time and a limit on the total time_horizon + routing.AddDimension( + tot_time_fn_index, # total time function callback + customers.time_horizon, + customers.time_horizon, + True, + 'Time', + ) + + time_dimension = routing.GetDimensionOrDie('Time') + for cust in customers.customers: + if cust.tw_open is not None: + time_dimension.CumulVar(manager.NodeToIndex(cust.index)).SetRange( + cust.tw_open.seconds, cust.tw_close.seconds + ) + """ To allow the dropping of orders, we add disjunctions to all the customer nodes. Each disjunction is a list of 1 index, which allows that customer to be active or not, with a penalty if not. The penalty should be larger than the cost of servicing that customer, or it will always be dropped! """ - # To add disjunctions just to the customers, make a list of non-depots. - non_depot = set(range(customers.number)) - non_depot.difference_update(vehicles.starts) - non_depot.difference_update(vehicles.ends) - penalty = 400000 # The cost for dropping a node from the plan. - nodes = [routing.AddDisjunction([manager.NodeToIndex(c)], penalty) for c in non_depot] - - # This is how you would implement partial routes if you already knew part - # of a feasible solution for example: - # partial = np.random.choice(list(non_depot), size=(4,5), replace=False) - - # routing.CloseModel() - # partial_list = [partial[0,:].tolist(), - # partial[1,:].tolist(), - # partial[2,:].tolist(), - # partial[3,:].tolist(), - # [],[],[],[]] - # print(routing.ApplyLocksToAllVehicles(partial_list, False)) - - # Solve the problem ! - assignment = routing.SolveWithParameters(parameters) - - # The rest is all optional for saving, printing or plotting the solution. - if assignment: - ## save the assignment, (Google Protobuf format) - #save_file_base = os.path.realpath(__file__).split('.')[0] - #if routing.WriteAssignment(save_file_base + '_assignment.ass'): - # print('succesfully wrote assignment to file ' + save_file_base + - # '_assignment.ass') - - print('The Objective Value is {0}'.format(assignment.ObjectiveValue())) - - plan_output, dropped = vehicle_output_string(manager, routing, assignment) - print(plan_output) - print('dropped nodes: ' + ', '.join(dropped)) - - # you could print debug information like this: - # print(routing.DebugOutputAssignment(assignment, 'Capacity')) - - vehicle_routes = {} - for veh in range(vehicles.number): - vehicle_routes[veh] = build_vehicle_route(manager, routing, assignment, - customers, veh) - - # Plotting of the routes in matplotlib. - fig = plt.figure() - ax = fig.add_subplot(111) - # Plot all the nodes as black dots. - clon, clat = zip(*[(c.lon, c.lat) for c in customers.customers]) - ax.plot(clon, clat, 'k.') - # plot the routes as arrows - plot_vehicle_routes(vehicle_routes, ax, customers, vehicles) - plt.show() - - else: - print('No assignment') + # To add disjunctions just to the customers, make a list of non-depots. + non_depot = set(range(customers.number)) + non_depot.difference_update(vehicles.starts) + non_depot.difference_update(vehicles.ends) + penalty = 400000 # The cost for dropping a node from the plan. + nodes = [ + routing.AddDisjunction([manager.NodeToIndex(c)], penalty) + for c in non_depot + ] + + # This is how you would implement partial routes if you already knew part + # of a feasible solution for example: + # partial = np.random.choice(list(non_depot), size=(4,5), replace=False) + + # routing.CloseModel() + # partial_list = [partial[0,:].tolist(), + # partial[1,:].tolist(), + # partial[2,:].tolist(), + # partial[3,:].tolist(), + # [],[],[],[]] + # print(routing.ApplyLocksToAllVehicles(partial_list, False)) + + # Solve the problem ! + assignment = routing.SolveWithParameters(parameters) + + # The rest is all optional for saving, printing or plotting the solution. + if assignment: + ## save the assignment, (Google Protobuf format) + # save_file_base = os.path.realpath(__file__).split('.')[0] + # if routing.WriteAssignment(save_file_base + '_assignment.ass'): + # print('succesfully wrote assignment to file ' + save_file_base + + # '_assignment.ass') + + print('The Objective Value is {0}'.format(assignment.ObjectiveValue())) + + plan_output, dropped = vehicle_output_string(manager, routing, assignment) + print(plan_output) + print('dropped nodes: ' + ', '.join(dropped)) + + # you could print debug information like this: + # print(routing.DebugOutputAssignment(assignment, 'Capacity')) + + vehicle_routes = {} + for veh in range(vehicles.number): + vehicle_routes[veh] = build_vehicle_route( + manager, routing, assignment, customers, veh + ) + + # Plotting of the routes in matplotlib. + fig = plt.figure() + ax = fig.add_subplot(111) + # Plot all the nodes as black dots. + clon, clat = zip(*[(c.lon, c.lat) for c in customers.customers]) + ax.plot(clon, clat, 'k.') + # plot the routes as arrows + plot_vehicle_routes(vehicle_routes, ax, customers, vehicles) + plt.show() + + else: + print('No assignment') if __name__ == '__main__': - main() + main() diff --git a/examples/python/flexible_job_shop_sat.py b/examples/python/flexible_job_shop_sat.py index aa617420c4c..106aff453b8 100644 --- a/examples/python/flexible_job_shop_sat.py +++ b/examples/python/flexible_job_shop_sat.py @@ -31,175 +31,175 @@ class SolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self) -> None: - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 + def __init__(self) -> None: + cp_model.CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 - def on_solution_callback(self) -> None: - """Called at each new solution.""" - print( - f"Solution {self.__solution_count}, time = {self.wall_time} s," - f" objective = {self.objective_value}" - ) - self.__solution_count += 1 + def on_solution_callback(self) -> None: + """Called at each new solution.""" + print( + f"Solution {self.__solution_count}, time = {self.wall_time} s," + f" objective = {self.objective_value}" + ) + self.__solution_count += 1 def flexible_jobshop() -> None: - """solve a small flexible jobshop problem.""" - # Data part. - jobs = [ # task = (processing_time, machine_id) - [ # Job 0 - [(3, 0), (1, 1), (5, 2)], # task 0 with 3 alternatives - [(2, 0), (4, 1), (6, 2)], # task 1 with 3 alternatives - [(2, 0), (3, 1), (1, 2)], # task 2 with 3 alternatives - ], - [ # Job 1 - [(2, 0), (3, 1), (4, 2)], - [(1, 0), (5, 1), (4, 2)], - [(2, 0), (1, 1), (4, 2)], - ], - [ # Job 2 - [(2, 0), (1, 1), (4, 2)], - [(2, 0), (3, 1), (4, 2)], - [(3, 0), (1, 1), (5, 2)], - ], - ] - - num_jobs = len(jobs) - all_jobs = range(num_jobs) - - num_machines = 3 - all_machines = range(num_machines) - - # Model the flexible jobshop problem. - model = cp_model.CpModel() - - horizon = 0 - for job in jobs: - for task in job: - max_task_duration = 0 - for alternative in task: - max_task_duration = max(max_task_duration, alternative[0]) - horizon += max_task_duration - - print(f"Horizon = {horizon}") - - # Global storage of variables. - intervals_per_resources = collections.defaultdict(list) - starts = {} # indexed by (job_id, task_id). - presences = {} # indexed by (job_id, task_id, alt_id). - job_ends: list[cp_model.IntVar] = [] - - # Scan the jobs and create the relevant variables and intervals. + """solve a small flexible jobshop problem.""" + # Data part. + jobs = [ # task = (processing_time, machine_id) + [ # Job 0 + [(3, 0), (1, 1), (5, 2)], # task 0 with 3 alternatives + [(2, 0), (4, 1), (6, 2)], # task 1 with 3 alternatives + [(2, 0), (3, 1), (1, 2)], # task 2 with 3 alternatives + ], + [ # Job 1 + [(2, 0), (3, 1), (4, 2)], + [(1, 0), (5, 1), (4, 2)], + [(2, 0), (1, 1), (4, 2)], + ], + [ # Job 2 + [(2, 0), (1, 1), (4, 2)], + [(2, 0), (3, 1), (4, 2)], + [(3, 0), (1, 1), (5, 2)], + ], + ] + + num_jobs = len(jobs) + all_jobs = range(num_jobs) + + num_machines = 3 + all_machines = range(num_machines) + + # Model the flexible jobshop problem. + model = cp_model.CpModel() + + horizon = 0 + for job in jobs: + for task in job: + max_task_duration = 0 + for alternative in task: + max_task_duration = max(max_task_duration, alternative[0]) + horizon += max_task_duration + + print(f"Horizon = {horizon}") + + # Global storage of variables. + intervals_per_resources = collections.defaultdict(list) + starts = {} # indexed by (job_id, task_id). + presences = {} # indexed by (job_id, task_id, alt_id). + job_ends: list[cp_model.IntVar] = [] + + # Scan the jobs and create the relevant variables and intervals. + for job_id in all_jobs: + job = jobs[job_id] + num_tasks = len(job) + previous_end = None + for task_id in range(num_tasks): + task = job[task_id] + + min_duration = task[0][0] + max_duration = task[0][0] + + num_alternatives = len(task) + all_alternatives = range(num_alternatives) + + for alt_id in range(1, num_alternatives): + alt_duration = task[alt_id][0] + min_duration = min(min_duration, alt_duration) + max_duration = max(max_duration, alt_duration) + + # Create main interval for the task. + suffix_name = f"_j{job_id}_t{task_id}" + start = model.new_int_var(0, horizon, "start" + suffix_name) + duration = model.new_int_var( + min_duration, max_duration, "duration" + suffix_name + ) + end = model.new_int_var(0, horizon, "end" + suffix_name) + interval = model.new_interval_var( + start, duration, end, "interval" + suffix_name + ) + + # Store the start for the solution. + starts[(job_id, task_id)] = start + + # Add precedence with previous task in the same job. + if previous_end is not None: + model.add(start >= previous_end) + previous_end = end + + # Create alternative intervals. + if num_alternatives > 1: + l_presences = [] + for alt_id in all_alternatives: + alt_suffix = f"_j{job_id}_t{task_id}_a{alt_id}" + l_presence = model.new_bool_var("presence" + alt_suffix) + l_start = model.new_int_var(0, horizon, "start" + alt_suffix) + l_duration = task[alt_id][0] + l_end = model.new_int_var(0, horizon, "end" + alt_suffix) + l_interval = model.new_optional_interval_var( + l_start, l_duration, l_end, l_presence, "interval" + alt_suffix + ) + l_presences.append(l_presence) + + # Link the primary/global variables with the local ones. + model.add(start == l_start).only_enforce_if(l_presence) + model.add(duration == l_duration).only_enforce_if(l_presence) + model.add(end == l_end).only_enforce_if(l_presence) + + # Add the local interval to the right machine. + intervals_per_resources[task[alt_id][1]].append(l_interval) + + # Store the presences for the solution. + presences[(job_id, task_id, alt_id)] = l_presence + + # Select exactly one presence variable. + model.add_exactly_one(l_presences) + else: + intervals_per_resources[task[0][1]].append(interval) + presences[(job_id, task_id, 0)] = model.new_constant(1) + + if previous_end is not None: + job_ends.append(previous_end) + + # Create machines constraints. + for machine_id in all_machines: + intervals = intervals_per_resources[machine_id] + if len(intervals) > 1: + model.add_no_overlap(intervals) + + # Makespan objective + makespan = model.new_int_var(0, horizon, "makespan") + model.add_max_equality(makespan, job_ends) + model.minimize(makespan) + + # Solve model. + solver = cp_model.CpSolver() + solution_printer = SolutionPrinter() + status = solver.solve(model, solution_printer) + + # Print final solution. + if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): + print(f"Optimal objective value: {solver.objective_value}") for job_id in all_jobs: - job = jobs[job_id] - num_tasks = len(job) - previous_end = None - for task_id in range(num_tasks): - task = job[task_id] - - min_duration = task[0][0] - max_duration = task[0][0] - - num_alternatives = len(task) - all_alternatives = range(num_alternatives) - - for alt_id in range(1, num_alternatives): - alt_duration = task[alt_id][0] - min_duration = min(min_duration, alt_duration) - max_duration = max(max_duration, alt_duration) - - # Create main interval for the task. - suffix_name = f"_j{job_id}_t{task_id}" - start = model.new_int_var(0, horizon, "start" + suffix_name) - duration = model.new_int_var( - min_duration, max_duration, "duration" + suffix_name - ) - end = model.new_int_var(0, horizon, "end" + suffix_name) - interval = model.new_interval_var( - start, duration, end, "interval" + suffix_name - ) - - # Store the start for the solution. - starts[(job_id, task_id)] = start - - # Add precedence with previous task in the same job. - if previous_end is not None: - model.add(start >= previous_end) - previous_end = end - - # Create alternative intervals. - if num_alternatives > 1: - l_presences = [] - for alt_id in all_alternatives: - alt_suffix = f"_j{job_id}_t{task_id}_a{alt_id}" - l_presence = model.new_bool_var("presence" + alt_suffix) - l_start = model.new_int_var(0, horizon, "start" + alt_suffix) - l_duration = task[alt_id][0] - l_end = model.new_int_var(0, horizon, "end" + alt_suffix) - l_interval = model.new_optional_interval_var( - l_start, l_duration, l_end, l_presence, "interval" + alt_suffix - ) - l_presences.append(l_presence) - - # Link the primary/global variables with the local ones. - model.add(start == l_start).only_enforce_if(l_presence) - model.add(duration == l_duration).only_enforce_if(l_presence) - model.add(end == l_end).only_enforce_if(l_presence) - - # Add the local interval to the right machine. - intervals_per_resources[task[alt_id][1]].append(l_interval) - - # Store the presences for the solution. - presences[(job_id, task_id, alt_id)] = l_presence - - # Select exactly one presence variable. - model.add_exactly_one(l_presences) - else: - intervals_per_resources[task[0][1]].append(interval) - presences[(job_id, task_id, 0)] = model.new_constant(1) - - if previous_end is not None: - job_ends.append(previous_end) - - # Create machines constraints. - for machine_id in all_machines: - intervals = intervals_per_resources[machine_id] - if len(intervals) > 1: - model.add_no_overlap(intervals) - - # Makespan objective - makespan = model.new_int_var(0, horizon, "makespan") - model.add_max_equality(makespan, job_ends) - model.minimize(makespan) - - # Solve model. - solver = cp_model.CpSolver() - solution_printer = SolutionPrinter() - status = solver.solve(model, solution_printer) - - # Print final solution. - if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): - print(f"Optimal objective value: {solver.objective_value}") - for job_id in all_jobs: - print(f"Job {job_id}") - for task_id, task in enumerate(jobs[job_id]): - start_value = solver.value(starts[(job_id, task_id)]) - machine: int = -1 - task_duration: int = -1 - selected: int = -1 - for alt_id, alt in enumerate(task): - if solver.boolean_value(presences[(job_id, task_id, alt_id)]): - task_duration, machine = alt - selected = alt_id - print( - f" task_{job_id}_{task_id} starts at {start_value} (alt" - f" {selected}, machine {machine}, duration {task_duration})" - ) - - print(solver.response_stats()) + print(f"Job {job_id}") + for task_id, task in enumerate(jobs[job_id]): + start_value = solver.value(starts[(job_id, task_id)]) + machine: int = -1 + task_duration: int = -1 + selected: int = -1 + for alt_id, alt in enumerate(task): + if solver.boolean_value(presences[(job_id, task_id, alt_id)]): + task_duration, machine = alt + selected = alt_id + print( + f" task_{job_id}_{task_id} starts at {start_value} (alt" + f" {selected}, machine {machine}, duration {task_duration})" + ) + + print(solver.response_stats()) flexible_jobshop() diff --git a/examples/python/gate_scheduling_sat.py b/examples/python/gate_scheduling_sat.py index 9cea61deb76..fdcad19f19f 100644 --- a/examples/python/gate_scheduling_sat.py +++ b/examples/python/gate_scheduling_sat.py @@ -30,135 +30,135 @@ def main(_) -> None: - """Solves the gate scheduling problem.""" - model = cp_model.CpModel() - - jobs = [ - [3, 3], # [duration, width] - [2, 5], - [1, 3], - [3, 7], - [7, 3], - [2, 2], - [2, 2], - [5, 5], - [10, 2], - [4, 3], - [2, 6], - [1, 2], - [6, 8], - [4, 5], - [3, 7], - ] - - max_width = 10 - - horizon = sum(t[0] for t in jobs) - num_jobs = len(jobs) - all_jobs = range(num_jobs) - - intervals = [] - intervals0 = [] - intervals1 = [] - performed = [] - starts = [] - ends = [] - demands = [] + """Solves the gate scheduling problem.""" + model = cp_model.CpModel() + + jobs = [ + [3, 3], # [duration, width] + [2, 5], + [1, 3], + [3, 7], + [7, 3], + [2, 2], + [2, 2], + [5, 5], + [10, 2], + [4, 3], + [2, 6], + [1, 2], + [6, 8], + [4, 5], + [3, 7], + ] + + max_width = 10 + + horizon = sum(t[0] for t in jobs) + num_jobs = len(jobs) + all_jobs = range(num_jobs) + + intervals = [] + intervals0 = [] + intervals1 = [] + performed = [] + starts = [] + ends = [] + demands = [] + + for i in all_jobs: + # Create main interval. + start = model.new_int_var(0, horizon, f"start_{i}") + duration = jobs[i][0] + end = model.new_int_var(0, horizon, f"end_{i}") + interval = model.new_interval_var(start, duration, end, f"interval_{i}") + starts.append(start) + intervals.append(interval) + ends.append(end) + demands.append(jobs[i][1]) + + # Create an optional copy of interval to be executed on machine 0. + performed_on_m0 = model.new_bool_var(f"perform_{i}_on_m0") + performed.append(performed_on_m0) + start0 = model.new_int_var(0, horizon, f"start_{i}_on_m0") + end0 = model.new_int_var(0, horizon, f"end_{i}_on_m0") + interval0 = model.new_optional_interval_var( + start0, duration, end0, performed_on_m0, f"interval_{i}_on_m0" + ) + intervals0.append(interval0) + + # Create an optional copy of interval to be executed on machine 1. + start1 = model.new_int_var(0, horizon, f"start_{i}_on_m1") + end1 = model.new_int_var(0, horizon, f"end_{i}_on_m1") + interval1 = model.new_optional_interval_var( + start1, + duration, + end1, + ~performed_on_m0, + f"interval_{i}_on_m1", + ) + intervals1.append(interval1) + + # We only propagate the constraint if the tasks is performed on the machine. + model.add(start0 == start).only_enforce_if(performed_on_m0) + model.add(start1 == start).only_enforce_if(~performed_on_m0) + + # Width constraint (modeled as a cumulative) + model.add_cumulative(intervals, demands, max_width) + + # Choose which machine to perform the jobs on. + model.add_no_overlap(intervals0) + model.add_no_overlap(intervals1) + + # Objective variable. + makespan = model.new_int_var(0, horizon, "makespan") + model.add_max_equality(makespan, ends) + model.minimize(makespan) + + # Symmetry breaking. + model.add(performed[0] == 0) + + # Solve model. + solver = cp_model.CpSolver() + solver.solve(model) + + # Output solution. + if visualization.RunFromIPython(): + output = visualization.SvgWrapper(solver.objective_value, max_width, 40.0) + output.AddTitle(f"Makespan = {solver.objective_value}") + color_manager = visualization.ColorManager() + color_manager.SeedRandomColor(0) for i in all_jobs: - # Create main interval. - start = model.new_int_var(0, horizon, f"start_{i}") - duration = jobs[i][0] - end = model.new_int_var(0, horizon, f"end_{i}") - interval = model.new_interval_var(start, duration, end, f"interval_{i}") - starts.append(start) - intervals.append(interval) - ends.append(end) - demands.append(jobs[i][1]) - - # Create an optional copy of interval to be executed on machine 0. - performed_on_m0 = model.new_bool_var(f"perform_{i}_on_m0") - performed.append(performed_on_m0) - start0 = model.new_int_var(0, horizon, f"start_{i}_on_m0") - end0 = model.new_int_var(0, horizon, f"end_{i}_on_m0") - interval0 = model.new_optional_interval_var( - start0, duration, end0, performed_on_m0, f"interval_{i}_on_m0" - ) - intervals0.append(interval0) - - # Create an optional copy of interval to be executed on machine 1. - start1 = model.new_int_var(0, horizon, f"start_{i}_on_m1") - end1 = model.new_int_var(0, horizon, f"end_{i}_on_m1") - interval1 = model.new_optional_interval_var( - start1, - duration, - end1, - ~performed_on_m0, - f"interval_{i}_on_m1", - ) - intervals1.append(interval1) - - # We only propagate the constraint if the tasks is performed on the machine. - model.add(start0 == start).only_enforce_if(performed_on_m0) - model.add(start1 == start).only_enforce_if(~performed_on_m0) - - # Width constraint (modeled as a cumulative) - model.add_cumulative(intervals, demands, max_width) - - # Choose which machine to perform the jobs on. - model.add_no_overlap(intervals0) - model.add_no_overlap(intervals1) - - # Objective variable. - makespan = model.new_int_var(0, horizon, "makespan") - model.add_max_equality(makespan, ends) - model.minimize(makespan) - - # Symmetry breaking. - model.add(performed[0] == 0) - - # Solve model. - solver = cp_model.CpSolver() - solver.solve(model) - - # Output solution. - if visualization.RunFromIPython(): - output = visualization.SvgWrapper(solver.objective_value, max_width, 40.0) - output.AddTitle(f"Makespan = {solver.objective_value}") - color_manager = visualization.ColorManager() - color_manager.SeedRandomColor(0) - - for i in all_jobs: - performed_machine = 1 - solver.value(performed[i]) - start_of_task = solver.value(starts[i]) - d_x = jobs[i][0] - d_y = jobs[i][1] - s_y = performed_machine * (max_width - d_y) - output.AddRectangle( - start_of_task, - s_y, - d_x, - d_y, - color_manager.RandomColor(), - "black", - f"j{i}", - ) - - output.AddXScale() - output.AddYScale() - output.Display() - else: - print("Solution") - print(f" - makespan = {solver.objective_value}") - for i in all_jobs: - performed_machine = 1 - solver.value(performed[i]) - start_of_task = solver.value(starts[i]) - print( - f" - Job {i} starts at {start_of_task} on machine" - f" {performed_machine}" - ) - print(solver.response_stats()) + performed_machine = 1 - solver.value(performed[i]) + start_of_task = solver.value(starts[i]) + d_x = jobs[i][0] + d_y = jobs[i][1] + s_y = performed_machine * (max_width - d_y) + output.AddRectangle( + start_of_task, + s_y, + d_x, + d_y, + color_manager.RandomColor(), + "black", + f"j{i}", + ) + + output.AddXScale() + output.AddYScale() + output.Display() + else: + print("Solution") + print(f" - makespan = {solver.objective_value}") + for i in all_jobs: + performed_machine = 1 - solver.value(performed[i]) + start_of_task = solver.value(starts[i]) + print( + f" - Job {i} starts at {start_of_task} on machine" + f" {performed_machine}" + ) + print(solver.response_stats()) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/golomb8.py b/examples/python/golomb8.py index cb2a2423ca0..4254aa34ee1 100755 --- a/examples/python/golomb8.py +++ b/examples/python/golomb8.py @@ -31,57 +31,55 @@ def main(_) -> None: - # Create the solver. - solver = pywrapcp.Solver("golomb ruler") - - size = 8 - var_max = size * size - all_vars = list(range(0, size)) - - marks = [solver.IntVar(0, var_max, "marks_%d" % i) for i in all_vars] - - objective = solver.Minimize(marks[size - 1], 1) - - solver.Add(marks[0] == 0) - - # We expand the creation of the diff array to avoid a pylint warning. - diffs = [] - for i in range(size - 1): - for j in range(i + 1, size): - diffs.append(marks[j] - marks[i]) - solver.Add(solver.AllDifferent(diffs)) - - solver.Add(marks[size - 1] - marks[size - 2] > marks[1] - marks[0]) - for i in range(size - 2): - solver.Add(marks[i + 1] > marks[i]) - - solution = solver.Assignment() - solution.Add(marks[size - 1]) - collector = solver.AllSolutionCollector(solution) - - solver.Solve( - solver.Phase(marks, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE), - [objective, collector], - ) - for i in range(0, collector.SolutionCount()): - obj_value = collector.Value(i, marks[size - 1]) - time = collector.WallTime(i) - branches = collector.Branches(i) - failures = collector.Failures(i) - print( - ("Solution #%i: value = %i, failures = %i, branches = %i," "time = %i ms") - % (i, obj_value, failures, branches, time) - ) - time = solver.WallTime() - branches = solver.Branches() - failures = solver.Failures() + # Create the solver. + solver = pywrapcp.Solver("golomb ruler") + + size = 8 + var_max = size * size + all_vars = list(range(0, size)) + + marks = [solver.IntVar(0, var_max, "marks_%d" % i) for i in all_vars] + + objective = solver.Minimize(marks[size - 1], 1) + + solver.Add(marks[0] == 0) + + # We expand the creation of the diff array to avoid a pylint warning. + diffs = [] + for i in range(size - 1): + for j in range(i + 1, size): + diffs.append(marks[j] - marks[i]) + solver.Add(solver.AllDifferent(diffs)) + + solver.Add(marks[size - 1] - marks[size - 2] > marks[1] - marks[0]) + for i in range(size - 2): + solver.Add(marks[i + 1] > marks[i]) + + solution = solver.Assignment() + solution.Add(marks[size - 1]) + collector = solver.AllSolutionCollector(solution) + + solver.Solve( + solver.Phase(marks, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE), + [objective, collector], + ) + for i in range(0, collector.SolutionCount()): + obj_value = collector.Value(i, marks[size - 1]) + time = collector.WallTime(i) + branches = collector.Branches(i) + failures = collector.Failures(i) print( - ( - "Total run : failures = %i, branches = %i, time = %i ms" - % (failures, branches, time) - ) + "Solution #%i: value = %i, failures = %i, branches = %i,time = %i ms" + % (i, obj_value, failures, branches, time) ) + time = solver.WallTime() + branches = solver.Branches() + failures = solver.Failures() + print(( + "Total run : failures = %i, branches = %i, time = %i ms" + % (failures, branches, time) + )) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/golomb_sat.py b/examples/python/golomb_sat.py index 6b4e19cc06c..28b2b81769f 100644 --- a/examples/python/golomb_sat.py +++ b/examples/python/golomb_sat.py @@ -39,57 +39,57 @@ def solve_golomb_ruler(order: int, params: str) -> None: - """Solve the Golomb ruler problem.""" - # Create the model. - model = cp_model.CpModel() - - var_max = order * order - all_vars = list(range(0, order)) - - marks = [model.new_int_var(0, var_max, f"marks_{i}") for i in all_vars] - - model.add(marks[0] == 0) - for i in range(order - 2): - model.add(marks[i + 1] > marks[i]) - - diffs = [] - for i in range(order - 1): - for j in range(i + 1, order): - diff = model.new_int_var(0, var_max, f"diff [{j},{i}]") - model.add(diff == marks[j] - marks[i]) - diffs.append(diff) - model.add_all_different(diffs) - - # symmetry breaking - if order > 2: - model.add(marks[order - 1] - marks[order - 2] > marks[1] - marks[0]) - - # Objective - model.minimize(marks[order - 1]) - - # Solve the model. - solver = cp_model.CpSolver() - if params: - text_format.Parse(params, solver.parameters) - solution_printer = cp_model.ObjectiveSolutionPrinter() - print(f"Golomb ruler(order={order})") - status = solver.solve(model, solution_printer) - - # Print solution. - if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): - for idx, var in enumerate(marks): - print(f"mark[{idx}]: {solver.value(var)}") - intervals = [solver.value(diff) for diff in diffs] - intervals.sort() - print(f"intervals: {intervals}") - print(solver.response_stats()) + """Solve the Golomb ruler problem.""" + # Create the model. + model = cp_model.CpModel() + + var_max = order * order + all_vars = list(range(0, order)) + + marks = [model.new_int_var(0, var_max, f"marks_{i}") for i in all_vars] + + model.add(marks[0] == 0) + for i in range(order - 2): + model.add(marks[i + 1] > marks[i]) + + diffs = [] + for i in range(order - 1): + for j in range(i + 1, order): + diff = model.new_int_var(0, var_max, f"diff [{j},{i}]") + model.add(diff == marks[j] - marks[i]) + diffs.append(diff) + model.add_all_different(diffs) + + # symmetry breaking + if order > 2: + model.add(marks[order - 1] - marks[order - 2] > marks[1] - marks[0]) + + # Objective + model.minimize(marks[order - 1]) + + # Solve the model. + solver = cp_model.CpSolver() + if params: + text_format.Parse(params, solver.parameters) + solution_printer = cp_model.ObjectiveSolutionPrinter() + print(f"Golomb ruler(order={order})") + status = solver.solve(model, solution_printer) + + # Print solution. + if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): + for idx, var in enumerate(marks): + print(f"mark[{idx}]: {solver.value(var)}") + intervals = [solver.value(diff) for diff in diffs] + intervals.sort() + print(f"intervals: {intervals}") + print(solver.response_stats()) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - solve_golomb_ruler(_ORDER.value, _PARAMS.value) + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + solve_golomb_ruler(_ORDER.value, _PARAMS.value) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/hidato_sat.py b/examples/python/hidato_sat.py index 879ab217452..99950674c72 100755 --- a/examples/python/hidato_sat.py +++ b/examples/python/hidato_sat.py @@ -21,202 +21,204 @@ def build_pairs(rows: int, cols: int) -> list[tuple[int, int]]: - """Build closeness pairs for consecutive numbers. - - Build set of allowed pairs such that two consecutive numbers touch - each other in the grid. - - Returns: - A list of pairs for allowed consecutive position of numbers. - - Args: - rows: the number of rows in the grid - cols: the number of columns in the grid - """ - result = [] - for x in range(rows): - for y in range(cols): - for dx in (-1, 0, 1): - for dy in (-1, 0, 1): - if ( - x + dx >= 0 - and x + dx < rows - and y + dy >= 0 - and y + dy < cols - and (dx != 0 or dy != 0) - ): - result.append((x * cols + y, (x + dx) * cols + (y + dy))) - return result + """Build closeness pairs for consecutive numbers. + + Build set of allowed pairs such that two consecutive numbers touch + each other in the grid. + + Returns: + A list of pairs for allowed consecutive position of numbers. + + Args: + rows: the number of rows in the grid + cols: the number of columns in the grid + """ + result = [] + for x in range(rows): + for y in range(cols): + for dx in (-1, 0, 1): + for dy in (-1, 0, 1): + if ( + x + dx >= 0 + and x + dx < rows + and y + dy >= 0 + and y + dy < cols + and (dx != 0 or dy != 0) + ): + result.append((x * cols + y, (x + dx) * cols + (y + dy))) + return result def print_solution(positions: list[int], rows: int, cols: int): - """Print a current solution.""" - # Create empty board. - board = [] - for _ in range(rows): - board.append([0] * cols) - # Fill board with solution value. - for k in range(rows * cols): - position = positions[k] - board[position // cols][position % cols] = k + 1 - # Print the board. - print("Solution") - print_matrix(board) + """Print a current solution.""" + # Create empty board. + board = [] + for _ in range(rows): + board.append([0] * cols) + # Fill board with solution value. + for k in range(rows * cols): + position = positions[k] + board[position // cols][position % cols] = k + 1 + # Print the board. + print("Solution") + print_matrix(board) def print_matrix(game: list[list[int]]) -> None: - """Pretty print of a matrix.""" - rows = len(game) - cols = len(game[0]) - for i in range(rows): - line = "" - for j in range(cols): - if game[i][j] == 0: - line += " ." - else: - line += f"{game[i][j]:3}" - print(line) + """Pretty print of a matrix.""" + rows = len(game) + cols = len(game[0]) + for i in range(rows): + line = "" + for j in range(cols): + if game[i][j] == 0: + line += " ." + else: + line += f"{game[i][j]:3}" + print(line) def build_puzzle(problem: int) -> Union[None, list[list[int]]]: - """Build the problem from its index.""" - # - # models, a 0 indicates an open cell which number is not yet known. - # - # - puzzle = None - if problem == 1: - # Simple problem - puzzle = [[6, 0, 9], [0, 2, 8], [1, 0, 0]] - - elif problem == 2: - puzzle = [ - [0, 44, 41, 0, 0, 0, 0], - [0, 43, 0, 28, 29, 0, 0], - [0, 1, 0, 0, 0, 33, 0], - [0, 2, 25, 4, 34, 0, 36], - [49, 16, 0, 23, 0, 0, 0], - [0, 19, 0, 0, 12, 7, 0], - [0, 0, 0, 14, 0, 0, 0], - ] - - elif problem == 3: - # Problems from the book: - # Gyora Bededek: 'Hidato: 2000 Pure Logic Puzzles' - # Problem 1 (Practice) - puzzle = [ - [0, 0, 20, 0, 0], - [0, 0, 0, 16, 18], - [22, 0, 15, 0, 0], - [23, 0, 1, 14, 11], - [0, 25, 0, 0, 12], - ] - - elif problem == 4: - # problem 2 (Practice) - puzzle = [ - [0, 0, 0, 0, 14], - [0, 18, 12, 0, 0], - [0, 0, 17, 4, 5], - [0, 0, 7, 0, 0], - [9, 8, 25, 1, 0], - ] - - elif problem == 5: - # problem 3 (Beginner) - puzzle = [ - [0, 26, 0, 0, 0, 18], - [0, 0, 27, 0, 0, 19], - [31, 23, 0, 0, 14, 0], - [0, 33, 8, 0, 15, 1], - [0, 0, 0, 5, 0, 0], - [35, 36, 0, 10, 0, 0], - ] - elif problem == 6: - # Problem 15 (Intermediate) - puzzle = [ - [64, 0, 0, 0, 0, 0, 0, 0], - [1, 63, 0, 59, 15, 57, 53, 0], - [0, 4, 0, 14, 0, 0, 0, 0], - [3, 0, 11, 0, 20, 19, 0, 50], - [0, 0, 0, 0, 22, 0, 48, 40], - [9, 0, 0, 32, 23, 0, 0, 41], - [27, 0, 0, 0, 36, 0, 46, 0], - [28, 30, 0, 35, 0, 0, 0, 0], - ] - return puzzle + """Build the problem from its index.""" + # + # models, a 0 indicates an open cell which number is not yet known. + # + # + puzzle = None + if problem == 1: + # Simple problem + puzzle = [[6, 0, 9], [0, 2, 8], [1, 0, 0]] + + elif problem == 2: + puzzle = [ + [0, 44, 41, 0, 0, 0, 0], + [0, 43, 0, 28, 29, 0, 0], + [0, 1, 0, 0, 0, 33, 0], + [0, 2, 25, 4, 34, 0, 36], + [49, 16, 0, 23, 0, 0, 0], + [0, 19, 0, 0, 12, 7, 0], + [0, 0, 0, 14, 0, 0, 0], + ] + + elif problem == 3: + # Problems from the book: + # Gyora Bededek: 'Hidato: 2000 Pure Logic Puzzles' + # Problem 1 (Practice) + puzzle = [ + [0, 0, 20, 0, 0], + [0, 0, 0, 16, 18], + [22, 0, 15, 0, 0], + [23, 0, 1, 14, 11], + [0, 25, 0, 0, 12], + ] + + elif problem == 4: + # problem 2 (Practice) + puzzle = [ + [0, 0, 0, 0, 14], + [0, 18, 12, 0, 0], + [0, 0, 17, 4, 5], + [0, 0, 7, 0, 0], + [9, 8, 25, 1, 0], + ] + + elif problem == 5: + # problem 3 (Beginner) + puzzle = [ + [0, 26, 0, 0, 0, 18], + [0, 0, 27, 0, 0, 19], + [31, 23, 0, 0, 14, 0], + [0, 33, 8, 0, 15, 1], + [0, 0, 0, 5, 0, 0], + [35, 36, 0, 10, 0, 0], + ] + elif problem == 6: + # Problem 15 (Intermediate) + puzzle = [ + [64, 0, 0, 0, 0, 0, 0, 0], + [1, 63, 0, 59, 15, 57, 53, 0], + [0, 4, 0, 14, 0, 0, 0, 0], + [3, 0, 11, 0, 20, 19, 0, 50], + [0, 0, 0, 0, 22, 0, 48, 40], + [9, 0, 0, 32, 23, 0, 0, 41], + [27, 0, 0, 0, 36, 0, 46, 0], + [28, 30, 0, 35, 0, 0, 0, 0], + ] + return puzzle def solve_hidato(puzzle: list[list[int]], index: int) -> None: - """solve the given hidato table.""" - # Create the model. - model = cp_model.CpModel() - - r = len(puzzle) - c = len(puzzle[0]) - if not visualization.RunFromIPython(): - print("") - print(f"----- Solving problem {index} -----") - print("") - print(f"Initial game ({r} x {c})") - print_matrix(puzzle) - - # - # Declare variables. - # - positions = [model.new_int_var(0, r * c - 1, f"p[{i}]") for i in range(r * c)] - - # - # Constraints. - # - model.add_all_different(positions) - - # - # Fill in the clues. - # - for i in range(r): - for j in range(c): - if puzzle[i][j] > 0: - model.add(positions[puzzle[i][j] - 1] == i * c + j) - - # Consecutive numbers much touch each other in the grid. - # We use an allowed assignment constraint to model it. - close_tuples = build_pairs(r, c) - for k in range(0, r * c - 1): - model.add_allowed_assignments([positions[k], positions[k + 1]], close_tuples) - - # - # Solution and search. - # - - solver = cp_model.CpSolver() - status = solver.solve(model) - - if status == cp_model.OPTIMAL: - if visualization.RunFromIPython(): - output = visualization.SvgWrapper(10, r, 40.0) - for i, var in enumerate(positions): - val = solver.value(var) - x = val % c - y = val // c - color = "white" if puzzle[y][x] == 0 else "lightgreen" - output.AddRectangle(x, r - y - 1, 1, 1, color, "black", str(i + 1)) - - output.AddTitle(f"Puzzle {index} solved in {solver.wall_time:.2f} s") - output.Display() - else: - print_solution( - [solver.value(x) for x in positions], - r, - c, - ) - print(solver.response_stats()) + """solve the given hidato table.""" + # Create the model. + model = cp_model.CpModel() + + r = len(puzzle) + c = len(puzzle[0]) + if not visualization.RunFromIPython(): + print("") + print(f"----- Solving problem {index} -----") + print("") + print(f"Initial game ({r} x {c})") + print_matrix(puzzle) + + # + # Declare variables. + # + positions = [model.new_int_var(0, r * c - 1, f"p[{i}]") for i in range(r * c)] + + # + # Constraints. + # + model.add_all_different(positions) + + # + # Fill in the clues. + # + for i in range(r): + for j in range(c): + if puzzle[i][j] > 0: + model.add(positions[puzzle[i][j] - 1] == i * c + j) + + # Consecutive numbers much touch each other in the grid. + # We use an allowed assignment constraint to model it. + close_tuples = build_pairs(r, c) + for k in range(0, r * c - 1): + model.add_allowed_assignments( + [positions[k], positions[k + 1]], close_tuples + ) + + # + # Solution and search. + # + + solver = cp_model.CpSolver() + status = solver.solve(model) + + if status == cp_model.OPTIMAL: + if visualization.RunFromIPython(): + output = visualization.SvgWrapper(10, r, 40.0) + for i, var in enumerate(positions): + val = solver.value(var) + x = val % c + y = val // c + color = "white" if puzzle[y][x] == 0 else "lightgreen" + output.AddRectangle(x, r - y - 1, 1, 1, color, "black", str(i + 1)) + + output.AddTitle(f"Puzzle {index} solved in {solver.wall_time:.2f} s") + output.Display() + else: + print_solution( + [solver.value(x) for x in positions], + r, + c, + ) + print(solver.response_stats()) def main(_): - for pb in range(1, 7): - solve_hidato(build_puzzle(pb), pb) + for pb in range(1, 7): + solve_hidato(build_puzzle(pb), pb) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/integer_programming.py b/examples/python/integer_programming.py index 8a040bd4337..e46c83123a0 100755 --- a/examples/python/integer_programming.py +++ b/examples/python/integer_programming.py @@ -18,106 +18,110 @@ def Announce(solver, api_type): - print( - "---- Integer programming example with " + solver + " (" + api_type + ") -----" - ) + print( + "---- Integer programming example with " + + solver + + " (" + + api_type + + ") -----" + ) def RunIntegerExampleNaturalLanguageAPI(optimization_problem_type): - """Example of simple integer program with natural language API.""" + """Example of simple integer program with natural language API.""" - solver = pywraplp.Solver.CreateSolver(optimization_problem_type) - if not solver: - return + solver = pywraplp.Solver.CreateSolver(optimization_problem_type) + if not solver: + return - Announce(optimization_problem_type, "natural language API") + Announce(optimization_problem_type, "natural language API") - infinity = solver.infinity() - # x1 and x2 are integer non-negative variables. - x1 = solver.IntVar(0.0, infinity, "x1") - x2 = solver.IntVar(0.0, infinity, "x2") + infinity = solver.infinity() + # x1 and x2 are integer non-negative variables. + x1 = solver.IntVar(0.0, infinity, "x1") + x2 = solver.IntVar(0.0, infinity, "x2") - solver.Minimize(x1 + 2 * x2) - solver.Add(3 * x1 + 2 * x2 >= 17) + solver.Minimize(x1 + 2 * x2) + solver.Add(3 * x1 + 2 * x2 >= 17) - SolveAndPrint(solver, [x1, x2]) + SolveAndPrint(solver, [x1, x2]) def RunIntegerExampleCppStyleAPI(optimization_problem_type): - """Example of simple integer program with the C++ style API.""" - solver = pywraplp.Solver.CreateSolver(optimization_problem_type) - if not solver: - return + """Example of simple integer program with the C++ style API.""" + solver = pywraplp.Solver.CreateSolver(optimization_problem_type) + if not solver: + return - Announce(optimization_problem_type, "C++ style API") + Announce(optimization_problem_type, "C++ style API") - infinity = solver.infinity() - # x1 and x2 are integer non-negative variables. - x1 = solver.IntVar(0.0, infinity, "x1") - x2 = solver.IntVar(0.0, infinity, "x2") + infinity = solver.infinity() + # x1 and x2 are integer non-negative variables. + x1 = solver.IntVar(0.0, infinity, "x1") + x2 = solver.IntVar(0.0, infinity, "x2") - # Minimize x1 + 2 * x2. - objective = solver.Objective() - objective.SetCoefficient(x1, 1) - objective.SetCoefficient(x2, 2) + # Minimize x1 + 2 * x2. + objective = solver.Objective() + objective.SetCoefficient(x1, 1) + objective.SetCoefficient(x2, 2) - # 2 * x2 + 3 * x1 >= 17. - ct = solver.Constraint(17, infinity) - ct.SetCoefficient(x1, 3) - ct.SetCoefficient(x2, 2) + # 2 * x2 + 3 * x1 >= 17. + ct = solver.Constraint(17, infinity) + ct.SetCoefficient(x1, 3) + ct.SetCoefficient(x2, 2) - SolveAndPrint(solver, [x1, x2]) + SolveAndPrint(solver, [x1, x2]) def SolveAndPrint(solver, variable_list): - """Solve the problem and print the solution.""" - print("Number of variables = %d" % solver.NumVariables()) - print("Number of constraints = %d" % solver.NumConstraints()) + """Solve the problem and print the solution.""" + print("Number of variables = %d" % solver.NumVariables()) + print("Number of constraints = %d" % solver.NumConstraints()) - result_status = solver.Solve() + result_status = solver.Solve() - # The problem has an optimal solution. - assert result_status == pywraplp.Solver.OPTIMAL + # The problem has an optimal solution. + assert result_status == pywraplp.Solver.OPTIMAL - # The solution looks legit (when using solvers others than - # GLOP_LINEAR_PROGRAMMING, verifying the solution is highly recommended!). - assert solver.VerifySolution(1e-7, True) + # The solution looks legit (when using solvers others than + # GLOP_LINEAR_PROGRAMMING, verifying the solution is highly recommended!). + assert solver.VerifySolution(1e-7, True) - print("Problem solved in %f milliseconds" % solver.wall_time()) + print("Problem solved in %f milliseconds" % solver.wall_time()) - # The objective value of the solution. - print("Optimal objective value = %f" % solver.Objective().Value()) + # The objective value of the solution. + print("Optimal objective value = %f" % solver.Objective().Value()) - # The value of each variable in the solution. - for variable in variable_list: - print("%s = %f" % (variable.name(), variable.solution_value())) + # The value of each variable in the solution. + for variable in variable_list: + print("%s = %f" % (variable.name(), variable.solution_value())) - print("Advanced usage:") - print("Problem solved in %d branch-and-bound nodes" % solver.nodes()) + print("Advanced usage:") + print("Problem solved in %d branch-and-bound nodes" % solver.nodes()) def RunAllIntegerExampleNaturalLanguageAPI(): - RunIntegerExampleNaturalLanguageAPI("GLPK") - # Disabling due to ASAN errors with CBC. - # RunIntegerExampleNaturalLanguageAPI('CBC') - RunIntegerExampleNaturalLanguageAPI("SCIP") - RunIntegerExampleNaturalLanguageAPI("SAT") - RunIntegerExampleNaturalLanguageAPI("XPRESS") + RunIntegerExampleNaturalLanguageAPI("GLPK") + # Disabling due to ASAN errors with CBC. + # RunIntegerExampleNaturalLanguageAPI('CBC') + RunIntegerExampleNaturalLanguageAPI("SCIP") + RunIntegerExampleNaturalLanguageAPI("SAT") + RunIntegerExampleNaturalLanguageAPI("XPRESS") def RunAllIntegerExampleCppStyleAPI(): - RunIntegerExampleCppStyleAPI("GLPK") - # Disabling due to ASAN errors with CBC. - # RunIntegerExampleCppStyleAPI('CBC') - RunIntegerExampleCppStyleAPI("SCIP") - RunIntegerExampleCppStyleAPI("SAT") - RunIntegerExampleCppStyleAPI("XPRESS") + RunIntegerExampleCppStyleAPI("GLPK") + # Disabling due to ASAN errors with CBC. + # RunIntegerExampleCppStyleAPI('CBC') + RunIntegerExampleCppStyleAPI("SCIP") + RunIntegerExampleCppStyleAPI("SAT") + RunIntegerExampleCppStyleAPI("XPRESS") def main(): - RunAllIntegerExampleNaturalLanguageAPI() - RunAllIntegerExampleCppStyleAPI() + RunAllIntegerExampleNaturalLanguageAPI() + RunAllIntegerExampleCppStyleAPI() if __name__ == "__main__": - main() + main() diff --git a/examples/python/jobshop_ft06_distance_sat.py b/examples/python/jobshop_ft06_distance_sat.py index ffe1901f1d7..8c3f5728b40 100755 --- a/examples/python/jobshop_ft06_distance_sat.py +++ b/examples/python/jobshop_ft06_distance_sat.py @@ -32,116 +32,116 @@ def distance_between_jobs(x: int, y: int) -> int: - """Returns the distance between tasks of job x and tasks of job y.""" - return abs(x - y) + """Returns the distance between tasks of job x and tasks of job y.""" + return abs(x - y) def jobshop_ft06_distance() -> None: - """Solves the ft06 jobshop with distances between tasks.""" - # Creates the model. - model = cp_model.CpModel() - - machines_count = 6 - jobs_count = 6 - all_machines = range(0, machines_count) - all_jobs = range(0, jobs_count) - - durations = [ - [1, 3, 6, 7, 3, 6], - [8, 5, 10, 10, 10, 4], - [5, 4, 8, 9, 1, 7], - [5, 5, 5, 3, 8, 9], - [9, 3, 5, 4, 3, 1], - [3, 3, 9, 10, 4, 1], - ] - - machines = [ - [2, 0, 1, 3, 5, 4], - [1, 2, 4, 5, 0, 3], - [2, 3, 5, 0, 1, 4], - [1, 0, 2, 3, 4, 5], - [2, 1, 4, 5, 0, 3], - [1, 3, 5, 0, 4, 2], - ] - - # Computes horizon statically. - horizon = 150 - - task_type = collections.namedtuple("task_type", "start end interval") - - # Creates jobs. - all_tasks = {} - for i in all_jobs: - for j in all_machines: - start_var = model.new_int_var(0, horizon, f"start_{i}_{j}") - duration = durations[i][j] - end_var = model.new_int_var(0, horizon, f"end_{i}_{j}") - interval_var = model.new_interval_var( - start_var, duration, end_var, f"interval_{i}_{j}" - ) - all_tasks[(i, j)] = task_type( - start=start_var, end=end_var, interval=interval_var - ) - - # Create disjuctive constraints. - for i in all_machines: - job_intervals = [] - job_indices = [] - job_starts = [] - job_ends = [] - for j in all_jobs: - for k in all_machines: - if machines[j][k] == i: - job_intervals.append(all_tasks[(j, k)].interval) - job_indices.append(j) - job_starts.append(all_tasks[(j, k)].start) - job_ends.append(all_tasks[(j, k)].end) - model.add_no_overlap(job_intervals) - - arcs = [] - for j1 in range(len(job_intervals)): - # Initial arc from the dummy node (0) to a task. - start_lit = model.new_bool_var(f"{j1} is first job") - arcs.append((0, j1 + 1, start_lit)) - # Final arc from an arc to the dummy node. - arcs.append((j1 + 1, 0, model.new_bool_var(f"{j1} is last job"))) - - for j2 in range(len(job_intervals)): - if j1 == j2: - continue - - lit = model.new_bool_var(f"{j2} follows {j1}") - arcs.append((j1 + 1, j2 + 1, lit)) - - # We add the reified precedence to link the literal with the - # times of the two tasks. - min_distance = distance_between_jobs(j1, j2) - model.add( - job_starts[j2] >= job_ends[j1] + min_distance - ).only_enforce_if(lit) - - model.add_circuit(arcs) - - # Precedences inside a job. - for i in all_jobs: - for j in range(0, machines_count - 1): - model.add(all_tasks[(i, j + 1)].start >= all_tasks[(i, j)].end) - - # Makespan objective. - obj_var = model.new_int_var(0, horizon, "makespan") - model.add_max_equality( - obj_var, [all_tasks[(i, machines_count - 1)].end for i in all_jobs] - ) - model.minimize(obj_var) - - # Solve model. - solver = cp_model.CpSolver() - status = solver.solve(model) - - # Output solution. - if status == cp_model.OPTIMAL: - print(f"Optimal makespan: {solver.objective_value}") - print(solver.response_stats()) + """Solves the ft06 jobshop with distances between tasks.""" + # Creates the model. + model = cp_model.CpModel() + + machines_count = 6 + jobs_count = 6 + all_machines = range(0, machines_count) + all_jobs = range(0, jobs_count) + + durations = [ + [1, 3, 6, 7, 3, 6], + [8, 5, 10, 10, 10, 4], + [5, 4, 8, 9, 1, 7], + [5, 5, 5, 3, 8, 9], + [9, 3, 5, 4, 3, 1], + [3, 3, 9, 10, 4, 1], + ] + + machines = [ + [2, 0, 1, 3, 5, 4], + [1, 2, 4, 5, 0, 3], + [2, 3, 5, 0, 1, 4], + [1, 0, 2, 3, 4, 5], + [2, 1, 4, 5, 0, 3], + [1, 3, 5, 0, 4, 2], + ] + + # Computes horizon statically. + horizon = 150 + + task_type = collections.namedtuple("task_type", "start end interval") + + # Creates jobs. + all_tasks = {} + for i in all_jobs: + for j in all_machines: + start_var = model.new_int_var(0, horizon, f"start_{i}_{j}") + duration = durations[i][j] + end_var = model.new_int_var(0, horizon, f"end_{i}_{j}") + interval_var = model.new_interval_var( + start_var, duration, end_var, f"interval_{i}_{j}" + ) + all_tasks[(i, j)] = task_type( + start=start_var, end=end_var, interval=interval_var + ) + + # Create disjuctive constraints. + for i in all_machines: + job_intervals = [] + job_indices = [] + job_starts = [] + job_ends = [] + for j in all_jobs: + for k in all_machines: + if machines[j][k] == i: + job_intervals.append(all_tasks[(j, k)].interval) + job_indices.append(j) + job_starts.append(all_tasks[(j, k)].start) + job_ends.append(all_tasks[(j, k)].end) + model.add_no_overlap(job_intervals) + + arcs = [] + for j1 in range(len(job_intervals)): + # Initial arc from the dummy node (0) to a task. + start_lit = model.new_bool_var(f"{j1} is first job") + arcs.append((0, j1 + 1, start_lit)) + # Final arc from an arc to the dummy node. + arcs.append((j1 + 1, 0, model.new_bool_var(f"{j1} is last job"))) + + for j2 in range(len(job_intervals)): + if j1 == j2: + continue + + lit = model.new_bool_var(f"{j2} follows {j1}") + arcs.append((j1 + 1, j2 + 1, lit)) + + # We add the reified precedence to link the literal with the + # times of the two tasks. + min_distance = distance_between_jobs(j1, j2) + model.add( + job_starts[j2] >= job_ends[j1] + min_distance + ).only_enforce_if(lit) + + model.add_circuit(arcs) + + # Precedences inside a job. + for i in all_jobs: + for j in range(0, machines_count - 1): + model.add(all_tasks[(i, j + 1)].start >= all_tasks[(i, j)].end) + + # Makespan objective. + obj_var = model.new_int_var(0, horizon, "makespan") + model.add_max_equality( + obj_var, [all_tasks[(i, machines_count - 1)].end for i in all_jobs] + ) + model.minimize(obj_var) + + # Solve model. + solver = cp_model.CpSolver() + status = solver.solve(model) + + # Output solution. + if status == cp_model.OPTIMAL: + print(f"Optimal makespan: {solver.objective_value}") + print(solver.response_stats()) jobshop_ft06_distance() diff --git a/examples/python/jobshop_ft06_sat.py b/examples/python/jobshop_ft06_sat.py index 0c22d03c97f..5fd7a600adc 100755 --- a/examples/python/jobshop_ft06_sat.py +++ b/examples/python/jobshop_ft06_sat.py @@ -30,90 +30,90 @@ def jobshop_ft06() -> None: - """Solves the ft06 jobshop.""" - # Creates the solver. - model = cp_model.CpModel() - - machines_count = 6 - jobs_count = 6 - all_machines = range(0, machines_count) - all_jobs = range(0, jobs_count) - - durations = [ - [1, 3, 6, 7, 3, 6], - [8, 5, 10, 10, 10, 4], - [5, 4, 8, 9, 1, 7], - [5, 5, 5, 3, 8, 9], - [9, 3, 5, 4, 3, 1], - [3, 3, 9, 10, 4, 1], - ] - - machines = [ - [2, 0, 1, 3, 5, 4], - [1, 2, 4, 5, 0, 3], - [2, 3, 5, 0, 1, 4], - [1, 0, 2, 3, 4, 5], - [2, 1, 4, 5, 0, 3], - [1, 3, 5, 0, 4, 2], - ] - - # Computes horizon dynamically. - horizon = sum([sum(durations[i]) for i in all_jobs]) - - task_type = collections.namedtuple("task_type", "start end interval") - - # Creates jobs. - all_tasks = {} - for i in all_jobs: - for j in all_machines: - start_var = model.new_int_var(0, horizon, f"start_{i}_{j}") - duration = durations[i][j] - end_var = model.new_int_var(0, horizon, f"end_{i}_{j}") - interval_var = model.new_interval_var( - start_var, duration, end_var, f"interval_{i}_{j}" - ) - all_tasks[(i, j)] = task_type( - start=start_var, end=end_var, interval=interval_var - ) - - # Create disjuctive constraints. - machine_to_jobs = {} - for i in all_machines: - machines_jobs = [] - for j in all_jobs: - for k in all_machines: - if machines[j][k] == i: - machines_jobs.append(all_tasks[(j, k)].interval) - machine_to_jobs[i] = machines_jobs - model.add_no_overlap(machines_jobs) - - # Precedences inside a job. - for i in all_jobs: - for j in range(0, machines_count - 1): - model.add(all_tasks[(i, j + 1)].start >= all_tasks[(i, j)].end) - - # Makespan objective. - obj_var = model.new_int_var(0, horizon, "makespan") - model.add_max_equality( - obj_var, [all_tasks[(i, machines_count - 1)].end for i in all_jobs] - ) - model.minimize(obj_var) - - # Solve the model. - solver = cp_model.CpSolver() - solver.parameters.log_search_progress = True - status = solver.solve(model) - - # Output the solution. - if status == cp_model.OPTIMAL: - if visualization.RunFromIPython(): - starts = [ - [solver.value(all_tasks[(i, j)][0]) for j in all_machines] - for i in all_jobs - ] - visualization.DisplayJobshop(starts, durations, machines, "FT06") - else: - print(f"Optimal makespan: {solver.objective_value}") + """Solves the ft06 jobshop.""" + # Creates the solver. + model = cp_model.CpModel() + + machines_count = 6 + jobs_count = 6 + all_machines = range(0, machines_count) + all_jobs = range(0, jobs_count) + + durations = [ + [1, 3, 6, 7, 3, 6], + [8, 5, 10, 10, 10, 4], + [5, 4, 8, 9, 1, 7], + [5, 5, 5, 3, 8, 9], + [9, 3, 5, 4, 3, 1], + [3, 3, 9, 10, 4, 1], + ] + + machines = [ + [2, 0, 1, 3, 5, 4], + [1, 2, 4, 5, 0, 3], + [2, 3, 5, 0, 1, 4], + [1, 0, 2, 3, 4, 5], + [2, 1, 4, 5, 0, 3], + [1, 3, 5, 0, 4, 2], + ] + + # Computes horizon dynamically. + horizon = sum([sum(durations[i]) for i in all_jobs]) + + task_type = collections.namedtuple("task_type", "start end interval") + + # Creates jobs. + all_tasks = {} + for i in all_jobs: + for j in all_machines: + start_var = model.new_int_var(0, horizon, f"start_{i}_{j}") + duration = durations[i][j] + end_var = model.new_int_var(0, horizon, f"end_{i}_{j}") + interval_var = model.new_interval_var( + start_var, duration, end_var, f"interval_{i}_{j}" + ) + all_tasks[(i, j)] = task_type( + start=start_var, end=end_var, interval=interval_var + ) + + # Create disjuctive constraints. + machine_to_jobs = {} + for i in all_machines: + machines_jobs = [] + for j in all_jobs: + for k in all_machines: + if machines[j][k] == i: + machines_jobs.append(all_tasks[(j, k)].interval) + machine_to_jobs[i] = machines_jobs + model.add_no_overlap(machines_jobs) + + # Precedences inside a job. + for i in all_jobs: + for j in range(0, machines_count - 1): + model.add(all_tasks[(i, j + 1)].start >= all_tasks[(i, j)].end) + + # Makespan objective. + obj_var = model.new_int_var(0, horizon, "makespan") + model.add_max_equality( + obj_var, [all_tasks[(i, machines_count - 1)].end for i in all_jobs] + ) + model.minimize(obj_var) + + # Solve the model. + solver = cp_model.CpSolver() + solver.parameters.log_search_progress = True + status = solver.solve(model) + + # Output the solution. + if status == cp_model.OPTIMAL: + if visualization.RunFromIPython(): + starts = [ + [solver.value(all_tasks[(i, j)][0]) for j in all_machines] + for i in all_jobs + ] + visualization.DisplayJobshop(starts, durations, machines, "FT06") + else: + print(f"Optimal makespan: {solver.objective_value}") jobshop_ft06() diff --git a/examples/python/jobshop_with_maintenance_sat.py b/examples/python/jobshop_with_maintenance_sat.py index 6c17e4074db..7b51fe8aaef 100644 --- a/examples/python/jobshop_with_maintenance_sat.py +++ b/examples/python/jobshop_with_maintenance_sat.py @@ -21,142 +21,145 @@ class SolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self) -> None: - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 + def __init__(self) -> None: + cp_model.CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 - def on_solution_callback(self) -> None: - """Called at each new solution.""" - print( - f"Solution {self.__solution_count}, time = {self.wall_time} s," - f" objective = {self.objective_value}" - ) - self.__solution_count += 1 - - -def jobshop_with_maintenance() -> None: - """Solves a jobshop with maintenance on one machine.""" - # Create the model. - model = cp_model.CpModel() - - jobs_data = [ # task = (machine_id, processing_time). - [(0, 3), (1, 2), (2, 2)], # Job0 - [(0, 2), (2, 1), (1, 4)], # Job1 - [(1, 4), (2, 3)], # Job2 - ] - - machines_count = 1 + max(task[0] for job in jobs_data for task in job) - all_machines = range(machines_count) - - # Computes horizon dynamically as the sum of all durations. - horizon = sum(task[1] for job in jobs_data for task in job) - - # Named tuple to store information about created variables. - task_type = collections.namedtuple("task_type", "start end interval") - # Named tuple to manipulate solution information. - assigned_task_type = collections.namedtuple( - "assigned_task_type", "start job index duration" + def on_solution_callback(self) -> None: + """Called at each new solution.""" + print( + f"Solution {self.__solution_count}, time = {self.wall_time} s," + f" objective = {self.objective_value}" ) + self.__solution_count += 1 - # Creates job intervals and add to the corresponding machine lists. - all_tasks = {} - machine_to_intervals = collections.defaultdict(list) +def jobshop_with_maintenance() -> None: + """Solves a jobshop with maintenance on one machine.""" + # Create the model. + model = cp_model.CpModel() + + jobs_data = [ # task = (machine_id, processing_time). + [(0, 3), (1, 2), (2, 2)], # Job0 + [(0, 2), (2, 1), (1, 4)], # Job1 + [(1, 4), (2, 3)], # Job2 + ] + + machines_count = 1 + max(task[0] for job in jobs_data for task in job) + all_machines = range(machines_count) + + # Computes horizon dynamically as the sum of all durations. + horizon = sum(task[1] for job in jobs_data for task in job) + + # Named tuple to store information about created variables. + task_type = collections.namedtuple("task_type", "start end interval") + # Named tuple to manipulate solution information. + assigned_task_type = collections.namedtuple( + "assigned_task_type", "start job index duration" + ) + + # Creates job intervals and add to the corresponding machine lists. + all_tasks = {} + machine_to_intervals = collections.defaultdict(list) + + for job_id, job in enumerate(jobs_data): + for entry in enumerate(job): + task_id, task = entry + machine, duration = task + suffix = f"_{job_id}_{task_id}" + start_var = model.new_int_var(0, horizon, "start" + suffix) + end_var = model.new_int_var(0, horizon, "end" + suffix) + interval_var = model.new_interval_var( + start_var, duration, end_var, "interval" + suffix + ) + all_tasks[job_id, task_id] = task_type( + start=start_var, end=end_var, interval=interval_var + ) + machine_to_intervals[machine].append(interval_var) + + # Add maintenance interval (machine 0 is not available on time {4, 5, 6, 7}). + machine_to_intervals[0].append(model.new_interval_var(4, 4, 8, "weekend_0")) + + # Create and add disjunctive constraints. + for machine in all_machines: + model.add_no_overlap(machine_to_intervals[machine]) + + # Precedences inside a job. + for job_id, job in enumerate(jobs_data): + for task_id in range(len(job) - 1): + model.add( + all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end + ) + + # Makespan objective. + obj_var = model.new_int_var(0, horizon, "makespan") + model.add_max_equality( + obj_var, + [ + all_tasks[job_id, len(job) - 1].end + for job_id, job in enumerate(jobs_data) + ], + ) + model.minimize(obj_var) + + # Solve model. + solver = cp_model.CpSolver() + solution_printer = SolutionPrinter() + status = solver.solve(model, solution_printer) + + # Output solution. + if status == cp_model.OPTIMAL: + # Create one list of assigned tasks per machine. + assigned_jobs = collections.defaultdict(list) for job_id, job in enumerate(jobs_data): - for entry in enumerate(job): - task_id, task = entry - machine, duration = task - suffix = f"_{job_id}_{task_id}" - start_var = model.new_int_var(0, horizon, "start" + suffix) - end_var = model.new_int_var(0, horizon, "end" + suffix) - interval_var = model.new_interval_var( - start_var, duration, end_var, "interval" + suffix - ) - all_tasks[job_id, task_id] = task_type( - start=start_var, end=end_var, interval=interval_var + for task_id, task in enumerate(job): + machine = task[0] + assigned_jobs[machine].append( + assigned_task_type( + start=solver.value(all_tasks[job_id, task_id].start), + job=job_id, + index=task_id, + duration=task[1], ) - machine_to_intervals[machine].append(interval_var) - - # Add maintenance interval (machine 0 is not available on time {4, 5, 6, 7}). - machine_to_intervals[0].append(model.new_interval_var(4, 4, 8, "weekend_0")) + ) - # Create and add disjunctive constraints. + # Create per machine output lines. + output = "" for machine in all_machines: - model.add_no_overlap(machine_to_intervals[machine]) + # Sort by starting time. + assigned_jobs[machine].sort() + sol_line_tasks = "Machine " + str(machine) + ": " + sol_line = " " - # Precedences inside a job. - for job_id, job in enumerate(jobs_data): - for task_id in range(len(job) - 1): - model.add( - all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end - ) + for assigned_task in assigned_jobs[machine]: + name = f"job_{assigned_task.job}_{assigned_task.index}" + # add spaces to output to align columns. + sol_line_tasks += f"{name:>10}" + start = assigned_task.start + duration = assigned_task.duration - # Makespan objective. - obj_var = model.new_int_var(0, horizon, "makespan") - model.add_max_equality( - obj_var, - [all_tasks[job_id, len(job) - 1].end for job_id, job in enumerate(jobs_data)], - ) - model.minimize(obj_var) - - # Solve model. - solver = cp_model.CpSolver() - solution_printer = SolutionPrinter() - status = solver.solve(model, solution_printer) - - # Output solution. - if status == cp_model.OPTIMAL: - # Create one list of assigned tasks per machine. - assigned_jobs = collections.defaultdict(list) - for job_id, job in enumerate(jobs_data): - for task_id, task in enumerate(job): - machine = task[0] - assigned_jobs[machine].append( - assigned_task_type( - start=solver.value(all_tasks[job_id, task_id].start), - job=job_id, - index=task_id, - duration=task[1], - ) - ) - - # Create per machine output lines. - output = "" - for machine in all_machines: - # Sort by starting time. - assigned_jobs[machine].sort() - sol_line_tasks = "Machine " + str(machine) + ": " - sol_line = " " - - for assigned_task in assigned_jobs[machine]: - name = f"job_{assigned_task.job}_{assigned_task.index}" - # add spaces to output to align columns. - sol_line_tasks += f"{name:>10}" - start = assigned_task.start - duration = assigned_task.duration - - sol_tmp = f"[{start}, {start + duration}]" - # add spaces to output to align columns. - sol_line += f"{sol_tmp:>10}" - - sol_line += "\n" - sol_line_tasks += "\n" - output += sol_line_tasks - output += sol_line - - # Finally print the solution found. - print(f"Optimal Schedule Length: {solver.objective_value}") - print(output) - print(solver.response_stats()) + sol_tmp = f"[{start}, {start + duration}]" + # add spaces to output to align columns. + sol_line += f"{sol_tmp:>10}" + + sol_line += "\n" + sol_line_tasks += "\n" + output += sol_line_tasks + output += sol_line + + # Finally print the solution found. + print(f"Optimal Schedule Length: {solver.objective_value}") + print(output) + print(solver.response_stats()) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - jobshop_with_maintenance() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + jobshop_with_maintenance() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/knapsack_2d_sat.py b/examples/python/knapsack_2d_sat.py index e7718215521..e1b5fd9eab9 100644 --- a/examples/python/knapsack_2d_sat.py +++ b/examples/python/knapsack_2d_sat.py @@ -44,8 +44,8 @@ def build_data() -> tuple[pd.Series, int, int]: - """Build the data frame.""" - data = """ + """Build the data frame.""" + data = """ item width height available value color k1 20 4 2 338.984 blue k2 12 17 6 849.246 orange @@ -59,360 +59,358 @@ def build_data() -> tuple[pd.Series, int, int]: k10 9 11 5 369.560 cyan """ - data = pd.read_table(io.StringIO(data), sep=r"\s+") - print("Input data") - print(data) + data = pd.read_table(io.StringIO(data), sep=r"\s+") + print("Input data") + print(data) - max_height = 20 - max_width = 30 + max_height = 20 + max_width = 30 - print(f"Container max_width:{max_width} max_height:{max_height}") - print(f"#Items: {len(data.index)}") - return (data, max_height, max_width) + print(f"Container max_width:{max_width} max_height:{max_height}") + print(f"#Items: {len(data.index)}") + return (data, max_height, max_width) def solve_with_duplicate_items( data: pd.Series, max_height: int, max_width: int ) -> None: - """solve the problem by building 2 items (rotated or not) for each item.""" - # Derived data (expanded to individual items). - data_widths = data["width"].to_numpy() - data_heights = data["height"].to_numpy() - data_availability = data["available"].to_numpy() - data_values = data["value"].to_numpy() - - # Non duplicated items data. - base_item_widths = np.repeat(data_widths, data_availability) - base_item_heights = np.repeat(data_heights, data_availability) - base_item_values = np.repeat(data_values, data_availability) - num_data_items = len(base_item_values) - - # Create rotated items by duplicating. - item_widths = np.concatenate((base_item_widths, base_item_heights)) - item_heights = np.concatenate((base_item_heights, base_item_widths)) - item_values = np.concatenate((base_item_values, base_item_values)) - - num_items = len(item_values) - - # OR-Tools model - model = cp_model.CpModel() - - # Variables - x_starts = [] - x_ends = [] - y_starts = [] - y_ends = [] - is_used = [] - x_intervals = [] - y_intervals = [] - - for i in range(num_items): - ## Is the item used? - is_used.append(model.new_bool_var(f"is_used{i}")) - - ## Item coordinates. - x_starts.append(model.new_int_var(0, max_width, f"x_start{i}")) - x_ends.append(model.new_int_var(0, max_width, f"x_end{i}")) - y_starts.append(model.new_int_var(0, max_height, f"y_start{i}")) - y_ends.append(model.new_int_var(0, max_height, f"y_end{i}")) - - ## Interval variables. - x_intervals.append( - model.new_interval_var( - x_starts[i], - item_widths[i] * is_used[i], - x_ends[i], - f"x_interval{i}", - ) - ) - y_intervals.append( - model.new_interval_var( - y_starts[i], - item_heights[i] * is_used[i], - y_ends[i], - f"y_interval{i}", - ) + """solve the problem by building 2 items (rotated or not) for each item.""" + # Derived data (expanded to individual items). + data_widths = data["width"].to_numpy() + data_heights = data["height"].to_numpy() + data_availability = data["available"].to_numpy() + data_values = data["value"].to_numpy() + + # Non duplicated items data. + base_item_widths = np.repeat(data_widths, data_availability) + base_item_heights = np.repeat(data_heights, data_availability) + base_item_values = np.repeat(data_values, data_availability) + num_data_items = len(base_item_values) + + # Create rotated items by duplicating. + item_widths = np.concatenate((base_item_widths, base_item_heights)) + item_heights = np.concatenate((base_item_heights, base_item_widths)) + item_values = np.concatenate((base_item_values, base_item_values)) + + num_items = len(item_values) + + # OR-Tools model + model = cp_model.CpModel() + + # Variables + x_starts = [] + x_ends = [] + y_starts = [] + y_ends = [] + is_used = [] + x_intervals = [] + y_intervals = [] + + for i in range(num_items): + ## Is the item used? + is_used.append(model.new_bool_var(f"is_used{i}")) + + ## Item coordinates. + x_starts.append(model.new_int_var(0, max_width, f"x_start{i}")) + x_ends.append(model.new_int_var(0, max_width, f"x_end{i}")) + y_starts.append(model.new_int_var(0, max_height, f"y_start{i}")) + y_ends.append(model.new_int_var(0, max_height, f"y_end{i}")) + + ## Interval variables. + x_intervals.append( + model.new_interval_var( + x_starts[i], + item_widths[i] * is_used[i], + x_ends[i], + f"x_interval{i}", ) - - # Unused boxes are fixed at (0.0). - model.add(x_starts[i] == 0).only_enforce_if(~is_used[i]) - model.add(y_starts[i] == 0).only_enforce_if(~is_used[i]) - - # Constraints. - - ## Only one of non-rotated/rotated pair can be used. - for i in range(num_data_items): - model.add(is_used[i] + is_used[i + num_data_items] <= 1) - - ## 2D no overlap. - model.add_no_overlap_2d(x_intervals, y_intervals) - - ## Objective. - model.maximize(cp_model.LinearExpr.weighted_sum(is_used, item_values)) - - # Output proto to file. - if _OUTPUT_PROTO.value: - print(f"Writing proto to {_OUTPUT_PROTO.value}") - with open(_OUTPUT_PROTO.value, "w") as text_file: - text_file.write(str(model)) - - # Solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - - status = solver.solve(model) - - # Report solution. - if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): - used = {i for i in range(num_items) if solver.boolean_value(is_used[i])} - data = pd.DataFrame( - { - "x_start": [solver.value(x_starts[i]) for i in used], - "y_start": [solver.value(y_starts[i]) for i in used], - "item_width": [item_widths[i] for i in used], - "item_height": [item_heights[i] for i in used], - "x_end": [solver.value(x_ends[i]) for i in used], - "y_end": [solver.value(y_ends[i]) for i in used], - "item_value": [item_values[i] for i in used], - } + ) + y_intervals.append( + model.new_interval_var( + y_starts[i], + item_heights[i] * is_used[i], + y_ends[i], + f"y_interval{i}", ) - print(data) + ) + + # Unused boxes are fixed at (0.0). + model.add(x_starts[i] == 0).only_enforce_if(~is_used[i]) + model.add(y_starts[i] == 0).only_enforce_if(~is_used[i]) + + # Constraints. + + ## Only one of non-rotated/rotated pair can be used. + for i in range(num_data_items): + model.add(is_used[i] + is_used[i + num_data_items] <= 1) + + ## 2D no overlap. + model.add_no_overlap_2d(x_intervals, y_intervals) + + ## Objective. + model.maximize(cp_model.LinearExpr.weighted_sum(is_used, item_values)) + + # Output proto to file. + if _OUTPUT_PROTO.value: + print(f"Writing proto to {_OUTPUT_PROTO.value}") + with open(_OUTPUT_PROTO.value, "w") as text_file: + text_file.write(str(model)) + + # Solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + + status = solver.solve(model) + + # Report solution. + if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): + used = {i for i in range(num_items) if solver.boolean_value(is_used[i])} + data = pd.DataFrame({ + "x_start": [solver.value(x_starts[i]) for i in used], + "y_start": [solver.value(y_starts[i]) for i in used], + "item_width": [item_widths[i] for i in used], + "item_height": [item_heights[i] for i in used], + "x_end": [solver.value(x_ends[i]) for i in used], + "y_end": [solver.value(y_ends[i]) for i in used], + "item_value": [item_values[i] for i in used], + }) + print(data) def solve_with_duplicate_optional_items( data: pd.Series, max_height: int, max_width: int ): - """solve the problem by building 2 optional items (rotated or not) for each item.""" - # Derived data (expanded to individual items). - data_widths = data["width"].to_numpy() - data_heights = data["height"].to_numpy() - data_availability = data["available"].to_numpy() - data_values = data["value"].to_numpy() - - # Non duplicated items data. - base_item_widths = np.repeat(data_widths, data_availability) - base_item_heights = np.repeat(data_heights, data_availability) - base_item_values = np.repeat(data_values, data_availability) - num_data_items = len(base_item_values) - - # Create rotated items by duplicating. - item_widths = np.concatenate((base_item_widths, base_item_heights)) - item_heights = np.concatenate((base_item_heights, base_item_widths)) - item_values = np.concatenate((base_item_values, base_item_values)) - - num_items = len(item_values) - - # OR-Tools model - model = cp_model.CpModel() - - # Variables - x_starts = [] - y_starts = [] - is_used = [] - x_intervals = [] - y_intervals = [] - - for i in range(num_items): - ## Is the item used? - is_used.append(model.new_bool_var(f"is_used{i}")) - - ## Item coordinates. - x_starts.append( - model.new_int_var(0, max_width - int(item_widths[i]), f"x_start{i}") - ) - y_starts.append( - model.new_int_var(0, max_height - int(item_heights[i]), f"y_start{i}") - ) - - ## Interval variables. - x_intervals.append( - model.new_optional_fixed_size_interval_var( - x_starts[i], item_widths[i], is_used[i], f"x_interval{i}" - ) - ) - y_intervals.append( - model.new_optional_fixed_size_interval_var( - y_starts[i], item_heights[i], is_used[i], f"y_interval{i}" - ) + """solve the problem by building 2 optional items (rotated or not) for each item.""" + # Derived data (expanded to individual items). + data_widths = data["width"].to_numpy() + data_heights = data["height"].to_numpy() + data_availability = data["available"].to_numpy() + data_values = data["value"].to_numpy() + + # Non duplicated items data. + base_item_widths = np.repeat(data_widths, data_availability) + base_item_heights = np.repeat(data_heights, data_availability) + base_item_values = np.repeat(data_values, data_availability) + num_data_items = len(base_item_values) + + # Create rotated items by duplicating. + item_widths = np.concatenate((base_item_widths, base_item_heights)) + item_heights = np.concatenate((base_item_heights, base_item_widths)) + item_values = np.concatenate((base_item_values, base_item_values)) + + num_items = len(item_values) + + # OR-Tools model + model = cp_model.CpModel() + + # Variables + x_starts = [] + y_starts = [] + is_used = [] + x_intervals = [] + y_intervals = [] + + for i in range(num_items): + ## Is the item used? + is_used.append(model.new_bool_var(f"is_used{i}")) + + ## Item coordinates. + x_starts.append( + model.new_int_var(0, max_width - int(item_widths[i]), f"x_start{i}") + ) + y_starts.append( + model.new_int_var(0, max_height - int(item_heights[i]), f"y_start{i}") + ) + + ## Interval variables. + x_intervals.append( + model.new_optional_fixed_size_interval_var( + x_starts[i], item_widths[i], is_used[i], f"x_interval{i}" ) - # Unused boxes are fixed at (0.0). - model.add(x_starts[i] == 0).only_enforce_if(~is_used[i]) - model.add(y_starts[i] == 0).only_enforce_if(~is_used[i]) - - # Constraints. - - ## Only one of non-rotated/rotated pair can be used. - for i in range(num_data_items): - model.add(is_used[i] + is_used[i + num_data_items] <= 1) - - ## 2D no overlap. - model.add_no_overlap_2d(x_intervals, y_intervals) - - ## Objective. - model.maximize(cp_model.LinearExpr.weighted_sum(is_used, item_values)) - - # Output proto to file. - if _OUTPUT_PROTO.value: - print(f"Writing proto to {_OUTPUT_PROTO.value}") - with open(_OUTPUT_PROTO.value, "w") as text_file: - text_file.write(str(model)) - - # solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - - status = solver.solve(model) - - # Report solution. - if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): - used = {i for i in range(num_items) if solver.boolean_value(is_used[i])} - data = pd.DataFrame( - { - "x_start": [solver.value(x_starts[i]) for i in used], - "y_start": [solver.value(y_starts[i]) for i in used], - "item_width": [item_widths[i] for i in used], - "item_height": [item_heights[i] for i in used], - "x_end": [solver.value(x_starts[i]) + item_widths[i] for i in used], - "y_end": [solver.value(y_starts[i]) + item_heights[i] for i in used], - "item_value": [item_values[i] for i in used], - } + ) + y_intervals.append( + model.new_optional_fixed_size_interval_var( + y_starts[i], item_heights[i], is_used[i], f"y_interval{i}" ) - print(data) + ) + # Unused boxes are fixed at (0.0). + model.add(x_starts[i] == 0).only_enforce_if(~is_used[i]) + model.add(y_starts[i] == 0).only_enforce_if(~is_used[i]) + + # Constraints. + + ## Only one of non-rotated/rotated pair can be used. + for i in range(num_data_items): + model.add(is_used[i] + is_used[i + num_data_items] <= 1) + + ## 2D no overlap. + model.add_no_overlap_2d(x_intervals, y_intervals) + + ## Objective. + model.maximize(cp_model.LinearExpr.weighted_sum(is_used, item_values)) + + # Output proto to file. + if _OUTPUT_PROTO.value: + print(f"Writing proto to {_OUTPUT_PROTO.value}") + with open(_OUTPUT_PROTO.value, "w") as text_file: + text_file.write(str(model)) + + # solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + + status = solver.solve(model) + + # Report solution. + if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): + used = {i for i in range(num_items) if solver.boolean_value(is_used[i])} + data = pd.DataFrame({ + "x_start": [solver.value(x_starts[i]) for i in used], + "y_start": [solver.value(y_starts[i]) for i in used], + "item_width": [item_widths[i] for i in used], + "item_height": [item_heights[i] for i in used], + "x_end": [solver.value(x_starts[i]) + item_widths[i] for i in used], + "y_end": [solver.value(y_starts[i]) + item_heights[i] for i in used], + "item_value": [item_values[i] for i in used], + }) + print(data) def solve_with_rotations(data: pd.Series, max_height: int, max_width: int): - """solve the problem by rotating items.""" - # Derived data (expanded to individual items). - data_widths = data["width"].to_numpy() - data_heights = data["height"].to_numpy() - data_availability = data["available"].to_numpy() - data_values = data["value"].to_numpy() - - item_widths = np.repeat(data_widths, data_availability) - item_heights = np.repeat(data_heights, data_availability) - item_values = np.repeat(data_values, data_availability) - - num_items = len(item_widths) - - # OR-Tools model. - model = cp_model.CpModel() - - # Coordinate variables for each rectangle. - x_starts = [] - x_sizes = [] - x_ends = [] - y_starts = [] - y_sizes = [] - y_ends = [] - x_intervals = [] - y_intervals = [] - - for i in range(num_items): - sizes = [0, int(item_widths[i]), int(item_heights[i])] - # X coordinates. - x_starts.append(model.new_int_var(0, max_width, f"x_start{i}")) - x_sizes.append( - model.new_int_var_from_domain( - cp_model.Domain.FromValues(sizes), f"x_size{i}" - ) + """solve the problem by rotating items.""" + # Derived data (expanded to individual items). + data_widths = data["width"].to_numpy() + data_heights = data["height"].to_numpy() + data_availability = data["available"].to_numpy() + data_values = data["value"].to_numpy() + + item_widths = np.repeat(data_widths, data_availability) + item_heights = np.repeat(data_heights, data_availability) + item_values = np.repeat(data_values, data_availability) + + num_items = len(item_widths) + + # OR-Tools model. + model = cp_model.CpModel() + + # Coordinate variables for each rectangle. + x_starts = [] + x_sizes = [] + x_ends = [] + y_starts = [] + y_sizes = [] + y_ends = [] + x_intervals = [] + y_intervals = [] + + for i in range(num_items): + sizes = [0, int(item_widths[i]), int(item_heights[i])] + # X coordinates. + x_starts.append(model.new_int_var(0, max_width, f"x_start{i}")) + x_sizes.append( + model.new_int_var_from_domain( + cp_model.Domain.FromValues(sizes), f"x_size{i}" ) - x_ends.append(model.new_int_var(0, max_width, f"x_end{i}")) - - # Y coordinates. - y_starts.append(model.new_int_var(0, max_height, f"y_start{i}")) - y_sizes.append( - model.new_int_var_from_domain( - cp_model.Domain.FromValues(sizes), f"y_size{i}" - ) + ) + x_ends.append(model.new_int_var(0, max_width, f"x_end{i}")) + + # Y coordinates. + y_starts.append(model.new_int_var(0, max_height, f"y_start{i}")) + y_sizes.append( + model.new_int_var_from_domain( + cp_model.Domain.FromValues(sizes), f"y_size{i}" ) - y_ends.append(model.new_int_var(0, max_height, f"y_end{i}")) + ) + y_ends.append(model.new_int_var(0, max_height, f"y_end{i}")) - ## Interval variables - x_intervals.append( - model.new_interval_var(x_starts[i], x_sizes[i], x_ends[i], f"x_interval{i}") + ## Interval variables + x_intervals.append( + model.new_interval_var( + x_starts[i], x_sizes[i], x_ends[i], f"x_interval{i}" ) - y_intervals.append( - model.new_interval_var(y_starts[i], y_sizes[i], y_ends[i], f"y_interval{i}") + ) + y_intervals.append( + model.new_interval_var( + y_starts[i], y_sizes[i], y_ends[i], f"y_interval{i}" ) - - # is_used[i] == True if and only if item i is selected. - is_used = [] - - # Constraints. - - ## for each item, decide is unselected, no_rotation, rotated. - for i in range(num_items): - not_selected = model.new_bool_var(f"not_selected_{i}") - no_rotation = model.new_bool_var(f"no_rotation_{i}") - rotated = model.new_bool_var(f"rotated_{i}") - - ### Exactly one state must be chosen. - model.add_exactly_one(not_selected, no_rotation, rotated) - - ### Define height and width according to the state. - dim1 = item_widths[i] - dim2 = item_heights[i] - # Unused boxes are fixed at (0.0). - model.add(x_sizes[i] == 0).only_enforce_if(not_selected) - model.add(y_sizes[i] == 0).only_enforce_if(not_selected) - model.add(x_starts[i] == 0).only_enforce_if(not_selected) - model.add(y_starts[i] == 0).only_enforce_if(not_selected) - # Sizes are fixed by the rotation. - model.add(x_sizes[i] == dim1).only_enforce_if(no_rotation) - model.add(y_sizes[i] == dim2).only_enforce_if(no_rotation) - model.add(x_sizes[i] == dim2).only_enforce_if(rotated) - model.add(y_sizes[i] == dim1).only_enforce_if(rotated) - - is_used.append(~not_selected) - - ## 2D no overlap. - model.add_no_overlap_2d(x_intervals, y_intervals) - - # Objective. - model.maximize(cp_model.LinearExpr.weighted_sum(is_used, item_values)) - - # Output proto to file. - if _OUTPUT_PROTO.value: - print(f"Writing proto to {_OUTPUT_PROTO.value}") - with open(_OUTPUT_PROTO.value, "w") as text_file: - text_file.write(str(model)) - - # solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - - status = solver.solve(model) - - # Report solution. - if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): - used = {i for i in range(num_items) if solver.boolean_value(is_used[i])} - data = pd.DataFrame( - { - "x_start": [solver.value(x_starts[i]) for i in used], - "y_start": [solver.value(y_starts[i]) for i in used], - "item_width": [solver.value(x_sizes[i]) for i in used], - "item_height": [solver.value(y_sizes[i]) for i in used], - "x_end": [solver.value(x_ends[i]) for i in used], - "y_end": [solver.value(y_ends[i]) for i in used], - "item_value": [item_values[i] for i in used], - } - ) - print(data) + ) + + # is_used[i] == True if and only if item i is selected. + is_used = [] + + # Constraints. + + ## for each item, decide is unselected, no_rotation, rotated. + for i in range(num_items): + not_selected = model.new_bool_var(f"not_selected_{i}") + no_rotation = model.new_bool_var(f"no_rotation_{i}") + rotated = model.new_bool_var(f"rotated_{i}") + + ### Exactly one state must be chosen. + model.add_exactly_one(not_selected, no_rotation, rotated) + + ### Define height and width according to the state. + dim1 = item_widths[i] + dim2 = item_heights[i] + # Unused boxes are fixed at (0.0). + model.add(x_sizes[i] == 0).only_enforce_if(not_selected) + model.add(y_sizes[i] == 0).only_enforce_if(not_selected) + model.add(x_starts[i] == 0).only_enforce_if(not_selected) + model.add(y_starts[i] == 0).only_enforce_if(not_selected) + # Sizes are fixed by the rotation. + model.add(x_sizes[i] == dim1).only_enforce_if(no_rotation) + model.add(y_sizes[i] == dim2).only_enforce_if(no_rotation) + model.add(x_sizes[i] == dim2).only_enforce_if(rotated) + model.add(y_sizes[i] == dim1).only_enforce_if(rotated) + + is_used.append(~not_selected) + + ## 2D no overlap. + model.add_no_overlap_2d(x_intervals, y_intervals) + + # Objective. + model.maximize(cp_model.LinearExpr.weighted_sum(is_used, item_values)) + + # Output proto to file. + if _OUTPUT_PROTO.value: + print(f"Writing proto to {_OUTPUT_PROTO.value}") + with open(_OUTPUT_PROTO.value, "w") as text_file: + text_file.write(str(model)) + + # solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + + status = solver.solve(model) + + # Report solution. + if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): + used = {i for i in range(num_items) if solver.boolean_value(is_used[i])} + data = pd.DataFrame({ + "x_start": [solver.value(x_starts[i]) for i in used], + "y_start": [solver.value(y_starts[i]) for i in used], + "item_width": [solver.value(x_sizes[i]) for i in used], + "item_height": [solver.value(y_sizes[i]) for i in used], + "x_end": [solver.value(x_ends[i]) for i in used], + "y_end": [solver.value(y_ends[i]) for i in used], + "item_value": [item_values[i] for i in used], + }) + print(data) def main(_): - """solve the problem with all models.""" - data, max_height, max_width = build_data() - if _MODEL.value == "duplicate": - solve_with_duplicate_items(data, max_height, max_width) - elif _MODEL.value == "optional": - solve_with_duplicate_optional_items(data, max_height, max_width) - else: - solve_with_rotations(data, max_height, max_width) + """solve the problem with all models.""" + data, max_height, max_width = build_data() + if _MODEL.value == "duplicate": + solve_with_duplicate_items(data, max_height, max_width) + elif _MODEL.value == "optional": + solve_with_duplicate_optional_items(data, max_height, max_width) + else: + solve_with_rotations(data, max_height, max_width) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/line_balancing_sat.py b/examples/python/line_balancing_sat.py index c80a747d3a0..060bcecd7e0 100644 --- a/examples/python/line_balancing_sat.py +++ b/examples/python/line_balancing_sat.py @@ -48,315 +48,315 @@ class SectionInfo: - """Store problem information for each section of the input file.""" + """Store problem information for each section of the input file.""" - def __init__(self): - self.value = None - self.index_map = {} - self.set_of_pairs = set() + def __init__(self): + self.value = None + self.index_map = {} + self.set_of_pairs = set() - def __str__(self): - if self.index_map: - return f"SectionInfo(index_map={self.index_map})" - elif self.set_of_pairs: - return f"SectionInfo(set_of_pairs={self.set_of_pairs})" - elif self.value is not None: - return f"SectionInfo(value={self.value})" - else: - return "SectionInfo()" + def __str__(self): + if self.index_map: + return f"SectionInfo(index_map={self.index_map})" + elif self.set_of_pairs: + return f"SectionInfo(set_of_pairs={self.set_of_pairs})" + elif self.value is not None: + return f"SectionInfo(value={self.value})" + else: + return "SectionInfo()" def read_problem(filename: str) -> Dict[str, SectionInfo]: - """Reads a .alb file and returns the problem.""" + """Reads a .alb file and returns the problem.""" - current_info = SectionInfo() + current_info = SectionInfo() - problem: Dict[str, SectionInfo] = {} - with open(filename, "r") as input_file: - print(f"Reading problem from '{filename}'") + problem: Dict[str, SectionInfo] = {} + with open(filename, "r") as input_file: + print(f"Reading problem from '{filename}'") - for line in input_file: - stripped_line = line.strip() - if not stripped_line: - continue + for line in input_file: + stripped_line = line.strip() + if not stripped_line: + continue - match_section_def = re.fullmatch(r"<([\w\s]+)>", stripped_line) - if match_section_def: - section_name = match_section_def.group(1) - if section_name == "end": - continue + match_section_def = re.fullmatch(r"<([\w\s]+)>", stripped_line) + if match_section_def: + section_name = match_section_def.group(1) + if section_name == "end": + continue - current_info = SectionInfo() - problem[section_name] = current_info - continue + current_info = SectionInfo() + problem[section_name] = current_info + continue - match_single_number = re.fullmatch(r"^([0-9]+)$", stripped_line) - if match_single_number: - current_info.value = int(match_single_number.group(1)) - continue + match_single_number = re.fullmatch(r"^([0-9]+)$", stripped_line) + if match_single_number: + current_info.value = int(match_single_number.group(1)) + continue - match_key_value = re.fullmatch(r"^([0-9]+)\s+([0-9]+)$", stripped_line) - if match_key_value: - key = int(match_key_value.group(1)) - value = int(match_key_value.group(2)) - current_info.index_map[key] = value - continue + match_key_value = re.fullmatch(r"^([0-9]+)\s+([0-9]+)$", stripped_line) + if match_key_value: + key = int(match_key_value.group(1)) + value = int(match_key_value.group(2)) + current_info.index_map[key] = value + continue - match_pair = re.fullmatch(r"^([0-9]+),([0-9]+)$", stripped_line) - if match_pair: - left = int(match_pair.group(1)) - right = int(match_pair.group(2)) - current_info.set_of_pairs.add((left, right)) - continue + match_pair = re.fullmatch(r"^([0-9]+),([0-9]+)$", stripped_line) + if match_pair: + left = int(match_pair.group(1)) + right = int(match_pair.group(2)) + current_info.set_of_pairs.add((left, right)) + continue - print(f"Unrecognized line '{stripped_line}'") + print(f"Unrecognized line '{stripped_line}'") - return problem + return problem def print_stats(problem: Dict[str, SectionInfo]) -> None: - print("Problem Statistics") - for key, value in problem.items(): - print(f" - {key}: {value}") + print("Problem Statistics") + for key, value in problem.items(): + print(f" - {key}: {value}") def solve_problem_greedily(problem: Dict[str, SectionInfo]) -> Dict[int, int]: - """Compute a greedy solution.""" - print("Solving using a Greedy heuristics") + """Compute a greedy solution.""" + print("Solving using a Greedy heuristics") - num_tasks = problem["number of tasks"].value - if num_tasks is None: - return {} - all_tasks = range(1, num_tasks + 1) # Tasks are 1 based in the data. - precedences = problem["precedence relations"].set_of_pairs - durations = problem["task times"].index_map - cycle_time = problem["cycle time"].value + num_tasks = problem["number of tasks"].value + if num_tasks is None: + return {} + all_tasks = range(1, num_tasks + 1) # Tasks are 1 based in the data. + precedences = problem["precedence relations"].set_of_pairs + durations = problem["task times"].index_map + cycle_time = problem["cycle time"].value - weights = collections.defaultdict(int) - successors = collections.defaultdict(list) + weights = collections.defaultdict(int) + successors = collections.defaultdict(list) - candidates = set(all_tasks) + candidates = set(all_tasks) - for before, after in precedences: - weights[after] += 1 - successors[before].append(after) - if after in candidates: - candidates.remove(after) + for before, after in precedences: + weights[after] += 1 + successors[before].append(after) + if after in candidates: + candidates.remove(after) - assignment: Dict[int, int] = {} - current_pod = 0 - residual_capacity = cycle_time + assignment: Dict[int, int] = {} + current_pod = 0 + residual_capacity = cycle_time - while len(assignment) < num_tasks: - if not candidates: - print("error empty") - break + while len(assignment) < num_tasks: + if not candidates: + print("error empty") + break - best = -1 - best_slack = cycle_time - best_duration = 0 + best = -1 + best_slack = cycle_time + best_duration = 0 - for c in candidates: - duration = durations[c] - slack = residual_capacity - duration - if slack < best_slack and slack >= 0: - best_slack = slack - best = c - best_duration = duration + for c in candidates: + duration = durations[c] + slack = residual_capacity - duration + if slack < best_slack and slack >= 0: + best_slack = slack + best = c + best_duration = duration - if best == -1: - current_pod += 1 - residual_capacity = cycle_time - continue + if best == -1: + current_pod += 1 + residual_capacity = cycle_time + continue - candidates.remove(best) - assignment[best] = current_pod - residual_capacity -= best_duration + candidates.remove(best) + assignment[best] = current_pod + residual_capacity -= best_duration - for succ in successors[best]: - weights[succ] -= 1 - if weights[succ] == 0: - candidates.add(succ) - del weights[succ] + for succ in successors[best]: + weights[succ] -= 1 + if weights[succ] == 0: + candidates.add(succ) + del weights[succ] - print(f" greedy solution uses {current_pod + 1} pods.") + print(f" greedy solution uses {current_pod + 1} pods.") - return assignment + return assignment def solve_problem_with_boolean_model( problem: Dict[str, SectionInfo], hint: Dict[int, int] ) -> None: - """solve the given problem.""" - - print("Solving using the Boolean model") - # problem data - num_tasks = problem["number of tasks"].value - if num_tasks is None: - return - all_tasks = range(1, num_tasks + 1) # Tasks are 1 based in the problem. - durations = problem["task times"].index_map - precedences = problem["precedence relations"].set_of_pairs - cycle_time = problem["cycle time"].value - - num_pods = max(p for _, p in hint.items()) + 1 if hint else num_tasks - 1 - all_pods = range(num_pods) - - model = cp_model.CpModel() - - # assign[t, p] indicates if task t is done on pod p. - assign = {} - # possible[t, p] indicates if task t is possible on pod p. - possible = {} - - # Create the variables - for t in all_tasks: - for p in all_pods: - assign[t, p] = model.new_bool_var(f"assign_{t}_{p}") - possible[t, p] = model.new_bool_var(f"possible_{t}_{p}") - - # active[p] indicates if pod p is active. - active = [model.new_bool_var(f"active_{p}") for p in all_pods] - - # Each task is done on exactly one pod. - for t in all_tasks: - model.add_exactly_one([assign[t, p] for p in all_pods]) - - # Total tasks assigned to one pod cannot exceed cycle time. + """solve the given problem.""" + + print("Solving using the Boolean model") + # problem data + num_tasks = problem["number of tasks"].value + if num_tasks is None: + return + all_tasks = range(1, num_tasks + 1) # Tasks are 1 based in the problem. + durations = problem["task times"].index_map + precedences = problem["precedence relations"].set_of_pairs + cycle_time = problem["cycle time"].value + + num_pods = max(p for _, p in hint.items()) + 1 if hint else num_tasks - 1 + all_pods = range(num_pods) + + model = cp_model.CpModel() + + # assign[t, p] indicates if task t is done on pod p. + assign = {} + # possible[t, p] indicates if task t is possible on pod p. + possible = {} + + # Create the variables + for t in all_tasks: for p in all_pods: - model.add(sum(assign[t, p] * durations[t] for t in all_tasks) <= cycle_time) + assign[t, p] = model.new_bool_var(f"assign_{t}_{p}") + possible[t, p] = model.new_bool_var(f"possible_{t}_{p}") - # Maintain the possible variables: - # possible at pod p -> possible at any pod after p - for t in all_tasks: - for p in range(num_pods - 1): - model.add_implication(possible[t, p], possible[t, p + 1]) + # active[p] indicates if pod p is active. + active = [model.new_bool_var(f"active_{p}") for p in all_pods] - # Link possible and active variables. - for t in all_tasks: - for p in all_pods: - model.add_implication(assign[t, p], possible[t, p]) - if p > 1: - model.add_implication(assign[t, p], ~possible[t, p - 1]) + # Each task is done on exactly one pod. + for t in all_tasks: + model.add_exactly_one([assign[t, p] for p in all_pods]) + + # Total tasks assigned to one pod cannot exceed cycle time. + for p in all_pods: + model.add(sum(assign[t, p] * durations[t] for t in all_tasks) <= cycle_time) - # Precedences. - for before, after in precedences: - for p in range(1, num_pods): - model.add_implication(assign[before, p], ~possible[after, p - 1]) + # Maintain the possible variables: + # possible at pod p -> possible at any pod after p + for t in all_tasks: + for p in range(num_pods - 1): + model.add_implication(possible[t, p], possible[t, p + 1]) - # Link active variables with the assign one. + # Link possible and active variables. + for t in all_tasks: for p in all_pods: - all_assign_vars = [assign[t, p] for t in all_tasks] - for a in all_assign_vars: - model.add_implication(a, active[p]) - model.add_bool_or(all_assign_vars + [~active[p]]) + model.add_implication(assign[t, p], possible[t, p]) + if p > 1: + model.add_implication(assign[t, p], ~possible[t, p - 1]) - # Force pods to be contiguous. This is critical to get good lower bounds - # on the objective, even if it makes feasibility harder. + # Precedences. + for before, after in precedences: for p in range(1, num_pods): - model.add_implication(~active[p - 1], ~active[p]) - for t in all_tasks: - model.add_implication(~active[p], possible[t, p - 1]) + model.add_implication(assign[before, p], ~possible[after, p - 1]) + + # Link active variables with the assign one. + for p in all_pods: + all_assign_vars = [assign[t, p] for t in all_tasks] + for a in all_assign_vars: + model.add_implication(a, active[p]) + model.add_bool_or(all_assign_vars + [~active[p]]) + + # Force pods to be contiguous. This is critical to get good lower bounds + # on the objective, even if it makes feasibility harder. + for p in range(1, num_pods): + model.add_implication(~active[p - 1], ~active[p]) + for t in all_tasks: + model.add_implication(~active[p], possible[t, p - 1]) - # Objective. - model.minimize(sum(active)) + # Objective. + model.minimize(sum(active)) - # add search hinting from the greedy solution. - for t in all_tasks: - model.add_hint(assign[t, hint[t]], 1) + # add search hinting from the greedy solution. + for t in all_tasks: + model.add_hint(assign[t, hint[t]], 1) - if _OUTPUT_PROTO.value: - print(f"Writing proto to {_OUTPUT_PROTO.value}") - model.export_to_file(_OUTPUT_PROTO.value) + if _OUTPUT_PROTO.value: + print(f"Writing proto to {_OUTPUT_PROTO.value}") + model.export_to_file(_OUTPUT_PROTO.value) - # solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - solver.parameters.log_search_progress = True - solver.solve(model) + # solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + solver.parameters.log_search_progress = True + solver.solve(model) def solve_problem_with_scheduling_model( problem: Dict[str, SectionInfo], hint: Dict[int, int] ) -> None: - """solve the given problem using a cumulative model.""" - - print("Solving using the scheduling model") - # Problem data - num_tasks = problem["number of tasks"].value - if num_tasks is None: - return - all_tasks = range(1, num_tasks + 1) # Tasks are 1 based in the data. - durations = problem["task times"].index_map - precedences = problem["precedence relations"].set_of_pairs - cycle_time = problem["cycle time"].value - - num_pods = max(p for _, p in hint.items()) + 1 if hint else num_tasks - - model = cp_model.CpModel() - - # pod[t] indicates on which pod the task is performed. - pods = {} - for t in all_tasks: - pods[t] = model.new_int_var(0, num_pods - 1, f"pod_{t}") - - # Create the variables - intervals = [] - demands = [] - for t in all_tasks: - interval = model.new_fixed_size_interval_var(pods[t], 1, "") - intervals.append(interval) - demands.append(durations[t]) - - # add terminating interval as the objective. - obj_var = model.new_int_var(1, num_pods, "obj_var") - obj_size = model.new_int_var(1, num_pods, "obj_duration") - obj_interval = model.new_interval_var( - obj_var, obj_size, num_pods + 1, "obj_interval" - ) - intervals.append(obj_interval) - demands.append(cycle_time) - - # Cumulative constraint. - model.add_cumulative(intervals, demands, cycle_time) - - # Precedences. - for before, after in precedences: - model.add(pods[after] >= pods[before]) - - # Objective. - model.minimize(obj_var) - - # add search hinting from the greedy solution. - for t in all_tasks: - model.add_hint(pods[t], hint[t]) - - if _OUTPUT_PROTO.value: - print(f"Writing proto to{_OUTPUT_PROTO.value}") - model.export_to_file(_OUTPUT_PROTO.value) - - # solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - solver.parameters.log_search_progress = True - solver.solve(model) + """solve the given problem using a cumulative model.""" + + print("Solving using the scheduling model") + # Problem data + num_tasks = problem["number of tasks"].value + if num_tasks is None: + return + all_tasks = range(1, num_tasks + 1) # Tasks are 1 based in the data. + durations = problem["task times"].index_map + precedences = problem["precedence relations"].set_of_pairs + cycle_time = problem["cycle time"].value + + num_pods = max(p for _, p in hint.items()) + 1 if hint else num_tasks + + model = cp_model.CpModel() + + # pod[t] indicates on which pod the task is performed. + pods = {} + for t in all_tasks: + pods[t] = model.new_int_var(0, num_pods - 1, f"pod_{t}") + + # Create the variables + intervals = [] + demands = [] + for t in all_tasks: + interval = model.new_fixed_size_interval_var(pods[t], 1, "") + intervals.append(interval) + demands.append(durations[t]) + + # add terminating interval as the objective. + obj_var = model.new_int_var(1, num_pods, "obj_var") + obj_size = model.new_int_var(1, num_pods, "obj_duration") + obj_interval = model.new_interval_var( + obj_var, obj_size, num_pods + 1, "obj_interval" + ) + intervals.append(obj_interval) + demands.append(cycle_time) + + # Cumulative constraint. + model.add_cumulative(intervals, demands, cycle_time) + + # Precedences. + for before, after in precedences: + model.add(pods[after] >= pods[before]) + + # Objective. + model.minimize(obj_var) + + # add search hinting from the greedy solution. + for t in all_tasks: + model.add_hint(pods[t], hint[t]) + + if _OUTPUT_PROTO.value: + print(f"Writing proto to{_OUTPUT_PROTO.value}") + model.export_to_file(_OUTPUT_PROTO.value) + + # solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + solver.parameters.log_search_progress = True + solver.solve(model) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") - problem = read_problem(_INPUT.value) - print_stats(problem) - greedy_solution = solve_problem_greedily(problem) + problem = read_problem(_INPUT.value) + print_stats(problem) + greedy_solution = solve_problem_greedily(problem) - if _MODEL.value == "boolean": - solve_problem_with_boolean_model(problem, greedy_solution) - elif _MODEL.value == "scheduling": - solve_problem_with_scheduling_model(problem, greedy_solution) + if _MODEL.value == "boolean": + solve_problem_with_boolean_model(problem, greedy_solution) + elif _MODEL.value == "scheduling": + solve_problem_with_scheduling_model(problem, greedy_solution) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/linear_assignment_api.py b/examples/python/linear_assignment_api.py index 34514974e4b..d569e7fab10 100644 --- a/examples/python/linear_assignment_api.py +++ b/examples/python/linear_assignment_api.py @@ -14,9 +14,9 @@ """Test linear sum assignment on a 4x4 matrix. - Example taken from: - http://www.ee.oulu.fi/~mpa/matreng/eem1_2-1.htm with kCost[0][1] - modified so the optimum solution is unique. +Example taken from: +http://www.ee.oulu.fi/~mpa/matreng/eem1_2-1.htm with kCost[0][1] +modified so the optimum solution is unique. """ from typing import Sequence @@ -25,37 +25,42 @@ def run_assignment_on_4x4_matrix(): - """Test linear sum assignment on a 4x4 matrix.""" - num_sources = 4 - num_targets = 4 - cost = [[90, 76, 75, 80], [35, 85, 55, 65], [125, 95, 90, 105], [45, 110, 95, 115]] - expected_cost = cost[0][3] + cost[1][2] + cost[2][1] + cost[3][0] - - assignment = linear_sum_assignment.SimpleLinearSumAssignment() - for source in range(0, num_sources): - for target in range(0, num_targets): - assignment.add_arc_with_cost(source, target, cost[source][target]) - - solve_status = assignment.solve() - if solve_status == assignment.OPTIMAL: - print("Successful solve.") - print("Total cost", assignment.optimal_cost(), "/", expected_cost) - for i in range(0, assignment.num_nodes()): - print( - "Left node %d assigned to right node %d with cost %d." - % (i, assignment.right_mate(i), assignment.assignment_cost(i)) - ) - elif solve_status == assignment.INFEASIBLE: - print("No perfect matching exists.") - elif solve_status == assignment.POSSIBLE_OVERFLOW: - print("Some input costs are too large and may cause an integer overflow.") + """Test linear sum assignment on a 4x4 matrix.""" + num_sources = 4 + num_targets = 4 + cost = [ + [90, 76, 75, 80], + [35, 85, 55, 65], + [125, 95, 90, 105], + [45, 110, 95, 115], + ] + expected_cost = cost[0][3] + cost[1][2] + cost[2][1] + cost[3][0] + + assignment = linear_sum_assignment.SimpleLinearSumAssignment() + for source in range(0, num_sources): + for target in range(0, num_targets): + assignment.add_arc_with_cost(source, target, cost[source][target]) + + solve_status = assignment.solve() + if solve_status == assignment.OPTIMAL: + print("Successful solve.") + print("Total cost", assignment.optimal_cost(), "/", expected_cost) + for i in range(0, assignment.num_nodes()): + print( + "Left node %d assigned to right node %d with cost %d." + % (i, assignment.right_mate(i), assignment.assignment_cost(i)) + ) + elif solve_status == assignment.INFEASIBLE: + print("No perfect matching exists.") + elif solve_status == assignment.POSSIBLE_OVERFLOW: + print("Some input costs are too large and may cause an integer overflow.") def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - run_assignment_on_4x4_matrix() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + run_assignment_on_4x4_matrix() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/linear_programming.py b/examples/python/linear_programming.py index a29847ce596..00e285b7cec 100644 --- a/examples/python/linear_programming.py +++ b/examples/python/linear_programming.py @@ -18,134 +18,136 @@ def Announce(solver, api_type): - print( - "---- Linear programming example with " + solver + " (" + api_type + ") -----" - ) + print( + "---- Linear programming example with " + + solver + + " (" + + api_type + + ") -----" + ) def RunLinearExampleNaturalLanguageAPI(optimization_problem_type): - """Example of simple linear program with natural language API.""" - solver = pywraplp.Solver.CreateSolver(optimization_problem_type) + """Example of simple linear program with natural language API.""" + solver = pywraplp.Solver.CreateSolver(optimization_problem_type) - if not solver: - return + if not solver: + return - Announce(optimization_problem_type, "natural language API") + Announce(optimization_problem_type, "natural language API") - infinity = solver.infinity() - # x1, x2 and x3 are continuous non-negative variables. - x1 = solver.NumVar(0.0, infinity, "x1") - x2 = solver.NumVar(0.0, infinity, "x2") - x3 = solver.NumVar(0.0, infinity, "x3") + infinity = solver.infinity() + # x1, x2 and x3 are continuous non-negative variables. + x1 = solver.NumVar(0.0, infinity, "x1") + x2 = solver.NumVar(0.0, infinity, "x2") + x3 = solver.NumVar(0.0, infinity, "x3") - solver.Maximize(10 * x1 + 6 * x2 + 4 * x3) - c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, "ConstraintName0") - c1 = solver.Add(2 * x1 + 2 * x2 + 6 * x3 <= 300) - sum_of_vars = sum([x1, x2, x3]) - c2 = solver.Add(sum_of_vars <= 100.0, "OtherConstraintName") + solver.Maximize(10 * x1 + 6 * x2 + 4 * x3) + c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, "ConstraintName0") + c1 = solver.Add(2 * x1 + 2 * x2 + 6 * x3 <= 300) + sum_of_vars = sum([x1, x2, x3]) + c2 = solver.Add(sum_of_vars <= 100.0, "OtherConstraintName") - SolveAndPrint( - solver, [x1, x2, x3], [c0, c1, c2], optimization_problem_type != "PDLP" - ) - # Print a linear expression's solution value. - print("Sum of vars: %s = %s" % (sum_of_vars, sum_of_vars.solution_value())) + SolveAndPrint( + solver, [x1, x2, x3], [c0, c1, c2], optimization_problem_type != "PDLP" + ) + # Print a linear expression's solution value. + print("Sum of vars: %s = %s" % (sum_of_vars, sum_of_vars.solution_value())) def RunLinearExampleCppStyleAPI(optimization_problem_type): - """Example of simple linear program with the C++ style API.""" - solver = pywraplp.Solver.CreateSolver(optimization_problem_type) - if not solver: - return - - Announce(optimization_problem_type, "C++ style API") - - infinity = solver.infinity() - # x1, x2 and x3 are continuous non-negative variables. - x1 = solver.NumVar(0.0, infinity, "x1") - x2 = solver.NumVar(0.0, infinity, "x2") - x3 = solver.NumVar(0.0, infinity, "x3") - - # Maximize 10 * x1 + 6 * x2 + 4 * x3. - objective = solver.Objective() - objective.SetCoefficient(x1, 10) - objective.SetCoefficient(x2, 6) - objective.SetCoefficient(x3, 4) - objective.SetMaximization() - - # x1 + x2 + x3 <= 100. - c0 = solver.Constraint(-infinity, 100.0, "c0") - c0.SetCoefficient(x1, 1) - c0.SetCoefficient(x2, 1) - c0.SetCoefficient(x3, 1) - - # 10 * x1 + 4 * x2 + 5 * x3 <= 600. - c1 = solver.Constraint(-infinity, 600.0, "c1") - c1.SetCoefficient(x1, 10) - c1.SetCoefficient(x2, 4) - c1.SetCoefficient(x3, 5) - - # 2 * x1 + 2 * x2 + 6 * x3 <= 300. - c2 = solver.Constraint(-infinity, 300.0, "c2") - c2.SetCoefficient(x1, 2) - c2.SetCoefficient(x2, 2) - c2.SetCoefficient(x3, 6) - - SolveAndPrint( - solver, [x1, x2, x3], [c0, c1, c2], optimization_problem_type != "PDLP" - ) + """Example of simple linear program with the C++ style API.""" + solver = pywraplp.Solver.CreateSolver(optimization_problem_type) + if not solver: + return + + Announce(optimization_problem_type, "C++ style API") + + infinity = solver.infinity() + # x1, x2 and x3 are continuous non-negative variables. + x1 = solver.NumVar(0.0, infinity, "x1") + x2 = solver.NumVar(0.0, infinity, "x2") + x3 = solver.NumVar(0.0, infinity, "x3") + + # Maximize 10 * x1 + 6 * x2 + 4 * x3. + objective = solver.Objective() + objective.SetCoefficient(x1, 10) + objective.SetCoefficient(x2, 6) + objective.SetCoefficient(x3, 4) + objective.SetMaximization() + + # x1 + x2 + x3 <= 100. + c0 = solver.Constraint(-infinity, 100.0, "c0") + c0.SetCoefficient(x1, 1) + c0.SetCoefficient(x2, 1) + c0.SetCoefficient(x3, 1) + + # 10 * x1 + 4 * x2 + 5 * x3 <= 600. + c1 = solver.Constraint(-infinity, 600.0, "c1") + c1.SetCoefficient(x1, 10) + c1.SetCoefficient(x2, 4) + c1.SetCoefficient(x3, 5) + + # 2 * x1 + 2 * x2 + 6 * x3 <= 300. + c2 = solver.Constraint(-infinity, 300.0, "c2") + c2.SetCoefficient(x1, 2) + c2.SetCoefficient(x2, 2) + c2.SetCoefficient(x3, 6) + + SolveAndPrint( + solver, [x1, x2, x3], [c0, c1, c2], optimization_problem_type != "PDLP" + ) def SolveAndPrint(solver, variable_list, constraint_list, is_precise): - """Solve the problem and print the solution.""" - print("Number of variables = %d" % solver.NumVariables()) - print("Number of constraints = %d" % solver.NumConstraints()) + """Solve the problem and print the solution.""" + print("Number of variables = %d" % solver.NumVariables()) + print("Number of constraints = %d" % solver.NumConstraints()) - result_status = solver.Solve() + result_status = solver.Solve() - # The problem has an optimal solution. - assert result_status == pywraplp.Solver.OPTIMAL + # The problem has an optimal solution. + assert result_status == pywraplp.Solver.OPTIMAL - # The solution looks legit (when using solvers others than - # GLOP_LINEAR_PROGRAMMING, verifying the solution is highly recommended!). - if is_precise: - assert solver.VerifySolution(1e-7, True) + # The solution looks legit (when using solvers others than + # GLOP_LINEAR_PROGRAMMING, verifying the solution is highly recommended!). + if is_precise: + assert solver.VerifySolution(1e-7, True) - print("Problem solved in %f milliseconds" % solver.wall_time()) + print("Problem solved in %f milliseconds" % solver.wall_time()) - # The objective value of the solution. - print("Optimal objective value = %f" % solver.Objective().Value()) + # The objective value of the solution. + print("Optimal objective value = %f" % solver.Objective().Value()) - # The value of each variable in the solution. - for variable in variable_list: - print("%s = %f" % (variable.name(), variable.solution_value())) + # The value of each variable in the solution. + for variable in variable_list: + print("%s = %f" % (variable.name(), variable.solution_value())) - print("Advanced usage:") - print("Problem solved in %d iterations" % solver.iterations()) - for variable in variable_list: - print("%s: reduced cost = %f" % (variable.name(), variable.reduced_cost())) - activities = solver.ComputeConstraintActivities() - for i, constraint in enumerate(constraint_list): - print( - ( - "constraint %d: dual value = %f\n activity = %f" - % (i, constraint.dual_value(), activities[constraint.index()]) - ) - ) + print("Advanced usage:") + print("Problem solved in %d iterations" % solver.iterations()) + for variable in variable_list: + print("%s: reduced cost = %f" % (variable.name(), variable.reduced_cost())) + activities = solver.ComputeConstraintActivities() + for i, constraint in enumerate(constraint_list): + print(( + "constraint %d: dual value = %f\n activity = %f" + % (i, constraint.dual_value(), activities[constraint.index()]) + )) def main(): - RunLinearExampleNaturalLanguageAPI("GLOP") - RunLinearExampleNaturalLanguageAPI("GLPK_LP") - RunLinearExampleNaturalLanguageAPI("CLP") - RunLinearExampleNaturalLanguageAPI("PDLP") - RunLinearExampleNaturalLanguageAPI("XPRESS_LP") + RunLinearExampleNaturalLanguageAPI("GLOP") + RunLinearExampleNaturalLanguageAPI("GLPK_LP") + RunLinearExampleNaturalLanguageAPI("CLP") + RunLinearExampleNaturalLanguageAPI("PDLP") + RunLinearExampleNaturalLanguageAPI("XPRESS_LP") - RunLinearExampleCppStyleAPI("GLOP") - RunLinearExampleCppStyleAPI("GLPK_LP") - RunLinearExampleCppStyleAPI("CLP") - RunLinearExampleCppStyleAPI("PDLP") - RunLinearExampleCppStyleAPI("XPRESS_LP") + RunLinearExampleCppStyleAPI("GLOP") + RunLinearExampleCppStyleAPI("GLPK_LP") + RunLinearExampleCppStyleAPI("CLP") + RunLinearExampleCppStyleAPI("PDLP") + RunLinearExampleCppStyleAPI("XPRESS_LP") if __name__ == "__main__": - main() + main() diff --git a/examples/python/magic_sequence_distribute.py b/examples/python/magic_sequence_distribute.py index ebfc8212ee2..ea8174badfd 100755 --- a/examples/python/magic_sequence_distribute.py +++ b/examples/python/magic_sequence_distribute.py @@ -30,28 +30,30 @@ def main(argv): - # Create the solver. - solver = pywrapcp.Solver("magic sequence") + # Create the solver. + solver = pywrapcp.Solver("magic sequence") - # Create an array of IntVars to hold the answers. - size = int(argv[1]) if len(argv) > 1 else 100 - all_values = list(range(0, size)) - all_vars = [solver.IntVar(0, size, "vars_%d" % i) for i in all_values] + # Create an array of IntVars to hold the answers. + size = int(argv[1]) if len(argv) > 1 else 100 + all_values = list(range(0, size)) + all_vars = [solver.IntVar(0, size, "vars_%d" % i) for i in all_values] - # The number of variables equal to j shall be the value of all_vars[j]. - solver.Add(solver.Distribute(all_vars, all_values, all_vars)) + # The number of variables equal to j shall be the value of all_vars[j]. + solver.Add(solver.Distribute(all_vars, all_values, all_vars)) - # The sum of all the values shall be equal to the size. - # (This constraint is redundant, but speeds up the search.) - solver.Add(solver.Sum(all_vars) == size) + # The sum of all the values shall be equal to the size. + # (This constraint is redundant, but speeds up the search.) + solver.Add(solver.Sum(all_vars) == size) - solver.NewSearch( - solver.Phase(all_vars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - ) - solver.NextSolution() - print(all_vars) - solver.EndSearch() + solver.NewSearch( + solver.Phase( + all_vars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + ) + solver.NextSolution() + print(all_vars) + solver.EndSearch() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/maximize_combinations_sat.py b/examples/python/maximize_combinations_sat.py index a23e90d3846..a36a34a75c7 100644 --- a/examples/python/maximize_combinations_sat.py +++ b/examples/python/maximize_combinations_sat.py @@ -21,55 +21,55 @@ def maximize_combinations_sat() -> None: - """Maximize the number of valid combinations of Boolean variables.""" - model = cp_model.CpModel() - cards: list[cp_model.IntVar] = [ - model.new_bool_var("card1"), - model.new_bool_var("card2"), - model.new_bool_var("card3"), - model.new_bool_var("card4"), - ] - - combos: list[list[cp_model.IntVar]] = [ - [cards[0], cards[1]], - [cards[0], cards[2]], - [cards[1], cards[3]], - [cards[0], cards[2], cards[3]], - ] - - deck_size: int = 3 - model.add(sum(cards) == deck_size) - - valid_combos: list[cp_model.IntVar] = [] - for combination in combos: - is_valid = model.new_bool_var("") - - # All true implies is_valid. - model.add_bool_and(is_valid).only_enforce_if(combination) - - # is_valid implies all true. - for literal in combination: - model.add_implication(is_valid, literal) - valid_combos.append(is_valid) - - model.maximize(sum(valid_combos)) - - solver = cp_model.CpSolver() - solver.parameters.log_search_progress = True - status = solver.solve(model) - - if status == cp_model.OPTIMAL: - print( - "chosen cards:", - [card.name for card in cards if solver.boolean_value(card)], - ) + """Maximize the number of valid combinations of Boolean variables.""" + model = cp_model.CpModel() + cards: list[cp_model.IntVar] = [ + model.new_bool_var("card1"), + model.new_bool_var("card2"), + model.new_bool_var("card3"), + model.new_bool_var("card4"), + ] + + combos: list[list[cp_model.IntVar]] = [ + [cards[0], cards[1]], + [cards[0], cards[2]], + [cards[1], cards[3]], + [cards[0], cards[2], cards[3]], + ] + + deck_size: int = 3 + model.add(sum(cards) == deck_size) + + valid_combos: list[cp_model.IntVar] = [] + for combination in combos: + is_valid = model.new_bool_var("") + + # All true implies is_valid. + model.add_bool_and(is_valid).only_enforce_if(combination) + + # is_valid implies all true. + for literal in combination: + model.add_implication(is_valid, literal) + valid_combos.append(is_valid) + + model.maximize(sum(valid_combos)) + + solver = cp_model.CpSolver() + solver.parameters.log_search_progress = True + status = solver.solve(model) + + if status == cp_model.OPTIMAL: + print( + "chosen cards:", + [card.name for card in cards if solver.boolean_value(card)], + ) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - maximize_combinations_sat() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + maximize_combinations_sat() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/maze_escape_sat.py b/examples/python/maze_escape_sat.py index 7a96e453d83..1f8761a3dd4 100644 --- a/examples/python/maze_escape_sat.py +++ b/examples/python/maze_escape_sat.py @@ -51,125 +51,125 @@ def add_neighbor( position_to_rank: Dict[Tuple[int, int, int], cp_model.IntVar], arcs: list[Tuple[int, int, cp_model.LiteralT]], ) -> None: - """Checks if the neighbor is valid, and adds it to the model.""" - if ( - x + dx < 0 - or x + dx >= size - or y + dy < 0 - or y + dy >= size - or z + dz < 0 - or z + dz >= size - ): - return - before_index = index_map[(x, y, z)] - before_rank = position_to_rank[(x, y, z)] - after_index = index_map[(x + dx, y + dy, z + dz)] - after_rank = position_to_rank[(x + dx, y + dy, z + dz)] - move_literal = model.new_bool_var("") - model.add(after_rank == before_rank + 1).only_enforce_if(move_literal) - arcs.append((before_index, after_index, move_literal)) + """Checks if the neighbor is valid, and adds it to the model.""" + if ( + x + dx < 0 + or x + dx >= size + or y + dy < 0 + or y + dy >= size + or z + dz < 0 + or z + dz >= size + ): + return + before_index = index_map[(x, y, z)] + before_rank = position_to_rank[(x, y, z)] + after_index = index_map[(x + dx, y + dy, z + dz)] + after_rank = position_to_rank[(x + dx, y + dy, z + dz)] + move_literal = model.new_bool_var("") + model.add(after_rank == before_rank + 1).only_enforce_if(move_literal) + arcs.append((before_index, after_index, move_literal)) def escape_the_maze(params: str, output_proto: str) -> None: - """Escapes the maze.""" - size = 4 - boxes = [(0, 1, 0), (2, 0, 1), (1, 3, 1), (3, 1, 3)] - start = (3, 3, 0) - end = (1, 0, 0) - - # Builds a map between each position in the grid and a unique integer between - # 0 and size^3 - 1. - index_map = {} - reverse_map = [] - counter = 0 + """Escapes the maze.""" + size = 4 + boxes = [(0, 1, 0), (2, 0, 1), (1, 3, 1), (3, 1, 3)] + start = (3, 3, 0) + end = (1, 0, 0) + + # Builds a map between each position in the grid and a unique integer between + # 0 and size^3 - 1. + index_map = {} + reverse_map = [] + counter = 0 + for x in range(size): + for y in range(size): + for z in range(size): + index_map[(x, y, z)] = counter + reverse_map.append((x, y, z)) + counter += 1 + + # Starts building the model. + model = cp_model.CpModel() + position_to_rank = {} + + for coord in reverse_map: + position_to_rank[coord] = model.new_int_var(0, counter - 1, f"rank_{coord}") + + # Path constraints. + model.add(position_to_rank[start] == 0) + model.add(position_to_rank[end] == counter - 1) + for i in range(len(boxes) - 1): + model.add(position_to_rank[boxes[i]] < position_to_rank[boxes[i + 1]]) + + # Circuit constraint: visit all blocks exactly once, and maintains the rank + # of each block. + arcs: list[Tuple[int, int, cp_model.LiteralT]] = [] + for x in range(size): + for y in range(size): + for z in range(size): + add_neighbor( + size, x, y, z, -1, 0, 0, model, index_map, position_to_rank, arcs + ) + add_neighbor( + size, x, y, z, 1, 0, 0, model, index_map, position_to_rank, arcs + ) + add_neighbor( + size, x, y, z, 0, -1, 0, model, index_map, position_to_rank, arcs + ) + add_neighbor( + size, x, y, z, 0, 1, 0, model, index_map, position_to_rank, arcs + ) + add_neighbor( + size, x, y, z, 0, 0, -1, model, index_map, position_to_rank, arcs + ) + add_neighbor( + size, x, y, z, 0, 0, 1, model, index_map, position_to_rank, arcs + ) + + # Closes the loop as the constraint expects a circuit, not a path. + arcs.append((index_map[end], index_map[start], True)) + + # Adds the circuit (hamiltonian path) constraint. + model.add_circuit(arcs) + + # Exports the model if required. + if output_proto: + model.export_to_file(output_proto) + + # Solve model. + solver = cp_model.CpSolver() + if params: + text_format.Parse(params, solver.parameters) + solver.parameters.log_search_progress = True + result = solver.solve(model) + + # Prints solution. + if result == cp_model.OPTIMAL: + path = [""] * counter for x in range(size): - for y in range(size): - for z in range(size): - index_map[(x, y, z)] = counter - reverse_map.append((x, y, z)) - counter += 1 - - # Starts building the model. - model = cp_model.CpModel() - position_to_rank = {} - - for coord in reverse_map: - position_to_rank[coord] = model.new_int_var(0, counter - 1, f"rank_{coord}") - - # Path constraints. - model.add(position_to_rank[start] == 0) - model.add(position_to_rank[end] == counter - 1) - for i in range(len(boxes) - 1): - model.add(position_to_rank[boxes[i]] < position_to_rank[boxes[i + 1]]) - - # Circuit constraint: visit all blocks exactly once, and maintains the rank - # of each block. - arcs: list[Tuple[int, int, cp_model.LiteralT]] = [] - for x in range(size): - for y in range(size): - for z in range(size): - add_neighbor( - size, x, y, z, -1, 0, 0, model, index_map, position_to_rank, arcs - ) - add_neighbor( - size, x, y, z, 1, 0, 0, model, index_map, position_to_rank, arcs - ) - add_neighbor( - size, x, y, z, 0, -1, 0, model, index_map, position_to_rank, arcs - ) - add_neighbor( - size, x, y, z, 0, 1, 0, model, index_map, position_to_rank, arcs - ) - add_neighbor( - size, x, y, z, 0, 0, -1, model, index_map, position_to_rank, arcs - ) - add_neighbor( - size, x, y, z, 0, 0, 1, model, index_map, position_to_rank, arcs - ) - - # Closes the loop as the constraint expects a circuit, not a path. - arcs.append((index_map[end], index_map[start], True)) - - # Adds the circuit (hamiltonian path) constraint. - model.add_circuit(arcs) - - # Exports the model if required. - if output_proto: - model.export_to_file(output_proto) - - # Solve model. - solver = cp_model.CpSolver() - if params: - text_format.Parse(params, solver.parameters) - solver.parameters.log_search_progress = True - result = solver.solve(model) - - # Prints solution. - if result == cp_model.OPTIMAL: - path = [""] * counter - for x in range(size): - for y in range(size): - for z in range(size): - position = (x, y, z) - rank = solver.value(position_to_rank[position]) - msg = f"({x}, {y}, {z})" - if position == start: - msg += " [start]" - elif position == end: - msg += " [end]" - else: - for b, box in enumerate(boxes): - if position == box: - msg += f" [boxes {b}]" - path[rank] = msg - print(path) + for y in range(size): + for z in range(size): + position = (x, y, z) + rank = solver.value(position_to_rank[position]) + msg = f"({x}, {y}, {z})" + if position == start: + msg += " [start]" + elif position == end: + msg += " [end]" + else: + for b, box in enumerate(boxes): + if position == box: + msg += f" [boxes {b}]" + path[rank] = msg + print(path) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - escape_the_maze(_PARAMS.value, _OUTPUT_PROTO.value) + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + escape_the_maze(_PARAMS.value, _OUTPUT_PROTO.value) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/memory_layout_and_infeasibility_sat.py b/examples/python/memory_layout_and_infeasibility_sat.py index 9956700c225..be51cd3f342 100644 --- a/examples/python/memory_layout_and_infeasibility_sat.py +++ b/examples/python/memory_layout_and_infeasibility_sat.py @@ -46,136 +46,138 @@ def solve_hard_model(output_proto: str, params: str) -> bool: - """Solves the hard assignment model.""" - print("Solving the hard assignment model") - model = cp_model.CpModel() + """Solves the hard assignment model.""" + print("Solving the hard assignment model") + model = cp_model.CpModel() - x_intervals: List[cp_model.IntervalVar] = [] - y_starts: List[cp_model.IntVar] = [] - y_intervals: List[cp_model.IntervalVar] = [] + x_intervals: List[cp_model.IntervalVar] = [] + y_starts: List[cp_model.IntVar] = [] + y_intervals: List[cp_model.IntervalVar] = [] - for start_time, end_time, demand, _ in DEMANDS: - x_interval = model.new_fixed_size_interval_var( - start_time, end_time - start_time + 1, "" - ) - y_start = model.new_int_var(0, CAPACITY - demand, "") - y_interval = model.new_fixed_size_interval_var(y_start, demand, "") + for start_time, end_time, demand, _ in DEMANDS: + x_interval = model.new_fixed_size_interval_var( + start_time, end_time - start_time + 1, "" + ) + y_start = model.new_int_var(0, CAPACITY - demand, "") + y_interval = model.new_fixed_size_interval_var(y_start, demand, "") - x_intervals.append(x_interval) - y_starts.append(y_start) - y_intervals.append(y_interval) + x_intervals.append(x_interval) + y_starts.append(y_start) + y_intervals.append(y_interval) - model.add_no_overlap_2d(x_intervals, y_intervals) + model.add_no_overlap_2d(x_intervals, y_intervals) - if output_proto: - model.export_to_file(output_proto) + if output_proto: + model.export_to_file(output_proto) - solver = cp_model.CpSolver() - if params: - text_format.Parse(params, solver.parameters) - status = solver.solve(model) - print(solver.response_stats()) + solver = cp_model.CpSolver() + if params: + text_format.Parse(params, solver.parameters) + status = solver.solve(model) + print(solver.response_stats()) - if status in (cp_model.FEASIBLE, cp_model.OPTIMAL): - for index, start_var in enumerate(y_starts): - print(f"task {index} buffer starts at {solver.value(start_var)}") + if status in (cp_model.FEASIBLE, cp_model.OPTIMAL): + for index, start_var in enumerate(y_starts): + print(f"task {index} buffer starts at {solver.value(start_var)}") - return status != cp_model.INFEASIBLE + return status != cp_model.INFEASIBLE def solve_soft_model_with_assumptions() -> None: - """Solves the soft model using assumptions.""" - print("Solving the soft model using assumptions") - - model = cp_model.CpModel() - - presences: List[cp_model.IntVar] = [] - x_intervals: List[cp_model.IntervalVar] = [] - y_starts: List[cp_model.IntVar] = [] - y_intervals: List[cp_model.IntervalVar] = [] - - for start, end, demand, unused_alignment in DEMANDS: - presence = model.new_bool_var("") - x_interval = model.new_optional_fixed_size_interval_var( - start, end - start + 1, presence, "" - ) - y_start = model.new_int_var(0, CAPACITY - demand, "") - y_interval = model.new_optional_fixed_size_interval_var( - y_start, demand, presence, "" - ) - - presences.append(presence) - x_intervals.append(x_interval) - y_starts.append(y_start) - y_intervals.append(y_interval) - - model.add_no_overlap_2d(x_intervals, y_intervals) - model.add_assumptions(presences) - - solver = cp_model.CpSolver() - status = solver.solve(model) - print(solver.response_stats()) - if status == cp_model.INFEASIBLE: - # The list actually contains the indices of the variables sufficient to - # explain infeasibility. - infeasible_variable_indices = solver.sufficient_assumptions_for_infeasibility() - infeasible_variable_indices_set = set(infeasible_variable_indices) - - for index, presence in enumerate(presences): - if presence.index in infeasible_variable_indices_set: - print(f"using task {index} is sufficient to explain infeasibility") + """Solves the soft model using assumptions.""" + print("Solving the soft model using assumptions") + + model = cp_model.CpModel() + + presences: List[cp_model.IntVar] = [] + x_intervals: List[cp_model.IntervalVar] = [] + y_starts: List[cp_model.IntVar] = [] + y_intervals: List[cp_model.IntervalVar] = [] + + for start, end, demand, unused_alignment in DEMANDS: + presence = model.new_bool_var("") + x_interval = model.new_optional_fixed_size_interval_var( + start, end - start + 1, presence, "" + ) + y_start = model.new_int_var(0, CAPACITY - demand, "") + y_interval = model.new_optional_fixed_size_interval_var( + y_start, demand, presence, "" + ) + + presences.append(presence) + x_intervals.append(x_interval) + y_starts.append(y_start) + y_intervals.append(y_interval) + + model.add_no_overlap_2d(x_intervals, y_intervals) + model.add_assumptions(presences) + + solver = cp_model.CpSolver() + status = solver.solve(model) + print(solver.response_stats()) + if status == cp_model.INFEASIBLE: + # The list actually contains the indices of the variables sufficient to + # explain infeasibility. + infeasible_variable_indices = ( + solver.sufficient_assumptions_for_infeasibility() + ) + infeasible_variable_indices_set = set(infeasible_variable_indices) + + for index, presence in enumerate(presences): + if presence.index in infeasible_variable_indices_set: + print(f"using task {index} is sufficient to explain infeasibility") def solve_soft_model_with_maximization(params: str) -> None: - """Solves the soft model using maximization.""" - print("Solving the soft model using minimization") - - model = cp_model.CpModel() - - presences: List[cp_model.IntVar] = [] - x_intervals: List[cp_model.IntervalVar] = [] - y_starts: List[cp_model.IntVar] = [] - y_intervals: List[cp_model.IntervalVar] = [] - - for start, end, demand, unused_alignment in DEMANDS: - presence = model.new_bool_var("") - x_interval = model.new_optional_fixed_size_interval_var( - start, end - start + 1, presence, "" - ) - y_start = model.new_int_var(0, CAPACITY - demand, "") - y_interval = model.new_optional_fixed_size_interval_var( - y_start, demand, presence, "" - ) - - presences.append(presence) - x_intervals.append(x_interval) - y_starts.append(y_start) - y_intervals.append(y_interval) - - model.add_no_overlap_2d(x_intervals, y_intervals) - - model.maximize(sum(presences)) - - solver = cp_model.CpSolver() - if params: - text_format.Parse(params, solver.parameters) - status = solver.solve(model) - print(solver.response_stats()) - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - for index, presence in enumerate(presences): - if not solver.boolean_value(presence): - print(f"task {index} does not fit") - else: - print(f"task {index} buffer starts at {solver.value(y_starts[index])}") + """Solves the soft model using maximization.""" + print("Solving the soft model using minimization") + + model = cp_model.CpModel() + + presences: List[cp_model.IntVar] = [] + x_intervals: List[cp_model.IntervalVar] = [] + y_starts: List[cp_model.IntVar] = [] + y_intervals: List[cp_model.IntervalVar] = [] + + for start, end, demand, unused_alignment in DEMANDS: + presence = model.new_bool_var("") + x_interval = model.new_optional_fixed_size_interval_var( + start, end - start + 1, presence, "" + ) + y_start = model.new_int_var(0, CAPACITY - demand, "") + y_interval = model.new_optional_fixed_size_interval_var( + y_start, demand, presence, "" + ) + + presences.append(presence) + x_intervals.append(x_interval) + y_starts.append(y_start) + y_intervals.append(y_interval) + + model.add_no_overlap_2d(x_intervals, y_intervals) + + model.maximize(sum(presences)) + + solver = cp_model.CpSolver() + if params: + text_format.Parse(params, solver.parameters) + status = solver.solve(model) + print(solver.response_stats()) + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + for index, presence in enumerate(presences): + if not solver.boolean_value(presence): + print(f"task {index} does not fit") + else: + print(f"task {index} buffer starts at {solver.value(y_starts[index])}") def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - if not solve_hard_model(_OUTPUT_PROTO.value, _PARAMS.value): - solve_soft_model_with_assumptions() - solve_soft_model_with_maximization(_PARAMS.value) + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + if not solve_hard_model(_OUTPUT_PROTO.value, _PARAMS.value): + solve_soft_model_with_assumptions() + solve_soft_model_with_maximization(_PARAMS.value) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/no_wait_baking_scheduling_sat.py b/examples/python/no_wait_baking_scheduling_sat.py index 9c81d756743..80ef8ac6ff1 100644 --- a/examples/python/no_wait_baking_scheduling_sat.py +++ b/examples/python/no_wait_baking_scheduling_sat.py @@ -51,261 +51,259 @@ class Task: - """A unit baking task. + """A unit baking task. - - Simple baking tasks have a fixed duration. They are performed by workers. - - Waiting/cooling/proofing tasks have a min and a max duration. - They are performed by machine or they use space resources. - """ + - Simple baking tasks have a fixed duration. They are performed by workers. + - Waiting/cooling/proofing tasks have a min and a max duration. + They are performed by machine or they use space resources. + """ - def __init__(self, name, min_duration, max_duration): - self.name = name - self.min_duration = min_duration - self.max_duration = max_duration + def __init__(self, name, min_duration, max_duration): + self.name = name + self.min_duration = min_duration + self.max_duration = max_duration class Skill: - """The skill of a worker or the capability of a machine.""" + """The skill of a worker or the capability of a machine.""" - def __init__(self, name, efficiency): - self.name = name - # Efficiency is currently not used. - self.efficiency = efficiency + def __init__(self, name, efficiency): + self.name = name + # Efficiency is currently not used. + self.efficiency = efficiency class Recipe: - """A recipe is a sequence of cooking tasks.""" + """A recipe is a sequence of cooking tasks.""" - def __init__(self, name): - self.name = name - self.tasks = [] + def __init__(self, name): + self.name = name + self.tasks = [] - def add_task( - self, resource_name: str, min_duration: int, max_duration: int - ) -> "Recipe": - self.tasks.append(Task(resource_name, min_duration, max_duration)) - return self + def add_task( + self, resource_name: str, min_duration: int, max_duration: int + ) -> "Recipe": + self.tasks.append(Task(resource_name, min_duration, max_duration)) + return self class Resource: - """A resource is a worker, a machine, or just some space for cakes to rest. + """A resource is a worker, a machine, or just some space for cakes to rest. - - Workers have a capacity of 1 and can have variable efficiency. - - Machines and spaces have a capacity greater or equal to one, but the - efficiency is fixed to 100. + - Workers have a capacity of 1 and can have variable efficiency. + - Machines and spaces have a capacity greater or equal to one, but the + efficiency is fixed to 100. - For a worker with efficiency k and a task of duration t, the resulting - work will have a duration `ceil(t * k)`. - """ + For a worker with efficiency k and a task of duration t, the resulting + work will have a duration `ceil(t * k)`. + """ - def __init__(self, name, capacity): - self.name = name - self.capacity = capacity - self.skills = [] + def __init__(self, name, capacity): + self.name = name + self.capacity = capacity + self.skills = [] - def add_skill(self, skill_name: str, efficiency: float) -> "Resource": - self.skills.append(Skill(skill_name, efficiency)) - return self + def add_skill(self, skill_name: str, efficiency: float) -> "Resource": + self.skills.append(Skill(skill_name, efficiency)) + return self class Order: - """An order is a recipe that should be delivered at a given due date.""" + """An order is a recipe that should be delivered at a given due date.""" - def __init__(self, unique_id, recipe_name, due_date, quantity): - """Builds an order. + def __init__(self, unique_id, recipe_name, due_date, quantity): + """Builds an order. - Args: - unique_id: A unique identifier for the order. Used to display the result. - recipe_name: The name of the recipe. It must match one of the recipes. - due_date: The due date in minutes since midnight. - quantity: How many cakes to prepare. - """ - self.unique_id = unique_id - self.recipe_name = recipe_name - self.due_date = due_date - self.quantity = quantity + Args: + unique_id: A unique identifier for the order. Used to display the result. + recipe_name: The name of the recipe. It must match one of the recipes. + due_date: The due date in minutes since midnight. + quantity: How many cakes to prepare. + """ + self.unique_id = unique_id + self.recipe_name = recipe_name + self.due_date = due_date + self.quantity = quantity def set_up_data() -> Tuple[List[Recipe], List[Resource], List[Order]]: - """Set up the bakery problem data.""" - - # Recipes. - croissant_recipe = Recipe(CROISSANT) - croissant_recipe.add_task(BAKING, 15, 15) - croissant_recipe.add_task(PROOFING, 60, 90) - croissant_recipe.add_task(COOKING, 20, 20) - croissant_recipe.add_task(DISPLAY, 5, 5 * 60) - - apple_pie_recipe = Recipe(APPLE_PIE) - apple_pie_recipe.add_task(BAKING, 25, 25) - apple_pie_recipe.add_task(PROOFING, 15, 60) - apple_pie_recipe.add_task(COOKING, 30, 30) - apple_pie_recipe.add_task(DECORATING, 5, 5) - apple_pie_recipe.add_task(DISPLAY, 5, 5 * 60) - - brioche_recipe = Recipe(BRIOCHE) - brioche_recipe.add_task(BAKING, 20, 20) - brioche_recipe.add_task(PROOFING, 60, 90) - brioche_recipe.add_task(COOKING, 30, 30) - brioche_recipe.add_task(DISPLAY, 5, 5 * 60) - - chocolate_cake_recipe = Recipe(CHOCOLATE_CAKE) - chocolate_cake_recipe.add_task(BAKING, 15, 15) - chocolate_cake_recipe.add_task(COOKING, 25, 25) - chocolate_cake_recipe.add_task(DECORATING, 15, 15) - chocolate_cake_recipe.add_task(DISPLAY, 5, 5 * 60) - recipes = [ - croissant_recipe, - apple_pie_recipe, - brioche_recipe, - chocolate_cake_recipe, - ] - - # Resources. - baker1 = Resource("baker1", 1).add_skill(BAKING, 1.0) - baker2 = Resource("baker2", 1).add_skill(BAKING, 1.0) - decorator1 = Resource("decorator1", 1).add_skill(DECORATING, 1.0) - waiting_space = Resource("waiting_space", 4).add_skill(PROOFING, 1.0) - oven = Resource("oven", 4).add_skill(COOKING, 1.0) - display_space = Resource("display_space", 12).add_skill(DISPLAY, 1.0) - resources = [baker1, baker2, decorator1, waiting_space, oven, display_space] - - # Orders - croissant_7am = Order("croissant_7am", CROISSANT, 7 * 60, 3) - croissant_8am = Order("croissant_8am", CROISSANT, 8 * 60, 3) - croissant_9am = Order("croissant_9am", CROISSANT, 9 * 60, 2) - croissant_10am = Order("croissant_10am", CROISSANT, 10 * 60, 1) - croissant_11am = Order("croissant_11am", CROISSANT, 11 * 60, 1) - brioche_10am = Order("brioche_10am", BRIOCHE, 10 * 60, 8) - brioche_12pm = Order("brioche_12pm", BRIOCHE, 12 * 60, 8) - apple_pie_1pm = Order("apple_pie_1pm", APPLE_PIE, 13 * 60, 10) - chocolate_4pm = Order("chocolate_4pm", CHOCOLATE_CAKE, 16 * 60, 10) - orders = [ - croissant_7am, - croissant_8am, - croissant_9am, - croissant_10am, - croissant_11am, - brioche_10am, - brioche_12pm, - apple_pie_1pm, - chocolate_4pm, - ] - - return recipes, resources, orders + """Set up the bakery problem data.""" + + # Recipes. + croissant_recipe = Recipe(CROISSANT) + croissant_recipe.add_task(BAKING, 15, 15) + croissant_recipe.add_task(PROOFING, 60, 90) + croissant_recipe.add_task(COOKING, 20, 20) + croissant_recipe.add_task(DISPLAY, 5, 5 * 60) + + apple_pie_recipe = Recipe(APPLE_PIE) + apple_pie_recipe.add_task(BAKING, 25, 25) + apple_pie_recipe.add_task(PROOFING, 15, 60) + apple_pie_recipe.add_task(COOKING, 30, 30) + apple_pie_recipe.add_task(DECORATING, 5, 5) + apple_pie_recipe.add_task(DISPLAY, 5, 5 * 60) + + brioche_recipe = Recipe(BRIOCHE) + brioche_recipe.add_task(BAKING, 20, 20) + brioche_recipe.add_task(PROOFING, 60, 90) + brioche_recipe.add_task(COOKING, 30, 30) + brioche_recipe.add_task(DISPLAY, 5, 5 * 60) + + chocolate_cake_recipe = Recipe(CHOCOLATE_CAKE) + chocolate_cake_recipe.add_task(BAKING, 15, 15) + chocolate_cake_recipe.add_task(COOKING, 25, 25) + chocolate_cake_recipe.add_task(DECORATING, 15, 15) + chocolate_cake_recipe.add_task(DISPLAY, 5, 5 * 60) + recipes = [ + croissant_recipe, + apple_pie_recipe, + brioche_recipe, + chocolate_cake_recipe, + ] + + # Resources. + baker1 = Resource("baker1", 1).add_skill(BAKING, 1.0) + baker2 = Resource("baker2", 1).add_skill(BAKING, 1.0) + decorator1 = Resource("decorator1", 1).add_skill(DECORATING, 1.0) + waiting_space = Resource("waiting_space", 4).add_skill(PROOFING, 1.0) + oven = Resource("oven", 4).add_skill(COOKING, 1.0) + display_space = Resource("display_space", 12).add_skill(DISPLAY, 1.0) + resources = [baker1, baker2, decorator1, waiting_space, oven, display_space] + + # Orders + croissant_7am = Order("croissant_7am", CROISSANT, 7 * 60, 3) + croissant_8am = Order("croissant_8am", CROISSANT, 8 * 60, 3) + croissant_9am = Order("croissant_9am", CROISSANT, 9 * 60, 2) + croissant_10am = Order("croissant_10am", CROISSANT, 10 * 60, 1) + croissant_11am = Order("croissant_11am", CROISSANT, 11 * 60, 1) + brioche_10am = Order("brioche_10am", BRIOCHE, 10 * 60, 8) + brioche_12pm = Order("brioche_12pm", BRIOCHE, 12 * 60, 8) + apple_pie_1pm = Order("apple_pie_1pm", APPLE_PIE, 13 * 60, 10) + chocolate_4pm = Order("chocolate_4pm", CHOCOLATE_CAKE, 16 * 60, 10) + orders = [ + croissant_7am, + croissant_8am, + croissant_9am, + croissant_10am, + croissant_11am, + brioche_10am, + brioche_12pm, + apple_pie_1pm, + chocolate_4pm, + ] + + return recipes, resources, orders def solve_with_cp_sat( recipes: List[Recipe], resources: List[Resource], orders: List[Order] ) -> None: - """Build the optimization model, and solve the problem.""" - - model = cp_model.CpModel() - horizon = 22 * 60 # 10PM. - start_work = 4 * 60 # 4am. - - # Parse recipes. - recipe_by_name = {} - for recipe in recipes: - recipe_by_name[recipe.name] = recipe - - # Parse resources. - resource_by_name = {} - resource_list_by_skill_name = collections.defaultdict(list) - for resource in resources: - resource_by_name[resource.name] = resource - for skill in resource.skills: - resource_list_by_skill_name[skill.name].append(resource) - - # Parse orders and create one optional copy per eligible resource and per - # task. - interval_list_by_resource_name = collections.defaultdict(list) - orders_sequence_of_events = collections.defaultdict(list) - sorted_orders = [] - tardiness_vars = [] - for order in orders: - for batch in range(order.quantity): - order_id = f"{order.unique_id}_{batch}" - sorted_orders.append(order_id) - previous_end = None - due_date = order.due_date - recipe = recipe_by_name[order.recipe_name] - for task in recipe.tasks: - skill_name = task.name - suffix = f"_{order.unique_id}_batch{batch}_{skill_name}" - - if previous_end is None: - start = model.new_int_var(start_work, horizon, f"start{suffix}") - orders_sequence_of_events[order_id].append( - (start, f"start{suffix}") - ) - else: - start = previous_end - - size = model.new_int_var( - task.min_duration, task.max_duration, f"size{suffix}" - ) - if task == recipe.tasks[-1]: - # The order must end after the due_date. Ideally, exactly at the - # due_date. - tardiness = model.new_int_var(0, horizon - due_date, f"end{suffix}") - end = tardiness + due_date - - # Store the end_var for the objective. - tardiness_vars.append(tardiness) - else: - end = model.new_int_var(start_work, horizon, f"end{suffix}") - orders_sequence_of_events[order_id].append((end, f"end{suffix}")) - previous_end = end - - # Per resource copy. - presence_literals = [] - for resource in resource_list_by_skill_name[skill_name]: - presence = model.new_bool_var(f"presence{suffix}_{resource.name}") - copy = model.new_optional_interval_var( - start, size, end, presence, f"interval{suffix}_{resource.name}" - ) - interval_list_by_resource_name[resource.name].append(copy) - presence_literals.append(presence) - - # Only one copy will be performed. - model.add_exactly_one(presence_literals) - - # Create resource constraints. - for resource in resources: - intervals = interval_list_by_resource_name[resource.name] - if resource.capacity == 1: - model.add_no_overlap(intervals) + """Build the optimization model, and solve the problem.""" + + model = cp_model.CpModel() + horizon = 22 * 60 # 10PM. + start_work = 4 * 60 # 4am. + + # Parse recipes. + recipe_by_name = {} + for recipe in recipes: + recipe_by_name[recipe.name] = recipe + + # Parse resources. + resource_by_name = {} + resource_list_by_skill_name = collections.defaultdict(list) + for resource in resources: + resource_by_name[resource.name] = resource + for skill in resource.skills: + resource_list_by_skill_name[skill.name].append(resource) + + # Parse orders and create one optional copy per eligible resource and per + # task. + interval_list_by_resource_name = collections.defaultdict(list) + orders_sequence_of_events = collections.defaultdict(list) + sorted_orders = [] + tardiness_vars = [] + for order in orders: + for batch in range(order.quantity): + order_id = f"{order.unique_id}_{batch}" + sorted_orders.append(order_id) + previous_end = None + due_date = order.due_date + recipe = recipe_by_name[order.recipe_name] + for task in recipe.tasks: + skill_name = task.name + suffix = f"_{order.unique_id}_batch{batch}_{skill_name}" + + if previous_end is None: + start = model.new_int_var(start_work, horizon, f"start{suffix}") + orders_sequence_of_events[order_id].append((start, f"start{suffix}")) else: - model.add_cumulative(intervals, [1] * len(intervals), resource.capacity) - - # The objective is to minimize the sum of the tardiness values of each jobs. - # The tardiness is difference between the end time of an order and its - # due date. - model.minimize(sum(tardiness_vars)) - - # Solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - solver.parameters.log_search_progress = True - status = solver.solve(model) - - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - for order_id in sorted_orders: - print(f"{order_id}:") - for time_expr, event_id in orders_sequence_of_events[order_id]: - time = solver.value(time_expr) - print(f" {event_id} at {time // 60}:{time % 60:02}") + start = previous_end + + size = model.new_int_var( + task.min_duration, task.max_duration, f"size{suffix}" + ) + if task == recipe.tasks[-1]: + # The order must end after the due_date. Ideally, exactly at the + # due_date. + tardiness = model.new_int_var(0, horizon - due_date, f"end{suffix}") + end = tardiness + due_date + + # Store the end_var for the objective. + tardiness_vars.append(tardiness) + else: + end = model.new_int_var(start_work, horizon, f"end{suffix}") + orders_sequence_of_events[order_id].append((end, f"end{suffix}")) + previous_end = end + + # Per resource copy. + presence_literals = [] + for resource in resource_list_by_skill_name[skill_name]: + presence = model.new_bool_var(f"presence{suffix}_{resource.name}") + copy = model.new_optional_interval_var( + start, size, end, presence, f"interval{suffix}_{resource.name}" + ) + interval_list_by_resource_name[resource.name].append(copy) + presence_literals.append(presence) + + # Only one copy will be performed. + model.add_exactly_one(presence_literals) + + # Create resource constraints. + for resource in resources: + intervals = interval_list_by_resource_name[resource.name] + if resource.capacity == 1: + model.add_no_overlap(intervals) + else: + model.add_cumulative(intervals, [1] * len(intervals), resource.capacity) + + # The objective is to minimize the sum of the tardiness values of each jobs. + # The tardiness is difference between the end time of an order and its + # due date. + model.minimize(sum(tardiness_vars)) + + # Solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + solver.parameters.log_search_progress = True + status = solver.solve(model) + + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + for order_id in sorted_orders: + print(f"{order_id}:") + for time_expr, event_id in orders_sequence_of_events[order_id]: + time = solver.value(time_expr) + print(f" {event_id} at {time // 60}:{time % 60:02}") def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") - recipes, resources, orders = set_up_data() - solve_with_cp_sat(recipes, resources, orders) + recipes, resources, orders = set_up_data() + solve_with_cp_sat(recipes, resources, orders) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/nqueens_sat.py b/examples/python/nqueens_sat.py index e2f29542a50..6669cdc358f 100644 --- a/examples/python/nqueens_sat.py +++ b/examples/python/nqueens_sat.py @@ -24,84 +24,84 @@ class NQueenSolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" - - def __init__(self, queens: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self._queens = queens - self._solution_count = 0 - self._start_time = time.time() - - @property - def solution_count(self) -> int: - return self._solution_count - - def on_solution_callback(self) -> None: - current_time = time.time() - print( - f"Solution{self._solution_count}, time =" - f" {current_time - self._start_time} s" - ) - self._solution_count += 1 - - all_queens = range(len(self._queens)) - for i in all_queens: - for j in all_queens: - if self.value(self._queens[j]) == i: - # There is a queen in column j, row i. - print("Q", end=" ") - else: - print("_", end=" ") - print() - print() + """Print intermediate solutions.""" + + def __init__(self, queens: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self._queens = queens + self._solution_count = 0 + self._start_time = time.time() + + @property + def solution_count(self) -> int: + return self._solution_count + + def on_solution_callback(self) -> None: + current_time = time.time() + print( + f"Solution{self._solution_count}, time =" + f" {current_time - self._start_time} s" + ) + self._solution_count += 1 + + all_queens = range(len(self._queens)) + for i in all_queens: + for j in all_queens: + if self.value(self._queens[j]) == i: + # There is a queen in column j, row i. + print("Q", end=" ") + else: + print("_", end=" ") + print() + print() def main(_): - board_size = _SIZE.value - - ### Creates the solver. - model = cp_model.CpModel() - - ### Creates the variables. - # The array index is the column, and the value is the row. - queens = [ - model.new_int_var(0, board_size - 1, "x%i" % i) for i in range(board_size) - ] - - ### Creates the constraints. - - # All columns must be different because the indices of queens are all - # different, so we just add the all different constraint on the rows. - model.add_all_different(queens) - - # No two queens can be on the same diagonal. - diag1 = [] - diag2 = [] - for i in range(board_size): - q1 = model.new_int_var(0, 2 * board_size, "diag1_%i" % i) - q2 = model.new_int_var(-board_size, board_size, "diag2_%i" % i) - diag1.append(q1) - diag2.append(q2) - model.add(q1 == queens[i] + i) - model.add(q2 == queens[i] - i) - model.add_all_different(diag1) - model.add_all_different(diag2) - - ### Solve model. - solver = cp_model.CpSolver() - solution_printer = NQueenSolutionPrinter(queens) - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - # solve. - solver.solve(model, solution_printer) - - print() - print("Statistics") - print(" - conflicts : %i" % solver.num_conflicts) - print(" - branches : %i" % solver.num_branches) - print(" - wall time : %f s" % solver.wall_time) - print(" - solutions found : %i" % solution_printer.solution_count) + board_size = _SIZE.value + + ### Creates the solver. + model = cp_model.CpModel() + + ### Creates the variables. + # The array index is the column, and the value is the row. + queens = [ + model.new_int_var(0, board_size - 1, "x%i" % i) for i in range(board_size) + ] + + ### Creates the constraints. + + # All columns must be different because the indices of queens are all + # different, so we just add the all different constraint on the rows. + model.add_all_different(queens) + + # No two queens can be on the same diagonal. + diag1 = [] + diag2 = [] + for i in range(board_size): + q1 = model.new_int_var(0, 2 * board_size, "diag1_%i" % i) + q2 = model.new_int_var(-board_size, board_size, "diag2_%i" % i) + diag1.append(q1) + diag2.append(q2) + model.add(q1 == queens[i] + i) + model.add(q2 == queens[i] - i) + model.add_all_different(diag1) + model.add_all_different(diag2) + + ### Solve model. + solver = cp_model.CpSolver() + solution_printer = NQueenSolutionPrinter(queens) + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + # solve. + solver.solve(model, solution_printer) + + print() + print("Statistics") + print(" - conflicts : %i" % solver.num_conflicts) + print(" - branches : %i" % solver.num_branches) + print(" - wall time : %f s" % solver.wall_time) + print(" - solutions found : %i" % solution_printer.solution_count) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/pell_equation_sat.py b/examples/python/pell_equation_sat.py index 3583c5557d3..38697fcc647 100644 --- a/examples/python/pell_equation_sat.py +++ b/examples/python/pell_equation_sat.py @@ -26,41 +26,41 @@ def solve_pell(coeff: int, max_value: int) -> None: - """Solves Pell's equation x^2 - coeff * y^2 = 1.""" - model = cp_model.CpModel() + """Solves Pell's equation x^2 - coeff * y^2 = 1.""" + model = cp_model.CpModel() - x = model.new_int_var(1, max_value, "x") - y = model.new_int_var(1, max_value, "y") + x = model.new_int_var(1, max_value, "x") + y = model.new_int_var(1, max_value, "y") - # Pell's equation: - x_square = model.new_int_var(1, max_value * max_value, "x_square") - y_square = model.new_int_var(1, max_value * max_value, "y_square") - model.add_multiplication_equality(x_square, x, x) - model.add_multiplication_equality(y_square, y, y) - model.add(x_square - coeff * y_square == 1) + # Pell's equation: + x_square = model.new_int_var(1, max_value * max_value, "x_square") + y_square = model.new_int_var(1, max_value * max_value, "y_square") + model.add_multiplication_equality(x_square, x, x) + model.add_multiplication_equality(y_square, y, y) + model.add(x_square - coeff * y_square == 1) - model.add_decision_strategy( - [x, y], cp_model.CHOOSE_MIN_DOMAIN_SIZE, cp_model.SELECT_MIN_VALUE - ) + model.add_decision_strategy( + [x, y], cp_model.CHOOSE_MIN_DOMAIN_SIZE, cp_model.SELECT_MIN_VALUE + ) - solver = cp_model.CpSolver() - solver.parameters.num_workers = 12 - solver.parameters.log_search_progress = True - solver.parameters.cp_model_presolve = True - solver.parameters.cp_model_probing_level = 0 + solver = cp_model.CpSolver() + solver.parameters.num_workers = 12 + solver.parameters.log_search_progress = True + solver.parameters.cp_model_presolve = True + solver.parameters.cp_model_probing_level = 0 - result = solver.solve(model) - if result == cp_model.OPTIMAL: - print(f"x={solver.value(x)} y={solver.value(y)} coeff={coeff}") - if solver.value(x) ** 2 - coeff * (solver.value(y) ** 2) != 1: - raise ValueError("Pell equation not satisfied.") + result = solver.solve(model) + if result == cp_model.OPTIMAL: + print(f"x={solver.value(x)} y={solver.value(y)} coeff={coeff}") + if solver.value(x) ** 2 - coeff * (solver.value(y) ** 2) != 1: + raise ValueError("Pell equation not satisfied.") def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - solve_pell(_COEFF.value, _MAX_VALUE.value) + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + solve_pell(_COEFF.value, _MAX_VALUE.value) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/pentominoes_sat.py b/examples/python/pentominoes_sat.py index 01479ec6ac0..36c0af65ef3 100644 --- a/examples/python/pentominoes_sat.py +++ b/examples/python/pentominoes_sat.py @@ -49,158 +49,158 @@ def is_one(mask: List[List[int]], x: int, y: int, orientation: int) -> bool: - """Returns true if the oriented piece is 1 at position [i][j]. - - The 3 bits in orientation respectively mean: transposition, symmetry by - x axis, symmetry by y axis. - - Args: - mask: The shape of the piece. - x: position. - y: position. - orientation: between 0 and 7. - """ - if orientation & 1: - tmp: int = x - x = y - y = tmp - if orientation & 2: - x = len(mask[0]) - 1 - x - if orientation & 4: - y = len(mask) - 1 - y - return mask[y][x] == 1 + """Returns true if the oriented piece is 1 at position [i][j]. + + The 3 bits in orientation respectively mean: transposition, symmetry by + x axis, symmetry by y axis. + + Args: + mask: The shape of the piece. + x: position. + y: position. + orientation: between 0 and 7. + """ + if orientation & 1: + tmp: int = x + x = y + y = tmp + if orientation & 2: + x = len(mask[0]) - 1 - x + if orientation & 4: + y = len(mask) - 1 - y + return mask[y][x] == 1 def get_height(mask: List[List[int]], orientation: int) -> int: - if orientation & 1: - return len(mask[0]) - return len(mask) + if orientation & 1: + return len(mask[0]) + return len(mask) def get_width(mask: List[List[int]], orientation: int) -> int: - if orientation & 1: - return len(mask) - return len(mask[0]) + if orientation & 1: + return len(mask) + return len(mask[0]) def orientation_is_redundant(mask: List[List[int]], orientation: int) -> bool: - """Checks if the current rotated figure is the same as a previous rotation.""" - size_i: int = get_width(mask, orientation) - size_j: int = get_height(mask, orientation) - for o in range(orientation): - if size_i != get_width(mask, o): - continue - if size_j != get_height(mask, o): - continue - - is_the_same: bool = True - for k in range(size_i): - if not is_the_same: - break - for l in range(size_j): - if not is_the_same: - break - if is_one(mask, k, l, orientation) != is_one(mask, k, l, o): - is_the_same = False - if is_the_same: - return True - return False + """Checks if the current rotated figure is the same as a previous rotation.""" + size_i: int = get_width(mask, orientation) + size_j: int = get_height(mask, orientation) + for o in range(orientation): + if size_i != get_width(mask, o): + continue + if size_j != get_height(mask, o): + continue + + is_the_same: bool = True + for k in range(size_i): + if not is_the_same: + break + for l in range(size_j): + if not is_the_same: + break + if is_one(mask, k, l, orientation) != is_one(mask, k, l, o): + is_the_same = False + if is_the_same: + return True + return False def generate_and_solve_problem(pieces: Dict[str, List[List[int]]]) -> None: - """Solves the pentominoes problem.""" - box_height = _HEIGHT.value - box_width = 5 * len(pieces) // box_height - print(f"Box has dimension {box_height} * {box_width}") - - model = cp_model.CpModel() - position_to_variables: List[List[List[cp_model.IntVar]]] = [ - [[] for _ in range(box_width)] for _ in range(box_height) - ] - - for name, mask in pieces.items(): - all_position_variables = [] - for orientation in range(8): - if orientation_is_redundant(mask, orientation): - continue - piece_width = get_width(mask, orientation) - piece_height = get_height(mask, orientation) - for i in range(box_width - piece_width + 1): - for j in range(box_height - piece_height + 1): - v = model.new_bool_var(name) - all_position_variables.append(v) - for k in range(piece_width): - for l in range(piece_height): - if is_one(mask, k, l, orientation): - position_to_variables[j + l][i + k].append(v) - - # Only one combination is selected. - model.add_exactly_one(all_position_variables) - - for one_column in position_to_variables: - for all_pieces_in_one_position in one_column: - model.add_exactly_one(all_pieces_in_one_position) - - # Solve the model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - status = solver.solve(model) + """Solves the pentominoes problem.""" + box_height = _HEIGHT.value + box_width = 5 * len(pieces) // box_height + print(f"Box has dimension {box_height} * {box_width}") + + model = cp_model.CpModel() + position_to_variables: List[List[List[cp_model.IntVar]]] = [ + [[] for _ in range(box_width)] for _ in range(box_height) + ] + + for name, mask in pieces.items(): + all_position_variables = [] + for orientation in range(8): + if orientation_is_redundant(mask, orientation): + continue + piece_width = get_width(mask, orientation) + piece_height = get_height(mask, orientation) + for i in range(box_width - piece_width + 1): + for j in range(box_height - piece_height + 1): + v = model.new_bool_var(name) + all_position_variables.append(v) + for k in range(piece_width): + for l in range(piece_height): + if is_one(mask, k, l, orientation): + position_to_variables[j + l][i + k].append(v) + + # Only one combination is selected. + model.add_exactly_one(all_position_variables) + + for one_column in position_to_variables: + for all_pieces_in_one_position in one_column: + model.add_exactly_one(all_pieces_in_one_position) + + # Solve the model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + status = solver.solve(model) + + print( + f"Problem {_PIECES.value} box {box_height}*{box_width} solved in" + f" {solver.wall_time}s with status {solver.status_name(status)}" + ) + + # Print the solution. + if status == cp_model.OPTIMAL: + for y in range(box_height): + line = "" + for x in range(box_width): + for v in position_to_variables[y][x]: + if solver.BooleanValue(v): + line += v.name + break + print(line) + +def main(argv: Sequence[str]) -> None: + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + + # Pieces are stored in a matrix. mask[height][width] + pieces: Dict[str, List[List[int]]] = { + "F": [[0, 1, 1], [1, 1, 0], [0, 1, 0]], + "I": [[1, 1, 1, 1, 1]], + "L": [[1, 1, 1, 1], [1, 0, 0, 0]], + "N": [[1, 1, 1, 0], [0, 0, 1, 1]], + "P": [[1, 1, 1], [1, 1, 0]], + "T": [[1, 1, 1], [0, 1, 0], [0, 1, 0]], + "U": [[1, 0, 1], [1, 1, 1]], + "V": [[1, 0, 0], [1, 0, 0], [1, 1, 1]], + "W": [[1, 0, 0], [1, 1, 0], [0, 1, 1]], + "X": [[0, 1, 0], [1, 1, 1], [0, 1, 0]], + "Y": [[1, 1, 1, 1], [0, 1, 0, 0]], + "Z": [[1, 1, 0], [0, 1, 0], [0, 1, 1]], + } + selected_pieces: Dict[str, List[List[int]]] = {} + for p in _PIECES.value: + if p not in pieces: + print(f"Piece {p} not found in the list of pieces") + return + selected_pieces[p] = pieces[p] + if (len(selected_pieces) * 5) % _HEIGHT.value != 0: print( - f"Problem {_PIECES.value} box {box_height}*{box_width} solved in" - f" {solver.wall_time}s with status {solver.status_name(status)}" + f"The height {_HEIGHT.value} does not divide the total area" + f" {5 * len(selected_pieces)}" ) + return + if _HEIGHT.value < 3 or 5 * len(selected_pieces) // _HEIGHT.value < 3: + print(f"The height {_HEIGHT.value} is not compatible with the pieces.") + return - # Print the solution. - if status == cp_model.OPTIMAL: - for y in range(box_height): - line = "" - for x in range(box_width): - for v in position_to_variables[y][x]: - if solver.BooleanValue(v): - line += v.name - break - print(line) - - -def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - - # Pieces are stored in a matrix. mask[height][width] - pieces: Dict[str, List[List[int]]] = { - "F": [[0, 1, 1], [1, 1, 0], [0, 1, 0]], - "I": [[1, 1, 1, 1, 1]], - "L": [[1, 1, 1, 1], [1, 0, 0, 0]], - "N": [[1, 1, 1, 0], [0, 0, 1, 1]], - "P": [[1, 1, 1], [1, 1, 0]], - "T": [[1, 1, 1], [0, 1, 0], [0, 1, 0]], - "U": [[1, 0, 1], [1, 1, 1]], - "V": [[1, 0, 0], [1, 0, 0], [1, 1, 1]], - "W": [[1, 0, 0], [1, 1, 0], [0, 1, 1]], - "X": [[0, 1, 0], [1, 1, 1], [0, 1, 0]], - "Y": [[1, 1, 1, 1], [0, 1, 0, 0]], - "Z": [[1, 1, 0], [0, 1, 0], [0, 1, 1]], - } - selected_pieces: Dict[str, List[List[int]]] = {} - for p in _PIECES.value: - if p not in pieces: - print(f"Piece {p} not found in the list of pieces") - return - selected_pieces[p] = pieces[p] - if (len(selected_pieces) * 5) % _HEIGHT.value != 0: - print( - f"The height {_HEIGHT.value} does not divide the total area" - f" {5 * len(selected_pieces)}" - ) - return - if _HEIGHT.value < 3 or 5 * len(selected_pieces) // _HEIGHT.value < 3: - print(f"The height {_HEIGHT.value} is not compatible with the pieces.") - return - - generate_and_solve_problem(selected_pieces) + generate_and_solve_problem(selected_pieces) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/prize_collecting_tsp.py b/examples/python/prize_collecting_tsp.py index a288498465d..7099fd3caa6 100755 --- a/examples/python/prize_collecting_tsp.py +++ b/examples/python/prize_collecting_tsp.py @@ -13,51 +13,1691 @@ # limitations under the License. """Simple prize collecting TSP problem with a max distance.""" -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting DISTANCE_MATRIX = [ - [0, 10938, 4542, 2835, 29441, 2171, 1611, 9208, 9528, 11111, 16120, 22606, 22127, 20627, 21246, 23387, 16697, 33609, 26184, 24772, 22644, 20655, 30492, 23296, 32979, 18141, 19248, 17129, 17192, 15645, 12658, 11210, 12094, 13175, 18162, 4968, 12308, 10084, 13026, 15056], - [10938, 0, 6422, 9742, 18988, 12974, 11216, 19715, 19004, 18271, 25070, 31971, 31632, 30571, 31578, 33841, 27315, 43964, 36944, 35689, 33569, 31481, 41360, 33760, 43631, 28730, 29976, 27803, 28076, 26408, 23504, 22025, 22000, 13197, 14936, 15146, 23246, 20956, 23963, 25994], - [4542, 6422, 0, 3644, 25173, 6552, 5092, 13584, 13372, 13766, 19805, 26537, 26117, 24804, 25590, 27784, 21148, 37981, 30693, 29315, 27148, 25071, 34943, 27472, 37281, 22389, 23592, 21433, 21655, 20011, 17087, 15612, 15872, 11653, 15666, 8842, 16843, 14618, 17563, 19589], - [2835, 9742, 3644, 0, 28681, 3851, 4341, 11660, 12294, 13912, 18893, 25283, 24777, 23173, 23636, 25696, 18950, 35927, 28233, 26543, 24127, 21864, 31765, 24018, 33904, 19005, 20295, 18105, 18551, 16763, 13958, 12459, 12296, 10370, 15331, 5430, 14044, 12135, 14771, 16743], - [29441, 18988, 25173, 28681, 0, 31590, 29265, 37173, 35501, 32929, 40239, 47006, 46892, 46542, 48112, 50506, 44539, 60103, 54208, 53557, 51878, 50074, 59849, 52645, 62415, 47544, 48689, 46560, 46567, 45086, 42083, 40648, 40971, 29929, 28493, 34015, 41473, 38935, 42160, 44198], - [2171, 12974, 6552, 3851, 31590, 0, 3046, 7856, 8864, 11330, 15411, 21597, 21065, 19382, 19791, 21845, 15099, 32076, 24425, 22848, 20600, 18537, 28396, 21125, 30825, 15975, 17101, 14971, 15104, 13503, 10544, 9080, 9983, 13435, 18755, 2947, 10344, 8306, 11069, 13078], - [1611, 11216, 5092, 4341, 29265, 3046, 0, 8526, 8368, 9573, 14904, 21529, 21085, 19719, 20504, 22713, 16118, 32898, 25728, 24541, 22631, 20839, 30584, 23755, 33278, 18557, 19545, 17490, 17309, 15936, 12881, 11498, 12944, 14711, 19589, 5993, 12227, 9793, 12925, 14967], - [9208, 19715, 13584, 11660, 37173, 7856, 8526, 0, 3248, 7855, 8245, 13843, 13272, 11526, 12038, 14201, 7599, 24411, 17259, 16387, 15050, 13999, 23134, 17899, 26460, 12894, 13251, 11680, 10455, 9997, 7194, 6574, 10678, 20959, 26458, 8180, 5255, 2615, 5730, 7552], - [9528, 19004, 13372, 12294, 35501, 8864, 8368, 3248, 0, 4626, 6598, 13168, 12746, 11567, 12731, 15083, 9120, 25037, 18718, 18433, 17590, 16888, 25630, 20976, 29208, 16055, 16300, 14838, 13422, 13165, 10430, 9813, 13777, 22300, 27564, 10126, 8388, 5850, 8778, 10422], - [11111, 18271, 13766, 13912, 32929, 11330, 9573, 7855, 4626, 0, 7318, 14185, 14005, 13655, 15438, 17849, 12839, 27179, 21947, 22230, 21814, 21366, 29754, 25555, 33535, 20674, 20872, 19457, 17961, 17787, 15048, 14372, 18115, 24280, 29101, 13400, 13008, 10467, 13375, 14935], - [16120, 25070, 19805, 18893, 40239, 15411, 14904, 8245, 6598, 7318, 0, 6939, 6702, 6498, 8610, 10961, 7744, 19889, 15350, 16403, 16975, 17517, 24357, 22176, 28627, 18093, 17672, 16955, 14735, 15510, 13694, 13768, 18317, 28831, 34148, 16326, 11276, 9918, 11235, 11891], - [22606, 31971, 26537, 25283, 47006, 21597, 21529, 13843, 13168, 14185, 6939, 0, 793, 3401, 5562, 6839, 8923, 13433, 11264, 13775, 15853, 17629, 21684, 22315, 26411, 19539, 18517, 18636, 16024, 17632, 16948, 17587, 22131, 34799, 40296, 21953, 14739, 14568, 14366, 14002], - [22127, 31632, 26117, 24777, 46892, 21065, 21085, 13272, 12746, 14005, 6702, 793, 0, 2608, 4809, 6215, 8151, 13376, 10702, 13094, 15099, 16845, 21039, 21535, 25744, 18746, 17725, 17845, 15232, 16848, 16197, 16859, 21391, 34211, 39731, 21345, 14006, 13907, 13621, 13225], - [20627, 30571, 24804, 23173, 46542, 19382, 19719, 11526, 11567, 13655, 6498, 3401, 2608, 0, 2556, 4611, 5630, 13586, 9157, 11005, 12681, 14285, 19044, 18996, 23644, 16138, 15126, 15240, 12625, 14264, 13736, 14482, 18958, 32292, 37879, 19391, 11621, 11803, 11188, 10671], - [21246, 31578, 25590, 23636, 48112, 19791, 20504, 12038, 12731, 15438, 8610, 5562, 4809, 2556, 0, 2411, 4917, 12395, 6757, 8451, 10292, 12158, 16488, 16799, 21097, 14374, 13194, 13590, 10943, 12824, 12815, 13779, 18042, 32259, 37918, 19416, 10975, 11750, 10424, 9475], - [23387, 33841, 27784, 25696, 50506, 21845, 22713, 14201, 15083, 17849, 10961, 6839, 6215, 4611, 2411, 0, 6760, 10232, 4567, 7010, 9607, 12003, 14846, 16408, 19592, 14727, 13336, 14109, 11507, 13611, 14104, 15222, 19237, 34013, 39703, 21271, 12528, 13657, 11907, 10633], - [16697, 27315, 21148, 18950, 44539, 15099, 16118, 7599, 9120, 12839, 7744, 8923, 8151, 5630, 4917, 6760, 0, 16982, 9699, 9400, 9302, 9823, 16998, 14534, 21042, 10911, 10190, 9900, 7397, 8758, 8119, 8948, 13353, 27354, 33023, 14542, 6106, 6901, 5609, 5084], - [33609, 43964, 37981, 35927, 60103, 32076, 32898, 24411, 25037, 27179, 19889, 13433, 13376, 13586, 12395, 10232, 16982, 0, 8843, 12398, 16193, 19383, 16423, 22583, 20997, 22888, 21194, 22640, 20334, 22636, 23801, 25065, 28675, 44048, 49756, 31426, 22528, 23862, 21861, 20315], - [26184, 36944, 30693, 28233, 54208, 24425, 25728, 17259, 18718, 21947, 15350, 11264, 10702, 9157, 6757, 4567, 9699, 8843, 0, 3842, 7518, 10616, 10666, 14237, 15515, 14053, 12378, 13798, 11537, 13852, 15276, 16632, 19957, 35660, 41373, 23361, 14333, 16125, 13624, 11866], - [24772, 35689, 29315, 26543, 53557, 22848, 24541, 16387, 18433, 22230, 16403, 13775, 13094, 11005, 8451, 7010, 9400, 12398, 3842, 0, 3795, 7014, 8053, 10398, 12657, 10633, 8889, 10569, 8646, 10938, 12906, 14366, 17106, 33171, 38858, 21390, 12507, 14748, 11781, 9802], - [22644, 33569, 27148, 24127, 51878, 20600, 22631, 15050, 17590, 21814, 16975, 15853, 15099, 12681, 10292, 9607, 9302, 16193, 7518, 3795, 0, 3250, 8084, 6873, 11763, 6949, 5177, 7050, 5619, 7730, 10187, 11689, 13792, 30012, 35654, 18799, 10406, 12981, 9718, 7682], - [20655, 31481, 25071, 21864, 50074, 18537, 20839, 13999, 16888, 21366, 17517, 17629, 16845, 14285, 12158, 12003, 9823, 19383, 10616, 7014, 3250, 0, 9901, 4746, 12531, 3737, 1961, 4036, 3588, 5109, 7996, 9459, 10846, 27094, 32690, 16451, 8887, 11624, 8304, 6471], - [30492, 41360, 34943, 31765, 59849, 28396, 30584, 23134, 25630, 29754, 24357, 21684, 21039, 19044, 16488, 14846, 16998, 16423, 10666, 8053, 8084, 9901, 0, 9363, 4870, 13117, 11575, 13793, 13300, 15009, 17856, 19337, 20454, 36551, 42017, 26352, 18403, 21033, 17737, 15720], - [23296, 33760, 27472, 24018, 52645, 21125, 23755, 17899, 20976, 25555, 22176, 22315, 21535, 18996, 16799, 16408, 14534, 22583, 14237, 10398, 6873, 4746, 9363, 0, 10020, 5211, 4685, 6348, 7636, 8010, 11074, 12315, 11926, 27537, 32880, 18634, 12644, 15358, 12200, 10674], - [32979, 43631, 37281, 33904, 62415, 30825, 33278, 26460, 29208, 33535, 28627, 26411, 25744, 23644, 21097, 19592, 21042, 20997, 15515, 12657, 11763, 12531, 4870, 10020, 0, 14901, 13738, 15855, 16118, 17348, 20397, 21793, 21936, 37429, 42654, 28485, 21414, 24144, 20816, 18908], - [18141, 28730, 22389, 19005, 47544, 15975, 18557, 12894, 16055, 20674, 18093, 19539, 18746, 16138, 14374, 14727, 10911, 22888, 14053, 10633, 6949, 3737, 13117, 5211, 14901, 0, 1777, 1217, 3528, 2896, 5892, 7104, 7338, 23517, 29068, 13583, 7667, 10304, 7330, 6204], - [19248, 29976, 23592, 20295, 48689, 17101, 19545, 13251, 16300, 20872, 17672, 18517, 17725, 15126, 13194, 13336, 10190, 21194, 12378, 8889, 5177, 1961, 11575, 4685, 13738, 1777, 0, 2217, 2976, 3610, 6675, 8055, 8965, 25197, 30774, 14865, 8007, 10742, 7532, 6000], - [17129, 27803, 21433, 18105, 46560, 14971, 17490, 11680, 14838, 19457, 16955, 18636, 17845, 15240, 13590, 14109, 9900, 22640, 13798, 10569, 7050, 4036, 13793, 6348, 15855, 1217, 2217, 0, 2647, 1686, 4726, 6000, 6810, 23060, 28665, 12674, 6450, 9094, 6117, 5066], - [17192, 28076, 21655, 18551, 46567, 15104, 17309, 10455, 13422, 17961, 14735, 16024, 15232, 12625, 10943, 11507, 7397, 20334, 11537, 8646, 5619, 3588, 13300, 7636, 16118, 3528, 2976, 2647, 0, 2320, 4593, 6093, 8479, 24542, 30219, 13194, 5301, 8042, 4735, 3039], - [15645, 26408, 20011, 16763, 45086, 13503, 15936, 9997, 13165, 17787, 15510, 17632, 16848, 14264, 12824, 13611, 8758, 22636, 13852, 10938, 7730, 5109, 15009, 8010, 17348, 2896, 3610, 1686, 2320, 0, 3086, 4444, 6169, 22301, 27963, 11344, 4780, 7408, 4488, 3721], - [12658, 23504, 17087, 13958, 42083, 10544, 12881, 7194, 10430, 15048, 13694, 16948, 16197, 13736, 12815, 14104, 8119, 23801, 15276, 12906, 10187, 7996, 17856, 11074, 20397, 5892, 6675, 4726, 4593, 3086, 0, 1501, 5239, 20390, 26101, 8611, 2418, 4580, 2599, 3496], - [11210, 22025, 15612, 12459, 40648, 9080, 11498, 6574, 9813, 14372, 13768, 17587, 16859, 14482, 13779, 15222, 8948, 25065, 16632, 14366, 11689, 9459, 19337, 12315, 21793, 7104, 8055, 6000, 6093, 4444, 1501, 0, 4608, 19032, 24747, 7110, 2860, 4072, 3355, 4772], - [12094, 22000, 15872, 12296, 40971, 9983, 12944, 10678, 13777, 18115, 18317, 22131, 21391, 18958, 18042, 19237, 13353, 28675, 19957, 17106, 13792, 10846, 20454, 11926, 21936, 7338, 8965, 6810, 8479, 6169, 5239, 4608, 0, 16249, 21866, 7146, 7403, 8446, 7773, 8614], - [13175, 13197, 11653, 10370, 29929, 13435, 14711, 20959, 22300, 24280, 28831, 34799, 34211, 32292, 32259, 34013, 27354, 44048, 35660, 33171, 30012, 27094, 36551, 27537, 37429, 23517, 25197, 23060, 24542, 22301, 20390, 19032, 16249, 0, 5714, 12901, 21524, 20543, 22186, 23805], - [18162, 14936, 15666, 15331, 28493, 18755, 19589, 26458, 27564, 29101, 34148, 40296, 39731, 37879, 37918, 39703, 33023, 49756, 41373, 38858, 35654, 32690, 42017, 32880, 42654, 29068, 30774, 28665, 30219, 27963, 26101, 24747, 21866, 5714, 0, 18516, 27229, 26181, 27895, 29519], - [4968, 15146, 8842, 5430, 34015, 2947, 5993, 8180, 10126, 13400, 16326, 21953, 21345, 19391, 19416, 21271, 14542, 31426, 23361, 21390, 18799, 16451, 26352, 18634, 28485, 13583, 14865, 12674, 13194, 11344, 8611, 7110, 7146, 12901, 18516, 0, 9029, 7668, 9742, 11614], - [12308, 23246, 16843, 14044, 41473, 10344, 12227, 5255, 8388, 13008, 11276, 14739, 14006, 11621, 10975, 12528, 6106, 22528, 14333, 12507, 10406, 8887, 18403, 12644, 21414, 7667, 8007, 6450, 5301, 4780, 2418, 2860, 7403, 21524, 27229, 9029, 0, 2747, 726, 2749], - [10084, 20956, 14618, 12135, 38935, 8306, 9793, 2615, 5850, 10467, 9918, 14568, 13907, 11803, 11750, 13657, 6901, 23862, 16125, 14748, 12981, 11624, 21033, 15358, 24144, 10304, 10742, 9094, 8042, 7408, 4580, 4072, 8446, 20543, 26181, 7668, 2747, 0, 3330, 5313], - [13026, 23963, 17563, 14771, 42160, 11069, 12925, 5730, 8778, 13375, 11235, 14366, 13621, 11188, 10424, 11907, 5609, 21861, 13624, 11781, 9718, 8304, 17737, 12200, 20816, 7330, 7532, 6117, 4735, 4488, 2599, 3355, 7773, 22186, 27895, 9742, 726, 3330, 0, 2042], - [15056, 25994, 19589, 16743, 44198, 13078, 14967, 7552, 10422, 14935, 11891, 14002, 13225, 10671, 9475, 10633, 5084, 20315, 11866, 9802, 7682, 6471, 15720, 10674, 18908, 6204, 6000, 5066, 3039, 3721, 3496, 4772, 8614, 23805, 29519, 11614, 2749, 5313, 2042, 0], -] # yapf: disable + [ + 0, + 10938, + 4542, + 2835, + 29441, + 2171, + 1611, + 9208, + 9528, + 11111, + 16120, + 22606, + 22127, + 20627, + 21246, + 23387, + 16697, + 33609, + 26184, + 24772, + 22644, + 20655, + 30492, + 23296, + 32979, + 18141, + 19248, + 17129, + 17192, + 15645, + 12658, + 11210, + 12094, + 13175, + 18162, + 4968, + 12308, + 10084, + 13026, + 15056, + ], + [ + 10938, + 0, + 6422, + 9742, + 18988, + 12974, + 11216, + 19715, + 19004, + 18271, + 25070, + 31971, + 31632, + 30571, + 31578, + 33841, + 27315, + 43964, + 36944, + 35689, + 33569, + 31481, + 41360, + 33760, + 43631, + 28730, + 29976, + 27803, + 28076, + 26408, + 23504, + 22025, + 22000, + 13197, + 14936, + 15146, + 23246, + 20956, + 23963, + 25994, + ], + [ + 4542, + 6422, + 0, + 3644, + 25173, + 6552, + 5092, + 13584, + 13372, + 13766, + 19805, + 26537, + 26117, + 24804, + 25590, + 27784, + 21148, + 37981, + 30693, + 29315, + 27148, + 25071, + 34943, + 27472, + 37281, + 22389, + 23592, + 21433, + 21655, + 20011, + 17087, + 15612, + 15872, + 11653, + 15666, + 8842, + 16843, + 14618, + 17563, + 19589, + ], + [ + 2835, + 9742, + 3644, + 0, + 28681, + 3851, + 4341, + 11660, + 12294, + 13912, + 18893, + 25283, + 24777, + 23173, + 23636, + 25696, + 18950, + 35927, + 28233, + 26543, + 24127, + 21864, + 31765, + 24018, + 33904, + 19005, + 20295, + 18105, + 18551, + 16763, + 13958, + 12459, + 12296, + 10370, + 15331, + 5430, + 14044, + 12135, + 14771, + 16743, + ], + [ + 29441, + 18988, + 25173, + 28681, + 0, + 31590, + 29265, + 37173, + 35501, + 32929, + 40239, + 47006, + 46892, + 46542, + 48112, + 50506, + 44539, + 60103, + 54208, + 53557, + 51878, + 50074, + 59849, + 52645, + 62415, + 47544, + 48689, + 46560, + 46567, + 45086, + 42083, + 40648, + 40971, + 29929, + 28493, + 34015, + 41473, + 38935, + 42160, + 44198, + ], + [ + 2171, + 12974, + 6552, + 3851, + 31590, + 0, + 3046, + 7856, + 8864, + 11330, + 15411, + 21597, + 21065, + 19382, + 19791, + 21845, + 15099, + 32076, + 24425, + 22848, + 20600, + 18537, + 28396, + 21125, + 30825, + 15975, + 17101, + 14971, + 15104, + 13503, + 10544, + 9080, + 9983, + 13435, + 18755, + 2947, + 10344, + 8306, + 11069, + 13078, + ], + [ + 1611, + 11216, + 5092, + 4341, + 29265, + 3046, + 0, + 8526, + 8368, + 9573, + 14904, + 21529, + 21085, + 19719, + 20504, + 22713, + 16118, + 32898, + 25728, + 24541, + 22631, + 20839, + 30584, + 23755, + 33278, + 18557, + 19545, + 17490, + 17309, + 15936, + 12881, + 11498, + 12944, + 14711, + 19589, + 5993, + 12227, + 9793, + 12925, + 14967, + ], + [ + 9208, + 19715, + 13584, + 11660, + 37173, + 7856, + 8526, + 0, + 3248, + 7855, + 8245, + 13843, + 13272, + 11526, + 12038, + 14201, + 7599, + 24411, + 17259, + 16387, + 15050, + 13999, + 23134, + 17899, + 26460, + 12894, + 13251, + 11680, + 10455, + 9997, + 7194, + 6574, + 10678, + 20959, + 26458, + 8180, + 5255, + 2615, + 5730, + 7552, + ], + [ + 9528, + 19004, + 13372, + 12294, + 35501, + 8864, + 8368, + 3248, + 0, + 4626, + 6598, + 13168, + 12746, + 11567, + 12731, + 15083, + 9120, + 25037, + 18718, + 18433, + 17590, + 16888, + 25630, + 20976, + 29208, + 16055, + 16300, + 14838, + 13422, + 13165, + 10430, + 9813, + 13777, + 22300, + 27564, + 10126, + 8388, + 5850, + 8778, + 10422, + ], + [ + 11111, + 18271, + 13766, + 13912, + 32929, + 11330, + 9573, + 7855, + 4626, + 0, + 7318, + 14185, + 14005, + 13655, + 15438, + 17849, + 12839, + 27179, + 21947, + 22230, + 21814, + 21366, + 29754, + 25555, + 33535, + 20674, + 20872, + 19457, + 17961, + 17787, + 15048, + 14372, + 18115, + 24280, + 29101, + 13400, + 13008, + 10467, + 13375, + 14935, + ], + [ + 16120, + 25070, + 19805, + 18893, + 40239, + 15411, + 14904, + 8245, + 6598, + 7318, + 0, + 6939, + 6702, + 6498, + 8610, + 10961, + 7744, + 19889, + 15350, + 16403, + 16975, + 17517, + 24357, + 22176, + 28627, + 18093, + 17672, + 16955, + 14735, + 15510, + 13694, + 13768, + 18317, + 28831, + 34148, + 16326, + 11276, + 9918, + 11235, + 11891, + ], + [ + 22606, + 31971, + 26537, + 25283, + 47006, + 21597, + 21529, + 13843, + 13168, + 14185, + 6939, + 0, + 793, + 3401, + 5562, + 6839, + 8923, + 13433, + 11264, + 13775, + 15853, + 17629, + 21684, + 22315, + 26411, + 19539, + 18517, + 18636, + 16024, + 17632, + 16948, + 17587, + 22131, + 34799, + 40296, + 21953, + 14739, + 14568, + 14366, + 14002, + ], + [ + 22127, + 31632, + 26117, + 24777, + 46892, + 21065, + 21085, + 13272, + 12746, + 14005, + 6702, + 793, + 0, + 2608, + 4809, + 6215, + 8151, + 13376, + 10702, + 13094, + 15099, + 16845, + 21039, + 21535, + 25744, + 18746, + 17725, + 17845, + 15232, + 16848, + 16197, + 16859, + 21391, + 34211, + 39731, + 21345, + 14006, + 13907, + 13621, + 13225, + ], + [ + 20627, + 30571, + 24804, + 23173, + 46542, + 19382, + 19719, + 11526, + 11567, + 13655, + 6498, + 3401, + 2608, + 0, + 2556, + 4611, + 5630, + 13586, + 9157, + 11005, + 12681, + 14285, + 19044, + 18996, + 23644, + 16138, + 15126, + 15240, + 12625, + 14264, + 13736, + 14482, + 18958, + 32292, + 37879, + 19391, + 11621, + 11803, + 11188, + 10671, + ], + [ + 21246, + 31578, + 25590, + 23636, + 48112, + 19791, + 20504, + 12038, + 12731, + 15438, + 8610, + 5562, + 4809, + 2556, + 0, + 2411, + 4917, + 12395, + 6757, + 8451, + 10292, + 12158, + 16488, + 16799, + 21097, + 14374, + 13194, + 13590, + 10943, + 12824, + 12815, + 13779, + 18042, + 32259, + 37918, + 19416, + 10975, + 11750, + 10424, + 9475, + ], + [ + 23387, + 33841, + 27784, + 25696, + 50506, + 21845, + 22713, + 14201, + 15083, + 17849, + 10961, + 6839, + 6215, + 4611, + 2411, + 0, + 6760, + 10232, + 4567, + 7010, + 9607, + 12003, + 14846, + 16408, + 19592, + 14727, + 13336, + 14109, + 11507, + 13611, + 14104, + 15222, + 19237, + 34013, + 39703, + 21271, + 12528, + 13657, + 11907, + 10633, + ], + [ + 16697, + 27315, + 21148, + 18950, + 44539, + 15099, + 16118, + 7599, + 9120, + 12839, + 7744, + 8923, + 8151, + 5630, + 4917, + 6760, + 0, + 16982, + 9699, + 9400, + 9302, + 9823, + 16998, + 14534, + 21042, + 10911, + 10190, + 9900, + 7397, + 8758, + 8119, + 8948, + 13353, + 27354, + 33023, + 14542, + 6106, + 6901, + 5609, + 5084, + ], + [ + 33609, + 43964, + 37981, + 35927, + 60103, + 32076, + 32898, + 24411, + 25037, + 27179, + 19889, + 13433, + 13376, + 13586, + 12395, + 10232, + 16982, + 0, + 8843, + 12398, + 16193, + 19383, + 16423, + 22583, + 20997, + 22888, + 21194, + 22640, + 20334, + 22636, + 23801, + 25065, + 28675, + 44048, + 49756, + 31426, + 22528, + 23862, + 21861, + 20315, + ], + [ + 26184, + 36944, + 30693, + 28233, + 54208, + 24425, + 25728, + 17259, + 18718, + 21947, + 15350, + 11264, + 10702, + 9157, + 6757, + 4567, + 9699, + 8843, + 0, + 3842, + 7518, + 10616, + 10666, + 14237, + 15515, + 14053, + 12378, + 13798, + 11537, + 13852, + 15276, + 16632, + 19957, + 35660, + 41373, + 23361, + 14333, + 16125, + 13624, + 11866, + ], + [ + 24772, + 35689, + 29315, + 26543, + 53557, + 22848, + 24541, + 16387, + 18433, + 22230, + 16403, + 13775, + 13094, + 11005, + 8451, + 7010, + 9400, + 12398, + 3842, + 0, + 3795, + 7014, + 8053, + 10398, + 12657, + 10633, + 8889, + 10569, + 8646, + 10938, + 12906, + 14366, + 17106, + 33171, + 38858, + 21390, + 12507, + 14748, + 11781, + 9802, + ], + [ + 22644, + 33569, + 27148, + 24127, + 51878, + 20600, + 22631, + 15050, + 17590, + 21814, + 16975, + 15853, + 15099, + 12681, + 10292, + 9607, + 9302, + 16193, + 7518, + 3795, + 0, + 3250, + 8084, + 6873, + 11763, + 6949, + 5177, + 7050, + 5619, + 7730, + 10187, + 11689, + 13792, + 30012, + 35654, + 18799, + 10406, + 12981, + 9718, + 7682, + ], + [ + 20655, + 31481, + 25071, + 21864, + 50074, + 18537, + 20839, + 13999, + 16888, + 21366, + 17517, + 17629, + 16845, + 14285, + 12158, + 12003, + 9823, + 19383, + 10616, + 7014, + 3250, + 0, + 9901, + 4746, + 12531, + 3737, + 1961, + 4036, + 3588, + 5109, + 7996, + 9459, + 10846, + 27094, + 32690, + 16451, + 8887, + 11624, + 8304, + 6471, + ], + [ + 30492, + 41360, + 34943, + 31765, + 59849, + 28396, + 30584, + 23134, + 25630, + 29754, + 24357, + 21684, + 21039, + 19044, + 16488, + 14846, + 16998, + 16423, + 10666, + 8053, + 8084, + 9901, + 0, + 9363, + 4870, + 13117, + 11575, + 13793, + 13300, + 15009, + 17856, + 19337, + 20454, + 36551, + 42017, + 26352, + 18403, + 21033, + 17737, + 15720, + ], + [ + 23296, + 33760, + 27472, + 24018, + 52645, + 21125, + 23755, + 17899, + 20976, + 25555, + 22176, + 22315, + 21535, + 18996, + 16799, + 16408, + 14534, + 22583, + 14237, + 10398, + 6873, + 4746, + 9363, + 0, + 10020, + 5211, + 4685, + 6348, + 7636, + 8010, + 11074, + 12315, + 11926, + 27537, + 32880, + 18634, + 12644, + 15358, + 12200, + 10674, + ], + [ + 32979, + 43631, + 37281, + 33904, + 62415, + 30825, + 33278, + 26460, + 29208, + 33535, + 28627, + 26411, + 25744, + 23644, + 21097, + 19592, + 21042, + 20997, + 15515, + 12657, + 11763, + 12531, + 4870, + 10020, + 0, + 14901, + 13738, + 15855, + 16118, + 17348, + 20397, + 21793, + 21936, + 37429, + 42654, + 28485, + 21414, + 24144, + 20816, + 18908, + ], + [ + 18141, + 28730, + 22389, + 19005, + 47544, + 15975, + 18557, + 12894, + 16055, + 20674, + 18093, + 19539, + 18746, + 16138, + 14374, + 14727, + 10911, + 22888, + 14053, + 10633, + 6949, + 3737, + 13117, + 5211, + 14901, + 0, + 1777, + 1217, + 3528, + 2896, + 5892, + 7104, + 7338, + 23517, + 29068, + 13583, + 7667, + 10304, + 7330, + 6204, + ], + [ + 19248, + 29976, + 23592, + 20295, + 48689, + 17101, + 19545, + 13251, + 16300, + 20872, + 17672, + 18517, + 17725, + 15126, + 13194, + 13336, + 10190, + 21194, + 12378, + 8889, + 5177, + 1961, + 11575, + 4685, + 13738, + 1777, + 0, + 2217, + 2976, + 3610, + 6675, + 8055, + 8965, + 25197, + 30774, + 14865, + 8007, + 10742, + 7532, + 6000, + ], + [ + 17129, + 27803, + 21433, + 18105, + 46560, + 14971, + 17490, + 11680, + 14838, + 19457, + 16955, + 18636, + 17845, + 15240, + 13590, + 14109, + 9900, + 22640, + 13798, + 10569, + 7050, + 4036, + 13793, + 6348, + 15855, + 1217, + 2217, + 0, + 2647, + 1686, + 4726, + 6000, + 6810, + 23060, + 28665, + 12674, + 6450, + 9094, + 6117, + 5066, + ], + [ + 17192, + 28076, + 21655, + 18551, + 46567, + 15104, + 17309, + 10455, + 13422, + 17961, + 14735, + 16024, + 15232, + 12625, + 10943, + 11507, + 7397, + 20334, + 11537, + 8646, + 5619, + 3588, + 13300, + 7636, + 16118, + 3528, + 2976, + 2647, + 0, + 2320, + 4593, + 6093, + 8479, + 24542, + 30219, + 13194, + 5301, + 8042, + 4735, + 3039, + ], + [ + 15645, + 26408, + 20011, + 16763, + 45086, + 13503, + 15936, + 9997, + 13165, + 17787, + 15510, + 17632, + 16848, + 14264, + 12824, + 13611, + 8758, + 22636, + 13852, + 10938, + 7730, + 5109, + 15009, + 8010, + 17348, + 2896, + 3610, + 1686, + 2320, + 0, + 3086, + 4444, + 6169, + 22301, + 27963, + 11344, + 4780, + 7408, + 4488, + 3721, + ], + [ + 12658, + 23504, + 17087, + 13958, + 42083, + 10544, + 12881, + 7194, + 10430, + 15048, + 13694, + 16948, + 16197, + 13736, + 12815, + 14104, + 8119, + 23801, + 15276, + 12906, + 10187, + 7996, + 17856, + 11074, + 20397, + 5892, + 6675, + 4726, + 4593, + 3086, + 0, + 1501, + 5239, + 20390, + 26101, + 8611, + 2418, + 4580, + 2599, + 3496, + ], + [ + 11210, + 22025, + 15612, + 12459, + 40648, + 9080, + 11498, + 6574, + 9813, + 14372, + 13768, + 17587, + 16859, + 14482, + 13779, + 15222, + 8948, + 25065, + 16632, + 14366, + 11689, + 9459, + 19337, + 12315, + 21793, + 7104, + 8055, + 6000, + 6093, + 4444, + 1501, + 0, + 4608, + 19032, + 24747, + 7110, + 2860, + 4072, + 3355, + 4772, + ], + [ + 12094, + 22000, + 15872, + 12296, + 40971, + 9983, + 12944, + 10678, + 13777, + 18115, + 18317, + 22131, + 21391, + 18958, + 18042, + 19237, + 13353, + 28675, + 19957, + 17106, + 13792, + 10846, + 20454, + 11926, + 21936, + 7338, + 8965, + 6810, + 8479, + 6169, + 5239, + 4608, + 0, + 16249, + 21866, + 7146, + 7403, + 8446, + 7773, + 8614, + ], + [ + 13175, + 13197, + 11653, + 10370, + 29929, + 13435, + 14711, + 20959, + 22300, + 24280, + 28831, + 34799, + 34211, + 32292, + 32259, + 34013, + 27354, + 44048, + 35660, + 33171, + 30012, + 27094, + 36551, + 27537, + 37429, + 23517, + 25197, + 23060, + 24542, + 22301, + 20390, + 19032, + 16249, + 0, + 5714, + 12901, + 21524, + 20543, + 22186, + 23805, + ], + [ + 18162, + 14936, + 15666, + 15331, + 28493, + 18755, + 19589, + 26458, + 27564, + 29101, + 34148, + 40296, + 39731, + 37879, + 37918, + 39703, + 33023, + 49756, + 41373, + 38858, + 35654, + 32690, + 42017, + 32880, + 42654, + 29068, + 30774, + 28665, + 30219, + 27963, + 26101, + 24747, + 21866, + 5714, + 0, + 18516, + 27229, + 26181, + 27895, + 29519, + ], + [ + 4968, + 15146, + 8842, + 5430, + 34015, + 2947, + 5993, + 8180, + 10126, + 13400, + 16326, + 21953, + 21345, + 19391, + 19416, + 21271, + 14542, + 31426, + 23361, + 21390, + 18799, + 16451, + 26352, + 18634, + 28485, + 13583, + 14865, + 12674, + 13194, + 11344, + 8611, + 7110, + 7146, + 12901, + 18516, + 0, + 9029, + 7668, + 9742, + 11614, + ], + [ + 12308, + 23246, + 16843, + 14044, + 41473, + 10344, + 12227, + 5255, + 8388, + 13008, + 11276, + 14739, + 14006, + 11621, + 10975, + 12528, + 6106, + 22528, + 14333, + 12507, + 10406, + 8887, + 18403, + 12644, + 21414, + 7667, + 8007, + 6450, + 5301, + 4780, + 2418, + 2860, + 7403, + 21524, + 27229, + 9029, + 0, + 2747, + 726, + 2749, + ], + [ + 10084, + 20956, + 14618, + 12135, + 38935, + 8306, + 9793, + 2615, + 5850, + 10467, + 9918, + 14568, + 13907, + 11803, + 11750, + 13657, + 6901, + 23862, + 16125, + 14748, + 12981, + 11624, + 21033, + 15358, + 24144, + 10304, + 10742, + 9094, + 8042, + 7408, + 4580, + 4072, + 8446, + 20543, + 26181, + 7668, + 2747, + 0, + 3330, + 5313, + ], + [ + 13026, + 23963, + 17563, + 14771, + 42160, + 11069, + 12925, + 5730, + 8778, + 13375, + 11235, + 14366, + 13621, + 11188, + 10424, + 11907, + 5609, + 21861, + 13624, + 11781, + 9718, + 8304, + 17737, + 12200, + 20816, + 7330, + 7532, + 6117, + 4735, + 4488, + 2599, + 3355, + 7773, + 22186, + 27895, + 9742, + 726, + 3330, + 0, + 2042, + ], + [ + 15056, + 25994, + 19589, + 16743, + 44198, + 13078, + 14967, + 7552, + 10422, + 14935, + 11891, + 14002, + 13225, + 10671, + 9475, + 10633, + 5084, + 20315, + 11866, + 9802, + 7682, + 6471, + 15720, + 10674, + 18908, + 6204, + 6000, + 5066, + 3039, + 3721, + 3496, + 4772, + 8614, + 23805, + 29519, + 11614, + 2749, + 5313, + 2042, + 0, + ], +] # yapf: disable MAX_DISTANCE = 80_000 @@ -67,99 +1707,97 @@ # Create a console solution printer. def print_solution(manager, routing, assignment): - """Prints assignment on console.""" - print(f'Objective: {assignment.ObjectiveValue()}') - # Display dropped nodes. - dropped_nodes = 'Dropped nodes:' - for index in range(routing.Size()): - if routing.IsStart(index) or routing.IsEnd(index): - continue - if assignment.Value(routing.NextVar(index)) == index: - node = manager.IndexToNode(index) - dropped_nodes += f' {node}({VISIT_VALUES[node]})' - print(dropped_nodes) - # Display routes - index = routing.Start(0) - plan_output = 'Route for vehicle 0:\n' - route_distance = 0 - value_collected = 0 - while not routing.IsEnd(index): - node = manager.IndexToNode(index) - value_collected += VISIT_VALUES[node] - plan_output += f' {node} ->' - previous_index = index - index = assignment.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - - plan_output += f' {manager.IndexToNode(index)}\n' - plan_output += f'Distance of the route: {route_distance}m\n' - plan_output += f'Value collected: {value_collected}/{sum(VISIT_VALUES)}\n' - print(plan_output) + """Prints assignment on console.""" + print(f'Objective: {assignment.ObjectiveValue()}') + # Display dropped nodes. + dropped_nodes = 'Dropped nodes:' + for index in range(routing.Size()): + if routing.IsStart(index) or routing.IsEnd(index): + continue + if assignment.Value(routing.NextVar(index)) == index: + node = manager.IndexToNode(index) + dropped_nodes += f' {node}({VISIT_VALUES[node]})' + print(dropped_nodes) + # Display routes + index = routing.Start(0) + plan_output = 'Route for vehicle 0:\n' + route_distance = 0 + value_collected = 0 + while not routing.IsEnd(index): + node = manager.IndexToNode(index) + value_collected += VISIT_VALUES[node] + plan_output += f' {node} ->' + previous_index = index + index = assignment.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) + + plan_output += f' {manager.IndexToNode(index)}\n' + plan_output += f'Distance of the route: {route_distance}m\n' + plan_output += f'Value collected: {value_collected}/{sum(VISIT_VALUES)}\n' + print(plan_output) def main(): - """Entry point of the program.""" - num_nodes = len(DISTANCE_MATRIX) - print(f'Num nodes = {num_nodes}') - num_vehicles = 1 - depot = 0 - all_nodes = range(num_nodes) - - # Create the routing index manager. - manager = pywrapcp.RoutingIndexManager( - num_nodes, - num_vehicles, - depot) - - # Create routing model. - routing = pywrapcp.RoutingModel(manager) - - # Create and register a transit callback. - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return DISTANCE_MATRIX[from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - - # Define cost of each arc. - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - - # Limit Vehicle distance. - dimension_name = 'Distance' - routing.AddDimension( - transit_callback_index, - 0, # no slack - MAX_DISTANCE, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name) - #distance_dimension = routing.GetDimensionOrDie(dimension_name) - #distance_dimension.SetGlobalSpanCostCoefficient(100) - - # Allow to drop nodes. - for node in range(1, num_nodes): - routing.AddDisjunction( - [manager.NodeToIndex(node)], - VISIT_VALUES[node]) - - # Setting first solution heuristic. - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH) - search_parameters.time_limit.FromSeconds(15) - #search_parameters.log_search = True - - # Solve the problem. - assignment = routing.SolveWithParameters(search_parameters) - - # Print solution on console. - if assignment: - print_solution(manager, routing, assignment) + """Entry point of the program.""" + num_nodes = len(DISTANCE_MATRIX) + print(f'Num nodes = {num_nodes}') + num_vehicles = 1 + depot = 0 + all_nodes = range(num_nodes) + + # Create the routing index manager. + manager = pywraprouting.RoutingIndexManager(num_nodes, num_vehicles, depot) + + # Create routing model. + routing = pywraprouting.RoutingModel(manager) + + # Create and register a transit callback. + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return DISTANCE_MATRIX[from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + + # Define cost of each arc. + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + + # Limit Vehicle distance. + dimension_name = 'Distance' + routing.AddDimension( + transit_callback_index, + 0, # no slack + MAX_DISTANCE, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + # distance_dimension = routing.GetDimensionOrDie(dimension_name) + # distance_dimension.SetGlobalSpanCostCoefficient(100) + + # Allow to drop nodes. + for node in range(1, num_nodes): + routing.AddDisjunction([manager.NodeToIndex(node)], VISIT_VALUES[node]) + + # Setting first solution heuristic. + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(15) + # search_parameters.log_search = True + + # Solve the problem. + assignment = routing.SolveWithParameters(search_parameters) + + # Print solution on console. + if assignment: + print_solution(manager, routing, assignment) if __name__ == '__main__': - main() + main() diff --git a/examples/python/prize_collecting_tsp_sat.py b/examples/python/prize_collecting_tsp_sat.py index 11ba7a0879a..2e351b37c7d 100644 --- a/examples/python/prize_collecting_tsp_sat.py +++ b/examples/python/prize_collecting_tsp_sat.py @@ -77,110 +77,110 @@ def print_solution( used_arcs: dict[tuple[int, int], cp_model.IntVar], num_nodes: int, ) -> None: - """Prints solution on console.""" - # Display dropped nodes. - dropped_nodes = "Dropped nodes:" - for i in range(num_nodes): - if i == 0: - continue - if not solver.boolean_value(visited_nodes[i]): - dropped_nodes += f" {i}({VISIT_VALUES[i]})" - print(dropped_nodes) - # Display routes - current_node = 0 - plan_output = "Route for vehicle 0:\n" - route_distance = 0 - value_collected = 0 - route_is_finished = False - while not route_is_finished: - value_collected += VISIT_VALUES[current_node] - plan_output += f" {current_node} ->" - # find next node - for node in range(num_nodes): - if node == current_node: - continue - if solver.boolean_value(used_arcs[current_node, node]): - route_distance += DISTANCE_MATRIX[current_node][node] - current_node = node - if current_node == 0: - route_is_finished = True - break - plan_output += f" {current_node}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - plan_output += f"value collected: {value_collected}/{sum(VISIT_VALUES)}\n" - print(plan_output) + """Prints solution on console.""" + # Display dropped nodes. + dropped_nodes = "Dropped nodes:" + for i in range(num_nodes): + if i == 0: + continue + if not solver.boolean_value(visited_nodes[i]): + dropped_nodes += f" {i}({VISIT_VALUES[i]})" + print(dropped_nodes) + # Display routes + current_node = 0 + plan_output = "Route for vehicle 0:\n" + route_distance = 0 + value_collected = 0 + route_is_finished = False + while not route_is_finished: + value_collected += VISIT_VALUES[current_node] + plan_output += f" {current_node} ->" + # find next node + for node in range(num_nodes): + if node == current_node: + continue + if solver.boolean_value(used_arcs[current_node, node]): + route_distance += DISTANCE_MATRIX[current_node][node] + current_node = node + if current_node == 0: + route_is_finished = True + break + plan_output += f" {current_node}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + plan_output += f"value collected: {value_collected}/{sum(VISIT_VALUES)}\n" + print(plan_output) def prize_collecting_tsp(): - """Entry point of the program.""" - num_nodes = len(DISTANCE_MATRIX) - all_nodes = range(num_nodes) - print(f"Num nodes = {num_nodes}") - - # Model. - model = cp_model.CpModel() - - obj_vars = [] - obj_coeffs = [] - visited_nodes = [] - used_arcs = {} - - # Create the circuit constraint. - arcs = [] - for i in all_nodes: - is_visited = model.new_bool_var(f"{i} is visited") - arcs.append((i, i, ~is_visited)) - - obj_vars.append(is_visited) - obj_coeffs.append(VISIT_VALUES[i]) - visited_nodes.append(is_visited) - - for j in all_nodes: - if i == j: - used_arcs[i, j] = ~is_visited - continue - arc_is_used = model.new_bool_var(f"{j} follows {i}") - arcs.append((i, j, arc_is_used)) - - obj_vars.append(arc_is_used) - obj_coeffs.append(-DISTANCE_MATRIX[i][j]) - used_arcs[i, j] = arc_is_used - - model.add_circuit(arcs) - - # Node 0 must be visited. - model.add(visited_nodes[0] == 1) - - # limit the route distance - model.add( - sum( - used_arcs[i, j] * DISTANCE_MATRIX[i][j] - for i in all_nodes - for j in all_nodes - ) - <= MAX_DISTANCE - ) - - # Maximize visited node values minus the travelled distance. - model.maximize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) - - # Solve and print out the solution. - solver = cp_model.CpSolver() - # To benefit from the linearization of the circuit constraint. - solver.parameters.max_time_in_seconds = 15.0 - solver.parameters.num_search_workers = 8 - solver.parameters.log_search_progress = True - - status = solver.solve(model) - if status == cp_model.FEASIBLE or status == cp_model.OPTIMAL: - print_solution(solver, visited_nodes, used_arcs, num_nodes) + """Entry point of the program.""" + num_nodes = len(DISTANCE_MATRIX) + all_nodes = range(num_nodes) + print(f"Num nodes = {num_nodes}") + + # Model. + model = cp_model.CpModel() + + obj_vars = [] + obj_coeffs = [] + visited_nodes = [] + used_arcs = {} + + # Create the circuit constraint. + arcs = [] + for i in all_nodes: + is_visited = model.new_bool_var(f"{i} is visited") + arcs.append((i, i, ~is_visited)) + + obj_vars.append(is_visited) + obj_coeffs.append(VISIT_VALUES[i]) + visited_nodes.append(is_visited) + + for j in all_nodes: + if i == j: + used_arcs[i, j] = ~is_visited + continue + arc_is_used = model.new_bool_var(f"{j} follows {i}") + arcs.append((i, j, arc_is_used)) + + obj_vars.append(arc_is_used) + obj_coeffs.append(-DISTANCE_MATRIX[i][j]) + used_arcs[i, j] = arc_is_used + + model.add_circuit(arcs) + + # Node 0 must be visited. + model.add(visited_nodes[0] == 1) + + # limit the route distance + model.add( + sum( + used_arcs[i, j] * DISTANCE_MATRIX[i][j] + for i in all_nodes + for j in all_nodes + ) + <= MAX_DISTANCE + ) + + # Maximize visited node values minus the travelled distance. + model.maximize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) + + # Solve and print out the solution. + solver = cp_model.CpSolver() + # To benefit from the linearization of the circuit constraint. + solver.parameters.max_time_in_seconds = 15.0 + solver.parameters.num_search_workers = 8 + solver.parameters.log_search_progress = True + + status = solver.solve(model) + if status == cp_model.FEASIBLE or status == cp_model.OPTIMAL: + print_solution(solver, visited_nodes, used_arcs, num_nodes) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - prize_collecting_tsp() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + prize_collecting_tsp() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/prize_collecting_vrp.py b/examples/python/prize_collecting_vrp.py index 493f763e839..0e3cf0a290b 100755 --- a/examples/python/prize_collecting_vrp.py +++ b/examples/python/prize_collecting_vrp.py @@ -13,51 +13,1691 @@ # limitations under the License. """Simple prize collecting VRP problem with a max distance.""" -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting DISTANCE_MATRIX = [ - [0, 10938, 4542, 2835, 29441, 2171, 1611, 9208, 9528, 11111, 16120, 22606, 22127, 20627, 21246, 23387, 16697, 33609, 26184, 24772, 22644, 20655, 30492, 23296, 32979, 18141, 19248, 17129, 17192, 15645, 12658, 11210, 12094, 13175, 18162, 4968, 12308, 10084, 13026, 15056], - [10938, 0, 6422, 9742, 18988, 12974, 11216, 19715, 19004, 18271, 25070, 31971, 31632, 30571, 31578, 33841, 27315, 43964, 36944, 35689, 33569, 31481, 41360, 33760, 43631, 28730, 29976, 27803, 28076, 26408, 23504, 22025, 22000, 13197, 14936, 15146, 23246, 20956, 23963, 25994], - [4542, 6422, 0, 3644, 25173, 6552, 5092, 13584, 13372, 13766, 19805, 26537, 26117, 24804, 25590, 27784, 21148, 37981, 30693, 29315, 27148, 25071, 34943, 27472, 37281, 22389, 23592, 21433, 21655, 20011, 17087, 15612, 15872, 11653, 15666, 8842, 16843, 14618, 17563, 19589], - [2835, 9742, 3644, 0, 28681, 3851, 4341, 11660, 12294, 13912, 18893, 25283, 24777, 23173, 23636, 25696, 18950, 35927, 28233, 26543, 24127, 21864, 31765, 24018, 33904, 19005, 20295, 18105, 18551, 16763, 13958, 12459, 12296, 10370, 15331, 5430, 14044, 12135, 14771, 16743], - [29441, 18988, 25173, 28681, 0, 31590, 29265, 37173, 35501, 32929, 40239, 47006, 46892, 46542, 48112, 50506, 44539, 60103, 54208, 53557, 51878, 50074, 59849, 52645, 62415, 47544, 48689, 46560, 46567, 45086, 42083, 40648, 40971, 29929, 28493, 34015, 41473, 38935, 42160, 44198], - [2171, 12974, 6552, 3851, 31590, 0, 3046, 7856, 8864, 11330, 15411, 21597, 21065, 19382, 19791, 21845, 15099, 32076, 24425, 22848, 20600, 18537, 28396, 21125, 30825, 15975, 17101, 14971, 15104, 13503, 10544, 9080, 9983, 13435, 18755, 2947, 10344, 8306, 11069, 13078], - [1611, 11216, 5092, 4341, 29265, 3046, 0, 8526, 8368, 9573, 14904, 21529, 21085, 19719, 20504, 22713, 16118, 32898, 25728, 24541, 22631, 20839, 30584, 23755, 33278, 18557, 19545, 17490, 17309, 15936, 12881, 11498, 12944, 14711, 19589, 5993, 12227, 9793, 12925, 14967], - [9208, 19715, 13584, 11660, 37173, 7856, 8526, 0, 3248, 7855, 8245, 13843, 13272, 11526, 12038, 14201, 7599, 24411, 17259, 16387, 15050, 13999, 23134, 17899, 26460, 12894, 13251, 11680, 10455, 9997, 7194, 6574, 10678, 20959, 26458, 8180, 5255, 2615, 5730, 7552], - [9528, 19004, 13372, 12294, 35501, 8864, 8368, 3248, 0, 4626, 6598, 13168, 12746, 11567, 12731, 15083, 9120, 25037, 18718, 18433, 17590, 16888, 25630, 20976, 29208, 16055, 16300, 14838, 13422, 13165, 10430, 9813, 13777, 22300, 27564, 10126, 8388, 5850, 8778, 10422], - [11111, 18271, 13766, 13912, 32929, 11330, 9573, 7855, 4626, 0, 7318, 14185, 14005, 13655, 15438, 17849, 12839, 27179, 21947, 22230, 21814, 21366, 29754, 25555, 33535, 20674, 20872, 19457, 17961, 17787, 15048, 14372, 18115, 24280, 29101, 13400, 13008, 10467, 13375, 14935], - [16120, 25070, 19805, 18893, 40239, 15411, 14904, 8245, 6598, 7318, 0, 6939, 6702, 6498, 8610, 10961, 7744, 19889, 15350, 16403, 16975, 17517, 24357, 22176, 28627, 18093, 17672, 16955, 14735, 15510, 13694, 13768, 18317, 28831, 34148, 16326, 11276, 9918, 11235, 11891], - [22606, 31971, 26537, 25283, 47006, 21597, 21529, 13843, 13168, 14185, 6939, 0, 793, 3401, 5562, 6839, 8923, 13433, 11264, 13775, 15853, 17629, 21684, 22315, 26411, 19539, 18517, 18636, 16024, 17632, 16948, 17587, 22131, 34799, 40296, 21953, 14739, 14568, 14366, 14002], - [22127, 31632, 26117, 24777, 46892, 21065, 21085, 13272, 12746, 14005, 6702, 793, 0, 2608, 4809, 6215, 8151, 13376, 10702, 13094, 15099, 16845, 21039, 21535, 25744, 18746, 17725, 17845, 15232, 16848, 16197, 16859, 21391, 34211, 39731, 21345, 14006, 13907, 13621, 13225], - [20627, 30571, 24804, 23173, 46542, 19382, 19719, 11526, 11567, 13655, 6498, 3401, 2608, 0, 2556, 4611, 5630, 13586, 9157, 11005, 12681, 14285, 19044, 18996, 23644, 16138, 15126, 15240, 12625, 14264, 13736, 14482, 18958, 32292, 37879, 19391, 11621, 11803, 11188, 10671], - [21246, 31578, 25590, 23636, 48112, 19791, 20504, 12038, 12731, 15438, 8610, 5562, 4809, 2556, 0, 2411, 4917, 12395, 6757, 8451, 10292, 12158, 16488, 16799, 21097, 14374, 13194, 13590, 10943, 12824, 12815, 13779, 18042, 32259, 37918, 19416, 10975, 11750, 10424, 9475], - [23387, 33841, 27784, 25696, 50506, 21845, 22713, 14201, 15083, 17849, 10961, 6839, 6215, 4611, 2411, 0, 6760, 10232, 4567, 7010, 9607, 12003, 14846, 16408, 19592, 14727, 13336, 14109, 11507, 13611, 14104, 15222, 19237, 34013, 39703, 21271, 12528, 13657, 11907, 10633], - [16697, 27315, 21148, 18950, 44539, 15099, 16118, 7599, 9120, 12839, 7744, 8923, 8151, 5630, 4917, 6760, 0, 16982, 9699, 9400, 9302, 9823, 16998, 14534, 21042, 10911, 10190, 9900, 7397, 8758, 8119, 8948, 13353, 27354, 33023, 14542, 6106, 6901, 5609, 5084], - [33609, 43964, 37981, 35927, 60103, 32076, 32898, 24411, 25037, 27179, 19889, 13433, 13376, 13586, 12395, 10232, 16982, 0, 8843, 12398, 16193, 19383, 16423, 22583, 20997, 22888, 21194, 22640, 20334, 22636, 23801, 25065, 28675, 44048, 49756, 31426, 22528, 23862, 21861, 20315], - [26184, 36944, 30693, 28233, 54208, 24425, 25728, 17259, 18718, 21947, 15350, 11264, 10702, 9157, 6757, 4567, 9699, 8843, 0, 3842, 7518, 10616, 10666, 14237, 15515, 14053, 12378, 13798, 11537, 13852, 15276, 16632, 19957, 35660, 41373, 23361, 14333, 16125, 13624, 11866], - [24772, 35689, 29315, 26543, 53557, 22848, 24541, 16387, 18433, 22230, 16403, 13775, 13094, 11005, 8451, 7010, 9400, 12398, 3842, 0, 3795, 7014, 8053, 10398, 12657, 10633, 8889, 10569, 8646, 10938, 12906, 14366, 17106, 33171, 38858, 21390, 12507, 14748, 11781, 9802], - [22644, 33569, 27148, 24127, 51878, 20600, 22631, 15050, 17590, 21814, 16975, 15853, 15099, 12681, 10292, 9607, 9302, 16193, 7518, 3795, 0, 3250, 8084, 6873, 11763, 6949, 5177, 7050, 5619, 7730, 10187, 11689, 13792, 30012, 35654, 18799, 10406, 12981, 9718, 7682], - [20655, 31481, 25071, 21864, 50074, 18537, 20839, 13999, 16888, 21366, 17517, 17629, 16845, 14285, 12158, 12003, 9823, 19383, 10616, 7014, 3250, 0, 9901, 4746, 12531, 3737, 1961, 4036, 3588, 5109, 7996, 9459, 10846, 27094, 32690, 16451, 8887, 11624, 8304, 6471], - [30492, 41360, 34943, 31765, 59849, 28396, 30584, 23134, 25630, 29754, 24357, 21684, 21039, 19044, 16488, 14846, 16998, 16423, 10666, 8053, 8084, 9901, 0, 9363, 4870, 13117, 11575, 13793, 13300, 15009, 17856, 19337, 20454, 36551, 42017, 26352, 18403, 21033, 17737, 15720], - [23296, 33760, 27472, 24018, 52645, 21125, 23755, 17899, 20976, 25555, 22176, 22315, 21535, 18996, 16799, 16408, 14534, 22583, 14237, 10398, 6873, 4746, 9363, 0, 10020, 5211, 4685, 6348, 7636, 8010, 11074, 12315, 11926, 27537, 32880, 18634, 12644, 15358, 12200, 10674], - [32979, 43631, 37281, 33904, 62415, 30825, 33278, 26460, 29208, 33535, 28627, 26411, 25744, 23644, 21097, 19592, 21042, 20997, 15515, 12657, 11763, 12531, 4870, 10020, 0, 14901, 13738, 15855, 16118, 17348, 20397, 21793, 21936, 37429, 42654, 28485, 21414, 24144, 20816, 18908], - [18141, 28730, 22389, 19005, 47544, 15975, 18557, 12894, 16055, 20674, 18093, 19539, 18746, 16138, 14374, 14727, 10911, 22888, 14053, 10633, 6949, 3737, 13117, 5211, 14901, 0, 1777, 1217, 3528, 2896, 5892, 7104, 7338, 23517, 29068, 13583, 7667, 10304, 7330, 6204], - [19248, 29976, 23592, 20295, 48689, 17101, 19545, 13251, 16300, 20872, 17672, 18517, 17725, 15126, 13194, 13336, 10190, 21194, 12378, 8889, 5177, 1961, 11575, 4685, 13738, 1777, 0, 2217, 2976, 3610, 6675, 8055, 8965, 25197, 30774, 14865, 8007, 10742, 7532, 6000], - [17129, 27803, 21433, 18105, 46560, 14971, 17490, 11680, 14838, 19457, 16955, 18636, 17845, 15240, 13590, 14109, 9900, 22640, 13798, 10569, 7050, 4036, 13793, 6348, 15855, 1217, 2217, 0, 2647, 1686, 4726, 6000, 6810, 23060, 28665, 12674, 6450, 9094, 6117, 5066], - [17192, 28076, 21655, 18551, 46567, 15104, 17309, 10455, 13422, 17961, 14735, 16024, 15232, 12625, 10943, 11507, 7397, 20334, 11537, 8646, 5619, 3588, 13300, 7636, 16118, 3528, 2976, 2647, 0, 2320, 4593, 6093, 8479, 24542, 30219, 13194, 5301, 8042, 4735, 3039], - [15645, 26408, 20011, 16763, 45086, 13503, 15936, 9997, 13165, 17787, 15510, 17632, 16848, 14264, 12824, 13611, 8758, 22636, 13852, 10938, 7730, 5109, 15009, 8010, 17348, 2896, 3610, 1686, 2320, 0, 3086, 4444, 6169, 22301, 27963, 11344, 4780, 7408, 4488, 3721], - [12658, 23504, 17087, 13958, 42083, 10544, 12881, 7194, 10430, 15048, 13694, 16948, 16197, 13736, 12815, 14104, 8119, 23801, 15276, 12906, 10187, 7996, 17856, 11074, 20397, 5892, 6675, 4726, 4593, 3086, 0, 1501, 5239, 20390, 26101, 8611, 2418, 4580, 2599, 3496], - [11210, 22025, 15612, 12459, 40648, 9080, 11498, 6574, 9813, 14372, 13768, 17587, 16859, 14482, 13779, 15222, 8948, 25065, 16632, 14366, 11689, 9459, 19337, 12315, 21793, 7104, 8055, 6000, 6093, 4444, 1501, 0, 4608, 19032, 24747, 7110, 2860, 4072, 3355, 4772], - [12094, 22000, 15872, 12296, 40971, 9983, 12944, 10678, 13777, 18115, 18317, 22131, 21391, 18958, 18042, 19237, 13353, 28675, 19957, 17106, 13792, 10846, 20454, 11926, 21936, 7338, 8965, 6810, 8479, 6169, 5239, 4608, 0, 16249, 21866, 7146, 7403, 8446, 7773, 8614], - [13175, 13197, 11653, 10370, 29929, 13435, 14711, 20959, 22300, 24280, 28831, 34799, 34211, 32292, 32259, 34013, 27354, 44048, 35660, 33171, 30012, 27094, 36551, 27537, 37429, 23517, 25197, 23060, 24542, 22301, 20390, 19032, 16249, 0, 5714, 12901, 21524, 20543, 22186, 23805], - [18162, 14936, 15666, 15331, 28493, 18755, 19589, 26458, 27564, 29101, 34148, 40296, 39731, 37879, 37918, 39703, 33023, 49756, 41373, 38858, 35654, 32690, 42017, 32880, 42654, 29068, 30774, 28665, 30219, 27963, 26101, 24747, 21866, 5714, 0, 18516, 27229, 26181, 27895, 29519], - [4968, 15146, 8842, 5430, 34015, 2947, 5993, 8180, 10126, 13400, 16326, 21953, 21345, 19391, 19416, 21271, 14542, 31426, 23361, 21390, 18799, 16451, 26352, 18634, 28485, 13583, 14865, 12674, 13194, 11344, 8611, 7110, 7146, 12901, 18516, 0, 9029, 7668, 9742, 11614], - [12308, 23246, 16843, 14044, 41473, 10344, 12227, 5255, 8388, 13008, 11276, 14739, 14006, 11621, 10975, 12528, 6106, 22528, 14333, 12507, 10406, 8887, 18403, 12644, 21414, 7667, 8007, 6450, 5301, 4780, 2418, 2860, 7403, 21524, 27229, 9029, 0, 2747, 726, 2749], - [10084, 20956, 14618, 12135, 38935, 8306, 9793, 2615, 5850, 10467, 9918, 14568, 13907, 11803, 11750, 13657, 6901, 23862, 16125, 14748, 12981, 11624, 21033, 15358, 24144, 10304, 10742, 9094, 8042, 7408, 4580, 4072, 8446, 20543, 26181, 7668, 2747, 0, 3330, 5313], - [13026, 23963, 17563, 14771, 42160, 11069, 12925, 5730, 8778, 13375, 11235, 14366, 13621, 11188, 10424, 11907, 5609, 21861, 13624, 11781, 9718, 8304, 17737, 12200, 20816, 7330, 7532, 6117, 4735, 4488, 2599, 3355, 7773, 22186, 27895, 9742, 726, 3330, 0, 2042], - [15056, 25994, 19589, 16743, 44198, 13078, 14967, 7552, 10422, 14935, 11891, 14002, 13225, 10671, 9475, 10633, 5084, 20315, 11866, 9802, 7682, 6471, 15720, 10674, 18908, 6204, 6000, 5066, 3039, 3721, 3496, 4772, 8614, 23805, 29519, 11614, 2749, 5313, 2042, 0], -] # yapf: disable + [ + 0, + 10938, + 4542, + 2835, + 29441, + 2171, + 1611, + 9208, + 9528, + 11111, + 16120, + 22606, + 22127, + 20627, + 21246, + 23387, + 16697, + 33609, + 26184, + 24772, + 22644, + 20655, + 30492, + 23296, + 32979, + 18141, + 19248, + 17129, + 17192, + 15645, + 12658, + 11210, + 12094, + 13175, + 18162, + 4968, + 12308, + 10084, + 13026, + 15056, + ], + [ + 10938, + 0, + 6422, + 9742, + 18988, + 12974, + 11216, + 19715, + 19004, + 18271, + 25070, + 31971, + 31632, + 30571, + 31578, + 33841, + 27315, + 43964, + 36944, + 35689, + 33569, + 31481, + 41360, + 33760, + 43631, + 28730, + 29976, + 27803, + 28076, + 26408, + 23504, + 22025, + 22000, + 13197, + 14936, + 15146, + 23246, + 20956, + 23963, + 25994, + ], + [ + 4542, + 6422, + 0, + 3644, + 25173, + 6552, + 5092, + 13584, + 13372, + 13766, + 19805, + 26537, + 26117, + 24804, + 25590, + 27784, + 21148, + 37981, + 30693, + 29315, + 27148, + 25071, + 34943, + 27472, + 37281, + 22389, + 23592, + 21433, + 21655, + 20011, + 17087, + 15612, + 15872, + 11653, + 15666, + 8842, + 16843, + 14618, + 17563, + 19589, + ], + [ + 2835, + 9742, + 3644, + 0, + 28681, + 3851, + 4341, + 11660, + 12294, + 13912, + 18893, + 25283, + 24777, + 23173, + 23636, + 25696, + 18950, + 35927, + 28233, + 26543, + 24127, + 21864, + 31765, + 24018, + 33904, + 19005, + 20295, + 18105, + 18551, + 16763, + 13958, + 12459, + 12296, + 10370, + 15331, + 5430, + 14044, + 12135, + 14771, + 16743, + ], + [ + 29441, + 18988, + 25173, + 28681, + 0, + 31590, + 29265, + 37173, + 35501, + 32929, + 40239, + 47006, + 46892, + 46542, + 48112, + 50506, + 44539, + 60103, + 54208, + 53557, + 51878, + 50074, + 59849, + 52645, + 62415, + 47544, + 48689, + 46560, + 46567, + 45086, + 42083, + 40648, + 40971, + 29929, + 28493, + 34015, + 41473, + 38935, + 42160, + 44198, + ], + [ + 2171, + 12974, + 6552, + 3851, + 31590, + 0, + 3046, + 7856, + 8864, + 11330, + 15411, + 21597, + 21065, + 19382, + 19791, + 21845, + 15099, + 32076, + 24425, + 22848, + 20600, + 18537, + 28396, + 21125, + 30825, + 15975, + 17101, + 14971, + 15104, + 13503, + 10544, + 9080, + 9983, + 13435, + 18755, + 2947, + 10344, + 8306, + 11069, + 13078, + ], + [ + 1611, + 11216, + 5092, + 4341, + 29265, + 3046, + 0, + 8526, + 8368, + 9573, + 14904, + 21529, + 21085, + 19719, + 20504, + 22713, + 16118, + 32898, + 25728, + 24541, + 22631, + 20839, + 30584, + 23755, + 33278, + 18557, + 19545, + 17490, + 17309, + 15936, + 12881, + 11498, + 12944, + 14711, + 19589, + 5993, + 12227, + 9793, + 12925, + 14967, + ], + [ + 9208, + 19715, + 13584, + 11660, + 37173, + 7856, + 8526, + 0, + 3248, + 7855, + 8245, + 13843, + 13272, + 11526, + 12038, + 14201, + 7599, + 24411, + 17259, + 16387, + 15050, + 13999, + 23134, + 17899, + 26460, + 12894, + 13251, + 11680, + 10455, + 9997, + 7194, + 6574, + 10678, + 20959, + 26458, + 8180, + 5255, + 2615, + 5730, + 7552, + ], + [ + 9528, + 19004, + 13372, + 12294, + 35501, + 8864, + 8368, + 3248, + 0, + 4626, + 6598, + 13168, + 12746, + 11567, + 12731, + 15083, + 9120, + 25037, + 18718, + 18433, + 17590, + 16888, + 25630, + 20976, + 29208, + 16055, + 16300, + 14838, + 13422, + 13165, + 10430, + 9813, + 13777, + 22300, + 27564, + 10126, + 8388, + 5850, + 8778, + 10422, + ], + [ + 11111, + 18271, + 13766, + 13912, + 32929, + 11330, + 9573, + 7855, + 4626, + 0, + 7318, + 14185, + 14005, + 13655, + 15438, + 17849, + 12839, + 27179, + 21947, + 22230, + 21814, + 21366, + 29754, + 25555, + 33535, + 20674, + 20872, + 19457, + 17961, + 17787, + 15048, + 14372, + 18115, + 24280, + 29101, + 13400, + 13008, + 10467, + 13375, + 14935, + ], + [ + 16120, + 25070, + 19805, + 18893, + 40239, + 15411, + 14904, + 8245, + 6598, + 7318, + 0, + 6939, + 6702, + 6498, + 8610, + 10961, + 7744, + 19889, + 15350, + 16403, + 16975, + 17517, + 24357, + 22176, + 28627, + 18093, + 17672, + 16955, + 14735, + 15510, + 13694, + 13768, + 18317, + 28831, + 34148, + 16326, + 11276, + 9918, + 11235, + 11891, + ], + [ + 22606, + 31971, + 26537, + 25283, + 47006, + 21597, + 21529, + 13843, + 13168, + 14185, + 6939, + 0, + 793, + 3401, + 5562, + 6839, + 8923, + 13433, + 11264, + 13775, + 15853, + 17629, + 21684, + 22315, + 26411, + 19539, + 18517, + 18636, + 16024, + 17632, + 16948, + 17587, + 22131, + 34799, + 40296, + 21953, + 14739, + 14568, + 14366, + 14002, + ], + [ + 22127, + 31632, + 26117, + 24777, + 46892, + 21065, + 21085, + 13272, + 12746, + 14005, + 6702, + 793, + 0, + 2608, + 4809, + 6215, + 8151, + 13376, + 10702, + 13094, + 15099, + 16845, + 21039, + 21535, + 25744, + 18746, + 17725, + 17845, + 15232, + 16848, + 16197, + 16859, + 21391, + 34211, + 39731, + 21345, + 14006, + 13907, + 13621, + 13225, + ], + [ + 20627, + 30571, + 24804, + 23173, + 46542, + 19382, + 19719, + 11526, + 11567, + 13655, + 6498, + 3401, + 2608, + 0, + 2556, + 4611, + 5630, + 13586, + 9157, + 11005, + 12681, + 14285, + 19044, + 18996, + 23644, + 16138, + 15126, + 15240, + 12625, + 14264, + 13736, + 14482, + 18958, + 32292, + 37879, + 19391, + 11621, + 11803, + 11188, + 10671, + ], + [ + 21246, + 31578, + 25590, + 23636, + 48112, + 19791, + 20504, + 12038, + 12731, + 15438, + 8610, + 5562, + 4809, + 2556, + 0, + 2411, + 4917, + 12395, + 6757, + 8451, + 10292, + 12158, + 16488, + 16799, + 21097, + 14374, + 13194, + 13590, + 10943, + 12824, + 12815, + 13779, + 18042, + 32259, + 37918, + 19416, + 10975, + 11750, + 10424, + 9475, + ], + [ + 23387, + 33841, + 27784, + 25696, + 50506, + 21845, + 22713, + 14201, + 15083, + 17849, + 10961, + 6839, + 6215, + 4611, + 2411, + 0, + 6760, + 10232, + 4567, + 7010, + 9607, + 12003, + 14846, + 16408, + 19592, + 14727, + 13336, + 14109, + 11507, + 13611, + 14104, + 15222, + 19237, + 34013, + 39703, + 21271, + 12528, + 13657, + 11907, + 10633, + ], + [ + 16697, + 27315, + 21148, + 18950, + 44539, + 15099, + 16118, + 7599, + 9120, + 12839, + 7744, + 8923, + 8151, + 5630, + 4917, + 6760, + 0, + 16982, + 9699, + 9400, + 9302, + 9823, + 16998, + 14534, + 21042, + 10911, + 10190, + 9900, + 7397, + 8758, + 8119, + 8948, + 13353, + 27354, + 33023, + 14542, + 6106, + 6901, + 5609, + 5084, + ], + [ + 33609, + 43964, + 37981, + 35927, + 60103, + 32076, + 32898, + 24411, + 25037, + 27179, + 19889, + 13433, + 13376, + 13586, + 12395, + 10232, + 16982, + 0, + 8843, + 12398, + 16193, + 19383, + 16423, + 22583, + 20997, + 22888, + 21194, + 22640, + 20334, + 22636, + 23801, + 25065, + 28675, + 44048, + 49756, + 31426, + 22528, + 23862, + 21861, + 20315, + ], + [ + 26184, + 36944, + 30693, + 28233, + 54208, + 24425, + 25728, + 17259, + 18718, + 21947, + 15350, + 11264, + 10702, + 9157, + 6757, + 4567, + 9699, + 8843, + 0, + 3842, + 7518, + 10616, + 10666, + 14237, + 15515, + 14053, + 12378, + 13798, + 11537, + 13852, + 15276, + 16632, + 19957, + 35660, + 41373, + 23361, + 14333, + 16125, + 13624, + 11866, + ], + [ + 24772, + 35689, + 29315, + 26543, + 53557, + 22848, + 24541, + 16387, + 18433, + 22230, + 16403, + 13775, + 13094, + 11005, + 8451, + 7010, + 9400, + 12398, + 3842, + 0, + 3795, + 7014, + 8053, + 10398, + 12657, + 10633, + 8889, + 10569, + 8646, + 10938, + 12906, + 14366, + 17106, + 33171, + 38858, + 21390, + 12507, + 14748, + 11781, + 9802, + ], + [ + 22644, + 33569, + 27148, + 24127, + 51878, + 20600, + 22631, + 15050, + 17590, + 21814, + 16975, + 15853, + 15099, + 12681, + 10292, + 9607, + 9302, + 16193, + 7518, + 3795, + 0, + 3250, + 8084, + 6873, + 11763, + 6949, + 5177, + 7050, + 5619, + 7730, + 10187, + 11689, + 13792, + 30012, + 35654, + 18799, + 10406, + 12981, + 9718, + 7682, + ], + [ + 20655, + 31481, + 25071, + 21864, + 50074, + 18537, + 20839, + 13999, + 16888, + 21366, + 17517, + 17629, + 16845, + 14285, + 12158, + 12003, + 9823, + 19383, + 10616, + 7014, + 3250, + 0, + 9901, + 4746, + 12531, + 3737, + 1961, + 4036, + 3588, + 5109, + 7996, + 9459, + 10846, + 27094, + 32690, + 16451, + 8887, + 11624, + 8304, + 6471, + ], + [ + 30492, + 41360, + 34943, + 31765, + 59849, + 28396, + 30584, + 23134, + 25630, + 29754, + 24357, + 21684, + 21039, + 19044, + 16488, + 14846, + 16998, + 16423, + 10666, + 8053, + 8084, + 9901, + 0, + 9363, + 4870, + 13117, + 11575, + 13793, + 13300, + 15009, + 17856, + 19337, + 20454, + 36551, + 42017, + 26352, + 18403, + 21033, + 17737, + 15720, + ], + [ + 23296, + 33760, + 27472, + 24018, + 52645, + 21125, + 23755, + 17899, + 20976, + 25555, + 22176, + 22315, + 21535, + 18996, + 16799, + 16408, + 14534, + 22583, + 14237, + 10398, + 6873, + 4746, + 9363, + 0, + 10020, + 5211, + 4685, + 6348, + 7636, + 8010, + 11074, + 12315, + 11926, + 27537, + 32880, + 18634, + 12644, + 15358, + 12200, + 10674, + ], + [ + 32979, + 43631, + 37281, + 33904, + 62415, + 30825, + 33278, + 26460, + 29208, + 33535, + 28627, + 26411, + 25744, + 23644, + 21097, + 19592, + 21042, + 20997, + 15515, + 12657, + 11763, + 12531, + 4870, + 10020, + 0, + 14901, + 13738, + 15855, + 16118, + 17348, + 20397, + 21793, + 21936, + 37429, + 42654, + 28485, + 21414, + 24144, + 20816, + 18908, + ], + [ + 18141, + 28730, + 22389, + 19005, + 47544, + 15975, + 18557, + 12894, + 16055, + 20674, + 18093, + 19539, + 18746, + 16138, + 14374, + 14727, + 10911, + 22888, + 14053, + 10633, + 6949, + 3737, + 13117, + 5211, + 14901, + 0, + 1777, + 1217, + 3528, + 2896, + 5892, + 7104, + 7338, + 23517, + 29068, + 13583, + 7667, + 10304, + 7330, + 6204, + ], + [ + 19248, + 29976, + 23592, + 20295, + 48689, + 17101, + 19545, + 13251, + 16300, + 20872, + 17672, + 18517, + 17725, + 15126, + 13194, + 13336, + 10190, + 21194, + 12378, + 8889, + 5177, + 1961, + 11575, + 4685, + 13738, + 1777, + 0, + 2217, + 2976, + 3610, + 6675, + 8055, + 8965, + 25197, + 30774, + 14865, + 8007, + 10742, + 7532, + 6000, + ], + [ + 17129, + 27803, + 21433, + 18105, + 46560, + 14971, + 17490, + 11680, + 14838, + 19457, + 16955, + 18636, + 17845, + 15240, + 13590, + 14109, + 9900, + 22640, + 13798, + 10569, + 7050, + 4036, + 13793, + 6348, + 15855, + 1217, + 2217, + 0, + 2647, + 1686, + 4726, + 6000, + 6810, + 23060, + 28665, + 12674, + 6450, + 9094, + 6117, + 5066, + ], + [ + 17192, + 28076, + 21655, + 18551, + 46567, + 15104, + 17309, + 10455, + 13422, + 17961, + 14735, + 16024, + 15232, + 12625, + 10943, + 11507, + 7397, + 20334, + 11537, + 8646, + 5619, + 3588, + 13300, + 7636, + 16118, + 3528, + 2976, + 2647, + 0, + 2320, + 4593, + 6093, + 8479, + 24542, + 30219, + 13194, + 5301, + 8042, + 4735, + 3039, + ], + [ + 15645, + 26408, + 20011, + 16763, + 45086, + 13503, + 15936, + 9997, + 13165, + 17787, + 15510, + 17632, + 16848, + 14264, + 12824, + 13611, + 8758, + 22636, + 13852, + 10938, + 7730, + 5109, + 15009, + 8010, + 17348, + 2896, + 3610, + 1686, + 2320, + 0, + 3086, + 4444, + 6169, + 22301, + 27963, + 11344, + 4780, + 7408, + 4488, + 3721, + ], + [ + 12658, + 23504, + 17087, + 13958, + 42083, + 10544, + 12881, + 7194, + 10430, + 15048, + 13694, + 16948, + 16197, + 13736, + 12815, + 14104, + 8119, + 23801, + 15276, + 12906, + 10187, + 7996, + 17856, + 11074, + 20397, + 5892, + 6675, + 4726, + 4593, + 3086, + 0, + 1501, + 5239, + 20390, + 26101, + 8611, + 2418, + 4580, + 2599, + 3496, + ], + [ + 11210, + 22025, + 15612, + 12459, + 40648, + 9080, + 11498, + 6574, + 9813, + 14372, + 13768, + 17587, + 16859, + 14482, + 13779, + 15222, + 8948, + 25065, + 16632, + 14366, + 11689, + 9459, + 19337, + 12315, + 21793, + 7104, + 8055, + 6000, + 6093, + 4444, + 1501, + 0, + 4608, + 19032, + 24747, + 7110, + 2860, + 4072, + 3355, + 4772, + ], + [ + 12094, + 22000, + 15872, + 12296, + 40971, + 9983, + 12944, + 10678, + 13777, + 18115, + 18317, + 22131, + 21391, + 18958, + 18042, + 19237, + 13353, + 28675, + 19957, + 17106, + 13792, + 10846, + 20454, + 11926, + 21936, + 7338, + 8965, + 6810, + 8479, + 6169, + 5239, + 4608, + 0, + 16249, + 21866, + 7146, + 7403, + 8446, + 7773, + 8614, + ], + [ + 13175, + 13197, + 11653, + 10370, + 29929, + 13435, + 14711, + 20959, + 22300, + 24280, + 28831, + 34799, + 34211, + 32292, + 32259, + 34013, + 27354, + 44048, + 35660, + 33171, + 30012, + 27094, + 36551, + 27537, + 37429, + 23517, + 25197, + 23060, + 24542, + 22301, + 20390, + 19032, + 16249, + 0, + 5714, + 12901, + 21524, + 20543, + 22186, + 23805, + ], + [ + 18162, + 14936, + 15666, + 15331, + 28493, + 18755, + 19589, + 26458, + 27564, + 29101, + 34148, + 40296, + 39731, + 37879, + 37918, + 39703, + 33023, + 49756, + 41373, + 38858, + 35654, + 32690, + 42017, + 32880, + 42654, + 29068, + 30774, + 28665, + 30219, + 27963, + 26101, + 24747, + 21866, + 5714, + 0, + 18516, + 27229, + 26181, + 27895, + 29519, + ], + [ + 4968, + 15146, + 8842, + 5430, + 34015, + 2947, + 5993, + 8180, + 10126, + 13400, + 16326, + 21953, + 21345, + 19391, + 19416, + 21271, + 14542, + 31426, + 23361, + 21390, + 18799, + 16451, + 26352, + 18634, + 28485, + 13583, + 14865, + 12674, + 13194, + 11344, + 8611, + 7110, + 7146, + 12901, + 18516, + 0, + 9029, + 7668, + 9742, + 11614, + ], + [ + 12308, + 23246, + 16843, + 14044, + 41473, + 10344, + 12227, + 5255, + 8388, + 13008, + 11276, + 14739, + 14006, + 11621, + 10975, + 12528, + 6106, + 22528, + 14333, + 12507, + 10406, + 8887, + 18403, + 12644, + 21414, + 7667, + 8007, + 6450, + 5301, + 4780, + 2418, + 2860, + 7403, + 21524, + 27229, + 9029, + 0, + 2747, + 726, + 2749, + ], + [ + 10084, + 20956, + 14618, + 12135, + 38935, + 8306, + 9793, + 2615, + 5850, + 10467, + 9918, + 14568, + 13907, + 11803, + 11750, + 13657, + 6901, + 23862, + 16125, + 14748, + 12981, + 11624, + 21033, + 15358, + 24144, + 10304, + 10742, + 9094, + 8042, + 7408, + 4580, + 4072, + 8446, + 20543, + 26181, + 7668, + 2747, + 0, + 3330, + 5313, + ], + [ + 13026, + 23963, + 17563, + 14771, + 42160, + 11069, + 12925, + 5730, + 8778, + 13375, + 11235, + 14366, + 13621, + 11188, + 10424, + 11907, + 5609, + 21861, + 13624, + 11781, + 9718, + 8304, + 17737, + 12200, + 20816, + 7330, + 7532, + 6117, + 4735, + 4488, + 2599, + 3355, + 7773, + 22186, + 27895, + 9742, + 726, + 3330, + 0, + 2042, + ], + [ + 15056, + 25994, + 19589, + 16743, + 44198, + 13078, + 14967, + 7552, + 10422, + 14935, + 11891, + 14002, + 13225, + 10671, + 9475, + 10633, + 5084, + 20315, + 11866, + 9802, + 7682, + 6471, + 15720, + 10674, + 18908, + 6204, + 6000, + 5066, + 3039, + 3721, + 3496, + 4772, + 8614, + 23805, + 29519, + 11614, + 2749, + 5313, + 2042, + 0, + ], +] # yapf: disable MAX_DISTANCE = 80_000 @@ -67,105 +1707,105 @@ # Create a console solution printer. def print_solution(manager, routing, assignment): - """Prints assignment on console.""" - print(f'Objective: {assignment.ObjectiveValue()}') - # Display dropped nodes. - dropped_nodes = 'Dropped nodes:' - for index in range(routing.Size()): - if routing.IsStart(index) or routing.IsEnd(index): - continue - if assignment.Value(routing.NextVar(index)) == index: - node = manager.IndexToNode(index) - dropped_nodes += f' {node}({VISIT_VALUES[node]})' - print(dropped_nodes) - # Display routes - total_distance = 0 - total_value_collected = 0 - for v in range(manager.GetNumberOfVehicles()): - index = routing.Start(v) - plan_output = f'Route for vehicle {v}:\n' - route_distance = 0 - value_collected = 0 - while not routing.IsEnd(index): - node = manager.IndexToNode(index) - value_collected += VISIT_VALUES[node] - plan_output += f' {node} ->' - previous_index = index - index = assignment.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle(previous_index, index, v) - plan_output += f' {manager.IndexToNode(index)}\n' - plan_output += f'Distance of the route: {route_distance}m\n' - plan_output += f'Value collected: {value_collected}\n' - print(plan_output) - total_distance += route_distance - total_value_collected += value_collected - print(f'Total Distance: {total_distance}m') - print(f'Total Value collected: {total_value_collected}/{sum(VISIT_VALUES)}') + """Prints assignment on console.""" + print(f'Objective: {assignment.ObjectiveValue()}') + # Display dropped nodes. + dropped_nodes = 'Dropped nodes:' + for index in range(routing.Size()): + if routing.IsStart(index) or routing.IsEnd(index): + continue + if assignment.Value(routing.NextVar(index)) == index: + node = manager.IndexToNode(index) + dropped_nodes += f' {node}({VISIT_VALUES[node]})' + print(dropped_nodes) + # Display routes + total_distance = 0 + total_value_collected = 0 + for v in range(manager.GetNumberOfVehicles()): + if not routing.IsVehicleUsed(assignment, v): + continue + index = routing.Start(v) + plan_output = f'Route for vehicle {v}:\n' + route_distance = 0 + value_collected = 0 + while not routing.IsEnd(index): + node = manager.IndexToNode(index) + value_collected += VISIT_VALUES[node] + plan_output += f' {node} ->' + previous_index = index + index = assignment.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle(previous_index, index, v) + plan_output += f' {manager.IndexToNode(index)}\n' + plan_output += f'Distance of the route: {route_distance}m\n' + plan_output += f'Value collected: {value_collected}\n' + print(plan_output) + total_distance += route_distance + total_value_collected += value_collected + print(f'Total Distance: {total_distance}m') + print(f'Total Value collected: {total_value_collected}/{sum(VISIT_VALUES)}') def main(): - """Entry point of the program.""" - num_nodes = len(DISTANCE_MATRIX) - print(f'Num nodes = {num_nodes}') - num_vehicles = 4 - depot = 0 - all_nodes = range(num_nodes) - - # Create the routing index manager. - manager = pywrapcp.RoutingIndexManager( - num_nodes, - num_vehicles, - depot) - - # Create routing model. - routing = pywrapcp.RoutingModel(manager) - - # Create and register a transit callback. - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return DISTANCE_MATRIX[from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - - # Define cost of each arc. - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - - # Limit Vehicle distance. - dimension_name = 'Distance' - routing.AddDimension( - transit_callback_index, - 0, # no slack - MAX_DISTANCE, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(1) - - # Allow to drop nodes. - for node in range(1, num_nodes): - routing.AddDisjunction( - [manager.NodeToIndex(node)], - VISIT_VALUES[node]) - - # Setting first solution heuristic. - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH) - search_parameters.time_limit.FromSeconds(15) - #search_parameters.log_search = True - - # Solve the problem. - assignment = routing.SolveWithParameters(search_parameters) - - # Print solution on console. - if assignment: - print_solution(manager, routing, assignment) + """Entry point of the program.""" + num_nodes = len(DISTANCE_MATRIX) + print(f'Num nodes = {num_nodes}') + num_vehicles = 4 + depot = 0 + all_nodes = range(num_nodes) + + # Create the routing index manager. + manager = pywraprouting.RoutingIndexManager(num_nodes, num_vehicles, depot) + + # Create routing model. + routing = pywraprouting.RoutingModel(manager) + + # Create and register a transit callback. + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return DISTANCE_MATRIX[from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + + # Define cost of each arc. + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + + # Limit Vehicle distance. + dimension_name = 'Distance' + routing.AddDimension( + transit_callback_index, + 0, # no slack + MAX_DISTANCE, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(1) + + # Allow to drop nodes. + for node in range(1, num_nodes): + routing.AddDisjunction([manager.NodeToIndex(node)], VISIT_VALUES[node]) + + # Setting first solution heuristic. + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(15) + # search_parameters.log_search = True + + # Solve the problem. + assignment = routing.SolveWithParameters(search_parameters) + + # Print solution on console. + if assignment: + print_solution(manager, routing, assignment) if __name__ == '__main__': - main() + main() diff --git a/examples/python/prize_collecting_vrp_sat.py b/examples/python/prize_collecting_vrp_sat.py index 210c5aeb652..373030df357 100644 --- a/examples/python/prize_collecting_vrp_sat.py +++ b/examples/python/prize_collecting_vrp_sat.py @@ -78,127 +78,128 @@ def print_solution( num_nodes: int, num_vehicles: int, ) -> None: - """Prints solution on console.""" - # Display dropped nodes. - dropped_nodes = "Dropped nodes:" - for node in range(num_nodes): - if node == 0: - continue - is_visited = sum( - [solver.boolean_value(visited_nodes[v][node]) for v in range(num_vehicles)] - ) - if not is_visited: - dropped_nodes += f" {node}({VISIT_VALUES[node]})" - print(dropped_nodes) - # Display routes - total_distance = 0 - total_value_collected = 0 - for v in range(num_vehicles): - current_node = 0 - plan_output = f"Route for vehicle {v}:\n" - route_distance = 0 - value_collected = 0 - route_is_finished = False - while not route_is_finished: - value_collected += VISIT_VALUES[current_node] - plan_output += f" {current_node} ->" - # find next node - for node in range(num_nodes): - if node == current_node: - continue - if solver.boolean_value(used_arcs[v][current_node, node]): - route_distance += DISTANCE_MATRIX[current_node][node] - current_node = node - if current_node == 0: - route_is_finished = True - break - plan_output += f" {current_node}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - plan_output += f"value collected: {value_collected}\n" - print(plan_output) - total_distance += route_distance - total_value_collected += value_collected - print(f"Total Distance: {total_distance}m") - print(f"Total value collected: {total_value_collected}/{sum(VISIT_VALUES)}") + """Prints solution on console.""" + # Display dropped nodes. + dropped_nodes = "Dropped nodes:" + for node in range(num_nodes): + if node == 0: + continue + is_visited = sum([ + solver.boolean_value(visited_nodes[v][node]) + for v in range(num_vehicles) + ]) + if not is_visited: + dropped_nodes += f" {node}({VISIT_VALUES[node]})" + print(dropped_nodes) + # Display routes + total_distance = 0 + total_value_collected = 0 + for v in range(num_vehicles): + current_node = 0 + plan_output = f"Route for vehicle {v}:\n" + route_distance = 0 + value_collected = 0 + route_is_finished = False + while not route_is_finished: + value_collected += VISIT_VALUES[current_node] + plan_output += f" {current_node} ->" + # find next node + for node in range(num_nodes): + if node == current_node: + continue + if solver.boolean_value(used_arcs[v][current_node, node]): + route_distance += DISTANCE_MATRIX[current_node][node] + current_node = node + if current_node == 0: + route_is_finished = True + break + plan_output += f" {current_node}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + plan_output += f"value collected: {value_collected}\n" + print(plan_output) + total_distance += route_distance + total_value_collected += value_collected + print(f"Total Distance: {total_distance}m") + print(f"Total value collected: {total_value_collected}/{sum(VISIT_VALUES)}") def prize_collecting_vrp(): - """Entry point of the program.""" - num_nodes = len(DISTANCE_MATRIX) - num_vehicles = 4 - print(f"Num nodes = {num_nodes}") - - # Model. - model = cp_model.CpModel() - - obj_vars = [] - obj_coeffs = [] - visited_nodes = {} - used_arcs = {} - - # Create the circuit constraint. - all_nodes = range(num_nodes) - for v in range(num_vehicles): - visited_nodes[v] = [] - used_arcs[v] = {} - arcs = [] - for i in all_nodes: - is_visited = model.new_bool_var(f"{i} is visited") - arcs.append((i, i, ~is_visited)) - - obj_vars.append(is_visited) - obj_coeffs.append(VISIT_VALUES[i]) - visited_nodes[v].append(is_visited) - - for j in all_nodes: - if i == j: - used_arcs[v][i, j] = ~is_visited - continue - arc_is_used = model.new_bool_var(f"{j} follows {i}") - arcs.append((i, j, arc_is_used)) - - obj_vars.append(arc_is_used) - obj_coeffs.append(-DISTANCE_MATRIX[i][j]) - used_arcs[v][i, j] = arc_is_used - - model.add_circuit(arcs) - - # Node 0 must be visited. - model.add(visited_nodes[v][0] == 1) - - # limit the route distance - model.add( - sum( - used_arcs[v][i, j] * DISTANCE_MATRIX[i][j] - for i in all_nodes - for j in all_nodes - ) - <= MAX_DISTANCE + """Entry point of the program.""" + num_nodes = len(DISTANCE_MATRIX) + num_vehicles = 4 + print(f"Num nodes = {num_nodes}") + + # Model. + model = cp_model.CpModel() + + obj_vars = [] + obj_coeffs = [] + visited_nodes = {} + used_arcs = {} + + # Create the circuit constraint. + all_nodes = range(num_nodes) + for v in range(num_vehicles): + visited_nodes[v] = [] + used_arcs[v] = {} + arcs = [] + for i in all_nodes: + is_visited = model.new_bool_var(f"{i} is visited") + arcs.append((i, i, ~is_visited)) + + obj_vars.append(is_visited) + obj_coeffs.append(VISIT_VALUES[i]) + visited_nodes[v].append(is_visited) + + for j in all_nodes: + if i == j: + used_arcs[v][i, j] = ~is_visited + continue + arc_is_used = model.new_bool_var(f"{j} follows {i}") + arcs.append((i, j, arc_is_used)) + + obj_vars.append(arc_is_used) + obj_coeffs.append(-DISTANCE_MATRIX[i][j]) + used_arcs[v][i, j] = arc_is_used + + model.add_circuit(arcs) + + # Node 0 must be visited. + model.add(visited_nodes[v][0] == 1) + + # limit the route distance + model.add( + sum( + used_arcs[v][i, j] * DISTANCE_MATRIX[i][j] + for i in all_nodes + for j in all_nodes ) + <= MAX_DISTANCE + ) - # Each node is visited at most once - for node in range(1, num_nodes): - model.add_at_most_one([visited_nodes[v][node] for v in range(num_vehicles)]) + # Each node is visited at most once + for node in range(1, num_nodes): + model.add_at_most_one([visited_nodes[v][node] for v in range(num_vehicles)]) - # Maximize visited node values minus the travelled distance. - model.maximize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) + # Maximize visited node values minus the travelled distance. + model.maximize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) - # Solve and print out the solution. - solver = cp_model.CpSolver() - solver.parameters.num_search_workers = 8 - solver.parameters.max_time_in_seconds = 15.0 - solver.parameters.log_search_progress = True + # Solve and print out the solution. + solver = cp_model.CpSolver() + solver.parameters.num_search_workers = 8 + solver.parameters.max_time_in_seconds = 15.0 + solver.parameters.log_search_progress = True - status = solver.solve(model) - if status == cp_model.FEASIBLE or status == cp_model.OPTIMAL: - print_solution(solver, visited_nodes, used_arcs, num_nodes, num_vehicles) + status = solver.solve(model) + if status == cp_model.FEASIBLE or status == cp_model.OPTIMAL: + print_solution(solver, visited_nodes, used_arcs, num_nodes, num_vehicles) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - prize_collecting_vrp() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + prize_collecting_vrp() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/proto_solve.py b/examples/python/proto_solve.py index 523d2f2c202..491774ea4c4 100644 --- a/examples/python/proto_solve.py +++ b/examples/python/proto_solve.py @@ -1,4 +1,3 @@ - from absl import app from absl import flags from ortools.linear_solver.python import model_builder @@ -6,34 +5,38 @@ FLAGS = flags.FLAGS _INPUT = flags.DEFINE_string('input', '', 'Input file to load and solve.') -_PARAMS = flags.DEFINE_string('params', '', 'Solver parameters in string format.') -_SOLVER = flags.DEFINE_string('solver', 'sat', 'Solver type to solve the model with.') +_PARAMS = flags.DEFINE_string( + 'params', '', 'Solver parameters in string format.' +) +_SOLVER = flags.DEFINE_string( + 'solver', 'sat', 'Solver type to solve the model with.' +) def main(_): - model = model_builder.ModelBuilder() + model = model_builder.ModelBuilder() - # Load MPS file. - if not model.import_from_mps_file(_INPUT.value): - print(f'Cannot import MPS file: \'{_INPUT.value}\'') - return + # Load MPS file. + if not model.import_from_mps_file(_INPUT.value): + print(f"Cannot import MPS file: '{_INPUT.value}'") + return - # Create solver. - solver = model_builder.ModelSolver(_SOLVER.value) - if not solver.solver_is_supported(): - print(f'Cannot create solver with name \'{_SOLVER.value}\'') - return + # Create solver. + solver = model_builder.ModelSolver(_SOLVER.value) + if not solver.solver_is_supported(): + print(f"Cannot create solver with name '{_SOLVER.value}'") + return - # Set parameters. - if _PARAMS.value: - solver.set_solver_specific_parameters(_PARAMS.value) + # Set parameters. + if _PARAMS.value: + solver.set_solver_specific_parameters(_PARAMS.value) - # Enable the output of the solver. - solver.enable_output(True) + # Enable the output of the solver. + solver.enable_output(True) - # And solve. - solver.solve(model) + # And solve. + solver.solve(model) if __name__ == '__main__': - app.run(main) + app.run(main) diff --git a/examples/python/pyflow_example.py b/examples/python/pyflow_example.py index 55db850530f..293ca4d0cf6 100644 --- a/examples/python/pyflow_example.py +++ b/examples/python/pyflow_example.py @@ -21,67 +21,72 @@ def max_flow_api(): - """MaxFlow simple interface example.""" - print("MaxFlow on a simple network.") - tails = [0, 0, 0, 0, 1, 2, 3, 3, 4] - heads = [1, 2, 3, 4, 3, 4, 4, 5, 5] - capacities = [5, 8, 5, 3, 4, 5, 6, 6, 4] - expected_total_flow = 10 - smf = max_flow.SimpleMaxFlow() - for i in range(0, len(tails)): - smf.add_arc_with_capacity(tails[i], heads[i], capacities[i]) - if smf.solve(0, 5) == smf.OPTIMAL: - print("Total flow", smf.optimal_flow(), "/", expected_total_flow) - for i in range(smf.num_arcs()): - print( - "From source %d to target %d: %d / %d" - % (smf.tail(i), smf.head(i), smf.flow(i), smf.capacity(i)) - ) - print("Source side min-cut:", smf.get_source_side_min_cut()) - print("Sink side min-cut:", smf.get_sink_side_min_cut()) - else: - print("There was an issue with the max flow input.") + """MaxFlow simple interface example.""" + print("MaxFlow on a simple network.") + tails = [0, 0, 0, 0, 1, 2, 3, 3, 4] + heads = [1, 2, 3, 4, 3, 4, 4, 5, 5] + capacities = [5, 8, 5, 3, 4, 5, 6, 6, 4] + expected_total_flow = 10 + smf = max_flow.SimpleMaxFlow() + for i in range(0, len(tails)): + smf.add_arc_with_capacity(tails[i], heads[i], capacities[i]) + if smf.solve(0, 5) == smf.OPTIMAL: + print("Total flow", smf.optimal_flow(), "/", expected_total_flow) + for i in range(smf.num_arcs()): + print( + "From source %d to target %d: %d / %d" + % (smf.tail(i), smf.head(i), smf.flow(i), smf.capacity(i)) + ) + print("Source side min-cut:", smf.get_source_side_min_cut()) + print("Sink side min-cut:", smf.get_sink_side_min_cut()) + else: + print("There was an issue with the max flow input.") def min_cost_flow_api(): - """MinCostFlow simple interface example. + """MinCostFlow simple interface example. - Note that this example is actually a linear sum assignment example and will - be more efficiently solved with the pywrapgraph.LinearSumAssignment class. - """ - print("MinCostFlow on 4x4 matrix.") - num_sources = 4 - num_targets = 4 - costs = [[90, 75, 75, 80], [35, 85, 55, 65], [125, 95, 90, 105], [45, 110, 95, 115]] - expected_cost = 275 - smcf = min_cost_flow.SimpleMinCostFlow() - for source in range(0, num_sources): - for target in range(0, num_targets): - smcf.add_arc_with_capacity_and_unit_cost( - source, num_sources + target, 1, costs[source][target] - ) - for node in range(0, num_sources): - smcf.set_node_supply(node, 1) - smcf.set_node_supply(num_sources + node, -1) - status = smcf.solve() - if status == smcf.OPTIMAL: - print("Total flow", smcf.optimal_cost(), "/", expected_cost) - for i in range(0, smcf.num_arcs()): - if smcf.flow(i) > 0: - print( - "From source %d to target %d: cost %d" - % (smcf.tail(i), smcf.head(i) - num_sources, smcf.unit_cost(i)) - ) - else: - print("There was an issue with the min cost flow input.") + Note that this example is actually a linear sum assignment example and will + be more efficiently solved with the pywrapgraph.LinearSumAssignment class. + """ + print("MinCostFlow on 4x4 matrix.") + num_sources = 4 + num_targets = 4 + costs = [ + [90, 75, 75, 80], + [35, 85, 55, 65], + [125, 95, 90, 105], + [45, 110, 95, 115], + ] + expected_cost = 275 + smcf = min_cost_flow.SimpleMinCostFlow() + for source in range(0, num_sources): + for target in range(0, num_targets): + smcf.add_arc_with_capacity_and_unit_cost( + source, num_sources + target, 1, costs[source][target] + ) + for node in range(0, num_sources): + smcf.set_node_supply(node, 1) + smcf.set_node_supply(num_sources + node, -1) + status = smcf.solve() + if status == smcf.OPTIMAL: + print("Total flow", smcf.optimal_cost(), "/", expected_cost) + for i in range(0, smcf.num_arcs()): + if smcf.flow(i) > 0: + print( + "From source %d to target %d: cost %d" + % (smcf.tail(i), smcf.head(i) - num_sources, smcf.unit_cost(i)) + ) + else: + print("There was an issue with the min cost flow input.") def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - max_flow_api() - min_cost_flow_api() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + max_flow_api() + min_cost_flow_api() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/qubo_sat.py b/examples/python/qubo_sat.py index fb54b9d5c9f..724014ca7d3 100644 --- a/examples/python/qubo_sat.py +++ b/examples/python/qubo_sat.py @@ -653,53 +653,53 @@ def solve_qubo() -> None: - """solve the Qubo problem.""" + """solve the Qubo problem.""" - # Build the model. - model = cp_model.CpModel() + # Build the model. + model = cp_model.CpModel() - num_vars = len(RAW_DATA) - all_vars = range(num_vars) - variables = [model.new_bool_var("x_%i" % i) for i in all_vars] + num_vars = len(RAW_DATA) + all_vars = range(num_vars) + variables = [model.new_bool_var("x_%i" % i) for i in all_vars] - obj_vars = [] - obj_coeffs = [] + obj_vars = [] + obj_coeffs = [] - for i in range(num_vars - 1): - x_i = variables[i] - for j in range(i + 1, num_vars): - coeff = RAW_DATA[i][j] + RAW_DATA[j][i] - if coeff == 0.0: - continue - x_j = variables[j] - var = model.new_bool_var("") - model.add_bool_or([~x_i, ~x_j, var]) - model.add_implication(var, x_i) - model.add_implication(var, x_j) - obj_vars.append(var) - obj_coeffs.append(coeff) + for i in range(num_vars - 1): + x_i = variables[i] + for j in range(i + 1, num_vars): + coeff = RAW_DATA[i][j] + RAW_DATA[j][i] + if coeff == 0.0: + continue + x_j = variables[j] + var = model.new_bool_var("") + model.add_bool_or([~x_i, ~x_j, var]) + model.add_implication(var, x_i) + model.add_implication(var, x_j) + obj_vars.append(var) + obj_coeffs.append(coeff) - for i in all_vars: - self_coeff = RAW_DATA[i][i] + RAW_DATA[i][-1] - if self_coeff != 0.0: - obj_vars.append(variables[i]) - obj_coeffs.append(self_coeff) + for i in all_vars: + self_coeff = RAW_DATA[i][i] + RAW_DATA[i][-1] + if self_coeff != 0.0: + obj_vars.append(variables[i]) + obj_coeffs.append(self_coeff) - model.minimize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) + model.minimize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) - ### Solve model. - solver = cp_model.CpSolver() - solver.parameters.num_search_workers = 16 - solver.parameters.log_search_progress = True - solver.parameters.max_time_in_seconds = 30 - solver.solve(model) + ### Solve model. + solver = cp_model.CpSolver() + solver.parameters.num_search_workers = 16 + solver.parameters.log_search_progress = True + solver.parameters.max_time_in_seconds = 30 + solver.solve(model) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - solve_qubo() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + solve_qubo() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/random_tsp.py b/examples/python/random_tsp.py index 701152fad08..60177a68fba 100755 --- a/examples/python/random_tsp.py +++ b/examples/python/random_tsp.py @@ -12,22 +12,22 @@ # limitations under the License. """Traveling Salesman Sample. - This is a sample using the routing library python wrapper to solve a - Traveling Salesman Problem. - The description of the problem can be found here: - http://en.wikipedia.org/wiki/Travelling_salesman_problem. - The optimization engine uses local search to improve solutions, first - solutions being generated using a cheapest addition heuristic. - Optionally one can randomly forbid a set of random connections between nodes - (forbidden arcs). +This is a sample using the routing library python wrapper to solve a +Traveling Salesman Problem. +The description of the problem can be found here: +http://en.wikipedia.org/wiki/Travelling_salesman_problem. +The optimization engine uses local search to improve solutions, first +solutions being generated using a cheapest addition heuristic. +Optionally one can randomly forbid a set of random connections between nodes +(forbidden arcs). """ import argparse from functools import partial import random -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting parser = argparse.ArgumentParser() @@ -35,114 +35,117 @@ '--tsp_size', default=10, type=int, - help='Size of Traveling Salesman Problem instance.') + help='Size of Traveling Salesman Problem instance.', +) parser.add_argument( '--tsp_use_random_matrix', default=True, type=bool, - help='Use random cost matrix.') + help='Use random cost matrix.', +) parser.add_argument( '--tsp_random_forbidden_connections', default=0, type=int, - help='Number of random forbidden connections.') + help='Number of random forbidden connections.', +) parser.add_argument( - '--tsp_random_seed', default=0, type=int, help='Random seed.') + '--tsp_random_seed', default=0, type=int, help='Random seed.' +) # Cost/distance functions. def Distance(manager, i, j): - """Sample function.""" - # Put your distance code here. - node_i = manager.IndexToNode(i) - node_j = manager.IndexToNode(j) - return node_i + node_j + """Sample function.""" + # Put your distance code here. + node_i = manager.IndexToNode(i) + node_j = manager.IndexToNode(j) + return node_i + node_j class RandomMatrix(object): - """Random matrix.""" - - def __init__(self, size, seed): - """Initialize random matrix.""" - - rand = random.Random() - rand.seed(seed) - distance_max = 100 - self.matrix = {} - for from_node in range(size): - self.matrix[from_node] = {} - for to_node in range(size): - if from_node == to_node: - self.matrix[from_node][to_node] = 0 - else: - self.matrix[from_node][to_node] = rand.randrange( - distance_max) + """Random matrix.""" + + def __init__(self, size, seed): + """Initialize random matrix.""" + + rand = random.Random() + rand.seed(seed) + distance_max = 100 + self.matrix = {} + for from_node in range(size): + self.matrix[from_node] = {} + for to_node in range(size): + if from_node == to_node: + self.matrix[from_node][to_node] = 0 + else: + self.matrix[from_node][to_node] = rand.randrange(distance_max) - def Distance(self, manager, from_index, to_index): - return self.matrix[manager.IndexToNode(from_index)][manager.IndexToNode( - to_index)] + def Distance(self, manager, from_index, to_index): + return self.matrix[manager.IndexToNode(from_index)][ + manager.IndexToNode(to_index) + ] def main(args): - # Create routing model - if args.tsp_size > 0: - # TSP of size args.tsp_size - # Second argument = 1 to build a single tour (it's a TSP). - # Nodes are indexed from 0 to args_tsp_size - 1, by default the start of - # the route is node 0. - manager = pywrapcp.RoutingIndexManager(args.tsp_size, 1, 0) - routing = pywrapcp.RoutingModel(manager) - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - # Setting first solution heuristic (cheapest addition). - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC) - - # Setting the cost function. - # Put a callback to the distance accessor here. The callback takes two - # arguments (the from and to node indices) and returns the distance between - # these indices. - cost = 0 - if args.tsp_use_random_matrix: - matrix = RandomMatrix(args.tsp_size, args.tsp_random_seed) - cost = routing.RegisterTransitCallback( - partial(matrix.Distance, manager)) - else: - cost = routing.RegisterTransitCallback(partial(Distance, manager)) - routing.SetArcCostEvaluatorOfAllVehicles(cost) - # Forbid node connections (randomly). - rand = random.Random() - rand.seed(args.tsp_random_seed) - forbidden_connections = 0 - while forbidden_connections < args.tsp_random_forbidden_connections: - from_node = rand.randrange(args.tsp_size - 1) - to_node = rand.randrange(args.tsp_size - 1) + 1 - if routing.NextVar(from_node).Contains(to_node): - print('Forbidding connection ' + str(from_node) + ' -> ' + - str(to_node)) - routing.NextVar(from_node).RemoveValue(to_node) - forbidden_connections += 1 - - # Solve, returns a solution if any. - assignment = routing.Solve() - if assignment: - # Solution cost. - print(assignment.ObjectiveValue()) - # Inspect solution. - # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1 - route_number = 0 - node = routing.Start(route_number) - route = '' - while not routing.IsEnd(node): - route += str(node) + ' -> ' - node = assignment.Value(routing.NextVar(node)) - route += '0' - print(route) - else: - print('No solution found.') + # Create routing model + if args.tsp_size > 0: + # TSP of size args.tsp_size + # Second argument = 1 to build a single tour (it's a TSP). + # Nodes are indexed from 0 to args_tsp_size - 1, by default the start of + # the route is node 0. + manager = pywraprouting.RoutingIndexManager(args.tsp_size, 1, 0) + routing = pywraprouting.RoutingModel(manager) + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + # Setting first solution heuristic (cheapest addition). + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + + # Setting the cost function. + # Put a callback to the distance accessor here. The callback takes two + # arguments (the from and to node indices) and returns the distance between + # these indices. + cost = 0 + if args.tsp_use_random_matrix: + matrix = RandomMatrix(args.tsp_size, args.tsp_random_seed) + cost = routing.RegisterTransitCallback(partial(matrix.Distance, manager)) + else: + cost = routing.RegisterTransitCallback(partial(Distance, manager)) + routing.SetArcCostEvaluatorOfAllVehicles(cost) + # Forbid node connections (randomly). + rand = random.Random() + rand.seed(args.tsp_random_seed) + forbidden_connections = 0 + while forbidden_connections < args.tsp_random_forbidden_connections: + from_node = rand.randrange(args.tsp_size - 1) + to_node = rand.randrange(args.tsp_size - 1) + 1 + if routing.NextVar(from_node).Contains(to_node): + print('Forbidding connection ' + str(from_node) + ' -> ' + str(to_node)) + routing.NextVar(from_node).RemoveValue(to_node) + forbidden_connections += 1 + + # Solve, returns a solution if any. + assignment = routing.Solve() + if assignment: + # Solution cost. + print(assignment.ObjectiveValue()) + # Inspect solution. + # Only one route here; otherwise iterate from 0 to routing.vehicles() - 1 + route_number = 0 + node = routing.Start(route_number) + route = '' + while not routing.IsEnd(node): + route += str(node) + ' -> ' + node = assignment.Value(routing.NextVar(node)) + route += '0' + print(route) else: - print('Specify an instance greater than 0.') + print('No solution found.') + else: + print('Specify an instance greater than 0.') if __name__ == '__main__': - main(parser.parse_args()) + main(parser.parse_args()) diff --git a/examples/python/rcpsp_sat.py b/examples/python/rcpsp_sat.py index 2b78e3d049a..6c7199de0dc 100644 --- a/examples/python/rcpsp_sat.py +++ b/examples/python/rcpsp_sat.py @@ -45,48 +45,50 @@ def print_problem_statistics(problem: rcpsp_pb2.RcpspProblem): - """Display various statistics on the problem.""" - - # Determine problem type. - problem_type = ( - "Resource Investment Problem" if problem.is_resource_investment else "RCPSP" - ) - - num_resources = len(problem.resources) - num_tasks = len(problem.tasks) - 2 # 2 sentinels. - tasks_with_alternatives = 0 - variable_duration_tasks = 0 - tasks_with_delay = 0 - - for task in problem.tasks: - if len(task.recipes) > 1: - tasks_with_alternatives += 1 - duration_0 = task.recipes[0].duration - for recipe in task.recipes: - if recipe.duration != duration_0: - variable_duration_tasks += 1 - break - if task.successor_delays: - tasks_with_delay += 1 - - if problem.is_rcpsp_max: - problem_type += "/Max delay" - # We print 2 less tasks as these are sentinel tasks that are not counted in - # the description of the rcpsp models. - if problem.is_consumer_producer: - print(f"Solving {problem_type} with:") - print(f" - {num_resources} reservoir resources") - print(f" - {num_tasks} tasks") - else: - print(f"Solving {problem_type} with:") - print(f" - {num_resources} renewable resources") - print(f" - {num_tasks} tasks") - if tasks_with_alternatives: - print(f" - {tasks_with_alternatives} tasks with alternative resources") - if variable_duration_tasks: - print(f" - {variable_duration_tasks} tasks with variable durations") - if tasks_with_delay: - print(f" - {tasks_with_delay} tasks with successor delays") + """Display various statistics on the problem.""" + + # Determine problem type. + problem_type = ( + "Resource Investment Problem" + if problem.is_resource_investment + else "RCPSP" + ) + + num_resources = len(problem.resources) + num_tasks = len(problem.tasks) - 2 # 2 sentinels. + tasks_with_alternatives = 0 + variable_duration_tasks = 0 + tasks_with_delay = 0 + + for task in problem.tasks: + if len(task.recipes) > 1: + tasks_with_alternatives += 1 + duration_0 = task.recipes[0].duration + for recipe in task.recipes: + if recipe.duration != duration_0: + variable_duration_tasks += 1 + break + if task.successor_delays: + tasks_with_delay += 1 + + if problem.is_rcpsp_max: + problem_type += "/Max delay" + # We print 2 less tasks as these are sentinel tasks that are not counted in + # the description of the rcpsp models. + if problem.is_consumer_producer: + print(f"Solving {problem_type} with:") + print(f" - {num_resources} reservoir resources") + print(f" - {num_tasks} tasks") + else: + print(f"Solving {problem_type} with:") + print(f" - {num_resources} renewable resources") + print(f" - {num_tasks} tasks") + if tasks_with_alternatives: + print(f" - {tasks_with_alternatives} tasks with alternative resources") + if variable_duration_tasks: + print(f" - {variable_duration_tasks} tasks with variable durations") + if tasks_with_delay: + print(f" - {tasks_with_delay} tasks with successor delays") def solve_rcpsp( @@ -97,305 +99,307 @@ def solve_rcpsp( source: int, sink: int, ) -> None: - """Parse and solve a given RCPSP problem in proto format. - - The model will only look at the tasks {source} + {sink} + active_tasks, and - ignore all others. - - Args: - problem: the description of the model to solve in protobuf format - proto_file: the name of the file to export the CpModel proto to. - params: the string representation of the parameters to pass to the sat - solver. - active_tasks: the set of active tasks to consider. - source: the source task in the graph. Its end will be forced to 0. - sink: the sink task of the graph. Its start is the makespan of the problem. - - Returns: - (lower_bound of the objective, best solution found, assignment) - """ - # Create the model. - model = cp_model.CpModel() - model.name = problem.name - - num_resources = len(problem.resources) - - all_active_tasks = list(active_tasks) - all_active_tasks.sort() - all_resources = range(num_resources) - - horizon = problem.deadline if problem.deadline != -1 else problem.horizon - if _HORIZON.value > 0: - horizon = _HORIZON.value - elif horizon == -1: # Naive computation. - horizon = sum(max(r.duration for r in t.recipes) for t in problem.tasks) - if problem.is_rcpsp_max: - for t in problem.tasks: - for sd in t.successor_delays: - for rd in sd.recipe_delays: - for d in rd.min_delays: - horizon += abs(d) - print(f"Horizon = {horizon}", flush=True) - - # Containers. - task_starts = {} - task_ends = {} - task_durations = {} - task_intervals = {} - task_resource_to_energy = {} - task_to_resource_demands = collections.defaultdict(list) - - task_to_presence_literals = collections.defaultdict(list) - task_to_recipe_durations = collections.defaultdict(list) - task_resource_to_fixed_demands = collections.defaultdict(dict) - task_resource_to_max_energy = collections.defaultdict(int) - - resource_to_sum_of_demand_max = collections.defaultdict(int) - - # Create task variables. - for t in all_active_tasks: - task = problem.tasks[t] - num_recipes = len(task.recipes) - all_recipes = range(num_recipes) - - start_var = model.new_int_var(0, horizon, f"start_of_task_{t}") - end_var = model.new_int_var(0, horizon, f"end_of_task_{t}") + """Parse and solve a given RCPSP problem in proto format. + + The model will only look at the tasks {source} + {sink} + active_tasks, and + ignore all others. + + Args: + problem: the description of the model to solve in protobuf format + proto_file: the name of the file to export the CpModel proto to. + params: the string representation of the parameters to pass to the sat + solver. + active_tasks: the set of active tasks to consider. + source: the source task in the graph. Its end will be forced to 0. + sink: the sink task of the graph. Its start is the makespan of the problem. + + Returns: + (lower_bound of the objective, best solution found, assignment) + """ + # Create the model. + model = cp_model.CpModel() + model.name = problem.name + + num_resources = len(problem.resources) + + all_active_tasks = list(active_tasks) + all_active_tasks.sort() + all_resources = range(num_resources) + + horizon = problem.deadline if problem.deadline != -1 else problem.horizon + if _HORIZON.value > 0: + horizon = _HORIZON.value + elif horizon == -1: # Naive computation. + horizon = sum(max(r.duration for r in t.recipes) for t in problem.tasks) + if problem.is_rcpsp_max: + for t in problem.tasks: + for sd in t.successor_delays: + for rd in sd.recipe_delays: + for d in rd.min_delays: + horizon += abs(d) + print(f"Horizon = {horizon}", flush=True) + + # Containers. + task_starts = {} + task_ends = {} + task_durations = {} + task_intervals = {} + task_resource_to_energy = {} + task_to_resource_demands = collections.defaultdict(list) + + task_to_presence_literals = collections.defaultdict(list) + task_to_recipe_durations = collections.defaultdict(list) + task_resource_to_fixed_demands = collections.defaultdict(dict) + task_resource_to_max_energy = collections.defaultdict(int) + + resource_to_sum_of_demand_max = collections.defaultdict(int) + + # Create task variables. + for t in all_active_tasks: + task = problem.tasks[t] + num_recipes = len(task.recipes) + all_recipes = range(num_recipes) + + start_var = model.new_int_var(0, horizon, f"start_of_task_{t}") + end_var = model.new_int_var(0, horizon, f"end_of_task_{t}") + + if num_recipes > 1: + # Create one literal per recipe. + literals = [ + model.new_bool_var(f"is_present_{t}_{r}") for r in all_recipes + ] + + # Exactly one recipe must be performed. + model.add_exactly_one(literals) - if num_recipes > 1: - # Create one literal per recipe. - literals = [model.new_bool_var(f"is_present_{t}_{r}") for r in all_recipes] + else: + literals = [1] - # Exactly one recipe must be performed. - model.add_exactly_one(literals) + # Temporary data structure to fill in 0 demands. + demand_matrix = collections.defaultdict(int) - else: - literals = [1] + # Scan recipes and build the demand matrix and the vector of durations. + for recipe_index, recipe in enumerate(task.recipes): + task_to_recipe_durations[t].append(recipe.duration) + for demand, resource in zip(recipe.demands, recipe.resources): + demand_matrix[(resource, recipe_index)] = demand - # Temporary data structure to fill in 0 demands. - demand_matrix = collections.defaultdict(int) + # Create the duration variable from the accumulated durations. + duration_var = model.new_int_var_from_domain( + cp_model.Domain.from_values(task_to_recipe_durations[t]), + f"duration_of_task_{t}", + ) - # Scan recipes and build the demand matrix and the vector of durations. - for recipe_index, recipe in enumerate(task.recipes): - task_to_recipe_durations[t].append(recipe.duration) - for demand, resource in zip(recipe.demands, recipe.resources): - demand_matrix[(resource, recipe_index)] = demand + # Link the recipe literals and the duration_var. + for r in range(num_recipes): + model.add(duration_var == task_to_recipe_durations[t][r]).only_enforce_if( + literals[r] + ) - # Create the duration variable from the accumulated durations. - duration_var = model.new_int_var_from_domain( - cp_model.Domain.from_values(task_to_recipe_durations[t]), - f"duration_of_task_{t}", - ) + # Create the interval of the task. + task_interval = model.new_interval_var( + start_var, duration_var, end_var, f"task_interval_{t}" + ) - # Link the recipe literals and the duration_var. - for r in range(num_recipes): - model.add(duration_var == task_to_recipe_durations[t][r]).only_enforce_if( - literals[r] - ) + # Store task variables. + task_starts[t] = start_var + task_ends[t] = end_var + task_durations[t] = duration_var + task_intervals[t] = task_interval + task_to_presence_literals[t] = literals - # Create the interval of the task. - task_interval = model.new_interval_var( - start_var, duration_var, end_var, f"task_interval_{t}" + # Create the demand variable of the task for each resource. + for res in all_resources: + demands = [demand_matrix[(res, recipe)] for recipe in all_recipes] + task_resource_to_fixed_demands[(t, res)] = demands + demand_var = model.new_int_var_from_domain( + cp_model.Domain.from_values(demands), f"demand_{t}_{res}" + ) + task_to_resource_demands[t].append(demand_var) + + # Link the recipe literals and the demand_var. + for r in all_recipes: + model.add(demand_var == demand_matrix[(res, r)]).only_enforce_if( + literals[r] ) - # Store task variables. - task_starts[t] = start_var - task_ends[t] = end_var - task_durations[t] = duration_var - task_intervals[t] = task_interval - task_to_presence_literals[t] = literals - - # Create the demand variable of the task for each resource. - for res in all_resources: - demands = [demand_matrix[(res, recipe)] for recipe in all_recipes] - task_resource_to_fixed_demands[(t, res)] = demands - demand_var = model.new_int_var_from_domain( - cp_model.Domain.from_values(demands), f"demand_{t}_{res}" - ) - task_to_resource_demands[t].append(demand_var) - - # Link the recipe literals and the demand_var. - for r in all_recipes: - model.add(demand_var == demand_matrix[(res, r)]).only_enforce_if( - literals[r] - ) - - resource_to_sum_of_demand_max[res] += max(demands) - - # Create the energy expression for (task, resource): - for res in all_resources: - task_resource_to_energy[(t, res)] = sum( - literals[r] - * task_to_recipe_durations[t][r] - * task_resource_to_fixed_demands[(t, res)][r] - for r in all_recipes - ) - task_resource_to_max_energy[(t, res)] = max( - task_to_recipe_durations[t][r] - * task_resource_to_fixed_demands[(t, res)][r] - for r in all_recipes - ) - - # Create makespan variable - makespan = model.new_int_var(0, horizon, "makespan") - makespan_size = model.new_int_var(1, horizon, "interval_makespan_size") - interval_makespan = model.new_interval_var( - makespan, - makespan_size, - model.new_constant(horizon + 1), - "interval_makespan", - ) + resource_to_sum_of_demand_max[res] += max(demands) - # Add precedences. - if problem.is_rcpsp_max: - # In RCPSP/Max problem, precedences are given and max delay (possible - # negative) between the starts of two tasks. - for task_id in all_active_tasks: - task = problem.tasks[task_id] - num_modes = len(task.recipes) - - for successor_index, next_id in enumerate(task.successors): - delay_matrix = task.successor_delays[successor_index] - num_next_modes = len(problem.tasks[next_id].recipes) - for m1 in range(num_modes): - s1 = task_starts[task_id] - p1 = task_to_presence_literals[task_id][m1] - if next_id == sink: - delay = delay_matrix.recipe_delays[m1].min_delays[0] - model.add(s1 + delay <= makespan).only_enforce_if(p1) - else: - for m2 in range(num_next_modes): - delay = delay_matrix.recipe_delays[m1].min_delays[m2] - s2 = task_starts[next_id] - p2 = task_to_presence_literals[next_id][m2] - model.add(s1 + delay <= s2).only_enforce_if([p1, p2]) - else: - # Normal dependencies (task ends before the start of successors). - for t in all_active_tasks: - for n in problem.tasks[t].successors: - if n == sink: - model.add(task_ends[t] <= makespan) - elif n in active_tasks: - model.add(task_ends[t] <= task_starts[n]) - - # Containers for resource investment problems. - capacities = [] # Capacity variables for all resources. - max_cost = 0 # Upper bound on the investment cost. - - # Create resources. + # Create the energy expression for (task, resource): for res in all_resources: - resource = problem.resources[res] - c = resource.max_capacity - if c == -1: - print(f"No capacity: {resource}") - c = resource_to_sum_of_demand_max[res] - - # RIP problems have only renewable resources, and no makespan. - if problem.is_resource_investment or resource.renewable: - intervals = [task_intervals[t] for t in all_active_tasks] - demands = [task_to_resource_demands[t][res] for t in all_active_tasks] - - if problem.is_resource_investment: - capacity = model.new_int_var(0, c, f"capacity_of_{res}") - model.add_cumulative(intervals, demands, capacity) - capacities.append(capacity) - max_cost += c * resource.unit_cost - else: # Standard renewable resource. - if _USE_INTERVAL_MAKESPAN.value: - intervals.append(interval_makespan) - demands.append(c) - - model.add_cumulative(intervals, demands, c) - else: # Non empty non renewable resource. (single mode only) - if problem.is_consumer_producer: - reservoir_starts = [] - reservoir_demands = [] - for t in all_active_tasks: - if task_resource_to_fixed_demands[(t, res)][0]: - reservoir_starts.append(task_starts[t]) - reservoir_demands.append( - task_resource_to_fixed_demands[(t, res)][0] - ) - model.add_reservoir_constraint( - reservoir_starts, - reservoir_demands, - resource.min_capacity, - resource.max_capacity, - ) - else: # No producer-consumer. We just sum the demands. - model.add( - cp_model.LinearExpr.sum( - [task_to_resource_demands[t][res] for t in all_active_tasks] - ) - <= c - ) - - # Objective. - if problem.is_resource_investment: - objective = model.new_int_var(0, max_cost, "capacity_costs") + task_resource_to_energy[(t, res)] = sum( + literals[r] + * task_to_recipe_durations[t][r] + * task_resource_to_fixed_demands[(t, res)][r] + for r in all_recipes + ) + task_resource_to_max_energy[(t, res)] = max( + task_to_recipe_durations[t][r] + * task_resource_to_fixed_demands[(t, res)][r] + for r in all_recipes + ) + + # Create makespan variable + makespan = model.new_int_var(0, horizon, "makespan") + makespan_size = model.new_int_var(1, horizon, "interval_makespan_size") + interval_makespan = model.new_interval_var( + makespan, + makespan_size, + model.new_constant(horizon + 1), + "interval_makespan", + ) + + # Add precedences. + if problem.is_rcpsp_max: + # In RCPSP/Max problem, precedences are given and max delay (possible + # negative) between the starts of two tasks. + for task_id in all_active_tasks: + task = problem.tasks[task_id] + num_modes = len(task.recipes) + + for successor_index, next_id in enumerate(task.successors): + delay_matrix = task.successor_delays[successor_index] + num_next_modes = len(problem.tasks[next_id].recipes) + for m1 in range(num_modes): + s1 = task_starts[task_id] + p1 = task_to_presence_literals[task_id][m1] + if next_id == sink: + delay = delay_matrix.recipe_delays[m1].min_delays[0] + model.add(s1 + delay <= makespan).only_enforce_if(p1) + else: + for m2 in range(num_next_modes): + delay = delay_matrix.recipe_delays[m1].min_delays[m2] + s2 = task_starts[next_id] + p2 = task_to_presence_literals[next_id][m2] + model.add(s1 + delay <= s2).only_enforce_if([p1, p2]) + else: + # Normal dependencies (task ends before the start of successors). + for t in all_active_tasks: + for n in problem.tasks[t].successors: + if n == sink: + model.add(task_ends[t] <= makespan) + elif n in active_tasks: + model.add(task_ends[t] <= task_starts[n]) + + # Containers for resource investment problems. + capacities = [] # Capacity variables for all resources. + max_cost = 0 # Upper bound on the investment cost. + + # Create resources. + for res in all_resources: + resource = problem.resources[res] + c = resource.max_capacity + if c == -1: + print(f"No capacity: {resource}") + c = resource_to_sum_of_demand_max[res] + + # RIP problems have only renewable resources, and no makespan. + if problem.is_resource_investment or resource.renewable: + intervals = [task_intervals[t] for t in all_active_tasks] + demands = [task_to_resource_demands[t][res] for t in all_active_tasks] + + if problem.is_resource_investment: + capacity = model.new_int_var(0, c, f"capacity_of_{res}") + model.add_cumulative(intervals, demands, capacity) + capacities.append(capacity) + max_cost += c * resource.unit_cost + else: # Standard renewable resource. + if _USE_INTERVAL_MAKESPAN.value: + intervals.append(interval_makespan) + demands.append(c) + + model.add_cumulative(intervals, demands, c) + else: # Non empty non renewable resource. (single mode only) + if problem.is_consumer_producer: + reservoir_starts = [] + reservoir_demands = [] + for t in all_active_tasks: + if task_resource_to_fixed_demands[(t, res)][0]: + reservoir_starts.append(task_starts[t]) + reservoir_demands.append( + task_resource_to_fixed_demands[(t, res)][0] + ) + model.add_reservoir_constraint( + reservoir_starts, + reservoir_demands, + resource.min_capacity, + resource.max_capacity, + ) + else: # No producer-consumer. We just sum the demands. model.add( - objective - == sum( - problem.resources[i].unit_cost * capacities[i] - for i in range(len(capacities)) + cp_model.LinearExpr.sum( + [task_to_resource_demands[t][res] for t in all_active_tasks] ) + <= c ) - else: - objective = makespan - model.minimize(objective) + # Objective. + if problem.is_resource_investment: + objective = model.new_int_var(0, max_cost, "capacity_costs") + model.add( + objective + == sum( + problem.resources[i].unit_cost * capacities[i] + for i in range(len(capacities)) + ) + ) + else: + objective = makespan - # Add sentinels. - task_starts[source] = 0 - task_ends[source] = 0 - task_to_presence_literals[0].append(True) - task_starts[sink] = makespan - task_to_presence_literals[sink].append(True) + model.minimize(objective) - # Write model to file. - if proto_file: - print(f"Writing proto to{proto_file}") - model.export_to_file(proto_file) + # Add sentinels. + task_starts[source] = 0 + task_ends[source] = 0 + task_to_presence_literals[0].append(True) + task_starts[sink] = makespan + task_to_presence_literals[sink].append(True) - # Solve model. - solver = cp_model.CpSolver() + # Write model to file. + if proto_file: + print(f"Writing proto to{proto_file}") + model.export_to_file(proto_file) - # Parse user specified parameters. - if params: - text_format.Parse(params, solver.parameters) + # Solve model. + solver = cp_model.CpSolver() - # Favor objective_shaving over objective_lb_search. - if solver.parameters.num_workers >= 16 and solver.parameters.num_workers < 24: - solver.parameters.ignore_subsolvers.append("objective_lb_search") - solver.parameters.extra_subsolvers.append("objective_shaving") + # Parse user specified parameters. + if params: + text_format.Parse(params, solver.parameters) - # Experimental: Specify the fact that the objective is a makespan - solver.parameters.push_all_tasks_toward_start = True + # Favor objective_shaving over objective_lb_search. + if solver.parameters.num_workers >= 16 and solver.parameters.num_workers < 24: + solver.parameters.ignore_subsolvers.append("objective_lb_search") + solver.parameters.extra_subsolvers.append("objective_shaving") - # Enable logging in the main solve. - solver.parameters.log_search_progress = True + # Experimental: Specify the fact that the objective is a makespan + solver.parameters.push_all_tasks_toward_start = True - # Solve the model. - solver.solve(model) + # Enable logging in the main solve. + solver.parameters.log_search_progress = True + + # Solve the model. + solver.solve(model) def main(_): - rcpsp_parser = rcpsp.RcpspParser() - rcpsp_parser.parse_file(_INPUT.value) + rcpsp_parser = rcpsp.RcpspParser() + rcpsp_parser.parse_file(_INPUT.value) - problem = rcpsp_parser.problem() - print_problem_statistics(problem) + problem = rcpsp_parser.problem() + print_problem_statistics(problem) - last_task = len(problem.tasks) - 1 + last_task = len(problem.tasks) - 1 - solve_rcpsp( - problem=problem, - proto_file=_OUTPUT_PROTO.value, - params=_PARAMS.value, - active_tasks=set(range(1, last_task)), - source=0, - sink=last_task, - ) + solve_rcpsp( + problem=problem, + proto_file=_OUTPUT_PROTO.value, + params=_PARAMS.value, + active_tasks=set(range(1, last_task)), + source=0, + sink=last_task, + ) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/reallocate_sat.py b/examples/python/reallocate_sat.py index d159a5a96c9..3146f8c0660 100644 --- a/examples/python/reallocate_sat.py +++ b/examples/python/reallocate_sat.py @@ -19,115 +19,115 @@ def main(): - # Data - data_0 = [ - [107, 107, 107, 0, 0], # pr1 - [0, 47, 47, 47, 0], # pr2 - [10, 10, 10, 0, 0], # pr3 - [0, 55, 55, 55, 55], # pr4 - ] - - data_1 = [ - [119444030, 0, 0, 0], - [34585586, 38358559, 31860661, 0], - [19654655, 21798799, 18106106, 0], - [298836792, 0, 0, 0], - [3713428, 4118530, 4107277, 3072018], - [6477273, 7183884, 5358471, 0], - [1485371, 1647412, 1642911, 1228807], - ] - - data_2 = [ - [1194440, 0, 0, 0], - [345855, 383585, 318606, 0], - [196546, 217987, 181061, 0], - [2988367, 0, 0, 0], - [37134, 41185, 41072, 30720], - [64772, 71838, 53584, 0], - [14853, 16474, 16429, 12288], - ] - - pr = data_0 - - num_pr = len(pr) - num_years = len(pr[1]) - total = sum(pr[p][y] for p in range(num_pr) for y in range(num_years)) - avg = total // num_years - - # Model - model = cp_model.CpModel() - - # Variables - delta = model.NewIntVar(0, total, "delta") - - contributions_per_years = collections.defaultdict(list) - contributions_per_prs = collections.defaultdict(list) - all_contribs = {} - - for p, inner_l in enumerate(pr): - for y, item in enumerate(inner_l): - if item != 0: - contrib = model.NewIntVar(0, total, "r%d c%d" % (p, y)) - contributions_per_years[y].append(contrib) - contributions_per_prs[p].append(contrib) - all_contribs[p, y] = contrib - - year_var = [model.NewIntVar(0, total, "y[%i]" % i) for i in range(num_years)] - - # Constraints - - # Maintain year_var. - for y in range(num_years): - model.Add(year_var[y] == sum(contributions_per_years[y])) - - # Fixed contributions per pr. + # Data + data_0 = [ + [107, 107, 107, 0, 0], # pr1 + [0, 47, 47, 47, 0], # pr2 + [10, 10, 10, 0, 0], # pr3 + [0, 55, 55, 55, 55], # pr4 + ] + + data_1 = [ + [119444030, 0, 0, 0], + [34585586, 38358559, 31860661, 0], + [19654655, 21798799, 18106106, 0], + [298836792, 0, 0, 0], + [3713428, 4118530, 4107277, 3072018], + [6477273, 7183884, 5358471, 0], + [1485371, 1647412, 1642911, 1228807], + ] + + data_2 = [ + [1194440, 0, 0, 0], + [345855, 383585, 318606, 0], + [196546, 217987, 181061, 0], + [2988367, 0, 0, 0], + [37134, 41185, 41072, 30720], + [64772, 71838, 53584, 0], + [14853, 16474, 16429, 12288], + ] + + pr = data_0 + + num_pr = len(pr) + num_years = len(pr[1]) + total = sum(pr[p][y] for p in range(num_pr) for y in range(num_years)) + avg = total // num_years + + # Model + model = cp_model.CpModel() + + # Variables + delta = model.NewIntVar(0, total, "delta") + + contributions_per_years = collections.defaultdict(list) + contributions_per_prs = collections.defaultdict(list) + all_contribs = {} + + for p, inner_l in enumerate(pr): + for y, item in enumerate(inner_l): + if item != 0: + contrib = model.NewIntVar(0, total, "r%d c%d" % (p, y)) + contributions_per_years[y].append(contrib) + contributions_per_prs[p].append(contrib) + all_contribs[p, y] = contrib + + year_var = [model.NewIntVar(0, total, "y[%i]" % i) for i in range(num_years)] + + # Constraints + + # Maintain year_var. + for y in range(num_years): + model.Add(year_var[y] == sum(contributions_per_years[y])) + + # Fixed contributions per pr. + for p in range(num_pr): + model.Add(sum(pr[p]) == sum(contributions_per_prs[p])) + + # Link delta with variables. + for y in range(num_years): + model.Add(year_var[y] >= avg - delta) + + for y in range(num_years): + model.Add(year_var[y] <= avg + delta) + + # Solve and output + model.Minimize(delta) + + # Solve model. + solver = cp_model.CpSolver() + status = solver.Solve(model) + + # Output solution. + if status == cp_model.OPTIMAL: + print("Data") + print(" - total = ", total) + print(" - year_average = ", avg) + print(" - number of projects = ", num_pr) + print(" - number of years = ", num_years) + + print(" - input production") for p in range(num_pr): - model.Add(sum(pr[p]) == sum(contributions_per_prs[p])) - - # Link delta with variables. - for y in range(num_years): - model.Add(year_var[y] >= avg - delta) + for y in range(num_years): + if pr[p][y] == 0: + print(" ", end="") + else: + print("%10i" % pr[p][y], end="") + print() + + print("Solution") + for p in range(num_pr): + for y in range(num_years): + if pr[p][y] == 0: + print(" ", end="") + else: + print("%10i" % solver.Value(all_contribs[p, y]), end="") + print() for y in range(num_years): - model.Add(year_var[y] <= avg + delta) - - # Solve and output - model.Minimize(delta) - - # Solve model. - solver = cp_model.CpSolver() - status = solver.Solve(model) - - # Output solution. - if status == cp_model.OPTIMAL: - print("Data") - print(" - total = ", total) - print(" - year_average = ", avg) - print(" - number of projects = ", num_pr) - print(" - number of years = ", num_years) - - print(" - input production") - for p in range(num_pr): - for y in range(num_years): - if pr[p][y] == 0: - print(" ", end="") - else: - print("%10i" % pr[p][y], end="") - print() - - print("Solution") - for p in range(num_pr): - for y in range(num_years): - if pr[p][y] == 0: - print(" ", end="") - else: - print("%10i" % solver.Value(all_contribs[p, y]), end="") - print() - - for y in range(num_years): - print("%10i" % solver.Value(year_var[y]), end="") - print() + print("%10i" % solver.Value(year_var[y]), end="") + print() if __name__ == "__main__": - main() + main() diff --git a/examples/python/shift_scheduling_sat.py b/examples/python/shift_scheduling_sat.py index a81c0839910..eb51466ed04 100644 --- a/examples/python/shift_scheduling_sat.py +++ b/examples/python/shift_scheduling_sat.py @@ -31,32 +31,32 @@ def negated_bounded_span( works: list[cp_model.BoolVarT], start: int, length: int ) -> list[cp_model.BoolVarT]: - """Filters an isolated sub-sequence of variables assigned to True. - - Extract the span of Boolean variables [start, start + length), negate them, - and if there is variables to the left/right of this span, surround the span by - them in non negated form. - - Args: - works: a list of variables to extract the span from. - start: the start to the span. - length: the length of the span. - - Returns: - a list of variables which conjunction will be false if the sub-list is - assigned to True, and correctly bounded by variables assigned to False, - or by the start or end of works. - """ - sequence = [] - # left border (start of works, or works[start - 1]) - if start > 0: - sequence.append(works[start - 1]) - for i in range(length): - sequence.append(~works[start + i]) - # right border (end of works or works[start + length]) - if start + length < len(works): - sequence.append(works[start + length]) - return sequence + """Filters an isolated sub-sequence of variables assigned to True. + + Extract the span of Boolean variables [start, start + length), negate them, + and if there is variables to the left/right of this span, surround the span by + them in non negated form. + + Args: + works: a list of variables to extract the span from. + start: the start to the span. + length: the length of the span. + + Returns: + a list of variables which conjunction will be false if the sub-list is + assigned to True, and correctly bounded by variables assigned to False, + or by the start or end of works. + """ + sequence = [] + # left border (start of works, or works[start - 1]) + if start > 0: + sequence.append(works[start - 1]) + for i in range(length): + sequence.append(~works[start + i]) + # right border (end of works or works[start + length]) + if start + length < len(works): + sequence.append(works[start + length]) + return sequence def add_soft_sequence_constraint( @@ -70,72 +70,72 @@ def add_soft_sequence_constraint( max_cost: int, prefix: str, ) -> tuple[list[cp_model.BoolVarT], list[int]]: - """Sequence constraint on true variables with soft and hard bounds. - - This constraint look at every maximal contiguous sequence of variables - assigned to true. If forbids sequence of length < hard_min or > hard_max. - Then it creates penalty terms if the length is < soft_min or > soft_max. - - Args: - model: the sequence constraint is built on this model. - works: a list of Boolean variables. - hard_min: any sequence of true variables must have a length of at least - hard_min. - soft_min: any sequence should have a length of at least soft_min, or a - linear penalty on the delta will be added to the objective. - min_cost: the coefficient of the linear penalty if the length is less than - soft_min. - soft_max: any sequence should have a length of at most soft_max, or a linear - penalty on the delta will be added to the objective. - hard_max: any sequence of true variables must have a length of at most - hard_max. - max_cost: the coefficient of the linear penalty if the length is more than - soft_max. - prefix: a base name for penalty literals. - - Returns: - a tuple (variables_list, coefficient_list) containing the different - penalties created by the sequence constraint. - """ - cost_literals = [] - cost_coefficients = [] - - # Forbid sequences that are too short. - for length in range(1, hard_min): - for start in range(len(works) - length + 1): - model.add_bool_or(negated_bounded_span(works, start, length)) - - # Penalize sequences that are below the soft limit. - if min_cost > 0: - for length in range(hard_min, soft_min): - for start in range(len(works) - length + 1): - span = negated_bounded_span(works, start, length) - name = f": under_span(start={start}, length={length})" - lit = model.new_bool_var(prefix + name) - span.append(lit) - model.add_bool_or(span) - cost_literals.append(lit) - # We filter exactly the sequence with a short length. - # The penalty is proportional to the delta with soft_min. - cost_coefficients.append(min_cost * (soft_min - length)) - - # Penalize sequences that are above the soft limit. - if max_cost > 0: - for length in range(soft_max + 1, hard_max + 1): - for start in range(len(works) - length + 1): - span = negated_bounded_span(works, start, length) - name = f": over_span(start={start}, length={length})" - lit = model.new_bool_var(prefix + name) - span.append(lit) - model.add_bool_or(span) - cost_literals.append(lit) - # Cost paid is max_cost * excess length. - cost_coefficients.append(max_cost * (length - soft_max)) - - # Just forbid any sequence of true variables with length hard_max + 1 - for start in range(len(works) - hard_max): - model.add_bool_or([~works[i] for i in range(start, start + hard_max + 1)]) - return cost_literals, cost_coefficients + """Sequence constraint on true variables with soft and hard bounds. + + This constraint look at every maximal contiguous sequence of variables + assigned to true. If forbids sequence of length < hard_min or > hard_max. + Then it creates penalty terms if the length is < soft_min or > soft_max. + + Args: + model: the sequence constraint is built on this model. + works: a list of Boolean variables. + hard_min: any sequence of true variables must have a length of at least + hard_min. + soft_min: any sequence should have a length of at least soft_min, or a + linear penalty on the delta will be added to the objective. + min_cost: the coefficient of the linear penalty if the length is less than + soft_min. + soft_max: any sequence should have a length of at most soft_max, or a linear + penalty on the delta will be added to the objective. + hard_max: any sequence of true variables must have a length of at most + hard_max. + max_cost: the coefficient of the linear penalty if the length is more than + soft_max. + prefix: a base name for penalty literals. + + Returns: + a tuple (variables_list, coefficient_list) containing the different + penalties created by the sequence constraint. + """ + cost_literals = [] + cost_coefficients = [] + + # Forbid sequences that are too short. + for length in range(1, hard_min): + for start in range(len(works) - length + 1): + model.add_bool_or(negated_bounded_span(works, start, length)) + + # Penalize sequences that are below the soft limit. + if min_cost > 0: + for length in range(hard_min, soft_min): + for start in range(len(works) - length + 1): + span = negated_bounded_span(works, start, length) + name = f": under_span(start={start}, length={length})" + lit = model.new_bool_var(prefix + name) + span.append(lit) + model.add_bool_or(span) + cost_literals.append(lit) + # We filter exactly the sequence with a short length. + # The penalty is proportional to the delta with soft_min. + cost_coefficients.append(min_cost * (soft_min - length)) + + # Penalize sequences that are above the soft limit. + if max_cost > 0: + for length in range(soft_max + 1, hard_max + 1): + for start in range(len(works) - length + 1): + span = negated_bounded_span(works, start, length) + name = f": over_span(start={start}, length={length})" + lit = model.new_bool_var(prefix + name) + span.append(lit) + model.add_bool_or(span) + cost_literals.append(lit) + # Cost paid is max_cost * excess length. + cost_coefficients.append(max_cost * (length - soft_max)) + + # Just forbid any sequence of true variables with length hard_max + 1 + for start in range(len(works) - hard_max): + model.add_bool_or([~works[i] for i in range(start, start + hard_max + 1)]) + return cost_literals, cost_coefficients def add_soft_sum_constraint( @@ -149,309 +149,312 @@ def add_soft_sum_constraint( max_cost: int, prefix: str, ) -> tuple[list[cp_model.IntVar], list[int]]: - """sum constraint with soft and hard bounds. - - This constraint counts the variables assigned to true from works. - If forbids sum < hard_min or > hard_max. - Then it creates penalty terms if the sum is < soft_min or > soft_max. - - Args: - model: the sequence constraint is built on this model. - works: a list of Boolean variables. - hard_min: any sequence of true variables must have a sum of at least - hard_min. - soft_min: any sequence should have a sum of at least soft_min, or a linear - penalty on the delta will be added to the objective. - min_cost: the coefficient of the linear penalty if the sum is less than - soft_min. - soft_max: any sequence should have a sum of at most soft_max, or a linear - penalty on the delta will be added to the objective. - hard_max: any sequence of true variables must have a sum of at most - hard_max. - max_cost: the coefficient of the linear penalty if the sum is more than - soft_max. - prefix: a base name for penalty variables. - - Returns: - a tuple (variables_list, coefficient_list) containing the different - penalties created by the sequence constraint. - """ - cost_variables = [] - cost_coefficients = [] - sum_var = model.new_int_var(hard_min, hard_max, "") - # This adds the hard constraints on the sum. - model.add(sum_var == sum(works)) - - # Penalize sums below the soft_min target. - if soft_min > hard_min and min_cost > 0: - delta = model.new_int_var(-len(works), len(works), "") - model.add(delta == soft_min - sum_var) - # TODO(user): Compare efficiency with only excess >= soft_min - sum_var. - excess = model.new_int_var(0, 7, prefix + ": under_sum") - model.add_max_equality(excess, [delta, 0]) - cost_variables.append(excess) - cost_coefficients.append(min_cost) - - # Penalize sums above the soft_max target. - if soft_max < hard_max and max_cost > 0: - delta = model.new_int_var(-7, 7, "") - model.add(delta == sum_var - soft_max) - excess = model.new_int_var(0, 7, prefix + ": over_sum") - model.add_max_equality(excess, [delta, 0]) - cost_variables.append(excess) - cost_coefficients.append(max_cost) - - return cost_variables, cost_coefficients + """sum constraint with soft and hard bounds. + + This constraint counts the variables assigned to true from works. + If forbids sum < hard_min or > hard_max. + Then it creates penalty terms if the sum is < soft_min or > soft_max. + + Args: + model: the sequence constraint is built on this model. + works: a list of Boolean variables. + hard_min: any sequence of true variables must have a sum of at least + hard_min. + soft_min: any sequence should have a sum of at least soft_min, or a linear + penalty on the delta will be added to the objective. + min_cost: the coefficient of the linear penalty if the sum is less than + soft_min. + soft_max: any sequence should have a sum of at most soft_max, or a linear + penalty on the delta will be added to the objective. + hard_max: any sequence of true variables must have a sum of at most + hard_max. + max_cost: the coefficient of the linear penalty if the sum is more than + soft_max. + prefix: a base name for penalty variables. + + Returns: + a tuple (variables_list, coefficient_list) containing the different + penalties created by the sequence constraint. + """ + cost_variables = [] + cost_coefficients = [] + sum_var = model.new_int_var(hard_min, hard_max, "") + # This adds the hard constraints on the sum. + model.add(sum_var == sum(works)) + + # Penalize sums below the soft_min target. + if soft_min > hard_min and min_cost > 0: + delta = model.new_int_var(-len(works), len(works), "") + model.add(delta == soft_min - sum_var) + # TODO(user): Compare efficiency with only excess >= soft_min - sum_var. + excess = model.new_int_var(0, 7, prefix + ": under_sum") + model.add_max_equality(excess, [delta, 0]) + cost_variables.append(excess) + cost_coefficients.append(min_cost) + + # Penalize sums above the soft_max target. + if soft_max < hard_max and max_cost > 0: + delta = model.new_int_var(-7, 7, "") + model.add(delta == sum_var - soft_max) + excess = model.new_int_var(0, 7, prefix + ": over_sum") + model.add_max_equality(excess, [delta, 0]) + cost_variables.append(excess) + cost_coefficients.append(max_cost) + + return cost_variables, cost_coefficients def solve_shift_scheduling(params: str, output_proto: str): - """Solves the shift scheduling problem.""" - # Data - num_employees = 8 - num_weeks = 3 - shifts = ["O", "M", "A", "N"] - - # Fixed assignment: (employee, shift, day). - # This fixes the first 2 days of the schedule. - fixed_assignments = [ - (0, 0, 0), - (1, 0, 0), - (2, 1, 0), - (3, 1, 0), - (4, 2, 0), - (5, 2, 0), - (6, 2, 3), - (7, 3, 0), - (0, 1, 1), - (1, 1, 1), - (2, 2, 1), - (3, 2, 1), - (4, 2, 1), - (5, 0, 1), - (6, 0, 1), - (7, 3, 1), - ] - - # Request: (employee, shift, day, weight) - # A negative weight indicates that the employee desire this assignment. - requests = [ - # Employee 3 does not want to work on the first Saturday (negative weight - # for the Off shift). - (3, 0, 5, -2), - # Employee 5 wants a night shift on the second Thursday (negative weight). - (5, 3, 10, -2), - # Employee 2 does not want a night shift on the first Friday (positive - # weight). - (2, 3, 4, 4), - ] - - # Shift constraints on continuous sequence : - # (shift, hard_min, soft_min, min_penalty, - # soft_max, hard_max, max_penalty) - shift_constraints = [ - # One or two consecutive days of rest, this is a hard constraint. - (0, 1, 1, 0, 2, 2, 0), - # between 2 and 3 consecutive days of night shifts, 1 and 4 are - # possible but penalized. - (3, 1, 2, 20, 3, 4, 5), - ] - - # Weekly sum constraints on shifts days: - # (shift, hard_min, soft_min, min_penalty, - # soft_max, hard_max, max_penalty) - weekly_sum_constraints = [ - # Constraints on rests per week. - (0, 1, 2, 7, 2, 3, 4), - # At least 1 night shift per week (penalized). At most 4 (hard). - (3, 0, 1, 3, 4, 4, 0), - ] - - # Penalized transitions: - # (previous_shift, next_shift, penalty (0 means forbidden)) - penalized_transitions = [ - # Afternoon to night has a penalty of 4. - (2, 3, 4), - # Night to morning is forbidden. - (3, 1, 0), - ] - - # daily demands for work shifts (morning, afternoon, night) for each day - # of the week starting on Monday. - weekly_cover_demands = [ - (2, 3, 1), # Monday - (2, 3, 1), # Tuesday - (2, 2, 2), # Wednesday - (2, 3, 1), # Thursday - (2, 2, 2), # Friday - (1, 2, 3), # Saturday - (1, 3, 1), # Sunday - ] - - # Penalty for exceeding the cover constraint per shift type. - excess_cover_penalties = (2, 2, 5) - - num_days = num_weeks * 7 - num_shifts = len(shifts) - - model = cp_model.CpModel() - - work = {} + """Solves the shift scheduling problem.""" + # Data + num_employees = 8 + num_weeks = 3 + shifts = ["O", "M", "A", "N"] + + # Fixed assignment: (employee, shift, day). + # This fixes the first 2 days of the schedule. + fixed_assignments = [ + (0, 0, 0), + (1, 0, 0), + (2, 1, 0), + (3, 1, 0), + (4, 2, 0), + (5, 2, 0), + (6, 2, 3), + (7, 3, 0), + (0, 1, 1), + (1, 1, 1), + (2, 2, 1), + (3, 2, 1), + (4, 2, 1), + (5, 0, 1), + (6, 0, 1), + (7, 3, 1), + ] + + # Request: (employee, shift, day, weight) + # A negative weight indicates that the employee desire this assignment. + requests = [ + # Employee 3 does not want to work on the first Saturday (negative weight + # for the Off shift). + (3, 0, 5, -2), + # Employee 5 wants a night shift on the second Thursday (negative weight). + (5, 3, 10, -2), + # Employee 2 does not want a night shift on the first Friday (positive + # weight). + (2, 3, 4, 4), + ] + + # Shift constraints on continuous sequence : + # (shift, hard_min, soft_min, min_penalty, + # soft_max, hard_max, max_penalty) + shift_constraints = [ + # One or two consecutive days of rest, this is a hard constraint. + (0, 1, 1, 0, 2, 2, 0), + # between 2 and 3 consecutive days of night shifts, 1 and 4 are + # possible but penalized. + (3, 1, 2, 20, 3, 4, 5), + ] + + # Weekly sum constraints on shifts days: + # (shift, hard_min, soft_min, min_penalty, + # soft_max, hard_max, max_penalty) + weekly_sum_constraints = [ + # Constraints on rests per week. + (0, 1, 2, 7, 2, 3, 4), + # At least 1 night shift per week (penalized). At most 4 (hard). + (3, 0, 1, 3, 4, 4, 0), + ] + + # Penalized transitions: + # (previous_shift, next_shift, penalty (0 means forbidden)) + penalized_transitions = [ + # Afternoon to night has a penalty of 4. + (2, 3, 4), + # Night to morning is forbidden. + (3, 1, 0), + ] + + # daily demands for work shifts (morning, afternoon, night) for each day + # of the week starting on Monday. + weekly_cover_demands = [ + (2, 3, 1), # Monday + (2, 3, 1), # Tuesday + (2, 2, 2), # Wednesday + (2, 3, 1), # Thursday + (2, 2, 2), # Friday + (1, 2, 3), # Saturday + (1, 3, 1), # Sunday + ] + + # Penalty for exceeding the cover constraint per shift type. + excess_cover_penalties = (2, 2, 5) + + num_days = num_weeks * 7 + num_shifts = len(shifts) + + model = cp_model.CpModel() + + work = {} + for e in range(num_employees): + for s in range(num_shifts): + for d in range(num_days): + work[e, s, d] = model.new_bool_var(f"work{e}_{s}_{d}") + + # Linear terms of the objective in a minimization context. + obj_int_vars: list[cp_model.IntVar] = [] + obj_int_coeffs: list[int] = [] + obj_bool_vars: list[cp_model.BoolVarT] = [] + obj_bool_coeffs: list[int] = [] + + # Exactly one shift per day. + for e in range(num_employees): + for d in range(num_days): + model.add_exactly_one(work[e, s, d] for s in range(num_shifts)) + + # Fixed assignments. + for e, s, d in fixed_assignments: + model.add(work[e, s, d] == 1) + + # Employee requests + for e, s, d, w in requests: + obj_bool_vars.append(work[e, s, d]) + obj_bool_coeffs.append(w) + + # Shift constraints + for ct in shift_constraints: + shift, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost = ct for e in range(num_employees): - for s in range(num_shifts): - for d in range(num_days): - work[e, s, d] = model.new_bool_var(f"work{e}_{s}_{d}") - - # Linear terms of the objective in a minimization context. - obj_int_vars: list[cp_model.IntVar] = [] - obj_int_coeffs: list[int] = [] - obj_bool_vars: list[cp_model.BoolVarT] = [] - obj_bool_coeffs: list[int] = [] - - # Exactly one shift per day. + works = [work[e, shift, d] for d in range(num_days)] + variables, coeffs = add_soft_sequence_constraint( + model, + works, + hard_min, + soft_min, + min_cost, + soft_max, + hard_max, + max_cost, + f"shift_constraint(employee {e}, shift {shift})", + ) + obj_bool_vars.extend(variables) + obj_bool_coeffs.extend(coeffs) + + # Weekly sum constraints + for ct in weekly_sum_constraints: + shift, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost = ct for e in range(num_employees): - for d in range(num_days): - model.add_exactly_one(work[e, s, d] for s in range(num_shifts)) - - # Fixed assignments. - for e, s, d in fixed_assignments: - model.add(work[e, s, d] == 1) - - # Employee requests - for e, s, d, w in requests: - obj_bool_vars.append(work[e, s, d]) - obj_bool_coeffs.append(w) - - # Shift constraints - for ct in shift_constraints: - shift, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost = ct - for e in range(num_employees): - works = [work[e, shift, d] for d in range(num_days)] - variables, coeffs = add_soft_sequence_constraint( - model, - works, - hard_min, - soft_min, - min_cost, - soft_max, - hard_max, - max_cost, - f"shift_constraint(employee {e}, shift {shift})", - ) - obj_bool_vars.extend(variables) - obj_bool_coeffs.extend(coeffs) - - # Weekly sum constraints - for ct in weekly_sum_constraints: - shift, hard_min, soft_min, min_cost, soft_max, hard_max, max_cost = ct - for e in range(num_employees): - for w in range(num_weeks): - works = [work[e, shift, d + w * 7] for d in range(7)] - variables, coeffs = add_soft_sum_constraint( - model, - works, - hard_min, - soft_min, - min_cost, - soft_max, - hard_max, - max_cost, - f"weekly_sum_constraint(employee {e}, shift {shift}, week {w})", - ) - obj_int_vars.extend(variables) - obj_int_coeffs.extend(coeffs) - - # Penalized transitions - for previous_shift, next_shift, cost in penalized_transitions: - for e in range(num_employees): - for d in range(num_days - 1): - transition = [ - ~work[e, previous_shift, d], - ~work[e, next_shift, d + 1], - ] - if cost == 0: - model.add_bool_or(transition) - else: - trans_var = model.new_bool_var( - f"transition (employee={e}, day={d})" - ) - transition.append(trans_var) - model.add_bool_or(transition) - obj_bool_vars.append(trans_var) - obj_bool_coeffs.append(cost) - - # Cover constraints - for s in range(1, num_shifts): - for w in range(num_weeks): - for d in range(7): - works = [work[e, s, w * 7 + d] for e in range(num_employees)] - # Ignore Off shift. - min_demand = weekly_cover_demands[d][s - 1] - worked = model.new_int_var(min_demand, num_employees, "") - model.add(worked == sum(works)) - over_penalty = excess_cover_penalties[s - 1] - if over_penalty > 0: - name = f"excess_demand(shift={s}, week={w}, day={d})" - excess = model.new_int_var(0, num_employees - min_demand, name) - model.add(excess == worked - min_demand) - obj_int_vars.append(excess) - obj_int_coeffs.append(over_penalty) - - # Objective - model.minimize( - sum(obj_bool_vars[i] * obj_bool_coeffs[i] for i in range(len(obj_bool_vars))) - + sum(obj_int_vars[i] * obj_int_coeffs[i] for i in range(len(obj_int_vars))) - ) - - if output_proto: - print(f"Writing proto to {output_proto}") - with open(output_proto, "w") as text_file: - text_file.write(str(model)) - - # Solve the model. - solver = cp_model.CpSolver() - if params: - text_format.Parse(params, solver.parameters) - solution_printer = cp_model.ObjectiveSolutionPrinter() - status = solver.solve(model, solution_printer) - - # Print solution. - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print() - header = " " - for w in range(num_weeks): - header += "M T W T F S S " - print(header) - for e in range(num_employees): - schedule = "" - for d in range(num_days): - for s in range(num_shifts): - if solver.boolean_value(work[e, s, d]): - schedule += shifts[s] + " " - print(f"worker {e}: {schedule}") - print() - print("Penalties:") - for i, var in enumerate(obj_bool_vars): - if solver.boolean_value(var): - penalty = obj_bool_coeffs[i] - if penalty > 0: - print(f" {var.name} violated, penalty={penalty}") - else: - print(f" {var.name} fulfilled, gain={-penalty}") - - for i, var in enumerate(obj_int_vars): - if solver.value(var) > 0: - print( - f" {var.name} violated by {solver.value(var)}, linear" - f" penalty={obj_int_coeffs[i]}" - ) - + for w in range(num_weeks): + works = [work[e, shift, d + w * 7] for d in range(7)] + variables, coeffs = add_soft_sum_constraint( + model, + works, + hard_min, + soft_min, + min_cost, + soft_max, + hard_max, + max_cost, + f"weekly_sum_constraint(employee {e}, shift {shift}, week {w})", + ) + obj_int_vars.extend(variables) + obj_int_coeffs.extend(coeffs) + + # Penalized transitions + for previous_shift, next_shift, cost in penalized_transitions: + for e in range(num_employees): + for d in range(num_days - 1): + transition = [ + ~work[e, previous_shift, d], + ~work[e, next_shift, d + 1], + ] + if cost == 0: + model.add_bool_or(transition) + else: + trans_var = model.new_bool_var(f"transition (employee={e}, day={d})") + transition.append(trans_var) + model.add_bool_or(transition) + obj_bool_vars.append(trans_var) + obj_bool_coeffs.append(cost) + + # Cover constraints + for s in range(1, num_shifts): + for w in range(num_weeks): + for d in range(7): + works = [work[e, s, w * 7 + d] for e in range(num_employees)] + # Ignore Off shift. + min_demand = weekly_cover_demands[d][s - 1] + worked = model.new_int_var(min_demand, num_employees, "") + model.add(worked == sum(works)) + over_penalty = excess_cover_penalties[s - 1] + if over_penalty > 0: + name = f"excess_demand(shift={s}, week={w}, day={d})" + excess = model.new_int_var(0, num_employees - min_demand, name) + model.add(excess == worked - min_demand) + obj_int_vars.append(excess) + obj_int_coeffs.append(over_penalty) + + # Objective + model.minimize( + sum( + obj_bool_vars[i] * obj_bool_coeffs[i] + for i in range(len(obj_bool_vars)) + ) + + sum( + obj_int_vars[i] * obj_int_coeffs[i] for i in range(len(obj_int_vars)) + ) + ) + + if output_proto: + print(f"Writing proto to {output_proto}") + with open(output_proto, "w") as text_file: + text_file.write(str(model)) + + # Solve the model. + solver = cp_model.CpSolver() + if params: + text_format.Parse(params, solver.parameters) + solution_printer = cp_model.ObjectiveSolutionPrinter() + status = solver.solve(model, solution_printer) + + # Print solution. + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print() + header = " " + for w in range(num_weeks): + header += "M T W T F S S " + print(header) + for e in range(num_employees): + schedule = "" + for d in range(num_days): + for s in range(num_shifts): + if solver.boolean_value(work[e, s, d]): + schedule += shifts[s] + " " + print(f"worker {e}: {schedule}") print() - print(solver.response_stats()) + print("Penalties:") + for i, var in enumerate(obj_bool_vars): + if solver.boolean_value(var): + penalty = obj_bool_coeffs[i] + if penalty > 0: + print(f" {var.name} violated, penalty={penalty}") + else: + print(f" {var.name} fulfilled, gain={-penalty}") + + for i, var in enumerate(obj_int_vars): + if solver.value(var) > 0: + print( + f" {var.name} violated by {solver.value(var)}, linear" + f" penalty={obj_int_coeffs[i]}" + ) + + print() + print(solver.response_stats()) def main(_): - solve_shift_scheduling(_PARAMS.value, _OUTPUT_PROTO.value) + solve_shift_scheduling(_PARAMS.value, _OUTPUT_PROTO.value) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/single_machine_scheduling_with_setup_release_due_dates_sat.py b/examples/python/single_machine_scheduling_with_setup_release_due_dates_sat.py index c54a67d26aa..511744c323a 100644 --- a/examples/python/single_machine_scheduling_with_setup_release_due_dates_sat.py +++ b/examples/python/single_machine_scheduling_with_setup_release_due_dates_sat.py @@ -38,482 +38,485 @@ # ---------------------------------------------------------------------------- # Intermediate solution printer class SolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self) -> None: - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 + def __init__(self) -> None: + cp_model.CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 - def on_solution_callback(self) -> None: - """Called at each new solution.""" - print( - f"Solution {self.__solution_count}, time = {self.wall_time} s," - f" objective = {self.objective_value}" - ) - - -def single_machine_scheduling(): - """Solves a complex single machine jobshop scheduling problem.""" - - parameters = _PARAMS.value - output_proto_file = _OUTPUT_PROTO.value - - # ---------------------------------------------------------------------------- - # Data. - - job_durations = [ - 2546, - 8589, - 5953, - 3710, - 3630, - 3016, - 4148, - 8706, - 1604, - 5502, - 9983, - 6209, - 9920, - 7860, - 2176, - ] - - setup_times = [ - [ - 3559, - 1638, - 2000, - 3676, - 2741, - 2439, - 2406, - 1526, - 1600, - 3356, - 4324, - 1923, - 3663, - 4103, - 2215, - ], - [ - 1442, - 3010, - 1641, - 4490, - 2060, - 2143, - 3376, - 3891, - 3513, - 2855, - 2653, - 1471, - 2257, - 1186, - 2354, - ], - [ - 1728, - 3583, - 3243, - 4080, - 2191, - 3644, - 4023, - 3510, - 2135, - 1346, - 1410, - 3565, - 3181, - 1126, - 4169, - ], - [ - 1291, - 1703, - 3103, - 4001, - 1712, - 1137, - 3341, - 3485, - 2557, - 2435, - 1972, - 1986, - 1522, - 4734, - 2520, - ], - [ - 4134, - 2200, - 1502, - 3995, - 1277, - 1808, - 1020, - 2078, - 2999, - 1605, - 1697, - 2323, - 2268, - 2288, - 4856, - ], - [ - 4974, - 2480, - 2492, - 4088, - 2587, - 4652, - 1478, - 3942, - 1222, - 3305, - 1206, - 1024, - 2605, - 3080, - 3516, - ], - [ - 1903, - 2584, - 2104, - 1609, - 4745, - 2691, - 1539, - 2544, - 2499, - 2074, - 4793, - 1756, - 2190, - 1298, - 2605, - ], - [ - 1407, - 2536, - 2296, - 1769, - 1449, - 3386, - 3046, - 1180, - 4132, - 4783, - 3386, - 3429, - 2450, - 3376, - 3719, - ], - [ - 3026, - 1637, - 3628, - 3096, - 1498, - 4947, - 1912, - 3703, - 4107, - 4730, - 1805, - 2189, - 1789, - 1985, - 3586, - ], - [ - 3940, - 1342, - 1601, - 2737, - 1748, - 3771, - 4052, - 1619, - 2558, - 3782, - 4383, - 3451, - 4904, - 1108, - 1750, - ], - [ - 1348, - 3162, - 1507, - 3936, - 1453, - 2953, - 4182, - 2968, - 3134, - 1042, - 3175, - 2805, - 4901, - 1735, - 1654, - ], - [ - 1099, - 1711, - 1245, - 1067, - 4343, - 3407, - 1108, - 1784, - 4803, - 2342, - 3377, - 2037, - 3563, - 1621, - 2840, - ], - [ - 2573, - 4222, - 3164, - 2563, - 3231, - 4731, - 2395, - 1033, - 4795, - 3288, - 2335, - 4935, - 4066, - 1440, - 4979, - ], - [ - 3321, - 1666, - 3573, - 2377, - 4649, - 4600, - 1065, - 2475, - 3658, - 3374, - 1138, - 4367, - 4728, - 3032, - 2198, - ], - [ - 2986, - 1180, - 4095, - 3132, - 3987, - 3880, - 3526, - 1460, - 4885, - 3827, - 4945, - 4419, - 3486, - 3805, - 3804, - ], - [ - 4163, - 3441, - 1217, - 2941, - 1210, - 3794, - 1779, - 1904, - 4255, - 4967, - 4003, - 3873, - 1002, - 2055, - 4295, - ], - ] - - due_dates = [ - -1, - -1, - 28569, - -1, - 98104, - 27644, - 55274, - 57364, - -1, - -1, - 60875, - 96637, - 77888, - -1, - -1, - ] - release_dates = [0, 0, 0, 0, 19380, 0, 0, 48657, 0, 27932, 0, 0, 24876, 0, 0] - - precedences = [(0, 2), (1, 2)] - - # ---------------------------------------------------------------------------- - # Helper data. - num_jobs = len(job_durations) - all_jobs = range(num_jobs) - - # ---------------------------------------------------------------------------- - # Preprocess. - if _PREPROCESS.value: - for job_id in all_jobs: - min_incoming_setup = min( - setup_times[j][job_id] for j in range(num_jobs + 1) - ) - if release_dates[job_id] != 0: - min_incoming_setup = min(min_incoming_setup, release_dates[job_id]) - if min_incoming_setup == 0: - continue - - print(f"job {job_id} has a min incoming setup of {min_incoming_setup}") - # We can transfer some setup times to the duration of the job. - job_durations[job_id] += min_incoming_setup - # Decrease corresponding incoming setup times. - for j in range(num_jobs + 1): - setup_times[j][job_id] -= min_incoming_setup - # Adjust release dates if needed. - if release_dates[job_id] != 0: - release_dates[job_id] -= min_incoming_setup - - # ---------------------------------------------------------------------------- - # Model. - model = cp_model.CpModel() - - # ---------------------------------------------------------------------------- - # Compute a maximum makespan greedily. - horizon = sum(job_durations) + sum( - max(setup_times[i][j] for i in range(num_jobs + 1)) for j in range(num_jobs) + def on_solution_callback(self) -> None: + """Called at each new solution.""" + print( + f"Solution {self.__solution_count}, time = {self.wall_time} s," + f" objective = {self.objective_value}" ) - print(f"Greedy horizon = {horizon}") - # ---------------------------------------------------------------------------- - # Global storage of variables. - intervals = [] - starts = [] - ends = [] - # ---------------------------------------------------------------------------- - # Scan the jobs and create the relevant variables and intervals. +def single_machine_scheduling(): + """Solves a complex single machine jobshop scheduling problem.""" + + parameters = _PARAMS.value + output_proto_file = _OUTPUT_PROTO.value + + # ---------------------------------------------------------------------------- + # Data. + + job_durations = [ + 2546, + 8589, + 5953, + 3710, + 3630, + 3016, + 4148, + 8706, + 1604, + 5502, + 9983, + 6209, + 9920, + 7860, + 2176, + ] + + setup_times = [ + [ + 3559, + 1638, + 2000, + 3676, + 2741, + 2439, + 2406, + 1526, + 1600, + 3356, + 4324, + 1923, + 3663, + 4103, + 2215, + ], + [ + 1442, + 3010, + 1641, + 4490, + 2060, + 2143, + 3376, + 3891, + 3513, + 2855, + 2653, + 1471, + 2257, + 1186, + 2354, + ], + [ + 1728, + 3583, + 3243, + 4080, + 2191, + 3644, + 4023, + 3510, + 2135, + 1346, + 1410, + 3565, + 3181, + 1126, + 4169, + ], + [ + 1291, + 1703, + 3103, + 4001, + 1712, + 1137, + 3341, + 3485, + 2557, + 2435, + 1972, + 1986, + 1522, + 4734, + 2520, + ], + [ + 4134, + 2200, + 1502, + 3995, + 1277, + 1808, + 1020, + 2078, + 2999, + 1605, + 1697, + 2323, + 2268, + 2288, + 4856, + ], + [ + 4974, + 2480, + 2492, + 4088, + 2587, + 4652, + 1478, + 3942, + 1222, + 3305, + 1206, + 1024, + 2605, + 3080, + 3516, + ], + [ + 1903, + 2584, + 2104, + 1609, + 4745, + 2691, + 1539, + 2544, + 2499, + 2074, + 4793, + 1756, + 2190, + 1298, + 2605, + ], + [ + 1407, + 2536, + 2296, + 1769, + 1449, + 3386, + 3046, + 1180, + 4132, + 4783, + 3386, + 3429, + 2450, + 3376, + 3719, + ], + [ + 3026, + 1637, + 3628, + 3096, + 1498, + 4947, + 1912, + 3703, + 4107, + 4730, + 1805, + 2189, + 1789, + 1985, + 3586, + ], + [ + 3940, + 1342, + 1601, + 2737, + 1748, + 3771, + 4052, + 1619, + 2558, + 3782, + 4383, + 3451, + 4904, + 1108, + 1750, + ], + [ + 1348, + 3162, + 1507, + 3936, + 1453, + 2953, + 4182, + 2968, + 3134, + 1042, + 3175, + 2805, + 4901, + 1735, + 1654, + ], + [ + 1099, + 1711, + 1245, + 1067, + 4343, + 3407, + 1108, + 1784, + 4803, + 2342, + 3377, + 2037, + 3563, + 1621, + 2840, + ], + [ + 2573, + 4222, + 3164, + 2563, + 3231, + 4731, + 2395, + 1033, + 4795, + 3288, + 2335, + 4935, + 4066, + 1440, + 4979, + ], + [ + 3321, + 1666, + 3573, + 2377, + 4649, + 4600, + 1065, + 2475, + 3658, + 3374, + 1138, + 4367, + 4728, + 3032, + 2198, + ], + [ + 2986, + 1180, + 4095, + 3132, + 3987, + 3880, + 3526, + 1460, + 4885, + 3827, + 4945, + 4419, + 3486, + 3805, + 3804, + ], + [ + 4163, + 3441, + 1217, + 2941, + 1210, + 3794, + 1779, + 1904, + 4255, + 4967, + 4003, + 3873, + 1002, + 2055, + 4295, + ], + ] + + due_dates = [ + -1, + -1, + 28569, + -1, + 98104, + 27644, + 55274, + 57364, + -1, + -1, + 60875, + 96637, + 77888, + -1, + -1, + ] + release_dates = [0, 0, 0, 0, 19380, 0, 0, 48657, 0, 27932, 0, 0, 24876, 0, 0] + + precedences = [(0, 2), (1, 2)] + + # ---------------------------------------------------------------------------- + # Helper data. + num_jobs = len(job_durations) + all_jobs = range(num_jobs) + + # ---------------------------------------------------------------------------- + # Preprocess. + if _PREPROCESS.value: for job_id in all_jobs: - duration = job_durations[job_id] - release_date = release_dates[job_id] - due_date = due_dates[job_id] if due_dates[job_id] != -1 else horizon - print( - f"job {job_id:2}: start = {release_date:5}, duration = {duration:4}," - f" end = {due_date:6}" + min_incoming_setup = min( + setup_times[j][job_id] for j in range(num_jobs + 1) + ) + if release_dates[job_id] != 0: + min_incoming_setup = min(min_incoming_setup, release_dates[job_id]) + if min_incoming_setup == 0: + continue + + print(f"job {job_id} has a min incoming setup of {min_incoming_setup}") + # We can transfer some setup times to the duration of the job. + job_durations[job_id] += min_incoming_setup + # Decrease corresponding incoming setup times. + for j in range(num_jobs + 1): + setup_times[j][job_id] -= min_incoming_setup + # Adjust release dates if needed. + if release_dates[job_id] != 0: + release_dates[job_id] -= min_incoming_setup + + # ---------------------------------------------------------------------------- + # Model. + model = cp_model.CpModel() + + # ---------------------------------------------------------------------------- + # Compute a maximum makespan greedily. + horizon = sum(job_durations) + sum( + max(setup_times[i][j] for i in range(num_jobs + 1)) + for j in range(num_jobs) + ) + print(f"Greedy horizon = {horizon}") + + # ---------------------------------------------------------------------------- + # Global storage of variables. + intervals = [] + starts = [] + ends = [] + + # ---------------------------------------------------------------------------- + # Scan the jobs and create the relevant variables and intervals. + for job_id in all_jobs: + duration = job_durations[job_id] + release_date = release_dates[job_id] + due_date = due_dates[job_id] if due_dates[job_id] != -1 else horizon + print( + f"job {job_id:2}: start = {release_date:5}, duration = {duration:4}," + f" end = {due_date:6}" + ) + name_suffix = f"_{job_id}" + start = model.new_int_var(release_date, due_date, "s" + name_suffix) + end = model.new_int_var(release_date, due_date, "e" + name_suffix) + interval = model.new_interval_var(start, duration, end, "i" + name_suffix) + starts.append(start) + ends.append(end) + intervals.append(interval) + + # No overlap constraint. + model.add_no_overlap(intervals) + + # ---------------------------------------------------------------------------- + # Transition times using a circuit constraint. + arcs = [] + for i in all_jobs: + # Initial arc from the dummy node (0) to a task. + start_lit = model.new_bool_var("") + arcs.append((0, i + 1, start_lit)) + # If this task is the first, set to minimum starting time. + min_start_time = max(release_dates[i], setup_times[0][i]) + model.add(starts[i] == min_start_time).only_enforce_if(start_lit) + # Final arc from an arc to the dummy node. + arcs.append((i + 1, 0, model.new_bool_var(""))) + + for j in all_jobs: + if i == j: + continue + + lit = model.new_bool_var(f"{j} follows {i}") + arcs.append((i + 1, j + 1, lit)) + + # We add the reified precedence to link the literal with the times of the + # two tasks. + # If release_dates[j] == 0, we can strenghten this precedence into an + # equality as we are minimizing the makespan. + if release_dates[j] == 0: + model.add(starts[j] == ends[i] + setup_times[i + 1][j]).only_enforce_if( + lit ) - name_suffix = f"_{job_id}" - start = model.new_int_var(release_date, due_date, "s" + name_suffix) - end = model.new_int_var(release_date, due_date, "e" + name_suffix) - interval = model.new_interval_var(start, duration, end, "i" + name_suffix) - starts.append(start) - ends.append(end) - intervals.append(interval) - - # No overlap constraint. - model.add_no_overlap(intervals) - - # ---------------------------------------------------------------------------- - # Transition times using a circuit constraint. - arcs = [] - for i in all_jobs: - # Initial arc from the dummy node (0) to a task. - start_lit = model.new_bool_var("") - arcs.append((0, i + 1, start_lit)) - # If this task is the first, set to minimum starting time. - min_start_time = max(release_dates[i], setup_times[0][i]) - model.add(starts[i] == min_start_time).only_enforce_if(start_lit) - # Final arc from an arc to the dummy node. - arcs.append((i + 1, 0, model.new_bool_var(""))) - - for j in all_jobs: - if i == j: - continue - - lit = model.new_bool_var(f"{j} follows {i}") - arcs.append((i + 1, j + 1, lit)) - - # We add the reified precedence to link the literal with the times of the - # two tasks. - # If release_dates[j] == 0, we can strenghten this precedence into an - # equality as we are minimizing the makespan. - if release_dates[j] == 0: - model.add(starts[j] == ends[i] + setup_times[i + 1][j]).only_enforce_if( - lit - ) - else: - model.add(starts[j] >= ends[i] + setup_times[i + 1][j]).only_enforce_if( - lit - ) - - model.add_circuit(arcs) - - # ---------------------------------------------------------------------------- - # Precedences. - for before, after in precedences: - print(f"job {after} is after job {before}") - model.add(ends[before] <= starts[after]) - - # ---------------------------------------------------------------------------- - # Objective. - makespan = model.new_int_var(0, horizon, "makespan") - model.add_max_equality(makespan, ends) - model.minimize(makespan) - - # ---------------------------------------------------------------------------- - # Write problem to file. - if output_proto_file: - print(f"Writing proto to {output_proto_file}") - with open(output_proto_file, "w") as text_file: - text_file.write(str(model)) - - # ---------------------------------------------------------------------------- - # Solve. - solver = cp_model.CpSolver() - if parameters: - text_format.Parse(parameters, solver.parameters) - solution_printer = SolutionPrinter() - solver.best_bound_callback = lambda a: print(f"New objective lower bound: {a}") - solver.solve(model, solution_printer) - for job_id in all_jobs: - print( - f"job {job_id} starts at {solver.value(starts[job_id])} end ends at" - f" {solver.value(ends[job_id])}" + else: + model.add(starts[j] >= ends[i] + setup_times[i + 1][j]).only_enforce_if( + lit ) + model.add_circuit(arcs) + + # ---------------------------------------------------------------------------- + # Precedences. + for before, after in precedences: + print(f"job {after} is after job {before}") + model.add(ends[before] <= starts[after]) + + # ---------------------------------------------------------------------------- + # Objective. + makespan = model.new_int_var(0, horizon, "makespan") + model.add_max_equality(makespan, ends) + model.minimize(makespan) + + # ---------------------------------------------------------------------------- + # Write problem to file. + if output_proto_file: + print(f"Writing proto to {output_proto_file}") + with open(output_proto_file, "w") as text_file: + text_file.write(str(model)) + + # ---------------------------------------------------------------------------- + # Solve. + solver = cp_model.CpSolver() + if parameters: + text_format.Parse(parameters, solver.parameters) + solution_printer = SolutionPrinter() + solver.best_bound_callback = lambda a: print( + f"New objective lower bound: {a}" + ) + solver.solve(model, solution_printer) + for job_id in all_jobs: + print( + f"job {job_id} starts at {solver.value(starts[job_id])} end ends at" + f" {solver.value(ends[job_id])}" + ) + def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - single_machine_scheduling() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + single_machine_scheduling() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/spread_robots_sat.py b/examples/python/spread_robots_sat.py index 27da1d65b9e..3a67396748f 100644 --- a/examples/python/spread_robots_sat.py +++ b/examples/python/spread_robots_sat.py @@ -22,7 +22,9 @@ from google.protobuf import text_format from ortools.sat.python import cp_model -_NUM_ROBOTS = flags.DEFINE_integer("num_robots", 8, "Number of robots to place.") +_NUM_ROBOTS = flags.DEFINE_integer( + "num_robots", 8, "Number of robots to place." +) _ROOM_SIZE = flags.DEFINE_integer( "room_size", 20, "Size of the square room where robots are." ) @@ -34,88 +36,88 @@ def spread_robots(num_robots: int, room_size: int, params: str) -> None: - """Optimize robots placement.""" - model = cp_model.CpModel() - - # Create the list of coordinates (x, y) for each robot. - x = [model.new_int_var(1, room_size, f"x_{i}") for i in range(num_robots)] - y = [model.new_int_var(1, room_size, f"y_{i}") for i in range(num_robots)] - - # The specification of the problem is to maximize the minimum euclidian - # distance between any two robots. Unfortunately, the euclidian distance - # uses the square root operation which is not defined on integer variables. - # To work around, we will create a min_square_distance variable, then we make - # sure that its value is less than the square of the euclidian distance - # between any two robots. - # - # This encoding has a low precision. To improve the precision, we will scale - # the domain of the min_square_distance variable by a constant factor, then - # multiply the square of the euclidian distance between two robots by the same - # factor. - # - # we create a scaled_min_square_distance variable with a domain of - # [0..scaling * max euclidian distance**2] such that - # forall i: - # scaled_min_square_distance <= scaling * (x_diff_sq[i] + y_diff_sq[i]) - scaling = 1000 - scaled_min_square_distance = model.new_int_var( - 0, 2 * scaling * room_size**2, "scaled_min_square_distance" + """Optimize robots placement.""" + model = cp_model.CpModel() + + # Create the list of coordinates (x, y) for each robot. + x = [model.new_int_var(1, room_size, f"x_{i}") for i in range(num_robots)] + y = [model.new_int_var(1, room_size, f"y_{i}") for i in range(num_robots)] + + # The specification of the problem is to maximize the minimum euclidian + # distance between any two robots. Unfortunately, the euclidian distance + # uses the square root operation which is not defined on integer variables. + # To work around, we will create a min_square_distance variable, then we make + # sure that its value is less than the square of the euclidian distance + # between any two robots. + # + # This encoding has a low precision. To improve the precision, we will scale + # the domain of the min_square_distance variable by a constant factor, then + # multiply the square of the euclidian distance between two robots by the same + # factor. + # + # we create a scaled_min_square_distance variable with a domain of + # [0..scaling * max euclidian distance**2] such that + # forall i: + # scaled_min_square_distance <= scaling * (x_diff_sq[i] + y_diff_sq[i]) + scaling = 1000 + scaled_min_square_distance = model.new_int_var( + 0, 2 * scaling * room_size**2, "scaled_min_square_distance" + ) + + # Build intermediate variables and get the list of squared distances on + # each dimension. + for i in range(num_robots - 1): + for j in range(i + 1, num_robots): + # Compute the distance on each dimension between robot i and robot j. + x_diff = model.new_int_var(-room_size, room_size, f"x_diff{i}") + y_diff = model.new_int_var(-room_size, room_size, f"y_diff{i}") + model.add(x_diff == x[i] - x[j]) + model.add(y_diff == y[i] - y[j]) + + # Compute the square of the previous differences. + x_diff_sq = model.new_int_var(0, room_size**2, f"x_diff_sq{i}") + y_diff_sq = model.new_int_var(0, room_size**2, f"y_diff_sq{i}") + model.add_multiplication_equality(x_diff_sq, x_diff, x_diff) + model.add_multiplication_equality(y_diff_sq, y_diff, y_diff) + + # We just need to be <= to the scaled square distance as we are + # maximizing the min distance, which is equivalent as maximizing the min + # square distance. + model.add(scaled_min_square_distance <= scaling * (x_diff_sq + y_diff_sq)) + + # Naive symmetry breaking. + for i in range(1, num_robots): + model.add(x[0] <= x[i]) + model.add(y[0] <= y[i]) + + # Objective + model.maximize(scaled_min_square_distance) + + # Creates a solver and solves the model. + solver = cp_model.CpSolver() + if params: + text_format.Parse(params, solver.parameters) + solver.parameters.log_search_progress = True + status = solver.solve(model) + + # Prints the solution. + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print( + f"Spread {num_robots} with a min pairwise distance of" + f" {math.sqrt(solver.objective_value / scaling)}" ) - - # Build intermediate variables and get the list of squared distances on - # each dimension. - for i in range(num_robots - 1): - for j in range(i + 1, num_robots): - # Compute the distance on each dimension between robot i and robot j. - x_diff = model.new_int_var(-room_size, room_size, f"x_diff{i}") - y_diff = model.new_int_var(-room_size, room_size, f"y_diff{i}") - model.add(x_diff == x[i] - x[j]) - model.add(y_diff == y[i] - y[j]) - - # Compute the square of the previous differences. - x_diff_sq = model.new_int_var(0, room_size**2, f"x_diff_sq{i}") - y_diff_sq = model.new_int_var(0, room_size**2, f"y_diff_sq{i}") - model.add_multiplication_equality(x_diff_sq, x_diff, x_diff) - model.add_multiplication_equality(y_diff_sq, y_diff, y_diff) - - # We just need to be <= to the scaled square distance as we are - # maximizing the min distance, which is equivalent as maximizing the min - # square distance. - model.add(scaled_min_square_distance <= scaling * (x_diff_sq + y_diff_sq)) - - # Naive symmetry breaking. - for i in range(1, num_robots): - model.add(x[0] <= x[i]) - model.add(y[0] <= y[i]) - - # Objective - model.maximize(scaled_min_square_distance) - - # Creates a solver and solves the model. - solver = cp_model.CpSolver() - if params: - text_format.Parse(params, solver.parameters) - solver.parameters.log_search_progress = True - status = solver.solve(model) - - # Prints the solution. - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print( - f"Spread {num_robots} with a min pairwise distance of" - f" {math.sqrt(solver.objective_value / scaling)}" - ) - for i in range(num_robots): - print(f"robot {i}: x={solver.value(x[i])} y={solver.value(y[i])}") - else: - print("No solution found.") + for i in range(num_robots): + print(f"robot {i}: x={solver.value(x[i])} y={solver.value(y[i])}") + else: + print("No solution found.") def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") - spread_robots(_NUM_ROBOTS.value, _ROOM_SIZE.value, _PARAMS.value) + spread_robots(_NUM_ROBOTS.value, _ROOM_SIZE.value, _PARAMS.value) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/steel_mill_slab_sat.py b/examples/python/steel_mill_slab_sat.py index 6f79d85fbe0..b0c0a101e4d 100644 --- a/examples/python/steel_mill_slab_sat.py +++ b/examples/python/steel_mill_slab_sat.py @@ -42,17 +42,17 @@ def build_problem( problem_id: int, ) -> tuple[int, list[int], int, list[tuple[int, int]]]: - """Build problem data.""" - if problem_id == 0: - capacities = [ - # fmt:off + """Build problem data.""" + if problem_id == 0: + capacities = [ + # fmt:off 0, 12, 14, 17, 18, 19, 20, 23, 24, 25, 26, 27, 28, 29, 30, 32, 35, 39, 42, 43, 44, - # fmt:on - ] - num_colors = 88 - num_slabs = 111 - orders = [ # (size, color) - # fmt:off + # fmt:on + ] + num_colors = 88 + num_slabs = 111 + orders = [ # (size, color) + # fmt:off (4, 1), (22, 2), (9, 3), (5, 4), (8, 5), (3, 6), (3, 4), (4, 7), (7, 4), (7, 8), (3, 6), (2, 6), (2, 4), (8, 9), (5, 10), (7, 11), (4, 7), (7, 11), (5, 10), (7, 11), (8, 9), (3, 1), (25, 12), (14, 13), @@ -69,242 +69,244 @@ def build_problem( (30, 73), (30, 74), (30, 75), (23, 76), (15, 77), (15, 78), (27, 79), (27, 80), (27, 81), (27, 82), (27, 83), (27, 84), (27, 79), (27, 85), (27, 86), (10, 87), (3, 88), - # fmt:on - ] - elif problem_id == 1: - capacities = [0, 17, 44] - num_colors = 23 - num_slabs = 30 - orders = [ # (size, color) - # fmt:off + # fmt:on + ] + elif problem_id == 1: + capacities = [0, 17, 44] + num_colors = 23 + num_slabs = 30 + orders = [ # (size, color) + # fmt:off (4, 1), (22, 2), (9, 3), (5, 4), (8, 5), (3, 6), (3, 4), (4, 7), (7, 4), (7, 8), (3, 6), (2, 6), (2, 4), (8, 9), (5, 10), (7, 11), (4, 7), (7, 11), (5, 10), (7, 11), (8, 9), (3, 1), (25, 12), (14, 13), (3, 6), (22, 14), (19, 15), (19, 15), (22, 16), (22, 17), (22, 18), (20, 19), (22, 20), (5, 21), (4, 22), (10, 23), - # fmt:on - ] - elif problem_id == 2: - capacities = [0, 17, 44] - num_colors = 15 - num_slabs = 20 - orders = [ # (size, color) - # fmt:off + # fmt:on + ] + elif problem_id == 2: + capacities = [0, 17, 44] + num_colors = 15 + num_slabs = 20 + orders = [ # (size, color) + # fmt:off (4, 1), (22, 2), (9, 3), (5, 4), (8, 5), (3, 6), (3, 4), (4, 7), (7, 4), (7, 8), (3, 6), (2, 6), (2, 4), (8, 9), (5, 10), (7, 11), (4, 7), (7, 11), (5, 10), (7, 11), (8, 9), (3, 1), (25, 12), (14, 13), (3, 6), (22, 14), (19, 15), (19, 15), - # fmt:on - ] - - else: # problem_id == 3, default problem. - capacities = [0, 17, 44] - num_colors = 8 - num_slabs = 10 - orders = [ # (size, color) - (4, 1), - (22, 2), - (9, 3), - (5, 4), - (8, 5), - (3, 6), - (3, 4), - (4, 7), - (7, 4), - (7, 8), - (3, 6), - ] - - return (num_slabs, capacities, num_colors, orders) + # fmt:on + ] + else: # problem_id == 3, default problem. + capacities = [0, 17, 44] + num_colors = 8 + num_slabs = 10 + orders = [ # (size, color) + (4, 1), + (22, 2), + (9, 3), + (5, 4), + (8, 5), + (3, 6), + (3, 4), + (4, 7), + (7, 4), + (7, 8), + (3, 6), + ] -class SteelMillSlabSolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" - - def __init__(self, orders, assign, load, loss) -> None: - cp_model.CpSolverSolutionCallback.__init__(self) - self.__orders = orders - self.__assign = assign - self.__load = load - self.__loss = loss - self.__solution_count = 0 - self.__all_orders = range(len(orders)) - self.__all_slabs = range(len(assign[0])) - self.__start_time = time.time() - - def on_solution_callback(self) -> None: - """Called on each new solution.""" - current_time = time.time() - objective = sum(self.value(l) for l in self.__loss) - print( - f"Solution {self.__solution_count}, time =" - f" {current_time - self.__start_time} s, objective = {objective}" - ) - self.__solution_count += 1 - orders_in_slab = [ - [o for o in self.__all_orders if self.value(self.__assign[o][s])] - for s in self.__all_slabs - ] - for s in self.__all_slabs: - if orders_in_slab[s]: - line = ( - f" - slab {s}, load = {self.value(self.__load[s])}, loss =" - f" {self.value(self.__loss[s])}, orders = [" - ) - for o in orders_in_slab[s]: - line += f"#{o}(w{self.__orders[o][0]}, c{self.__orders[o][1]})" - line += "]" - print(line) + return (num_slabs, capacities, num_colors, orders) -def steel_mill_slab(problem_id: int, break_symmetries: bool) -> None: - """Solves the Steel Mill Slab Problem.""" - ### Load problem. - num_slabs, capacities, num_colors, orders = build_problem(problem_id) - - num_orders = len(orders) - num_capacities = len(capacities) - all_slabs = range(num_slabs) - all_colors = range(num_colors) - all_orders = range(len(orders)) +class SteelMillSlabSolutionPrinter(cp_model.CpSolverSolutionCallback): + """Print intermediate solutions.""" + + def __init__(self, orders, assign, load, loss) -> None: + cp_model.CpSolverSolutionCallback.__init__(self) + self.__orders = orders + self.__assign = assign + self.__load = load + self.__loss = loss + self.__solution_count = 0 + self.__all_orders = range(len(orders)) + self.__all_slabs = range(len(assign[0])) + self.__start_time = time.time() + + def on_solution_callback(self) -> None: + """Called on each new solution.""" + current_time = time.time() + objective = sum(self.value(l) for l in self.__loss) print( - f"Solving steel mill with {num_orders} orders, {num_slabs} slabs, and" - f" {num_capacities - 1} capacities" + f"Solution {self.__solution_count}, time =" + f" {current_time - self.__start_time} s, objective = {objective}" ) - - # Compute auxiliary data. - widths = [x[0] for x in orders] - colors = [x[1] for x in orders] - max_capacity = max(capacities) - loss_array = [ - min(x for x in capacities if x >= c) - c for c in range(max_capacity + 1) + self.__solution_count += 1 + orders_in_slab = [ + [o for o in self.__all_orders if self.value(self.__assign[o][s])] + for s in self.__all_slabs ] - max_loss = max(loss_array) - orders_per_color = [ - [o for o in all_orders if colors[o] == c + 1] for c in all_colors - ] - unique_color_orders = [ - o for o in all_orders if len(orders_per_color[colors[o] - 1]) == 1 - ] - - ### Model problem. - - # Create the model and the decision variables. - model = cp_model.CpModel() - assign = [ - [model.new_bool_var(f"assign_{o}_to_slab_{s}") for s in all_slabs] - for o in all_orders - ] - loads = [model.new_int_var(0, max_capacity, f"load_of_slab_{s}") for s in all_slabs] - color_is_in_slab = [ - [model.new_bool_var(f"color_{c + 1}_in_slab_{s}") for c in all_colors] - for s in all_slabs - ] - - # Compute load of all slabs. - for s in all_slabs: - model.add(sum(assign[o][s] * widths[o] for o in all_orders) == loads[s]) - - # Orders are assigned to one slab. - for o in all_orders: - model.add_exactly_one(assign[o]) - - # Redundant constraint (sum of loads == sum of widths). - model.add(sum(loads) == sum(widths)) - - # Link present_colors and assign. - for c in all_colors: - for s in all_slabs: - for o in orders_per_color[c]: - model.add_implication(assign[o][s], color_is_in_slab[s][c]) - model.add_implication(~color_is_in_slab[s][c], ~assign[o][s]) + for s in self.__all_slabs: + if orders_in_slab[s]: + line = ( + f" - slab {s}, load = {self.value(self.__load[s])}, loss =" + f" {self.value(self.__loss[s])}, orders = [" + ) + for o in orders_in_slab[s]: + line += f"#{o}(w{self.__orders[o][0]}, c{self.__orders[o][1]})" + line += "]" + print(line) - # At most two colors per slab. - for s in all_slabs: - model.add(sum(color_is_in_slab[s]) <= 2) - # Project previous constraint on unique_color_orders +def steel_mill_slab(problem_id: int, break_symmetries: bool) -> None: + """Solves the Steel Mill Slab Problem.""" + ### Load problem. + num_slabs, capacities, num_colors, orders = build_problem(problem_id) + + num_orders = len(orders) + num_capacities = len(capacities) + all_slabs = range(num_slabs) + all_colors = range(num_colors) + all_orders = range(len(orders)) + print( + f"Solving steel mill with {num_orders} orders, {num_slabs} slabs, and" + f" {num_capacities - 1} capacities" + ) + + # Compute auxiliary data. + widths = [x[0] for x in orders] + colors = [x[1] for x in orders] + max_capacity = max(capacities) + loss_array = [ + min(x for x in capacities if x >= c) - c for c in range(max_capacity + 1) + ] + max_loss = max(loss_array) + orders_per_color = [ + [o for o in all_orders if colors[o] == c + 1] for c in all_colors + ] + unique_color_orders = [ + o for o in all_orders if len(orders_per_color[colors[o] - 1]) == 1 + ] + + ### Model problem. + + # Create the model and the decision variables. + model = cp_model.CpModel() + assign = [ + [model.new_bool_var(f"assign_{o}_to_slab_{s}") for s in all_slabs] + for o in all_orders + ] + loads = [ + model.new_int_var(0, max_capacity, f"load_of_slab_{s}") for s in all_slabs + ] + color_is_in_slab = [ + [model.new_bool_var(f"color_{c + 1}_in_slab_{s}") for c in all_colors] + for s in all_slabs + ] + + # Compute load of all slabs. + for s in all_slabs: + model.add(sum(assign[o][s] * widths[o] for o in all_orders) == loads[s]) + + # Orders are assigned to one slab. + for o in all_orders: + model.add_exactly_one(assign[o]) + + # Redundant constraint (sum of loads == sum of widths). + model.add(sum(loads) == sum(widths)) + + # Link present_colors and assign. + for c in all_colors: for s in all_slabs: - model.add(sum(assign[o][s] for o in unique_color_orders) <= 2) - - # Symmetry breaking. - for s in range(num_slabs - 1): - model.add(loads[s] >= loads[s + 1]) - - # Collect equivalent orders. - width_to_unique_color_order = {} - ordered_equivalent_orders = [] - for c in all_colors: - colored_orders = orders_per_color[c] - if not colored_orders: - continue - if len(colored_orders) == 1: - o = colored_orders[0] - w = widths[o] - if w not in width_to_unique_color_order: - width_to_unique_color_order[w] = [o] - else: - width_to_unique_color_order[w].append(o) - else: - local_width_to_order = {} - for o in colored_orders: - w = widths[o] - if w not in local_width_to_order: - local_width_to_order[w] = [] - local_width_to_order[w].append(o) - for _, os in local_width_to_order.items(): - if len(os) > 1: - for p in range(len(os) - 1): - ordered_equivalent_orders.append((os[p], os[p + 1])) - for _, os in width_to_unique_color_order.items(): + for o in orders_per_color[c]: + model.add_implication(assign[o][s], color_is_in_slab[s][c]) + model.add_implication(~color_is_in_slab[s][c], ~assign[o][s]) + + # At most two colors per slab. + for s in all_slabs: + model.add(sum(color_is_in_slab[s]) <= 2) + + # Project previous constraint on unique_color_orders + for s in all_slabs: + model.add(sum(assign[o][s] for o in unique_color_orders) <= 2) + + # Symmetry breaking. + for s in range(num_slabs - 1): + model.add(loads[s] >= loads[s + 1]) + + # Collect equivalent orders. + width_to_unique_color_order = {} + ordered_equivalent_orders = [] + for c in all_colors: + colored_orders = orders_per_color[c] + if not colored_orders: + continue + if len(colored_orders) == 1: + o = colored_orders[0] + w = widths[o] + if w not in width_to_unique_color_order: + width_to_unique_color_order[w] = [o] + else: + width_to_unique_color_order[w].append(o) + else: + local_width_to_order = {} + for o in colored_orders: + w = widths[o] + if w not in local_width_to_order: + local_width_to_order[w] = [] + local_width_to_order[w].append(o) + for _, os in local_width_to_order.items(): if len(os) > 1: - for p in range(len(os) - 1): - ordered_equivalent_orders.append((os[p], os[p + 1])) - - # Create position variables if there are symmetries to be broken. - if break_symmetries and ordered_equivalent_orders: - print( - f" - creating {len(ordered_equivalent_orders)} symmetry breaking" - " constraints" + for p in range(len(os) - 1): + ordered_equivalent_orders.append((os[p], os[p + 1])) + for _, os in width_to_unique_color_order.items(): + if len(os) > 1: + for p in range(len(os) - 1): + ordered_equivalent_orders.append((os[p], os[p + 1])) + + # Create position variables if there are symmetries to be broken. + if break_symmetries and ordered_equivalent_orders: + print( + f" - creating {len(ordered_equivalent_orders)} symmetry breaking" + " constraints" + ) + positions = {} + for p in ordered_equivalent_orders: + if p[0] not in positions: + positions[p[0]] = model.new_int_var( + 0, num_slabs - 1, f"position_of_slab_{p[0]}" ) - positions = {} - for p in ordered_equivalent_orders: - if p[0] not in positions: - positions[p[0]] = model.new_int_var( - 0, num_slabs - 1, f"position_of_slab_{p[0]}" - ) - model.add_map_domain(positions[p[0]], assign[p[0]]) - if p[1] not in positions: - positions[p[1]] = model.new_int_var( - 0, num_slabs - 1, f"position_of_slab_{p[1]}" - ) - model.add_map_domain(positions[p[1]], assign[p[1]]) - # Finally add the symmetry breaking constraint. - model.add(positions[p[0]] <= positions[p[1]]) - - # Objective. - obj = model.new_int_var(0, num_slabs * max_loss, "obj") - losses = [model.new_int_var(0, max_loss, f"loss_{s}") for s in all_slabs] - for s in all_slabs: - model.add_element(loads[s], loss_array, losses[s]) - model.add(obj == sum(losses)) - model.minimize(obj) - - ### Solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - objective_printer = cp_model.ObjectiveSolutionPrinter() - status = solver.solve(model, objective_printer) - - ### Output the solution. - if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): - print( - f"Loss = {solver.objective_value}, time = {solver.wall_time} s," - f" {solver.num_conflicts} conflicts" + model.add_map_domain(positions[p[0]], assign[p[0]]) + if p[1] not in positions: + positions[p[1]] = model.new_int_var( + 0, num_slabs - 1, f"position_of_slab_{p[1]}" ) - else: - print("No solution") + model.add_map_domain(positions[p[1]], assign[p[1]]) + # Finally add the symmetry breaking constraint. + model.add(positions[p[0]] <= positions[p[1]]) + + # Objective. + obj = model.new_int_var(0, num_slabs * max_loss, "obj") + losses = [model.new_int_var(0, max_loss, f"loss_{s}") for s in all_slabs] + for s in all_slabs: + model.add_element(loads[s], loss_array, losses[s]) + model.add(obj == sum(losses)) + model.minimize(obj) + + ### Solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + objective_printer = cp_model.ObjectiveSolutionPrinter() + status = solver.solve(model, objective_printer) + + ### Output the solution. + if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): + print( + f"Loss = {solver.objective_value}, time = {solver.wall_time} s," + f" {solver.num_conflicts} conflicts" + ) + else: + print("No solution") def collect_valid_slabs_dp( @@ -313,264 +315,269 @@ def collect_valid_slabs_dp( widths: list[int], loss_array: list[int], ) -> list[list[int]]: - """Collect valid columns (assign, loss) for one slab.""" - start_time = time.time() - - max_capacity = max(capacities) - - valid_assignment = collections.namedtuple("valid_assignment", "orders load colors") - all_valid_assignments = [valid_assignment(orders=[], load=0, colors=[])] - - for order_id, new_color in enumerate(colors): - new_width = widths[order_id] - new_assignments = [] - for assignment in all_valid_assignments: - if assignment.load + new_width > max_capacity: - continue - new_colors = list(assignment.colors) - if new_color not in new_colors: - new_colors.append(new_color) - if len(new_colors) > 2: - continue - new_assignment = valid_assignment( - orders=assignment.orders + [order_id], - load=assignment.load + new_width, - colors=new_colors, - ) - new_assignments.append(new_assignment) - all_valid_assignments.extend(new_assignments) + """Collect valid columns (assign, loss) for one slab.""" + start_time = time.time() - print( - f"{len(all_valid_assignments)} assignments created in" - f" {time.time() - start_time:2f} s" - ) - tuples = [] - for assignment in all_valid_assignments: - solution = [0] * len(colors) - for i in assignment.orders: - solution[i] = 1 - solution.append(loss_array[assignment.load]) - solution.append(assignment.load) - tuples.append(solution) - - return tuples - - -def steel_mill_slab_with_valid_slabs(problem_id: int, break_symmetries: bool) -> None: - """Solves the Steel Mill Slab Problem.""" - ### Load problem. - (num_slabs, capacities, num_colors, orders) = build_problem(problem_id) - - num_orders = len(orders) - num_capacities = len(capacities) - all_slabs = range(num_slabs) - all_colors = range(num_colors) - all_orders = range(len(orders)) - print( - f"Solving steel mill with {num_orders} orders, {num_slabs} slabs, and" - f" {num_capacities - 1} capacities" - ) - - # Compute auxiliary data. - widths = [x[0] for x in orders] - colors = [x[1] for x in orders] - max_capacity = max(capacities) - loss_array = [ - min(x for x in capacities if x >= c) - c for c in range(max_capacity + 1) - ] - max_loss = max(loss_array) + max_capacity = max(capacities) - ### Model problem. - - # Create the model and the decision variables. - model = cp_model.CpModel() - assign = [ - [model.new_bool_var(r"assign_{o}_to_slab_{s}") for s in all_slabs] - for o in all_orders - ] - loads = [model.new_int_var(0, max_capacity, f"load_{s}") for s in all_slabs] - losses = [model.new_int_var(0, max_loss, f"loss_{s}") for s in all_slabs] + valid_assignment = collections.namedtuple( + "valid_assignment", "orders load colors" + ) + all_valid_assignments = [valid_assignment(orders=[], load=0, colors=[])] - unsorted_valid_slabs = collect_valid_slabs_dp( - capacities, colors, widths, loss_array + for order_id, new_color in enumerate(colors): + new_width = widths[order_id] + new_assignments = [] + for assignment in all_valid_assignments: + if assignment.load + new_width > max_capacity: + continue + new_colors = list(assignment.colors) + if new_color not in new_colors: + new_colors.append(new_color) + if len(new_colors) > 2: + continue + new_assignment = valid_assignment( + orders=assignment.orders + [order_id], + load=assignment.load + new_width, + colors=new_colors, + ) + new_assignments.append(new_assignment) + all_valid_assignments.extend(new_assignments) + + print( + f"{len(all_valid_assignments)} assignments created in" + f" {time.time() - start_time:2f} s" + ) + tuples = [] + for assignment in all_valid_assignments: + solution = [0] * len(colors) + for i in assignment.orders: + solution[i] = 1 + solution.append(loss_array[assignment.load]) + solution.append(assignment.load) + tuples.append(solution) + + return tuples + + +def steel_mill_slab_with_valid_slabs( + problem_id: int, break_symmetries: bool +) -> None: + """Solves the Steel Mill Slab Problem.""" + ### Load problem. + (num_slabs, capacities, num_colors, orders) = build_problem(problem_id) + + num_orders = len(orders) + num_capacities = len(capacities) + all_slabs = range(num_slabs) + all_colors = range(num_colors) + all_orders = range(len(orders)) + print( + f"Solving steel mill with {num_orders} orders, {num_slabs} slabs, and" + f" {num_capacities - 1} capacities" + ) + + # Compute auxiliary data. + widths = [x[0] for x in orders] + colors = [x[1] for x in orders] + max_capacity = max(capacities) + loss_array = [ + min(x for x in capacities if x >= c) - c for c in range(max_capacity + 1) + ] + max_loss = max(loss_array) + + ### Model problem. + + # Create the model and the decision variables. + model = cp_model.CpModel() + assign = [ + [model.new_bool_var(r"assign_{o}_to_slab_{s}") for s in all_slabs] + for o in all_orders + ] + loads = [model.new_int_var(0, max_capacity, f"load_{s}") for s in all_slabs] + losses = [model.new_int_var(0, max_loss, f"loss_{s}") for s in all_slabs] + + unsorted_valid_slabs = collect_valid_slabs_dp( + capacities, colors, widths, loss_array + ) + # Sort slab by descending load/loss. Remove duplicates. + valid_slabs = sorted(unsorted_valid_slabs, key=lambda c: 1000 * c[-1] + c[-2]) + + for s in all_slabs: + model.add_allowed_assignments( + [assign[o][s] for o in all_orders] + [losses[s], loads[s]], valid_slabs ) - # Sort slab by descending load/loss. Remove duplicates. - valid_slabs = sorted(unsorted_valid_slabs, key=lambda c: 1000 * c[-1] + c[-2]) - - for s in all_slabs: - model.add_allowed_assignments( - [assign[o][s] for o in all_orders] + [losses[s], loads[s]], valid_slabs - ) - - # Orders are assigned to one slab. - for o in all_orders: - model.add_exactly_one(assign[o]) - - # Redundant constraint (sum of loads == sum of widths). - model.add(sum(loads) == sum(widths)) - - # Symmetry breaking. - for s in range(num_slabs - 1): - model.add(loads[s] >= loads[s + 1]) - - # Collect equivalent orders. - if break_symmetries: - print("Breaking symmetries") - width_to_unique_color_order = {} - ordered_equivalent_orders = [] - orders_per_color = [ - [o for o in all_orders if colors[o] == c + 1] for c in all_colors - ] - for c in all_colors: - colored_orders = orders_per_color[c] - if not colored_orders: - continue - if len(colored_orders) == 1: - o = colored_orders[0] - w = widths[o] - if w not in width_to_unique_color_order: - width_to_unique_color_order[w] = [o] - else: - width_to_unique_color_order[w].append(o) - else: - local_width_to_order = {} - for o in colored_orders: - w = widths[o] - if w not in local_width_to_order: - local_width_to_order[w] = [] - local_width_to_order[w].append(o) - for _, os in local_width_to_order.items(): - if len(os) > 1: - for p in range(len(os) - 1): - ordered_equivalent_orders.append((os[p], os[p + 1])) - for _, os in width_to_unique_color_order.items(): - if len(os) > 1: - for p in range(len(os) - 1): - ordered_equivalent_orders.append((os[p], os[p + 1])) - - # Create position variables if there are symmetries to be broken. - if ordered_equivalent_orders: - print( - f" - creating {len(ordered_equivalent_orders)} symmetry breaking" - " constraints" - ) - positions = {} - for p in ordered_equivalent_orders: - if p[0] not in positions: - positions[p[0]] = model.new_int_var( - 0, num_slabs - 1, f"position_of_slab_{p[0]}" - ) - model.add_map_domain(positions[p[0]], assign[p[0]]) - if p[1] not in positions: - positions[p[1]] = model.new_int_var( - 0, num_slabs - 1, f"position_of_slab_{p[1]}" - ) - model.add_map_domain(positions[p[1]], assign[p[1]]) - # Finally add the symmetry breaking constraint. - model.add(positions[p[0]] <= positions[p[1]]) - - # Objective. - model.minimize(sum(losses)) - - print("Model created") - - ### Solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - - solution_printer = SteelMillSlabSolutionPrinter(orders, assign, loads, losses) - status = solver.solve(model, solution_printer) - - ### Output the solution. - if status == cp_model.OPTIMAL: - print( - f"Loss = {solver.objective_value}, time = {solver.wall_time:2f} s," - f" {solver.num_conflicts} conflicts" - ) - else: - print("No solution") + # Orders are assigned to one slab. + for o in all_orders: + model.add_exactly_one(assign[o]) -def steel_mill_slab_with_column_generation(problem_id: int) -> None: - """Solves the Steel Mill Slab Problem.""" - ### Load problem. - (num_slabs, capacities, _, orders) = build_problem(problem_id) + # Redundant constraint (sum of loads == sum of widths). + model.add(sum(loads) == sum(widths)) - num_orders = len(orders) - num_capacities = len(capacities) - all_orders = range(len(orders)) - print( - f"Solving steel mill with {num_orders} orders, {num_slabs} slabs, and" - f" {num_capacities - 1} capacities" - ) + # Symmetry breaking. + for s in range(num_slabs - 1): + model.add(loads[s] >= loads[s + 1]) - # Compute auxiliary data. - widths = [x[0] for x in orders] - colors = [x[1] for x in orders] - max_capacity = max(capacities) - loss_array = [ - min(x for x in capacities if x >= c) - c for c in range(max_capacity + 1) + # Collect equivalent orders. + if break_symmetries: + print("Breaking symmetries") + width_to_unique_color_order = {} + ordered_equivalent_orders = [] + orders_per_color = [ + [o for o in all_orders if colors[o] == c + 1] for c in all_colors ] + for c in all_colors: + colored_orders = orders_per_color[c] + if not colored_orders: + continue + if len(colored_orders) == 1: + o = colored_orders[0] + w = widths[o] + if w not in width_to_unique_color_order: + width_to_unique_color_order[w] = [o] + else: + width_to_unique_color_order[w].append(o) + else: + local_width_to_order = {} + for o in colored_orders: + w = widths[o] + if w not in local_width_to_order: + local_width_to_order[w] = [] + local_width_to_order[w].append(o) + for _, os in local_width_to_order.items(): + if len(os) > 1: + for p in range(len(os) - 1): + ordered_equivalent_orders.append((os[p], os[p + 1])) + for _, os in width_to_unique_color_order.items(): + if len(os) > 1: + for p in range(len(os) - 1): + ordered_equivalent_orders.append((os[p], os[p + 1])) - ### Model problem. - - # Generate all valid slabs (columns) - unsorted_valid_slabs = collect_valid_slabs_dp( - capacities, colors, widths, loss_array + # Create position variables if there are symmetries to be broken. + if ordered_equivalent_orders: + print( + f" - creating {len(ordered_equivalent_orders)} symmetry breaking" + " constraints" + ) + positions = {} + for p in ordered_equivalent_orders: + if p[0] not in positions: + positions[p[0]] = model.new_int_var( + 0, num_slabs - 1, f"position_of_slab_{p[0]}" + ) + model.add_map_domain(positions[p[0]], assign[p[0]]) + if p[1] not in positions: + positions[p[1]] = model.new_int_var( + 0, num_slabs - 1, f"position_of_slab_{p[1]}" + ) + model.add_map_domain(positions[p[1]], assign[p[1]]) + # Finally add the symmetry breaking constraint. + model.add(positions[p[0]] <= positions[p[1]]) + + # Objective. + model.minimize(sum(losses)) + + print("Model created") + + ### Solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + + solution_printer = SteelMillSlabSolutionPrinter(orders, assign, loads, losses) + status = solver.solve(model, solution_printer) + + ### Output the solution. + if status == cp_model.OPTIMAL: + print( + f"Loss = {solver.objective_value}, time = {solver.wall_time:2f} s," + f" {solver.num_conflicts} conflicts" ) + else: + print("No solution") - # Sort slab by descending load/loss. Remove duplicates. - valid_slabs = sorted(unsorted_valid_slabs, key=lambda c: 1000 * c[-1] + c[-2]) - all_valid_slabs = range(len(valid_slabs)) - # create model and decision variables. - model = cp_model.CpModel() - selected = [model.new_bool_var(f"selected_{i}") for i in all_valid_slabs] - - for order_id in all_orders: - model.add( - sum(selected[i] for i, slab in enumerate(valid_slabs) if slab[order_id]) - == 1 - ) - - # Redundant constraint (sum of loads == sum of widths). +def steel_mill_slab_with_column_generation(problem_id: int) -> None: + """Solves the Steel Mill Slab Problem.""" + ### Load problem. + (num_slabs, capacities, _, orders) = build_problem(problem_id) + + num_orders = len(orders) + num_capacities = len(capacities) + all_orders = range(len(orders)) + print( + f"Solving steel mill with {num_orders} orders, {num_slabs} slabs, and" + f" {num_capacities - 1} capacities" + ) + + # Compute auxiliary data. + widths = [x[0] for x in orders] + colors = [x[1] for x in orders] + max_capacity = max(capacities) + loss_array = [ + min(x for x in capacities if x >= c) - c for c in range(max_capacity + 1) + ] + + ### Model problem. + + # Generate all valid slabs (columns) + unsorted_valid_slabs = collect_valid_slabs_dp( + capacities, colors, widths, loss_array + ) + + # Sort slab by descending load/loss. Remove duplicates. + valid_slabs = sorted(unsorted_valid_slabs, key=lambda c: 1000 * c[-1] + c[-2]) + all_valid_slabs = range(len(valid_slabs)) + + # create model and decision variables. + model = cp_model.CpModel() + selected = [model.new_bool_var(f"selected_{i}") for i in all_valid_slabs] + + for order_id in all_orders: model.add( - sum(selected[i] * valid_slabs[i][-1] for i in all_valid_slabs) == sum(widths) + sum(selected[i] for i, slab in enumerate(valid_slabs) if slab[order_id]) + == 1 ) - # Objective. - model.minimize(sum(selected[i] * valid_slabs[i][-2] for i in all_valid_slabs)) + # Redundant constraint (sum of loads == sum of widths). + model.add( + sum(selected[i] * valid_slabs[i][-1] for i in all_valid_slabs) + == sum(widths) + ) - print("Model created") + # Objective. + model.minimize(sum(selected[i] * valid_slabs[i][-2] for i in all_valid_slabs)) - ### Solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - solution_printer = cp_model.ObjectiveSolutionPrinter() - status = solver.solve(model, solution_printer) + print("Model created") - ### Output the solution. - if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): - print( - f"Loss = {solver.objective_value}, time = {solver.wall_time:2f} s," - f" {solver.num_conflicts} conflicts" - ) - else: - print("No solution") + ### Solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + solution_printer = cp_model.ObjectiveSolutionPrinter() + status = solver.solve(model, solution_printer) + + ### Output the solution. + if status in (cp_model.OPTIMAL, cp_model.FEASIBLE): + print( + f"Loss = {solver.objective_value}, time = {solver.wall_time:2f} s," + f" {solver.num_conflicts} conflicts" + ) + else: + print("No solution") def main(_): - if _SOLVER.value == "sat": - steel_mill_slab(_PROBLEM.value, _BREAK_SYMMETRIES.value) - elif _SOLVER.value == "sat_table": - steel_mill_slab_with_valid_slabs(_PROBLEM.value, _BREAK_SYMMETRIES.value) - elif _SOLVER.value == "sat_column": - steel_mill_slab_with_column_generation(_PROBLEM.value) - else: - print(f"Unknown model {_SOLVER.value}") + if _SOLVER.value == "sat": + steel_mill_slab(_PROBLEM.value, _BREAK_SYMMETRIES.value) + elif _SOLVER.value == "sat_table": + steel_mill_slab_with_valid_slabs(_PROBLEM.value, _BREAK_SYMMETRIES.value) + elif _SOLVER.value == "sat_column": + steel_mill_slab_with_column_generation(_PROBLEM.value) + else: + print(f"Unknown model {_SOLVER.value}") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/sudoku_sat.py b/examples/python/sudoku_sat.py index 069322242cd..ebda54126be 100755 --- a/examples/python/sudoku_sat.py +++ b/examples/python/sudoku_sat.py @@ -18,62 +18,62 @@ def solve_sudoku() -> None: - """Solves the sudoku problem with the CP-SAT solver.""" - # Create the model. - model = cp_model.CpModel() + """Solves the sudoku problem with the CP-SAT solver.""" + # Create the model. + model = cp_model.CpModel() - cell_size = 3 - line_size = cell_size**2 - line = list(range(0, line_size)) - cell = list(range(0, cell_size)) + cell_size = 3 + line_size = cell_size**2 + line = list(range(0, line_size)) + cell = list(range(0, cell_size)) - initial_grid = [ - [0, 6, 0, 0, 5, 0, 0, 2, 0], - [0, 0, 0, 3, 0, 0, 0, 9, 0], - [7, 0, 0, 6, 0, 0, 0, 1, 0], - [0, 0, 6, 0, 3, 0, 4, 0, 0], - [0, 0, 4, 0, 7, 0, 1, 0, 0], - [0, 0, 5, 0, 9, 0, 8, 0, 0], - [0, 4, 0, 0, 0, 1, 0, 0, 6], - [0, 3, 0, 0, 0, 8, 0, 0, 0], - [0, 2, 0, 0, 4, 0, 0, 5, 0], - ] + initial_grid = [ + [0, 6, 0, 0, 5, 0, 0, 2, 0], + [0, 0, 0, 3, 0, 0, 0, 9, 0], + [7, 0, 0, 6, 0, 0, 0, 1, 0], + [0, 0, 6, 0, 3, 0, 4, 0, 0], + [0, 0, 4, 0, 7, 0, 1, 0, 0], + [0, 0, 5, 0, 9, 0, 8, 0, 0], + [0, 4, 0, 0, 0, 1, 0, 0, 6], + [0, 3, 0, 0, 0, 8, 0, 0, 0], + [0, 2, 0, 0, 4, 0, 0, 5, 0], + ] - grid = {} - for i in line: - for j in line: - grid[(i, j)] = model.new_int_var(1, line_size, "grid %i %i" % (i, j)) + grid = {} + for i in line: + for j in line: + grid[(i, j)] = model.new_int_var(1, line_size, "grid %i %i" % (i, j)) - # AllDifferent on rows. - for i in line: - model.add_all_different(grid[(i, j)] for j in line) + # AllDifferent on rows. + for i in line: + model.add_all_different(grid[(i, j)] for j in line) - # AllDifferent on columns. - for j in line: - model.add_all_different(grid[(i, j)] for i in line) + # AllDifferent on columns. + for j in line: + model.add_all_different(grid[(i, j)] for i in line) - # AllDifferent on cells. - for i in cell: - for j in cell: - one_cell = [] - for di in cell: - for dj in cell: - one_cell.append(grid[(i * cell_size + di, j * cell_size + dj)]) + # AllDifferent on cells. + for i in cell: + for j in cell: + one_cell = [] + for di in cell: + for dj in cell: + one_cell.append(grid[(i * cell_size + di, j * cell_size + dj)]) - model.add_all_different(one_cell) + model.add_all_different(one_cell) - # Initial values. - for i in line: - for j in line: - if initial_grid[i][j]: - model.add(grid[(i, j)] == initial_grid[i][j]) + # Initial values. + for i in line: + for j in line: + if initial_grid[i][j]: + model.add(grid[(i, j)] == initial_grid[i][j]) - # Solves and prints out the solution. - solver = cp_model.CpSolver() - status = solver.solve(model) - if status == cp_model.OPTIMAL: - for i in line: - print([int(solver.value(grid[(i, j)])) for j in line]) + # Solves and prints out the solution. + solver = cp_model.CpSolver() + status = solver.solve(model) + if status == cp_model.OPTIMAL: + for i in line: + print([int(solver.value(grid[(i, j)])) for j in line]) solve_sudoku() diff --git a/examples/python/task_allocation_sat.py b/examples/python/task_allocation_sat.py index c35b9c6e20e..ab7448ca5cf 100644 --- a/examples/python/task_allocation_sat.py +++ b/examples/python/task_allocation_sat.py @@ -24,10 +24,10 @@ def task_allocation_sat() -> None: - """Solves the task allocation problem.""" - # Availability matrix. - available = [ - # fmt:off + """Solves the task allocation problem.""" + # Availability matrix. + available = [ + # fmt:off [ 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 @@ -228,71 +228,75 @@ def task_allocation_sat() -> None: 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ], - # fmt:on - ] + # fmt:on + ] - ntasks = len(available) - nslots = len(available[0]) + ntasks = len(available) + nslots = len(available[0]) - # sets - all_tasks = range(ntasks) - all_slots = range(nslots) + # sets + all_tasks = range(ntasks) + all_slots = range(nslots) - # max tasks per time slot - capacity = 3 + # max tasks per time slot + capacity = 3 - # Model - model = cp_model.CpModel() - assign = {} - for task in all_tasks: - for slot in all_slots: - assign[(task, slot)] = model.new_bool_var(f"x[{task}][{slot}]") - count = model.new_int_var(0, nslots, "count") - slot_used = [model.new_bool_var(f"slot_used[{s}]") for s in all_slots] + # Model + model = cp_model.CpModel() + assign = {} + for task in all_tasks: + for slot in all_slots: + assign[(task, slot)] = model.new_bool_var(f"x[{task}][{slot}]") + count = model.new_int_var(0, nslots, "count") + slot_used = [model.new_bool_var(f"slot_used[{s}]") for s in all_slots] - for task in all_tasks: - model.add( - sum( - assign[(task, slot)] for slot in all_slots if available[task][slot] == 1 - ) - == 1 + for task in all_tasks: + model.add( + sum( + assign[(task, slot)] + for slot in all_slots + if available[task][slot] == 1 ) + == 1 + ) - for slot in all_slots: - model.add( - sum( - assign[(task, slot)] for task in all_tasks if available[task][slot] == 1 - ) - <= capacity + for slot in all_slots: + model.add( + sum( + assign[(task, slot)] + for task in all_tasks + if available[task][slot] == 1 ) - model.add_bool_or( - [assign[(task, slot)] for task in all_tasks if available[task][slot] == 1] - ).only_enforce_if(slot_used[slot]) - for task in all_tasks: - if available[task][slot] == 1: - model.add_implication(~slot_used[slot], ~assign[(task, slot)]) - else: - model.add(assign[(task, slot)] == 0) + <= capacity + ) + model.add_bool_or([ + assign[(task, slot)] for task in all_tasks if available[task][slot] == 1 + ]).only_enforce_if(slot_used[slot]) + for task in all_tasks: + if available[task][slot] == 1: + model.add_implication(~slot_used[slot], ~assign[(task, slot)]) + else: + model.add(assign[(task, slot)] == 0) - model.add(count == sum(slot_used)) - # Redundant constraint. This instance is easier if we add this constraint. - # model.add(count >= (nslots + capacity - 1) // capacity) + model.add(count == sum(slot_used)) + # Redundant constraint. This instance is easier if we add this constraint. + # model.add(count >= (nslots + capacity - 1) // capacity) - model.minimize(count) + model.minimize(count) - # Create a solver and solve the problem. - solver = cp_model.CpSolver() - # Uses the portfolion of heuristics. - solver.parameters.log_search_progress = True - solver.parameters.num_search_workers = 16 - solver.solve(model) + # Create a solver and solve the problem. + solver = cp_model.CpSolver() + # Uses the portfolion of heuristics. + solver.parameters.log_search_progress = True + solver.parameters.num_search_workers = 16 + solver.solve(model) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - task_allocation_sat() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + task_allocation_sat() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/tasks_and_workers_assignment_sat.py b/examples/python/tasks_and_workers_assignment_sat.py index bdb8983087a..1fe7abfa04d 100644 --- a/examples/python/tasks_and_workers_assignment_sat.py +++ b/examples/python/tasks_and_workers_assignment_sat.py @@ -20,114 +20,120 @@ class ObjectivePrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 + def __init__(self): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 - def on_solution_callback(self): - print( - "Solution %i, time = %f s, objective = %i" - % (self.__solution_count, self.wall_time, self.objective_value) - ) - self.__solution_count += 1 + def on_solution_callback(self): + print( + "Solution %i, time = %f s, objective = %i" + % (self.__solution_count, self.wall_time, self.objective_value) + ) + self.__solution_count += 1 def tasks_and_workers_assignment_sat() -> None: - """solve the assignment problem.""" - model = cp_model.CpModel() - - # CP-SAT solver is integer only. - task_cost = [24, 10, 7, 2, 11, 16, 1, 13, 9, 27] - num_tasks = len(task_cost) - num_workers = 3 - num_groups = 2 - all_workers = range(num_workers) - all_groups = range(num_groups) - all_tasks = range(num_tasks) - - # Variables - - ## x_ij = 1 if worker i is assigned to group j - x = {} - for i in all_workers: - for j in all_groups: - x[i, j] = model.new_bool_var("x[%i,%i]" % (i, j)) - - ## y_kj is 1 if task k is assigned to group j - y = {} - for k in all_tasks: - for j in all_groups: - y[k, j] = model.new_bool_var("x[%i,%i]" % (k, j)) - - # Constraints - - # Each task k is assigned to a group and only one. - for k in all_tasks: - model.add(sum(y[k, j] for j in all_groups) == 1) - - # Each worker i is assigned to a group and only one. - for i in all_workers: - model.add(sum(x[i, j] for j in all_groups) == 1) - - # Cost per group - sum_of_costs = sum(task_cost) - averages = [] - num_workers_in_group = [] - scaled_sum_of_costs_in_group = [] - scaling = 1000 # We introduce scaling to deal with floating point average. + """solve the assignment problem.""" + model = cp_model.CpModel() + + # CP-SAT solver is integer only. + task_cost = [24, 10, 7, 2, 11, 16, 1, 13, 9, 27] + num_tasks = len(task_cost) + num_workers = 3 + num_groups = 2 + all_workers = range(num_workers) + all_groups = range(num_groups) + all_tasks = range(num_tasks) + + # Variables + + ## x_ij = 1 if worker i is assigned to group j + x = {} + for i in all_workers: for j in all_groups: - n = model.new_int_var(1, num_workers, "num_workers_in_group_%i" % j) - model.add(n == sum(x[i, j] for i in all_workers)) - c = model.new_int_var(0, sum_of_costs * scaling, "sum_of_costs_of_group_%i" % j) - model.add(c == sum(y[k, j] * task_cost[k] * scaling for k in all_tasks)) - a = model.new_int_var(0, sum_of_costs * scaling, "average_cost_of_group_%i" % j) - model.add_division_equality(a, c, n) - - averages.append(a) - num_workers_in_group.append(n) - scaled_sum_of_costs_in_group.append(c) - - # All workers are assigned. - model.add(sum(num_workers_in_group) == num_workers) - - # Objective. - obj = model.new_int_var(0, sum_of_costs * scaling, "obj") - model.add_max_equality(obj, averages) - model.minimize(obj) - - # Solve and print out the solution. - solver = cp_model.CpSolver() - solver.parameters.max_time_in_seconds = 60 * 60 * 2 - objective_printer = ObjectivePrinter() - status = solver.solve(model, objective_printer) - print(solver.response_stats()) - - if status == cp_model.OPTIMAL: - for j in all_groups: - print("Group %i" % j) - for i in all_workers: - if solver.boolean_value(x[i, j]): - print(" - worker %i" % i) - for k in all_tasks: - if solver.boolean_value(y[k, j]): - print(" - task %i with cost %i" % (k, task_cost[k])) - print( - " - sum_of_costs = %i" - % (solver.value(scaled_sum_of_costs_in_group[j]) // scaling) - ) - print(" - average cost = %f" % (solver.value(averages[j]) * 1.0 / scaling)) + x[i, j] = model.new_bool_var("x[%i,%i]" % (i, j)) + + ## y_kj is 1 if task k is assigned to group j + y = {} + for k in all_tasks: + for j in all_groups: + y[k, j] = model.new_bool_var("x[%i,%i]" % (k, j)) + + # Constraints + + # Each task k is assigned to a group and only one. + for k in all_tasks: + model.add(sum(y[k, j] for j in all_groups) == 1) + + # Each worker i is assigned to a group and only one. + for i in all_workers: + model.add(sum(x[i, j] for j in all_groups) == 1) + + # Cost per group + sum_of_costs = sum(task_cost) + averages = [] + num_workers_in_group = [] + scaled_sum_of_costs_in_group = [] + scaling = 1000 # We introduce scaling to deal with floating point average. + for j in all_groups: + n = model.new_int_var(1, num_workers, "num_workers_in_group_%i" % j) + model.add(n == sum(x[i, j] for i in all_workers)) + c = model.new_int_var( + 0, sum_of_costs * scaling, "sum_of_costs_of_group_%i" % j + ) + model.add(c == sum(y[k, j] * task_cost[k] * scaling for k in all_tasks)) + a = model.new_int_var( + 0, sum_of_costs * scaling, "average_cost_of_group_%i" % j + ) + model.add_division_equality(a, c, n) + + averages.append(a) + num_workers_in_group.append(n) + scaled_sum_of_costs_in_group.append(c) + + # All workers are assigned. + model.add(sum(num_workers_in_group) == num_workers) + + # Objective. + obj = model.new_int_var(0, sum_of_costs * scaling, "obj") + model.add_max_equality(obj, averages) + model.minimize(obj) + + # Solve and print out the solution. + solver = cp_model.CpSolver() + solver.parameters.max_time_in_seconds = 60 * 60 * 2 + objective_printer = ObjectivePrinter() + status = solver.solve(model, objective_printer) + print(solver.response_stats()) + + if status == cp_model.OPTIMAL: + for j in all_groups: + print("Group %i" % j) + for i in all_workers: + if solver.boolean_value(x[i, j]): + print(" - worker %i" % i) + for k in all_tasks: + if solver.boolean_value(y[k, j]): + print(" - task %i with cost %i" % (k, task_cost[k])) + print( + " - sum_of_costs = %i" + % (solver.value(scaled_sum_of_costs_in_group[j]) // scaling) + ) + print( + " - average cost = %f" % (solver.value(averages[j]) * 1.0 / scaling) + ) tasks_and_workers_assignment_sat() def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - tasks_and_workers_assignment_sat() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + tasks_and_workers_assignment_sat() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/test_scheduling_sat.py b/examples/python/test_scheduling_sat.py index fdec2ba13bd..62d037d49bb 100644 --- a/examples/python/test_scheduling_sat.py +++ b/examples/python/test_scheduling_sat.py @@ -45,8 +45,8 @@ def build_data() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: - """Build the data frame.""" - tests_str = """ + """Build the data frame.""" + tests_str = """ Name Operator TestTime AveragePower T1 O1 300 200 T2 O1 150 40 @@ -55,24 +55,24 @@ def build_data() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: T5 O3 210 140 """ - operators_str = """ + operators_str = """ Operator Supply O1 S1 O2 S2 O3 S2 """ - supplies_str = """ + supplies_str = """ Supply MaxAllowedPower S1 230 S2 210 """ - tests_data = pd.read_table(io.StringIO(tests_str), sep=r"\s+") - operators_data = pd.read_table(io.StringIO(operators_str), sep=r"\s+") - supplies_data = pd.read_table(io.StringIO(supplies_str), sep=r"\s+") + tests_data = pd.read_table(io.StringIO(tests_str), sep=r"\s+") + operators_data = pd.read_table(io.StringIO(operators_str), sep=r"\s+") + supplies_data = pd.read_table(io.StringIO(supplies_str), sep=r"\s+") - return (tests_data, operators_data, supplies_data) + return (tests_data, operators_data, supplies_data) def solve( @@ -80,97 +80,97 @@ def solve( operator_data: pd.DataFrame, supplies_data: pd.DataFrame, ) -> None: - """Solve the scheduling of tests problem.""" - - # Parses data. - operator_to_supply: Dict[str, str] = {} - for _, row in operator_data.iterrows(): - operator_to_supply[row["Operator"]] = row["Supply"] - - supply_to_max_power: Dict[str, int] = {} - for _, row in supplies_data.iterrows(): - supply_to_max_power[row["Supply"]] = row["MaxAllowedPower"] - - horizon = tests_data["TestTime"].sum() - - # OR-Tools model. - model = cp_model.CpModel() - - # Create containers. - tests_per_supply: Dict[str, Tuple[list[cp_model.IntervalVar], list[int]]] = {} - test_supply: Dict[str, str] = {} - test_starts: Dict[str, cp_model.IntVar] = {} - test_durations: Dict[str, int] = {} - test_powers: Dict[str, int] = {} - all_ends = [] - - # Creates intervals. - for _, row in tests_data.iterrows(): - name: str = row["Name"] - operator: str = row["Operator"] - test_time: int = row["TestTime"] - average_power: int = row["AveragePower"] - supply: str = operator_to_supply[operator] - - start = model.new_int_var(0, horizon - test_time, f"start_{name}") - interval = model.new_fixed_size_interval_var( - start, test_time, f"interval_{name}" - ) - - # Bookkeeping. - test_starts[name] = start - test_durations[name] = test_time - test_powers[name] = average_power - test_supply[name] = supply - if supply not in tests_per_supply.keys(): - tests_per_supply[supply] = ([], []) - tests_per_supply[supply][0].append(interval) - tests_per_supply[supply][1].append(average_power) - all_ends.append(start + test_time) - - # Create supply cumulative constraints. - for supply, (intervals, demands) in tests_per_supply.items(): - model.add_cumulative(intervals, demands, supply_to_max_power[supply]) - - # Objective. - makespan = model.new_int_var(0, horizon, "makespan") - for end in all_ends: - model.add(makespan >= end) - model.minimize(makespan) - - # Solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - status = solver.solve(model) - - # Report solution. - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print(f"Makespan = {solver.value(makespan)}") - for name, start in test_starts.items(): - print( - f"{name}: start:{solver.value(start)} duration:{test_durations[name]}" - f" power:{test_powers[name]} on supply {test_supply[name]}" - ) + """Solve the scheduling of tests problem.""" + + # Parses data. + operator_to_supply: Dict[str, str] = {} + for _, row in operator_data.iterrows(): + operator_to_supply[row["Operator"]] = row["Supply"] + + supply_to_max_power: Dict[str, int] = {} + for _, row in supplies_data.iterrows(): + supply_to_max_power[row["Supply"]] = row["MaxAllowedPower"] + + horizon = tests_data["TestTime"].sum() + + # OR-Tools model. + model = cp_model.CpModel() + + # Create containers. + tests_per_supply: Dict[str, Tuple[list[cp_model.IntervalVar], list[int]]] = {} + test_supply: Dict[str, str] = {} + test_starts: Dict[str, cp_model.IntVar] = {} + test_durations: Dict[str, int] = {} + test_powers: Dict[str, int] = {} + all_ends = [] + + # Creates intervals. + for _, row in tests_data.iterrows(): + name: str = row["Name"] + operator: str = row["Operator"] + test_time: int = row["TestTime"] + average_power: int = row["AveragePower"] + supply: str = operator_to_supply[operator] + + start = model.new_int_var(0, horizon - test_time, f"start_{name}") + interval = model.new_fixed_size_interval_var( + start, test_time, f"interval_{name}" + ) + + # Bookkeeping. + test_starts[name] = start + test_durations[name] = test_time + test_powers[name] = average_power + test_supply[name] = supply + if supply not in tests_per_supply.keys(): + tests_per_supply[supply] = ([], []) + tests_per_supply[supply][0].append(interval) + tests_per_supply[supply][1].append(average_power) + all_ends.append(start + test_time) + + # Create supply cumulative constraints. + for supply, (intervals, demands) in tests_per_supply.items(): + model.add_cumulative(intervals, demands, supply_to_max_power[supply]) + + # Objective. + makespan = model.new_int_var(0, horizon, "makespan") + for end in all_ends: + model.add(makespan >= end) + model.minimize(makespan) + + # Solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + status = solver.solve(model) + + # Report solution. + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print(f"Makespan = {solver.value(makespan)}") + for name, start in test_starts.items(): + print( + f"{name}: start:{solver.value(start)} duration:{test_durations[name]}" + f" power:{test_powers[name]} on supply {test_supply[name]}" + ) def main(argv: Sequence[str]) -> None: - """Builds the data and solve the scheduling problem.""" - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") + """Builds the data and solve the scheduling problem.""" + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") - tests_data, operators_data, supplies_data = build_data() - print("Tests data") - print(tests_data) - print() - print("Operators data") - print(operators_data) - print() - print("Supplies data") - print(supplies_data) + tests_data, operators_data, supplies_data = build_data() + print("Tests data") + print(tests_data) + print() + print("Operators data") + print(operators_data) + print() + print("Supplies data") + print(supplies_data) - solve(tests_data, operators_data, supplies_data) + solve(tests_data, operators_data, supplies_data) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/transit_time.py b/examples/python/transit_time.py index bfc5551e2cc..28d06594069 100755 --- a/examples/python/transit_time.py +++ b/examples/python/transit_time.py @@ -14,211 +14,243 @@ # See the License for the specific language governing permissions and # limitations under the License. """Display Transit Time - Distances are in meters and time in minutes. +Distances are in meters and time in minutes. - Manhattan average block: 750ft x 264ft -> 228m x 80m - src: https://nyti.ms/2GDoRIe "NY Times: Know Your distance" - here we use: 114m x 80m city block +Manhattan average block: 750ft x 264ft -> 228m x 80m +src: https://nyti.ms/2GDoRIe "NY Times: Know Your distance" +here we use: 114m x 80m city block """ from ortools.constraint_solver import pywrapcp -from ortools.constraint_solver import routing_enums_pb2 ########################### # Problem Data Definition # ########################### -class Vehicle(): - """Stores the property of a vehicle""" - - def __init__(self): - """Initializes the vehicle properties""" - self._capacity = 15 - # Travel speed: 5km/h to convert in m/min - self._speed = 5 * 60 / 3.6 - - @property - def speed(self): - """Gets the average travel speed of a vehicle""" - return self._speed - - -class CityBlock(): - """City block definition""" - - @property - def width(self): - """Gets Block size West to East""" - return 228 / 2 - - @property - def height(self): - """Gets Block size North to South""" - return 80 - - -class DataProblem(): - """Stores the data for the problem""" - - def __init__(self): - """Initializes the data for the problem""" - self._vehicle = Vehicle() - - # Locations in block unit - locations = \ - [(4, 4), # depot - (2, 0), (8, 0), # row 0 - (0, 1), (1, 1), - (5, 2), (7, 2), - (3, 3), (6, 3), - (5, 5), (8, 5), - (1, 6), (2, 6), - (3, 7), (6, 7), - (0, 8), (7, 8)] - # locations in meters using the city block dimension - city_block = CityBlock() - self._locations = [(loc[0] * city_block.width, - loc[1] * city_block.height) for loc in locations] - - self._depot = 0 - - self._demands = \ - [0, # depot - 1, 1, # 1, 2 - 2, 4, # 3, 4 - 2, 4, # 5, 6 - 8, 8, # 7, 8 - 1, 2, # 9,10 - 1, 2, # 11,12 - 4, 4, # 13, 14 - 8, 8] # 15, 16 - - self._time_windows = \ - [(0, 0), - (75, 85), (75, 85), # 1, 2 - (60, 70), (45, 55), # 3, 4 - (0, 8), (50, 60), # 5, 6 - (0, 10), (10, 20), # 7, 8 - (0, 10), (75, 85), # 9, 10 - (85, 95), (5, 15), # 11, 12 - (15, 25), (10, 20), # 13, 14 - (45, 55), (30, 40)] # 15, 16 - - @property - def vehicle(self): - """Gets a vehicle""" - return self._vehicle - - @property - def locations(self): - """Gets locations""" - return self._locations - - @property - def num_locations(self): - """Gets number of locations""" - return len(self.locations) - - @property - def depot(self): - """Gets depot location index""" - return self._depot - - @property - def demands(self): - """Gets demands at each location""" - return self._demands - - @property - def time_per_demand_unit(self): - """Gets the time (in min) to load a demand""" - return 5 # 5 minutes/unit - - @property - def time_windows(self): - """Gets (start time, end time) for each locations""" - return self._time_windows +class Vehicle: + """Stores the property of a vehicle""" + + def __init__(self): + """Initializes the vehicle properties""" + self._capacity = 15 + # Travel speed: 5km/h to convert in m/min + self._speed = 5 * 60 / 3.6 + + @property + def speed(self): + """Gets the average travel speed of a vehicle""" + return self._speed + + +class CityBlock: + """City block definition""" + + @property + def width(self): + """Gets Block size West to East""" + return 228 / 2 + + @property + def height(self): + """Gets Block size North to South""" + return 80 + + +class DataProblem: + """Stores the data for the problem""" + + def __init__(self): + """Initializes the data for the problem""" + self._vehicle = Vehicle() + + # Locations in block unit + locations = [ + (4, 4), # depot + (2, 0), + (8, 0), # row 0 + (0, 1), + (1, 1), + (5, 2), + (7, 2), + (3, 3), + (6, 3), + (5, 5), + (8, 5), + (1, 6), + (2, 6), + (3, 7), + (6, 7), + (0, 8), + (7, 8), + ] + # locations in meters using the city block dimension + city_block = CityBlock() + self._locations = [ + (loc[0] * city_block.width, loc[1] * city_block.height) + for loc in locations + ] + + self._depot = 0 + + self._demands = [ + 0, # depot + 1, + 1, # 1, 2 + 2, + 4, # 3, 4 + 2, + 4, # 5, 6 + 8, + 8, # 7, 8 + 1, + 2, # 9,10 + 1, + 2, # 11,12 + 4, + 4, # 13, 14 + 8, + 8, + ] # 15, 16 + + self._time_windows = [ + (0, 0), + (75, 85), + (75, 85), # 1, 2 + (60, 70), + (45, 55), # 3, 4 + (0, 8), + (50, 60), # 5, 6 + (0, 10), + (10, 20), # 7, 8 + (0, 10), + (75, 85), # 9, 10 + (85, 95), + (5, 15), # 11, 12 + (15, 25), + (10, 20), # 13, 14 + (45, 55), + (30, 40), + ] # 15, 16 + + @property + def vehicle(self): + """Gets a vehicle""" + return self._vehicle + + @property + def locations(self): + """Gets locations""" + return self._locations + + @property + def num_locations(self): + """Gets number of locations""" + return len(self.locations) + + @property + def depot(self): + """Gets depot location index""" + return self._depot + + @property + def demands(self): + """Gets demands at each location""" + return self._demands + + @property + def time_per_demand_unit(self): + """Gets the time (in min) to load a demand""" + return 5 # 5 minutes/unit + + @property + def time_windows(self): + """Gets (start time, end time) for each locations""" + return self._time_windows ####################### # Problem Constraints # ####################### def manhattan_distance(position_1, position_2): - """Computes the Manhattan distance between two points""" - return ( - abs(position_1[0] - position_2[0]) + abs(position_1[1] - position_2[1])) + """Computes the Manhattan distance between two points""" + return abs(position_1[0] - position_2[0]) + abs(position_1[1] - position_2[1]) class CreateTimeEvaluator(object): - """Creates callback to get total times between locations.""" - - @staticmethod - def service_time(data, node): - """Gets the service time for the specified location.""" - return data.demands[node] * data.time_per_demand_unit - - @staticmethod - def travel_time(data, from_node, to_node): - """Gets the travel times between two locations.""" + """Creates callback to get total times between locations.""" + + @staticmethod + def service_time(data, node): + """Gets the service time for the specified location.""" + return data.demands[node] * data.time_per_demand_unit + + @staticmethod + def travel_time(data, from_node, to_node): + """Gets the travel times between two locations.""" + if from_node == to_node: + travel_time = 0 + else: + travel_time = ( + manhattan_distance(data.locations[from_node], data.locations[to_node]) + / data.vehicle.speed + ) + return travel_time + + def __init__(self, data): + """Initializes the total time matrix.""" + self._total_time = {} + # precompute total time to have time callback in O(1) + for from_node in range(data.num_locations): + self._total_time[from_node] = {} + for to_node in range(data.num_locations): if from_node == to_node: - travel_time = 0 + self._total_time[from_node][to_node] = 0 else: - travel_time = manhattan_distance(data.locations[ - from_node], data.locations[to_node]) / data.vehicle.speed - return travel_time - - def __init__(self, data): - """Initializes the total time matrix.""" - self._total_time = {} - # precompute total time to have time callback in O(1) - for from_node in range(data.num_locations): - self._total_time[from_node] = {} - for to_node in range(data.num_locations): - if from_node == to_node: - self._total_time[from_node][to_node] = 0 - else: - self._total_time[from_node][to_node] = int( - self.service_time(data, from_node) + self.travel_time( - data, from_node, to_node)) - - def time_evaluator(self, from_node, to_node): - """Returns the total time between the two nodes""" - return self._total_time[from_node][to_node] + self._total_time[from_node][to_node] = int( + self.service_time(data, from_node) + + self.travel_time(data, from_node, to_node) + ) + + def time_evaluator(self, from_node, to_node): + """Returns the total time between the two nodes""" + return self._total_time[from_node][to_node] def print_transit_time(route, time_evaluator): - """Print transit time between nodes of a route""" - total_time = 0 - for i, j in route: - total_time += time_evaluator(i, j) - print('{0} -> {1}: {2}min'.format(i, j, time_evaluator(i, j))) - print('Total time: {0}min\n'.format(total_time)) + """Print transit time between nodes of a route""" + total_time = 0 + for i, j in route: + total_time += time_evaluator(i, j) + print('{0} -> {1}: {2}min'.format(i, j, time_evaluator(i, j))) + print('Total time: {0}min\n'.format(total_time)) ######## # Main # ######## def main(): - """Entry point of the program""" - # Instantiate the data problem. - data = DataProblem() + """Entry point of the program""" + # Instantiate the data problem. + data = DataProblem() - # Print Transit Time - time_evaluator = CreateTimeEvaluator(data).time_evaluator - print('Route 0:') - print_transit_time([[0, 5], [5, 8], [8, 6], [6, 2], [2, 0]], time_evaluator) + # Print Transit Time + time_evaluator = CreateTimeEvaluator(data).time_evaluator + print('Route 0:') + print_transit_time([[0, 5], [5, 8], [8, 6], [6, 2], [2, 0]], time_evaluator) - print('Route 1:') - print_transit_time([[0, 9], [9, 14], [14, 16], [16, 10], [10, 0]], - time_evaluator) + print('Route 1:') + print_transit_time( + [[0, 9], [9, 14], [14, 16], [16, 10], [10, 0]], time_evaluator + ) - print('Route 2:') - print_transit_time([[0, 12], [12, 13], [13, 15], [15, 11], [11, 0]], - time_evaluator) + print('Route 2:') + print_transit_time( + [[0, 12], [12, 13], [13, 15], [15, 11], [11, 0]], time_evaluator + ) - print('Route 3:') - print_transit_time([[0, 7], [7, 4], [4, 3], [3, 1], [1, 0]], time_evaluator) + print('Route 3:') + print_transit_time([[0, 7], [7, 4], [4, 3], [3, 1], [1, 0]], time_evaluator) if __name__ == '__main__': - main() + main() diff --git a/examples/python/tsp_sat.py b/examples/python/tsp_sat.py index ade7306cc43..1cc7fbf873c 100644 --- a/examples/python/tsp_sat.py +++ b/examples/python/tsp_sat.py @@ -63,65 +63,65 @@ def main(): - """Entry point of the program.""" - num_nodes = len(DISTANCE_MATRIX) - all_nodes = range(num_nodes) - print("Num nodes =", num_nodes) - - # Model. - model = cp_model.CpModel() - - obj_vars = [] - obj_coeffs = [] - - # Create the circuit constraint. - arcs = [] - arc_literals = {} + """Entry point of the program.""" + num_nodes = len(DISTANCE_MATRIX) + all_nodes = range(num_nodes) + print("Num nodes =", num_nodes) + + # Model. + model = cp_model.CpModel() + + obj_vars = [] + obj_coeffs = [] + + # Create the circuit constraint. + arcs = [] + arc_literals = {} + for i in all_nodes: + for j in all_nodes: + if i == j: + continue + + lit = model.new_bool_var("%i follows %i" % (j, i)) + arcs.append((i, j, lit)) + arc_literals[i, j] = lit + + obj_vars.append(lit) + obj_coeffs.append(DISTANCE_MATRIX[i][j]) + + model.add_circuit(arcs) + + # Minimize weighted sum of arcs. Because this s + model.minimize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) + + # Solve and print out the solution. + solver = cp_model.CpSolver() + solver.parameters.log_search_progress = True + # To benefit from the linearization of the circuit constraint. + solver.parameters.linearization_level = 2 + + solver.solve(model) + print(solver.response_stats()) + + current_node = 0 + str_route = "%i" % current_node + route_is_finished = False + route_distance = 0 + while not route_is_finished: for i in all_nodes: - for j in all_nodes: - if i == j: - continue - - lit = model.new_bool_var("%i follows %i" % (j, i)) - arcs.append((i, j, lit)) - arc_literals[i, j] = lit - - obj_vars.append(lit) - obj_coeffs.append(DISTANCE_MATRIX[i][j]) - - model.add_circuit(arcs) - - # Minimize weighted sum of arcs. Because this s - model.minimize(sum(obj_vars[i] * obj_coeffs[i] for i in range(len(obj_vars)))) - - # Solve and print out the solution. - solver = cp_model.CpSolver() - solver.parameters.log_search_progress = True - # To benefit from the linearization of the circuit constraint. - solver.parameters.linearization_level = 2 - - solver.solve(model) - print(solver.response_stats()) - - current_node = 0 - str_route = "%i" % current_node - route_is_finished = False - route_distance = 0 - while not route_is_finished: - for i in all_nodes: - if i == current_node: - continue - if solver.boolean_value(arc_literals[current_node, i]): - str_route += " -> %i" % i - route_distance += DISTANCE_MATRIX[current_node][i] - current_node = i - if current_node == 0: - route_is_finished = True - break - - print("Route:", str_route) - print("Travelled distance:", route_distance) + if i == current_node: + continue + if solver.boolean_value(arc_literals[current_node, i]): + str_route += " -> %i" % i + route_distance += DISTANCE_MATRIX[current_node][i] + current_node = i + if current_node == 0: + route_is_finished = True + break + + print("Route:", str_route) + print("Travelled distance:", route_distance) if __name__ == "__main__": - main() + main() diff --git a/examples/python/vendor_scheduling_sat.py b/examples/python/vendor_scheduling_sat.py index 066037d3500..f0478cf51e1 100644 --- a/examples/python/vendor_scheduling_sat.py +++ b/examples/python/vendor_scheduling_sat.py @@ -20,148 +20,148 @@ class SolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" - - def __init__( - self, - num_vendors, - num_hours, - possible_schedules, - selected_schedules, - hours_stat, - min_vendors, - ): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 - self.__num_vendors = num_vendors - self.__num_hours = num_hours - self.__possible_schedules = possible_schedules - self.__selected_schedules = selected_schedules - self.__hours_stat = hours_stat - self.__min_vendors = min_vendors - - def on_solution_callback(self): - """Called at each new solution.""" - self.__solution_count += 1 - print("Solution %i: ", self.__solution_count) - print(" min vendors:", self.__min_vendors) - for i in range(self.__num_vendors): - print( - " - vendor %i: " % i, - self.__possible_schedules[self.value(self.__selected_schedules[i])], - ) - print() - - for j in range(self.__num_hours): - print(" - # workers on day%2i: " % j, end=" ") - print(self.value(self.__hours_stat[j]), end=" ") - print() - print() - - def solution_count(self): - """Returns the number of solution found.""" - return self.__solution_count + """Print intermediate solutions.""" + + def __init__( + self, + num_vendors, + num_hours, + possible_schedules, + selected_schedules, + hours_stat, + min_vendors, + ): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 + self.__num_vendors = num_vendors + self.__num_hours = num_hours + self.__possible_schedules = possible_schedules + self.__selected_schedules = selected_schedules + self.__hours_stat = hours_stat + self.__min_vendors = min_vendors + + def on_solution_callback(self): + """Called at each new solution.""" + self.__solution_count += 1 + print("Solution %i: ", self.__solution_count) + print(" min vendors:", self.__min_vendors) + for i in range(self.__num_vendors): + print( + " - vendor %i: " % i, + self.__possible_schedules[self.value(self.__selected_schedules[i])], + ) + print() + + for j in range(self.__num_hours): + print(" - # workers on day%2i: " % j, end=" ") + print(self.value(self.__hours_stat[j]), end=" ") + print() + print() + + def solution_count(self): + """Returns the number of solution found.""" + return self.__solution_count def vendor_scheduling_sat() -> None: - """Create the shift scheduling model and solve it.""" - # Create the model. - model = cp_model.CpModel() - - # - # data - # - num_vendors = 9 - num_hours = 10 - num_work_types = 1 - - traffic = [100, 500, 100, 200, 320, 300, 200, 220, 300, 120] - max_traffic_per_vendor = 100 - - # Last columns are : - # index_of_the_schedule, sum of worked hours (per work type). - # The index is useful for branching. - possible_schedules = [ - [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 8], - [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 4], - [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 2, 5], - [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 3, 4], - [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 4, 3], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0], - ] - - num_possible_schedules = len(possible_schedules) - selected_schedules = [] - vendors_stat = [] - hours_stat = [] - - # Auxiliary data - min_vendors = [t // max_traffic_per_vendor for t in traffic] - all_vendors = range(num_vendors) - all_hours = range(num_hours) - - # - # Declare variables - # - x = {} - - for v in all_vendors: - tmp = [] - for h in all_hours: - x[v, h] = model.new_int_var(0, num_work_types, "x[%i,%i]" % (v, h)) - tmp.append(x[v, h]) - selected_schedule = model.new_int_var( - 0, num_possible_schedules - 1, "s[%i]" % v - ) - hours = model.new_int_var(0, num_hours, "h[%i]" % v) - selected_schedules.append(selected_schedule) - vendors_stat.append(hours) - tmp.append(selected_schedule) - tmp.append(hours) - - model.add_allowed_assignments(tmp, possible_schedules) - - # - # Statistics and constraints for each hour - # + """Create the shift scheduling model and solve it.""" + # Create the model. + model = cp_model.CpModel() + + # + # data + # + num_vendors = 9 + num_hours = 10 + num_work_types = 1 + + traffic = [100, 500, 100, 200, 320, 300, 200, 220, 300, 120] + max_traffic_per_vendor = 100 + + # Last columns are : + # index_of_the_schedule, sum of worked hours (per work type). + # The index is useful for branching. + possible_schedules = [ + [1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 0, 8], + [1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 4], + [0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 2, 5], + [0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 3, 4], + [1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 4, 3], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0], + ] + + num_possible_schedules = len(possible_schedules) + selected_schedules = [] + vendors_stat = [] + hours_stat = [] + + # Auxiliary data + min_vendors = [t // max_traffic_per_vendor for t in traffic] + all_vendors = range(num_vendors) + all_hours = range(num_hours) + + # + # Declare variables + # + x = {} + + for v in all_vendors: + tmp = [] for h in all_hours: - workers = model.new_int_var(0, 1000, "workers[%i]" % h) - model.add(workers == sum(x[v, h] for v in all_vendors)) - hours_stat.append(workers) - model.add(workers * max_traffic_per_vendor >= traffic[h]) - - # - # Redundant constraint: sort selected_schedules - # - for v in range(num_vendors - 1): - model.add(selected_schedules[v] <= selected_schedules[v + 1]) - - # Solve model. - solver = cp_model.CpSolver() - solver.parameters.enumerate_all_solutions = True - solution_printer = SolutionPrinter( - num_vendors, - num_hours, - possible_schedules, - selected_schedules, - hours_stat, - min_vendors, + x[v, h] = model.new_int_var(0, num_work_types, "x[%i,%i]" % (v, h)) + tmp.append(x[v, h]) + selected_schedule = model.new_int_var( + 0, num_possible_schedules - 1, "s[%i]" % v ) - status = solver.solve(model, solution_printer) - print("Status = %s" % solver.status_name(status)) - - print("Statistics") - print(" - conflicts : %i" % solver.num_conflicts) - print(" - branches : %i" % solver.num_branches) - print(" - wall time : %f s" % solver.wall_time) - print(" - number of solutions found: %i" % solution_printer.solution_count()) + hours = model.new_int_var(0, num_hours, "h[%i]" % v) + selected_schedules.append(selected_schedule) + vendors_stat.append(hours) + tmp.append(selected_schedule) + tmp.append(hours) + + model.add_allowed_assignments(tmp, possible_schedules) + + # + # Statistics and constraints for each hour + # + for h in all_hours: + workers = model.new_int_var(0, 1000, "workers[%i]" % h) + model.add(workers == sum(x[v, h] for v in all_vendors)) + hours_stat.append(workers) + model.add(workers * max_traffic_per_vendor >= traffic[h]) + + # + # Redundant constraint: sort selected_schedules + # + for v in range(num_vendors - 1): + model.add(selected_schedules[v] <= selected_schedules[v + 1]) + + # Solve model. + solver = cp_model.CpSolver() + solver.parameters.enumerate_all_solutions = True + solution_printer = SolutionPrinter( + num_vendors, + num_hours, + possible_schedules, + selected_schedules, + hours_stat, + min_vendors, + ) + status = solver.solve(model, solution_printer) + print("Status = %s" % solver.status_name(status)) + + print("Statistics") + print(" - conflicts : %i" % solver.num_conflicts) + print(" - branches : %i" % solver.num_branches) + print(" - wall time : %f s" % solver.wall_time) + print(" - number of solutions found: %i" % solution_printer.solution_count()) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - vendor_scheduling_sat() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + vendor_scheduling_sat() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/wedding_optimal_chart_sat.py b/examples/python/wedding_optimal_chart_sat.py index fc95b0f04ef..c582a497274 100644 --- a/examples/python/wedding_optimal_chart_sat.py +++ b/examples/python/wedding_optimal_chart_sat.py @@ -43,208 +43,209 @@ class WeddingChartPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" - - def __init__(self, seats, names, num_tables, num_guests): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 - self.__start_time = time.time() - self.__seats = seats - self.__names = names - self.__num_tables = num_tables - self.__num_guests = num_guests - - def on_solution_callback(self): - current_time = time.time() - objective = self.objective_value - print( - "Solution %i, time = %f s, objective = %i" - % (self.__solution_count, current_time - self.__start_time, objective) - ) - self.__solution_count += 1 + """Print intermediate solutions.""" + + def __init__(self, seats, names, num_tables, num_guests): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 + self.__start_time = time.time() + self.__seats = seats + self.__names = names + self.__num_tables = num_tables + self.__num_guests = num_guests + + def on_solution_callback(self): + current_time = time.time() + objective = self.objective_value + print( + "Solution %i, time = %f s, objective = %i" + % (self.__solution_count, current_time - self.__start_time, objective) + ) + self.__solution_count += 1 - for t in range(self.__num_tables): - print("Table %d: " % t) - for g in range(self.__num_guests): - if self.value(self.__seats[(t, g)]): - print(" " + self.__names[g]) + for t in range(self.__num_tables): + print("Table %d: " % t) + for g in range(self.__num_guests): + if self.value(self.__seats[(t, g)]): + print(" " + self.__names[g]) - def num_solutions(self) -> int: - return self.__solution_count + def num_solutions(self) -> int: + return self.__solution_count def build_data(): - """Build the data model.""" - # Easy problem (from the paper) - # num_tables = 2 - # table_capacity = 10 - # min_known_neighbors = 1 - - # Slightly harder problem (also from the paper) - num_tables = 5 - table_capacity = 4 - min_known_neighbors = 1 - - # Connection matrix: who knows who, and how strong - # is the relation - connections = [ - [1, 50, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [50, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 50, 1, 1, 1, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 50, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 1, 50, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 50, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 1, 1, 1, 50, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 1, 1, 50, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 10, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 50, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], - ] - - # Names of the guests. B: Bride side, G: Groom side - names = [ - "Deb (B)", - "John (B)", - "Martha (B)", - "Travis (B)", - "Allan (B)", - "Lois (B)", - "Jayne (B)", - "Brad (B)", - "Abby (B)", - "Mary Helen (G)", - "Lee (G)", - "Annika (G)", - "Carl (G)", - "Colin (G)", - "Shirley (G)", - "DeAnn (G)", - "Lori (G)", - ] - return num_tables, table_capacity, min_known_neighbors, connections, names + """Build the data model.""" + # Easy problem (from the paper) + # num_tables = 2 + # table_capacity = 10 + # min_known_neighbors = 1 + + # Slightly harder problem (also from the paper) + num_tables = 5 + table_capacity = 4 + min_known_neighbors = 1 + + # Connection matrix: who knows who, and how strong + # is the relation + connections = [ + [1, 50, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [50, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 50, 1, 1, 1, 1, 10, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 50, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 50, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 50, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 1, 50, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 1, 1, 1, 1, 50, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [1, 1, 10, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 50, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 50, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1], + ] + + # Names of the guests. B: Bride side, G: Groom side + names = [ + "Deb (B)", + "John (B)", + "Martha (B)", + "Travis (B)", + "Allan (B)", + "Lois (B)", + "Jayne (B)", + "Brad (B)", + "Abby (B)", + "Mary Helen (G)", + "Lee (G)", + "Annika (G)", + "Carl (G)", + "Colin (G)", + "Shirley (G)", + "DeAnn (G)", + "Lori (G)", + ] + return num_tables, table_capacity, min_known_neighbors, connections, names def solve_with_discrete_model() -> None: - """Discrete approach.""" - num_tables, table_capacity, min_known_neighbors, connections, names = build_data() - - num_guests = len(connections) - - all_tables = range(num_tables) - all_guests = range(num_guests) - - # Create the cp model. - model = cp_model.CpModel() - - # - # Decision variables - # - seats = {} - for t in all_tables: - for g in all_guests: - seats[(t, g)] = model.new_bool_var("guest %i seats on table %i" % (g, t)) - - colocated = {} - for g1 in range(num_guests - 1): - for g2 in range(g1 + 1, num_guests): - colocated[(g1, g2)] = model.new_bool_var( - "guest %i seats with guest %i" % (g1, g2) - ) - - same_table = {} - for g1 in range(num_guests - 1): - for g2 in range(g1 + 1, num_guests): - for t in all_tables: - same_table[(g1, g2, t)] = model.new_bool_var( - "guest %i seats with guest %i on table %i" % (g1, g2, t) - ) - - # Objective - model.maximize( - sum( - connections[g1][g2] * colocated[g1, g2] - for g1 in range(num_guests - 1) - for g2 in range(g1 + 1, num_guests) - if connections[g1][g2] > 0 - ) - ) + """Discrete approach.""" + num_tables, table_capacity, min_known_neighbors, connections, names = ( + build_data() + ) - # - # Constraints - # + num_guests = len(connections) - # Everybody seats at one table. - for g in all_guests: - model.add(sum(seats[(t, g)] for t in all_tables) == 1) - - # Tables have a max capacity. - for t in all_tables: - model.add(sum(seats[(t, g)] for g in all_guests) <= table_capacity) - - # Link colocated with seats - for g1 in range(num_guests - 1): - for g2 in range(g1 + 1, num_guests): - for t in all_tables: - # Link same_table and seats. - model.add_bool_or( - [ - ~seats[(t, g1)], - ~seats[(t, g2)], - same_table[(g1, g2, t)], - ] - ) - model.add_implication(same_table[(g1, g2, t)], seats[(t, g1)]) - model.add_implication(same_table[(g1, g2, t)], seats[(t, g2)]) - - # Link colocated and same_table. - model.add( - sum(same_table[(g1, g2, t)] for t in all_tables) == colocated[(g1, g2)] - ) - - # Min known neighbors rule. + all_tables = range(num_tables) + all_guests = range(num_guests) + + # Create the cp model. + model = cp_model.CpModel() + + # + # Decision variables + # + seats = {} + for t in all_tables: for g in all_guests: - model.add( - sum( - same_table[(g, g2, t)] - for g2 in range(g + 1, num_guests) - for t in all_tables - if connections[g][g2] > 0 - ) - + sum( - same_table[(g1, g, t)] - for g1 in range(g) - for t in all_tables - if connections[g1][g] > 0 - ) - >= min_known_neighbors + seats[(t, g)] = model.new_bool_var("guest %i seats on table %i" % (g, t)) + + colocated = {} + for g1 in range(num_guests - 1): + for g2 in range(g1 + 1, num_guests): + colocated[(g1, g2)] = model.new_bool_var( + "guest %i seats with guest %i" % (g1, g2) + ) + + same_table = {} + for g1 in range(num_guests - 1): + for g2 in range(g1 + 1, num_guests): + for t in all_tables: + same_table[(g1, g2, t)] = model.new_bool_var( + "guest %i seats with guest %i on table %i" % (g1, g2, t) + ) + + # Objective + model.maximize( + sum( + connections[g1][g2] * colocated[g1, g2] + for g1 in range(num_guests - 1) + for g2 in range(g1 + 1, num_guests) + if connections[g1][g2] > 0 + ) + ) + + # + # Constraints + # + + # Everybody seats at one table. + for g in all_guests: + model.add(sum(seats[(t, g)] for t in all_tables) == 1) + + # Tables have a max capacity. + for t in all_tables: + model.add(sum(seats[(t, g)] for g in all_guests) <= table_capacity) + + # Link colocated with seats + for g1 in range(num_guests - 1): + for g2 in range(g1 + 1, num_guests): + for t in all_tables: + # Link same_table and seats. + model.add_bool_or([ + ~seats[(t, g1)], + ~seats[(t, g2)], + same_table[(g1, g2, t)], + ]) + model.add_implication(same_table[(g1, g2, t)], seats[(t, g1)]) + model.add_implication(same_table[(g1, g2, t)], seats[(t, g2)]) + + # Link colocated and same_table. + model.add( + sum(same_table[(g1, g2, t)] for t in all_tables) + == colocated[(g1, g2)] + ) + + # Min known neighbors rule. + for g in all_guests: + model.add( + sum( + same_table[(g, g2, t)] + for g2 in range(g + 1, num_guests) + for t in all_tables + if connections[g][g2] > 0 + ) + + sum( + same_table[(g1, g, t)] + for g1 in range(g) + for t in all_tables + if connections[g1][g] > 0 ) + >= min_known_neighbors + ) - # Symmetry breaking. First guest seats on the first table. - model.add(seats[(0, 0)] == 1) + # Symmetry breaking. First guest seats on the first table. + model.add(seats[(0, 0)] == 1) - ### Solve model. - solver = cp_model.CpSolver() - solution_printer = WeddingChartPrinter(seats, names, num_tables, num_guests) - solver.solve(model, solution_printer) + ### Solve model. + solver = cp_model.CpSolver() + solution_printer = WeddingChartPrinter(seats, names, num_tables, num_guests) + solver.solve(model, solution_printer) - print("Statistics") - print(" - conflicts : %i" % solver.num_conflicts) - print(" - branches : %i" % solver.num_branches) - print(" - wall time : %f s" % solver.wall_time) - print(" - num solutions: %i" % solution_printer.num_solutions()) + print("Statistics") + print(" - conflicts : %i" % solver.num_conflicts) + print(" - branches : %i" % solver.num_branches) + print(" - wall time : %f s" % solver.wall_time) + print(" - num solutions: %i" % solution_printer.num_solutions()) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - solve_with_discrete_model() + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + solve_with_discrete_model() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/weighted_latency_problem_sat.py b/examples/python/weighted_latency_problem_sat.py index 36616bb26b3..892ae443685 100644 --- a/examples/python/weighted_latency_problem_sat.py +++ b/examples/python/weighted_latency_problem_sat.py @@ -23,11 +23,15 @@ from ortools.sat.python import cp_model _NUM_NODES = flags.DEFINE_integer("num_nodes", 12, "Number of nodes to visit.") -_GRID_SIZE = flags.DEFINE_integer("grid_size", 20, "Size of the grid where nodes are.") +_GRID_SIZE = flags.DEFINE_integer( + "grid_size", 20, "Size of the grid where nodes are." +) _PROFIT_RANGE = flags.DEFINE_integer("profit_range", 50, "Range of profit.") _SEED = flags.DEFINE_integer("seed", 0, "Random seed.") _PARAMS = flags.DEFINE_string( - "params", "num_search_workers:16, max_time_in_seconds:5", "Sat solver parameters." + "params", + "num_search_workers:16, max_time_in_seconds:5", + "Sat solver parameters.", ) _PROTO_FILE = flags.DEFINE_string( "proto_file", "", "If not empty, output the proto to this file." @@ -35,80 +39,81 @@ def build_model(): - """Create the nodes and the profit.""" - random.seed(_SEED.value) - x = [] - y = [] + """Create the nodes and the profit.""" + random.seed(_SEED.value) + x = [] + y = [] + x.append(random.randint(0, _GRID_SIZE.value)) + y.append(random.randint(0, _GRID_SIZE.value)) + for _ in range(_NUM_NODES.value): x.append(random.randint(0, _GRID_SIZE.value)) y.append(random.randint(0, _GRID_SIZE.value)) - for _ in range(_NUM_NODES.value): - x.append(random.randint(0, _GRID_SIZE.value)) - y.append(random.randint(0, _GRID_SIZE.value)) - profits = [] - profits.append(0) - for _ in range(_NUM_NODES.value): - profits.append(random.randint(1, _PROFIT_RANGE.value)) - sum_of_profits = sum(profits) - profits = [p / sum_of_profits for p in profits] + profits = [] + profits.append(0) + for _ in range(_NUM_NODES.value): + profits.append(random.randint(1, _PROFIT_RANGE.value)) + sum_of_profits = sum(profits) + profits = [p / sum_of_profits for p in profits] - return x, y, profits + return x, y, profits def solve_with_cp_sat(x, y, profits) -> None: - """Solves the problem with the CP-SAT solver.""" - model = cp_model.CpModel() - - # because of the manhattan distance, the sum of distances is bounded by this. - horizon = _GRID_SIZE.value * 2 * _NUM_NODES.value - times = [ - model.new_int_var(0, horizon, f"x_{i}") for i in range(_NUM_NODES.value + 1) - ] - - # Node 0 is the start node. - model.add(times[0] == 0) - - # Create the circuit constraint. - arcs = [] - for i in range(_NUM_NODES.value + 1): - for j in range(_NUM_NODES.value + 1): - if i == j: - continue - # We use a manhattan distance between nodes. - distance = abs(x[i] - x[j]) + abs(y[i] - y[j]) - lit = model.new_bool_var(f"{i}_to_{j}") - arcs.append((i, j, lit)) - - # add transitions between nodes. - if i == 0: - # Initial transition - model.add(times[j] == distance).only_enforce_if(lit) - elif j != 0: - # We do not care for the last transition. - model.add(times[j] == times[i] + distance).only_enforce_if(lit) - model.add_circuit(arcs) - - model.minimize(cp_model.LinearExpr.weighted_sum(times, profits)) - - if _PROTO_FILE.value: - model.export_to_file(_PROTO_FILE.value) - - # Solve model. - solver = cp_model.CpSolver() - if _PARAMS.value: - text_format.Parse(_PARAMS.value, solver.parameters) - solver.parameters.log_search_progress = True - solver.solve(model) + """Solves the problem with the CP-SAT solver.""" + model = cp_model.CpModel() + + # because of the manhattan distance, the sum of distances is bounded by this. + horizon = _GRID_SIZE.value * 2 * _NUM_NODES.value + times = [ + model.new_int_var(0, horizon, f"x_{i}") + for i in range(_NUM_NODES.value + 1) + ] + + # Node 0 is the start node. + model.add(times[0] == 0) + + # Create the circuit constraint. + arcs = [] + for i in range(_NUM_NODES.value + 1): + for j in range(_NUM_NODES.value + 1): + if i == j: + continue + # We use a manhattan distance between nodes. + distance = abs(x[i] - x[j]) + abs(y[i] - y[j]) + lit = model.new_bool_var(f"{i}_to_{j}") + arcs.append((i, j, lit)) + + # add transitions between nodes. + if i == 0: + # Initial transition + model.add(times[j] == distance).only_enforce_if(lit) + elif j != 0: + # We do not care for the last transition. + model.add(times[j] == times[i] + distance).only_enforce_if(lit) + model.add_circuit(arcs) + + model.minimize(cp_model.LinearExpr.weighted_sum(times, profits)) + + if _PROTO_FILE.value: + model.export_to_file(_PROTO_FILE.value) + + # Solve model. + solver = cp_model.CpSolver() + if _PARAMS.value: + text_format.Parse(_PARAMS.value, solver.parameters) + solver.parameters.log_search_progress = True + solver.solve(model) def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") - x, y, profits = build_model() - solve_with_cp_sat(x, y, profits) - # TODO(user): Implement routing model. + x, y, profits = build_model() + solve_with_cp_sat(x, y, profits) + # TODO(user): Implement routing model. if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/python/zebra_sat.py b/examples/python/zebra_sat.py index 204be39f1ac..2bf81d424af 100755 --- a/examples/python/zebra_sat.py +++ b/examples/python/zebra_sat.py @@ -39,84 +39,90 @@ # pylint: disable=too-many-statements def solve_zebra(): - """Solves the zebra problem.""" - - # Create the model. - model = cp_model.CpModel() - - red = model.new_int_var(1, 5, "red") - green = model.new_int_var(1, 5, "green") - yellow = model.new_int_var(1, 5, "yellow") - blue = model.new_int_var(1, 5, "blue") - ivory = model.new_int_var(1, 5, "ivory") - - englishman = model.new_int_var(1, 5, "englishman") - spaniard = model.new_int_var(1, 5, "spaniard") - japanese = model.new_int_var(1, 5, "japanese") - ukrainian = model.new_int_var(1, 5, "ukrainian") - norwegian = model.new_int_var(1, 5, "norwegian") - - dog = model.new_int_var(1, 5, "dog") - snails = model.new_int_var(1, 5, "snails") - fox = model.new_int_var(1, 5, "fox") - zebra = model.new_int_var(1, 5, "zebra") - horse = model.new_int_var(1, 5, "horse") - - tea = model.new_int_var(1, 5, "tea") - coffee = model.new_int_var(1, 5, "coffee") - water = model.new_int_var(1, 5, "water") - milk = model.new_int_var(1, 5, "milk") - fruit_juice = model.new_int_var(1, 5, "fruit juice") - - old_gold = model.new_int_var(1, 5, "old gold") - kools = model.new_int_var(1, 5, "kools") - chesterfields = model.new_int_var(1, 5, "chesterfields") - lucky_strike = model.new_int_var(1, 5, "lucky strike") - parliaments = model.new_int_var(1, 5, "parliaments") - - model.add_all_different(red, green, yellow, blue, ivory) - model.add_all_different(englishman, spaniard, japanese, ukrainian, norwegian) - model.add_all_different(dog, snails, fox, zebra, horse) - model.add_all_different(tea, coffee, water, milk, fruit_juice) - model.add_all_different(parliaments, kools, chesterfields, lucky_strike, old_gold) - - model.add(englishman == red) - model.add(spaniard == dog) - model.add(coffee == green) - model.add(ukrainian == tea) - model.add(green == ivory + 1) - model.add(old_gold == snails) - model.add(kools == yellow) - model.add(milk == 3) - model.add(norwegian == 1) - - diff_fox_chesterfields = model.new_int_var(-4, 4, "diff_fox_chesterfields") - model.add(diff_fox_chesterfields == fox - chesterfields) - model.add_abs_equality(1, diff_fox_chesterfields) - - diff_horse_kools = model.new_int_var(-4, 4, "diff_horse_kools") - model.add(diff_horse_kools == horse - kools) - model.add_abs_equality(1, diff_horse_kools) - - model.add(lucky_strike == fruit_juice) - model.add(japanese == parliaments) - - diff_norwegian_blue = model.new_int_var(-4, 4, "diff_norwegian_blue") - model.add(diff_norwegian_blue == norwegian - blue) - model.add_abs_equality(1, diff_norwegian_blue) - - # Solve and print out the solution. - solver = cp_model.CpSolver() - status = solver.solve(model) - - if status == cp_model.OPTIMAL: - people = [englishman, spaniard, japanese, ukrainian, norwegian] - water_drinker = [p for p in people if solver.value(p) == solver.value(water)][0] - zebra_owner = [p for p in people if solver.value(p) == solver.value(zebra)][0] - print("The", water_drinker.name, "drinks water.") - print("The", zebra_owner.name, "owns the zebra.") - else: - print("No solutions to the zebra problem, this is unusual!") + """Solves the zebra problem.""" + + # Create the model. + model = cp_model.CpModel() + + red = model.new_int_var(1, 5, "red") + green = model.new_int_var(1, 5, "green") + yellow = model.new_int_var(1, 5, "yellow") + blue = model.new_int_var(1, 5, "blue") + ivory = model.new_int_var(1, 5, "ivory") + + englishman = model.new_int_var(1, 5, "englishman") + spaniard = model.new_int_var(1, 5, "spaniard") + japanese = model.new_int_var(1, 5, "japanese") + ukrainian = model.new_int_var(1, 5, "ukrainian") + norwegian = model.new_int_var(1, 5, "norwegian") + + dog = model.new_int_var(1, 5, "dog") + snails = model.new_int_var(1, 5, "snails") + fox = model.new_int_var(1, 5, "fox") + zebra = model.new_int_var(1, 5, "zebra") + horse = model.new_int_var(1, 5, "horse") + + tea = model.new_int_var(1, 5, "tea") + coffee = model.new_int_var(1, 5, "coffee") + water = model.new_int_var(1, 5, "water") + milk = model.new_int_var(1, 5, "milk") + fruit_juice = model.new_int_var(1, 5, "fruit juice") + + old_gold = model.new_int_var(1, 5, "old gold") + kools = model.new_int_var(1, 5, "kools") + chesterfields = model.new_int_var(1, 5, "chesterfields") + lucky_strike = model.new_int_var(1, 5, "lucky strike") + parliaments = model.new_int_var(1, 5, "parliaments") + + model.add_all_different(red, green, yellow, blue, ivory) + model.add_all_different(englishman, spaniard, japanese, ukrainian, norwegian) + model.add_all_different(dog, snails, fox, zebra, horse) + model.add_all_different(tea, coffee, water, milk, fruit_juice) + model.add_all_different( + parliaments, kools, chesterfields, lucky_strike, old_gold + ) + + model.add(englishman == red) + model.add(spaniard == dog) + model.add(coffee == green) + model.add(ukrainian == tea) + model.add(green == ivory + 1) + model.add(old_gold == snails) + model.add(kools == yellow) + model.add(milk == 3) + model.add(norwegian == 1) + + diff_fox_chesterfields = model.new_int_var(-4, 4, "diff_fox_chesterfields") + model.add(diff_fox_chesterfields == fox - chesterfields) + model.add_abs_equality(1, diff_fox_chesterfields) + + diff_horse_kools = model.new_int_var(-4, 4, "diff_horse_kools") + model.add(diff_horse_kools == horse - kools) + model.add_abs_equality(1, diff_horse_kools) + + model.add(lucky_strike == fruit_juice) + model.add(japanese == parliaments) + + diff_norwegian_blue = model.new_int_var(-4, 4, "diff_norwegian_blue") + model.add(diff_norwegian_blue == norwegian - blue) + model.add_abs_equality(1, diff_norwegian_blue) + + # Solve and print out the solution. + solver = cp_model.CpSolver() + status = solver.solve(model) + + if status == cp_model.OPTIMAL: + people = [englishman, spaniard, japanese, ukrainian, norwegian] + water_drinker = [ + p for p in people if solver.value(p) == solver.value(water) + ][0] + zebra_owner = [p for p in people if solver.value(p) == solver.value(zebra)][ + 0 + ] + print("The", water_drinker.name, "drinks water.") + print("The", zebra_owner.name, "owns the zebra.") + else: + print("No solutions to the zebra problem, this is unusual!") solve_zebra() diff --git a/examples/service/solve_math_opt_model_via_http.py b/examples/service/solve_math_opt_model_via_http.py index 2142906218e..59bbccd1b6f 100644 --- a/examples/service/solve_math_opt_model_via_http.py +++ b/examples/service/solve_math_opt_model_via_http.py @@ -30,42 +30,42 @@ def request_example() -> None: - """Run example using MathOpt `remote_http_solve` function.""" - # Set up the API key. - api_key = _API_KEY.value - if not api_key: - print( - "API key is required. See" - " https://developers.google.com/optimization/service/setup for" - " instructions." - ) - return + """Run example using MathOpt `remote_http_solve` function.""" + # Set up the API key. + api_key = _API_KEY.value + if not api_key: + print( + "API key is required. See" + " https://developers.google.com/optimization/service/setup for" + " instructions." + ) + return - # Build a MathOpt model - model = mathopt.Model(name="my_model") - x = model.add_binary_variable(name="x") - y = model.add_variable(lb=0.0, ub=2.5, name="y") - model.add_linear_constraint(x + y <= 1.5, name="c") - model.maximize(2 * x + y) - try: - result, logs = remote_http_solve.remote_http_solve( - model, - mathopt.SolverType.GSCIP, - mathopt.SolveParameters(enable_output=True), - api_key=api_key, - ) - print("Objective value: ", result.objective_value()) - print("x: ", result.variable_values(x)) - print("y: ", result.variable_values(y)) - print("\n".join(logs)) - except remote_http_solve.OptimizationServiceError as err: - print(err) + # Build a MathOpt model + model = mathopt.Model(name="my_model") + x = model.add_binary_variable(name="x") + y = model.add_variable(lb=0.0, ub=2.5, name="y") + model.add_linear_constraint(x + y <= 1.5, name="c") + model.maximize(2 * x + y) + try: + result, logs = remote_http_solve.remote_http_solve( + model, + mathopt.SolverType.GSCIP, + mathopt.SolveParameters(enable_output=True), + api_key=api_key, + ) + print("Objective value: ", result.objective_value()) + print("x: ", result.variable_values(x)) + print("y: ", result.variable_values(y)) + print("\n".join(logs)) + except remote_http_solve.OptimizationServiceError as err: + print(err) def main(argv: Sequence[str]) -> None: - del argv # Unused. - request_example() + del argv # Unused. + request_example() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/examples/tests/dual_loading.py b/examples/tests/dual_loading.py index 8ab1c1c88ea..3ff44591fde 100755 --- a/examples/tests/dual_loading.py +++ b/examples/tests/dual_loading.py @@ -5,7 +5,7 @@ def main(): cp = pywrapcp.Solver("test") - lp = pywraplp.Solver.CreateSolver('GLOP') + lp = pywraplp.Solver.CreateSolver("GLOP") if __name__ == "__main__": diff --git a/examples/tests/issue117.py b/examples/tests/issue117.py index 68ec9d59a48..4801077c023 100755 --- a/examples/tests/issue117.py +++ b/examples/tests/issue117.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 from collections import namedtuple -from ortools.constraint_solver import pywrapcp +from ortools.routing import pywraprouting VEHICLE_COUNT = 30 VEHICLE_CAPACITY = 200 -Customer = namedtuple("Customer", ['index', 'demand', 'x', 'y']) +Customer = namedtuple('Customer', ['index', 'demand', 'x', 'y']) print('Init') @@ -14,21 +14,22 @@ customers.append(Customer(1, 1, 2.0, 2.0)) customer_count = len(customers) -manager = pywrapcp.RoutingIndexManager(3, VEHICLE_COUNT, 0) -routing = pywrapcp.RoutingModel(manager) +manager = pywraprouting.RoutingIndexManager(3, VEHICLE_COUNT, 0) +routing = pywraprouting.RoutingModel(manager) print('Demand Constraint') demands = [] for i in range(0, customer_count): - demands.append(customers[i][1]) -routing.AddVectorDimension(demands, VEHICLE_CAPACITY, True, "Demand") + demands.append(customers[i][1]) +routing.AddVectorDimension(demands, VEHICLE_CAPACITY, True, 'Demand') print('Adding Costs') def distance_callback(from_index, to_index): - #static just for the sake of the example - return 1 + # static just for the sake of the example + return 1 + transit_callback_index = routing.RegisterTransitCallback(distance_callback) routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) @@ -41,25 +42,25 @@ def distance_callback(from_index, to_index): routes = [] for i in range(0, routing.vehicles()): - route_number = i - routes.append([]) - node = routing.Start(route_number) - route = [] - route.append(0) - if routing.IsVehicleUsed(assignment, i): - while True: - node = assignment.Value(routing.NextVar(node)) + route_number = i + routes.append([]) + node = routing.Start(route_number) + route = [] + route.append(0) + if routing.IsVehicleUsed(assignment, i): + while True: + node = assignment.Value(routing.NextVar(node)) - if not routing.IsEnd(node): - route.append(int(node)) - else: - break + if not routing.IsEnd(node): + route.append(int(node)) + else: + break - route.append(0) - routes[route_number].append(route) + route.append(0) + routes[route_number].append(route) -#This are the routes as list of lists +# This are the routes as list of lists routes = [el[0] for el in routes] -#Now try to read the routes into a new assigment object fails +# Now try to read the routes into a new assigment object fails assignment2 = routing.ReadAssignmentFromRoutes(routes, True) diff --git a/examples/tests/issue1231.py b/examples/tests/issue1231.py index e78a2dc933a..bcf8353a862 100755 --- a/examples/tests/issue1231.py +++ b/examples/tests/issue1231.py @@ -24,9 +24,10 @@ from ortools.constraint_solver import pywrapcp from os import abort + def CPIsFun(): # Constraint programming engine - solver = pywrapcp.Solver('CP is fun!'); + solver = pywrapcp.Solver('CP is fun!') kBase = 10 @@ -34,16 +35,16 @@ def CPIsFun(): digits = list(range(0, kBase)) digits_without_zero = list(range(1, kBase)) - c = solver.IntVar(digits_without_zero, 'C'); - p = solver.IntVar(digits, 'P'); - i = solver.IntVar(digits_without_zero, 'I'); - s = solver.IntVar(digits, 'S'); - f = solver.IntVar(digits_without_zero, 'F'); - u = solver.IntVar(digits, 'U'); - n = solver.IntVar(digits, 'N'); - t = solver.IntVar(digits_without_zero, 'T'); - r = solver.IntVar(digits, 'R'); - e = solver.IntVar(digits, 'E'); + c = solver.IntVar(digits_without_zero, 'C') + p = solver.IntVar(digits, 'P') + i = solver.IntVar(digits_without_zero, 'I') + s = solver.IntVar(digits, 'S') + f = solver.IntVar(digits_without_zero, 'F') + u = solver.IntVar(digits, 'U') + n = solver.IntVar(digits, 'N') + t = solver.IntVar(digits_without_zero, 'T') + r = solver.IntVar(digits, 'R') + e = solver.IntVar(digits, 'E') # We need to group variables in a list to use the constraint AllDifferent. letters = [c, p, i, s, f, u, n, t, r, e] @@ -55,20 +56,30 @@ def CPIsFun(): solver.Add(solver.AllDifferent(letters)) # CP + IS + FUN = TRUE - solver.Add (p + s + n + kBase * (c + i + u) + kBase * kBase * f == - e + kBase * u + kBase * kBase * r + kBase * kBase * kBase * t) + solver.Add( + p + s + n + kBase * (c + i + u) + kBase * kBase * f + == e + kBase * u + kBase * kBase * r + kBase * kBase * kBase * t + ) - db = solver.Phase(letters, solver.INT_VAR_DEFAULT, - solver.INT_VALUE_DEFAULT) + db = solver.Phase(letters, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT) solver.NewSearch(db) while solver.NextSolution(): print(letters) # Is CP + IS + FUN = TRUE? - assert (kBase*c.Value() + p.Value() + kBase*i.Value() + s.Value() + - kBase*kBase*f.Value() + kBase*u.Value() + n.Value() == - kBase*kBase*kBase*t.Value() + kBase*kBase*r.Value() + - kBase*u.Value() + e.Value()) + assert ( + kBase * c.Value() + + p.Value() + + kBase * i.Value() + + s.Value() + + kBase * kBase * f.Value() + + kBase * u.Value() + + n.Value() + == kBase * kBase * kBase * t.Value() + + kBase * kBase * r.Value() + + kBase * u.Value() + + e.Value() + ) solver.EndSearch() diff --git a/examples/tests/issue128.py b/examples/tests/issue128.py index 71286d5cf46..87435e0bc7e 100755 --- a/examples/tests/issue128.py +++ b/examples/tests/issue128.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 from ortools.constraint_solver import pywrapcp + def test_v0(): - print('test_v0') - solver = pywrapcp.Solver('') + print("test_v0") + solver = pywrapcp.Solver("") # we have two tasks of durations 4 and 7 task1 = solver.FixedDurationIntervalVar(0, 5, 4, False, "task1") @@ -11,8 +12,12 @@ def test_v0(): tasks = [task1, task2] # to each task, a post task of duration 64 is attached - postTask1 = solver.FixedDurationIntervalVar(4, 74 + 64, 64, False, "postTask1") - postTask2 = solver.FixedDurationIntervalVar(4, 77 + 64, 64, False, "postTask2") + postTask1 = solver.FixedDurationIntervalVar( + 4, 74 + 64, 64, False, "postTask1" + ) + postTask2 = solver.FixedDurationIntervalVar( + 4, 77 + 64, 64, False, "postTask2" + ) postTasks = [postTask1, postTask2] solver.Add(postTask1.StartsAtEnd(task1)) @@ -25,24 +30,38 @@ def test_v0(): postTask2UsesRes1 = solver.IntVar(0, 1, "post task 2 using resource 1") postTask2UsesRes2 = solver.IntVar(0, 1, "post task 2 using resource 2") - indicators = [postTask1UsesRes1, postTask1UsesRes2, postTask2UsesRes1, postTask2UsesRes2] + indicators = [ + postTask1UsesRes1, + postTask1UsesRes2, + postTask2UsesRes1, + postTask2UsesRes2, + ] # each post task needs exactly one resource solver.Add(postTask1UsesRes1 + postTask1UsesRes2 == 1) solver.Add(postTask2UsesRes1 + postTask2UsesRes2 == 1) # each resource cannot be used simultaneously by more than one post task - solver.Add(solver.Cumulative(postTasks, [postTask1UsesRes1, postTask2UsesRes1], 1, "cumul1")) - solver.Add(solver.Cumulative(postTasks, [postTask1UsesRes2, postTask2UsesRes2], 1, "cumul2")) + solver.Add( + solver.Cumulative( + postTasks, [postTask1UsesRes1, postTask2UsesRes1], 1, "cumul1" + ) + ) + solver.Add( + solver.Cumulative( + postTasks, [postTask1UsesRes2, postTask2UsesRes2], 1, "cumul2" + ) + ) # using constant demands instead, the correct solution is found # solver.Add(solver.Cumulative(postTasks, [0, 1], 1, "")) # solver.Add(solver.Cumulative(postTasks, [1, 0], 1, "")) - # search setup and solving dbInterval = solver.Phase(tasks + postTasks, solver.INTERVAL_DEFAULT) - dbInt = solver.Phase(indicators, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT) + dbInt = solver.Phase( + indicators, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT + ) makespan = solver.Max([task1.EndExpr().Var(), task2.EndExpr().Var()]) optimize = solver.Optimize(False, makespan, 1) @@ -56,20 +75,22 @@ def test_v0(): if collector.SolutionCount() > 0: for i, task in enumerate(tasks): - print("task {} runs from {} to {}".format( - i, - collector.StartValue(0, task), - collector.EndValue(0, task))) + print( + "task {} runs from {} to {}".format( + i, collector.StartValue(0, task), collector.EndValue(0, task) + ) + ) for i, task in enumerate(postTasks): print("postTask {} starts at {}".format(i, collector.StartValue(0, task))) for indicator in indicators: - print('{} -> {}'.format(indicator.Name(), collector.Value(0, indicator))) + print("{} -> {}".format(indicator.Name(), collector.Value(0, indicator))) else: - print('No solution') + print("No solution") + def test_v1(): - print('test_v1') - solver = pywrapcp.Solver('') + print("test_v1") + solver = pywrapcp.Solver("") # we have two tasks of durations 4 and 7 task1 = solver.FixedDurationIntervalVar(0, 5, 4, False, "task1") @@ -83,7 +104,6 @@ def test_v1(): task2_r2 = solver.FixedDurationIntervalVar(0, 5, 7, True, "task2_2") tasks_r2 = [task1_r2, task2_r2] - # to each task, a post task of duration 64 is attached postTask1 = solver.FixedDurationStartSyncedOnEndIntervalVar(task1, 64, 0) postTask2 = solver.FixedDurationStartSyncedOnEndIntervalVar(task2, 64, 0) @@ -95,14 +115,28 @@ def test_v1(): postTask1_r2 = solver.FixedDurationIntervalVar(4, 9, 64, True, "pTask1_2") postTask2_r2 = solver.FixedDurationIntervalVar(4, 11, 64, True, "pTask2_2") - copies = [ task1_r1, task2_r1, task1_r2, task2_r2, - postTask1_r1, postTask1_r2, postTask2_r1, postTask2_r2 ] + copies = [ + task1_r1, + task2_r1, + task1_r2, + task2_r2, + postTask1_r1, + postTask1_r2, + postTask2_r1, + postTask2_r2, + ] # each resource cannot be used simultaneously by more than one post task - solver.Add(solver.DisjunctiveConstraint( - [task1_r1, task2_r1, postTask1_r1, postTask2_r1], "disj1")) - solver.Add(solver.DisjunctiveConstraint( - [task1_r2, task2_r2, postTask1_r2, postTask2_r2], "disj1")) + solver.Add( + solver.DisjunctiveConstraint( + [task1_r1, task2_r1, postTask1_r1, postTask2_r1], "disj1" + ) + ) + solver.Add( + solver.DisjunctiveConstraint( + [task1_r2, task2_r2, postTask1_r2, postTask2_r2], "disj1" + ) + ) # Only one resource available solver.Add(task1_r1.PerformedExpr() + task1_r2.PerformedExpr() == 1) @@ -118,13 +152,17 @@ def test_v1(): # Indicators (no need to add both as they are constrained together) indicators = [ - task1_r1.PerformedExpr(), task2_r1.PerformedExpr(), - postTask1_r1.PerformedExpr(), postTask2_r1.PerformedExpr()] + task1_r1.PerformedExpr(), + task2_r1.PerformedExpr(), + postTask1_r1.PerformedExpr(), + postTask2_r1.PerformedExpr(), + ] # search setup and solving dbInterval = solver.Phase(tasks + postTasks, solver.INTERVAL_DEFAULT) dbInt = solver.Phase( - indicators, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT) + indicators, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT + ) makespan = solver.Max([task1.EndExpr(), task2.EndExpr()]) optimize = solver.Minimize(makespan, 1) @@ -139,19 +177,26 @@ def test_v1(): solver.Solve(phase, [collector, optimize]) if collector.SolutionCount() > 0: - print('solution with makespan', collector.ObjectiveValue(0)) + print("solution with makespan", collector.ObjectiveValue(0)) for task in tasks: - print("task {} runs from {} to {}".format( - task.Name(), - collector.StartValue(0, task), - collector.EndValue(0, task))) + print( + "task {} runs from {} to {}".format( + task.Name(), + collector.StartValue(0, task), + collector.EndValue(0, task), + ) + ) for task in postTasks: - print("postTask {} starts at {}".format( - task.Name(), collector.StartValue(0, task))) + print( + "postTask {} starts at {}".format( + task.Name(), collector.StartValue(0, task) + ) + ) for task in copies: print(task.Name(), collector.PerformedValue(0, task)) else: - print('No solution') + print("No solution") + test_v0() test_v1() diff --git a/examples/tests/issue2.py b/examples/tests/issue2.py index c6d36b1da22..9233d9584bc 100755 --- a/examples/tests/issue2.py +++ b/examples/tests/issue2.py @@ -3,6 +3,7 @@ # Control-C test. Hit Control-C during execution of this program. + def main(): solver = pywrapcp.Solver("time limit test") n = 10 @@ -12,17 +13,15 @@ def main(): solution = solver.Assignment() solution.Add(x) - db = solver.Phase(x, - solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase(x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) time_limit = 2000 branch_limit = 100000000 failures_limit = 100000000 solutions_limit = 10000000 - limits = ( - solver.Limit( - time_limit, branch_limit, failures_limit, solutions_limit, True)) + limits = solver.Limit( + time_limit, branch_limit, failures_limit, solutions_limit, True + ) search_log = solver.SearchLog(1000) assignment = solver.Assignment() diff --git a/examples/tests/issue22.cs b/examples/tests/issue22.cs index a1cffd7fb30..f928d8db075 100644 --- a/examples/tests/issue22.cs +++ b/examples/tests/issue22.cs @@ -17,6 +17,7 @@ using System.IO; using System.Text.RegularExpressions; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; public class Issue22Test { diff --git a/examples/tests/issue3.py b/examples/tests/issue3.py index c74b1853dbc..9b8a865b696 100755 --- a/examples/tests/issue3.py +++ b/examples/tests/issue3.py @@ -18,7 +18,7 @@ from time import time from random import randint -#----------------helper for binpacking posting---------------- +# ----------------helper for binpacking posting---------------- def binpacking(cp, binvars, weights, loadvars): @@ -33,36 +33,81 @@ def binpacking(cp, binvars, weights, loadvars): cp.Add(solver.Sum([b[i] * weights[i] for i in range(nitems)]) == l[j]) cp.Add(solver.Sum(loadvars) == sum(weights)) -#------------------------------data reading------------------- + +# ------------------------------data reading------------------- maxcapa = 44 weights = [4, 22, 9, 5, 8, 3, 3, 4, 7, 7, 3] loss = [ - 0, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, 1, 0, 2, 1, 0, 0, 0, 0, 2, 1, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 1, 0, 3, 2, 1, 0, 2, 1, 0, 0, 0] + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 1, + 0, + 2, + 1, + 0, + 0, + 0, + 0, + 2, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 2, + 1, + 0, + 3, + 2, + 1, + 0, + 2, + 1, + 0, + 0, + 0, +] nbslab = 11 -#------------------solver and variable declaration------------- +# ------------------solver and variable declaration------------- solver = pywrapcp.Solver('Steel Mill Slab') -x = [solver.IntVar(0, nbslab-1, 'x' + str(i)) for i in range(nbslab)] +x = [solver.IntVar(0, nbslab - 1, 'x' + str(i)) for i in range(nbslab)] l = [solver.IntVar(0, maxcapa, 'l' + str(i)) for i in range(nbslab)] obj = solver.IntVar(0, nbslab * maxcapa, 'obj') -#-------------------post of the constraints-------------- +# -------------------post of the constraints-------------- binpacking(solver, x, weights[:nbslab], l) -solver.Add(solver.Sum([solver.Element(loss, l[s]) - for s in range(nbslab)]) == obj) +solver.Add( + solver.Sum([solver.Element(loss, l[s]) for s in range(nbslab)]) == obj +) sol = [2, 0, 0, 0, 0, 1, 2, 2, 1, 1, 2] -#------------start the search and optimization----------- +# ------------start the search and optimization----------- objective = solver.Minimize(obj, 1) -db = solver.Phase(x, solver.INT_VAR_DEFAULT, - solver.INT_VALUE_DEFAULT) +db = solver.Phase(x, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT) # solver.NewSearch(db,[objective]) #segfault if I comment this while solver.NextSolution(): diff --git a/examples/tests/issue4.py b/examples/tests/issue4.py index 870328ad272..77f1d340b17 100755 --- a/examples/tests/issue4.py +++ b/examples/tests/issue4.py @@ -11,17 +11,15 @@ def main(): solution = solver.Assignment() solution.Add(x) - db = solver.Phase(x, - solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase(x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) time_limit = 2000 branch_limit = 100000000 failures_limit = 100000000 solutions_limit = 10000000 - limits = ( - solver.Limit( - time_limit, branch_limit, failures_limit, solutions_limit, True)) + limits = solver.Limit( + time_limit, branch_limit, failures_limit, solutions_limit, True + ) search_log = solver.SearchLog(1000) diff --git a/examples/tests/issue46.py b/examples/tests/issue46.py index 338cb72f373..a9adbc0ceb3 100755 --- a/examples/tests/issue46.py +++ b/examples/tests/issue46.py @@ -17,78 +17,81 @@ class AssignToStartMin(pywrapcp.PyDecisionBuilder): - def __init__(self, intervals): - pywrapcp.PyDecisionBuilder.__init__(self) - self.__intervals = intervals - def Next(self, solver): - for interval in self.__intervals: - interval.SetStartMax(interval.StartMin()) - return None + def __init__(self, intervals): + pywrapcp.PyDecisionBuilder.__init__(self) + self.__intervals = intervals - def DebugString(self): - return 'CustomDecisionBuilder' + def Next(self, solver): + for interval in self.__intervals: + interval.SetStartMax(interval.StartMin()) + return None + + def DebugString(self): + return 'CustomDecisionBuilder' def NoSequence(): - print('NoSequence') - solver = pywrapcp.Solver('Ordo') - tasks = [] - [ - tasks.append( - solver.FixedDurationIntervalVar(0, 25, 5, False, 'Tasks%i' % i)) - for i in range(3) - ] - print(tasks) - disj = solver.DisjunctiveConstraint(tasks, 'Disjunctive') - solver.Add(disj) - collector = solver.AllSolutionCollector() - collector.Add(tasks) - intervalPhase = solver.Phase(tasks, solver.INTERVAL_DEFAULT) - solver.Solve(intervalPhase, [collector]) - print(collector.SolutionCount()) - for i in range(collector.SolutionCount()): - print("Solution ", i) - print(collector.ObjectiveValue(i)) - print([collector.StartValue(i, tasks[j]) for j in range(3)]) - print([collector.EndValue(i, tasks[j]) for j in range(3)]) + print('NoSequence') + solver = pywrapcp.Solver('Ordo') + tasks = [] + [ + tasks.append( + solver.FixedDurationIntervalVar(0, 25, 5, False, 'Tasks%i' % i) + ) + for i in range(3) + ] + print(tasks) + disj = solver.DisjunctiveConstraint(tasks, 'Disjunctive') + solver.Add(disj) + collector = solver.AllSolutionCollector() + collector.Add(tasks) + intervalPhase = solver.Phase(tasks, solver.INTERVAL_DEFAULT) + solver.Solve(intervalPhase, [collector]) + print(collector.SolutionCount()) + for i in range(collector.SolutionCount()): + print('Solution ', i) + print(collector.ObjectiveValue(i)) + print([collector.StartValue(i, tasks[j]) for j in range(3)]) + print([collector.EndValue(i, tasks[j]) for j in range(3)]) def Sequence(): - print('Sequence') - solver = pywrapcp.Solver('Ordo') - tasks = [] - [ - tasks.append( - solver.FixedDurationIntervalVar(0, 25, 5, False, 'Tasks%i' % i)) - for i in range(3) - ] - print(tasks) - disj = solver.DisjunctiveConstraint(tasks, 'Disjunctive') - solver.Add(disj) - sequence = [] - sequence.append(disj.SequenceVar()) - sequence[0].RankFirst(0) - collector = solver.AllSolutionCollector() - collector.Add(sequence) - collector.Add(tasks) - sequencePhase = solver.Phase(sequence, solver.SEQUENCE_DEFAULT) - intervalPhase = AssignToStartMin(tasks) - # intervalPhase = solver.Phase(tasks, solver.INTERVAL_DEFAULT) - mainPhase = solver.Compose([sequencePhase, intervalPhase]) - solver.Solve(mainPhase, [collector]) - print(collector.SolutionCount()) - for i in range(collector.SolutionCount()): - print("Solution ", i) - print(collector.ObjectiveValue(i)) - print([collector.StartValue(i, tasks[j]) for j in range(3)]) - print([collector.EndValue(i, tasks[j]) for j in range(3)]) + print('Sequence') + solver = pywrapcp.Solver('Ordo') + tasks = [] + [ + tasks.append( + solver.FixedDurationIntervalVar(0, 25, 5, False, 'Tasks%i' % i) + ) + for i in range(3) + ] + print(tasks) + disj = solver.DisjunctiveConstraint(tasks, 'Disjunctive') + solver.Add(disj) + sequence = [] + sequence.append(disj.SequenceVar()) + sequence[0].RankFirst(0) + collector = solver.AllSolutionCollector() + collector.Add(sequence) + collector.Add(tasks) + sequencePhase = solver.Phase(sequence, solver.SEQUENCE_DEFAULT) + intervalPhase = AssignToStartMin(tasks) + # intervalPhase = solver.Phase(tasks, solver.INTERVAL_DEFAULT) + mainPhase = solver.Compose([sequencePhase, intervalPhase]) + solver.Solve(mainPhase, [collector]) + print(collector.SolutionCount()) + for i in range(collector.SolutionCount()): + print('Solution ', i) + print(collector.ObjectiveValue(i)) + print([collector.StartValue(i, tasks[j]) for j in range(3)]) + print([collector.EndValue(i, tasks[j]) for j in range(3)]) def main(): - NoSequence() - Sequence() + NoSequence() + Sequence() if __name__ == '__main__': - main() + main() diff --git a/examples/tests/issue5.py b/examples/tests/issue5.py index d12a3e97c3c..082703f8d15 100755 --- a/examples/tests/issue5.py +++ b/examples/tests/issue5.py @@ -13,51 +13,51 @@ # See the License for the specific language governing permissions and # limitations under the License. -''' - A programming puzzle from Einav in Google CP Solver. - - From - 'A programming puzzle from Einav' - http://gcanyon.wordpress.com/2009/10/28/a-programming-puzzle-from-einav/ - - My friend Einav gave me this programming puzzle to work on. Given - this array of positive and negative numbers: - 33 30 -10 -6 18 7 -11 -23 6 - ... - -25 4 16 30 33 -23 -4 4 -23 - - You can flip the sign of entire rows and columns, as many of them - as you like. The goal is to make all the rows and columns sum to positive - numbers (or zero), and then to find the solution (there are more than one) - that has the smallest overall sum. So for example, for this array: - 33 30 -10 - -16 19 9 - -17 -12 -14 - You could flip the sign for the bottom row to get this array: - 33 30 -10 - -16 19 9 - 17 12 14 - Now all the rows and columns have positive sums, and the overall total is - 108. - But you could instead flip the second and third columns, and the second - row, to get this array: - 33 -30 10 - 16 19 9 - -17 12 14 - All the rows and columns still total positive, and the overall sum is just - 66. So this solution is better (I don't know if it's the best) - A pure brute force solution would have to try over 30 billion solutions. - I wrote code to solve this in J. I'll post that separately. - - Compare with the following models: - * MiniZinc http://www.hakank.org/minizinc/einav_puzzle.mzn - * SICStus: http://hakank.org/sicstus/einav_puzzle.pl - - - This model was created by Hakan Kjellerstrand (hakank@bonetmail.com) - Also see my other Google CP Solver models: - http://www.hakank.org/google_or_tools/ -''' +""" +A programming puzzle from Einav in Google CP Solver. + +From +'A programming puzzle from Einav' +http://gcanyon.wordpress.com/2009/10/28/a-programming-puzzle-from-einav/ + +My friend Einav gave me this programming puzzle to work on. Given +this array of positive and negative numbers: +33 30 -10 -6 18 7 -11 -23 6 +... +-25 4 16 30 33 -23 -4 4 -23 + +You can flip the sign of entire rows and columns, as many of them +as you like. The goal is to make all the rows and columns sum to positive +numbers (or zero), and then to find the solution (there are more than one) +that has the smallest overall sum. So for example, for this array: +33 30 -10 +-16 19 9 +-17 -12 -14 +You could flip the sign for the bottom row to get this array: +33 30 -10 +-16 19 9 +17 12 14 +Now all the rows and columns have positive sums, and the overall total is +108. +But you could instead flip the second and third columns, and the second +row, to get this array: +33 -30 10 +16 19 9 +-17 12 14 +All the rows and columns still total positive, and the overall sum is just +66. So this solution is better (I don't know if it's the best) +A pure brute force solution would have to try over 30 billion solutions. +I wrote code to solve this in J. I'll post that separately. + +Compare with the following models: +* MiniZinc http://www.hakank.org/minizinc/einav_puzzle.mzn +* SICStus: http://hakank.org/sicstus/einav_puzzle.pl + + +This model was created by Hakan Kjellerstrand (hakank@bonetmail.com) +Also see my other Google CP Solver models: +http://www.hakank.org/google_or_tools/ +""" from ortools.constraint_solver import pywrapcp @@ -71,39 +71,41 @@ def main(): # # small problem -# data = [ -# [ 33, 30, -10], -# [-16, 19, 9], -# [-17, -12, -14] -# ] - - data = [[33, 30, 10, -6, 18, -7, -11, 23, -6], - [16, -19, 9, -26, -8, -19, -8, -21, -14], - [17, 12, -14, 31, -30, 13, -13, 19, 16], - [-6, -11, 1, 17, -12, -4, -7, 14, -21], - [18, -31, 34, -22, 17, -19, 20, 24, 6], - [33, -18, 17, -15, 31, -5, 3, 27, -3], - [-18, -20, -18, 31, 6, 4, -2, -12, 24], - [27, 14, 4, -29, -3, 5, -29, 8, -12], - [-15, -7, -23, 23, -9, -8, 6, 8, -12], - [33, -23, -19, -4, -8, -7, 11, -12, 31], - [-20, 19, -15, -30, 11, 32, 7, 14, -5], - [-23, 18, -32, -2, -31, -7, 8, 24, 16], - [32, -4, -10, -14, -6, -1, 0, 23, 23], - [25, 0, -23, 22, 12, 28, -27, 15, 4], - [-30, -13, -16, -3, -3, -32, -3, 27, -31], - [22, 1, 26, 4, -2, -13, 26, 17, 14], - [-9, -18, 3, -20, -27, -32, -11, 27, 13], - [-17, 33, -7, 19, -32, 13, -31, -2, -24], - [-31, 27, -31, -29, 15, 2, 29, -15, 33], - [-18, -23, 15, 28, 0, 30, -4, 12, -32], - [-3, 34, 27, -25, -18, 26, 1, 34, 26], - [-21, -31, -10, -13, -30, -17, -12, -26, 31], - [23, -31, -19, 21, -17, -10, 2, -23, 23], - [-3, 6, 0, -3, -32, 0, -10, -25, 14], - [-19, 9, 14, -27, 20, 15, -5, -27, 18], - [11, -6, 24, 7, -17, 26, 20, -31, -25], - [-25, 4, -16, 30, 33, 23, -4, -4, 23]] + # data = [ + # [ 33, 30, -10], + # [-16, 19, 9], + # [-17, -12, -14] + # ] + + data = [ + [33, 30, 10, -6, 18, -7, -11, 23, -6], + [16, -19, 9, -26, -8, -19, -8, -21, -14], + [17, 12, -14, 31, -30, 13, -13, 19, 16], + [-6, -11, 1, 17, -12, -4, -7, 14, -21], + [18, -31, 34, -22, 17, -19, 20, 24, 6], + [33, -18, 17, -15, 31, -5, 3, 27, -3], + [-18, -20, -18, 31, 6, 4, -2, -12, 24], + [27, 14, 4, -29, -3, 5, -29, 8, -12], + [-15, -7, -23, 23, -9, -8, 6, 8, -12], + [33, -23, -19, -4, -8, -7, 11, -12, 31], + [-20, 19, -15, -30, 11, 32, 7, 14, -5], + [-23, 18, -32, -2, -31, -7, 8, 24, 16], + [32, -4, -10, -14, -6, -1, 0, 23, 23], + [25, 0, -23, 22, 12, 28, -27, 15, 4], + [-30, -13, -16, -3, -3, -32, -3, 27, -31], + [22, 1, 26, 4, -2, -13, 26, 17, 14], + [-9, -18, 3, -20, -27, -32, -11, 27, 13], + [-17, 33, -7, 19, -32, 13, -31, -2, -24], + [-31, 27, -31, -29, 15, 2, 29, -15, 33], + [-18, -23, 15, 28, 0, 30, -4, 12, -32], + [-3, 34, 27, -25, -18, 26, 1, 34, 26], + [-21, -31, -10, -13, -30, -17, -12, -26, 31], + [23, -31, -19, 21, -17, -10, 2, -23, 23], + [-3, 6, 0, -3, -32, 0, -10, -25, 14], + [-19, 9, 14, -27, 20, 15, -5, -27, 18], + [11, -6, 24, 7, -17, 26, 20, -31, -25], + [-25, 4, -16, 30, 33, 23, -4, -4, 23], + ] rows = len(data) cols = len(data[0]) @@ -116,10 +118,8 @@ def main(): for j in range(cols): x[i, j] = solver.IntVar(-100, 100, 'x[%i,%i]' % (i, j)) - row_signs = [solver.IntVar([-1, 1], 'row_signs(%i)' % i) - for i in range(rows)] - col_signs = [solver.IntVar([-1, 1], 'col_signs(%i)' % j) - for j in range(cols)] + row_signs = [solver.IntVar([-1, 1], 'row_signs(%i)' % i) for i in range(rows)] + col_signs = [solver.IntVar([-1, 1], 'col_signs(%i)' % j) for j in range(cols)] # # constraints @@ -131,15 +131,17 @@ def main(): total_sum = solver.Sum([x[i, j] for i in range(rows) for j in range(cols)]) # row sums - row_sums = [solver.Sum([x[i, j] for j in range(cols)]).Var() - for i in range(rows)] + row_sums = [ + solver.Sum([x[i, j] for j in range(cols)]).Var() for i in range(rows) + ] # >= 0 for i in range(rows): row_sums[i].SetMin(0) # column sums - col_sums = [solver.Sum([x[i, j] for i in range(rows)]).Var() - for j in range(cols)] + col_sums = [ + solver.Sum([x[i, j] for i in range(rows)]).Var() for j in range(cols) + ] for j in range(cols): col_sums[j].SetMin(0) @@ -149,9 +151,11 @@ def main(): # # search and result # - db = solver.Phase(col_signs + row_signs, - solver.CHOOSE_FIRST_UNBOUND, - solver.ASSIGN_MIN_VALUE) + db = solver.Phase( + col_signs + row_signs, + solver.CHOOSE_FIRST_UNBOUND, + solver.ASSIGN_MIN_VALUE, + ) search_log = solver.SearchLog(100000, total_sum) solver.NewSearch(db, [objective, search_log]) @@ -164,7 +168,7 @@ def main(): print('col_sums:', [col_sums[j].Value() for j in range(cols)]) for i in range(rows): for j in range(cols): - print(x[i, j].Value(),', ') + print(x[i, j].Value(), ', ') print('\n') print('\n') diff --git a/examples/tests/issue62.py b/examples/tests/issue62.py index afb6e0b2caa..5cce214e7e3 100755 --- a/examples/tests/issue62.py +++ b/examples/tests/issue62.py @@ -3,26 +3,28 @@ def main(): - solver = pywrapcp.Solver('Ordo') - tasks = [solver.FixedDurationIntervalVar(0, 25, 5, False, 'Tasks%i' %i) - for i in range(3)] - print(tasks) - disj = solver.DisjunctiveConstraint(tasks, 'Disjunctive') - sequence = [] - sequence.append(disj.SequenceVar()) - solver.Add(disj) - collector = solver.AllSolutionCollector() - collector.Add(sequence) - collector.Add(tasks) - sequencePhase = solver.Phase(sequence, solver.SEQUENCE_DEFAULT) - intervalPhase = solver.Phase(tasks, solver.INTERVAL_DEFAULT) - mainPhase = solver.Compose([sequencePhase, intervalPhase]) - solver.Solve(mainPhase, [ collector]) - print(collector.SolutionCount()) - for i in range(collector.SolutionCount()): - print("Solution " , i) - print([collector.StartValue(i, tasks[j]) for j in range(3)]) - print([collector.EndValue(i, tasks[j]) for j in range(3)]) + solver = pywrapcp.Solver('Ordo') + tasks = [ + solver.FixedDurationIntervalVar(0, 25, 5, False, 'Tasks%i' % i) + for i in range(3) + ] + print(tasks) + disj = solver.DisjunctiveConstraint(tasks, 'Disjunctive') + sequence = [] + sequence.append(disj.SequenceVar()) + solver.Add(disj) + collector = solver.AllSolutionCollector() + collector.Add(sequence) + collector.Add(tasks) + sequencePhase = solver.Phase(sequence, solver.SEQUENCE_DEFAULT) + intervalPhase = solver.Phase(tasks, solver.INTERVAL_DEFAULT) + mainPhase = solver.Compose([sequencePhase, intervalPhase]) + solver.Solve(mainPhase, [collector]) + print(collector.SolutionCount()) + for i in range(collector.SolutionCount()): + print('Solution ', i) + print([collector.StartValue(i, tasks[j]) for j in range(3)]) + print([collector.EndValue(i, tasks[j]) for j in range(3)]) if __name__ == '__main__': diff --git a/makefiles/Makefile.cpp.mk b/makefiles/Makefile.cpp.mk index 3d3e656ba0c..d26ede715f6 100644 --- a/makefiles/Makefile.cpp.mk +++ b/makefiles/Makefile.cpp.mk @@ -35,7 +35,7 @@ endif BUILD_TYPE ?= Release USE_COINOR ?= ON USE_GLPK ?= OFF -USE_HIGHS ?= OFF +USE_HIGHS ?= ON USE_PDLP := ON # OFF not supported USE_SCIP ?= ON USE_CPLEX ?= OFF diff --git a/makefiles/Makefile.dotnet.mk b/makefiles/Makefile.dotnet.mk index a8bc1d53996..0b95e2efad5 100644 --- a/makefiles/Makefile.dotnet.mk +++ b/makefiles/Makefile.dotnet.mk @@ -162,7 +162,7 @@ endif cd $(TEMP_DOTNET_DIR)$S$1$S$$* && "$(DOTNET_BIN)" clean -c Release -v minimal endef -DOTNET_SAMPLES := init algorithms graph constraint_solver linear_solver sat util +DOTNET_SAMPLES := init algorithms graph constraint_solver linear_solver routing sat util $(foreach sample,$(DOTNET_SAMPLES),$(eval $(call dotnet-sample-target,$(sample)))) # Examples @@ -307,7 +307,7 @@ endif cd $(TEMP_DOTNET_DIR)$S$1$S$$* && "$(DOTNET_BIN)" clean -c Release -v minimal endef -DOTNET_TESTS := init algorithms graph constraint_solver linear_solver sat util +DOTNET_TESTS := init algorithms graph constraint_solver linear_solver routing sat util $(foreach test,$(DOTNET_TESTS),$(eval $(call dotnet-test-target,$(test)))) #################### diff --git a/makefiles/Makefile.java.mk b/makefiles/Makefile.java.mk index e8e59a5cd0b..da28cac308f 100644 --- a/makefiles/Makefile.java.mk +++ b/makefiles/Makefile.java.mk @@ -176,7 +176,7 @@ rjava_%: \ cd $(TEMP_JAVA_DIR)$S$1$S$$* && "$(MVN_BIN)" exec:java $(ARGS) endef -JAVA_SAMPLES := init algorithms graph constraint_solver linear_solver sat util +JAVA_SAMPLES := init algorithms graph constraint_solver linear_solver routing sat util $(foreach sample,$(JAVA_SAMPLES),$(eval $(call java-sample-target,$(sample),$(subst _,,$(sample))))) # Examples @@ -275,7 +275,7 @@ rjava_%: \ cd $(TEMP_JAVA_DIR)$S$1$S$$* && "$(MVN_BIN)" test $(ARGS) endef -JAVA_TESTS := init algorithms graph constraint_solver linear_solver sat util +JAVA_TESTS := init algorithms graph constraint_solver linear_solver routing sat util $(foreach test,$(JAVA_TESTS),$(eval $(call java-test-target,$(test)))) #################### diff --git a/ortools/algorithms/BUILD.bazel b/ortools/algorithms/BUILD.bazel index 4f4def32e75..ed81524193e 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -240,6 +240,7 @@ cc_test( "//ortools/base:gmock_main", "//ortools/util:time_limit", "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/types:span", ], ) diff --git a/ortools/algorithms/knapsack_solver_test.cc b/ortools/algorithms/knapsack_solver_test.cc index 1589f20e437..2c7572aead4 100644 --- a/ortools/algorithms/knapsack_solver_test.cc +++ b/ortools/algorithms/knapsack_solver_test.cc @@ -18,6 +18,7 @@ #include #include "absl/base/macros.h" +#include "absl/types/span.h" #include "gtest/gtest.h" #include "ortools/util/time_limit.h" @@ -26,8 +27,8 @@ namespace { const int kInvalidSolution = -1; -bool IsSolutionValid(const std::vector& profits, - const std::vector >& weights, +bool IsSolutionValid(absl::Span profits, + absl::Span> weights, const std::vector& capacities, const std::vector& best_solution, int64_t optimal_profit) { @@ -59,7 +60,7 @@ int64_t SolveKnapsackProblemUsingSpecificSolverAndReduction( std::vector profits(profit_array, profit_array + number_of_items); std::vector capacities(capacity_array, capacity_array + number_of_dimensions); - std::vector > weights; + std::vector> weights; for (int i = 0; i < number_of_dimensions; ++i) { const int64_t* one_dimension = weight_array + number_of_items * i; std::vector weights_one_dimension(one_dimension, @@ -484,7 +485,7 @@ TEST(KnapsackSolverTest, SolveTwoDimensionsSettingPrimaryPropagator) { std::vector profits(kProfitArray, kProfitArray + kArraySize); std::vector capacities(kCapacityArray, kCapacityArray + kNumberOfDimensions); - std::vector > weights; + std::vector> weights; for (int i = 0; i < kNumberOfDimensions; ++i) { const int64_t* one_dimension = kWeightArray + kArraySize * i; std::vector weights_one_dimension(one_dimension, diff --git a/ortools/algorithms/n_choose_k_test.cc b/ortools/algorithms/n_choose_k_test.cc index 3c5c86bfe3f..2d35c7c28df 100644 --- a/ortools/algorithms/n_choose_k_test.cc +++ b/ortools/algorithms/n_choose_k_test.cc @@ -28,14 +28,14 @@ #include "benchmark/benchmark.h" #include "gtest/gtest.h" #include "ortools/base/dump_vars.h" -//#include "ortools/base/fuzztest.h" +#include "ortools/base/fuzztest.h" #include "ortools/base/gmock.h" #include "ortools/base/mathutil.h" #include "ortools/util/flat_matrix.h" namespace operations_research { namespace { -//using ::fuzztest::NonNegative; +using ::fuzztest::NonNegative; using ::testing::HasSubstr; using ::testing::status::IsOkAndHolds; using ::testing::status::StatusIs; @@ -271,12 +271,11 @@ void MatchesLogCombinations(int n, int k) { << " (value: " << approx << "), which fits in int64_t"; } } -/* FUZZ_TEST(NChooseKTest, MatchesLogCombinations) // Ideally we'd test with `uint64_t`, but `LogCombinations` only accepts // `int`. .WithDomains(NonNegative(), NonNegative()); -*/ + template void BM_NChooseK(benchmark::State& state) { static constexpr int kNumInputs = 1000; diff --git a/ortools/algorithms/python/knapsack_solver_test.py b/ortools/algorithms/python/knapsack_solver_test.py index 95c990183b2..17ace59fd65 100755 --- a/ortools/algorithms/python/knapsack_solver_test.py +++ b/ortools/algorithms/python/knapsack_solver_test.py @@ -23,249 +23,247 @@ class PyWrapAlgorithmsKnapsackSolverTest(absltest.TestCase): - def RealSolve(self, profits, weights, capacities, solver_type, use_reduction): - solver = knapsack_solver.KnapsackSolver(solver_type, "solver") - solver.set_use_reduction(use_reduction) - solver.init(profits, weights, capacities) - profit = solver.solve() + def RealSolve(self, profits, weights, capacities, solver_type, use_reduction): + solver = knapsack_solver.KnapsackSolver(solver_type, "solver") + solver.set_use_reduction(use_reduction) + solver.init(profits, weights, capacities) + profit = solver.solve() - return profit + return profit - def SolveKnapsackProblemUsingSpecificSolver( - self, profits, weights, capacities, solver_type - ): - result_when_reduction = self.RealSolve( - profits, weights, capacities, solver_type, True - ) - result_when_no_reduction = self.RealSolve( - profits, weights, capacities, solver_type, False - ) + def SolveKnapsackProblemUsingSpecificSolver( + self, profits, weights, capacities, solver_type + ): + result_when_reduction = self.RealSolve( + profits, weights, capacities, solver_type, True + ) + result_when_no_reduction = self.RealSolve( + profits, weights, capacities, solver_type, False + ) - if result_when_reduction == result_when_no_reduction: - return result_when_reduction - else: - return self._invalid_solution + if result_when_reduction == result_when_no_reduction: + return result_when_reduction + else: + return self._invalid_solution - def SolveKnapsackProblem(self, profits, weights, capacities): - self._invalid_solution = -1 - max_number_of_items_for_brute_force = 15 - max_number_of_items_for_divide_and_conquer = 32 - max_number_of_items_for_64_items_solver = 64 - number_of_items = len(profits) - # This test is ran as size = 'small. To be fast enough, the dynamic - # programming solver should be limited to instances with capacities smaller - # than 10^6. - max_capacity_for_dynamic_programming_solver = 1000000 - generic_profit = self.SolveKnapsackProblemUsingSpecificSolver( - profits, - weights, - capacities, - knapsack_solver.SolverType.KNAPSACK_MULTIDIMENSION_BRANCH_AND_BOUND_SOLVER, - ) + def SolveKnapsackProblem(self, profits, weights, capacities): + self._invalid_solution = -1 + max_number_of_items_for_brute_force = 15 + max_number_of_items_for_divide_and_conquer = 32 + max_number_of_items_for_64_items_solver = 64 + number_of_items = len(profits) + # This test is ran as size = 'small. To be fast enough, the dynamic + # programming solver should be limited to instances with capacities smaller + # than 10^6. + max_capacity_for_dynamic_programming_solver = 1000000 + generic_profit = self.SolveKnapsackProblemUsingSpecificSolver( + profits, + weights, + capacities, + knapsack_solver.SolverType.KNAPSACK_MULTIDIMENSION_BRANCH_AND_BOUND_SOLVER, + ) - if generic_profit == self._invalid_solution: - return self._invalid_solution + if generic_profit == self._invalid_solution: + return self._invalid_solution - # Disabled due to ASAN raising a runtime error: - # outside the range of representable values of type 'int' - # cbc_profit = self.SolveKnapsackProblemUsingSpecificSolver( - # profits, - # weights, - # capacities, - # knapsack_solver.SolverType. - # KNAPSACK_MULTIDIMENSION_CBC_MIP_SOLVER) - # if cbc_profit != generic_profit: - # return self._invalid_solution + # Disabled due to ASAN raising a runtime error: + # outside the range of representable values of type 'int' + # cbc_profit = self.SolveKnapsackProblemUsingSpecificSolver( + # profits, + # weights, + # capacities, + # knapsack_solver.SolverType. + # KNAPSACK_MULTIDIMENSION_CBC_MIP_SOLVER) + # if cbc_profit != generic_profit: + # return self._invalid_solution - try: - scip_profit = self.SolveKnapsackProblemUsingSpecificSolver( - profits, - weights, - capacities, - knapsack_solver.SolverType.KNAPSACK_MULTIDIMENSION_SCIP_MIP_SOLVER, - ) - if scip_profit != generic_profit: - return self._invalid_solution - except AttributeError: - print("SCIP support not compiled in") + try: + scip_profit = self.SolveKnapsackProblemUsingSpecificSolver( + profits, + weights, + capacities, + knapsack_solver.SolverType.KNAPSACK_MULTIDIMENSION_SCIP_MIP_SOLVER, + ) + if scip_profit != generic_profit: + return self._invalid_solution + except AttributeError: + print("SCIP support not compiled in") - if len(weights) > 1: - return generic_profit + if len(weights) > 1: + return generic_profit - if number_of_items <= max_number_of_items_for_brute_force: - brute_force_profit = self.SolveKnapsackProblemUsingSpecificSolver( - profits, - weights, - capacities, - knapsack_solver.SolverType.KNAPSACK_BRUTE_FORCE_SOLVER, - ) - if brute_force_profit != generic_profit: - return self._invalid_solution + if number_of_items <= max_number_of_items_for_brute_force: + brute_force_profit = self.SolveKnapsackProblemUsingSpecificSolver( + profits, + weights, + capacities, + knapsack_solver.SolverType.KNAPSACK_BRUTE_FORCE_SOLVER, + ) + if brute_force_profit != generic_profit: + return self._invalid_solution - if number_of_items <= max_number_of_items_for_64_items_solver: - items64_profit = self.SolveKnapsackProblemUsingSpecificSolver( - profits, - weights, - capacities, - knapsack_solver.SolverType.KNAPSACK_64ITEMS_SOLVER, - ) - if items64_profit != generic_profit: - return self._invalid_solution + if number_of_items <= max_number_of_items_for_64_items_solver: + items64_profit = self.SolveKnapsackProblemUsingSpecificSolver( + profits, + weights, + capacities, + knapsack_solver.SolverType.KNAPSACK_64ITEMS_SOLVER, + ) + if items64_profit != generic_profit: + return self._invalid_solution - if capacities[0] <= max_capacity_for_dynamic_programming_solver: - dynamic_programming_profit = self.SolveKnapsackProblemUsingSpecificSolver( - profits, - weights, - capacities, - knapsack_solver.SolverType.KNAPSACK_DYNAMIC_PROGRAMMING_SOLVER, - ) - if dynamic_programming_profit != generic_profit: - return self._invalid_solution + if capacities[0] <= max_capacity_for_dynamic_programming_solver: + dynamic_programming_profit = self.SolveKnapsackProblemUsingSpecificSolver( + profits, + weights, + capacities, + knapsack_solver.SolverType.KNAPSACK_DYNAMIC_PROGRAMMING_SOLVER, + ) + if dynamic_programming_profit != generic_profit: + return self._invalid_solution - if number_of_items <= max_number_of_items_for_divide_and_conquer: - divide_and_conquer_profit = self.SolveKnapsackProblemUsingSpecificSolver( - profits, - weights, - capacities, - knapsack_solver.SolverType.KNAPSACK_DIVIDE_AND_CONQUER_SOLVER, - ) - if divide_and_conquer_profit != generic_profit: - return self._invalid_solution + if number_of_items <= max_number_of_items_for_divide_and_conquer: + divide_and_conquer_profit = self.SolveKnapsackProblemUsingSpecificSolver( + profits, + weights, + capacities, + knapsack_solver.SolverType.KNAPSACK_DIVIDE_AND_CONQUER_SOLVER, + ) + if divide_and_conquer_profit != generic_profit: + return self._invalid_solution - return generic_profit + return generic_profit - def testSolveOneDimension(self): - profits = [1, 2, 3, 4, 5, 6, 7, 8, 9] - weights = [[1, 2, 3, 4, 5, 6, 7, 8, 9]] - capacities = [34] - optimal_profit = 34 - profit = self.SolveKnapsackProblem(profits, weights, capacities) - self.assertEqual(optimal_profit, profit) + def testSolveOneDimension(self): + profits = [1, 2, 3, 4, 5, 6, 7, 8, 9] + weights = [[1, 2, 3, 4, 5, 6, 7, 8, 9]] + capacities = [34] + optimal_profit = 34 + profit = self.SolveKnapsackProblem(profits, weights, capacities) + self.assertEqual(optimal_profit, profit) - def testSolveTwoDimensions(self): - profits = [1, 2, 3, 4, 5, 6, 7, 8, 9] - weights = [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 1, 1, 1, 1, 1, 1, 1]] - capacities = [34, 4] - optimal_profit = 30 - profit = self.SolveKnapsackProblem(profits, weights, capacities) - self.assertEqual(optimal_profit, profit) + def testSolveTwoDimensions(self): + profits = [1, 2, 3, 4, 5, 6, 7, 8, 9] + weights = [[1, 2, 3, 4, 5, 6, 7, 8, 9], [1, 1, 1, 1, 1, 1, 1, 1, 1]] + capacities = [34, 4] + optimal_profit = 30 + profit = self.SolveKnapsackProblem(profits, weights, capacities) + self.assertEqual(optimal_profit, profit) - def testSolveBigOneDimension(self): - profits = [ - 360, - 83, - 59, - 130, - 431, - 67, - 230, - 52, - 93, - 125, - 670, - 892, - 600, - 38, - 48, - 147, - 78, - 256, - 63, - 17, - 120, - 164, - 432, - 35, - 92, - 110, - 22, - 42, - 50, - 323, - 514, - 28, - 87, - 73, - 78, - 15, - 26, - 78, - 210, - 36, - 85, - 189, - 274, - 43, - 33, - 10, - 19, - 389, - 276, - 312, - ] - weights = [ - [ - 7, - 0, - 30, - 22, - 80, - 94, - 11, - 81, - 70, - 64, - 59, - 18, - 0, - 36, - 3, - 8, - 15, - 42, - 9, - 0, - 42, - 47, - 52, - 32, - 26, - 48, - 55, - 6, - 29, - 84, - 2, - 4, - 18, - 56, - 7, - 29, - 93, - 44, - 71, - 3, - 86, - 66, - 31, - 65, - 0, - 79, - 20, - 65, - 52, - 13, - ] - ] - capacities = [850] - optimal_profit = 7534 - profit = self.SolveKnapsackProblem(profits, weights, capacities) - self.assertEqual(optimal_profit, profit) + def testSolveBigOneDimension(self): + profits = [ + 360, + 83, + 59, + 130, + 431, + 67, + 230, + 52, + 93, + 125, + 670, + 892, + 600, + 38, + 48, + 147, + 78, + 256, + 63, + 17, + 120, + 164, + 432, + 35, + 92, + 110, + 22, + 42, + 50, + 323, + 514, + 28, + 87, + 73, + 78, + 15, + 26, + 78, + 210, + 36, + 85, + 189, + 274, + 43, + 33, + 10, + 19, + 389, + 276, + 312, + ] + weights = [[ + 7, + 0, + 30, + 22, + 80, + 94, + 11, + 81, + 70, + 64, + 59, + 18, + 0, + 36, + 3, + 8, + 15, + 42, + 9, + 0, + 42, + 47, + 52, + 32, + 26, + 48, + 55, + 6, + 29, + 84, + 2, + 4, + 18, + 56, + 7, + 29, + 93, + 44, + 71, + 3, + 86, + 66, + 31, + 65, + 0, + 79, + 20, + 65, + 52, + 13, + ]] + capacities = [850] + optimal_profit = 7534 + profit = self.SolveKnapsackProblem(profits, weights, capacities) + self.assertEqual(optimal_profit, profit) def main(_): - absltest.main() + absltest.main() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/algorithms/samples/knapsack.py b/ortools/algorithms/samples/knapsack.py index eb63388c4ef..84da3b67962 100644 --- a/ortools/algorithms/samples/knapsack.py +++ b/ortools/algorithms/samples/knapsack.py @@ -16,58 +16,59 @@ # [START program] # [START import] from ortools.algorithms.python import knapsack_solver + # [END import] def main(): - # Create the solver. - # [START solver] - solver = knapsack_solver.KnapsackSolver( - knapsack_solver.SolverType.KNAPSACK_MULTIDIMENSION_BRANCH_AND_BOUND_SOLVER, - "KnapsackExample", - ) - # [END solver] + # Create the solver. + # [START solver] + solver = knapsack_solver.KnapsackSolver( + knapsack_solver.SolverType.KNAPSACK_MULTIDIMENSION_BRANCH_AND_BOUND_SOLVER, + "KnapsackExample", + ) + # [END solver] - # [START data] - values = [ - # fmt:off + # [START data] + values = [ + # fmt:off 360, 83, 59, 130, 431, 67, 230, 52, 93, 125, 670, 892, 600, 38, 48, 147, 78, 256, 63, 17, 120, 164, 432, 35, 92, 110, 22, 42, 50, 323, 514, 28, 87, 73, 78, 15, 26, 78, 210, 36, 85, 189, 274, 43, 33, 10, 19, 389, 276, 312 - # fmt:on - ] - weights = [ - # fmt: off + # fmt:on + ] + weights = [ + # fmt: off [7, 0, 30, 22, 80, 94, 11, 81, 70, 64, 59, 18, 0, 36, 3, 8, 15, 42, 9, 0, 42, 47, 52, 32, 26, 48, 55, 6, 29, 84, 2, 4, 18, 56, 7, 29, 93, 44, 71, 3, 86, 66, 31, 65, 0, 79, 20, 65, 52, 13], - # fmt: on - ] - capacities = [850] - # [END data] + # fmt: on + ] + capacities = [850] + # [END data] - # [START solve] - solver.init(values, weights, capacities) - computed_value = solver.solve() - # [END solve] + # [START solve] + solver.init(values, weights, capacities) + computed_value = solver.solve() + # [END solve] - # [START print_solution] - packed_items = [] - packed_weights = [] - total_weight = 0 - print("Total value =", computed_value) - for i in range(len(values)): - if solver.best_solution_contains(i): - packed_items.append(i) - packed_weights.append(weights[0][i]) - total_weight += weights[0][i] - print("Total weight:", total_weight) - print("Packed items:", packed_items) - print("Packed_weights:", packed_weights) - # [END print_solution] + # [START print_solution] + packed_items = [] + packed_weights = [] + total_weight = 0 + print("Total value =", computed_value) + for i in range(len(values)): + if solver.best_solution_contains(i): + packed_items.append(i) + packed_weights.append(weights[0][i]) + total_weight += weights[0][i] + print("Total weight:", total_weight) + print("Packed items:", packed_items) + print("Packed_weights:", packed_weights) + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/algorithms/samples/simple_knapsack_program.py b/ortools/algorithms/samples/simple_knapsack_program.py index 03ab5729d3c..0e5f0c5546d 100644 --- a/ortools/algorithms/samples/simple_knapsack_program.py +++ b/ortools/algorithms/samples/simple_knapsack_program.py @@ -16,45 +16,46 @@ """A simple knapsack problem.""" # [START import] from ortools.algorithms.python import knapsack_solver + # [END import] def main(): - # Create the solver. - # [START solver] - solver = knapsack_solver.KnapsackSolver( - knapsack_solver.SolverType.KNAPSACK_DYNAMIC_PROGRAMMING_SOLVER, - "test", - ) - # [END solver] - - # [START data] - weights = [ - # fmt:off + # Create the solver. + # [START solver] + solver = knapsack_solver.KnapsackSolver( + knapsack_solver.SolverType.KNAPSACK_DYNAMIC_PROGRAMMING_SOLVER, + "test", + ) + # [END solver] + + # [START data] + weights = [ + # fmt:off [565, 406, 194, 130, 435, 367, 230, 315, 393, 125, 670, 892, 600, 293, 712, 147, 421, 255], - # fmt:on - ] - capacities = [850] - values = weights[0] - # [END data] + # fmt:on + ] + capacities = [850] + values = weights[0] + # [END data] - # [START solve] - solver.init(values, weights, capacities) - computed_value = solver.solve() - # [END solve] + # [START solve] + solver.init(values, weights, capacities) + computed_value = solver.solve() + # [END solve] - # [START print_solution] - packed_items = [ - x for x in range(0, len(weights[0])) if solver.best_solution_contains(x) - ] - packed_weights = [weights[0][i] for i in packed_items] + # [START print_solution] + packed_items = [ + x for x in range(0, len(weights[0])) if solver.best_solution_contains(x) + ] + packed_weights = [weights[0][i] for i in packed_items] - print("Packed items: ", packed_items) - print("Packed weights: ", packed_weights) - print("Total weight (same as total value): ", computed_value) - # [END print_solution] + print("Packed items: ", packed_items) + print("Packed weights: ", packed_weights) + print("Total weight (same as total value): ", computed_value) + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index f5750c3e04b..568647b0e26 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -74,8 +74,8 @@ cc_library( "version.h", ], copts = [ - "-DOR_TOOLS_MAJOR=9", - "-DOR_TOOLS_MINOR=13", + "-DOR_TOOLS_MAJOR=10", + "-DOR_TOOLS_MINOR=0", "-DOR_TOOLS_PATCH=9999", ], linkopts = select({ @@ -183,6 +183,8 @@ cc_library( "//conditions:default": [], }), deps = [ + ":strong_int", + ":strong_vector", "@abseil-cpp//absl/container:inlined_vector", ], ) @@ -199,27 +201,13 @@ cc_test( }), deps = [ ":dump_vars", + ":strong_int", + ":strong_vector", "@abseil-cpp//absl/strings", "@googletest//:gtest_main", ], ) -cc_library( - name = "dynamic_library", - hdrs = ["dynamic_library.h"], - linkopts = select({ - "on_linux": ["-Wl,--no-as-needed -ldl"], - "on_macos": [], - "on_windows": [], - "//conditions:default": [], - }), - deps = [ - ":base", - ":logging", - "@abseil-cpp//absl/strings", - ], -) - cc_library( name = "encodingutils", hdrs = ["encodingutils.h"], diff --git a/ortools/base/dump_vars.h b/ortools/base/dump_vars.h index 8413948cd39..b2814c2e539 100644 --- a/ortools/base/dump_vars.h +++ b/ortools/base/dump_vars.h @@ -48,6 +48,8 @@ #include #include "absl/container/inlined_vector.h" +#include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.h" /* need extra level to force extra eval */ #define DUMP_FOR_EACH_N0(F) @@ -138,6 +140,16 @@ std::ostream& operator<<(std::ostream& os, const ::std::optional& opt) { return os; } +// needed by graph tests +template +std::ostream& operator<<(std::ostream& os, + const ::util_intops::StrongVector& vec) { + for (U it : vec) { + os << ::std::to_string(it) << ','; + } + return os; +} + using DumpNames = ::std::vector<::std::string>; struct print_fields { diff --git a/ortools/base/dump_vars_test.cc b/ortools/base/dump_vars_test.cc index 1a295f386a5..81b4e5ae8d7 100644 --- a/ortools/base/dump_vars_test.cc +++ b/ortools/base/dump_vars_test.cc @@ -21,6 +21,12 @@ #include #include "gtest/gtest.h" +#include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.h" + +namespace util_intops { +DEFINE_STRONG_INT_TYPE(CustomStrongInt, uint32_t); +} // namespace util_intops namespace operations_research::base { namespace { @@ -124,6 +130,19 @@ TEST(DumpVars, Vector) { EXPECT_EQ("vec = 49.299999,3.140000,", DUMP_VARS(vec).str()); } +TEST(DumpVars, StrongInt) { + ::util_intops::CustomStrongInt val(42); + EXPECT_EQ(R"(val = 42)", ToString(DUMP_VARS(val))); + EXPECT_EQ(R"(val = 42)", DUMP_VARS(val).str()); +} + +TEST(DumpVars, StrongVector) { + ::util_intops::StrongVector<::util_intops::CustomStrongInt, float> vec = { + 49.3, 3.14}; + EXPECT_EQ(R"(vec = 49.299999,3.140000,)", ToString(DUMP_VARS(vec))); + EXPECT_EQ(R"(vec = 49.299999,3.140000,)", DUMP_VARS(vec).str()); +} + TEST(DumpVars, Optional) { std::optional of = {}; EXPECT_EQ("of = (none)", ToString(DUMP_VARS(of))); diff --git a/ortools/base/proto_enum_utils.h b/ortools/base/proto_enum_utils.h index a78dd61a72c..bdf03310564 100644 --- a/ortools/base/proto_enum_utils.h +++ b/ortools/base/proto_enum_utils.h @@ -175,8 +175,19 @@ namespace internal { template class RepeatedEnumView { public: - class Iterator : public std::iterator { + class Iterator +#if __cplusplus < 201703L + : public std::iterator +#endif + { public: + using difference_type = ptrdiff_t; + using value_type = E; +#if __cplusplus >= 201703L + using iterator_category = std::input_iterator_tag; + using pointer = E*; + using reference = E&; +#endif explicit Iterator(RepeatedField::const_iterator ptr) : ptr_(ptr) {} bool operator==(const Iterator& it) const { return ptr_ == it.ptr_; } bool operator!=(const Iterator& it) const { return ptr_ != it.ptr_; } diff --git a/ortools/constraint_solver/BUILD.bazel b/ortools/constraint_solver/BUILD.bazel index 11956f4539e..33bf13b8134 100644 --- a/ortools/constraint_solver/BUILD.bazel +++ b/ortools/constraint_solver/BUILD.bazel @@ -186,7 +186,6 @@ cc_library( ":search_limit_cc_proto", ":search_stats_cc_proto", ":solver_parameters_cc_proto", - ":routing_parameters_cc_proto", "//ortools/base", "//ortools/base:base_export", "//ortools/base:bitmap", @@ -230,265 +229,3 @@ cc_library( "@abseil-cpp//absl/types:span", ], ) - -# ----- Routing and ArcRouting ----- - -proto_library( - name = "routing_enums_proto", - srcs = ["routing_enums.proto"], -) - -cc_proto_library( - name = "routing_enums_cc_proto", - deps = [":routing_enums_proto"], -) - -java_proto_library( - name = "routing_enums_java_proto", - deps = [":routing_enums_proto"], -) - -proto_library( - name = "routing_ils_proto", - srcs = ["routing_ils.proto"], - deps = [":routing_enums_proto"], -) - -cc_proto_library( - name = "routing_ils_cc_proto", - deps = [":routing_ils_proto"], -) - -py_proto_library( - name = "routing_ils_py_pb2", - deps = [":routing_ils_proto"], -) - -java_proto_library( - name = "routing_ils_java_proto", - deps = [":routing_ils_proto"], -) - -proto_library( - name = "routing_parameters_proto", - srcs = ["routing_parameters.proto"], - deps = [ - ":routing_enums_proto", - ":routing_ils_proto", - ":solver_parameters_proto", - "//ortools/sat:sat_parameters_proto", - "//ortools/util:optional_boolean_proto", - "@protobuf//:duration_proto", - ], -) - -cc_proto_library( - name = "routing_parameters_cc_proto", - deps = [":routing_parameters_proto"], -) - -java_proto_library( - name = "routing_parameters_java_proto", - deps = [":routing_parameters_proto"], -) - -py_proto_library( - name = "routing_parameters_py_pb2", - deps = [":routing_parameters_proto"], -) - -py_proto_library( - name = "routing_enums_py_pb2", - deps = [":routing_enums_proto"], -) - -cc_library( - name = "routing_parameters", - srcs = ["routing_parameters.cc"], - hdrs = ["routing_parameters.h"], - deps = [ - ":cp", - ":routing_enums_cc_proto", - ":routing_ils_cc_proto", - ":routing_parameters_cc_proto", - ":routing_parameters_utils", - ":solver_parameters_cc_proto", - "//ortools/base", - "//ortools/base:proto_enum_utils", - "//ortools/base:protoutil", - "//ortools/base:types", - "//ortools/port:proto_utils", - "//ortools/sat:sat_parameters_cc_proto", - "//ortools/util:optional_boolean_cc_proto", - "//ortools/util:testing_utils", - "@abseil-cpp//absl/container:flat_hash_map", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/strings:str_format", - "@abseil-cpp//absl/time", - "@protobuf//:protobuf", - ], -) - -cc_library( - name = "routing_parameters_utils", - srcs = ["routing_parameters_utils.cc"], - hdrs = ["routing_parameters_utils.h"], - deps = [ - ":routing_parameters_cc_proto", - "//ortools/util:optional_boolean_cc_proto", - "@abseil-cpp//absl/types:span", - ], -) - -cc_library( - name = "routing_types", - hdrs = ["routing_types.h"], - deps = [ - "//ortools/base:int_type", - "//ortools/util:piecewise_linear_function", - ], -) - -cc_library( - name = "routing_utils", - srcs = ["routing_utils.cc"], - hdrs = ["routing_utils.h"], - visibility = ["//visibility:public"], - deps = [ - "//ortools/util:saturated_arithmetic", - "@abseil-cpp//absl/log:check", - "@abseil-cpp//absl/types:span", - ], -) - -cc_library( - name = "routing_neighborhoods", - srcs = ["routing_neighborhoods.cc"], - hdrs = ["routing_neighborhoods.h"], - visibility = ["//visibility:public"], - deps = [ - ":cp", - ":routing_types", - ":routing_utils", - "//ortools/base:types", - "//ortools/util:bitset", - "//ortools/util:saturated_arithmetic", - "@abseil-cpp//absl/log:check", - "@abseil-cpp//absl/types:span", - ], -) - -cc_library( - name = "routing_index_manager", - srcs = ["routing_index_manager.cc"], - hdrs = ["routing_index_manager.h"], - deps = [ - ":routing_types", - "//ortools/base", - "//ortools/base:strong_vector", - "//ortools/base:types", - "@abseil-cpp//absl/container:flat_hash_set", - "@abseil-cpp//absl/log:check", - "@abseil-cpp//absl/types:span", - ], -) - -cc_library( - name = "routing", - srcs = [ - "routing.cc", - "routing_breaks.cc", - "routing_constraints.cc", - "routing_decision_builders.cc", - "routing_filters.cc", - "routing_flow.cc", - "routing_ils.cc", - "routing_insertion_lns.cc", - "routing_lp_scheduling.cc", - "routing_sat.cc", - "routing_search.cc", - ], - hdrs = [ - "routing.h", - "routing_constraints.h", - "routing_decision_builders.h", - "routing_filter_committables.h", - "routing_filters.h", - "routing_ils.h", - "routing_insertion_lns.h", - "routing_lp_scheduling.h", - "routing_search.h", - ], - copts = select({ - "on_linux": [], - "on_macos": [], - "on_windows": ["/Zc:preprocessor"], - "//conditions:default": [], - }), - deps = [ - ":cp", - ":routing_enums_cc_proto", - ":routing_index_manager", - ":routing_neighborhoods", - ":routing_parameters", - ":routing_parameters_cc_proto", - ":routing_types", - ":routing_utils", - ":solver_parameters_cc_proto", - "//ortools/base", - "//ortools/base:dump_vars", - "//ortools/base:int_type", - "//ortools/base:map_util", - "//ortools/base:mathutil", - "//ortools/base:protoutil", - "//ortools/base:stl_util", - "//ortools/base:strong_vector", - "//ortools/base:types", - "//ortools/glop:lp_solver", - "//ortools/glop:parameters_cc_proto", - "//ortools/graph", - "//ortools/graph:christofides", - "//ortools/graph:connected_components", - "//ortools/graph:linear_assignment", - "//ortools/graph:min_cost_flow", - "//ortools/lp_data", - "//ortools/lp_data:base", - "//ortools/port:proto_utils", - "//ortools/sat:cp_model_cc_proto", - "//ortools/sat:cp_model_solver", - "//ortools/sat:integer", - "//ortools/sat:lp_utils", - "//ortools/sat:model", - "//ortools/sat:sat_parameters_cc_proto", - "//ortools/util:bitset", - "//ortools/util:flat_matrix", - "//ortools/util:optional_boolean_cc_proto", - "//ortools/util:piecewise_linear_function", - "//ortools/util:range_minimum_query", - "//ortools/util:range_query_function", - "//ortools/util:saturated_arithmetic", - "//ortools/util:sorted_interval_list", - "//ortools/util:scheduling", - "//ortools/util:time_limit", - "@abseil-cpp//absl/algorithm:container", - "@abseil-cpp//absl/base:core_headers", - "@abseil-cpp//absl/container:btree", - "@abseil-cpp//absl/container:flat_hash_map", - "@abseil-cpp//absl/container:flat_hash_set", - "@abseil-cpp//absl/container:inlined_vector", - "@abseil-cpp//absl/flags:flag", - "@abseil-cpp//absl/functional:bind_front", - "@abseil-cpp//absl/hash", - "@abseil-cpp//absl/log", - "@abseil-cpp//absl/log:check", - "@abseil-cpp//absl/log:die_if_null", - "@abseil-cpp//absl/memory", - "@abseil-cpp//absl/status:statusor", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/strings:str_format", - "@abseil-cpp//absl/time", - "@abseil-cpp//absl/types:span", - "@protobuf//:protobuf", - ], -) - diff --git a/ortools/constraint_solver/README.md b/ortools/constraint_solver/README.md index 0a6688c8146..70a560091c1 100644 --- a/ortools/constraint_solver/README.md +++ b/ortools/constraint_solver/README.md @@ -34,26 +34,8 @@ important for performance. ## Routing solver [Vehicle Routing](http://en.wikipedia.org/wiki/Vehicle_routing) is a useful -extension that is implemented on top of the CP solver library. - -To begin, skim: - -* [routing.h](../constraint_solver/routing.h): -The vehicle routing library lets one model and solve generic vehicle routing -problems ranging from the Traveling Salesman Problem to more complex problems -such as the Capacitated Vehicle Routing Problem with Time Windows. - -### Parameters - -* [routing_parameters.proto](../constraint_solver/routing_parameters.proto): -The Vehicle Routing solver parameters. -* [routing_enums.proto](../constraint_solver/routing_enums.proto): -Enums used to define routing parameters. - -### Solution - -* [assignment.proto](assignment.proto): -Holds the solution of a Routing problem. +extension that is implemented on top of the CP solver library. It is now +available as [a separate module](../routing/README.md). ## Recipes diff --git a/ortools/constraint_solver/constraint_solver.cc b/ortools/constraint_solver/constraint_solver.cc index d739d2744c9..de6ebe007f2 100644 --- a/ortools/constraint_solver/constraint_solver.cc +++ b/ortools/constraint_solver/constraint_solver.cc @@ -1024,7 +1024,7 @@ class Search { bool AtSolution(); bool AcceptSolution(); void NoMoreSolutions(); - bool LocalOptimum(); + bool ContinueAtLocalOptimum(); bool AcceptDelta(Assignment* delta, Assignment* deltadelta); void AcceptNeighbor(); void AcceptUncheckedNeighbor(); @@ -1316,15 +1316,15 @@ bool Search::AtSolution() { void Search::NoMoreSolutions() { CALL_EVENT_LISTENERS(NoMoreSolutions); } -bool Search::LocalOptimum() { - bool at_local_optimum = false; +bool Search::ContinueAtLocalOptimum() { + bool continue_at_local_optimum = false; for (SearchMonitor* const monitor : GetEventListeners(Solver::MonitorEvent::kLocalOptimum)) { - if (monitor->LocalOptimum()) { - at_local_optimum = true; + if (monitor->AtLocalOptimum()) { + continue_at_local_optimum = true; } } - return at_local_optimum; + return continue_at_local_optimum; } bool Search::AcceptDelta(Assignment* delta, Assignment* deltadelta) { @@ -1375,7 +1375,9 @@ void Search::Accept(ModelVisitor* const visitor) const { #undef CALL_EVENT_LISTENERS -bool LocalOptimumReached(Search* search) { return search->LocalOptimum(); } +bool ContinueAtLocalOptimum(Search* search) { + return search->ContinueAtLocalOptimum(); +} bool AcceptDelta(Search* search, Assignment* delta, Assignment* deltadelta) { return search->AcceptDelta(delta, deltadelta); @@ -2894,7 +2896,7 @@ void SearchMonitor::EndInitialPropagation() {} bool SearchMonitor::AcceptSolution() { return true; } bool SearchMonitor::AtSolution() { return false; } void SearchMonitor::NoMoreSolutions() {} -bool SearchMonitor::LocalOptimum() { return false; } +bool SearchMonitor::AtLocalOptimum() { return false; } bool SearchMonitor::AcceptDelta([[maybe_unused]] Assignment* delta, [[maybe_unused]] Assignment* deltadelta) { return true; @@ -3252,6 +3254,10 @@ std::string Solver::SearchContext(const Search* search) const { return search->search_context(); } +bool Solver::AcceptSolution(Search* search) const { + return search->AcceptSolution(); +} + Assignment* Solver::GetOrCreateLocalSearchState() { if (local_search_state_ == nullptr) { local_search_state_ = std::make_unique(this); diff --git a/ortools/constraint_solver/constraint_solver.h b/ortools/constraint_solver/constraint_solver.h index 523e82fe969..3040a32089b 100644 --- a/ortools/constraint_solver/constraint_solver.h +++ b/ortools/constraint_solver/constraint_solver.h @@ -3145,6 +3145,7 @@ class Solver { void SetSearchContext(Search* search, absl::string_view search_context); std::string SearchContext() const; std::string SearchContext(const Search* search) const; + bool AcceptSolution(Search* search) const; /// Returns (or creates) an assignment representing the state of local search. // TODO(user): Investigate if this should be moved to Search. Assignment* GetOrCreateLocalSearchState(); @@ -3975,9 +3976,9 @@ class SearchMonitor : public BaseObject { /// When the search tree is finished. virtual void NoMoreSolutions(); - /// When a local optimum is reached. If 'true' is returned, the last solution - /// is discarded and the search proceeds with the next one. - virtual bool LocalOptimum(); + /// Called when a local optimum is reached. If 'true' is returned, the last + /// solution is discarded and the search proceeds with the next one. + virtual bool AtLocalOptimum(); /// virtual bool AcceptDelta(Assignment* delta, Assignment* deltadelta); diff --git a/ortools/constraint_solver/csharp/CMakeLists.txt b/ortools/constraint_solver/csharp/CMakeLists.txt index f0b1d4067ec..c852e752cb1 100644 --- a/ortools/constraint_solver/csharp/CMakeLists.txt +++ b/ortools/constraint_solver/csharp/CMakeLists.txt @@ -11,18 +11,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -set_property(SOURCE routing.i PROPERTY CPLUSPLUS ON) -set_property(SOURCE routing.i PROPERTY SWIG_MODULE_NAME operations_research_constraint_solver) -set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS +set_property(SOURCE constraint_solver.i PROPERTY CPLUSPLUS ON) +set_property(SOURCE constraint_solver.i PROPERTY SWIG_MODULE_NAME ConstraintSolverGlobals) +set_property(SOURCE constraint_solver.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) -set_property(SOURCE routing.i PROPERTY COMPILE_OPTIONS +set_property(SOURCE constraint_solver.i PROPERTY COMPILE_OPTIONS -namespace ${DOTNET_PROJECT}.ConstraintSolver -dllimport google-ortools-native) swig_add_library(dotnet_constraint_solver TYPE OBJECT LANGUAGE csharp OUTPUT_DIR ${DOTNET_PROJECT_DIR}/ortools/constraint_solver - SOURCES routing.i) + SOURCES constraint_solver.i) #target_include_directories(dotnet_constraint_solver PRIVATE ${DOTNET_INCLUDE_DIRS}) set_target_properties(dotnet_constraint_solver PROPERTIES diff --git a/ortools/constraint_solver/csharp/ConstraintSolverTests.cs b/ortools/constraint_solver/csharp/ConstraintSolverTests.cs index f4d9c47ffed..d471e1e8e1d 100644 --- a/ortools/constraint_solver/csharp/ConstraintSolverTests.cs +++ b/ortools/constraint_solver/csharp/ConstraintSolverTests.cs @@ -14,7 +14,7 @@ using System; using Xunit; using Google.OrTools.ConstraintSolver; -using static Google.OrTools.ConstraintSolver.operations_research_constraint_solver; +using static Google.OrTools.ConstraintSolver.ConstraintSolverGlobals; namespace Google.OrTools.Tests { diff --git a/ortools/constraint_solver/csharp/constraint_solver.i b/ortools/constraint_solver/csharp/constraint_solver.i index e7d9b53671b..4bdac92a98b 100644 --- a/ortools/constraint_solver/csharp/constraint_solver.i +++ b/ortools/constraint_solver/csharp/constraint_solver.i @@ -34,7 +34,7 @@ class ConstraintSolverParameters; class RegularLimitParameters; } // namespace operations_research -%module(directors="1") operations_research; +%module(directors="1") ConstraintSolverGlobals; #pragma SWIG nowarn=473 %{ @@ -947,13 +947,13 @@ PROTO_INPUT(operations_research::CpModel, PROTO2_RETURN(operations_research::CpModel, Google.OrTools.ConstraintSolver.CpModel) -// Add needed import to operations_research_constraint_solver.cs +// Add needed import to ConstraintSolverGlobals.cs %pragma(csharp) moduleimports=%{ %} namespace operations_research { // Globals -// IMPORTANT(user): Global will be placed in operations_research_constraint_solver.cs +// IMPORTANT(user): Global will be placed in ConstraintSolverGlobals.cs // Ignored: %ignore FillValues; } // namespace operations_research diff --git a/ortools/constraint_solver/docs/CP.md b/ortools/constraint_solver/docs/CP.md index 2865706a0fc..9909a28221d 100644 --- a/ortools/constraint_solver/docs/CP.md +++ b/ortools/constraint_solver/docs/CP.md @@ -12,12 +12,13 @@ Java and .Net. Each language have different requirements for the code samples. ### C++ code samples ```cpp +// Snippet from ortools/constraint_solver/samples/simple_cp_program.cc #include #include +#include "ortools/base/init_google.h" #include "absl/base/log_severity.h" #include "absl/log/globals.h" -#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" namespace operations_research { @@ -73,56 +74,57 @@ int main(int argc, char* argv[]) { ### Python code samples ```python -#!/usr/bin/env python3 +# Snippet from ortools/constraint_solver/samples/simple_cp_program.py """Simple Constraint optimization example.""" from ortools.constraint_solver import pywrapcp def main(): - """Entry point of the program.""" - # Instantiate the solver. - solver = pywrapcp.Solver("CPSimple") - - # Create the variables. - num_vals = 3 - x = solver.IntVar(0, num_vals - 1, "x") - y = solver.IntVar(0, num_vals - 1, "y") - z = solver.IntVar(0, num_vals - 1, "z") - - # Constraint 0: x != y. - solver.Add(x != y) - print("Number of constraints: ", solver.Constraints()) - - # Solve the problem. - decision_builder = solver.Phase( - [x, y, z], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE - ) - - # Print solution on console. - count = 0 - solver.NewSearch(decision_builder) - while solver.NextSolution(): - count += 1 - solution = f"Solution {count}:\n" - for var in [x, y, z]: - solution += f" {var.Name()} = {var.Value()}" - print(solution) - solver.EndSearch() - print(f"Number of solutions found: {count}") - - print("Advanced usage:") - print(f"Problem solved in {solver.WallTime()}ms") - print(f"Memory usage: {pywrapcp.Solver.MemoryUsage()}bytes") + """Entry point of the program.""" + # Instantiate the solver. + solver = pywrapcp.Solver("CPSimple") + + # Create the variables. + num_vals = 3 + x = solver.IntVar(0, num_vals - 1, "x") + y = solver.IntVar(0, num_vals - 1, "y") + z = solver.IntVar(0, num_vals - 1, "z") + + # Constraint 0: x != y. + solver.Add(x != y) + print("Number of constraints: ", solver.Constraints()) + + # Solve the problem. + decision_builder = solver.Phase( + [x, y, z], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + + # Print solution on console. + count = 0 + solver.NewSearch(decision_builder) + while solver.NextSolution(): + count += 1 + solution = f"Solution {count}:\n" + for var in [x, y, z]: + solution += f" {var.Name()} = {var.Value()}" + print(solution) + solver.EndSearch() + print(f"Number of solutions found: {count}") + + print("Advanced usage:") + print(f"Problem solved in {solver.WallTime()}ms") + print(f"Memory usage: {pywrapcp.Solver.MemoryUsage()}bytes") if __name__ == "__main__": - main() + main() ``` ### Java code samples ```java +// Snippet from ortools/constraint_solver/samples/SimpleCpProgram.java package com.google.ortools.constraintsolver.samples; import com.google.ortools.Loader; import com.google.ortools.constraintsolver.DecisionBuilder; @@ -148,74 +150,78 @@ public class SimpleCpProgram { final IntVar z = solver.makeIntVar(0, numVals - 1, "z"); // Constraint 0: x != y.. - solver.addConstraint(solver.makeAllDifferent(new IntVar[] {x, y})); + solver.addConstraint(solver.makeAllDifferent(new IntVar[]{x, y})); logger.info("Number of constraints: " + solver.constraints()); // Solve the problem. final DecisionBuilder db = solver.makePhase( - new IntVar[] {x, y, z}, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE); + new IntVar[]{x, y, z}, + Solver.CHOOSE_FIRST_UNBOUND, + Solver.ASSIGN_MIN_VALUE); // Print solution on console. int count = 0; solver.newSearch(db); while (solver.nextSolution()) { ++count; - logger.info( - String.format("Solution: %d\n x=%d y=%d z=%d", count, x.value(), y.value(), z.value())); + logger.info(String.format("Solution: %d\n x=%d y=%d z=%d" + , count + , x.value() + , y.value() + , z.value())); } solver.endSearch(); logger.info("Number of solutions found: " + solver.solutions()); - logger.info(String.format("Advanced usage:\nProblem solved in %d ms\nMemory usage: %d bytes", - solver.wallTime(), Solver.memoryUsage())); + logger.info(String.format( + "Advanced usage:\nProblem solved in %d ms\nMemory usage: %d bytes" + , solver.wallTime(), Solver.memoryUsage())); } } ``` ### .Net code samples -```cs +```csharp +// Snippet from ortools/constraint_solver/samples/SimpleCpProgram.cs using System; using Google.OrTools.ConstraintSolver; /// /// This is a simple CP program. /// -public class SimpleCpProgram -{ - public static void Main(String[] args) - { - // Instantiate the solver. - Solver solver = new Solver("CpSimple"); - - // Create the variables. - const long numVals = 3; - IntVar x = solver.MakeIntVar(0, numVals - 1, "x"); - IntVar y = solver.MakeIntVar(0, numVals - 1, "y"); - IntVar z = solver.MakeIntVar(0, numVals - 1, "z"); - - // Constraint 0: x != y.. - solver.Add(solver.MakeAllDifferent(new IntVar[] { x, y })); - Console.WriteLine($"Number of constraints: {solver.Constraints()}"); - - // Solve the problem. - DecisionBuilder db = - solver.MakePhase(new IntVar[] { x, y, z }, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE); - - // Print solution on console. - int count = 0; - solver.NewSearch(db); - while (solver.NextSolution()) - { - ++count; - Console.WriteLine($"Solution: {count}\n x={x.Value()} y={y.Value()} z={z.Value()}"); - } - solver.EndSearch(); - Console.WriteLine($"Number of solutions found: {solver.Solutions()}"); - - Console.WriteLine("Advanced usage:"); - Console.WriteLine($"Problem solved in {solver.WallTime()}ms"); - Console.WriteLine($"Memory usage: {Solver.MemoryUsage()}bytes"); +public class SimpleCpProgram { + public static void Main(String[] args) { + // Instantiate the solver. + Solver solver = new Solver("CpSimple"); + + // Create the variables. + const long numVals = 3; + IntVar x = solver.MakeIntVar(0, numVals - 1, "x"); + IntVar y = solver.MakeIntVar(0, numVals - 1, "y"); + IntVar z = solver.MakeIntVar(0, numVals - 1, "z"); + + // Constraint 0: x != y.. + solver.Add(solver.MakeAllDifferent(new IntVar[] { x, y })); + Console.WriteLine($"Number of constraints: {solver.Constraints()}"); + + // Solve the problem. + DecisionBuilder db = solver.MakePhase(new IntVar[] { x, y, z }, Solver.CHOOSE_FIRST_UNBOUND, + Solver.ASSIGN_MIN_VALUE); + + // Print solution on console. + int count = 0; + solver.NewSearch(db); + while (solver.NextSolution()) { + ++count; + Console.WriteLine($"Solution: {count}\n x={x.Value()} y={y.Value()} z={z.Value()}"); } + solver.EndSearch(); + Console.WriteLine($"Number of solutions found: {solver.Solutions()}"); + + Console.WriteLine("Advanced usage:"); + Console.WriteLine($"Problem solved in {solver.WallTime()}ms"); + Console.WriteLine($"Memory usage: {Solver.MemoryUsage()}bytes"); + } } ``` diff --git a/ortools/constraint_solver/docs/README.md b/ortools/constraint_solver/docs/README.md index c28d352b506..8ce4e8294bf 100644 --- a/ortools/constraint_solver/docs/README.md +++ b/ortools/constraint_solver/docs/README.md @@ -11,7 +11,7 @@ You can find here the documentation for the two following OR-Tools components. **note:** We **strongly recommend** using the [CP-SAT solver](../../sat) rather than the original CP solver. -* [Routing Solver](ROUTING.md) +* [Routing Solver](../../routing/docs/ROUTING.md) A specialized library for identifying best vehicle routes given constraints. diff --git a/ortools/constraint_solver/docs/routing_svg.py b/ortools/constraint_solver/docs/routing_svg.py deleted file mode 100755 index 54f188a52c9..00000000000 --- a/ortools/constraint_solver/docs/routing_svg.py +++ /dev/null @@ -1,1219 +0,0 @@ -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Generate SVG for a Routing problem.""" - -# [START import] -import argparse -from ortools.constraint_solver import pywrapcp -from ortools.constraint_solver import routing_enums_pb2 -# [END import] - - -# [START data_model] -class DataModel(object): # pylint: disable=too-many-instance-attributes - """Stores the data for the problem.""" - - def __init__(self, args): - # Locations in block units - locations = [ - (4, 4), # depot - (2, 0), - (8, 0), # locations to visit - (0, 1), - (1, 1), - (5, 2), - (7, 2), - (3, 3), - (6, 3), - (5, 5), - (8, 5), - (1, 6), - (2, 6), - (3, 7), - (6, 7), - (0, 8), - (7, 8), - ] - # Convert locations in meters using a city block dimension of 114m x 80m. - self._locations = [(l[0] * 114, l[1] * 80) for l in locations] - self._distance_matrix = [ - [ - 0, - 548, - 776, - 696, - 582, - 274, - 502, - 194, - 308, - 194, - 536, - 502, - 388, - 354, - 468, - 776, - 662, - ], - [ - 548, - 0, - 684, - 308, - 194, - 502, - 730, - 354, - 696, - 742, - 1084, - 594, - 480, - 674, - 1016, - 868, - 1210, - ], - [ - 776, - 684, - 0, - 992, - 878, - 502, - 274, - 810, - 468, - 742, - 400, - 1278, - 1164, - 1130, - 788, - 1552, - 754, - ], - [ - 696, - 308, - 992, - 0, - 114, - 650, - 878, - 502, - 844, - 890, - 1232, - 514, - 628, - 822, - 1164, - 560, - 1358, - ], - [ - 582, - 194, - 878, - 114, - 0, - 536, - 764, - 388, - 730, - 776, - 1118, - 400, - 514, - 708, - 1050, - 674, - 1244, - ], - [ - 274, - 502, - 502, - 650, - 536, - 0, - 228, - 308, - 194, - 240, - 582, - 776, - 662, - 628, - 514, - 1050, - 708, - ], - [ - 502, - 730, - 274, - 878, - 764, - 228, - 0, - 536, - 194, - 468, - 354, - 1004, - 890, - 856, - 514, - 1278, - 480, - ], - [ - 194, - 354, - 810, - 502, - 388, - 308, - 536, - 0, - 342, - 388, - 730, - 468, - 354, - 320, - 662, - 742, - 856, - ], - [ - 308, - 696, - 468, - 844, - 730, - 194, - 194, - 342, - 0, - 274, - 388, - 810, - 696, - 662, - 320, - 1084, - 514, - ], - [ - 194, - 742, - 742, - 890, - 776, - 240, - 468, - 388, - 274, - 0, - 342, - 536, - 422, - 388, - 274, - 810, - 468, - ], - [ - 536, - 1084, - 400, - 1232, - 1118, - 582, - 354, - 730, - 388, - 342, - 0, - 878, - 764, - 730, - 388, - 1152, - 354, - ], - [ - 502, - 594, - 1278, - 514, - 400, - 776, - 1004, - 468, - 810, - 536, - 878, - 0, - 114, - 308, - 650, - 274, - 844, - ], - [ - 388, - 480, - 1164, - 628, - 514, - 662, - 890, - 354, - 696, - 422, - 764, - 114, - 0, - 194, - 536, - 388, - 730, - ], - [ - 354, - 674, - 1130, - 822, - 708, - 628, - 856, - 320, - 662, - 388, - 730, - 308, - 194, - 0, - 342, - 422, - 536, - ], - [ - 468, - 1016, - 788, - 1164, - 1050, - 514, - 514, - 662, - 320, - 274, - 388, - 650, - 536, - 342, - 0, - 764, - 194, - ], - [ - 776, - 868, - 1552, - 560, - 674, - 1050, - 1278, - 742, - 1084, - 810, - 1152, - 274, - 388, - 422, - 764, - 0, - 798, - ], - [ - 662, - 1210, - 754, - 1358, - 1244, - 708, - 480, - 856, - 514, - 468, - 354, - 844, - 730, - 536, - 194, - 798, - 0, - ], - ] - self._time_matrix = [ - [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], - [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], - [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], - [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], - [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], - [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], - [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], - [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], - [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], - [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], - [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], - [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], - [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], - [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], - [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], - [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], - [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], - ] - self._time_windows = [ - (0, 5), # depot - (7, 12), # 1 - (10, 15), # 2 - (5, 14), # 3 - (5, 13), # 4 - (0, 5), # 5 - (5, 10), # 6 - (0, 10), # 7 - (5, 10), # 8 - (0, 5), # 9 - (10, 16), # 10 - (10, 15), # 11 - (0, 5), # 12 - (5, 10), # 13 - (7, 12), # 14 - (10, 15), # 15 - (5, 15), # 16 - ] - if args["drop_nodes"]: - self._demands = [0, 1, 1, 3, 6, 3, 6, 8, 8, 1, 2, 1, 2, 6, 6, 8, 8] - else: - self._demands = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8] - self._pickups_deliveries = [ - [1, 6], - [2, 10], - [4, 3], - [5, 9], - [7, 8], - [15, 11], - [13, 12], - [16, 14], - ] - - if args["tsp"]: - self._num_vehicles = 1 - else: - self._num_vehicles = 4 - self._vehicle_capacities = [15, 15, 15, 15] - - if args["resources"]: - self._vehicle_load_time = 5 - self._vehicle_unload_time = 5 - - self._depot = 0 - self._depot_capacity = 2 - self._starts = [1, 2, 15, 16] - self._ends = [0, 0, 0, 0] - - @property - def locations(self): - """Gets the locations.""" - return self._locations - - @property - def distance_matrix(self): - """Gets the distance matrix.""" - return self._distance_matrix - - @property - def time_matrix(self): - """Gets the time matrix.""" - return self._time_matrix - - @property - def time_windows(self): - """Gets the time windows.""" - return self._time_windows - - @property - def demands(self): - """Gets the locations demands.""" - return self._demands - - @property - def pickups_deliveries(self): - """Gets the pickups deliveries.""" - return self._pickups_deliveries - - @property - def num_vehicles(self): - """Gets the number of vehicles.""" - return self._num_vehicles - - @property - def vehicle_capacities(self): - """Gets the capacity of each vehicles.""" - return self._vehicle_capacities - - @property - def vehicle_load_time(self): - """Gets the load time of each vehicles.""" - return self._vehicle_load_time - - @property - def vehicle_unload_time(self): - """Gets the unload time of each vehicles.""" - return self._vehicle_unload_time - - @property - def depot_capacity(self): - """Gets the depot capacity.""" - return self._depot_capacity - - @property - def depot(self): - """Gets the depot node index.""" - return self._depot - - @property - def starts(self): - """Gets the start nodes indices.""" - return self._starts - - @property - def ends(self): - """Gets the end nodes indices.""" - return self._ends - - # [END data_model] - - -########### -# Printer # -########### -class GoogleColorPalette(object): - """Google color codes palette.""" - - def __init__(self): - """Initialize Google ColorPalette.""" - self._colors = [ - ("blue", r"#4285F4"), - ("red", r"#EA4335"), - ("yellow", r"#FBBC05"), - ("green", r"#34A853"), - ("black", r"#101010"), - ("white", r"#FFFFFF"), - ] - - def __getitem__(self, key): - """Gets color name from idx.""" - return self._colors[key][0] - - def __len__(self): - """Gets the number of colors.""" - return len(self._colors) - - @property - def colors(self): - """Gets the colors list.""" - return self._colors - - def name(self, idx): - """Return color name from idx.""" - return self._colors[idx][0] - - def value(self, idx): - """Return color value from idx.""" - return self._colors[idx][1] - - def value_from_name(self, name): - """Return color value from name.""" - return dict(self._colors)[name] - - -class SVG(object): - """SVG draw primitives.""" - - @staticmethod - def header(size, margin): - """Writes header.""" - print( - r''.format( - width=size[0] + 2 * margin, height=size[1] + 2 * margin, margin=margin - ) - ) - - @staticmethod - def definitions(colors): - """Writes definitions.""" - print( - r"" - ) - print(r"") - for color in colors: - print( - r' '.format(colorname=color[0]) - ) - print( - r' '.format( - color=color[1] - ) - ) - print(r" ") - print(r"") - - @staticmethod - def footer(): - """Writes svg footer.""" - print(r"") - - @staticmethod - def draw_line(position_1, position_2, size, fg_color): - """Draws a line.""" - line_style = (r'style="stroke-width:{sz};stroke:{fg};fill:none"').format( - sz=size, fg=fg_color - ) - print( - r''.format( - x1=position_1[0], - y1=position_1[1], - x2=position_2[0], - y2=position_2[1], - style=line_style, - ) - ) - - @staticmethod - def draw_polyline(position_1, position_2, size, fg_color, colorname): - """Draws a line with arrow maker in the middle.""" - polyline_style = ( - r'style="stroke-width:{sz};stroke:{fg};fill:none;' - 'marker-mid:url(#arrow_{colorname})"' - ).format(sz=size, fg=fg_color, colorname=colorname) - print( - r''.format( - x1=position_1[0], - y1=position_1[1], - x2=(position_1[0] + position_2[0]) / 2, - y2=(position_1[1] + position_2[1]) / 2, - x3=position_2[0], - y3=position_2[1], - style=polyline_style, - ) - ) - - @staticmethod - def draw_circle(position, radius, size, fg_color, bg_color="white"): - """Print a circle.""" - circle_style = (r'style="stroke-width:{sz};stroke:{fg};fill:{bg}"').format( - sz=size, fg=fg_color, bg=bg_color - ) - print( - r''.format( - cx=position[0], cy=position[1], r=radius, style=circle_style - ) - ) - - @staticmethod - def draw_text(text, position, size, fg_color="none", bg_color="black"): - """Print a middle centred text.""" - text_style = ( - r'style="text-anchor:middle;font-weight:bold;' - 'font-size:{sz};stroke:{fg};fill:{bg}"' - ).format(sz=size, fg=fg_color, bg=bg_color) - print( - r'{txt}'.format( - x=position[0], y=position[1], dy=size / 3, style=text_style, txt=text - ) - ) - - -class SVGPrinter(object): # pylint: disable=too-many-instance-attributes - """Generate Problem as svg file to stdout.""" - - # pylint: disable=too-many-arguments - def __init__(self, args, data, manager=None, routing=None, assignment=None): - """Initializes the printer.""" - self._args = args - self._data = data - self._manager = manager - self._routing = routing - self._assignment = assignment - # Design variables - self._color_palette = GoogleColorPalette() - self._svg = SVG() - # City block size 114mx80m - self._radius = min(114, 80) / 3 - self._stroke_width = self._radius / 4 - - @property - def data(self): - """Gets the Data Model.""" - return self._data - - @property - def manager(self): - """Gets the RoutingIndexManager.""" - return self._manager - - @property - def routing(self): - """Gets the Routing solver.""" - return self._routing - - @property - def assignment(self): - """Gets the assignment.""" - return self._assignment - - @property - def color_palette(self): - """Gets the color palette.""" - return self._color_palette - - @property - def svg(self): - """Gets the svg.""" - return self._svg - - def draw_grid(self): - """Draws the city grid.""" - print(r"") - color = "#969696" - # Horizontal streets - for i in range(9): - p_1 = [0, i * 80] - p_2 = [8 * 114, p_1[1]] - self._svg.draw_line(p_1, p_2, 2, color) - # Vertical streets - for i in range(9): - p_1 = [i * 114, 0] - p_2 = [p_1[0], 8 * 80] - self._svg.draw_line(p_1, p_2, 2, color) - - def draw_depot(self): - """Draws the depot.""" - print(r"") - color = self._color_palette.value_from_name("black") - loc = self._data.locations[self._data.depot] - self._svg.draw_circle(loc, self._radius, self._stroke_width, color, "white") - self._svg.draw_text(self._data.depot, loc, self._radius, "none", color) - - def draw_depots(self): - """Draws the depot.""" - print(r"") - # print starts - for vehicle_idx, start in enumerate(self._data.starts): - del vehicle_idx - color = self._color_palette.value_from_name("black") - # color = self._color_palette.value(vehicle_idx) - loc = self._data.locations[start] - self._svg.draw_circle(loc, self._radius, self._stroke_width, color, "white") - self._svg.draw_text(start, loc, self._radius, "none", color) - # print end - color = self._color_palette.value_from_name("black") - loc = self._data.locations[0] - self._svg.draw_circle(loc, self._radius, self._stroke_width, color, "white") - self._svg.draw_text(0, loc, self._radius, "none", color) - - def draw_locations(self): - """Draws all the locations but the depot.""" - print(r"") - color = self._color_palette.value_from_name("blue") - if not self._args["starts_ends"]: - for idx, loc in enumerate(self._data.locations): - if idx == self._data.depot: - continue - self._svg.draw_circle( - loc, self._radius, self._stroke_width, color, "white" - ) - self._svg.draw_text(idx, loc, self._radius, "none", color) - else: - for idx, loc in enumerate(self._data.locations): - if idx in self._data.starts + self._data.ends: - continue - self._svg.draw_circle( - loc, self._radius, self._stroke_width, color, "white" - ) - self._svg.draw_text(idx, loc, self._radius, "none", color) - - def draw_demands(self): - """Draws all the demands.""" - print(r"") - for idx, loc in enumerate(self._data.locations): - if idx == self._data.depot: - continue - demand = self._data.demands[idx] - position = [ - x + y for x, y in zip(loc, [self._radius * 1.2, self._radius * 1.1]) - ] - color = self._color_palette.value_from_name("red") - # color = self._color_palette.value(int(math.log(demand, 2))) - self._svg.draw_text(demand, position, self._radius, "none", color) - - def draw_pickups_deliveries(self): - """Draws all pickups deliveries.""" - print(r"") - colorname = "red" - color = self._color_palette.value_from_name(colorname) - for pickup_delivery in self._data.pickups_deliveries: - self._svg.draw_polyline( - self._data.locations[pickup_delivery[0]], - self._data.locations[pickup_delivery[1]], - self._stroke_width, - color, - colorname, - ) - - def draw_time_windows(self): - """Draws all the time windows.""" - print(r"") - for idx, loc in enumerate(self._data.locations): - if idx == self._data.depot: - continue - time_window = self._data.time_windows[idx] - position = [ - x + y for x, y in zip(loc, [self._radius * 0, -self._radius * 1.6]) - ] - color = self._color_palette.value_from_name("red") - self._svg.draw_text( - "[{t1},{t2}]".format(t1=time_window[0], t2=time_window[1]), - position, - self._radius * 0.75, - "white", - color, - ) - - ############## - ## ROUTES ## - ############## - - def draw_drop_nodes(self): - """Draws the dropped nodes.""" - print(r"") - if self._assignment is None: - print("") - # Display dropped nodes. - dropped_nodes = [] - for node in range(self._routing.Size()): - if self._routing.IsStart(node) or self._routing.IsEnd(node): - continue - if self._assignment.Value(self._routing.NextVar(node)) == node: - dropped_nodes.append(self._manager.IndexToNode(node)) - color = self._color_palette.value_from_name("black") - for node_idx in dropped_nodes: - loc = self._data.locations[node_idx] - self._svg.draw_circle(loc, self._radius, self._stroke_width, color, "white") - self._svg.draw_text(node_idx, loc, self._radius, "none", color) - - def routes(self): - """Creates the route list from the assignment.""" - if self._assignment is None: - print("") - return [] - routes = [] - for vehicle_id in range(self._data.num_vehicles): - index = self._routing.Start(vehicle_id) - route = [] - while not self._routing.IsEnd(index): - node_index = self._manager.IndexToNode(index) - route.append(node_index) - index = self._assignment.Value(self._routing.NextVar(index)) - node_index = self._manager.IndexToNode(index) - route.append(node_index) - routes.append(route) - return routes - - def draw_route(self, route, color, colorname): - """Draws a Route.""" - # First print route - previous_loc_idx = None - for loc_idx in route: - if previous_loc_idx is not None and previous_loc_idx != loc_idx: - self._svg.draw_polyline( - self._data.locations[previous_loc_idx], - self._data.locations[loc_idx], - self._stroke_width, - color, - colorname, - ) - previous_loc_idx = loc_idx - # Then print location along the route - for loc_idx in route: - if loc_idx != self._data.depot: - loc = self._data.locations[loc_idx] - self._svg.draw_circle( - loc, self._radius, self._stroke_width, color, "white" - ) - self._svg.draw_text(loc_idx, loc, self._radius, "none", color) - - def draw_routes(self): - """Draws the routes.""" - print(r"") - for route_idx, route in enumerate(self.routes()): - print(r"".format(idx=route_idx)) - color = self._color_palette.value(route_idx) - colorname = self._color_palette.name(route_idx) - self.draw_route(route, color, colorname) - - def tw_routes(self): - """Creates the route time window list from the assignment.""" - if self._assignment is None: - print("") - return [] - time_dimension = self._routing.GetDimensionOrDie("Time") - loc_routes = [] - tw_routes = [] - for vehicle_id in range(self._data.num_vehicles): - index = self._routing.Start(vehicle_id) - # index = self._assignment.Value(self._routing.NextVar(index)) - loc_route = [] - tw_route = [] - while True: - node_index = self._manager.IndexToNode(index) - loc_route.append(node_index) - time_var = time_dimension.CumulVar(index) - t_min = self._assignment.Min(time_var) - t_max = self._assignment.Max(time_var) - tw_route.append((t_min, t_max)) - if self._routing.IsEnd(index): - break - index = self._assignment.Value(self._routing.NextVar(index)) - loc_routes.append(loc_route) - tw_routes.append(tw_route) - return zip(loc_routes, tw_routes) - - def draw_tw_route(self, route_idx, locations, tw_route, color): - """Draws the time windows for a Route.""" - is_start = -1 - for loc_idx, time_window in zip(locations, tw_route): - loc = self._data.locations[loc_idx] - if loc_idx == 0: # special case for depot - position = [ - x + y - for x, y in zip( - loc, [self._radius * is_start, self._radius * (1.8 + route_idx)] - ) - ] - is_start = 1 - else: - position = [ - x + y for x, y in zip(loc, [self._radius * 0, self._radius * 1.8]) - ] - self._svg.draw_text( - "[{t_min}]".format(t_min=time_window[0]), - position, - self._radius * 0.75, - "white", - color, - ) - - def draw_tw_routes(self): - """Draws the time window routes.""" - print(r"") - for route_idx, loc_tw in enumerate(self.tw_routes()): - print(r"".format(route_idx)) - color = self._color_palette.value(route_idx) - self.draw_tw_route(route_idx, loc_tw[0], loc_tw[1], color) - - def print_to_console(self): - """Prints a full svg document on stdout.""" - margin = self._radius * 2 + 2 - size = [8 * 114, 8 * 80] - self._svg.header(size, margin) - self._svg.definitions(self._color_palette.colors) - self.draw_grid() - if not self._args["solution"]: - if self._args["pickup_delivery"]: - self.draw_pickups_deliveries() - self.draw_locations() - else: - self.draw_routes() - self.draw_drop_nodes() - if self._args["starts_ends"]: - self.draw_depots() - else: - self.draw_depot() - if self._args["capacity"]: - self.draw_demands() - if self._args["drop_nodes"]: - self.draw_demands() - if self._args["time_windows"] or self._args["resources"]: - self.draw_time_windows() - if (self._args["time_windows"] or self._args["resources"]) and self._args[ - "solution" - ]: - self.draw_tw_routes() - self._svg.footer() - - -######## -# Main # -######## -def main(): # pylint: disable=too-many-locals,too-many-branches - """Entry point of the program.""" - parser = argparse.ArgumentParser(description="Output VRP as svg image.") - parser.add_argument("-tsp", "--tsp", action="store_true", help="use 1 vehicle") - parser.add_argument("-vrp", "--vrp", action="store_true", help="use 4 vehicle") - parser.add_argument( - "-gs", "--global-span", action="store_true", help="use global span constraints" - ) - parser.add_argument( - "-c", "--capacity", action="store_true", help="use capacity constraints" - ) - parser.add_argument( - "-r", "--resources", action="store_true", help="use resources constraints" - ) - parser.add_argument( - "-dn", - "--drop-nodes", - action="store_true", - help="allow drop nodes (disjuntion constraints)", - ) - parser.add_argument( - "-tw", "--time-windows", action="store_true", help="use time-window constraints" - ) - parser.add_argument( - "-se", "--starts-ends", action="store_true", help="use multiple starts & ends" - ) - parser.add_argument( - "-pd", - "--pickup-delivery", - action="store_true", - help="use pickup & delivery constraints", - ) - parser.add_argument( - "-fifo", "--fifo", action="store_true", help="use pickup & delivery FIFO Policy" - ) - parser.add_argument( - "-lifo", "--lifo", action="store_true", help="use pickup & delivery LIFO Policy" - ) - parser.add_argument("-s", "--solution", action="store_true", help="print solution") - args = vars(parser.parse_args()) - - # Instantiate the data problem. - # [START data] - data = DataModel(args) - # [END data] - - if not args["solution"]: - # Print svg on cout - printer = SVGPrinter(args, data) - printer.print_to_console() - return 0 - - # Create the routing index manager. - # [START index_manager] - if args["starts_ends"]: - manager = pywrapcp.RoutingIndexManager( - len(data.locations), data.num_vehicles, data.starts, data.ends - ) - else: - manager = pywrapcp.RoutingIndexManager( - len(data.locations), data.num_vehicles, data.depot - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - - # [END routing_model] - - # Register distance callback - def distance_callback(from_index, to_index): - """Returns the manhattan distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data.distance_matrix[from_node][to_node] - - distance_callback_index = routing.RegisterTransitCallback(distance_callback) - - # Register time callback - def time_callback(from_index, to_index): - """Returns the manhattan distance travel time between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data.time_matrix[from_node][to_node] - - time_callback_index = routing.RegisterTransitCallback(time_callback) - - # Register demands callback - def demand_callback(from_index): - """Returns the demand of the node.""" - # Convert from routing variable Index to demands NodeIndex. - from_node = manager.IndexToNode(from_index) - return data.demands[from_node] - - demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback) - - if args["time_windows"] or args["resources"]: - routing.SetArcCostEvaluatorOfAllVehicles(time_callback_index) - else: - routing.SetArcCostEvaluatorOfAllVehicles(distance_callback_index) - - if args["global_span"] or args["pickup_delivery"]: - dimension_name = "Distance" - routing.AddDimension(distance_callback_index, 0, 3000, True, dimension_name) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - - if args["capacity"] or args["drop_nodes"]: - routing.AddDimensionWithVehicleCapacity( - demand_callback_index, 0, data.vehicle_capacities, True, "Capacity" - ) - - if args["drop_nodes"]: - # Allow to drop nodes. - penalty = 1000 - for node in range(1, len(data.locations)): - routing.AddDisjunction([manager.NodeToIndex(node)], penalty) - - if args["pickup_delivery"]: - dimension_name = "Distance" - routing.AddDimension(distance_callback_index, 0, 3000, True, dimension_name) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - for request in data.pickups_deliveries: - pickup_index = manager.NodeToIndex(request[0]) - delivery_index = manager.NodeToIndex(request[1]) - routing.AddPickupAndDelivery(pickup_index, delivery_index) - routing.solver().Add( - routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index) - ) - routing.solver().Add( - distance_dimension.CumulVar(pickup_index) - <= distance_dimension.CumulVar(delivery_index) - ) - if args["fifo"]: - routing.SetPickupAndDeliveryPolicyOfAllVehicles( - pywrapcp.RoutingModel.PICKUP_AND_DELIVERY_FIFO - ) - if args["lifo"]: - routing.SetPickupAndDeliveryPolicyOfAllVehicles( - pywrapcp.RoutingModel.PICKUP_AND_DELIVERY_LIFO - ) - - if args["starts_ends"]: - dimension_name = "Distance" - routing.AddDimension(distance_callback_index, 0, 2000, True, dimension_name) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - - time = "Time" - if args["time_windows"] or args["resources"]: - routing.AddDimension(time_callback_index, 30, 30, False, time) - time_dimension = routing.GetDimensionOrDie(time) - # Add time window constraints for each location except depot and 'copy' the - # slack var in the solution object (aka Assignment) to print it. - for location_idx, time_window in enumerate(data.time_windows): - if location_idx == 0: - continue - index = manager.NodeToIndex(location_idx) - time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) - routing.AddToAssignment(time_dimension.SlackVar(index)) - # Add time window constraints for each vehicle start node and 'copy' the - # slack var in the solution object (aka Assignment) to print it. - for vehicle_id in range(data.num_vehicles): - index = routing.Start(vehicle_id) - time_window = data.time_windows[0] - time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) - routing.AddToAssignment(time_dimension.SlackVar(index)) - - # Instantiate route start and end times to produce feasible times. - for vehicle_id in range(data.num_vehicles): - routing.AddVariableMinimizedByFinalizer( - time_dimension.CumulVar(routing.End(vehicle_id)) - ) - routing.AddVariableMinimizedByFinalizer( - time_dimension.CumulVar(routing.Start(vehicle_id)) - ) - - if args["resources"]: - # Add resource constraints at the depot. - time_dimension = routing.GetDimensionOrDie(time) - solver = routing.solver() - intervals = [] - for i in range(data.num_vehicles): - # Add loading time at start of routes - intervals.append( - solver.FixedDurationIntervalVar( - time_dimension.CumulVar(routing.Start(i)), - data.vehicle_load_time, - "depot_interval", - ) - ) - # Add unloading time at end of routes. - intervals.append( - solver.FixedDurationIntervalVar( - time_dimension.CumulVar(routing.End(i)), - data.vehicle_unload_time, - "depot_interval ", - ) - ) - - depot_usage = [1 for i in range(data.num_vehicles * 2)] - solver.AddConstraint( - solver.Cumulative(intervals, depot_usage, data.depot_capacity, "depot") - ) - - # Setting first solution heuristic (cheapest addition). - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - # pylint: disable=no-member - if not args["pickup_delivery"]: - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - else: - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION - ) - - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.time_limit.FromSeconds(2) - - # Solve the problem. - assignment = routing.SolveWithParameters(search_parameters) - # Print the solution. - printer = SVGPrinter(args, data, manager, routing, assignment) - printer.print_to_console() - return 0 - - -if __name__ == "__main__": - main() diff --git a/ortools/constraint_solver/java/CMakeLists.txt b/ortools/constraint_solver/java/CMakeLists.txt index cd894902a16..e1116674378 100644 --- a/ortools/constraint_solver/java/CMakeLists.txt +++ b/ortools/constraint_solver/java/CMakeLists.txt @@ -11,17 +11,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -set_property(SOURCE routing.i PROPERTY CPLUSPLUS ON) -set_property(SOURCE routing.i PROPERTY SWIG_MODULE_NAME main) -set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS +set_property(SOURCE constraint_solver.i PROPERTY CPLUSPLUS ON) +set_property(SOURCE constraint_solver.i PROPERTY SWIG_MODULE_NAME Globals) +set_property(SOURCE constraint_solver.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) -set_property(SOURCE routing.i PROPERTY COMPILE_OPTIONS +set_property(SOURCE constraint_solver.i PROPERTY COMPILE_OPTIONS -package ${JAVA_PACKAGE}.constraintsolver) swig_add_library(jniconstraint_solver TYPE OBJECT LANGUAGE java OUTPUT_DIR ${JAVA_PROJECT_DIR}/${JAVA_SRC_PATH}/constraintsolver - SOURCES routing.i) + SOURCES constraint_solver.i) target_include_directories(jniconstraint_solver PRIVATE ${JNI_INCLUDE_DIRS}) set_target_properties(jniconstraint_solver PROPERTIES diff --git a/ortools/constraint_solver/java/constraint_solver.i b/ortools/constraint_solver/java/constraint_solver.i index 8c0d82e7ceb..88f8540460d 100644 --- a/ortools/constraint_solver/java/constraint_solver.i +++ b/ortools/constraint_solver/java/constraint_solver.i @@ -14,7 +14,7 @@ // TODO(user): Refactor this file to adhere to the SWIG style guide. // Used for free functions. -%module(directors="1") operations_research; +%module(directors="1") Globals; %include "enumsimple.swg" %include "exception.i" @@ -1558,7 +1558,7 @@ CONVERT_VECTOR(operations_research::SymmetryBreaker, SymmetryBreaker); %rename (toString) *::DebugString; %rename("%(lowercamelcase)s", %$isvariable) ""; -// Add needed import to mainJNI.java +// Add needed import to GlobalsJNI.java %pragma(java) jniclassimports=%{ // Used to wrap std::function // see https://docs.oracle.com/javase/8/docs/api/java/util/function/Supplier.html @@ -1622,14 +1622,14 @@ PROTO_INPUT(operations_research::RegularLimitParameters, PROTO2_RETURN(operations_research::RegularLimitParameters, com.google.ortools.constraintsolver.RegularLimitParameters) -// Add needed import to main.java +// Add needed import to Globals.java %pragma(java) moduleimports=%{ %} namespace operations_research { // Globals -// IMPORTANT(user): Globals will be placed in main.java -// i.e. use `import com.[...].constraintsolver.main` +// IMPORTANT(user): Globals will be placed in Globals.java +// i.e. use `import com.[...].constraintsolver.Globals` %ignore FillValues; %rename (areAllBooleans) AreAllBooleans; %rename (areAllBound) AreAllBound; diff --git a/ortools/constraint_solver/local_search.cc b/ortools/constraint_solver/local_search.cc index 92d4cbca9df..2a0f237af5c 100644 --- a/ortools/constraint_solver/local_search.cc +++ b/ortools/constraint_solver/local_search.cc @@ -63,8 +63,8 @@ namespace operations_research { // Utility methods to ensure the communication between local search and the // search. -// Returns true if a local optimum has been reached and cannot be improved. -bool LocalOptimumReached(Search* search); +// Returns true if the search must continue after reaching the local optimum. +bool ContinueAtLocalOptimum(Search* search); // Returns true if the search accepts the delta (actually checking this by // calling AcceptDelta on the monitors of the search). @@ -4130,7 +4130,9 @@ Decision* FindOneNeighbor::Next(Solver* const solver) { if (solutions_since_last_check_ >= check_period_) { solutions_since_last_check_ = 0; } - const bool accept = !check_solution || solver->SolveAndCommit(restore); + const bool accept = !check_solution || + (solver->SolveAndCommit(restore) && + solver->AcceptSolution(solver->TopLevelSearch())); solver->GetLocalSearchMonitor()->EndAcceptNeighbor(ls_operator_, accept); if (accept) { @@ -4392,7 +4394,7 @@ class NestedSolveDecision : public Decision { private: DecisionBuilder* const db_; - bool restore_; + const bool restore_; std::vector monitors_; int state_; }; @@ -4647,15 +4649,21 @@ Decision* LocalSearch::Next(Solver* const solver) { const int state = decision->state(); switch (state) { case NestedSolveDecision::DECISION_FAILED: { - const bool local_optimum_reached = - LocalOptimumReached(solver->ActiveSearch()); - if (local_optimum_reached) { + // NOTE: The DECISION_FAILED state can be reached when no first solution + // was found by the solver, so we should only consider to be at a local + // optimum and call ContinueAtLocalOptimum() when we've reached the last + // nested decision. + const bool continue_at_local_optimum = + nested_decision_index_ == nested_decisions_.size() - 1 && + ContinueAtLocalOptimum(solver->ActiveSearch()); + if (continue_at_local_optimum) { // A local optimum has been reached. The search will continue only if we // accept up-hill moves (due to metaheuristics). In this case we need to // reset neighborhood optimal routes. ls_operator_->Reset(); } - if (!local_optimum_reached || solver->IsUncheckedSolutionLimitReached()) { + if (!continue_at_local_optimum || + solver->IsUncheckedSolutionLimitReached()) { nested_decision_index_ = -1; // Stop the search } solver->Fail(); diff --git a/ortools/constraint_solver/python/CMakeLists.txt b/ortools/constraint_solver/python/CMakeLists.txt index 702943a30e0..8fe4444aa78 100644 --- a/ortools/constraint_solver/python/CMakeLists.txt +++ b/ortools/constraint_solver/python/CMakeLists.txt @@ -11,16 +11,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -set_property(SOURCE routing.i PROPERTY CPLUSPLUS ON) -set_property(SOURCE routing.i PROPERTY SWIG_MODULE_NAME pywrapcp) -set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS +# constraint_solver +pybind11_add_module(constraint_solver_pybind11 MODULE constraint_solver.cc) +set_target_properties(constraint_solver_pybind11 PROPERTIES + LIBRARY_OUTPUT_NAME "constraint_solver") + +# note: macOS is APPLE and also UNIX ! +if(APPLE) + set_target_properties(constraint_solver_pybind11 PROPERTIES + SUFFIX ".so" + INSTALL_RPATH "@loader_path;@loader_path/../../../${PYTHON_PROJECT}/.libs") +elseif(UNIX) + set_target_properties(constraint_solver_pybind11 PROPERTIES + INSTALL_RPATH "$ORIGIN:$ORIGIN/../../../${PYTHON_PROJECT}/.libs") +endif() + +target_link_libraries(constraint_solver_pybind11 PRIVATE + ${PROJECT_NAMESPACE}::ortools + pybind11_native_proto_caster) +add_library(${PROJECT_NAMESPACE}::constraint_solver_pybind11 ALIAS constraint_solver_pybind11) + +# legacy pywrapcp +set_property(SOURCE constraint_solver.i PROPERTY CPLUSPLUS ON) +set_property(SOURCE constraint_solver.i PROPERTY SWIG_MODULE_NAME pywrapcp) +set_property(SOURCE constraint_solver.i PROPERTY COMPILE_DEFINITIONS ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) -set_property(SOURCE routing.i PROPERTY COMPILE_OPTIONS -nofastunpack) +set_property(SOURCE constraint_solver.i PROPERTY COMPILE_OPTIONS -nofastunpack) swig_add_library(pywrapcp TYPE MODULE LANGUAGE python OUTPUT_DIR ${PYTHON_PROJECT_DIR}/constraint_solver - SOURCES routing.i) + SOURCES constraint_solver.i) target_include_directories(pywrapcp PRIVATE ${Python3_INCLUDE_DIRS}) set_property(TARGET pywrapcp PROPERTY SWIG_USE_TARGET_INCLUDE_DIRECTORIES ON) diff --git a/ortools/constraint_solver/python/constraint_solver.cc b/ortools/constraint_solver/python/constraint_solver.cc new file mode 100644 index 00000000000..c771e67b18f --- /dev/null +++ b/ortools/constraint_solver/python/constraint_solver.cc @@ -0,0 +1,334 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/constraint_solver/constraint_solver.h" + +#include // For FailureProtect. See below. + +#include +#include +#include +#include + +#include "absl/strings/string_view.h" +#include "ortools/constraint_solver/assignment.pb.h" +#include "ortools/constraint_solver/python/constraint_solver_doc.h" +#include "pybind11/cast.h" +#include "pybind11/functional.h" +#include "pybind11/gil.h" +#include "pybind11/pybind11.h" +#include "pybind11/stl.h" +#include "pybind11_protobuf/native_proto_caster.h" + +using ::operations_research::Assignment; +using ::operations_research::AssignmentProto; +using ::operations_research::BaseObject; +using ::operations_research::Constraint; +using ::operations_research::ConstraintSolverParameters; +using ::operations_research::DecisionBuilder; +using ::operations_research::IntervalVar; +using ::operations_research::IntExpr; +using ::operations_research::IntVar; +using ::operations_research::ModelVisitor; +using ::operations_research::PropagationBaseObject; +using ::operations_research::Solver; +using ::pybind11::arg; + +// Used in the PROTECT_FROM_FAILURE macro. See below. +namespace { + +struct FailureProtect { + jmp_buf exception_buffer; + void JumpBack() { longjmp(exception_buffer, 1); } +}; + +} // namespace + +#define PROTECT_FROM_FAILURE(this_, action) \ + Solver* solver = this_->solver(); \ + FailureProtect protect; \ + solver->set_fail_intercept([&protect]() { protect.JumpBack(); }); \ + if (setjmp(protect.exception_buffer) == 0) { \ + this_->action; \ + solver->clear_fail_intercept(); \ + } else { \ + solver->clear_fail_intercept(); \ + throw pybind11::value_error("Solver fails outside of solve()"); \ + } + +class BaseObjectPythonHelper { + public: + static std::string DebugString(BaseObject* this_) { + return this_->DebugString(); + } +}; + +class PropagationBaseObjectPythonHelper : BaseObjectPythonHelper { + public: + static std::string DebugString(PropagationBaseObject* this_) { + return this_->DebugString(); + } + static Solver* solver(PropagationBaseObject* this_) { + return this_->solver(); + } + + static std::string name(PropagationBaseObject* this_) { + return this_->name(); + } + + static void SetName(PropagationBaseObject* this_, absl::string_view name) { + this_->set_name(name); + } +}; + +class IntExprPythonHelper : PropagationBaseObjectPythonHelper { + public: + static int64_t Min(IntExpr* this_) { return this_->Min(); } + static int64_t Max(IntExpr* this_) { return this_->Max(); } + static void SetMin(IntExpr* this_, int64_t m) { + PROTECT_FROM_FAILURE(this_, SetMin(m)); + } + static void SetMax(IntExpr* this_, int64_t m) { + PROTECT_FROM_FAILURE(this_, SetMax(m)); + } + static void SetRange(IntExpr* this_, int64_t mi, int64_t ma) { + PROTECT_FROM_FAILURE(this_, SetRange(mi, ma)); + } + static void SetValue(IntExpr* this_, int64_t v) { + PROTECT_FROM_FAILURE(this_, SetValue(v)); + } + static bool Bound(IntExpr* this_) { return this_->Bound(); } +}; + +class IntVarPythonHelper : IntExprPythonHelper { + public: + static std::string name(IntVar* this_) { return this_->name(); } + static int64_t Value(IntVar* this_) { return this_->Value(); } + static void RemoveValue(IntVar* this_, int64_t v) { + PROTECT_FROM_FAILURE(this_, RemoveValue(v)); + } + static int64_t Size(IntVar* this_) { return this_->Size(); } +}; + +PYBIND11_MODULE(constraint_solver, m) { + pybind11_protobuf::ImportNativeProtoCasters(); + + pybind11::class_(m, "Solver", DOC(operations_research, Solver)) + .def(pybind11::init()) + .def(pybind11::init()) + .def("__str__", &Solver::DebugString) + .def("default_solver_parameters", &Solver::DefaultSolverParameters) + .def("parameters", &Solver::parameters) + .def("local_search_profile", &Solver::LocalSearchProfile) + .def("new_int_var", + pybind11::overload_cast( + &Solver::MakeIntVar), + DOC(operations_research, Solver, MakeIntVar), + pybind11::return_value_policy::reference_internal) + .def("new_int_var", + pybind11::overload_cast(&Solver::MakeIntVar), + DOC(operations_research, Solver, MakeIntVar), + pybind11::return_value_policy::reference_internal) + .def("new_int_var", + pybind11::overload_cast&, + const std::string&>(&Solver::MakeIntVar), + DOC(operations_research, Solver, MakeIntVar_2), + pybind11::return_value_policy::reference_internal) + .def("new_int_var", + pybind11::overload_cast&>( + &Solver::MakeIntVar), + DOC(operations_research, Solver, MakeIntVar_2), + pybind11::return_value_policy::reference_internal) + .def("add", &Solver::AddConstraint, + DOC(operations_research, Solver, AddConstraint), arg("c")) + .def("accept", &Solver::Accept, DOC(operations_research, Solver, Accept), + arg("visitor")) + .def("print_model_visitor", &Solver::MakePrintModelVisitor, + DOC(operations_research, Solver, MakePrintModelVisitor), + pybind11::return_value_policy::reference_internal); + + pybind11::class_(m, "BaseObject", + DOC(operations_research, BaseObject)) + .def("__str__", &BaseObjectPythonHelper::DebugString); + + pybind11::class_( + m, "PropagationBaseObject", + DOC(operations_research, PropagationBaseObject)) + .def_property("name", &PropagationBaseObjectPythonHelper::name, + &PropagationBaseObjectPythonHelper::SetName); + + // Note: no ctor. + pybind11::class_( + m, "IntExpr", DOC(operations_research, IntExpr)) + .def_property_readonly("min", &IntExprPythonHelper::Min, + DOC(operations_research, IntExpr, Min)) + .def_property_readonly("max", &IntExprPythonHelper::Max, + DOC(operations_research, IntExpr, Max)) + .def("set_min", &IntExprPythonHelper::SetMin, + DOC(operations_research, IntExpr, SetMin), arg("m")) + .def("set_max", &IntExprPythonHelper::SetMax, + DOC(operations_research, IntExpr, SetMax), arg("m")) + .def("set_range", &IntExprPythonHelper::SetRange, + DOC(operations_research, IntExpr, SetRange), arg("mi"), arg("ma")) + .def("set_value", &IntExprPythonHelper::SetValue, + DOC(operations_research, IntExpr, SetValue), arg("v")) + .def("bound", &IntExprPythonHelper::Bound, + DOC(operations_research, IntExpr, Bound)) + .def( + "__add__", + [](IntExpr* e, int64_t arg) { return e->solver()->MakeSum(e, arg); }, + pybind11::return_value_policy::reference_internal) + .def( + "__add__", + [](IntExpr* e, IntExpr* arg) { return e->solver()->MakeSum(e, arg); }, + pybind11::return_value_policy::reference_internal) + .def( + "__radd__", + [](IntExpr* e, int64_t arg) { return e->solver()->MakeSum(e, arg); }, + pybind11::return_value_policy::reference_internal) + .def( + "__radd__", + [](IntExpr* e, IntExpr* arg) { return e->solver()->MakeSum(e, arg); }, + pybind11::return_value_policy::reference_internal) + .def( + "__mul__", + [](IntExpr* e, int64_t arg) { return e->solver()->MakeProd(e, arg); }, + pybind11::return_value_policy::reference_internal) + .def( + "__mul__", + [](IntExpr* e, IntExpr* arg) { + return e->solver()->MakeProd(e, arg); + }, + pybind11::return_value_policy::reference_internal) + .def( + "__rmul__", + [](IntExpr* e, int64_t arg) { return e->solver()->MakeProd(e, arg); }, + pybind11::return_value_policy::reference_internal) + .def( + "__rmul__", + [](IntExpr* e, IntExpr* arg) { + return e->solver()->MakeProd(e, arg); + }, + pybind11::return_value_policy::reference_internal) + .def( + "__eq__", + [](IntExpr* left, IntExpr* right) { + return left->solver()->MakeEquality(left, right); + }, + pybind11::return_value_policy::reference_internal) + .def( + "__eq__", + [](IntExpr* left, int64_t right) { + return left->solver()->MakeEquality(left, right); + }, + pybind11::return_value_policy::reference_internal); + + // Note: no ctor. + pybind11::class_(m, "IntVar", + DOC(operations_research, IntVar)) + .def("value", &IntVarPythonHelper::Value, + DOC(operations_research, IntVar, Value)) + .def("remove_value", &IntVarPythonHelper::RemoveValue, + DOC(operations_research, IntVar, RemoveValue), arg("v")) + .def("size", &IntVarPythonHelper::Size, + DOC(operations_research, IntVar, Size)); + + // Note: no ctor. + pybind11::class_(m, "Constraint", + DOC(operations_research, Constraint)) + .def("var", &Constraint::Var, DOC(operations_research, Constraint, Var)); + + // Note: no ctor. + pybind11::class_( + m, "DecisionBuilder", DOC(operations_research, DecisionBuilder)) + .def_property("name", &DecisionBuilder::GetName, + &DecisionBuilder::set_name); + + // Note: no ctor. + pybind11::class_( + m, "ModelVisitor", DOC(operations_research, ModelVisitor)); + + pybind11::class_( + m, "Assignment", DOC(operations_research, Assignment)) + .def(pybind11::init()) + .def("clear", &Assignment::Clear) + .def("empty", &Assignment::Empty) + .def("size", &Assignment::Size) + .def("num_int_vars", &Assignment::NumIntVars) + .def("num_interval_vars", &Assignment::NumIntervalVars) + .def("num_sequence_vars", &Assignment::NumSequenceVars) + .def("store", &Assignment::Store) + .def("restore", &Assignment::Restore) + .def("load", + pybind11::overload_cast(&Assignment::Load), + arg("filename")) + .def("load", + pybind11::overload_cast(&Assignment::Load), + arg("assignment_proto")) + .def("add_objective", &Assignment::AddObjective, arg("v")) + .def("add_objectives", &Assignment::AddObjectives, arg("vars")) + .def("clear_objective", &Assignment::ClearObjective) + .def("num_objectives", &Assignment::NumObjectives) + .def("objective", &Assignment::Objective) + .def("objective_from_index", &Assignment::ObjectiveFromIndex, + arg("index")) + .def("has_objective", &Assignment::HasObjective) + .def("has_objective_from_index", &Assignment::HasObjectiveFromIndex, + arg("index")) + .def("objective_min", &Assignment::ObjectiveMin) + .def("objective_max", &Assignment::ObjectiveMax) + .def("objective_value", &Assignment::ObjectiveValue) + .def("objective_bound", &Assignment::ObjectiveBound) + .def("set_objective_min", &Assignment::SetObjectiveMin, arg("m")) + .def("set_objective_max", &Assignment::SetObjectiveMax, arg("m")) + .def("set_objective_value", &Assignment::SetObjectiveValue, arg("value")) + .def("set_objective_range", &Assignment::SetObjectiveRange, arg("l"), + arg("u")) + .def("objective_min_from_index", &Assignment::ObjectiveMinFromIndex, + arg("index")) + .def("objective_max_from_index", &Assignment::ObjectiveMaxFromIndex, + arg("index")) + .def("objective_value_from_index", &Assignment::ObjectiveValueFromIndex, + arg("index")) + .def("objective_bound_from_index", &Assignment::ObjectiveBoundFromIndex, + arg("index")) + .def("set_objective_min_from_index", + &Assignment::SetObjectiveMinFromIndex, arg("index"), arg("m")) + .def("set_objective_max_from_index", + &Assignment::SetObjectiveMaxFromIndex, arg("index"), arg("m")) + .def("set_objective_range_from_index", + &Assignment::SetObjectiveRangeFromIndex, arg("index"), arg("l"), + arg("u")) + .def("add", pybind11::overload_cast(&Assignment::Add), + arg("var")) + .def("add", + pybind11::overload_cast&>( + &Assignment::Add), + arg("var")) + .def("min", &Assignment::Min, arg("var")) + .def("max", &Assignment::Max, arg("var")) + .def("value", &Assignment::Value, arg("var")) + .def("bound", &Assignment::Bound, arg("var")) + .def("set_min", &Assignment::SetMin, arg("var"), arg("m")) + .def("set_max", &Assignment::SetMax, arg("var"), arg("m")) + .def("set_range", &Assignment::SetRange, arg("var"), arg("l"), arg("u")) + .def("set_value", &Assignment::SetValue, arg("var"), arg("value")) + .def("add", pybind11::overload_cast(&Assignment::Add), + arg("var")) + .def("add", + pybind11::overload_cast&>( + &Assignment::Add), + arg("var")); + // missing IntervalVar, SequenceVar, active/deactivate, contains, copy +} diff --git a/ortools/constraint_solver/python/constraint_solver.i b/ortools/constraint_solver/python/constraint_solver.i index 1e2ce4a2b40..498e4af1466 100644 --- a/ortools/constraint_solver/python/constraint_solver.i +++ b/ortools/constraint_solver/python/constraint_solver.i @@ -21,15 +21,15 @@ // // USAGE EXAMPLES (most of which are also unit tests): // - ./pywrapcp_test.py -// - ortools/python/appointments.py -// - ortools/python/golomb8.py -// - ortools/python/hidato_table.py -// - ortools/python/jobshop_ft06.py -// - ortools/python/magic_sequence_distribute.py -// - ortools/python/rabbit_pheasant.py -// - ortools/python/simple_meeting.py -// - ortools/python/sudoku.py -// - ortools/python/zebra.py +// - examples/python/appointments.py +// - examples/python/golomb8.py +// - examples/python/hidato_table.py +// - examples/python/jobshop_ft06.py +// - examples/python/magic_sequence_distribute.py +// - examples/python/rabbit_pheasant.py +// - examples/python/simple_meeting.py +// - examples/python/sudoku.py +// - examples/python/zebra.py %include "ortools/base/base.i" %include "ortools/util/python/proto.i" diff --git a/ortools/constraint_solver/python/constraint_solver_doc.h b/ortools/constraint_solver/python/constraint_solver_doc.h new file mode 100644 index 00000000000..c8dff6346f2 --- /dev/null +++ b/ortools/constraint_solver/python/constraint_solver_doc.h @@ -0,0 +1,6158 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + This file contains docstrings for use in the Python bindings. + Do not edit! They were automatically extracted by pybind11_mkdoc. + */ + +#define __EXPAND(x) x +#define __COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT +#define __VA_SIZE(...) __EXPAND(__COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1)) +#define __CAT1(a, b) a##b +#define __CAT2(a, b) __CAT1(a, b) +#define __DOC1(n1) __doc_##n1 +#define __DOC2(n1, n2) __doc_##n1##_##n2 +#define __DOC3(n1, n2, n3) __doc_##n1##_##n2##_##n3 +#define __DOC4(n1, n2, n3, n4) __doc_##n1##_##n2##_##n3##_##n4 +#define __DOC5(n1, n2, n3, n4, n5) __doc_##n1##_##n2##_##n3##_##n4##_##n5 +#define __DOC6(n1, n2, n3, n4, n5, n6) \ + __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6 +#define __DOC7(n1, n2, n3, n4, n5, n6, n7) \ + __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 +#define DOC(...) \ + __EXPAND(__EXPAND(__CAT2(__DOC, __VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) + +#if defined(__GNUG__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#endif + +static const char* __doc_ABSL_DECLARE_FLAG = R"doc()doc"; + +static const char* __doc_File = R"doc()doc"; + +static const char* __doc_operations_research_Assignment = + R"doc(An Assignment is a variable -> domains mapping, used to report +solutions to the user.)doc"; + +static const char* __doc_operations_research_Assignment_2 = + R"doc(An Assignment is a variable -> domains mapping, used to report +solutions to the user.)doc"; + +static const char* __doc_operations_research_AssignmentContainer = R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_Add = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_AddAtPosition = + R"doc(Advanced usage: Adds element at a given position; position has to have +been allocated with AssignmentContainer::Resize() beforehand.)doc"; + +static const char* + __doc_operations_research_AssignmentContainer_AreAllElementsBound = + R"doc()doc"; + +static const char* + __doc_operations_research_AssignmentContainer_AssignmentContainer = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_Clear = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_Contains = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_Copy = + R"doc(Copies all the elements of 'container' to this container, clearing its +previous content.)doc"; + +static const char* + __doc_operations_research_AssignmentContainer_CopyIntersection = + R"doc(Copies the elements of 'container' which are already in the calling +container.)doc"; + +static const char* __doc_operations_research_AssignmentContainer_Element = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_Element_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_AssignmentContainer_ElementPtrOrNull = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_Empty = + R"doc()doc"; + +static const char* + __doc_operations_research_AssignmentContainer_EnsureMapIsUpToDate = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_FastAdd = + R"doc(Adds element without checking its presence in the container.)doc"; + +static const char* __doc_operations_research_AssignmentContainer_Find = + R"doc()doc"; + +static const char* + __doc_operations_research_AssignmentContainer_MutableElement = R"doc()doc"; + +static const char* + __doc_operations_research_AssignmentContainer_MutableElement_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_AssignmentContainer_MutableElementOrNull = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_Resize = + R"doc(Advanced usage: Resizes the container, potentially adding elements +with null variables.)doc"; + +static const char* __doc_operations_research_AssignmentContainer_Restore = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_Size = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_Store = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_elements = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_elements_2 = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_elements_map = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentContainer_operator_eq = + R"doc(Returns true if this and 'container' both represent the same V* -> E +map. Runs in linear time; requires that the == operator on the type E +is well defined.)doc"; + +static const char* __doc_operations_research_AssignmentContainer_operator_ne = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentElement = R"doc()doc"; + +static const char* __doc_operations_research_AssignmentElement_Activate = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentElement_Activated = + R"doc()doc"; + +static const char* + __doc_operations_research_AssignmentElement_AssignmentElement = R"doc()doc"; + +static const char* __doc_operations_research_AssignmentElement_Deactivate = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentElement_activated = + R"doc()doc"; + +static const char* __doc_operations_research_AssignmentProto = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Activate = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Activate_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Activate_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ActivateObjective = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_ActivateObjectiveFromIndex = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Activated = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Activated_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Activated_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ActivatedObjective = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_ActivatedObjectiveFromIndex = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Add = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Add_2 = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Add_3 = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Add_4 = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Add_5 = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Add_6 = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_AddObjective = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_AddObjectives = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_AreAllElementsBound = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Assignment = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Assignment_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Assignment_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_BackwardSequence = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Bound = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Clear = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ClearObjective = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Contains = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Contains_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Contains_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Copy = + R"doc(Copies 'assignment' to the current assignment, clearing its previous +content.)doc"; + +static const char* __doc_operations_research_Assignment_CopyIntersection = + R"doc(Copies the intersection of the two assignments to the current +assignment.)doc"; + +static const char* __doc_operations_research_Assignment_Deactivate = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Deactivate_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Deactivate_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_DeactivateObjective = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_DeactivateObjectiveFromIndex = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_DurationMax = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_DurationMin = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_DurationValue = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Empty = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_EndMax = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_EndMin = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_EndValue = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_FastAdd = + R"doc(Adds without checking if variable has been previously added.)doc"; + +static const char* __doc_operations_research_Assignment_FastAdd_2 = + R"doc(Adds without checking if variable has been previously added.)doc"; + +static const char* __doc_operations_research_Assignment_FastAdd_3 = + R"doc(Adds without checking if the variable had been previously added.)doc"; + +static const char* __doc_operations_research_Assignment_ForwardSequence = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_HasObjective = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_HasObjectiveFromIndex = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_IntVarContainer = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_IntervalVarContainer = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Load = + R"doc(Loads an assignment from a file; does not add variables to the +assignment (only the variables contained in the assignment are +modified).)doc"; + +static const char* __doc_operations_research_Assignment_Load_2 = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Load_3 = + R"doc(#if !defined(SWIG))doc"; + +static const char* __doc_operations_research_Assignment_Max = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Min = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_MutableIntVarContainer = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_MutableIntervalVarContainer = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_MutableSequenceVarContainer = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_NumIntVars = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_NumIntervalVars = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_NumObjectives = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_NumSequenceVars = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Objective = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ObjectiveBound = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_ObjectiveBoundFromIndex = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ObjectiveFromIndex = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ObjectiveMax = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ObjectiveMaxFromIndex = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ObjectiveMin = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ObjectiveMinFromIndex = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_ObjectiveValue = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_ObjectiveValueFromIndex = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_PerformedMax = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_PerformedMin = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_PerformedValue = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Restore = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Save = + R"doc(Saves the assignment to a file.)doc"; + +static const char* __doc_operations_research_Assignment_Save_2 = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Save_3 = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SequenceVarContainer = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetBackwardSequence = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetDurationMax = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetDurationMin = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetDurationRange = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetDurationValue = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetEndMax = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetEndMin = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetEndRange = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetEndValue = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetForwardSequence = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetMax = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetMin = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetObjectiveMax = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_SetObjectiveMaxFromIndex = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetObjectiveMin = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_SetObjectiveMinFromIndex = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetObjectiveRange = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_SetObjectiveRangeFromIndex = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetObjectiveValue = + R"doc()doc"; + +static const char* + __doc_operations_research_Assignment_SetObjectiveValueFromIndex = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetPerformedMax = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetPerformedMin = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetPerformedRange = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetPerformedValue = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetRange = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetSequence = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetStartMax = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetStartMin = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetStartRange = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetStartValue = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetUnperformed = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_SetValue = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Size = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_StartMax = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_StartMin = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_StartValue = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Store = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Unperformed = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_Value = R"doc()doc"; + +static const char* __doc_operations_research_Assignment_int_var_container = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_interval_var_container = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_objective_elements = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_operator_eq = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_operator_ne = + R"doc()doc"; + +static const char* __doc_operations_research_Assignment_sequence_var_container = + R"doc()doc"; + +static const char* __doc_operations_research_BaseObject = + R"doc(A BaseObject is the root of all reversibly allocated objects. A +DebugString method and the associated << operator are implemented as a +convenience.)doc"; + +static const char* __doc_operations_research_BaseObject_2 = + R"doc(A BaseObject is the root of all reversibly allocated objects. A +DebugString method and the associated << operator are implemented as a +convenience.)doc"; + +static const char* __doc_operations_research_BaseObject_BaseObject = + R"doc()doc"; + +static const char* __doc_operations_research_BaseObject_BaseObject_2 = + R"doc()doc"; + +static const char* __doc_operations_research_BaseObject_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_BaseObject_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_CastConstraint = + R"doc(Cast constraints are special channeling constraints designed to keep a +variable in sync with an expression. They are created internally when +Var() is called on a subclass of IntExpr.)doc"; + +static const char* __doc_operations_research_CastConstraint_2 = + R"doc(Cast constraints are special channeling constraints designed to keep a +variable in sync with an expression. They are created internally when +Var() is called on a subclass of IntExpr.)doc"; + +static const char* __doc_operations_research_CastConstraint_CastConstraint = + R"doc()doc"; + +static const char* __doc_operations_research_CastConstraint_target_var = + R"doc()doc"; + +static const char* __doc_operations_research_CastConstraint_target_var_2 = + R"doc()doc"; + +static const char* __doc_operations_research_ClockTimer = R"doc()doc"; + +static const char* __doc_operations_research_Constraint = + R"doc(A constraint is the main modeling object. It provides two methods: - +Post() is responsible for creating the demons and attaching them to +immediate demons(). - InitialPropagate() is called once just after +Post and performs the initial propagation. The subsequent propagations +will be performed by the demons Posted during the post() method.)doc"; + +static const char* __doc_operations_research_Constraint_2 = + R"doc(A constraint is the main modeling object. It provides two methods: - +Post() is responsible for creating the demons and attaching them to +immediate demons(). - InitialPropagate() is called once just after +Post and performs the initial propagation. The subsequent propagations +will be performed by the demons Posted during the post() method.)doc"; + +static const char* __doc_operations_research_Constraint_Accept = + R"doc(Accepts the given visitor.)doc"; + +static const char* __doc_operations_research_Constraint_Constraint = + R"doc()doc"; + +static const char* __doc_operations_research_Constraint_Constraint_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Constraint_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_Constraint_InitialPropagate = + R"doc(This method performs the initial propagation of the constraint. It is +called just after the post.)doc"; + +static const char* __doc_operations_research_Constraint_IsCastConstraint = + R"doc(Is the constraint created by a cast from expression to integer +variable?)doc"; + +static const char* __doc_operations_research_Constraint_Post = + R"doc(This method is called when the constraint is processed by the solver. +Its main usage is to attach demons to variables.)doc"; + +static const char* __doc_operations_research_Constraint_PostAndPropagate = + R"doc(Calls Post and then Propagate to initialize the constraints. This is +usually done in the root node.)doc"; + +static const char* __doc_operations_research_Constraint_Var = + R"doc(Creates a Boolean variable representing the status of the constraint +(false = constraint is violated, true = constraint is satisfied). It +returns nullptr if the constraint does not support this API.)doc"; + +static const char* __doc_operations_research_Constraint_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_CpRandomSeed = R"doc()doc"; + +static const char* __doc_operations_research_Decision = + R"doc(A Decision represents a choice point in the search tree. The two main +methods are Apply() to go left, or Refute() to go right.)doc"; + +static const char* __doc_operations_research_Decision_2 = + R"doc(A Decision represents a choice point in the search tree. The two main +methods are Apply() to go left, or Refute() to go right.)doc"; + +static const char* __doc_operations_research_DecisionBuilder = + R"doc(A DecisionBuilder is responsible for creating the search tree. The +important method is Next(), which returns the next decision to +execute.)doc"; + +static const char* __doc_operations_research_DecisionBuilder_2 = + R"doc(A DecisionBuilder is responsible for creating the search tree. The +important method is Next(), which returns the next decision to +execute.)doc"; + +static const char* __doc_operations_research_DecisionBuilder_Accept = + R"doc()doc"; + +static const char* __doc_operations_research_DecisionBuilder_AppendMonitors = + R"doc(This method will be called at the start of the search. It asks the +decision builder if it wants to append search monitors to the list of +active monitors for this search. Please note there are no checks at +this point for duplication.)doc"; + +static const char* __doc_operations_research_DecisionBuilder_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_DecisionBuilder_DecisionBuilder = + R"doc()doc"; + +static const char* __doc_operations_research_DecisionBuilder_DecisionBuilder_2 = + R"doc()doc"; + +static const char* __doc_operations_research_DecisionBuilder_GetName = + R"doc()doc"; + +static const char* __doc_operations_research_DecisionBuilder_Next = + R"doc(This is the main method of the decision builder class. It must return +a decision (an instance of the class Decision). If it returns nullptr, +this means that the decision builder has finished its work.)doc"; + +static const char* __doc_operations_research_DecisionBuilder_name = R"doc()doc"; + +static const char* __doc_operations_research_DecisionBuilder_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_DecisionBuilder_set_name = + R"doc()doc"; + +static const char* __doc_operations_research_DecisionVisitor = + R"doc(A DecisionVisitor is used to inspect a decision. It contains virtual +methods for all type of 'declared' decisions.)doc"; + +static const char* __doc_operations_research_DecisionVisitor_2 = + R"doc(A DecisionVisitor is used to inspect a decision. It contains virtual +methods for all type of 'declared' decisions.)doc"; + +static const char* __doc_operations_research_DecisionVisitor_DecisionVisitor = + R"doc()doc"; + +static const char* __doc_operations_research_DecisionVisitor_DecisionVisitor_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_DecisionVisitor_VisitRankFirstInterval = + R"doc()doc"; + +static const char* + __doc_operations_research_DecisionVisitor_VisitRankLastInterval = + R"doc()doc"; + +static const char* + __doc_operations_research_DecisionVisitor_VisitScheduleOrExpedite = + R"doc()doc"; + +static const char* + __doc_operations_research_DecisionVisitor_VisitScheduleOrPostpone = + R"doc()doc"; + +static const char* + __doc_operations_research_DecisionVisitor_VisitSetVariableValue = + R"doc()doc"; + +static const char* + __doc_operations_research_DecisionVisitor_VisitSplitVariableDomain = + R"doc()doc"; + +static const char* + __doc_operations_research_DecisionVisitor_VisitUnknownDecision = + R"doc()doc"; + +static const char* __doc_operations_research_DecisionVisitor_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_Decision_Accept = + R"doc(Accepts the given visitor.)doc"; + +static const char* __doc_operations_research_Decision_Apply = + R"doc(Apply will be called first when the decision is executed.)doc"; + +static const char* __doc_operations_research_Decision_DebugString = R"doc()doc"; + +static const char* __doc_operations_research_Decision_Decision = R"doc()doc"; + +static const char* __doc_operations_research_Decision_Decision_2 = R"doc()doc"; + +static const char* __doc_operations_research_Decision_Refute = + R"doc(Refute will be called after a backtrack.)doc"; + +static const char* __doc_operations_research_Decision_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_DefaultPhaseParameters = + R"doc(This struct holds all parameters for the default search. +DefaultPhaseParameters is only used by Solver::MakeDefaultPhase +methods. Note this is for advanced users only.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_DefaultPhaseParameters = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_DisplayLevel = R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_DisplayLevel_NONE = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_DisplayLevel_NORMAL = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_DisplayLevel_VERBOSE = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_ValueSelection = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_ValueSelection_SELECT_MAX_IMPACT = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_ValueSelection_SELECT_MIN_IMPACT = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_VariableSelection = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_VariableSelection_CHOOSE_MAX_AVERAGE_IMPACT = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_VariableSelection_CHOOSE_MAX_SUM_IMPACT = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_VariableSelection_CHOOSE_MAX_VALUE_IMPACT = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_decision_builder = + R"doc(When defined, this overrides the default impact based decision +builder.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_display_level = + R"doc(This represents the amount of information displayed by the default +search. NONE means no display, VERBOSE means extra information.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_heuristic_num_failures_limit = + R"doc(The failure limit for each heuristic that we run.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_heuristic_period = + R"doc(The distance in nodes between each run of the heuristics. A negative +or null value will mean that we will not run heuristics at all.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_initialization_splits = + R"doc(Maximum number of intervals that the initialization of impacts will +scan per variable.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_persistent_impact = + R"doc(Whether to keep the impact from the first search for other searches, +or to recompute the impact for each new search.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_random_seed = + R"doc(Seed used to initialize the random part in some heuristics.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_run_all_heuristics = + R"doc(The default phase will run heuristics periodically. This parameter +indicates if we should run all heuristics, or a randomly selected one.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_use_last_conflict = + R"doc(Should we use last conflict method. The default is false.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_value_selection_schema = + R"doc(This parameter describes which value to select for a given var.)doc"; + +static const char* + __doc_operations_research_DefaultPhaseParameters_var_selection_schema = + R"doc(This parameter describes how the next variable to instantiate will be +chosen.)doc"; + +static const char* __doc_operations_research_Demon = + R"doc(A Demon is the base element of a propagation queue. It is the main +object responsible for implementing the actual propagation of the +constraint and pruning the inconsistent values in the domains of the +variables. The main concept is that demons are listeners that are +attached to the variables and listen to their modifications. There are +two methods: - Run() is the actual method called when the demon is +processed. - priority() returns its priority. Standard priorities are +slow, normal or fast. "immediate" is reserved for variables and is +treated separately.)doc"; + +static const char* __doc_operations_research_Demon_2 = + R"doc(A Demon is the base element of a propagation queue. It is the main +object responsible for implementing the actual propagation of the +constraint and pruning the inconsistent values in the domains of the +variables. The main concept is that demons are listeners that are +attached to the variables and listen to their modifications. There are +two methods: - Run() is the actual method called when the demon is +processed. - priority() returns its priority. Standard priorities are +slow, normal or fast. "immediate" is reserved for variables and is +treated separately.)doc"; + +static const char* __doc_operations_research_DemonProfiler = R"doc()doc"; + +static const char* __doc_operations_research_Demon_DebugString = R"doc()doc"; + +static const char* __doc_operations_research_Demon_Demon = + R"doc(This indicates the priority of a demon. Immediate demons are treated +separately and corresponds to variables.)doc"; + +static const char* __doc_operations_research_Demon_Demon_2 = R"doc()doc"; + +static const char* __doc_operations_research_Demon_Run = + R"doc(This is the main callback of the demon.)doc"; + +static const char* __doc_operations_research_Demon_desinhibit = + R"doc(This method un-inhibits the demon that was previously inhibited.)doc"; + +static const char* __doc_operations_research_Demon_inhibit = + R"doc(This method inhibits the demon in the search tree below the current +position.)doc"; + +static const char* __doc_operations_research_Demon_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_Demon_priority = + R"doc(This method returns the priority of the demon. Usually a demon is +fast, slow or normal. Immediate demons are reserved for internal use +to maintain variables.)doc"; + +static const char* __doc_operations_research_Demon_set_stamp = R"doc()doc"; + +static const char* __doc_operations_research_Demon_stamp = R"doc()doc"; + +static const char* __doc_operations_research_Demon_stamp_2 = R"doc()doc"; + +static const char* __doc_operations_research_Dimension = R"doc()doc"; + +static const char* __doc_operations_research_DisjunctiveConstraint = + R"doc()doc"; + +static const char* __doc_operations_research_DisjunctiveConstraint_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctiveConstraint_DisjunctiveConstraint = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctiveConstraint_DisjunctiveConstraint_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctiveConstraint_MakeSequenceVar = + R"doc(Creates a sequence variable from the constraint.)doc"; + +static const char* + __doc_operations_research_DisjunctiveConstraint_SetTransitionTime = + R"doc(Add a transition time between intervals. It forces the distance +between the end of interval a and start of interval b that follows it +to be at least transition_time(a, b). This function must always return +a positive or null value.)doc"; + +static const char* + __doc_operations_research_DisjunctiveConstraint_TransitionTime = + R"doc()doc"; + +static const char* __doc_operations_research_DisjunctiveConstraint_actives = + R"doc()doc"; + +static const char* __doc_operations_research_DisjunctiveConstraint_intervals = + R"doc()doc"; + +static const char* __doc_operations_research_DisjunctiveConstraint_nexts = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctiveConstraint_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_DisjunctiveConstraint_time_cumuls = + R"doc()doc"; + +static const char* __doc_operations_research_DisjunctiveConstraint_time_slacks = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctiveConstraint_transition_time = + R"doc()doc"; + +static const char* __doc_operations_research_ImprovementSearchLimit = + R"doc()doc"; + +static const char* __doc_operations_research_ImprovementSearchLimit_2 = + R"doc()doc"; + +static const char* __doc_operations_research_ImprovementSearchLimit_AtSolution = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_CheckWithOffset = + R"doc()doc"; + +static const char* __doc_operations_research_ImprovementSearchLimit_Copy = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_ImprovementSearchLimit = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_ImprovementSearchLimit_2 = + R"doc()doc"; + +static const char* __doc_operations_research_ImprovementSearchLimit_Init = + R"doc()doc"; + +static const char* __doc_operations_research_ImprovementSearchLimit_Install = + R"doc()doc"; + +static const char* __doc_operations_research_ImprovementSearchLimit_MakeClone = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_best_objectives = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_gradient_stage = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_improvement_rate_coefficient = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_improvement_rate_solutions_distance = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_improvements = R"doc()doc"; + +static const char* __doc_operations_research_ImprovementSearchLimit_maximize = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_objective_offsets = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_objective_scaling_factors = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_objective_updated = + R"doc()doc"; + +static const char* + __doc_operations_research_ImprovementSearchLimit_objective_vars = + R"doc()doc"; + +static const char* __doc_operations_research_ImprovementSearchLimit_thresholds = + R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues = + R"doc(Utility class to encapsulate an IntVarIterator and use it in a range- +based loop. See the code snippet above IntVarIterator. + +It contains DEBUG_MODE-enabled code that DCHECKs that the same +iterator instance isn't being iterated on in multiple places +simultaneously.)doc"; + +static const char* __doc_operations_research_InitAndGetValues_InitAndGetValues = + R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues_Iterator = + R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues_Iterator_2 = + R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues_Iterator_Begin = + R"doc(These are the only way to construct an Iterator.)doc"; + +static const char* __doc_operations_research_InitAndGetValues_Iterator_End = + R"doc()doc"; + +static const char* + __doc_operations_research_InitAndGetValues_Iterator_Iterator = R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues_Iterator_is_end = + R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues_Iterator_it = + R"doc()doc"; + +static const char* + __doc_operations_research_InitAndGetValues_Iterator_operator_inc = + R"doc()doc"; + +static const char* + __doc_operations_research_InitAndGetValues_Iterator_operator_mul = + R"doc()doc"; + +static const char* + __doc_operations_research_InitAndGetValues_Iterator_operator_ne = + R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues_begin = + R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues_begin_was_called = + R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues_end = R"doc()doc"; + +static const char* __doc_operations_research_InitAndGetValues_it = R"doc()doc"; + +static const char* __doc_operations_research_IntExpr = + R"doc(The class IntExpr is the base of all integer expressions in constraint +programming. It contains the basic protocol for an expression: - +setting and modifying its bound - querying if it is bound - listening +to events modifying its bounds - casting it into a variable (instance +of IntVar))doc"; + +static const char* __doc_operations_research_IntExpr_2 = + R"doc(The class IntExpr is the base of all integer expressions in constraint +programming. It contains the basic protocol for an expression: - +setting and modifying its bound - querying if it is bound - listening +to events modifying its bounds - casting it into a variable (instance +of IntVar))doc"; + +static const char* __doc_operations_research_IntExpr_Accept = + R"doc(Accepts the given visitor.)doc"; + +static const char* __doc_operations_research_IntExpr_Bound = + R"doc(Returns true if the min and the max of the expression are equal.)doc"; + +static const char* __doc_operations_research_IntExpr_IntExpr = R"doc()doc"; + +static const char* __doc_operations_research_IntExpr_IntExpr_2 = R"doc()doc"; + +static const char* __doc_operations_research_IntExpr_IsVar = + R"doc(Returns true if the expression is indeed a variable.)doc"; + +static const char* __doc_operations_research_IntExpr_Max = R"doc()doc"; + +static const char* __doc_operations_research_IntExpr_Min = R"doc()doc"; + +static const char* __doc_operations_research_IntExpr_Range = + R"doc(By default calls Min() and Max(), but can be redefined when Min and +Max code can be factorized.)doc"; + +static const char* __doc_operations_research_IntExpr_SetMax = R"doc()doc"; + +static const char* __doc_operations_research_IntExpr_SetMin = R"doc()doc"; + +static const char* __doc_operations_research_IntExpr_SetRange = + R"doc(This method sets both the min and the max of the expression.)doc"; + +static const char* __doc_operations_research_IntExpr_SetValue = + R"doc(This method sets the value of the expression.)doc"; + +static const char* __doc_operations_research_IntExpr_Var = + R"doc(Creates a variable from the expression.)doc"; + +static const char* __doc_operations_research_IntExpr_VarWithName = + R"doc(Creates a variable from the expression and set the name of the +resulting var. If the expression is already a variable, then it will +set the name of the expression, possibly overwriting it. This is just +a shortcut to Var() followed by set_name().)doc"; + +static const char* __doc_operations_research_IntExpr_WhenRange = + R"doc(Attach a demon that will watch the min or the max of the expression.)doc"; + +static const char* __doc_operations_research_IntExpr_WhenRange_2 = + R"doc(Attach a demon that will watch the min or the max of the expression.)doc"; + +static const char* __doc_operations_research_IntExpr_WhenRange_3 = + R"doc(Attach a demon that will watch the min or the max of the expression.)doc"; + +static const char* __doc_operations_research_IntExpr_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_IntVar = + R"doc(The class IntVar is a subset of IntExpr. In addition to the IntExpr +protocol, it offers persistence, removing values from the domains, and +a finer model for events.)doc"; + +static const char* __doc_operations_research_IntVar_2 = + R"doc(The class IntVar is a subset of IntExpr. In addition to the IntExpr +protocol, it offers persistence, removing values from the domains, and +a finer model for events.)doc"; + +static const char* __doc_operations_research_IntVarAssignment = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Bound = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Clone = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Copy = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_IntVarElement = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_IntVarElement_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_LoadFromProto = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Max = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Min = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Reset = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Restore = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_SetMax = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_SetMin = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_SetRange = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_SetValue = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Store = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Value = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_Var = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_WriteToProto = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_max = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_min = R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_operator_eq = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_operator_ne = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarElement_var = R"doc()doc"; + +static const char* __doc_operations_research_IntVarIterator = + R"doc(IntVar* current_var; std::unique_ptr +it(current_var->MakeHoleIterator(false)); for (const int64_t hole : +InitAndGetValues(it)) { /// use the hole })doc"; + +static const char* __doc_operations_research_IntVarIterator_DebugString = + R"doc(Pretty Print.)doc"; + +static const char* __doc_operations_research_IntVarIterator_Init = + R"doc(This method must be called before each loop.)doc"; + +static const char* __doc_operations_research_IntVarIterator_Next = + R"doc(This method moves the iterator to the next value.)doc"; + +static const char* __doc_operations_research_IntVarIterator_Ok = + R"doc(This method indicates if we can call Value() or not.)doc"; + +static const char* __doc_operations_research_IntVarIterator_Value = + R"doc(This method returns the current value of the iterator.)doc"; + +static const char* __doc_operations_research_IntVarLocalSearchFilter = + R"doc()doc"; + +static const char* __doc_operations_research_IntVar_Accept = + R"doc(Accepts the given visitor.)doc"; + +static const char* __doc_operations_research_IntVar_Contains = + R"doc(This method returns whether the value 'v' is in the domain of the +variable.)doc"; + +static const char* __doc_operations_research_IntVar_IntVar = R"doc()doc"; + +static const char* __doc_operations_research_IntVar_IntVar_2 = R"doc()doc"; + +static const char* __doc_operations_research_IntVar_IntVar_3 = R"doc()doc"; + +static const char* __doc_operations_research_IntVar_IsDifferent = R"doc()doc"; + +static const char* __doc_operations_research_IntVar_IsEqual = + R"doc(IsEqual)doc"; + +static const char* __doc_operations_research_IntVar_IsGreaterOrEqual = + R"doc()doc"; + +static const char* __doc_operations_research_IntVar_IsLessOrEqual = R"doc()doc"; + +static const char* __doc_operations_research_IntVar_IsVar = R"doc()doc"; + +static const char* __doc_operations_research_IntVar_MakeDomainIterator = + R"doc(Creates a domain iterator. When 'reversible' is false, the returned +object is created on the normal C++ heap and the solver does NOT take +ownership of the object.)doc"; + +static const char* __doc_operations_research_IntVar_MakeHoleIterator = + R"doc(Creates a hole iterator. When 'reversible' is false, the returned +object is created on the normal C++ heap and the solver does NOT take +ownership of the object.)doc"; + +static const char* __doc_operations_research_IntVar_OldMax = + R"doc(Returns the previous max.)doc"; + +static const char* __doc_operations_research_IntVar_OldMin = + R"doc(Returns the previous min.)doc"; + +static const char* __doc_operations_research_IntVar_RemoveInterval = + R"doc(This method removes the interval 'l' .. 'u' from the domain of the +variable. It assumes that 'l' <= 'u'.)doc"; + +static const char* __doc_operations_research_IntVar_RemoveValue = + R"doc(This method removes the value 'v' from the domain of the variable.)doc"; + +static const char* __doc_operations_research_IntVar_RemoveValues = + R"doc(This method remove the values from the domain of the variable.)doc"; + +static const char* __doc_operations_research_IntVar_SetValues = + R"doc(This method intersects the current domain with the values in the +array.)doc"; + +static const char* __doc_operations_research_IntVar_Size = + R"doc(This method returns the number of values in the domain of the +variable.)doc"; + +static const char* __doc_operations_research_IntVar_Value = + R"doc(This method returns the value of the variable. This method checks +before that the variable is bound.)doc"; + +static const char* __doc_operations_research_IntVar_Var = R"doc()doc"; + +static const char* __doc_operations_research_IntVar_VarType = R"doc()doc"; + +static const char* __doc_operations_research_IntVar_WhenBound = + R"doc(This method attaches a demon that will be awakened when the variable +is bound.)doc"; + +static const char* __doc_operations_research_IntVar_WhenBound_2 = + R"doc(This method attaches a closure that will be awakened when the variable +is bound.)doc"; + +static const char* __doc_operations_research_IntVar_WhenBound_3 = + R"doc(This method attaches an action that will be awakened when the variable +is bound.)doc"; + +static const char* __doc_operations_research_IntVar_WhenDomain = + R"doc(This method attaches a demon that will watch any domain modification +of the domain of the variable.)doc"; + +static const char* __doc_operations_research_IntVar_WhenDomain_2 = + R"doc(This method attaches a closure that will watch any domain modification +of the domain of the variable.)doc"; + +static const char* __doc_operations_research_IntVar_WhenDomain_3 = + R"doc(This method attaches an action that will watch any domain modification +of the domain of the variable.)doc"; + +static const char* __doc_operations_research_IntVar_index = + R"doc(Returns the index of the variable.)doc"; + +static const char* __doc_operations_research_IntVar_index_2 = R"doc()doc"; + +static const char* __doc_operations_research_IntVar_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar = + R"doc(Interval variables are often used in scheduling. The main +characteristics of an IntervalVar are the start position, duration, +and end date. All these characteristics can be queried and set, and +demons can be posted on their modifications. + +An important aspect is optionality: an IntervalVar can be performed or +not. If unperformed, then it simply does not exist, and its +characteristics cannot be accessed any more. An interval var is +automatically marked as unperformed when it is not consistent anymore +(start greater than end, duration < 0...))doc"; + +static const char* __doc_operations_research_IntervalVar_2 = + R"doc(Interval variables are often used in scheduling. The main +characteristics of an IntervalVar are the start position, duration, +and end date. All these characteristics can be queried and set, and +demons can be posted on their modifications. + +An important aspect is optionality: an IntervalVar can be performed or +not. If unperformed, then it simply does not exist, and its +characteristics cannot be accessed any more. An interval var is +automatically marked as unperformed when it is not consistent anymore +(start greater than end, duration < 0...))doc"; + +static const char* __doc_operations_research_IntervalVarAssignment = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement = R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_Bound = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_Clone = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_Copy = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_DurationMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_DurationMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_DurationValue = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_EndMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_EndMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_EndValue = + R"doc()doc"; + +static const char* + __doc_operations_research_IntervalVarElement_IntervalVarElement = + R"doc()doc"; + +static const char* + __doc_operations_research_IntervalVarElement_IntervalVarElement_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_LoadFromProto = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_PerformedMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_PerformedMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_PerformedValue = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_Reset = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_Restore = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetDurationMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetDurationMin = + R"doc()doc"; + +static const char* + __doc_operations_research_IntervalVarElement_SetDurationRange = R"doc()doc"; + +static const char* + __doc_operations_research_IntervalVarElement_SetDurationValue = R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetEndMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetEndMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetEndRange = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetEndValue = + R"doc()doc"; + +static const char* + __doc_operations_research_IntervalVarElement_SetPerformedMax = R"doc()doc"; + +static const char* + __doc_operations_research_IntervalVarElement_SetPerformedMin = R"doc()doc"; + +static const char* + __doc_operations_research_IntervalVarElement_SetPerformedRange = + R"doc()doc"; + +static const char* + __doc_operations_research_IntervalVarElement_SetPerformedValue = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetStartMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetStartMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetStartRange = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_SetStartValue = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_StartMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_StartMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_StartValue = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_Store = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_Var = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_WriteToProto = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_duration_max = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_duration_min = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_end_max = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_end_min = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_operator_eq = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_operator_ne = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_performed_max = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_performed_min = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_start_max = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_start_min = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVarElement_var = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_Accept = + R"doc(Accepts the given visitor.)doc"; + +static const char* __doc_operations_research_IntervalVar_CannotBePerformed = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_DurationExpr = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_DurationMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_DurationMin = + R"doc(These methods query, set, and watch the duration of the interval var.)doc"; + +static const char* __doc_operations_research_IntervalVar_EndExpr = R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_EndMax = R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_EndMin = + R"doc(These methods query, set, and watch the end position of the interval +var.)doc"; + +static const char* __doc_operations_research_IntervalVar_IntervalVar = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_IntervalVar_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_IsPerformedBound = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_MayBePerformed = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_MustBePerformed = + R"doc(These methods query, set, and watch the performed status of the +interval var.)doc"; + +static const char* __doc_operations_research_IntervalVar_OldDurationMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_OldDurationMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_OldEndMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_OldEndMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_OldStartMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_OldStartMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_PerformedExpr = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SafeDurationExpr = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SafeEndExpr = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SafeStartExpr = + R"doc(These methods create expressions encapsulating the start, end and +duration of the interval var. If the interval var is unperformed, they +will return the unperformed_value.)doc"; + +static const char* __doc_operations_research_IntervalVar_SetDurationMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SetDurationMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SetDurationRange = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SetEndMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SetEndMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SetEndRange = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SetPerformed = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SetStartMax = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SetStartMin = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_SetStartRange = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_StartExpr = + R"doc(These methods create expressions encapsulating the start, end and +duration of the interval var. Please note that these must not be used +if the interval var is unperformed.)doc"; + +static const char* __doc_operations_research_IntervalVar_StartMax = R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_StartMin = + R"doc(These methods query, set, and watch the start position of the interval +var.)doc"; + +static const char* __doc_operations_research_IntervalVar_WasPerformedBound = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenAnything = + R"doc(Attaches a demon awakened when anything about this interval changes.)doc"; + +static const char* __doc_operations_research_IntervalVar_WhenAnything_2 = + R"doc(Attaches a closure awakened when anything about this interval changes.)doc"; + +static const char* __doc_operations_research_IntervalVar_WhenAnything_3 = + R"doc(Attaches an action awakened when anything about this interval changes.)doc"; + +static const char* __doc_operations_research_IntervalVar_WhenDurationBound = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenDurationBound_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenDurationBound_3 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenDurationRange = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenDurationRange_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenDurationRange_3 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenEndBound = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenEndBound_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenEndBound_3 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenEndRange = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenEndRange_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenEndRange_3 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenPerformedBound = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenPerformedBound_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenPerformedBound_3 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenStartBound = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenStartBound_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenStartBound_3 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenStartRange = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenStartRange_2 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_WhenStartRange_3 = + R"doc()doc"; + +static const char* __doc_operations_research_IntervalVar_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_LightIntFunctionElementCt = + R"doc()doc"; + +static const char* __doc_operations_research_LightIntIntFunctionElementCt = + R"doc()doc"; + +static const char* __doc_operations_research_LocalSearch = R"doc()doc"; + +static const char* __doc_operations_research_LocalSearchFilter = R"doc()doc"; + +static const char* __doc_operations_research_LocalSearchFilterManager = + R"doc()doc"; + +static const char* __doc_operations_research_LocalSearchMonitor = R"doc()doc"; + +static const char* __doc_operations_research_LocalSearchOperator = R"doc()doc"; + +static const char* __doc_operations_research_LocalSearchPhaseParameters = + R"doc()doc"; + +static const char* __doc_operations_research_LocalSearchProfiler = R"doc()doc"; + +static const char* __doc_operations_research_ModelCache = R"doc()doc"; + +static const char* __doc_operations_research_ModelVisitor = + R"doc(Model visitor.)doc"; + +static const char* __doc_operations_research_ModelVisitor_2 = + R"doc(Model visitor.)doc"; + +static const char* __doc_operations_research_ModelVisitor_BeginVisitConstraint = + R"doc()doc"; + +static const char* __doc_operations_research_ModelVisitor_BeginVisitExtension = + R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_BeginVisitIntegerExpression = + R"doc()doc"; + +static const char* __doc_operations_research_ModelVisitor_BeginVisitModel = + R"doc(Begin/End visit element.)doc"; + +static const char* __doc_operations_research_ModelVisitor_EndVisitConstraint = + R"doc()doc"; + +static const char* __doc_operations_research_ModelVisitor_EndVisitExtension = + R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_EndVisitIntegerExpression = + R"doc()doc"; + +static const char* __doc_operations_research_ModelVisitor_EndVisitModel = + R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitInt64ToBoolExtension = + R"doc(Using SWIG on callbacks is troublesome, so we hide these methods +during the wrapping.)doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitInt64ToInt64AsArray = + R"doc(Expands function as array when index min is 0.)doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitInt64ToInt64Extension = + R"doc()doc"; + +static const char* __doc_operations_research_ModelVisitor_VisitIntegerArgument = + R"doc(Visit integer arguments.)doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitIntegerArrayArgument = + R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitIntegerExpressionArgument = + R"doc(Visit integer expression argument.)doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitIntegerMatrixArgument = + R"doc()doc"; + +static const char* __doc_operations_research_ModelVisitor_VisitIntegerVariable = + R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitIntegerVariable_2 = R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitIntegerVariableArrayArgument = + R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitIntegerVariableEvaluatorArgument = + R"doc(Helpers.)doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitIntervalArgument = + R"doc(Visit interval argument.)doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitIntervalArrayArgument = + R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitIntervalVariable = R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitSequenceArgument = + R"doc(Visit sequence argument.)doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitSequenceArrayArgument = + R"doc()doc"; + +static const char* + __doc_operations_research_ModelVisitor_VisitSequenceVariable = R"doc()doc"; + +static const char* __doc_operations_research_NumericalRev = + R"doc(Subclass of Rev which adds numerical operations.)doc"; + +static const char* __doc_operations_research_NumericalRevArray = + R"doc(Subclass of RevArray which adds numerical operations.)doc"; + +static const char* __doc_operations_research_NumericalRevArray_Add = + R"doc()doc"; + +static const char* __doc_operations_research_NumericalRevArray_Decr = + R"doc()doc"; + +static const char* __doc_operations_research_NumericalRevArray_Incr = + R"doc()doc"; + +static const char* + __doc_operations_research_NumericalRevArray_NumericalRevArray = R"doc()doc"; + +static const char* __doc_operations_research_NumericalRev_Add = R"doc()doc"; + +static const char* __doc_operations_research_NumericalRev_Decr = R"doc()doc"; + +static const char* __doc_operations_research_NumericalRev_Incr = R"doc()doc"; + +static const char* __doc_operations_research_NumericalRev_NumericalRev = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor = R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_2 = R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_Accept = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_AcceptDelta = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_AtSolution = + R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_BestInternalValue = R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_BestValue = + R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_CurrentInternalValue = + R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_CurrentInternalValuesAreConstraining = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_EnterSearch = + R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_MakeMinimizationVarsLessOrEqualWithSteps = + R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_MakeMinimizationVarsLessOrEqualWithStepsStatus = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_Maximize = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_MinimizationVar = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_ObjectiveMonitor = + R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_ObjectiveMonitor_2 = R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_ObjectiveVar = + R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_SetCurrentInternalValue = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_Size = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_Step = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_best_values = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_current_values = + R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_found_initial_solution = + R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_minimization_vars = R"doc()doc"; + +static const char* + __doc_operations_research_ObjectiveMonitor_minimization_vars_2 = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_objective_vars = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_objective_vars_2 = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_steps = + R"doc()doc"; + +static const char* __doc_operations_research_ObjectiveMonitor_upper_bounds = + R"doc()doc"; + +static const char* __doc_operations_research_One = + R"doc(This method returns 1)doc"; + +static const char* __doc_operations_research_OptimizeVar = + R"doc(This class encapsulates an objective. It requires the direction +(minimize or maximize), the variable to optimize, and the improvement +step.)doc"; + +static const char* __doc_operations_research_OptimizeVar_2 = + R"doc(This class encapsulates an objective. It requires the direction +(minimize or maximize), the variable to optimize, and the improvement +step.)doc"; + +static const char* __doc_operations_research_OptimizeVar_AcceptSolution = + R"doc()doc"; + +static const char* __doc_operations_research_OptimizeVar_ApplyBound = + R"doc()doc"; + +static const char* __doc_operations_research_OptimizeVar_AtSolution = + R"doc()doc"; + +static const char* __doc_operations_research_OptimizeVar_BeginNextDecision = + R"doc(Internal methods.)doc"; + +static const char* __doc_operations_research_OptimizeVar_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_OptimizeVar_Name = R"doc()doc"; + +static const char* __doc_operations_research_OptimizeVar_OptimizeVar = + R"doc()doc"; + +static const char* __doc_operations_research_OptimizeVar_OptimizeVar_2 = + R"doc()doc"; + +static const char* __doc_operations_research_OptimizeVar_RefuteDecision = + R"doc()doc"; + +static const char* __doc_operations_research_OptimizeVar_best = + R"doc(Returns the best value found during search.)doc"; + +static const char* __doc_operations_research_OptimizeVar_var = + R"doc(Returns the variable that is optimized.)doc"; + +static const char* __doc_operations_research_Pack = R"doc()doc"; + +static const char* __doc_operations_research_Pack_2 = R"doc()doc"; + +static const char* __doc_operations_research_Pack_Accept = R"doc()doc"; + +static const char* + __doc_operations_research_Pack_AddCountAssignedItemsDimension = + R"doc(This dimension links 'count_var' to the actual number of items +assigned to a bin in the pack.)doc"; + +static const char* __doc_operations_research_Pack_AddCountUsedBinDimension = + R"doc(This dimension links 'count_var' to the actual number of bins used in +the pack.)doc"; + +static const char* + __doc_operations_research_Pack_AddSumVariableWeightsLessOrEqualConstantDimension = + R"doc(This dimension imposes: forall b in bins, sum (i in items: usage[i] * +is_assigned(i, b)) <= capacity[b] where is_assigned(i, b) is true if +and only if item i is assigned to the bin b. + +This can be used to model shapes of items by linking variables of the +same item on parallel dimensions with an allowed assignment +constraint.)doc"; + +static const char* + __doc_operations_research_Pack_AddWeightedSumEqualVarDimension = + R"doc(This dimension imposes that for all bins b, the weighted sum +(weights[i]) of all objects i assigned to 'b' is equal to loads[b].)doc"; + +static const char* + __doc_operations_research_Pack_AddWeightedSumEqualVarDimension_2 = + R"doc(This dimension imposes that for all bins b, the weighted sum +(weights->Run(i, b)) of all objects i assigned to 'b' is equal to +loads[b].)doc"; + +static const char* + __doc_operations_research_Pack_AddWeightedSumLessOrEqualConstantDimension = + R"doc(This dimension imposes that for all bins b, the weighted sum +(weights[i]) of all objects i assigned to 'b' is less or equal +'bounds[b]'.)doc"; + +static const char* + __doc_operations_research_Pack_AddWeightedSumLessOrEqualConstantDimension_2 = + R"doc(This dimension imposes that for all bins b, the weighted sum +(weights->Run(i)) of all objects i assigned to 'b' is less or equal to +'bounds[b]'. Ownership of the callback is transferred to the pack +constraint.)doc"; + +static const char* + __doc_operations_research_Pack_AddWeightedSumLessOrEqualConstantDimension_3 = + R"doc(This dimension imposes that for all bins b, the weighted sum +(weights->Run(i, b) of all objects i assigned to 'b' is less or equal +to 'bounds[b]'. Ownership of the callback is transferred to the pack +constraint.)doc"; + +static const char* + __doc_operations_research_Pack_AddWeightedSumOfAssignedDimension = + R"doc(This dimension enforces that cost_var == sum of weights[i] for all +objects 'i' assigned to a bin.)doc"; + +static const char* __doc_operations_research_Pack_Assign = R"doc()doc"; + +static const char* __doc_operations_research_Pack_AssignAllPossibleToBin = + R"doc()doc"; + +static const char* __doc_operations_research_Pack_AssignAllRemainingItems = + R"doc()doc"; + +static const char* __doc_operations_research_Pack_AssignFirstPossibleToBin = + R"doc()doc"; + +static const char* __doc_operations_research_Pack_AssignVar = R"doc()doc"; + +static const char* __doc_operations_research_Pack_ClearAll = R"doc()doc"; + +static const char* __doc_operations_research_Pack_DebugString = R"doc()doc"; + +static const char* __doc_operations_research_Pack_InitialPropagate = + R"doc()doc"; + +static const char* __doc_operations_research_Pack_IsAssignedStatusKnown = + R"doc()doc"; + +static const char* __doc_operations_research_Pack_IsInProcess = R"doc()doc"; + +static const char* __doc_operations_research_Pack_IsPossible = R"doc()doc"; + +static const char* __doc_operations_research_Pack_IsUndecided = R"doc()doc"; + +static const char* __doc_operations_research_Pack_OneDomain = R"doc()doc"; + +static const char* __doc_operations_research_Pack_Pack = R"doc()doc"; + +static const char* __doc_operations_research_Pack_Post = R"doc()doc"; + +static const char* __doc_operations_research_Pack_Propagate = R"doc()doc"; + +static const char* __doc_operations_research_Pack_PropagateDelayed = + R"doc()doc"; + +static const char* __doc_operations_research_Pack_RemoveAllPossibleFromBin = + R"doc()doc"; + +static const char* __doc_operations_research_Pack_SetAssigned = R"doc()doc"; + +static const char* __doc_operations_research_Pack_SetImpossible = R"doc()doc"; + +static const char* __doc_operations_research_Pack_SetUnassigned = R"doc()doc"; + +static const char* __doc_operations_research_Pack_UnassignAllRemainingItems = + R"doc()doc"; + +static const char* __doc_operations_research_Pack_bins = R"doc()doc"; + +static const char* __doc_operations_research_Pack_demon = R"doc()doc"; + +static const char* __doc_operations_research_Pack_dims = R"doc()doc"; + +static const char* __doc_operations_research_Pack_forced = R"doc()doc"; + +static const char* __doc_operations_research_Pack_holes = R"doc()doc"; + +static const char* __doc_operations_research_Pack_in_process = R"doc()doc"; + +static const char* __doc_operations_research_Pack_removed = R"doc()doc"; + +static const char* __doc_operations_research_Pack_stamp = R"doc()doc"; + +static const char* __doc_operations_research_Pack_to_set = R"doc()doc"; + +static const char* __doc_operations_research_Pack_to_unset = R"doc()doc"; + +static const char* __doc_operations_research_Pack_unprocessed = R"doc()doc"; + +static const char* __doc_operations_research_Pack_vars = R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder = + R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder_2 = + R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder_Accept = + R"doc()doc"; + +static const char* + __doc_operations_research_ProfiledDecisionBuilder_AppendMonitors = + R"doc()doc"; + +static const char* + __doc_operations_research_ProfiledDecisionBuilder_DebugString = R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder_Next = + R"doc()doc"; + +static const char* + __doc_operations_research_ProfiledDecisionBuilder_ProfiledDecisionBuilder = + R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder_db = + R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder_name = + R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder_name_2 = + R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder_seconds = + R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder_seconds_2 = + R"doc()doc"; + +static const char* __doc_operations_research_ProfiledDecisionBuilder_timer = + R"doc()doc"; + +static const char* __doc_operations_research_PropagationBaseObject = + R"doc(The PropagationBaseObject is a subclass of BaseObject that is also +friend to the Solver class. It allows accessing methods useful when +writing new constraints or new expressions.)doc"; + +static const char* __doc_operations_research_PropagationBaseObject_2 = + R"doc(The PropagationBaseObject is a subclass of BaseObject that is also +friend to the Solver class. It allows accessing methods useful when +writing new constraints or new expressions.)doc"; + +static const char* __doc_operations_research_PropagationBaseObject_BaseName = + R"doc(Returns a base name for automatic naming.)doc"; + +static const char* __doc_operations_research_PropagationBaseObject_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_PropagationBaseObject_EnqueueAll = + R"doc()doc"; + +static const char* + __doc_operations_research_PropagationBaseObject_EnqueueDelayedDemon = + R"doc(This method pushes the demon onto the propagation queue. It will be +processed directly if the queue is empty. It will be enqueued +according to its priority otherwise.)doc"; + +static const char* __doc_operations_research_PropagationBaseObject_EnqueueVar = + R"doc()doc"; + +static const char* __doc_operations_research_PropagationBaseObject_ExecuteAll = + R"doc()doc"; + +static const char* __doc_operations_research_PropagationBaseObject_FreezeQueue = + R"doc(This method freezes the propagation queue. It is useful when you need +to apply multiple modifications at once.)doc"; + +static const char* __doc_operations_research_PropagationBaseObject_HasName = + R"doc(Returns whether the object has been named or not.)doc"; + +static const char* + __doc_operations_research_PropagationBaseObject_PropagationBaseObject = + R"doc()doc"; + +static const char* + __doc_operations_research_PropagationBaseObject_PropagationBaseObject_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_PropagationBaseObject_UnfreezeQueue = + R"doc(This method unfreezes the propagation queue. All modifications that +happened when the queue was frozen will be processed.)doc"; + +static const char* __doc_operations_research_PropagationBaseObject_name = + R"doc(Object naming.)doc"; + +static const char* + __doc_operations_research_PropagationBaseObject_operator_assign = + R"doc()doc"; + +static const char* + __doc_operations_research_PropagationBaseObject_reset_action_on_fail = + R"doc(This method clears the failure callback.)doc"; + +static const char* + __doc_operations_research_PropagationBaseObject_set_action_on_fail = + R"doc()doc"; + +static const char* __doc_operations_research_PropagationBaseObject_set_name = + R"doc()doc"; + +static const char* + __doc_operations_research_PropagationBaseObject_set_variable_to_clean_on_fail = + R"doc(Shortcut for variable cleaner.)doc"; + +static const char* __doc_operations_research_PropagationBaseObject_solver = + R"doc()doc"; + +static const char* __doc_operations_research_PropagationBaseObject_solver_2 = + R"doc()doc"; + +static const char* __doc_operations_research_PropagationMonitor = R"doc()doc"; + +static const char* __doc_operations_research_Queue = R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit = + R"doc(Usual limit based on wall_time, number of explored branches and number +of failures in the search tree)doc"; + +static const char* __doc_operations_research_RegularLimit_2 = + R"doc(Usual limit based on wall_time, number of explored branches and number +of failures in the search tree)doc"; + +static const char* __doc_operations_research_RegularLimitParameters = + R"doc()doc"; + +static const char* + __doc_operations_research_RegularLimit_AbsoluteSolverDeadline = R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_Accept = R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_CheckTime = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_CheckWithOffset = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_Copy = R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_ExitSearch = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_GetPercent = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_Init = R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_Install = R"doc()doc"; + +static const char* + __doc_operations_research_RegularLimit_IsUncheckedSolutionLimitReached = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_MakeClone = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_MakeIdenticalClone = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_ProgressPercent = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_RegularLimit = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_TimeElapsed = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_UpdateLimits = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_branches = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_branches_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_branches_offset = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_check_count = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_cumulative = + R"doc(If cumulative if false, then the limit applies to each search +independently. If it's true, the limit applies globally to all search +for which this monitor is used. When cumulative is true, the offset +fields have two different meanings depending on context: - within a +search, it's an offset to be subtracted from the current value - +outside of search, it's the amount consumed in previous searches)doc"; + +static const char* __doc_operations_research_RegularLimit_duration_limit = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_duration_limit_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_failures = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_failures_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_failures_offset = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_last_time_elapsed = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_next_check = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_smart_time_check = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_solutions = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_solutions_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_solutions_offset = + R"doc()doc"; + +static const char* + __doc_operations_research_RegularLimit_solver_time_at_limit_start = + R"doc()doc"; + +static const char* __doc_operations_research_RegularLimit_wall_time = + R"doc()doc"; + +static const char* __doc_operations_research_Rev = + R"doc(This class adds reversibility to a POD type. It contains the stamp +optimization. i.e. the SaveValue call is done only once per node of +the search tree. Please note that actual stamps always starts at 1, +thus an initial value of 0 will always trigger the first SaveValue.)doc"; + +static const char* __doc_operations_research_RevArray = + R"doc(Reversible array of POD types. It contains the stamp optimization. +I.e., the SaveValue call is done only once per node of the search +tree. Please note that actual stamp always starts at 1, thus an +initial value of 0 always triggers the first SaveValue.)doc"; + +static const char* __doc_operations_research_RevArray_RevArray = R"doc()doc"; + +static const char* __doc_operations_research_RevArray_SetValue = R"doc()doc"; + +static const char* __doc_operations_research_RevArray_Value = R"doc()doc"; + +static const char* __doc_operations_research_RevArray_operator_array = + R"doc()doc"; + +static const char* __doc_operations_research_RevArray_size = R"doc()doc"; + +static const char* __doc_operations_research_RevArray_size_2 = R"doc()doc"; + +static const char* __doc_operations_research_RevArray_stamps = R"doc()doc"; + +static const char* __doc_operations_research_RevArray_values = R"doc()doc"; + +static const char* __doc_operations_research_RevBitMatrix = R"doc()doc"; + +static const char* __doc_operations_research_Rev_Rev = R"doc()doc"; + +static const char* __doc_operations_research_Rev_SetValue = R"doc()doc"; + +static const char* __doc_operations_research_Rev_Value = R"doc()doc"; + +static const char* __doc_operations_research_Rev_stamp = R"doc()doc"; + +static const char* __doc_operations_research_Rev_value = R"doc()doc"; + +static const char* __doc_operations_research_Search = R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit = + R"doc(Base class of all search limits.)doc"; + +static const char* __doc_operations_research_SearchLimit_2 = + R"doc(Base class of all search limits.)doc"; + +static const char* __doc_operations_research_SearchLimit_BeginNextDecision = + R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit_Check = + R"doc(This method is called to check the status of the limit. A return value +of true indicates that we have indeed crossed the limit. In that case, +this method will not be called again and the remaining search will be +discarded.)doc"; + +static const char* __doc_operations_research_SearchLimit_CheckWithOffset = + R"doc(Same as Check() but adds the 'offset' value to the current time when +time is considered in the limit.)doc"; + +static const char* __doc_operations_research_SearchLimit_Copy = + R"doc(Copy a limit. Warning: leads to a direct (no check) downcasting of +'limit' so one needs to be sure both SearchLimits are of the same +type.)doc"; + +static const char* __doc_operations_research_SearchLimit_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit_EnterSearch = + R"doc(Internal methods.)doc"; + +static const char* __doc_operations_research_SearchLimit_Init = + R"doc(This method is called when the search limit is initialized.)doc"; + +static const char* __doc_operations_research_SearchLimit_Install = R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit_MakeClone = + R"doc(Allocates a clone of the limit.)doc"; + +static const char* __doc_operations_research_SearchLimit_PeriodicCheck = + R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit_RefuteDecision = + R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit_SearchLimit = + R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit_SearchLimit_2 = + R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit_TopPeriodicCheck = + R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit_crossed = + R"doc(Returns true if the limit has been crossed.)doc"; + +static const char* __doc_operations_research_SearchLimit_crossed_2 = + R"doc()doc"; + +static const char* __doc_operations_research_SearchLimit_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_SearchMonitor = + R"doc(A search monitor is a simple set of callbacks to monitor all search +events)doc"; + +static const char* __doc_operations_research_SearchMonitor_2 = + R"doc(A search monitor is a simple set of callbacks to monitor all search +events)doc"; + +static const char* __doc_operations_research_SearchMonitor_Accept = + R"doc(Accepts the given model visitor.)doc"; + +static const char* __doc_operations_research_SearchMonitor_AcceptDelta = + R"doc()doc"; + +static const char* __doc_operations_research_SearchMonitor_AcceptNeighbor = + R"doc(After accepting a neighbor during local search.)doc"; + +static const char* __doc_operations_research_SearchMonitor_AcceptSolution = + R"doc(This method is called when a solution is found. It asserts whether the +solution is valid. A value of false indicates that the solution should +be discarded.)doc"; + +static const char* + __doc_operations_research_SearchMonitor_AcceptUncheckedNeighbor = + R"doc(After accepting an unchecked neighbor during local search.)doc"; + +static const char* __doc_operations_research_SearchMonitor_AfterDecision = + R"doc(Just after refuting or applying the decision, apply is true after +Apply. This is called only if the Apply() or Refute() methods have not +failed.)doc"; + +static const char* __doc_operations_research_SearchMonitor_ApplyDecision = + R"doc(Before applying the decision.)doc"; + +static const char* __doc_operations_research_SearchMonitor_AtSolution = + R"doc(This method is called when a valid solution is found. If the return +value is true, then search will resume after. If the result is false, +then search will stop there.)doc"; + +static const char* __doc_operations_research_SearchMonitor_BeginFail = + R"doc(Just when the failure occurs.)doc"; + +static const char* + __doc_operations_research_SearchMonitor_BeginInitialPropagation = + R"doc(Before the initial propagation.)doc"; + +static const char* __doc_operations_research_SearchMonitor_BeginNextDecision = + R"doc(Before calling DecisionBuilder::Next.)doc"; + +static const char* __doc_operations_research_SearchMonitor_EndFail = + R"doc(After completing the backtrack.)doc"; + +static const char* + __doc_operations_research_SearchMonitor_EndInitialPropagation = + R"doc(After the initial propagation.)doc"; + +static const char* __doc_operations_research_SearchMonitor_EndNextDecision = + R"doc(After calling DecisionBuilder::Next, along with the returned decision.)doc"; + +static const char* __doc_operations_research_SearchMonitor_EnterSearch = + R"doc(Beginning of the search.)doc"; + +static const char* __doc_operations_research_SearchMonitor_ExitSearch = + R"doc(End of the search.)doc"; + +static const char* __doc_operations_research_SearchMonitor_Install = + R"doc(Registers itself on the solver such that it gets notified of the +search and propagation events. Override to incrementally install +listeners for specific events.)doc"; + +static const char* + __doc_operations_research_SearchMonitor_IsUncheckedSolutionLimitReached = + R"doc(Returns true if the limit of solutions has been reached including +unchecked solutions.)doc"; + +static const char* __doc_operations_research_SearchMonitor_ListenToEvent = + R"doc()doc"; + +static const char* __doc_operations_research_SearchMonitor_LocalOptimum = + R"doc(When a local optimum is reached. If 'true' is returned, the last +solution is discarded and the search proceeds with the next one.)doc"; + +static const char* __doc_operations_research_SearchMonitor_NoMoreSolutions = + R"doc(When the search tree is finished.)doc"; + +static const char* __doc_operations_research_SearchMonitor_PeriodicCheck = + R"doc(Periodic call to check limits in long running methods.)doc"; + +static const char* __doc_operations_research_SearchMonitor_ProgressPercent = + R"doc(Returns a percentage representing the propress of the search before +reaching limits.)doc"; + +static const char* __doc_operations_research_SearchMonitor_RefuteDecision = + R"doc(Before refuting the decision.)doc"; + +static const char* __doc_operations_research_SearchMonitor_RestartSearch = + R"doc(Restart the search.)doc"; + +static const char* __doc_operations_research_SearchMonitor_SearchMonitor = + R"doc()doc"; + +static const char* __doc_operations_research_SearchMonitor_SearchMonitor_2 = + R"doc()doc"; + +static const char* __doc_operations_research_SearchMonitor_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_SearchMonitor_solver = R"doc()doc"; + +static const char* __doc_operations_research_SearchMonitor_solver_2 = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVar = + R"doc(A sequence variable is a variable whose domain is a set of possible +orderings of the interval variables. It allows ordering of tasks. It +has two sets of methods: ComputePossibleFirstsAndLasts(), which +returns the list of interval variables that can be ranked first or +last; and RankFirst/RankNotFirst/RankLast/RankNotLast, which can be +used to create the search decision.)doc"; + +static const char* __doc_operations_research_SequenceVar_2 = + R"doc(A sequence variable is a variable whose domain is a set of possible +orderings of the interval variables. It allows ordering of tasks. It +has two sets of methods: ComputePossibleFirstsAndLasts(), which +returns the list of interval variables that can be ranked first or +last; and RankFirst/RankNotFirst/RankLast/RankNotLast, which can be +used to create the search decision.)doc"; + +static const char* __doc_operations_research_SequenceVarAssignment = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement = + R"doc(The SequenceVarElement stores a partial representation of ranked +interval variables in the underlying sequence variable. This +representation consists of three vectors: - the forward sequence. That +is the list of interval variables ranked first in the sequence. The +first element of the backward sequence is the first interval in the +sequence variable. - the backward sequence. That is the list of +interval variables ranked last in the sequence. The first element of +the backward sequence is the last interval in the sequence variable. - +The list of unperformed interval variables. Furthermore, if all +performed variables are ranked, then by convention, the +forward_sequence will contain all such variables and the +backward_sequence will be empty.)doc"; + +static const char* + __doc_operations_research_SequenceVarElement_BackwardSequence = R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_Bound = + R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVarElement_CheckClassInvariants = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_Clone = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_Copy = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_DebugString = + R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVarElement_ForwardSequence = R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_LoadFromProto = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_Reset = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_Restore = + R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVarElement_SequenceVarElement = + R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVarElement_SequenceVarElement_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVarElement_SetBackwardSequence = + R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVarElement_SetForwardSequence = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_SetSequence = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_SetUnperformed = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_Store = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_Unperformed = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_Var = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_WriteToProto = + R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVarElement_backward_sequence = + R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVarElement_forward_sequence = R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_operator_eq = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_operator_ne = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_unperformed = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVarElement_var = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVar_Accept = + R"doc(Accepts the given visitor.)doc"; + +static const char* __doc_operations_research_SequenceVar_ActiveHorizonRange = + R"doc(Returns the minimum start min and the maximum end max of all unranked +interval vars in the sequence.)doc"; + +static const char* + __doc_operations_research_SequenceVar_ComputeBackwardFrontier = R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVar_ComputeForwardFrontier = R"doc()doc"; + +static const char* + __doc_operations_research_SequenceVar_ComputePossibleFirstsAndLasts = + R"doc(Computes the set of indices of interval variables that can be ranked +first in the set of unranked activities.)doc"; + +static const char* __doc_operations_research_SequenceVar_ComputeStatistics = + R"doc(Compute statistics on the sequence.)doc"; + +static const char* __doc_operations_research_SequenceVar_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVar_DurationRange = + R"doc(Returns the minimum and maximum duration of combined interval vars in +the sequence.)doc"; + +static const char* __doc_operations_research_SequenceVar_FillSequence = + R"doc(Clears 'rank_first' and 'rank_last', and fills them with the intervals +in the order of the ranks. If all variables are ranked, 'rank_first' +will contain all variables, and 'rank_last' will contain none. +'unperformed' will contains all such interval variables. rank_first +and rank_last represents different directions. rank_first[0] +corresponds to the first interval of the sequence. rank_last[0] +corresponds to the last interval of the sequence.)doc"; + +static const char* __doc_operations_research_SequenceVar_HorizonRange = + R"doc(Returns the minimum start min and the maximum end max of all interval +vars in the sequence.)doc"; + +static const char* __doc_operations_research_SequenceVar_Interval = + R"doc(Returns the index_th interval of the sequence.)doc"; + +static const char* __doc_operations_research_SequenceVar_Next = + R"doc(Returns the next of the index_th interval of the sequence.)doc"; + +static const char* __doc_operations_research_SequenceVar_RankFirst = + R"doc(Ranks the index_th interval var first of all unranked interval vars. +After that, it will no longer be considered ranked.)doc"; + +static const char* __doc_operations_research_SequenceVar_RankLast = + R"doc(Ranks the index_th interval var first of all unranked interval vars. +After that, it will no longer be considered ranked.)doc"; + +static const char* __doc_operations_research_SequenceVar_RankNotFirst = + R"doc(Indicates that the index_th interval var will not be ranked first of +all currently unranked interval vars.)doc"; + +static const char* __doc_operations_research_SequenceVar_RankNotLast = + R"doc(Indicates that the index_th interval var will not be ranked first of +all currently unranked interval vars.)doc"; + +static const char* __doc_operations_research_SequenceVar_RankSequence = + R"doc(Applies the following sequence of ranks, ranks first, then rank last. +rank_first and rank_last represents different directions. +rank_first[0] corresponds to the first interval of the sequence. +rank_last[0] corresponds to the last interval of the sequence. All +intervals in the unperformed vector will be marked as such.)doc"; + +static const char* __doc_operations_research_SequenceVar_SequenceVar = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVar_UpdatePrevious = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVar_intervals = + R"doc()doc"; + +static const char* __doc_operations_research_SequenceVar_nexts = R"doc()doc"; + +static const char* __doc_operations_research_SequenceVar_previous = R"doc()doc"; + +static const char* __doc_operations_research_SequenceVar_size = + R"doc(Returns the number of interval vars in the sequence.)doc"; + +static const char* __doc_operations_research_SetAssignmentFromAssignment = + R"doc(Given a "source_assignment", clears the "target_assignment" and adds +all IntVars in "target_vars", with the values of the variables set +according to the corresponding values of "source_vars" in +"source_assignment". source_vars and target_vars must have the same +number of elements. The source and target assignments can belong to +different Solvers.)doc"; + +static const char* __doc_operations_research_SimpleRevFIFO = R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector = + R"doc(This class is the root class of all solution collectors. It implements +a basic query API to be used independently of the collector used.)doc"; + +static const char* __doc_operations_research_SolutionCollector_2 = + R"doc(This class is the root class of all solution collectors. It implements +a basic query API to be used independently of the collector used.)doc"; + +static const char* __doc_operations_research_SolutionCollector_Add = + R"doc(Add API.)doc"; + +static const char* __doc_operations_research_SolutionCollector_Add_2 = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_Add_3 = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_Add_4 = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_Add_5 = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_Add_6 = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_AddObjective = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_AddObjectives = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_BackwardSequence = + R"doc(This is a shortcut to get the BackwardSequence of 'var' in the nth +solution. The backward sequence is the list of ranked interval +variables starting from the end of the sequence.)doc"; + +static const char* + __doc_operations_research_SolutionCollector_BuildSolutionDataForCurrentState = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_DebugString = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_DurationValue = + R"doc(This is a shortcut to get the DurationValue of 'var' in the nth +solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_EndValue = + R"doc(This is a shortcut to get the EndValue of 'var' in the nth solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_EnterSearch = + R"doc(Beginning of the search.)doc"; + +static const char* __doc_operations_research_SolutionCollector_ForwardSequence = + R"doc(This is a shortcut to get the ForwardSequence of 'var' in the nth +solution. The forward sequence is the list of ranked interval +variables starting from the start of the sequence.)doc"; + +static const char* __doc_operations_research_SolutionCollector_FreeSolution = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_Install = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_ObjectiveValueFromIndex = + R"doc(Returns the value of the index-th objective of the nth solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_PerformedValue = + R"doc(This is a shortcut to get the PerformedValue of 'var' in the nth +solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_PopSolution = + R"doc(Remove and delete the last popped solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_Push = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_PushSolution = + R"doc(Push the current state as a new solution.)doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionCollector = R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionCollector_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionCollector_3 = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_SolutionData = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionData_ObjectiveValue = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionData_ObjectiveValueFromIndex = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionData_branches = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionData_failures = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionData_operator_lt = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionData_solution = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_SolutionData_time = R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_StartValue = + R"doc(This is a shortcut to get the StartValue of 'var' in the nth solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_Unperformed = + R"doc(This is a shortcut to get the list of unperformed of 'var' in the nth +solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_Value = + R"doc(This is a shortcut to get the Value of 'var' in the nth solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_branches = + R"doc(Returns the number of branches when the nth solution was found.)doc"; + +static const char* __doc_operations_research_SolutionCollector_check_index = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_failures = + R"doc(Returns the number of failures encountered at the time of the nth +solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_has_solution = + R"doc(Returns whether any solutions were stored during the search.)doc"; + +static const char* + __doc_operations_research_SolutionCollector_last_solution_or_null = + R"doc(Returns the last solution if there are any, nullptr otherwise.)doc"; + +static const char* __doc_operations_research_SolutionCollector_objective_value = + R"doc(Returns the objective value of the nth solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_prototype = + R"doc()doc"; + +static const char* + __doc_operations_research_SolutionCollector_recycle_solutions = R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_solution = + R"doc(Returns the nth solution.)doc"; + +static const char* __doc_operations_research_SolutionCollector_solution_count = + R"doc(Returns how many solutions were stored during the search.)doc"; + +static const char* __doc_operations_research_SolutionCollector_solution_data = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_solution_pool = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionCollector_wall_time = + R"doc(Returns the wall time in ms for the nth solution.)doc"; + +static const char* __doc_operations_research_SolutionPool = + R"doc(This class is used to manage a pool of solutions. It can transform a +single point local search into a multipoint local search.)doc"; + +static const char* __doc_operations_research_SolutionPool_2 = + R"doc(This class is used to manage a pool of solutions. It can transform a +single point local search into a multipoint local search.)doc"; + +static const char* __doc_operations_research_SolutionPool_GetNextSolution = + R"doc(This method is called when the local search starts a new neighborhood +to initialize the default assignment.)doc"; + +static const char* __doc_operations_research_SolutionPool_Initialize = + R"doc(This method is called to initialize the solution pool with the +assignment from the local search.)doc"; + +static const char* __doc_operations_research_SolutionPool_RegisterNewSolution = + R"doc(This method is called when a new solution has been accepted by the +local search.)doc"; + +static const char* __doc_operations_research_SolutionPool_SolutionPool = + R"doc()doc"; + +static const char* __doc_operations_research_SolutionPool_SyncNeeded = + R"doc(This method checks if the local solution needs to be updated with an +external one.)doc"; + +static const char* __doc_operations_research_Solver = + R"doc(Solver Class + +A solver represents the main computation engine. It implements the +entire range of Constraint Programming protocols: - Reversibility - +Propagation - Search + +Usually, Constraint Programming code consists of - the creation of the +Solver, - the creation of the decision variables of the model, - the +creation of the constraints of the model and their addition to the +solver() through the AddConstraint() method, - the creation of the +main DecisionBuilder class, - the launch of the solve() method with +the decision builder. + +For the time being, Solver is neither MT_SAFE nor MT_HOT.)doc"; + +static const char* __doc_operations_research_Solver_ABSL_DEPRECATED = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_ABSL_DEPRECATED_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_Accept = + R"doc(Accepts the given model visitor.)doc"; + +static const char* __doc_operations_research_Solver_ActiveSearch = + R"doc(Returns the active search, nullptr outside search.)doc"; + +static const char* __doc_operations_research_Solver_AddBacktrackAction = + R"doc(When SaveValue() is not the best way to go, one can create a +reversible action that will be called upon backtrack. The "fast" +parameter indicates whether we need restore all values saved through +SaveValue() before calling this method.)doc"; + +static const char* __doc_operations_research_Solver_AddCastConstraint = + R"doc(Adds 'constraint' to the solver and marks it as a cast constraint, +that is, a constraint created calling Var() on an expression. This is +used internally.)doc"; + +static const char* __doc_operations_research_Solver_AddConstraint = + R"doc(Adds the constraint 'c' to the model. + +After calling this method, and until there is a backtrack that undoes +the addition, any assignment of variables to values must satisfy the +given constraint in order to be considered feasible. There are two +fairly different use cases: + +- the most common use case is modeling: the given constraint is really +part of the problem that the user is trying to solve. In this use +case, AddConstraint is called outside of search (i.e., with ``state() +== OUTSIDE_SEARCH``). Most users should only use AddConstraint in this +way. In this case, the constraint will belong to the model forever: it +cannot be removed by backtracking. + +- a rarer use case is that 'c' is not a real constraint of the model. +It may be a constraint generated by a branching decision (a constraint +whose goal is to restrict the search space), a symmetry breaking +constraint (a constraint that does restrict the search space, but in a +way that cannot have an impact on the quality of the solutions in the +subtree), or an inferred constraint that, while having no semantic +value to the model (it does not restrict the set of solutions), is +worth having because we believe it may strengthen the propagation. In +these cases, it happens that the constraint is added during the search +(i.e., with state() == IN_SEARCH or state() == IN_ROOT_NODE). When a +constraint is added during a search, it applies only to the subtree of +the search tree rooted at the current node, and will be automatically +removed by backtracking. + +This method does not take ownership of the constraint. If the +constraint has been created by any factory method (Solver::MakeXXX), +it will automatically be deleted. However, power users who implement +their own constraints should do: +solver.AddConstraint(solver.RevAlloc(new MyConstraint(...));)doc"; + +static const char* __doc_operations_research_Solver_AddLocalSearchMonitor = + R"doc(Adds the local search monitor to the solver. This is called internally +when a propagation monitor is passed to the Solve() or NewSearch() +method.)doc"; + +static const char* __doc_operations_research_Solver_AddPropagationMonitor = + R"doc(Adds the propagation monitor to the solver. This is called internally +when a propagation monitor is passed to the Solve() or NewSearch() +method.)doc"; + +static const char* __doc_operations_research_Solver_BacktrackOneLevel = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_BacktrackToSentinel = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_BinaryIntervalRelation = + R"doc(This enum is used in Solver::MakeIntervalVarRelation to specify the +temporal relation between the two intervals t1 and t2.)doc"; + +static const char* + __doc_operations_research_Solver_BinaryIntervalRelation_ENDS_AFTER_END = + R"doc(t1 ends after t2 end, i.e. End(t1) >= End(t2) + delay.)doc"; + +static const char* + __doc_operations_research_Solver_BinaryIntervalRelation_ENDS_AFTER_START = + R"doc(t1 ends after t2 start, i.e. End(t1) >= Start(t2) + delay.)doc"; + +static const char* + __doc_operations_research_Solver_BinaryIntervalRelation_ENDS_AT_END = + R"doc(t1 ends at t2 end, i.e. End(t1) == End(t2) + delay.)doc"; + +static const char* + __doc_operations_research_Solver_BinaryIntervalRelation_ENDS_AT_START = + R"doc(t1 ends at t2 start, i.e. End(t1) == Start(t2) + delay.)doc"; + +static const char* + __doc_operations_research_Solver_BinaryIntervalRelation_STARTS_AFTER_END = + R"doc(t1 starts after t2 end, i.e. Start(t1) >= End(t2) + delay.)doc"; + +static const char* + __doc_operations_research_Solver_BinaryIntervalRelation_STARTS_AFTER_START = + R"doc(t1 starts after t2 start, i.e. Start(t1) >= Start(t2) + delay.)doc"; + +static const char* + __doc_operations_research_Solver_BinaryIntervalRelation_STARTS_AT_END = + R"doc(t1 starts at t2 end, i.e. Start(t1) == End(t2) + delay.)doc"; + +static const char* + __doc_operations_research_Solver_BinaryIntervalRelation_STARTS_AT_START = + R"doc(t1 starts at t2 start, i.e. Start(t1) == Start(t2) + delay.)doc"; + +static const char* + __doc_operations_research_Solver_BinaryIntervalRelation_STAYS_IN_SYNC = + R"doc(STARTS_AT_START and ENDS_AT_END at the same time. t1 starts at t2 +start, i.e. Start(t1) == Start(t2) + delay. t1 ends at t2 end, i.e. +End(t1) == End(t2).)doc"; + +static const char* __doc_operations_research_Solver_Cache = + R"doc(Returns the cache of the model.)doc"; + +static const char* __doc_operations_research_Solver_CastExpression = + R"doc(Internal. If the variables is the result of expr->Var(), this method +returns expr, nullptr otherwise.)doc"; + +static const char* __doc_operations_research_Solver_CheckAssignment = + R"doc(Checks whether the given assignment satisfies all relevant +constraints.)doc"; + +static const char* __doc_operations_research_Solver_CheckConstraint = + R"doc(Checks whether adding this constraint will lead to an immediate +failure. It will return false if the model is already inconsistent, or +if adding the constraint makes it inconsistent.)doc"; + +static const char* __doc_operations_research_Solver_CheckFail = R"doc()doc"; + +static const char* __doc_operations_research_Solver_ClearLocalSearchState = + R"doc(Clears the local search state.)doc"; + +static const char* __doc_operations_research_Solver_ClearNeighbors = + R"doc(Manipulate neighbors count; to be used for testing purposes only. +TODO(user): Find a workaround to avoid exposing this.)doc"; + +static const char* __doc_operations_research_Solver_Compose = + R"doc(Creates a decision builder which sequentially composes decision +builders. At each leaf of a decision builder, the next decision +builder is therefore called. For instance, Compose(db1, db2) will +result in the following tree: d1 tree | / | \ | db1 leaves | / | \ | +db2 tree db2 tree db2 tree |)doc"; + +static const char* __doc_operations_research_Solver_Compose_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Compose_3 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Compose_4 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_ConcatenateOperators = + R"doc(Creates a local search operator which concatenates a vector of +operators. Each operator from the vector is called sequentially. By +default, when a neighbor is found the neighborhood exploration +restarts from the last active operator (the one which produced the +neighbor). This can be overridden by setting restart to true to force +the exploration to start from the first operator in the vector. + +The default behavior can also be overridden using an evaluation +callback to set the order in which the operators are explored (the +callback is called in LocalSearchOperator::Start()). The first +argument of the callback is the index of the operator which produced +the last move, the second argument is the index of the operator to be +evaluated. Ownership of the callback is taken by ConcatenateOperators. + +Example: + +const int kPriorities = {10, 100, 10, 0}; int64_t Evaluate(int +active_operator, int current_operator) { return +kPriorities[current_operator]; } + +LocalSearchOperator* concat = solver.ConcatenateOperators(operators, +NewPermanentCallback(&Evaluate)); + +The elements of the vector operators will be sorted by increasing +priority and explored in that order (tie-breaks are handled by keeping +the relative operator order in the vector). This would result in the +following order: operators[3], operators[0], operators[2], +operators[1].)doc"; + +static const char* __doc_operations_research_Solver_ConcatenateOperators_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_ConcatenateOperators_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_CurrentlyInSolve = + R"doc(Returns true whether the current search has been created using a +Solve() call instead of a NewSearch one. It returns false if the +solver is not in search at all.)doc"; + +static const char* __doc_operations_research_Solver_DebugString = + R"doc(misc debug string.)doc"; + +static const char* __doc_operations_research_Solver_DecisionModification = + R"doc(The Solver is responsible for creating the search tree. Thanks to the +DecisionBuilder, it creates a new decision with two branches at each +node: left and right. The DecisionModification enum is used to specify +how the branch selector should behave.)doc"; + +static const char* + __doc_operations_research_Solver_DecisionModification_KEEP_LEFT = + R"doc(Right branches are ignored. This is used to make the code faster when +backtrack makes no sense or is not useful. This is faster as there is +no need to create one new node per decision.)doc"; + +static const char* + __doc_operations_research_Solver_DecisionModification_KEEP_RIGHT = + R"doc(Left branches are ignored. This is used to make the code faster when +backtrack makes no sense or is not useful. This is faster as there is +no need to create one new node per decision.)doc"; + +static const char* + __doc_operations_research_Solver_DecisionModification_KILL_BOTH = + R"doc(Backtracks to the previous decisions, i.e. left and right branches are +not applied.)doc"; + +static const char* + __doc_operations_research_Solver_DecisionModification_NO_CHANGE = + R"doc(Keeps the default behavior, i.e. apply left branch first, and then +right branch in case of backtracking.)doc"; + +static const char* + __doc_operations_research_Solver_DecisionModification_SWITCH_BRANCHES = + R"doc(Applies right branch first. Left branch will be applied in case of +backtracking.)doc"; + +static const char* __doc_operations_research_Solver_DefaultSolverParameters = + R"doc(Create a ConstraintSolverParameters proto with all the default values.)doc"; + +static const char* __doc_operations_research_Solver_DemonPriority = + R"doc(This enum represents the three possible priorities for a demon in the +Solver queue. Note: this is for advanced users only.)doc"; + +static const char* + __doc_operations_research_Solver_DemonPriority_DELAYED_PRIORITY = + R"doc(DELAYED_PRIORITY is the lowest priority: Demons will be processed +after VAR_PRIORITY and NORMAL_PRIORITY demons.)doc"; + +static const char* + __doc_operations_research_Solver_DemonPriority_NORMAL_PRIORITY = + R"doc(NORMAL_PRIORITY is the highest priority: Demons will be processed +first.)doc"; + +static const char* __doc_operations_research_Solver_DemonPriority_VAR_PRIORITY = + R"doc(VAR_PRIORITY is between DELAYED_PRIORITY and NORMAL_PRIORITY.)doc"; + +static const char* __doc_operations_research_Solver_EndSearch = R"doc()doc"; + +static const char* __doc_operations_research_Solver_EnqueueAll = R"doc()doc"; + +static const char* __doc_operations_research_Solver_EnqueueDelayedDemon = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_EnqueueVar = R"doc()doc"; + +static const char* __doc_operations_research_Solver_EvaluatorLocalSearchOperators = + R"doc(This enum is used in Solver::MakeOperator associated with an evaluator +to specify the neighborhood to create.)doc"; + +static const char* + __doc_operations_research_Solver_EvaluatorLocalSearchOperators_LK = + R"doc(Lin-Kernighan local search. While the accumulated local gain is +positive, perform a 2opt or a 3opt move followed by a series of 2opt +moves. Return a neighbor for which the global gain is positive.)doc"; + +static const char* + __doc_operations_research_Solver_EvaluatorLocalSearchOperators_TSPLNS = + R"doc(TSP-base LNS. Randomly merge consecutive nodes until n "meta"-nodes +remain and solve the corresponding TSP. This is an "unlimited" +neighborhood which must be stopped by search limits. To force +diversification, the operator iteratively forces each node to serve as +base of a meta-node.)doc"; + +static const char* + __doc_operations_research_Solver_EvaluatorLocalSearchOperators_TSPOPT = + R"doc(Sliding TSP operator. Uses an exact dynamic programming algorithm to +solve the TSP corresponding to path sub-chains. For a subchain 1 -> 2 +-> 3 -> 4 -> 5 -> 6, solves the TSP on nodes A, 2, 3, 4, 5, where A is +a merger of nodes 1 and 6 such that cost(A,i) = cost(1,i) and +cost(i,A) = cost(i,6).)doc"; + +static const char* __doc_operations_research_Solver_EvaluatorStrategy = + R"doc(This enum is used by Solver::MakePhase to specify how to select +variables and values during the search. In Solver::MakePhase(const +std::vector&, IntVarStrategy, IntValueStrategy), variables +are selected first, and then the associated value. In +Solver::MakePhase(const std::vector& vars, IndexEvaluator2, +EvaluatorStrategy), the selection is done scanning every pair +. The next selected pair is then the best +among all possibilities, i.e. the pair with the smallest evaluation. +As this is costly, two options are offered: static or dynamic +evaluation.)doc"; + +static const char* + __doc_operations_research_Solver_EvaluatorStrategy_CHOOSE_DYNAMIC_GLOBAL_BEST = + R"doc(Pairs are compared each time a variable is selected. That way all +pairs are relevant and evaluation is accurate. This strategy runs in +O(number-of-pairs) at each variable selection, versus O(1) in the +static version.)doc"; + +static const char* + __doc_operations_research_Solver_EvaluatorStrategy_CHOOSE_STATIC_GLOBAL_BEST = + R"doc(Pairs are compared at the first call of the selector, and results are +cached. Next calls to the selector use the previous computation, and +so are not up-to-date, e.g. some pairs may not be +possible anymore due to propagation since the first to call.)doc"; + +static const char* __doc_operations_research_Solver_ExecuteAll = R"doc()doc"; + +static const char* __doc_operations_research_Solver_ExportProfilingOverview = + R"doc(Exports the profiling information in a human readable overview. The +parameter profile_level used to create the solver must be set to true.)doc"; + +static const char* __doc_operations_research_Solver_Fail = + R"doc(Abandon the current branch in the search tree. A backtrack will +follow.)doc"; + +static const char* __doc_operations_research_Solver_FinishCurrentSearch = + R"doc(Tells the solver to kill or restart the current search.)doc"; + +static const char* __doc_operations_research_Solver_FreezeQueue = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_GetConstraintSolverStatistics = + R"doc(Returns detailed cp search statistics.)doc"; + +static const char* __doc_operations_research_Solver_GetLocalSearchMonitor = + R"doc(Returns the local search monitor.)doc"; + +static const char* __doc_operations_research_Solver_GetLocalSearchStatistics = + R"doc(Returns detailed local search statistics.)doc"; + +static const char* __doc_operations_research_Solver_GetName = R"doc(Naming)doc"; + +static const char* __doc_operations_research_Solver_GetNewIntVarIndex = + R"doc(Variable indexing (note that indexing is not reversible). Returns a +new index for an IntVar.)doc"; + +static const char* + __doc_operations_research_Solver_GetOrCreateLocalSearchState = + R"doc(Returns (or creates) an assignment representing the state of local +search.)doc"; + +static const char* __doc_operations_research_Solver_GetPropagationMonitor = + R"doc(Returns the propagation monitor.)doc"; + +static const char* __doc_operations_research_Solver_HasName = + R"doc(Returns whether the object has been named or not.)doc"; + +static const char* __doc_operations_research_Solver_ImprovementSearchLimit = + R"doc(Limits the search based on the improvements of 'objective_var'. Stops +the search when the improvement rate gets lower than a threshold +value. This threshold value is computed based on the improvement rate +during the first phase of the search.)doc"; + +static const char* __doc_operations_research_Solver_IncrementNeighbors = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_IncrementUncheckedSolutionCounter = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_Init = R"doc()doc"; + +static const char* __doc_operations_research_Solver_InitCachedConstraint = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_InitCachedIntConstants = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_InstrumentsDemons = + R"doc(Returns whether we are instrumenting demons.)doc"; + +static const char* __doc_operations_research_Solver_InstrumentsVariables = + R"doc(Returns whether we are tracing variables.)doc"; + +static const char* __doc_operations_research_Solver_IntValueStrategy = + R"doc(This enum describes the strategy used to select the next variable +value to set.)doc"; + +static const char* + __doc_operations_research_Solver_IntValueStrategy_ASSIGN_CENTER_VALUE = + R"doc(Selects the first possible value which is the closest to the center of +the domain of the selected variable. The center is defined as (min + +max) / 2.)doc"; + +static const char* + __doc_operations_research_Solver_IntValueStrategy_ASSIGN_MAX_VALUE = + R"doc(Selects the max value of the selected variable.)doc"; + +static const char* + __doc_operations_research_Solver_IntValueStrategy_ASSIGN_MIN_VALUE = + R"doc(Selects the min value of the selected variable.)doc"; + +static const char* + __doc_operations_research_Solver_IntValueStrategy_ASSIGN_RANDOM_VALUE = + R"doc(Selects randomly one of the possible values of the selected variable.)doc"; + +static const char* + __doc_operations_research_Solver_IntValueStrategy_INT_VALUE_DEFAULT = + R"doc(The default behavior is ASSIGN_MIN_VALUE.)doc"; + +static const char* + __doc_operations_research_Solver_IntValueStrategy_INT_VALUE_SIMPLE = + R"doc(The simple selection is ASSIGN_MIN_VALUE.)doc"; + +static const char* + __doc_operations_research_Solver_IntValueStrategy_SPLIT_LOWER_HALF = + R"doc(Split the domain in two around the center, and choose the lower part +first.)doc"; + +static const char* + __doc_operations_research_Solver_IntValueStrategy_SPLIT_UPPER_HALF = + R"doc(Split the domain in two around the center, and choose the lower part +first.)doc"; + +static const char* __doc_operations_research_Solver_IntVarStrategy = + R"doc(This enum describes the strategy used to select the next branching +variable at each node during the search.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_FIRST_UNBOUND = + R"doc(Select the first unbound variable. Variables are considered in the +order of the vector of IntVars used to create the selector.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_HIGHEST_MAX = + R"doc(Among unbound variables, select the variable with the highest maximal +value. In case of a tie, the first one is selected, first being +defined by the order in the vector of IntVars used to create the +selector.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_LOWEST_MIN = + R"doc(Among unbound variables, select the variable with the smallest minimal +value. In case of a tie, the first one is selected, "first" defined by +the order in the vector of IntVars used to create the selector.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_MAX_REGRET_ON_MIN = + R"doc(Among unbound variables, select the variable with the largest gap +between the first and the second values of the domain.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_MAX_SIZE = + R"doc(Among unbound variables, select the variable with the highest size. In +case of a tie, the first one is selected, first being defined by the +order in the vector of IntVars used to create the selector.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_MIN_SIZE = + R"doc(Among unbound variables, select the variable with the smallest size. +In case of a tie, the first one is selected, first being defined by +the order in the vector of IntVars used to create the selector.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_MIN_SIZE_HIGHEST_MAX = + R"doc(Among unbound variables, select the variable with the smallest size, +i.e., the smallest number of possible values. In case of a tie, the +selected variable is the one with the highest max value. In case of a +tie, the first one is selected, first being defined by the order in +the vector of IntVars used to create the selector.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_MIN_SIZE_HIGHEST_MIN = + R"doc(Among unbound variables, select the variable with the smallest size, +i.e., the smallest number of possible values. In case of a tie, the +selected variable is the one with the highest min value. In case of a +tie, the first one is selected, first being defined by the order in +the vector of IntVars used to create the selector.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_MIN_SIZE_LOWEST_MAX = + R"doc(Among unbound variables, select the variable with the smallest size, +i.e., the smallest number of possible values. In case of a tie, the +selected variables is the one with the lowest max value. In case of a +tie, the first one is selected, first being defined by the order in +the vector of IntVars used to create the selector.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_MIN_SIZE_LOWEST_MIN = + R"doc(Among unbound variables, select the variable with the smallest size, +i.e., the smallest number of possible values. In case of a tie, the +selected variables is the one with the lowest min value. In case of a +tie, the first one is selected, first being defined by the order in +the vector of IntVars used to create the selector.)doc"; + +static const char* __doc_operations_research_Solver_IntVarStrategy_CHOOSE_PATH = + R"doc(Selects the next unbound variable on a path, the path being defined by +the variables: var[i] corresponds to the index of the next of i.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_CHOOSE_RANDOM = + R"doc(Randomly select one of the remaining unbound variables.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_INT_VAR_DEFAULT = + R"doc(The default behavior is CHOOSE_FIRST_UNBOUND.)doc"; + +static const char* + __doc_operations_research_Solver_IntVarStrategy_INT_VAR_SIMPLE = + R"doc(The simple selection is CHOOSE_FIRST_UNBOUND.)doc"; + +static const char* __doc_operations_research_Solver_IntegerCastInfo = + R"doc(Holds semantic information stating that the 'expression' has been cast +into 'variable' using the Var() method, and that 'maintainer' is +responsible for maintaining the equality between 'variable' and +'expression'.)doc"; + +static const char* + __doc_operations_research_Solver_IntegerCastInfo_IntegerCastInfo = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_IntegerCastInfo_IntegerCastInfo_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_IntegerCastInfo_expression = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_IntegerCastInfo_maintainer = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_IntegerCastInfo_variable = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_InternalSaveValue = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_InternalSaveValue_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_InternalSaveValue_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_InternalSaveValue_4 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_InternalSaveValue_5 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_InternalSaveValue_6 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_InternalSaveValue_7 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_IntervalStrategy = + R"doc(This enum describes the straregy used to select the next interval +variable and its value to be fixed.)doc"; + +static const char* + __doc_operations_research_Solver_IntervalStrategy_INTERVAL_DEFAULT = + R"doc(The default is INTERVAL_SET_TIMES_FORWARD.)doc"; + +static const char* + __doc_operations_research_Solver_IntervalStrategy_INTERVAL_SET_TIMES_BACKWARD = + R"doc(Selects the variable with the highest ending time of all variables, +and fixes the ending time to this highest values.)doc"; + +static const char* + __doc_operations_research_Solver_IntervalStrategy_INTERVAL_SET_TIMES_FORWARD = + R"doc(Selects the variable with the lowest starting time of all variables, +and fixes its starting time to this lowest value.)doc"; + +static const char* + __doc_operations_research_Solver_IntervalStrategy_INTERVAL_SIMPLE = + R"doc(The simple is INTERVAL_SET_TIMES_FORWARD.)doc"; + +static const char* __doc_operations_research_Solver_IsADifference = + R"doc(Internal.)doc"; + +static const char* __doc_operations_research_Solver_IsBooleanVar = + R"doc(Returns true if expr represents either boolean_var or 1 - boolean_var. +In that case, it fills inner_var and is_negated to be true if the +expression is 1 - boolean_var -- equivalent to not(boolean_var).)doc"; + +static const char* + __doc_operations_research_Solver_IsLocalSearchProfilingEnabled = + R"doc(Returns whether we are profiling local search.)doc"; + +static const char* __doc_operations_research_Solver_IsProduct = + R"doc(Returns true if expr represents a product of a expr and a constant. In +that case, it fills inner_expr and coefficient with these, and returns +true. In the other case, it fills inner_expr with expr, coefficient +with 1, and returns false.)doc"; + +static const char* __doc_operations_research_Solver_IsProfilingEnabled = + R"doc(Returns whether we are profiling the solver.)doc"; + +static const char* + __doc_operations_research_Solver_IsUncheckedSolutionLimitReached = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_JumpToSentinel = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_JumpToSentinelWhenNested = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_LocalSearchFilterBound = + R"doc(This enum is used in Solver::MakeLocalSearchObjectiveFilter. It +specifies the behavior of the objective filter to create. The goal is +to define under which condition a move is accepted based on the +current objective value.)doc"; + +static const char* __doc_operations_research_Solver_LocalSearchFilterBound_EQ = + R"doc(Move is accepted when the current objective value is in the interval +objective.Min .. objective.Max.)doc"; + +static const char* __doc_operations_research_Solver_LocalSearchFilterBound_GE = + R"doc(Move is accepted when the current objective value >= objective.Min.)doc"; + +static const char* __doc_operations_research_Solver_LocalSearchFilterBound_LE = + R"doc(Move is accepted when the current objective value <= objective.Max.)doc"; + +static const char* __doc_operations_research_Solver_LocalSearchOperators = + R"doc(This enum is used in Solver::MakeOperator to specify the neighborhood +to create.)doc"; + +static const char* __doc_operations_research_Solver_LocalSearchOperators_CROSS = + R"doc(Operator which cross exchanges the starting chains of 2 paths, +including exchanging the whole paths. First and last nodes are not +moved. Possible neighbors for the paths 1 -> 2 -> 3 -> 4 -> 5 and 6 -> +7 -> 8 (where (1, 5) and (6, 8) are first and last nodes of the paths +and can therefore not be moved): 1 -> [7] -> 3 -> 4 -> 5 6 -> [2] -> 8 +1 -> [7] -> 4 -> 5 6 -> [2 -> 3] -> 8 1 -> [7] -> 5 6 -> [2 -> 3 -> 4] +-> 8)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_DECREMENT = + R"doc(Operator which defines a neighborhood to decrement values. The +behavior is the same as INCREMENT, except values are decremented +instead of incremented.)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_EXCHANGE = + R"doc(Operator which exchanges the positions of two nodes. Possible +neighbors for the path 1 -> 2 -> 3 -> 4 -> 5 (where (1, 5) are first +and last nodes of the path and can therefore not be moved): 1 -> [3] +-> [2] -> 4 -> 5 1 -> [4] -> 3 -> [2] -> 5 1 -> 2 -> [4] -> [3] -> 5)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_EXTENDEDSWAPACTIVE = + R"doc(Operator which makes an inactive node active and an active one +inactive. It is similar to SwapActiveOperator except that it tries to +insert the inactive node in all possible positions instead of just the +position of the node made inactive. Possible neighbors for the path 1 +-> 2 -> 3 -> 4 with 5 inactive (where 1 and 4 are first and last nodes +of the path) are: 1 -> [5] -> 3 -> 4 with 2 inactive 1 -> 3 -> [5] -> +4 with 2 inactive 1 -> [5] -> 2 -> 4 with 3 inactive 1 -> 2 -> [5] -> +4 with 3 inactive)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_FULLPATHLNS = + R"doc(Operator which relaxes one entire path and all inactive nodes, thus +defining num_paths neighbors.)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_INCREMENT = + R"doc(Operator which defines one neighbor per variable. Each neighbor tries +to increment by one the value of the corresponding variable. When a +new solution is found the neighborhood is rebuilt from scratch, i.e., +tries to increment values in the variable order. Consider for instance +variables x and y. x is incremented one by one to its max, and when it +is not possible to increment x anymore, y is incremented once. If this +is a solution, then next neighbor tries to increment x.)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_MAKEACTIVE = + R"doc(Operator which inserts an inactive node into a path. Possible +neighbors for the path 1 -> 2 -> 3 -> 4 with 5 inactive (where 1 and 4 +are first and last nodes of the path) are: 1 -> [5] -> 2 -> 3 -> 4 1 +-> 2 -> [5] -> 3 -> 4 1 -> 2 -> 3 -> [5] -> 4)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_MAKECHAININACTIVE = + R"doc(Operator which makes a "chain" of path nodes inactive. Possible +neighbors for the path 1 -> 2 -> 3 -> 4 (where 1 and 4 are first and +last nodes of the path) are: 1 -> 3 -> 4 with 2 inactive 1 -> 2 -> 4 +with 3 inactive 1 -> 4 with 2 and 3 inactive)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_MAKEINACTIVE = + R"doc(Operator which makes path nodes inactive. Possible neighbors for the +path 1 -> 2 -> 3 -> 4 (where 1 and 4 are first and last nodes of the +path) are: 1 -> 3 -> 4 with 2 inactive 1 -> 2 -> 4 with 3 inactive)doc"; + +static const char* __doc_operations_research_Solver_LocalSearchOperators_OROPT = + R"doc(Relocate: OROPT and RELOCATE. Operator which moves a sub-chain of a +path to another position; the specified chain length is the fixed +length of the chains being moved. When this length is 1, the operator +simply moves a node to another position. Possible neighbors for the +path 1 -> 2 -> 3 -> 4 -> 5, for a chain length of 2 (where (1, 5) are +first and last nodes of the path and can therefore not be moved): 1 -> +4 -> [2 -> 3] -> 5 1 -> [3 -> 4] -> 2 -> 5 + +Using Relocate with chain lengths of 1, 2 and 3 together is equivalent +to the OrOpt operator on a path. The OrOpt operator is a limited +version of 3Opt (breaks 3 arcs on a path).)doc"; + +static const char* __doc_operations_research_Solver_LocalSearchOperators_PATHLNS = + R"doc(Operator which relaxes two sub-chains of three consecutive arcs each. +Each sub-chain is defined by a start node and the next three arcs. +Those six arcs are relaxed to build a new neighbor. PATHLNS explores +all possible pairs of starting nodes and so defines n^2 neighbors, n +being the number of nodes. Note that the two sub-chains can be part of +the same path; they even may overlap.)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_RELOCATE = + R"doc(Relocate neighborhood with length of 1 (see OROPT comment).)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_SIMPLELNS = + R"doc(Operator which defines one neighbor per variable. Each neighbor +relaxes one variable. When a new solution is found the neighborhood is +rebuilt from scratch. Consider for instance variables x and y. First x +is relaxed and the solver is looking for the best possible solution +(with only x relaxed). Then y is relaxed, and the solver is looking +for a new solution. If a new solution is found, then the next variable +to be relaxed is x.)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_SWAPACTIVE = + R"doc(Operator which replaces an active node by an inactive one. Possible +neighbors for the path 1 -> 2 -> 3 -> 4 with 5 inactive (where 1 and 4 +are first and last nodes of the path) are: 1 -> [5] -> 3 -> 4 with 2 +inactive 1 -> 2 -> [5] -> 4 with 3 inactive)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_TWOOPT = + R"doc(Operator which reverses a sub-chain of a path. It is called TwoOpt +because it breaks two arcs on the path; resulting paths are called +two-optimal. Possible neighbors for the path 1 -> 2 -> 3 -> 4 -> 5 +(where (1, 5) are first and last nodes of the path and can therefore +not be moved): 1 -> [3 -> 2] -> 4 -> 5 1 -> [4 -> 3 -> 2] -> 5 1 -> 2 +-> [4 -> 3] -> 5)doc"; + +static const char* + __doc_operations_research_Solver_LocalSearchOperators_UNACTIVELNS = + R"doc(Operator which relaxes all inactive nodes and one sub-chain of six +consecutive arcs. That way the path can be improved by inserting +inactive nodes or swapping arcs.)doc"; + +static const char* __doc_operations_research_Solver_LocalSearchProfile = + R"doc(Returns local search profiling information in a human readable format.)doc"; + +static const char* __doc_operations_research_Solver_MakeAbs = R"doc(|expr|)doc"; + +static const char* __doc_operations_research_Solver_MakeAbsEquality = + R"doc(Creates the constraint abs(var) == abs_var.)doc"; + +static const char* __doc_operations_research_Solver_MakeAcceptFilter = + R"doc(Local Search Filters)doc"; + +static const char* __doc_operations_research_Solver_MakeActionDemon = + R"doc(Creates a demon from a callback.)doc"; + +static const char* __doc_operations_research_Solver_MakeAllDifferent = + R"doc(All variables are pairwise different. This corresponds to the stronger +version of the propagation algorithm.)doc"; + +static const char* __doc_operations_research_Solver_MakeAllDifferent_2 = + R"doc(All variables are pairwise different. If 'stronger_propagation' is +true, stronger, and potentially slower propagation will occur. This +API will be deprecated in the future.)doc"; + +static const char* __doc_operations_research_Solver_MakeAllDifferentExcept = + R"doc(All variables are pairwise different, unless they are assigned to the +escape value.)doc"; + +static const char* __doc_operations_research_Solver_MakeAllSolutionCollector = + R"doc(Collect all solutions of the search.)doc"; + +static const char* __doc_operations_research_Solver_MakeAllSolutionCollector_2 = + R"doc(Collect all solutions of the search. The variables will need to be +added later.)doc"; + +static const char* __doc_operations_research_Solver_MakeAllowedAssignments = + R"doc(This method creates a constraint where the graph of the relation +between the variables is given in extension. There are 'arity' +variables involved in the relation and the graph is given by a integer +tuple set.)doc"; + +static const char* __doc_operations_research_Solver_MakeApplyBranchSelector = + R"doc(Creates a decision builder that will set the branch selector.)doc"; + +static const char* __doc_operations_research_Solver_MakeAssignVariableValue = + R"doc(Decisions.)doc"; + +static const char* + __doc_operations_research_Solver_MakeAssignVariableValueOrDoNothing = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeAssignVariableValueOrFail = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeAssignVariablesValues = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeAssignVariablesValuesOrDoNothing = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeAssignVariablesValuesOrFail = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeAssignment = + R"doc(This method creates an empty assignment.)doc"; + +static const char* __doc_operations_research_Solver_MakeAssignment_2 = + R"doc(This method creates an assignment which is a copy of 'a'.)doc"; + +static const char* __doc_operations_research_Solver_MakeAtMost = + R"doc(|{i | vars[i] == value}| <= max_count)doc"; + +static const char* __doc_operations_research_Solver_MakeAtSolutionCallback = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeBestLexicographicValueSolutionCollector = + R"doc(Same as above, but supporting lexicographic objectives; 'maximize' +specifies the optimization direction for each objective in +'assignment'.)doc"; + +static const char* + __doc_operations_research_Solver_MakeBestLexicographicValueSolutionCollector_2 = + R"doc(Same as above, but supporting lexicographic objectives; 'maximize' +specifies the optimization direction for each objective.)doc"; + +static const char* + __doc_operations_research_Solver_MakeBestValueSolutionCollector = + R"doc(Collect the solution corresponding to the optimal value of the +objective of 'assignment'; if 'assignment' does not have an objective +no solution is collected. This collector only collects one solution +corresponding to the best objective value (the first one found).)doc"; + +static const char* + __doc_operations_research_Solver_MakeBestValueSolutionCollector_2 = + R"doc(Collect the solution corresponding to the optimal value of the +objective of the internal assignment; if this assignment does not have +an objective no solution is collected. This collector only collects +one solution corresponding to the best objective value (the first one +found). The variables and objective(s) will need to be added later.)doc"; + +static const char* __doc_operations_research_Solver_MakeBetweenCt = + R"doc((l <= expr <= u))doc"; + +static const char* __doc_operations_research_Solver_MakeBoolVar = + R"doc(MakeBoolVar will create a variable with a {0, 1} domain.)doc"; + +static const char* __doc_operations_research_Solver_MakeBoolVar_2 = + R"doc(MakeBoolVar will create a variable with a {0, 1} domain.)doc"; + +static const char* __doc_operations_research_Solver_MakeBoolVarArray = + R"doc(This method will append the vector vars with 'var_count' boolean +variables having name "name" where is the index of the +variable.)doc"; + +static const char* __doc_operations_research_Solver_MakeBoolVarArray_2 = + R"doc(This method will append the vector vars with 'var_count' boolean +variables having no names.)doc"; + +static const char* __doc_operations_research_Solver_MakeBoolVarArray_3 = + R"doc(Same but allocates an array and returns it.)doc"; + +static const char* __doc_operations_research_Solver_MakeCircuit = + R"doc(Force the "nexts" variable to create a complete Hamiltonian path.)doc"; + +static const char* __doc_operations_research_Solver_MakeClosureDemon = + R"doc(!defined(SWIG) Creates a demon from a closure.)doc"; + +static const char* __doc_operations_research_Solver_MakeConditionalExpression = + R"doc(Conditional Expr condition ? expr : unperformed_value)doc"; + +static const char* __doc_operations_research_Solver_MakeConstantRestart = + R"doc(This search monitor will restart the search periodically after +'frequency' failures.)doc"; + +static const char* __doc_operations_research_Solver_MakeConstraintAdder = + R"doc(Returns a decision builder that will add the given constraint to the +model.)doc"; + +static const char* + __doc_operations_research_Solver_MakeConstraintInitialPropagateCallback = + R"doc(This method is a specialized case of the MakeConstraintDemon method to +call the InitiatePropagate of the constraint 'ct'.)doc"; + +static const char* __doc_operations_research_Solver_MakeConvexPiecewiseExpr = + R"doc(Convex piecewise function.)doc"; + +static const char* __doc_operations_research_Solver_MakeCount = + R"doc(|{i | vars[i] == value}| == max_count)doc"; + +static const char* __doc_operations_research_Solver_MakeCount_2 = + R"doc(|{i | vars[i] == value}| == max_count)doc"; + +static const char* __doc_operations_research_Solver_MakeCover = + R"doc(This constraint states that the target_var is the convex hull of the +intervals. If none of the interval variables is performed, then the +target var is unperformed too. Also, if the target variable is +unperformed, then all the intervals variables are unperformed too.)doc"; + +static const char* __doc_operations_research_Solver_MakeCumulative = + R"doc(This constraint forces that, for any integer t, the sum of the demands +corresponding to an interval containing t does not exceed the given +capacity. + +Intervals and demands should be vectors of equal size. + +Demands should only contain non-negative values. Zero values are +supported, and the corresponding intervals are filtered out, as they +neither impact nor are impacted by this constraint.)doc"; + +static const char* __doc_operations_research_Solver_MakeCumulative_2 = + R"doc(This constraint forces that, for any integer t, the sum of the demands +corresponding to an interval containing t does not exceed the given +capacity. + +Intervals and demands should be vectors of equal size. + +Demands should only contain non-negative values. Zero values are +supported, and the corresponding intervals are filtered out, as they +neither impact nor are impacted by this constraint.)doc"; + +static const char* __doc_operations_research_Solver_MakeCumulative_3 = + R"doc(This constraint forces that, for any integer t, the sum of the demands +corresponding to an interval containing t does not exceed the given +capacity. + +Intervals and demands should be vectors of equal size. + +Demands should only contain non-negative values. Zero values are +supported, and the corresponding intervals are filtered out, as they +neither impact nor are impacted by this constraint.)doc"; + +static const char* __doc_operations_research_Solver_MakeCumulative_4 = + R"doc(This constraint enforces that, for any integer t, the sum of the +demands corresponding to an interval containing t does not exceed the +given capacity. + +Intervals and demands should be vectors of equal size. + +Demands should only contain non-negative values. Zero values are +supported, and the corresponding intervals are filtered out, as they +neither impact nor are impacted by this constraint.)doc"; + +static const char* __doc_operations_research_Solver_MakeCumulative_5 = + R"doc(This constraint enforces that, for any integer t, the sum of demands +corresponding to an interval containing t does not exceed the given +capacity. + +Intervals and demands should be vectors of equal size. + +Demands should be positive.)doc"; + +static const char* __doc_operations_research_Solver_MakeCumulative_6 = + R"doc(This constraint enforces that, for any integer t, the sum of demands +corresponding to an interval containing t does not exceed the given +capacity. + +Intervals and demands should be vectors of equal size. + +Demands should be positive.)doc"; + +static const char* __doc_operations_research_Solver_MakeDecision = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeDecisionBuilderFromAssignment = + R"doc(Returns a decision builder for which the left-most leaf corresponds to +assignment, the rest of the tree being explored using 'db'.)doc"; + +static const char* __doc_operations_research_Solver_MakeDefaultPhase = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeDefaultPhase_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeDefaultRegularLimitParameters = + R"doc(Creates a regular limit proto containing default values.)doc"; + +static const char* __doc_operations_research_Solver_MakeDefaultSolutionPool = + R"doc(Solution Pool.)doc"; + +static const char* + __doc_operations_research_Solver_MakeDelayedConstraintInitialPropagateCallback = + R"doc(This method is a specialized case of the MakeConstraintDemon method to +call the InitiatePropagate of the constraint 'ct' with low priority.)doc"; + +static const char* __doc_operations_research_Solver_MakeDelayedPathCumul = + R"doc(Delayed version of the same constraint: propagation on the nexts +variables is delayed until all constraints have propagated.)doc"; + +static const char* __doc_operations_research_Solver_MakeDeviation = + R"doc(Deviation constraint: sum_i |n * vars[i] - total_sum| <= deviation_var +and sum_i vars[i] == total_sum n = #vars)doc"; + +static const char* __doc_operations_research_Solver_MakeDifference = + R"doc(left - right)doc"; + +static const char* __doc_operations_research_Solver_MakeDifference_2 = + R"doc(value - expr)doc"; + +static const char* __doc_operations_research_Solver_MakeDisjunctiveConstraint = + R"doc(This constraint forces all interval vars into an non-overlapping +sequence. Intervals with zero duration can be scheduled anywhere.)doc"; + +static const char* __doc_operations_research_Solver_MakeDistribute = + R"doc(Aggregated version of count: |{i | v[i] == values[j]}| == cards[j])doc"; + +static const char* __doc_operations_research_Solver_MakeDistribute_2 = + R"doc(Aggregated version of count: |{i | v[i] == values[j]}| == cards[j])doc"; + +static const char* __doc_operations_research_Solver_MakeDistribute_3 = + R"doc(Aggregated version of count: |{i | v[i] == j}| == cards[j])doc"; + +static const char* __doc_operations_research_Solver_MakeDistribute_4 = + R"doc(Aggregated version of count with bounded cardinalities: forall j in 0 +.. card_size - 1: card_min <= |{i | v[i] == j}| <= card_max)doc"; + +static const char* __doc_operations_research_Solver_MakeDistribute_5 = + R"doc(Aggregated version of count with bounded cardinalities: forall j in 0 +.. card_size - 1: card_min[j] <= |{i | v[i] == j}| <= card_max[j])doc"; + +static const char* __doc_operations_research_Solver_MakeDistribute_6 = + R"doc(Aggregated version of count with bounded cardinalities: forall j in 0 +.. card_size - 1: card_min[j] <= |{i | v[i] == j}| <= card_max[j])doc"; + +static const char* __doc_operations_research_Solver_MakeDistribute_7 = + R"doc(Aggregated version of count with bounded cardinalities: forall j in 0 +.. card_size - 1: card_min[j] <= |{i | v[i] == values[j]}| <= +card_max[j])doc"; + +static const char* __doc_operations_research_Solver_MakeDistribute_8 = + R"doc(Aggregated version of count with bounded cardinalities: forall j in 0 +.. card_size - 1: card_min[j] <= |{i | v[i] == values[j]}| <= +card_max[j])doc"; + +static const char* __doc_operations_research_Solver_MakeDiv = + R"doc(expr / value (integer division))doc"; + +static const char* __doc_operations_research_Solver_MakeDiv_2 = + R"doc(numerator / denominator (integer division). Terms need to be positive.)doc"; + +static const char* __doc_operations_research_Solver_MakeElement = + R"doc(values[index])doc"; + +static const char* __doc_operations_research_Solver_MakeElement_2 = + R"doc(values[index])doc"; + +static const char* __doc_operations_research_Solver_MakeElement_3 = + R"doc(Function-based element. The constraint takes ownership of the +callback. The callback must be able to cope with any possible value in +the domain of 'index' (potentially negative ones too).)doc"; + +static const char* __doc_operations_research_Solver_MakeElement_4 = + R"doc(2D version of function-based element expression, values(expr1, expr2).)doc"; + +static const char* __doc_operations_research_Solver_MakeElement_5 = + R"doc(vars[expr])doc"; + +static const char* __doc_operations_research_Solver_MakeElement_6 = + R"doc(vars(argument))doc"; + +static const char* __doc_operations_research_Solver_MakeElementEquality = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeElementEquality_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeElementEquality_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeElementEquality_4 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeEnterSearchCallback = + R"doc(----- Callback-based search monitors -----)doc"; + +static const char* __doc_operations_research_Solver_MakeEquality = + R"doc(left == right)doc"; + +static const char* __doc_operations_research_Solver_MakeEquality_2 = + R"doc(expr == value)doc"; + +static const char* __doc_operations_research_Solver_MakeEquality_3 = + R"doc(expr == value)doc"; + +static const char* __doc_operations_research_Solver_MakeEquality_4 = + R"doc(This constraints states that the two interval variables are equal.)doc"; + +static const char* __doc_operations_research_Solver_MakeExitSearchCallback = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeFailDecision = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeFalseConstraint = + R"doc(This constraint always fails.)doc"; + +static const char* __doc_operations_research_Solver_MakeFalseConstraint_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeFirstSolutionCollector = + R"doc(Collect the first solution of the search.)doc"; + +static const char* __doc_operations_research_Solver_MakeFirstSolutionCollector_2 = + R"doc(Collect the first solution of the search. The variables will need to +be added later.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationEndSyncedOnEndIntervalVar = + R"doc(Creates an interval var with a fixed duration whose end is +synchronized with the end of another interval, with a given offset. +The performed status is also in sync with the performed status of the +given interval variable.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationEndSyncedOnStartIntervalVar = + R"doc(Creates an interval var with a fixed duration whose end is +synchronized with the start of another interval, with a given offset. +The performed status is also in sync with the performed status of the +given interval variable.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationIntervalVar = + R"doc(Creates an interval var with a fixed duration. The duration must be +greater than 0. If optional is true, then the interval can be +performed or unperformed. If optional is false, then the interval is +always performed.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationIntervalVar_2 = + R"doc(Creates a performed interval var with a fixed duration. The duration +must be greater than 0.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationIntervalVar_3 = + R"doc(Creates an interval var with a fixed duration, and performed_variable. +The duration must be greater than 0.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationIntervalVarArray = + R"doc(This method fills the vector with 'count' interval variables built +with the corresponding parameters.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationIntervalVarArray_2 = + R"doc(This method fills the vector with 'count' interval var built with the +corresponding start variables.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationIntervalVarArray_3 = + R"doc(This method fills the vector with interval variables built with the +corresponding start variables.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationIntervalVarArray_4 = + R"doc(This method fills the vector with interval variables built with the +corresponding start variables.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationIntervalVarArray_5 = + R"doc(This method fills the vector with interval variables built with the +corresponding start and performed variables.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationIntervalVarArray_6 = + R"doc(This method fills the vector with interval variables built with the +corresponding start and performed variables.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationStartSyncedOnEndIntervalVar = + R"doc(Creates an interval var with a fixed duration whose start is +synchronized with the end of another interval, with a given offset. +The performed status is also in sync with the performed status of the +given interval variable.)doc"; + +static const char* + __doc_operations_research_Solver_MakeFixedDurationStartSyncedOnStartIntervalVar = + R"doc(Creates an interval var with a fixed duration whose start is +synchronized with the start of another interval, with a given offset. +The performed status is also in sync with the performed status of the +given interval variable.)doc"; + +static const char* __doc_operations_research_Solver_MakeFixedInterval = + R"doc(Creates a fixed and performed interval.)doc"; + +static const char* __doc_operations_research_Solver_MakeGenericTabuSearch = + R"doc(Creates a Tabu Search based on the vars |vars|. A solution is "tabu" +if all the vars in |vars| keep their value.)doc"; + +static const char* __doc_operations_research_Solver_MakeGreater = + R"doc(left > right)doc"; + +static const char* __doc_operations_research_Solver_MakeGreater_2 = + R"doc(expr > value)doc"; + +static const char* __doc_operations_research_Solver_MakeGreater_3 = + R"doc(expr > value)doc"; + +static const char* __doc_operations_research_Solver_MakeGreaterOrEqual = + R"doc(left >= right)doc"; + +static const char* __doc_operations_research_Solver_MakeGreaterOrEqual_2 = + R"doc(expr >= value)doc"; + +static const char* __doc_operations_research_Solver_MakeGreaterOrEqual_3 = + R"doc(expr >= value)doc"; + +static const char* __doc_operations_research_Solver_MakeGuidedLocalSearch = + R"doc(Creates a Guided Local Search monitor. Description here: +http://en.wikipedia.org/wiki/Guided_Local_Search)doc"; + +static const char* __doc_operations_research_Solver_MakeGuidedLocalSearch_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeIfThenElseCt = + R"doc(Special cases with arrays of size two.)doc"; + +static const char* __doc_operations_research_Solver_MakeIndexExpression = + R"doc(Returns the expression expr such that vars[expr] == value. It assumes +that vars are all different.)doc"; + +static const char* __doc_operations_research_Solver_MakeIndexOfConstraint = + R"doc(This constraint is a special case of the element constraint with an +array of integer variables, where the variables are all different and +the index variable is constrained such that vars[index] == target.)doc"; + +static const char* + __doc_operations_research_Solver_MakeIndexOfFirstMaxValueConstraint = + R"doc(Creates a constraint that binds the index variable to the index of the +first variable with the maximum value.)doc"; + +static const char* + __doc_operations_research_Solver_MakeIndexOfFirstMinValueConstraint = + R"doc(Creates a constraint that binds the index variable to the index of the +first variable with the minimum value.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntConst = + R"doc(IntConst will create a constant expression.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntConst_2 = + R"doc(IntConst will create a constant expression.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntVar = + R"doc(MakeIntVar will create the best range based int var for the bounds +given.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntVar_2 = + R"doc(MakeIntVar will create a variable with the given sparse domain.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntVar_3 = + R"doc(MakeIntVar will create a variable with the given sparse domain.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntVar_4 = + R"doc(MakeIntVar will create the best range based int var for the bounds +given.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntVar_5 = + R"doc(MakeIntVar will create a variable with the given sparse domain.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntVar_6 = + R"doc(MakeIntVar will create a variable with the given sparse domain.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntVarArray = + R"doc(This method will append the vector vars with 'var_count' variables +having bounds vmin and vmax and having name "name" where is the +index of the variable.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntVarArray_2 = + R"doc(This method will append the vector vars with 'var_count' variables +having bounds vmin and vmax and having no names.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntVarArray_3 = + R"doc(Same but allocates an array and returns it.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntervalRelaxedMax = + R"doc(Creates and returns an interval variable that wraps around the given +one, relaxing the max start and end. Relaxing means making unbounded +when optional. If the variable is non optional, this method returns +interval_var. + +More precisely, such an interval variable behaves as follows: * When +the underlying must be performed, the returned interval variable +behaves exactly as the underlying; * When the underlying may or may +not be performed, the returned interval variable behaves like the +underlying, except that it is unbounded on the max side; * When the +underlying cannot be performed, the returned interval variable is of +duration 0 and must be performed in an interval unbounded on both +sides. + +This is very useful for implementing propagators that may only modify +the start min or end min.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntervalRelaxedMin = + R"doc(Creates and returns an interval variable that wraps around the given +one, relaxing the min start and end. Relaxing means making unbounded +when optional. If the variable is non-optional, this method returns +interval_var. + +More precisely, such an interval variable behaves as follows: * When +the underlying must be performed, the returned interval variable +behaves exactly as the underlying; * When the underlying may or may +not be performed, the returned interval variable behaves like the +underlying, except that it is unbounded on the min side; * When the +underlying cannot be performed, the returned interval variable is of +duration 0 and must be performed in an interval unbounded on both +sides. + +This is very useful to implement propagators that may only modify the +start max or end max.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntervalVar = + R"doc(Creates an interval var by specifying the bounds on start, duration, +and end.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntervalVarArray = + R"doc(This method fills the vector with 'count' interval var built with the +corresponding parameters.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntervalVarRelation = + R"doc(This method creates a relation between an interval var and a date.)doc"; + +static const char* __doc_operations_research_Solver_MakeIntervalVarRelation_2 = + R"doc(This method creates a relation between two interval vars.)doc"; + +static const char* + __doc_operations_research_Solver_MakeIntervalVarRelationWithDelay = + R"doc(This method creates a relation between two interval vars. The given +delay is added to the second interval. i.e.: t1 STARTS_AFTER_END of t2 +with a delay of 2 means t1 will start at least two units of time after +the end of t2.)doc"; + +static const char* + __doc_operations_research_Solver_MakeInversePermutationConstraint = + R"doc(Creates a constraint that enforces that 'left' and 'right' both +represent permutations of [0..left.size()-1], and that 'right' is the +inverse permutation of 'left', i.e. for all i in [0..left.size()-1], +right[left[i]] = i.)doc"; + +static const char* __doc_operations_research_Solver_MakeIsBetweenCt = + R"doc(b == (l <= expr <= u))doc"; + +static const char* __doc_operations_research_Solver_MakeIsBetweenVar = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeIsDifferentCstCt = + R"doc(boolvar == (var != value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsDifferentCstVar = + R"doc(status var of (var != value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsDifferentCt = + R"doc(b == (v1 != v2))doc"; + +static const char* __doc_operations_research_Solver_MakeIsDifferentVar = + R"doc(status var of (v1 != v2))doc"; + +static const char* __doc_operations_research_Solver_MakeIsEqualCstCt = + R"doc(boolvar == (var == value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsEqualCstVar = + R"doc(status var of (var == value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsEqualCt = + R"doc(b == (v1 == v2))doc"; + +static const char* __doc_operations_research_Solver_MakeIsEqualVar = + R"doc(status var of (v1 == v2))doc"; + +static const char* __doc_operations_research_Solver_MakeIsGreaterCstCt = + R"doc(b == (v > c))doc"; + +static const char* __doc_operations_research_Solver_MakeIsGreaterCstVar = + R"doc(status var of (var > value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsGreaterCt = + R"doc(b == (left > right))doc"; + +static const char* __doc_operations_research_Solver_MakeIsGreaterOrEqualCstCt = + R"doc(boolvar == (var >= value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsGreaterOrEqualCstVar = + R"doc(status var of (var >= value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsGreaterOrEqualCt = + R"doc(b == (left >= right))doc"; + +static const char* __doc_operations_research_Solver_MakeIsGreaterOrEqualVar = + R"doc(status var of (left >= right))doc"; + +static const char* __doc_operations_research_Solver_MakeIsGreaterVar = + R"doc(status var of (left > right))doc"; + +static const char* __doc_operations_research_Solver_MakeIsLessCstCt = + R"doc(b == (v < c))doc"; + +static const char* __doc_operations_research_Solver_MakeIsLessCstVar = + R"doc(status var of (var < value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsLessCt = + R"doc(b == (left < right))doc"; + +static const char* __doc_operations_research_Solver_MakeIsLessOrEqualCstCt = + R"doc(boolvar == (var <= value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsLessOrEqualCstVar = + R"doc(status var of (var <= value))doc"; + +static const char* __doc_operations_research_Solver_MakeIsLessOrEqualCt = + R"doc(b == (left <= right))doc"; + +static const char* __doc_operations_research_Solver_MakeIsLessOrEqualVar = + R"doc(status var of (left <= right))doc"; + +static const char* __doc_operations_research_Solver_MakeIsLessVar = + R"doc(status var of (left < right))doc"; + +static const char* + __doc_operations_research_Solver_MakeIsLexicalLessOrEqualWithOffsetsCt = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeIsMemberCt = + R"doc(boolvar == (expr in set))doc"; + +static const char* __doc_operations_research_Solver_MakeIsMemberCt_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeIsMemberVar = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeIsMemberVar_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeLastSolutionCollector = + R"doc(Collect the last solution of the search.)doc"; + +static const char* __doc_operations_research_Solver_MakeLastSolutionCollector_2 = + R"doc(Collect the last solution of the search. The variables will need to be +added later.)doc"; + +static const char* __doc_operations_research_Solver_MakeLess = + R"doc(left < right)doc"; + +static const char* __doc_operations_research_Solver_MakeLess_2 = + R"doc(expr < value)doc"; + +static const char* __doc_operations_research_Solver_MakeLess_3 = + R"doc(expr < value)doc"; + +static const char* __doc_operations_research_Solver_MakeLessOrEqual = + R"doc(left <= right)doc"; + +static const char* __doc_operations_research_Solver_MakeLessOrEqual_2 = + R"doc(expr <= value)doc"; + +static const char* __doc_operations_research_Solver_MakeLessOrEqual_3 = + R"doc(expr <= value)doc"; + +static const char* __doc_operations_research_Solver_MakeLexicalLess = + R"doc(Creates a constraint that enforces that left is lexicographically less +than right.)doc"; + +static const char* __doc_operations_research_Solver_MakeLexicalLessOrEqual = + R"doc(Creates a constraint that enforces that left is lexicographically less +than or equal to right.)doc"; + +static const char* + __doc_operations_research_Solver_MakeLexicalLessOrEqualWithOffsets = + R"doc(Creates a constraint that enforces that left is lexicographically less +than or equal to right with an offset. This means that for the first +index i such that left[i] is not in [right[i] - (offset[i] - 1), +right[i]], left[i] + offset[i] <= right[i]. Offset values must be > 0.)doc"; + +static const char* __doc_operations_research_Solver_MakeLexicographicOptimize = + R"doc(Creates a lexicographic objective, following the order of the +variables given. Each variable has a corresponding optimization +direction and step.)doc"; + +static const char* + __doc_operations_research_Solver_MakeLexicographicSimulatedAnnealing = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeLexicographicTabuSearch = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeLightElement = + R"doc(Returns a light one-dimension function-based element constraint +ensuring var == values(index). The constraint does not perform bound +reduction of the resulting variable until the index variable is bound. +If deep_serialize returns false, the model visitor will not extract +all possible values from the values function.)doc"; + +static const char* __doc_operations_research_Solver_MakeLightElement_2 = + R"doc(Light two-dimension function-based element constraint ensuring var == +values(index1, index2). The constraint does not perform bound +reduction of the resulting variable until the index variables are +bound. If deep_serialize returns false, the model visitor will not +extract all possible values from the values function.)doc"; + +static const char* __doc_operations_research_Solver_MakeLocalSearchPhase = + R"doc(Local Search decision builders factories. Local search is used to +improve a given solution. This initial solution can be specified +either by an Assignment or by a DecisionBulder, and the corresponding +variables, the initial solution being the first solution found by the +DecisionBuilder. The LocalSearchPhaseParameters parameter holds the +actual definition of the local search phase: - a local search operator +used to explore the neighborhood of the current solution, - a decision +builder to instantiate unbound variables once a neighbor has been +defined; in the case of LNS-based operators instantiates fragment +variables; search monitors can be added to this sub-search by wrapping +the decision builder with MakeSolveOnce. - a search limit specifying +how long local search looks for neighbors before accepting one; the +last neighbor is always taken and in the case of a greedy search, this +corresponds to the best local neighbor; first-accept (which is the +default behavior) can be modeled using a solution found limit of 1, - +a vector of local search filters used to speed up the search by +pruning unfeasible neighbors. Metaheuristics can be added by defining +specialized search monitors; currently down/up-hill climbing is +available through OptimizeVar, as well as Guided Local Search, Tabu +Search and Simulated Annealing.)doc"; + +static const char* __doc_operations_research_Solver_MakeLocalSearchPhase_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeLocalSearchPhase_3 = + R"doc(Variant with a sub_decison_builder specific to the first solution.)doc"; + +static const char* __doc_operations_research_Solver_MakeLocalSearchPhase_4 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeLocalSearchPhaseParameters = + R"doc(Local Search Phase Parameters)doc"; + +static const char* + __doc_operations_research_Solver_MakeLocalSearchPhaseParameters_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeLocalSearchPhaseParameters_3 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeLocalSearchPhaseParameters_4 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeLocalSearchPhaseParameters_5 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeLocalSearchPhaseParameters_6 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeLubyRestart = + R"doc(This search monitor will restart the search periodically. At the +iteration n, it will restart after scale_factor * Luby(n) failures +where Luby is the Luby Strategy (i.e. 1 1 2 1 1 2 4 1 1 2 1 1 2 4 +8...).)doc"; + +static const char* __doc_operations_research_Solver_MakeMapDomain = + R"doc(This constraint maps the domain of 'var' onto the array of variables +'actives'. That is for all i in [0 .. size - 1]: actives[i] == 1 <=> +var->Contains(i);)doc"; + +static const char* __doc_operations_research_Solver_MakeMax = + R"doc(std::max(vars))doc"; + +static const char* __doc_operations_research_Solver_MakeMax_2 = + R"doc(std::max(left, right))doc"; + +static const char* __doc_operations_research_Solver_MakeMax_3 = + R"doc(std::max(expr, value))doc"; + +static const char* __doc_operations_research_Solver_MakeMax_4 = + R"doc(std::max(expr, value))doc"; + +static const char* __doc_operations_research_Solver_MakeMaxEquality = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeMaximize = + R"doc(Creates a maximization objective.)doc"; + +static const char* __doc_operations_research_Solver_MakeMemberCt = + R"doc(expr in set. Propagation is lazy, i.e. this constraint does not +creates holes in the domain of the variable.)doc"; + +static const char* __doc_operations_research_Solver_MakeMemberCt_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeMin = + R"doc(std::min(vars))doc"; + +static const char* __doc_operations_research_Solver_MakeMin_2 = + R"doc(std::min (left, right))doc"; + +static const char* __doc_operations_research_Solver_MakeMin_3 = + R"doc(std::min(expr, value))doc"; + +static const char* __doc_operations_research_Solver_MakeMin_4 = + R"doc(std::min(expr, value))doc"; + +static const char* __doc_operations_research_Solver_MakeMinEquality = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeMinimize = + R"doc(Creates a minimization objective.)doc"; + +static const char* __doc_operations_research_Solver_MakeMirrorInterval = + R"doc(Creates an interval var that is the mirror image of the given one, +that is, the interval var obtained by reversing the axis.)doc"; + +static const char* __doc_operations_research_Solver_MakeModulo = + R"doc(Modulo expression x % mod (with the python convention for modulo).)doc"; + +static const char* __doc_operations_research_Solver_MakeModulo_2 = + R"doc(Modulo expression x % mod (with the python convention for modulo).)doc"; + +static const char* __doc_operations_research_Solver_MakeMonotonicElement = + R"doc(Function based element. The constraint takes ownership of the +callback. The callback must be monotonic. It must be able to cope with +any possible value in the domain of 'index' (potentially negative ones +too). Furtermore, monotonicity is not checked. Thus giving a non- +monotonic function, or specifying an incorrect increasing parameter +will result in undefined behavior.)doc"; + +static const char* __doc_operations_research_Solver_MakeMoveTowardTargetOperator = + R"doc(Creates a local search operator that tries to move the assignment of +some variables toward a target. The target is given as an Assignment. +This operator generates neighbors in which the only difference +compared to the current state is that one variable that belongs to the +target assignment is set to its target value.)doc"; + +static const char* + __doc_operations_research_Solver_MakeMoveTowardTargetOperator_2 = + R"doc(Creates a local search operator that tries to move the assignment of +some variables toward a target. The target is given either as two +vectors: a vector of variables and a vector of associated target +values. The two vectors should be of the same length. This operator +generates neighbors in which the only difference compared to the +current state is that one variable that belongs to the given vector is +set to its target value.)doc"; + +static const char* + __doc_operations_research_Solver_MakeNBestLexicographicValueSolutionCollector = + R"doc(Same as above but supporting lexicographic objectives; 'maximize' +specifies the optimization direction for each objective.)doc"; + +static const char* + __doc_operations_research_Solver_MakeNBestLexicographicValueSolutionCollector_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeNBestValueSolutionCollector = + R"doc(Same as MakeBestValueSolutionCollector but collects the best +solution_count solutions. Collected solutions are sorted in increasing +optimality order (the best solution is the last one).)doc"; + +static const char* + __doc_operations_research_Solver_MakeNBestValueSolutionCollector_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeNeighborhoodLimit = + R"doc(Creates a local search operator that wraps another local search +operator and limits the number of neighbors explored (i.e., calls to +MakeNextNeighbor from the current solution (between two calls to +Start()). When this limit is reached, MakeNextNeighbor() returns +false. The counter is cleared when Start() is called.)doc"; + +static const char* __doc_operations_research_Solver_MakeNestedOptimize = + R"doc(NestedOptimize will collapse a search tree described by a decision +builder 'db' and a set of monitors and wrap it into a single point. If +there are no solutions to this nested tree, then NestedOptimize will +fail. If there are solutions, it will find the best as described by +the mandatory objective in the solution as well as the optimization +direction, instantiate all variables to this solution, and return +nullptr.)doc"; + +static const char* __doc_operations_research_Solver_MakeNestedOptimize_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeNestedOptimize_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeNestedOptimize_4 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeNestedOptimize_5 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeNestedOptimize_6 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeNoCycle = + R"doc(Prevent cycles. The "nexts" variables represent the next in the chain. +"active" variables indicate if the corresponding next variable is +active; this could be useful to model unperformed nodes in a routing +problem. A callback can be added to specify sink values (by default +sink values are values >= vars.size()). Ownership of the callback is +passed to the constraint. If assume_paths is either not specified or +true, the constraint assumes the "nexts" variables represent paths +(and performs a faster propagation); otherwise the constraint assumes +they represent a forest.)doc"; + +static const char* __doc_operations_research_Solver_MakeNoCycle_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeNonEquality = + R"doc(left != right)doc"; + +static const char* __doc_operations_research_Solver_MakeNonEquality_2 = + R"doc(expr != value)doc"; + +static const char* __doc_operations_research_Solver_MakeNonEquality_3 = + R"doc(expr != value)doc"; + +static const char* + __doc_operations_research_Solver_MakeNonOverlappingBoxesConstraint = + R"doc(This constraint states that all the boxes must not overlap. The +coordinates of box i are: (x_vars[i], y_vars[i]), (x_vars[i], +y_vars[i] + y_size[i]), (x_vars[i] + x_size[i], y_vars[i]), (x_vars[i] ++ x_size[i], y_vars[i] + y_size[i]). The sizes must be non-negative. +Boxes with a zero dimension can be pushed like any box.)doc"; + +static const char* + __doc_operations_research_Solver_MakeNonOverlappingBoxesConstraint_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeNonOverlappingBoxesConstraint_3 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeNonOverlappingNonStrictBoxesConstraint = + R"doc(This constraint states that all the boxes must not overlap. The +coordinates of box i are: (x_vars[i], y_vars[i]), (x_vars[i], +y_vars[i] + y_size[i]), (x_vars[i] + x_size[i], y_vars[i]), (x_vars[i] ++ x_size[i], y_vars[i] + y_size[i]). The sizes must be positive. Boxes +with a zero dimension can be placed anywhere.)doc"; + +static const char* + __doc_operations_research_Solver_MakeNonOverlappingNonStrictBoxesConstraint_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeNonOverlappingNonStrictBoxesConstraint_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeNotBetweenCt = + R"doc((expr < l || expr > u) This constraint is lazy as it will not make +holes in the domain of variables. It will propagate only when +expr->Min() >= l or expr->Max() <= u.)doc"; + +static const char* __doc_operations_research_Solver_MakeNotMemberCt = + R"doc(expr not in set.)doc"; + +static const char* __doc_operations_research_Solver_MakeNotMemberCt_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeNotMemberCt_3 = + R"doc(expr should not be in the list of forbidden intervals +[start[i]..end[i]].)doc"; + +static const char* __doc_operations_research_Solver_MakeNotMemberCt_4 = + R"doc(expr should not be in the list of forbidden intervals +[start[i]..end[i]].)doc"; + +static const char* __doc_operations_research_Solver_MakeNotMemberCt_5 = + R"doc(expr should not be in the list of forbidden intervals.)doc"; + +static const char* __doc_operations_research_Solver_MakeNullIntersect = + R"doc(Creates a constraint that states that all variables in the first +vector are different from all variables in the second group. Thus the +set of values in the first vector does not intersect with the set of +values in the second vector.)doc"; + +static const char* __doc_operations_research_Solver_MakeNullIntersectExcept = + R"doc(Creates a constraint that states that all variables in the first +vector are different from all variables from the second group, unless +they are assigned to the escape value. Thus the set of values in the +first vector minus the escape value does not intersect with the set of +values in the second vector.)doc"; + +static const char* __doc_operations_research_Solver_MakeOperator = + R"doc(Local Search Operators.)doc"; + +static const char* __doc_operations_research_Solver_MakeOperator_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeOperator_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeOperator_4 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeOpposite = + R"doc(-expr)doc"; + +static const char* __doc_operations_research_Solver_MakeOptimize = + R"doc(Creates a objective with a given sense (true = maximization).)doc"; + +static const char* __doc_operations_research_Solver_MakePack = + R"doc(This constraint packs all variables onto 'number_of_bins' variables. +For any given variable, a value of 'number_of_bins' indicates that the +variable is not assigned to any bin. Dimensions, i.e., cumulative +constraints on this packing, can be added directly from the pack +class.)doc"; + +static const char* __doc_operations_research_Solver_MakePathConnected = + R"doc(Check whether more propagation is needed.)doc"; + +static const char* __doc_operations_research_Solver_MakePathCumul = + R"doc(Creates a constraint which accumulates values along a path such that: +cumuls[next[i]] = cumuls[i] + transits[i]. Active variables indicate +if the corresponding next variable is active; this could be useful to +model unperformed nodes in a routing problem.)doc"; + +static const char* __doc_operations_research_Solver_MakePathCumul_2 = + R"doc(Creates a constraint which accumulates values along a path such that: +cumuls[next[i]] = cumuls[i] + transit_evaluator(i, next[i]). Active +variables indicate if the corresponding next variable is active; this +could be useful to model unperformed nodes in a routing problem. +Ownership of transit_evaluator is taken and it must be a repeatable +callback.)doc"; + +static const char* __doc_operations_research_Solver_MakePathCumul_3 = + R"doc(Creates a constraint which accumulates values along a path such that: +cumuls[next[i]] = cumuls[i] + transit_evaluator(i, next[i]) + +slacks[i]. Active variables indicate if the corresponding next +variable is active; this could be useful to model unperformed nodes in +a routing problem. Ownership of transit_evaluator is taken and it must +be a repeatable callback.)doc"; + +static const char* + __doc_operations_research_Solver_MakePathEnergyCostConstraint = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakePathPrecedenceConstraint = + R"doc(the implementation can easily be modified to do that; evaluate the +impact on models solved with local search.)doc"; + +static const char* + __doc_operations_research_Solver_MakePathPrecedenceConstraint_2 = + R"doc(Same as MakePathPrecedenceConstraint but ensures precedence pairs on +some paths follow a LIFO or FIFO order. LIFO order: given 2 pairs +(a,b) and (c,d), if a is before c on the path then d must be before b +or b must be before c. FIFO order: given 2 pairs (a,b) and (c,d), if a +is before c on the path then b must be before d. LIFO (resp. FIFO) +orders are enforced only on paths starting by indices in +lifo_path_starts (resp. fifo_path_start).)doc"; + +static const char* + __doc_operations_research_Solver_MakePathTransitPrecedenceConstraint = + R"doc(Same as MakePathPrecedenceConstraint but will force i to be before j +if the sum of transits on the path from i to j is strictly positive.)doc"; + +static const char* __doc_operations_research_Solver_MakePhase = + R"doc(for all other functions that have several homonyms in this .h).)doc"; + +static const char* __doc_operations_research_Solver_MakePhase_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePhase_3 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePhase_4 = + R"doc(var_val1_val2_comparator(var, val1, val2) is true iff assigning value +"val1" to variable "var" is better than assigning value "val2".)doc"; + +static const char* __doc_operations_research_Solver_MakePhase_5 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePhase_6 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePhase_7 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePhase_8 = + R"doc(Shortcuts for small arrays.)doc"; + +static const char* __doc_operations_research_Solver_MakePhase_9 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePhase_10 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePhase_11 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePhase_12 = + R"doc(Returns a decision builder which assigns values to variables which +minimize the values returned by the evaluator. The arguments passed to +the evaluator callback are the indices of the variables in vars and +the values of these variables. Ownership of the callback is passed to +the decision builder.)doc"; + +static const char* __doc_operations_research_Solver_MakePhase_13 = + R"doc(Returns a decision builder which assigns values to variables which +minimize the values returned by the evaluator. In case of tie breaks, +the second callback is used to choose the best index in the array of +equivalent pairs with equivalent evaluations. The arguments passed to +the evaluator callback are the indices of the variables in vars and +the values of these variables. Ownership of the callback is passed to +the decision builder.)doc"; + +static const char* __doc_operations_research_Solver_MakePhase_14 = + R"doc(Scheduling phases.)doc"; + +static const char* __doc_operations_research_Solver_MakePhase_15 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePiecewiseLinearExpr = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakePower = + R"doc(expr ^ n (n > 0))doc"; + +static const char* __doc_operations_research_Solver_MakePrintModelVisitor = + R"doc(Prints the model.)doc"; + +static const char* __doc_operations_research_Solver_MakeProd = + R"doc(left * right)doc"; + +static const char* __doc_operations_research_Solver_MakeProd_2 = + R"doc(expr * value)doc"; + +static const char* + __doc_operations_research_Solver_MakeProfiledDecisionBuilderWrapper = + R"doc(Activates profiling on a decision builder.)doc"; + +static const char* __doc_operations_research_Solver_MakeRandomLnsOperator = + R"doc(Creates a large neighborhood search operator which creates fragments +(set of relaxed variables) with up to number_of_variables random +variables (sampling with replacement is performed meaning that at most +number_of_variables variables are selected). Warning: this operator +will always return neighbors; using it without a search limit will +result in a non-ending search. Optionally a random seed can be +specified.)doc"; + +static const char* __doc_operations_research_Solver_MakeRandomLnsOperator_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeRankFirstInterval = + R"doc(Returns a decision that tries to rank first the ith interval var in +the sequence variable.)doc"; + +static const char* __doc_operations_research_Solver_MakeRankLastInterval = + R"doc(Returns a decision that tries to rank last the ith interval var in the +sequence variable.)doc"; + +static const char* __doc_operations_research_Solver_MakeRejectFilter = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeRestoreAssignment = + R"doc(Returns a DecisionBuilder which restores an Assignment (calls void +Assignment::Restore()))doc"; + +static const char* __doc_operations_research_Solver_MakeScalProd = + R"doc(scalar product)doc"; + +static const char* __doc_operations_research_Solver_MakeScalProd_2 = + R"doc(scalar product)doc"; + +static const char* __doc_operations_research_Solver_MakeScalProdEquality = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeScalProdEquality_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeScalProdEquality_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeScalProdEquality_4 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeScalProdGreaterOrEqual = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeScalProdGreaterOrEqual_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeScalProdLessOrEqual = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeScalProdLessOrEqual_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeScheduleOrExpedite = + R"doc(Returns a decision that tries to schedule a task at a given time. On +the Apply branch, it will set that interval var as performed and set +its end to 'est'. On the Refute branch, it will just update the +'marker' to 'est' - 1. This decision is used in the +INTERVAL_SET_TIMES_BACKWARD strategy.)doc"; + +static const char* __doc_operations_research_Solver_MakeScheduleOrPostpone = + R"doc(Returns a decision that tries to schedule a task at a given time. On +the Apply branch, it will set that interval var as performed and set +its start to 'est'. On the Refute branch, it will just update the +'marker' to 'est' + 1. This decision is used in the +INTERVAL_SET_TIMES_FORWARD strategy.)doc"; + +static const char* __doc_operations_research_Solver_MakeSearchLog = + R"doc(The SearchMonitors below will display a periodic search log on +LOG(INFO) every branch_period branches explored.)doc"; + +static const char* __doc_operations_research_Solver_MakeSearchLog_2 = + R"doc(At each solution, this monitor also display the var value.)doc"; + +static const char* __doc_operations_research_Solver_MakeSearchLog_3 = + R"doc(At each solution, this monitor will also display result of @p +display_callback.)doc"; + +static const char* __doc_operations_research_Solver_MakeSearchLog_4 = + R"doc(At each solution, this monitor will display the 'var' value and the +result of @p display_callback.)doc"; + +static const char* __doc_operations_research_Solver_MakeSearchLog_5 = + R"doc(At each solution, this monitor will display the 'vars' values and the +result of @p display_callback.)doc"; + +static const char* __doc_operations_research_Solver_MakeSearchLog_6 = + R"doc(OptimizeVar Search Logs At each solution, this monitor will also +display the 'opt_var' value.)doc"; + +static const char* __doc_operations_research_Solver_MakeSearchLog_7 = + R"doc(Creates a search monitor that will also print the result of the +display callback.)doc"; + +static const char* __doc_operations_research_Solver_MakeSearchLog_8 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSearchProgressBar = + R"doc(Creates a search monitor tracking the progress of the search in a +progress bar. If a search limit is specified in the search, the bar +shows the progress percentage before reaching the limit. If no limit +is specified, an activity bar is displayed.)doc"; + +static const char* __doc_operations_research_Solver_MakeSearchTrace = + R"doc(Creates a search monitor that will trace precisely the behavior of the +search. Use this only for low level debugging.)doc"; + +static const char* __doc_operations_research_Solver_MakeSemiContinuousExpr = + R"doc(Semi continuous Expression (x <= 0 -> f(x) = 0; x > 0 -> f(x) = ax + +b) a >= 0 and b >= 0)doc"; + +static const char* __doc_operations_research_Solver_MakeSimulatedAnnealing = + R"doc(Creates a Simulated Annealing monitor.)doc"; + +static const char* __doc_operations_research_Solver_MakeSolveOnce = + R"doc(SolveOnce will collapse a search tree described by a decision builder +'db' and a set of monitors and wrap it into a single point. If there +are no solutions to this nested tree, then SolveOnce will fail. If +there is a solution, it will find it and returns nullptr.)doc"; + +static const char* __doc_operations_research_Solver_MakeSolveOnce_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSolveOnce_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSolveOnce_4 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSolveOnce_5 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSolveOnce_6 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSortingConstraint = + R"doc(Creates a constraint binding the arrays of variables "vars" and +"sorted_vars": sorted_vars[0] must be equal to the minimum of all +variables in vars, and so on: the value of sorted_vars[i] must be +equal to the i-th value of variables invars. + +This constraint propagates in both directions: from "vars" to +"sorted_vars" and vice-versa. + +Behind the scenes, this constraint maintains that: - sorted is always +increasing. - whatever the values of vars, there exists a permutation +that injects its values into the sorted variables. + +For more info, please have a look at: https://mpi- +inf.mpg.de/~mehlhorn/ftp/Mehlhorn-Thiel.pdf)doc"; + +static const char* __doc_operations_research_Solver_MakeSplitVariableDomain = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSquare = + R"doc(expr * expr)doc"; + +static const char* __doc_operations_research_Solver_MakeStatisticsModelVisitor = + R"doc(Displays some nice statistics on the model.)doc"; + +static const char* __doc_operations_research_Solver_MakeStoreAssignment = + R"doc(Returns a DecisionBuilder which stores an Assignment (calls void +Assignment::Store()))doc"; + +static const char* + __doc_operations_research_Solver_MakeStrictDisjunctiveConstraint = + R"doc(This constraint forces all interval vars into an non-overlapping +sequence. Intervals with zero durations cannot overlap with over +intervals.)doc"; + +static const char* __doc_operations_research_Solver_MakeSubCircuit = + R"doc(Force the "nexts" variable to create a complete Hamiltonian path for +those that do not loop upon themselves.)doc"; + +static const char* __doc_operations_research_Solver_MakeSum = + R"doc(left + right.)doc"; + +static const char* __doc_operations_research_Solver_MakeSum_2 = + R"doc(expr + value.)doc"; + +static const char* __doc_operations_research_Solver_MakeSum_3 = + R"doc(sum of all vars.)doc"; + +static const char* __doc_operations_research_Solver_MakeSumEquality = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSumEquality_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSumGreaterOrEqual = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSumLessOrEqual = + R"doc(Variation on arrays.)doc"; + +static const char* __doc_operations_research_Solver_MakeSumObjectiveFilter = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSumObjectiveFilter_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSymmetryManager = + R"doc(Symmetry Breaking.)doc"; + +static const char* __doc_operations_research_Solver_MakeSymmetryManager_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSymmetryManager_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSymmetryManager_4 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeSymmetryManager_5 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeTabuSearch = + R"doc(Creates a Tabu Search monitor. In the context of local search the +behavior is similar to MakeOptimize(), creating an objective in a +given sense. The behavior differs once a local optimum is reached: +thereafter solutions which degrade the value of the objective are +allowed if they are not "tabu". A solution is "tabu" if it doesn't +respect the following rules: - improving the best solution found so +far - variables in the "keep" list must keep their value, variables in +the "forbid" list must not take the value they have in the list. +Variables with new values enter the tabu lists after each new solution +found and leave the lists after a given number of iterations (called +tenure). Only the variables passed to the method can enter the lists. +The tabu criterion is softened by the tabu factor which gives the +number of "tabu" violations which is tolerated; a factor of 1 means no +violations allowed; a factor of 0 means all violations are allowed.)doc"; + +static const char* __doc_operations_research_Solver_MakeTemporalDisjunction = + R"doc(This constraint implements a temporal disjunction between two interval +vars t1 and t2. 'alt' indicates which alternative was chosen (alt == 0 +is equivalent to t1 before t2).)doc"; + +static const char* __doc_operations_research_Solver_MakeTemporalDisjunction_2 = + R"doc(This constraint implements a temporal disjunction between two interval +vars.)doc"; + +static const char* __doc_operations_research_Solver_MakeTransitionConstraint = + R"doc(This constraint create a finite automaton that will check the sequence +of variables vars. It uses a transition table called +'transition_table'. Each transition is a triple (current_state, +variable_value, new_state). The initial state is given, and the set of +accepted states is decribed by 'final_states'. These states are hidden +inside the constraint. Only the transitions (i.e. the variables) are +visible.)doc"; + +static const char* __doc_operations_research_Solver_MakeTransitionConstraint_2 = + R"doc(This constraint create a finite automaton that will check the sequence +of variables vars. It uses a transition table called +'transition_table'. Each transition is a triple (current_state, +variable_value, new_state). The initial state is given, and the set of +accepted states is decribed by 'final_states'. These states are hidden +inside the constraint. Only the transitions (i.e. the variables) are +visible.)doc"; + +static const char* __doc_operations_research_Solver_MakeTrueConstraint = + R"doc(This constraint always succeeds.)doc"; + +static const char* __doc_operations_research_Solver_MakeVariableDegreeVisitor = + R"doc(Compute the number of constraints a variable is attached to.)doc"; + +static const char* __doc_operations_research_Solver_MakeVariableDomainFilter = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeVariableGreaterOrEqualValue = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MakeVariableLessOrEqualValue = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MakeWeightedMaximize = + R"doc(Creates a maximization weigthed objective.)doc"; + +static const char* __doc_operations_research_Solver_MakeWeightedMaximize_2 = + R"doc(Creates a maximization weigthed objective.)doc"; + +static const char* __doc_operations_research_Solver_MakeWeightedMinimize = + R"doc(Creates a minimization weighted objective. The actual objective is +scalar_prod(sub_objectives, weights).)doc"; + +static const char* __doc_operations_research_Solver_MakeWeightedMinimize_2 = + R"doc(Creates a minimization weighted objective. The actual objective is +scalar_prod(sub_objectives, weights).)doc"; + +static const char* __doc_operations_research_Solver_MakeWeightedOptimize = + R"doc(Creates a weighted objective with a given sense (true = maximization).)doc"; + +static const char* __doc_operations_research_Solver_MakeWeightedOptimize_2 = + R"doc(Creates a weighted objective with a given sense (true = maximization).)doc"; + +static const char* __doc_operations_research_Solver_MarkerType = + R"doc(This enum is used internally in private methods Solver::PushState and +Solver::PopState to tag states in the search tree.)doc"; + +static const char* __doc_operations_research_Solver_MarkerType_CHOICE_POINT = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MarkerType_REVERSIBLE_ACTION = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MarkerType_SENTINEL = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MarkerType_SIMPLE_MARKER = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MemoryUsage = + R"doc(Current memory usage in bytes)doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent = + R"doc(Search monitor events.)doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent_kAccept = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent_kAcceptDelta = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kAcceptNeighbor = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kAcceptSolution = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kAcceptUncheckedNeighbor = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kAfterDecision = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kApplyDecision = R"doc()doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent_kAtSolution = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent_kBeginFail = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kBeginInitialPropagation = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kBeginNextDecision = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent_kEndFail = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kEndInitialPropagation = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kEndNextDecision = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent_kEnterSearch = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent_kExitSearch = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kIsUncheckedSolutionLimitReached = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent_kLast = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_MonitorEvent_kLocalOptimum = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kNoMoreSolutions = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kPeriodicCheck = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kProgressPercent = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kRefuteDecision = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MonitorEvent_kRestartSearch = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_MultiArmedBanditConcatenateOperators = + R"doc(Creates a local search operator which concatenates a vector of +operators. Uses Multi-Armed Bandit approach for choosing the next +operator to use. Sorts operators based on Upper Confidence Bound +Algorithm which evaluates each operator as sum of average improvement +and exploration function. + +Updates the order of operators when accepts a neighbor with objective +improvement.)doc"; + +static const char* __doc_operations_research_Solver_NameAllVariables = + R"doc(Returns whether all variables should be named.)doc"; + +static const char* __doc_operations_research_Solver_NewSearch = + R"doc(@{ Decomposed search. The code for a top level search should look like +solver->NewSearch(db); while (solver->NextSolution()) { //.. use the +current solution } solver()->EndSearch();)doc"; + +static const char* __doc_operations_research_Solver_NewSearch_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_NewSearch_3 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_NewSearch_4 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_NewSearch_5 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_NewSearch_6 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_NextSolution = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Now = + R"doc(The 'absolute time' as seen by the solver. Unless a user-provided +clock was injected via SetClock() (eg. for unit tests), this is a real +walltime, shifted so that it was 0 at construction. All so-called +"walltime" limits are relative to this time.)doc"; + +static const char* __doc_operations_research_Solver_OptimizationDirection = + R"doc(Optimization directions.)doc"; + +static const char* + __doc_operations_research_Solver_OptimizationDirection_MAXIMIZATION = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_OptimizationDirection_MINIMIZATION = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_OptimizationDirection_NOT_SET = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_ParentSearch = + R"doc(Returns the Search object which is the parent of the active search, +i.e., the search below the top of the stack. If the active search is +at the bottom of the stack, returns the active search.)doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification = + R"doc(A constraint that maintains the energy cost of paths. Energy is the +integral of force applied over distance. More formally, the energy +used on a path is: energy[path] = sum(node | paths[node] == path /\ +node not end) forces[next[node]] * distances[node] where forces[n] is +the force needed to move loads accumulated until, but excluding weight +and distances[n] is the distance from n to its successor. For +instance, if a path has a route with two pickup/delivery pairs where +the first shipment weighs 1 unit, the second weighs 2 units, and the +distance between nodes is one, the {force/distance} of nodes would be: +start{0/1} P1{0/1} P2{1/1} D1{3/1} D2{2/1} end{0/0}. The energy would +be 0*1 + 1*1 + 3*1 + 2*1 + 0*1. The cost per unit of energy is +cost_per_unit_below_threshold until the force reaches the threshold, +then it is cost_per_unit_above_threshold: min(threshold, +force.CumulVar(Next(node))) * distance.TransitVar(node) * +cost_per_unit_below_threshold + max(0, force.CumulVar(Next(node)) - +threshold) * distance.TransitVar(node) * +cost_per_unit_above_threshold.)doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_EnergyCost = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_EnergyCost_IsNull = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_EnergyCost_cost_per_unit_above_threshold = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_EnergyCost_cost_per_unit_below_threshold = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_EnergyCost_threshold = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_costs = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_distances = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_forces = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_nexts = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_path_ends = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_path_energy_costs = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_path_starts = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_path_used_when_empty = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_PathEnergyCostConstraintSpecification_paths = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_PopState = R"doc()doc"; + +static const char* __doc_operations_research_Solver_PopState_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_ProcessConstraints = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_PushSentinel = R"doc()doc"; + +static const char* __doc_operations_research_Solver_PushState = + R"doc(The PushState and PopState methods manipulates the states of the +reversible objects. They are visible only because they are useful to +write unitary tests.)doc"; + +static const char* __doc_operations_research_Solver_PushState_2 = + R"doc(Initialization. To be called by the constructors only.)doc"; + +static const char* __doc_operations_research_Solver_Rand32 = + R"doc(Returns a random value between 0 and 'size' - 1;)doc"; + +static const char* __doc_operations_research_Solver_Rand64 = + R"doc(Returns a random value between 0 and 'size' - 1;)doc"; + +static const char* __doc_operations_research_Solver_RandomConcatenateOperators = + R"doc(Randomized version of local search concatenator; calls a random +operator at each call to MakeNextNeighbor().)doc"; + +static const char* + __doc_operations_research_Solver_RandomConcatenateOperators_2 = + R"doc(Randomized version of local search concatenator; calls a random +operator at each call to MakeNextNeighbor(). The provided seed is used +to initialize the random number generator.)doc"; + +static const char* __doc_operations_research_Solver_ReSeed = + R"doc(Reseed the solver random generator.)doc"; + +static const char* __doc_operations_research_Solver_RegisterDemon = + R"doc(Adds a new demon and wraps it inside a DemonProfiler if necessary.)doc"; + +static const char* __doc_operations_research_Solver_RegisterIntExpr = + R"doc(Registers a new IntExpr and wraps it inside a TraceIntExpr if +necessary.)doc"; + +static const char* __doc_operations_research_Solver_RegisterIntVar = + R"doc(Registers a new IntVar and wraps it inside a TraceIntVar if necessary.)doc"; + +static const char* __doc_operations_research_Solver_RegisterIntervalVar = + R"doc(Registers a new IntervalVar and wraps it inside a TraceIntervalVar if +necessary.)doc"; + +static const char* __doc_operations_research_Solver_RegularLimit = + R"doc(Creates a search limit that constrains the running time.)doc"; + +static const char* __doc_operations_research_Solver_RestartCurrentSearch = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_RestartSearch = R"doc()doc"; + +static const char* __doc_operations_research_Solver_RevAlloc = + R"doc(Registers the given object as being reversible. By calling this +method, the caller gives ownership of the object to the solver, which +will delete it when there is a backtrack out of the current state. + +Returns the argument for convenience: this way, the caller may +directly invoke a constructor in the argument, without having to store +the pointer first. + +This function is only for users that define their own subclasses of +BaseObject: for all subclasses predefined in the library, the +corresponding factory methods (e.g., MakeIntVar(...), +MakeAllDifferent(...) already take care of the registration.)doc"; + +static const char* __doc_operations_research_Solver_RevAllocArray = + R"doc(Like RevAlloc() above, but for an array of objects: the array must +have been allocated with the new[] operator. The entire array will be +deleted when backtracking out of the current state. + +This method is valid for arrays of int, int64_t, uint64_t, bool, +BaseObject*, IntVar*, IntExpr*, and Constraint*.)doc"; + +static const char* __doc_operations_research_Solver_RunUncheckedLocalSearch = + R"doc(Experimental: runs a local search on the given initial solution, +checking the feasibility and the objective value of solutions using +the filter manager only (solutions are never restored in the CP +world). Only greedy descent is supported.)doc"; + +static const char* + __doc_operations_research_Solver_RunUncheckedLocalSearchInternal = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SafeRevAlloc = R"doc()doc"; + +static const char* __doc_operations_research_Solver_SafeRevAllocArray = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SafeRevAllocArray_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SafeRevAllocArray_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SafeRevAllocArray_4 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SafeRevAllocArray_5 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SafeRevAllocArray_6 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SafeRevAllocArray_7 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SafeRevAllocArray_8 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SaveAndAdd = + R"doc(All-in-one SaveAndAdd_value.)doc"; + +static const char* __doc_operations_research_Solver_SaveAndSetValue = + R"doc(All-in-one SaveAndSetValue.)doc"; + +static const char* __doc_operations_research_Solver_SaveValue = + R"doc(SaveValue() saves the value of the corresponding object. It must be +called before modifying the object. The value will be restored upon +backtrack.)doc"; + +static const char* __doc_operations_research_Solver_SearchContext = R"doc()doc"; + +static const char* __doc_operations_research_Solver_SearchContext_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SearchDepth = + R"doc(Gets the search depth of the current active search. Returns -1 if +there is no active search opened.)doc"; + +static const char* __doc_operations_research_Solver_SearchLeftDepth = + R"doc(Gets the search left depth of the current active search. Returns -1 if +there is no active search opened.)doc"; + +static const char* __doc_operations_research_Solver_SearchLimit = + R"doc(Creates a search limit that is reached when either of the underlying +limit is reached. That is, the returned limit is more stringent than +both argument limits.)doc"; + +static const char* __doc_operations_research_Solver_SearchLogParameters = + R"doc(Creates a search monitor from logging parameters.)doc"; + +static const char* + __doc_operations_research_Solver_SearchLogParameters_branch_period = + R"doc(SearchMonitors will display a periodic search log every branch_period +branches explored.)doc"; + +static const char* + __doc_operations_research_Solver_SearchLogParameters_display_callback = + R"doc(SearchMonitors will display the result of display_callback at each new +solution found and when the search finishes if +display_on_new_solutions_only is false.)doc"; + +static const char* + __doc_operations_research_Solver_SearchLogParameters_display_on_new_solutions_only = + R"doc(To be used to protect from cases where display_callback assumes +variables are instantiated, which only happens in AtSolution().)doc"; + +static const char* + __doc_operations_research_Solver_SearchLogParameters_objective = + R"doc(SearchMonitors will display values of objective or variables (both +cannot be used together).)doc"; + +static const char* + __doc_operations_research_Solver_SearchLogParameters_offsets = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_SearchLogParameters_scaling_factors = + R"doc(When displayed, objective or var values will be scaled and offset by +the given values in the following way: scaling_factor * (value + +offset).)doc"; + +static const char* + __doc_operations_research_Solver_SearchLogParameters_variables = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SequenceStrategy = + R"doc(Used for scheduling. Not yet implemented.)doc"; + +static const char* + __doc_operations_research_Solver_SequenceStrategy_CHOOSE_MIN_SLACK_RANK_FORWARD = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_SequenceStrategy_CHOOSE_RANDOM_RANK_FORWARD = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_SequenceStrategy_SEQUENCE_DEFAULT = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_SequenceStrategy_SEQUENCE_SIMPLE = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SetBranchSelector = + R"doc(Sets the given branch selector on the current active search.)doc"; + +static const char* __doc_operations_research_Solver_SetClock = + R"doc(Set the clock in the timer. Does not take ownership. For dependency +injection.)doc"; + +static const char* __doc_operations_research_Solver_SetName = R"doc()doc"; + +static const char* __doc_operations_research_Solver_SetSearchContext = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SetUseFastLocalSearch = + R"doc(enabled for metaheuristics. Disables/enables fast local search.)doc"; + +static const char* __doc_operations_research_Solver_ShouldFail = + R"doc(See http://cs/file:constraint_solver.i%20ShouldFail.)doc"; + +static const char* __doc_operations_research_Solver_Solve = + R"doc(@{ Solves the problem using the given DecisionBuilder and returns true +if a solution was found and accepted. + +These methods are the ones most users should use to search for a +solution. Note that the definition of 'solution' is subtle. A solution +here is defined as a leaf of the search tree with respect to the given +decision builder for which there is no failure. What this means is +that, contrary to intuition, a solution may not have all variables of +the model bound. It is the responsibility of the decision builder to +keep returning decisions until all variables are indeed bound. The +most extreme counterexample is calling Solve with a trivial decision +builder whose Next() method always returns nullptr. In this case, +Solve immediately returns 'true', since not assigning any variable to +any value is a solution, unless the root node propagation discovers +that the model is infeasible. + +This function must be called either from outside of search, or from +within the Next() method of a decision builder. + +Solve will terminate whenever any of the following event arise: * A +search monitor asks the solver to terminate the search by calling +solver()->FinishCurrentSearch(). * A solution is found that is +accepted by all search monitors, and none of the search monitors +decides to search for another one. + +Upon search termination, there will be a series of backtracks all the +way to the top level. This means that a user cannot expect to inspect +the solution by querying variables after a call to Solve(): all the +information will be lost. In order to do something with the solution, +the user must either: + +* Use a search monitor that can process such a leaf. See, in +particular, the SolutionCollector class. * Do not use Solve. Instead, +use the more fine-grained approach using methods NewSearch(...), +NextSolution(), and EndSearch(). + +Parameter ``db``: + The decision builder that will generate the search tree. + +Parameter ``monitors``: + A vector of search monitors that will be notified of various + events during the search. In their reaction to these events, such + monitors may influence the search.)doc"; + +static const char* __doc_operations_research_Solver_Solve_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Solve_3 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Solve_4 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Solve_5 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Solve_6 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_SolveAndCommit = + R"doc(SolveAndCommit using a decision builder and up to three search +monitors, usually one for the objective, one for the limits and one to +collect solutions. + +The difference between a SolveAndCommit() and a Solve() method call is +the fact that SolveAndCommit will not backtrack all modifications at +the end of the search. This method is only usable during the Next() +method of a decision builder.)doc"; + +static const char* __doc_operations_research_Solver_SolveAndCommit_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SolveAndCommit_3 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SolveAndCommit_4 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SolveAndCommit_5 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_SolveDepth = + R"doc(Gets the number of nested searches. It returns 0 outside search, 1 +during the top level search, 2 or more in case of nested searches.)doc"; + +static const char* __doc_operations_research_Solver_Solver = + R"doc(Solver API)doc"; + +static const char* __doc_operations_research_Solver_Solver_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Solver_3 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_SolverState = + R"doc(This enum represents the state of the solver w.r.t. the search.)doc"; + +static const char* __doc_operations_research_Solver_SolverState_AT_SOLUTION = + R"doc(After successful NextSolution and before EndSearch.)doc"; + +static const char* __doc_operations_research_Solver_SolverState_IN_ROOT_NODE = + R"doc(Executing the root node.)doc"; + +static const char* __doc_operations_research_Solver_SolverState_IN_SEARCH = + R"doc(Executing the search code.)doc"; + +static const char* + __doc_operations_research_Solver_SolverState_NO_MORE_SOLUTIONS = + R"doc(After failed NextSolution and before EndSearch.)doc"; + +static const char* __doc_operations_research_Solver_SolverState_OUTSIDE_SEARCH = + R"doc(Before search, after search.)doc"; + +static const char* + __doc_operations_research_Solver_SolverState_PROBLEM_INFEASIBLE = + R"doc(After search, the model is infeasible.)doc"; + +static const char* __doc_operations_research_Solver_TopLevelSearch = + R"doc(Returns the Search object that is at the bottom of the search stack. +Contrast with ActiveSearch(), which returns the search at the top of +the stack.)doc"; + +static const char* __doc_operations_research_Solver_TopPeriodicCheck = + R"doc(Performs PeriodicCheck on the top-level search; for instance, can be +called from a nested solve to check top-level limits.)doc"; + +static const char* __doc_operations_research_Solver_TopProgressPercent = + R"doc(Returns a percentage representing the propress of the search before +reaching the limits of the top-level search (can be called from a +nested solve).)doc"; + +static const char* __doc_operations_research_Solver_Try = + R"doc("Try"-builders "recursively". For instance, Try(a,b,c,d) will give a +tree unbalanced to the right, whereas Try(Try(a,b), Try(b,c)) will +give a balanced tree. Investigate if we should only provide the binary +version and/or if we should balance automatically.)doc"; + +static const char* __doc_operations_research_Solver_Try_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Try_3 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_Try_4 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_UnaryIntervalRelation = + R"doc(This enum is used in Solver::MakeIntervalVarRelation to specify the +temporal relation between an interval t and an integer d.)doc"; + +static const char* + __doc_operations_research_Solver_UnaryIntervalRelation_AVOID_DATE = + R"doc(STARTS_AFTER or ENDS_BEFORE, i.e. d is not in t. t starts after d, +i.e. Start(t) >= d. t ends before d, i.e. End(t) <= d.)doc"; + +static const char* + __doc_operations_research_Solver_UnaryIntervalRelation_CROSS_DATE = + R"doc(STARTS_BEFORE and ENDS_AFTER at the same time, i.e. d is in t. t +starts before d, i.e. Start(t) <= d. t ends after d, i.e. End(t) >= d.)doc"; + +static const char* + __doc_operations_research_Solver_UnaryIntervalRelation_ENDS_AFTER = + R"doc(t ends after d, i.e. End(t) >= d.)doc"; + +static const char* + __doc_operations_research_Solver_UnaryIntervalRelation_ENDS_AT = + R"doc(t ends at d, i.e. End(t) == d.)doc"; + +static const char* + __doc_operations_research_Solver_UnaryIntervalRelation_ENDS_BEFORE = + R"doc(t ends before d, i.e. End(t) <= d.)doc"; + +static const char* + __doc_operations_research_Solver_UnaryIntervalRelation_STARTS_AFTER = + R"doc(t starts after d, i.e. Start(t) >= d.)doc"; + +static const char* + __doc_operations_research_Solver_UnaryIntervalRelation_STARTS_AT = + R"doc(t starts at d, i.e. Start(t) == d.)doc"; + +static const char* + __doc_operations_research_Solver_UnaryIntervalRelation_STARTS_BEFORE = + R"doc(t starts before d, i.e. Start(t) <= d.)doc"; + +static const char* __doc_operations_research_Solver_UnfreezeQueue = R"doc()doc"; + +static const char* __doc_operations_research_Solver_UnsafeRevAlloc = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_UnsafeRevAllocArray = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_UnsafeRevAllocArrayAux = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_UnsafeRevAllocAux = + R"doc(UnsafeRevAlloc is used internally for cells in SimpleRevFIFO and other +structures like this.)doc"; + +static const char* __doc_operations_research_Solver_UseFastLocalSearch = + R"doc(Returns true if fast local search is enabled.)doc"; + +static const char* __doc_operations_research_Solver_VirtualMemorySize = + R"doc(Current virtual memory size in bytes)doc"; + +static const char* __doc_operations_research_Solver_accepted_neighbors = + R"doc(The number of accepted neighbors.)doc"; + +static const char* __doc_operations_research_Solver_accepted_neighbors_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_additional_constraint_index = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_additional_constraints_list = R"doc()doc"; + +static const char* + __doc_operations_research_Solver_additional_constraints_parent_list = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_anonymous_variable_index = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_balancing_decision = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_balancing_decision_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_branches = + R"doc(The number of branches explored since the creation of the solver.)doc"; + +static const char* __doc_operations_research_Solver_branches_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_cached_constants = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_cast_constraints = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_cast_information = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_check_alloc_state = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_clear_fail_intercept = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_const_parameters = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_constraint_index = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_constraints = + R"doc(Counts the number of constraints that have been added to the solver +before the search.)doc"; + +static const char* __doc_operations_research_Solver_constraints_list = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_context = + R"doc(Gets the current context of the search.)doc"; + +static const char* __doc_operations_research_Solver_context_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_decisions = R"doc()doc"; + +static const char* __doc_operations_research_Solver_demon_profiler = + R"doc(Access to demon profiler.)doc"; + +static const char* __doc_operations_research_Solver_demon_profiler_2 = + R"doc(Demon monitor)doc"; + +static const char* __doc_operations_research_Solver_demon_runs = + R"doc(The number of demons executed during search for a given priority.)doc"; + +static const char* __doc_operations_research_Solver_demon_runs_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_empty_name = R"doc()doc"; + +static const char* __doc_operations_research_Solver_fail_decision = R"doc()doc"; + +static const char* __doc_operations_research_Solver_fail_intercept = + R"doc(intercept failures)doc"; + +static const char* __doc_operations_research_Solver_fail_stamp = + R"doc(The fail_stamp() is incremented after each backtrack.)doc"; + +static const char* __doc_operations_research_Solver_fail_stamp_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_fails = R"doc()doc"; + +static const char* __doc_operations_research_Solver_failures = + R"doc(The number of failures encountered since the creation of the solver.)doc"; + +static const char* __doc_operations_research_Solver_false_constraint = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_filtered_neighbors = + R"doc(The number of filtered neighbors (neighbors accepted by filters).)doc"; + +static const char* __doc_operations_research_Solver_filtered_neighbors_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_local_search_monitor = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_local_search_profiler = + R"doc(Local search profiler monitor)doc"; + +static const char* __doc_operations_research_Solver_local_search_state = + R"doc(Local search state.)doc"; + +static const char* __doc_operations_research_Solver_model_cache = R"doc()doc"; + +static const char* __doc_operations_research_Solver_model_name = + R"doc(Returns the name of the model.)doc"; + +static const char* __doc_operations_research_Solver_name = R"doc()doc"; + +static const char* __doc_operations_research_Solver_neighbors = + R"doc(The number of neighbors created.)doc"; + +static const char* __doc_operations_research_Solver_neighbors_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_num_int_vars = R"doc()doc"; + +static const char* __doc_operations_research_Solver_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_optimization_direction = + R"doc(The direction of optimization, getter and setter.)doc"; + +static const char* __doc_operations_research_Solver_optimization_direction_2 = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_parameters = + R"doc(Stored Parameters.)doc"; + +static const char* __doc_operations_research_Solver_parameters_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_print_trace = R"doc()doc"; + +static const char* __doc_operations_research_Solver_propagation_monitor = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_propagation_object_names = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_queue = R"doc()doc"; + +static const char* __doc_operations_research_Solver_random = R"doc()doc"; + +static const char* __doc_operations_research_Solver_reset_action_on_fail = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_searches = R"doc()doc"; + +static const char* __doc_operations_research_Solver_set_action_on_fail = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_set_context = + R"doc(Sets the current context of the search.)doc"; + +static const char* __doc_operations_research_Solver_set_fail_intercept = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_set_optimization_direction = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_set_variable_to_clean_on_fail = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_should_fail = R"doc()doc"; + +static const char* __doc_operations_research_Solver_solutions = + R"doc(The number of solutions found since the start of the search.)doc"; + +static const char* __doc_operations_research_Solver_stamp = + R"doc(The stamp indicates how many moves in the search tree we have +performed. It is useful to detect if we need to update same lazy +structures.)doc"; + +static const char* __doc_operations_research_Solver_state = + R"doc(State of the solver.)doc"; + +static const char* __doc_operations_research_Solver_state_2 = R"doc()doc"; + +static const char* __doc_operations_research_Solver_timer = R"doc()doc"; + +static const char* __doc_operations_research_Solver_tmp_vector = + R"doc(Unsafe temporary vector. It is used to avoid leaks in operations that +need storage and that may fail. See IntVar::SetValues() for instance. +It is not locked; do not use in a multi-threaded or reentrant setup.)doc"; + +static const char* __doc_operations_research_Solver_trail = R"doc()doc"; + +static const char* __doc_operations_research_Solver_true_constraint = + R"doc(Cached constraints.)doc"; + +static const char* __doc_operations_research_Solver_unchecked_solutions = + R"doc(The number of unchecked solutions found by local search.)doc"; + +static const char* + __doc_operations_research_Solver_unnamed_enum_at_util_operations_research_constraint_solver_constraint_solver_h_3315_3 = + R"doc(interval of constants cached, inclusive:)doc"; + +static const char* + __doc_operations_research_Solver_unnamed_enum_at_util_operations_research_constraint_solver_constraint_solver_h_3315_3_MAX_CACHED_INT_CONST = + R"doc()doc"; + +static const char* + __doc_operations_research_Solver_unnamed_enum_at_util_operations_research_constraint_solver_constraint_solver_h_3315_3_MIN_CACHED_INT_CONST = + R"doc()doc"; + +static const char* __doc_operations_research_Solver_use_fast_local_search = + R"doc(Local search mode)doc"; + +static const char* __doc_operations_research_Solver_wall_time = + R"doc(DEPRECATED: Use Now() instead. Time elapsed, in ms since the creation +of the solver.)doc"; + +static const char* __doc_operations_research_StateInfo = R"doc()doc"; + +static const char* __doc_operations_research_SymmetryBreaker = R"doc()doc"; + +static const char* __doc_operations_research_Trail = R"doc()doc"; + +static const char* __doc_operations_research_Zero = + R"doc(This method returns 0. It is useful when 0 can be cast either as a +pointer or as an integer value and thus lead to an ambiguous function +call.)doc"; + +static const char* __doc_operations_research_operator_lshift = R"doc()doc"; + +static const char* __doc_operations_research_operator_lshift_2 = R"doc()doc"; + +static const char* __doc_operations_research_operator_lshift_3 = R"doc()doc"; + +static const char* __doc_util_Clock = R"doc()doc"; + +#if defined(__GNUG__) +#pragma GCC diagnostic pop +#endif diff --git a/ortools/constraint_solver/python/constraint_solver_test.py b/ortools/constraint_solver/python/constraint_solver_test.py new file mode 100644 index 00000000000..7806bf06753 --- /dev/null +++ b/ortools/constraint_solver/python/constraint_solver_test.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test for constraint_solver pybind11 layer.""" + +from absl.testing import absltest +from ortools.constraint_solver.python import constraint_solver + + +class ConstraintSolverTest(absltest.TestCase): + + def test_create_solver(self): + print("test_create_solver") + solver = constraint_solver.Solver("test_create_solver") + print(solver) + + def test_create_int_var(self): + print("test_create_int_var") + solver = constraint_solver.Solver("test_create_int_var") + x = solver.new_int_var(0, 10, "x") + self.assertEqual(str(x), "x(0..10)") + self.assertEqual(x.min, 0) + self.assertEqual(x.max, 10) + self.assertEqual(x.name, "x") + + y = solver.new_int_var([0, 2, 4]) + self.assertEqual(y.min, 0) + self.assertEqual(y.max, 4) + self.assertEmpty(y.name) + y.name = "y" + self.assertEqual(y.name, "y") + + def test_create_int_expr(self): + print("test_create_int_expr") + solver = constraint_solver.Solver("test_create_int_expr") + x = solver.new_int_var(0, 10, "x") + y = solver.new_int_var(0, 10, "y") + + x_plus_3 = x + 3 + self.assertEqual(str(x_plus_3), "(x(0..10) + 3)") + print(x_plus_3) + self.assertEqual(x_plus_3.min, 3) + self.assertEqual(x_plus_3.max, 13) + + self.assertEqual(str(x * 5), "(x(0..10) * 5)") + self.assertEqual(str(x + y), "(x(0..10) + y(0..10))") + self.assertEqual(str(2 + x), "(x(0..10) + 2)") + self.assertEqual(str(7 * x), "(x(0..10) * 7)") + self.assertEqual(str(x * y), "(x(0..10) * y(0..10))") + self.assertEqual(str(x + 2 * y + 5), "((x(0..10) + (y(0..10) * 2)) + 5)") + + def test_fail_outside_solve(self): + print("test_fail_outside_solve") + solver = constraint_solver.Solver("test_fail_outside_solve") + x = solver.new_int_var(0, 10, "x") + try: + x.set_min(20) + except ValueError: + print(" fail caught") + + def test_rabbits_pheasants(self): + print("test_rabbits_pheasants") + solver = constraint_solver.Solver("test_rabbits_pheasants") + rabbits = solver.new_int_var(0, 20, "rabbits") + pheasants = solver.new_int_var(0, 20, "pheasants") + solver.add(rabbits + pheasants == 20) + solver.add(4 * rabbits + 2 * pheasants == 56) + solver.accept(solver.print_model_visitor()) + + +if __name__ == "__main__": + absltest.main() diff --git a/ortools/constraint_solver/python/pywrapcp_test.py b/ortools/constraint_solver/python/pywrapcp_test.py index b0be201a264..44eb4c5f85c 100755 --- a/ortools/constraint_solver/python/pywrapcp_test.py +++ b/ortools/constraint_solver/python/pywrapcp_test.py @@ -23,1370 +23,1403 @@ def inc_callback(i): - return i + 1 + return i + 1 class ClassIncCallback: - def __init__(self, increment): - self.__increment = increment + def __init__(self, increment): + self.__increment = increment - def inc_method(self, i): - return i + self.__increment + def inc_method(self, i): + return i + self.__increment class TestIntVarContainerAPI(absltest.TestCase): - def test_contains(self): - self.assertTrue( - hasattr(pywrapcp.IntVarContainer, "Contains"), - dir(pywrapcp.IntVarContainer), - ) + def test_contains(self): + self.assertTrue( + hasattr(pywrapcp.IntVarContainer, "Contains"), + dir(pywrapcp.IntVarContainer), + ) - def test_element(self): - self.assertTrue( - hasattr(pywrapcp.IntVarContainer, "Element"), - dir(pywrapcp.IntVarContainer), - ) + def test_element(self): + self.assertTrue( + hasattr(pywrapcp.IntVarContainer, "Element"), + dir(pywrapcp.IntVarContainer), + ) - def test_size(self): - self.assertTrue( - hasattr(pywrapcp.IntVarContainer, "Size"), dir(pywrapcp.IntVarContainer) - ) + def test_size(self): + self.assertTrue( + hasattr(pywrapcp.IntVarContainer, "Size"), dir(pywrapcp.IntVarContainer) + ) - def test_store(self): - self.assertTrue( - hasattr(pywrapcp.IntVarContainer, "Store"), - dir(pywrapcp.IntVarContainer), - ) + def test_store(self): + self.assertTrue( + hasattr(pywrapcp.IntVarContainer, "Store"), + dir(pywrapcp.IntVarContainer), + ) - def test_restore(self): - self.assertTrue( - hasattr(pywrapcp.IntVarContainer, "Restore"), - dir(pywrapcp.IntVarContainer), - ) + def test_restore(self): + self.assertTrue( + hasattr(pywrapcp.IntVarContainer, "Restore"), + dir(pywrapcp.IntVarContainer), + ) class TestIntervalVarContainerAPI(absltest.TestCase): - def test_contains(self): - self.assertTrue( - hasattr(pywrapcp.IntervalVarContainer, "Contains"), - dir(pywrapcp.IntervalVarContainer), - ) - - def test_element(self): - self.assertTrue( - hasattr(pywrapcp.IntervalVarContainer, "Element"), - dir(pywrapcp.IntervalVarContainer), - ) - - def test_size(self): - self.assertTrue( - hasattr(pywrapcp.IntervalVarContainer, "Size"), - dir(pywrapcp.IntervalVarContainer), - ) - - def test_store(self): - self.assertTrue( - hasattr(pywrapcp.IntervalVarContainer, "Store"), - dir(pywrapcp.IntervalVarContainer), - ) - - def test_restore(self): - self.assertTrue( - hasattr(pywrapcp.IntervalVarContainer, "Restore"), - dir(pywrapcp.IntervalVarContainer), - ) + def test_contains(self): + self.assertTrue( + hasattr(pywrapcp.IntervalVarContainer, "Contains"), + dir(pywrapcp.IntervalVarContainer), + ) + + def test_element(self): + self.assertTrue( + hasattr(pywrapcp.IntervalVarContainer, "Element"), + dir(pywrapcp.IntervalVarContainer), + ) + + def test_size(self): + self.assertTrue( + hasattr(pywrapcp.IntervalVarContainer, "Size"), + dir(pywrapcp.IntervalVarContainer), + ) + + def test_store(self): + self.assertTrue( + hasattr(pywrapcp.IntervalVarContainer, "Store"), + dir(pywrapcp.IntervalVarContainer), + ) + + def test_restore(self): + self.assertTrue( + hasattr(pywrapcp.IntervalVarContainer, "Restore"), + dir(pywrapcp.IntervalVarContainer), + ) class TestSequenceVarContainerAPI(absltest.TestCase): - def test_contains(self): - self.assertTrue( - hasattr(pywrapcp.SequenceVarContainer, "Contains"), - dir(pywrapcp.SequenceVarContainer), - ) - - def test_element(self): - self.assertTrue( - hasattr(pywrapcp.SequenceVarContainer, "Element"), - dir(pywrapcp.SequenceVarContainer), - ) - - def test_size(self): - self.assertTrue( - hasattr(pywrapcp.SequenceVarContainer, "Size"), - dir(pywrapcp.SequenceVarContainer), - ) - - def test_store(self): - self.assertTrue( - hasattr(pywrapcp.SequenceVarContainer, "Store"), - dir(pywrapcp.SequenceVarContainer), - ) - - def test_restore(self): - self.assertTrue( - hasattr(pywrapcp.SequenceVarContainer, "Restore"), - dir(pywrapcp.SequenceVarContainer), - ) + def test_contains(self): + self.assertTrue( + hasattr(pywrapcp.SequenceVarContainer, "Contains"), + dir(pywrapcp.SequenceVarContainer), + ) + + def test_element(self): + self.assertTrue( + hasattr(pywrapcp.SequenceVarContainer, "Element"), + dir(pywrapcp.SequenceVarContainer), + ) + + def test_size(self): + self.assertTrue( + hasattr(pywrapcp.SequenceVarContainer, "Size"), + dir(pywrapcp.SequenceVarContainer), + ) + + def test_store(self): + self.assertTrue( + hasattr(pywrapcp.SequenceVarContainer, "Store"), + dir(pywrapcp.SequenceVarContainer), + ) + + def test_restore(self): + self.assertTrue( + hasattr(pywrapcp.SequenceVarContainer, "Restore"), + dir(pywrapcp.SequenceVarContainer), + ) class PyWrapCPTest(absltest.TestCase): - def testRabbitPheasant(self): - # Create the solver. - solver = pywrapcp.Solver("testRabbitPheasant") - - # Create the variables. - pheasant = solver.IntVar(0, 100, "pheasant") - rabbit = solver.IntVar(0, 100, "rabbit") - - # Create the constraints. - solver.Add(pheasant + rabbit == 20) - solver.Add(pheasant * 2 + rabbit * 4 == 56) - - # Create the search phase. - db = solver.Phase( - [rabbit, pheasant], solver.INT_VAR_DEFAULT, solver.ASSIGN_MIN_VALUE - ) - - # Create assignment - solution = solver.Assignment() - solution.Add(rabbit) - solution.Add(pheasant) - - collector = solver.FirstSolutionCollector(solution) - - # And solve. - solver.Solve(db, collector) - - self.assertEqual(1, collector.SolutionCount()) - current = collector.Solution(0) - - self.assertEqual(12, current.Value(pheasant)) - self.assertEqual(8, current.Value(rabbit)) - - def testSolverParameters(self): - # Create the parameters. - params = pywrapcp.Solver.DefaultSolverParameters() - self.assertIsInstance(params, solver_parameters_pb2.ConstraintSolverParameters) - self.assertFalse(params.trace_propagation) - params.trace_propagation = True - self.assertTrue(params.trace_propagation) - - # Create the solver. - solver = pywrapcp.Solver("testRabbitPheasantWithParameters", params) - inside_params = solver.Parameters() - self.assertTrue(inside_params.trace_propagation) - - def testSolverParametersFields(self): - params = solver_parameters_pb2.ConstraintSolverParameters() - bool_params = [ - "store_names", - "name_cast_variables", - "name_all_variables", - "profile_propagation", - "trace_propagation", - "trace_search", - "print_model", - "print_model_stats", - "print_added_constraints", - "disable_solve", - ] - for p in bool_params: - for v in [True, False]: - setattr(params, p, v) - self.assertEqual(getattr(params, p), v) - - int_params = ["trail_block_size", "array_split_size"] - for p in int_params: - for v in [10, 100]: - setattr(params, p, v) - self.assertEqual(getattr(params, p), v) - - string_params = ["profile_file"] - for p in string_params: - for v in ["", "tmp_file"]: - setattr(params, p, v) - self.assertEqual(getattr(params, p), v) - - def testIntVarAPI(self): - # Create the solver. - solver = pywrapcp.Solver("testIntVarAPI") - - c = solver.IntConst(3, "c") - self.assertEqual(3, c.Min()) - self.assertEqual(3, c.Max()) - self.assertEqual(3, c.Value()) - self.assertTrue(c.Bound()) - - b = solver.BoolVar("b") - self.assertEqual(0, b.Min()) - self.assertEqual(1, b.Max()) - - v1 = solver.IntVar(3, 10, "v1") - self.assertEqual(3, v1.Min()) - self.assertEqual(10, v1.Max()) - - v2 = solver.IntVar([1, 5, 3], "v2") - self.assertEqual(1, v2.Min()) - self.assertEqual(5, v2.Max()) - self.assertEqual(3, v2.Size()) - - # pylint: disable=too-many-statements - def testIntegerArithmetic(self): - solver = pywrapcp.Solver("testIntegerArithmetic") - - v1 = solver.IntVar(0, 10, "v1") - v2 = solver.IntVar(0, 10, "v2") - v3 = solver.IntVar(0, 10, "v3") - - e1 = v1 + v2 - e2 = v1 + 2 - e3 = solver.Sum([v1, v2, v3 * 3]) - - e4 = v1 - 3 - e5 = v1 - v2 - e6 = -v1 - - e7 = abs(e6) - e8 = v3.Square() - - e9 = v1 * 3 - e10 = v1 * v2 - - e11 = v2.IndexOf([0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) - e11b = v2.IndexOf([0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) - e12 = solver.Min(e1, e2) - e13 = solver.Min(e3, 3) - e14 = solver.Min([e1 + 1, e2 + 2, e3 + 3]) - - e15 = solver.Max(e1, e2) - e16 = solver.Max(e3, 3) - e17 = solver.Max([e1 + 1, e2 + 2, e3 + 3]) - - solver.Add(v1 == 1) - solver.Add(v2 == 2) - solver.Add(v3 == 3) - - db = solver.Phase([v1, v2, v3], solver.INT_VAR_DEFAULT, solver.ASSIGN_MIN_VALUE) - - solver.NewSearch(db) - solver.NextSolution() - - self.assertEqual(1, v1.Value()) - self.assertEqual(2, v2.Value()) - self.assertEqual(3, v3.Value()) - - self.assertEqual(3, e1.Min()) - self.assertEqual(3, e1.Max()) - self.assertEqual(3, e2.Min()) - self.assertEqual(3, e2.Max()) - self.assertEqual(12, e3.Min()) - self.assertEqual(12, e3.Max()) - self.assertEqual(-2, e4.Min()) - self.assertEqual(-2, e4.Max()) - self.assertEqual(-1, e5.Min()) - self.assertEqual(-1, e5.Max()) - self.assertEqual(-1, e6.Min()) - self.assertEqual(-1, e6.Max()) - self.assertEqual(1, e7.Min()) - self.assertEqual(1, e7.Max()) - self.assertEqual(9, e8.Min()) - self.assertEqual(9, e8.Max()) - self.assertEqual(3, e9.Min()) - self.assertEqual(3, e9.Max()) - self.assertEqual(2, e10.Min()) - self.assertEqual(2, e10.Max()) - self.assertEqual(4, e11.Min()) - self.assertEqual(4, e11.Max()) - self.assertEqual(4, e11b.Min()) - self.assertEqual(4, e11b.Max()) - self.assertEqual(3, e12.Min()) - self.assertEqual(3, e12.Max()) - self.assertEqual(3, e13.Min()) - self.assertEqual(3, e13.Max()) - self.assertEqual(4, e14.Min()) - self.assertEqual(4, e14.Max()) - self.assertEqual(3, e15.Min()) - self.assertEqual(3, e15.Max()) - self.assertEqual(12, e16.Min()) - self.assertEqual(12, e16.Max()) - self.assertEqual(15, e17.Min()) - self.assertEqual(15, e17.Max()) - solver.EndSearch() - - def testStatusVar(self): - solver = pywrapcp.Solver("testStatusVar") - v1 = solver.IntVar(0, 10, "v1") - v2 = solver.IntVar(0, 10, "v2") - c1 = v1 == 3 - c2 = v1 != 2 - print(c1) - print(c1.Var()) - print(c2) - print(c2.Var()) - e3 = v1 + c1 - print(e3) - e4 = c1 + c2 == 1 - print(e4) - e5 = solver.Min(c1, c2) - print(e5) - e6 = solver.Max([c1, c2, e3]) - print(e6) - e7 = 1 + c2 - print(e7) - e8 = solver.Max([v1 > 3, v1 <= 2, v2, v2 <= 0, v2 > 5]) - print(e8) - e9 = solver.Min([v1 == v2, v1 != v2, v1 < v2, v1 > v2, v1 <= v2, v1 >= v2]) - print(e9) - - def testAllowedAssignment(self): - solver = pywrapcp.Solver("testAllowedAssignment") - - v1 = solver.IntVar(0, 10, "v1") - v2 = solver.IntVar(0, 10, "v2") - v3 = solver.IntVar(0, 10, "v3") - - tuples = [(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4)] - dvars = [v1, v2, v3] - solver.Add(solver.AllowedAssignments(dvars, tuples)) - db = solver.Phase(dvars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - - solver.NewSearch(db) - counter = 0 - while solver.NextSolution(): - self.assertEqual(counter, v1.Value()) - self.assertEqual(counter, v2.Value()) - self.assertEqual(counter, v3.Value()) - counter += 1 - solver.EndSearch() - self.assertEqual(5, counter) - - def testAllowedAssignment2(self): - solver = pywrapcp.Solver("testAllowedAssignment") - - v1 = solver.IntVar(0, 10, "v1") - v2 = solver.IntVar(0, 10, "v2") - v3 = solver.IntVar(0, 10, "v3") - - dvars = [v1, v2, v3] - solver.Add(solver.AllowedAssignments(dvars, [(x, x, x) for x in range(5)])) - db = solver.Phase(dvars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - - solver.NewSearch(db) - counter = 0 - while solver.NextSolution(): - self.assertEqual(counter, v1.Value()) - self.assertEqual(counter, v2.Value()) - self.assertEqual(counter, v3.Value()) - counter += 1 - solver.EndSearch() - self.assertEqual(5, counter) - - def testIntExprToIntVarCast(self): - solver = pywrapcp.Solver("testIntExprToIntVarCast") - - var1 = solver.IntVar(0, 10, "var1") - var2 = solver.IntVar(0, 10, "var2") - values = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0] - # This test fails if the cast is not correctly done. - expr = (var1 + var2).IndexOf(values) - self.assertTrue(expr) - - def testIntExprToIntVarCastInSolution(self): - solver = pywrapcp.Solver("testIntExprToIntVarCastInSolution") - - var1 = solver.IntVar(0, 10, "var1") - var2 = solver.IntVar(0, 10, "var2") - solution = solver.Assignment() - expr = var1 + var2 - solution.Add(expr) - solution.Store() - # The next line fails if the cast is not correctly done. - self.assertEqual(20, solution.Max(expr)) - - def testIndexOf(self): - solver = pywrapcp.Solver("element") - index = solver.IntVar(0, 2, "index") - element = index.IndexOf([1, 2, 3]) - self.assertEqual(1, element.Min()) - self.assertEqual(3, element.Max()) - - def testElementFunction(self): - solver = pywrapcp.Solver("element") - index = solver.IntVar(0, 2, "index") - element = solver.ElementFunction(inc_callback, index) - self.assertEqual(1, element.Min()) - self.assertEqual(3, element.Max()) - - def testElementMethod(self): - solver = pywrapcp.Solver("element") - index = solver.IntVar(0, 2, "index") - class_callback = ClassIncCallback(2) - class_method = class_callback.inc_method - self.assertEqual(5, class_method(3)) - element = solver.ElementFunction(class_method, index) - self.assertEqual(2, element.Min()) - self.assertEqual(4, element.Max()) - - # TODO(user): better test all other ForwardSequence methods. - def testForwardSequence(self): - solver = pywrapcp.Solver("element") - intervals = [ - solver.FixedDurationIntervalVar(0, 10, 5, False, "Youpi") for _ in range(10) - ] - disjunction = solver.DisjunctiveConstraint(intervals, "Blup") - sequence = disjunction.SequenceVar() - assignment = solver.Assignment() - assignment.Add(sequence) - assignment.SetForwardSequence(sequence, [1, 3, 5]) - self.assertListEqual(assignment.ForwardSequence(sequence), [1, 3, 5]) - - def test_member(self): - solver = pywrapcp.Solver("test member") - x = solver.IntVar(1, 10, "x") - ct = x.Member([1, 2, 3, 5]) - print("Constraint: {}".format(ct)) - - def test_sparse_var(self): - solver = pywrapcp.Solver("test sparse") - x = solver.IntVar([1, 3, 5], "x") - self.assertTrue(x.Contains(1)) - self.assertFalse(x.Contains(2)) - # print(x) - - def test_modulo(self): - solver = pywrapcp.Solver("test modulo") - x = solver.IntVar(0, 10, "x") - y = solver.IntVar(2, 4, "y") - print(x % 3) - print(x % y) - - def test_modulo2(self): - solver = pywrapcp.Solver("test modulo") - x = solver.IntVar([-7, 7], "x") - y = solver.IntVar([-4, 4], "y") - z = (x % y).Var() - t = (x // y).Var() - db = solver.Phase([x, y], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - solver.NewSearch(db) - while solver.NextSolution(): - print( - "x = %d, y = %d, x %% y = %d, x div y = %d" - % (x.Value(), y.Value(), z.Value(), t.Value()) - ) - solver.EndSearch() - - def test_limit(self): - solver = pywrapcp.Solver("test limit") - # limit_proto = solver.DefaultSearchLimitParameters() - limit_proto = search_limit_pb2.RegularLimitParameters(time=10000, branches=10) - print("limit proto: {}".format(limit_proto)) - limit = solver.Limit(limit_proto) - print("limit: {}".format(limit)) - - def test_export(self): - solver = pywrapcp.Solver("test export") - x = solver.IntVar(1, 10, "x") - ct = x.Member([1, 2, 3, 5]) - solver.Add(ct) - # proto = model_pb2.CpModel() - # proto.model = 'wrong name' - # solver.ExportModel(proto) - # print(repr(proto)) - # print(str(proto)) - - def test_size_1_var(self): - solver = pywrapcp.Solver("test_size_1_var") - x = solver.IntVar([0], "x") - self.assertTrue(x.Contains(0)) - self.assertFalse(x.Contains(1)) - - def test_cumulative_api(self): - solver = pywrapcp.Solver("Problem") - - # Vars - intervals = [ - solver.FixedDurationIntervalVar(0, 10, 5, False, "S_%s" % a) - for a in range(10) - ] - demands = [a % 3 + 2 for a in range(10)] - capacity = solver.IntVar(2, 5) - solver.Add(solver.Cumulative(intervals, demands, capacity, "cumul")) - - def test_search_alldiff(self): - solver = pywrapcp.Solver("test_search_alldiff") - in_pos = [solver.IntVar(0, 7, "%i" % i) for i in range(8)] - solver.Add(solver.AllDifferent(in_pos)) - aux_phase = solver.Phase( - in_pos, solver.CHOOSE_LOWEST_MIN, solver.ASSIGN_MAX_VALUE - ) - collector = solver.FirstSolutionCollector() - for i in range(8): - collector.Add(in_pos[i]) - solver.Solve(aux_phase, [collector]) - for i in range(8): - print(collector.Value(0, in_pos[i])) + def testRabbitPheasant(self): + # Create the solver. + solver = pywrapcp.Solver("testRabbitPheasant") + + # Create the variables. + pheasant = solver.IntVar(0, 100, "pheasant") + rabbit = solver.IntVar(0, 100, "rabbit") + + # Create the constraints. + solver.Add(pheasant + rabbit == 20) + solver.Add(pheasant * 2 + rabbit * 4 == 56) + + # Create the search phase. + db = solver.Phase( + [rabbit, pheasant], solver.INT_VAR_DEFAULT, solver.ASSIGN_MIN_VALUE + ) + + # Create assignment + solution = solver.Assignment() + solution.Add(rabbit) + solution.Add(pheasant) + + collector = solver.FirstSolutionCollector(solution) + + # And solve. + solver.Solve(db, collector) + + self.assertEqual(1, collector.SolutionCount()) + current = collector.Solution(0) + + self.assertEqual(12, current.Value(pheasant)) + self.assertEqual(8, current.Value(rabbit)) + + def testSolverParameters(self): + # Create the parameters. + params = pywrapcp.Solver.DefaultSolverParameters() + self.assertIsInstance( + params, solver_parameters_pb2.ConstraintSolverParameters + ) + self.assertFalse(params.trace_propagation) + params.trace_propagation = True + self.assertTrue(params.trace_propagation) + + # Create the solver. + solver = pywrapcp.Solver("testRabbitPheasantWithParameters", params) + inside_params = solver.Parameters() + self.assertTrue(inside_params.trace_propagation) + + def testSolverParametersFields(self): + params = solver_parameters_pb2.ConstraintSolverParameters() + bool_params = [ + "store_names", + "name_cast_variables", + "name_all_variables", + "profile_propagation", + "trace_propagation", + "trace_search", + "print_model", + "print_model_stats", + "print_added_constraints", + "disable_solve", + ] + for p in bool_params: + for v in [True, False]: + setattr(params, p, v) + self.assertEqual(getattr(params, p), v) + + int_params = ["trail_block_size", "array_split_size"] + for p in int_params: + for v in [10, 100]: + setattr(params, p, v) + self.assertEqual(getattr(params, p), v) + + string_params = ["profile_file"] + for p in string_params: + for v in ["", "tmp_file"]: + setattr(params, p, v) + self.assertEqual(getattr(params, p), v) + + def testIntVarAPI(self): + # Create the solver. + solver = pywrapcp.Solver("testIntVarAPI") + + c = solver.IntConst(3, "c") + self.assertEqual(3, c.Min()) + self.assertEqual(3, c.Max()) + self.assertEqual(3, c.Value()) + self.assertTrue(c.Bound()) + + b = solver.BoolVar("b") + self.assertEqual(0, b.Min()) + self.assertEqual(1, b.Max()) + + v1 = solver.IntVar(3, 10, "v1") + self.assertEqual(3, v1.Min()) + self.assertEqual(10, v1.Max()) + + v2 = solver.IntVar([1, 5, 3], "v2") + self.assertEqual(1, v2.Min()) + self.assertEqual(5, v2.Max()) + self.assertEqual(3, v2.Size()) + + # pylint: disable=too-many-statements + def testIntegerArithmetic(self): + solver = pywrapcp.Solver("testIntegerArithmetic") + + v1 = solver.IntVar(0, 10, "v1") + v2 = solver.IntVar(0, 10, "v2") + v3 = solver.IntVar(0, 10, "v3") + + e1 = v1 + v2 + e2 = v1 + 2 + e3 = solver.Sum([v1, v2, v3 * 3]) + + e4 = v1 - 3 + e5 = v1 - v2 + e6 = -v1 + + e7 = abs(e6) + e8 = v3.Square() + + e9 = v1 * 3 + e10 = v1 * v2 + + e11 = v2.IndexOf([0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) + e11b = v2.IndexOf([0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20]) + e12 = solver.Min(e1, e2) + e13 = solver.Min(e3, 3) + e14 = solver.Min([e1 + 1, e2 + 2, e3 + 3]) + + e15 = solver.Max(e1, e2) + e16 = solver.Max(e3, 3) + e17 = solver.Max([e1 + 1, e2 + 2, e3 + 3]) + + solver.Add(v1 == 1) + solver.Add(v2 == 2) + solver.Add(v3 == 3) + + db = solver.Phase( + [v1, v2, v3], solver.INT_VAR_DEFAULT, solver.ASSIGN_MIN_VALUE + ) + + solver.NewSearch(db) + solver.NextSolution() + + self.assertEqual(1, v1.Value()) + self.assertEqual(2, v2.Value()) + self.assertEqual(3, v3.Value()) + + self.assertEqual(3, e1.Min()) + self.assertEqual(3, e1.Max()) + self.assertEqual(3, e2.Min()) + self.assertEqual(3, e2.Max()) + self.assertEqual(12, e3.Min()) + self.assertEqual(12, e3.Max()) + self.assertEqual(-2, e4.Min()) + self.assertEqual(-2, e4.Max()) + self.assertEqual(-1, e5.Min()) + self.assertEqual(-1, e5.Max()) + self.assertEqual(-1, e6.Min()) + self.assertEqual(-1, e6.Max()) + self.assertEqual(1, e7.Min()) + self.assertEqual(1, e7.Max()) + self.assertEqual(9, e8.Min()) + self.assertEqual(9, e8.Max()) + self.assertEqual(3, e9.Min()) + self.assertEqual(3, e9.Max()) + self.assertEqual(2, e10.Min()) + self.assertEqual(2, e10.Max()) + self.assertEqual(4, e11.Min()) + self.assertEqual(4, e11.Max()) + self.assertEqual(4, e11b.Min()) + self.assertEqual(4, e11b.Max()) + self.assertEqual(3, e12.Min()) + self.assertEqual(3, e12.Max()) + self.assertEqual(3, e13.Min()) + self.assertEqual(3, e13.Max()) + self.assertEqual(4, e14.Min()) + self.assertEqual(4, e14.Max()) + self.assertEqual(3, e15.Min()) + self.assertEqual(3, e15.Max()) + self.assertEqual(12, e16.Min()) + self.assertEqual(12, e16.Max()) + self.assertEqual(15, e17.Min()) + self.assertEqual(15, e17.Max()) + solver.EndSearch() + + def testStatusVar(self): + solver = pywrapcp.Solver("testStatusVar") + v1 = solver.IntVar(0, 10, "v1") + v2 = solver.IntVar(0, 10, "v2") + c1 = v1 == 3 + c2 = v1 != 2 + print(c1) + print(c1.Var()) + print(c2) + print(c2.Var()) + e3 = v1 + c1 + print(e3) + e4 = c1 + c2 == 1 + print(e4) + e5 = solver.Min(c1, c2) + print(e5) + e6 = solver.Max([c1, c2, e3]) + print(e6) + e7 = 1 + c2 + print(e7) + e8 = solver.Max([v1 > 3, v1 <= 2, v2, v2 <= 0, v2 > 5]) + print(e8) + e9 = solver.Min([v1 == v2, v1 != v2, v1 < v2, v1 > v2, v1 <= v2, v1 >= v2]) + print(e9) + + def testAllowedAssignment(self): + solver = pywrapcp.Solver("testAllowedAssignment") + + v1 = solver.IntVar(0, 10, "v1") + v2 = solver.IntVar(0, 10, "v2") + v3 = solver.IntVar(0, 10, "v3") + + tuples = [(0, 0, 0), (1, 1, 1), (2, 2, 2), (3, 3, 3), (4, 4, 4)] + dvars = [v1, v2, v3] + solver.Add(solver.AllowedAssignments(dvars, tuples)) + db = solver.Phase( + dvars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + + solver.NewSearch(db) + counter = 0 + while solver.NextSolution(): + self.assertEqual(counter, v1.Value()) + self.assertEqual(counter, v2.Value()) + self.assertEqual(counter, v3.Value()) + counter += 1 + solver.EndSearch() + self.assertEqual(5, counter) + + def testAllowedAssignment2(self): + solver = pywrapcp.Solver("testAllowedAssignment") + + v1 = solver.IntVar(0, 10, "v1") + v2 = solver.IntVar(0, 10, "v2") + v3 = solver.IntVar(0, 10, "v3") + + dvars = [v1, v2, v3] + solver.Add(solver.AllowedAssignments(dvars, [(x, x, x) for x in range(5)])) + db = solver.Phase( + dvars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + + solver.NewSearch(db) + counter = 0 + while solver.NextSolution(): + self.assertEqual(counter, v1.Value()) + self.assertEqual(counter, v2.Value()) + self.assertEqual(counter, v3.Value()) + counter += 1 + solver.EndSearch() + self.assertEqual(5, counter) + + def testIntExprToIntVarCast(self): + solver = pywrapcp.Solver("testIntExprToIntVarCast") + + var1 = solver.IntVar(0, 10, "var1") + var2 = solver.IntVar(0, 10, "var2") + values = [1, 3, 5, 7, 9, 2, 4, 6, 8, 0] + # This test fails if the cast is not correctly done. + expr = (var1 + var2).IndexOf(values) + self.assertTrue(expr) + + def testIntExprToIntVarCastInSolution(self): + solver = pywrapcp.Solver("testIntExprToIntVarCastInSolution") + + var1 = solver.IntVar(0, 10, "var1") + var2 = solver.IntVar(0, 10, "var2") + solution = solver.Assignment() + expr = var1 + var2 + solution.Add(expr) + solution.Store() + # The next line fails if the cast is not correctly done. + self.assertEqual(20, solution.Max(expr)) + + def testIndexOf(self): + solver = pywrapcp.Solver("element") + index = solver.IntVar(0, 2, "index") + element = index.IndexOf([1, 2, 3]) + self.assertEqual(1, element.Min()) + self.assertEqual(3, element.Max()) + + def testElementFunction(self): + solver = pywrapcp.Solver("element") + index = solver.IntVar(0, 2, "index") + element = solver.ElementFunction(inc_callback, index) + self.assertEqual(1, element.Min()) + self.assertEqual(3, element.Max()) + + def testElementMethod(self): + solver = pywrapcp.Solver("element") + index = solver.IntVar(0, 2, "index") + class_callback = ClassIncCallback(2) + class_method = class_callback.inc_method + self.assertEqual(5, class_method(3)) + element = solver.ElementFunction(class_method, index) + self.assertEqual(2, element.Min()) + self.assertEqual(4, element.Max()) + + # TODO(user): better test all other ForwardSequence methods. + def testForwardSequence(self): + solver = pywrapcp.Solver("element") + intervals = [ + solver.FixedDurationIntervalVar(0, 10, 5, False, "Youpi") + for _ in range(10) + ] + disjunction = solver.DisjunctiveConstraint(intervals, "Blup") + sequence = disjunction.SequenceVar() + assignment = solver.Assignment() + assignment.Add(sequence) + assignment.SetForwardSequence(sequence, [1, 3, 5]) + self.assertListEqual(assignment.ForwardSequence(sequence), [1, 3, 5]) + + def test_member(self): + solver = pywrapcp.Solver("test member") + x = solver.IntVar(1, 10, "x") + ct = x.Member([1, 2, 3, 5]) + print("Constraint: {}".format(ct)) + + def test_sparse_var(self): + solver = pywrapcp.Solver("test sparse") + x = solver.IntVar([1, 3, 5], "x") + self.assertTrue(x.Contains(1)) + self.assertFalse(x.Contains(2)) + # print(x) + + def test_modulo(self): + solver = pywrapcp.Solver("test modulo") + x = solver.IntVar(0, 10, "x") + y = solver.IntVar(2, 4, "y") + print(x % 3) + print(x % y) + + def test_modulo2(self): + solver = pywrapcp.Solver("test modulo") + x = solver.IntVar([-7, 7], "x") + y = solver.IntVar([-4, 4], "y") + z = (x % y).Var() + t = (x // y).Var() + db = solver.Phase( + [x, y], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + solver.NewSearch(db) + while solver.NextSolution(): + print( + "x = %d, y = %d, x %% y = %d, x div y = %d" + % (x.Value(), y.Value(), z.Value(), t.Value()) + ) + solver.EndSearch() + + def test_limit(self): + solver = pywrapcp.Solver("test limit") + # limit_proto = solver.DefaultSearchLimitParameters() + limit_proto = search_limit_pb2.RegularLimitParameters( + time=10000, branches=10 + ) + print("limit proto: {}".format(limit_proto)) + limit = solver.Limit(limit_proto) + print("limit: {}".format(limit)) + + def test_export(self): + solver = pywrapcp.Solver("test export") + x = solver.IntVar(1, 10, "x") + ct = x.Member([1, 2, 3, 5]) + solver.Add(ct) + # proto = model_pb2.CpModel() + # proto.model = 'wrong name' + # solver.ExportModel(proto) + # print(repr(proto)) + # print(str(proto)) + + def test_size_1_var(self): + solver = pywrapcp.Solver("test_size_1_var") + x = solver.IntVar([0], "x") + self.assertTrue(x.Contains(0)) + self.assertFalse(x.Contains(1)) + + def test_cumulative_api(self): + solver = pywrapcp.Solver("Problem") + + # Vars + intervals = [ + solver.FixedDurationIntervalVar(0, 10, 5, False, "S_%s" % a) + for a in range(10) + ] + demands = [a % 3 + 2 for a in range(10)] + capacity = solver.IntVar(2, 5) + solver.Add(solver.Cumulative(intervals, demands, capacity, "cumul")) + + def test_search_alldiff(self): + solver = pywrapcp.Solver("test_search_alldiff") + in_pos = [solver.IntVar(0, 7, "%i" % i) for i in range(8)] + solver.Add(solver.AllDifferent(in_pos)) + aux_phase = solver.Phase( + in_pos, solver.CHOOSE_LOWEST_MIN, solver.ASSIGN_MAX_VALUE + ) + collector = solver.FirstSolutionCollector() + for i in range(8): + collector.Add(in_pos[i]) + solver.Solve(aux_phase, [collector]) + for i in range(8): + print(collector.Value(0, in_pos[i])) class CustomSearchMonitor(pywrapcp.SearchMonitor): - def __init__(self, solver, nexts): - pywrapcp.SearchMonitor.__init__(self, solver) - self._nexts = nexts + def __init__(self, solver, nexts): + pywrapcp.SearchMonitor.__init__(self, solver) + self._nexts = nexts - def BeginInitialPropagation(self): - print(self._nexts) + def BeginInitialPropagation(self): + print(self._nexts) - def EndInitialPropagation(self): - print(self._nexts) + def EndInitialPropagation(self): + print(self._nexts) class SearchMonitorTest(absltest.TestCase): - def test_search_monitor(self): - print("test_search_monitor") - solver = pywrapcp.Solver("test search monitor") - x = solver.IntVar(1, 10, "x") - ct = x == 3 - solver.Add(ct) - db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - monitor = CustomSearchMonitor(solver, x) - solver.Solve(db, monitor) + def test_search_monitor(self): + print("test_search_monitor") + solver = pywrapcp.Solver("test search monitor") + x = solver.IntVar(1, 10, "x") + ct = x == 3 + solver.Add(ct) + db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) + monitor = CustomSearchMonitor(solver, x) + solver.Solve(db, monitor) class CustomDemon(pywrapcp.PyDemon): - def __init__(self, x): - super().__init__() - self._x = x - print("Demon built") + def __init__(self, x): + super().__init__() + self._x = x + print("Demon built") - def Run(self, solver): - print("in Run(), saw " + str(self._x)) + def Run(self, solver): + print("in Run(), saw " + str(self._x)) class DemonTest(absltest.TestCase): - def test_demon(self): - print("test_demon") - solver = pywrapcp.Solver("test export") - x = solver.IntVar(1, 10, "x") - demon = CustomDemon(x) - demon.Run(solver) + def test_demon(self): + print("test_demon") + solver = pywrapcp.Solver("test export") + x = solver.IntVar(1, 10, "x") + demon = CustomDemon(x) + demon.Run(solver) class CustomConstraint(pywrapcp.PyConstraint): - def __init__(self, solver, x): - super().__init__(solver) - self._x = x - print("Constraint built") + def __init__(self, solver, x): + super().__init__(solver) + self._x = x + print("Constraint built") - def Post(self): - print("in Post()", file=sys.stderr) - self._demon = CustomDemon(self._x) - self._x.WhenBound(self._demon) - print("out of Post()", file=sys.stderr) + def Post(self): + print("in Post()", file=sys.stderr) + self._demon = CustomDemon(self._x) + self._x.WhenBound(self._demon) + print("out of Post()", file=sys.stderr) - def InitialPropagate(self): - print("in InitialPropagate()") - self._x.SetMin(5) - print(self._x) - print("out of InitialPropagate()") + def InitialPropagate(self): + print("in InitialPropagate()") + self._x.SetMin(5) + print(self._x) + print("out of InitialPropagate()") - def DebugString(self): - return "CustomConstraint" + def DebugString(self): + return "CustomConstraint" class InitialPropagateDemon(pywrapcp.PyDemon): - def __init__(self, constraint): - super().__init__() - self._ct = constraint + def __init__(self, constraint): + super().__init__() + self._ct = constraint - def Run(self, solver): - self._ct.InitialPropagate() + def Run(self, solver): + self._ct.InitialPropagate() class DumbGreaterOrEqualToFive(pywrapcp.PyConstraint): - def __init__(self, solver, x): - super().__init__(solver) - self._x = x + def __init__(self, solver, x): + super().__init__(solver) + self._x = x - def Post(self): - self._demon = InitialPropagateDemon(self) - self._x.WhenBound(self._demon) + def Post(self): + self._demon = InitialPropagateDemon(self) + self._x.WhenBound(self._demon) - def InitialPropagate(self): - if self._x.Bound(): - if self._x.Value() < 5: - print("Reject %d" % self._x.Value(), file=sys.stderr) - self.solver().Fail() - else: - print("Accept %d" % self._x.Value(), file=sys.stderr) + def InitialPropagate(self): + if self._x.Bound(): + if self._x.Value() < 5: + print("Reject %d" % self._x.Value(), file=sys.stderr) + self.solver().Fail() + else: + print("Accept %d" % self._x.Value(), file=sys.stderr) class WatchDomain(pywrapcp.PyDemon): - def __init__(self, x): - super().__init__() - self._x = x + def __init__(self, x): + super().__init__() + self._x = x - def Run(self, solver): - for i in self._x.HoleIterator(): - print("Removed %d" % i) + def Run(self, solver): + for i in self._x.HoleIterator(): + print("Removed %d" % i) class HoleConstraint(pywrapcp.PyConstraint): - def __init__(self, solver, x): - super().__init__(solver) - self._x = x + def __init__(self, solver, x): + super().__init__(solver) + self._x = x - def Post(self): - self._demon = WatchDomain(self._x) - self._x.WhenDomain(self._demon) + def Post(self): + self._demon = WatchDomain(self._x) + self._x.WhenDomain(self._demon) - def InitialPropagate(self): - self._x.RemoveValue(5) + def InitialPropagate(self): + self._x.RemoveValue(5) class BinarySum(pywrapcp.PyConstraint): - def __init__(self, solver, x, y, z): - super().__init__(solver) - self._x = x - self._y = y - self._z = z - - def Post(self): - self._demon = InitialPropagateDemon(self) - self._x.WhenRange(self._demon) - self._y.WhenRange(self._demon) - self._z.WhenRange(self._demon) - - def InitialPropagate(self): - self._z.SetRange(self._x.Min() + self._y.Min(), self._x.Max() + self._y.Max()) - self._x.SetRange(self._z.Min() - self._y.Max(), self._z.Max() - self._y.Min()) - self._y.SetRange(self._z.Min() - self._x.Max(), self._z.Max() - self._x.Min()) + def __init__(self, solver, x, y, z): + super().__init__(solver) + self._x = x + self._y = y + self._z = z + + def Post(self): + self._demon = InitialPropagateDemon(self) + self._x.WhenRange(self._demon) + self._y.WhenRange(self._demon) + self._z.WhenRange(self._demon) + + def InitialPropagate(self): + self._z.SetRange( + self._x.Min() + self._y.Min(), self._x.Max() + self._y.Max() + ) + self._x.SetRange( + self._z.Min() - self._y.Max(), self._z.Max() - self._y.Min() + ) + self._y.SetRange( + self._z.Min() - self._x.Max(), self._z.Max() - self._x.Min() + ) class ConstraintTest(absltest.TestCase): - def test_member(self): - print("test_member") - solver = pywrapcp.Solver("test member") - x = solver.IntVar(1, 10, "x") - constraint = x.Member([1, 2, 3, 5]) - print(constraint) - - def test_sparse_var(self): - print("test_sparse_var") - solver = pywrapcp.Solver("test_sparse_var") - x = solver.IntVar([1, 3, 5], "x") - print(x) - - def test_modulo(self): - print("test_modulo") - solver = pywrapcp.Solver("test_modulo") - x = solver.IntVar(0, 10, "x") - y = solver.IntVar(2, 4, "y") - print(x % 3) - print(x % y) - - def test_limit(self): - solver = pywrapcp.Solver("test_limit") - # TODO(user): expose the proto-based MakeLimit() API in or-tools and test it - # here. - time = 10000 # ms - branches = 10 - failures = sys.maxsize - solutions = sys.maxsize - smart_time_check = True - cumulative = False - limit = solver.Limit( - time, branches, failures, solutions, smart_time_check, cumulative - ) - print(limit) - - def test_search_monitor(self): - print("test_search_monitor") - solver = pywrapcp.Solver("test search_monitor") - x = solver.IntVar(1, 10, "x") - ct = x == 3 - solver.Add(ct) - db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - monitor = CustomSearchMonitor(solver, x) - solver.Solve(db, monitor) - - def test_constraint(self): - print("test_constraint") - solver = pywrapcp.Solver("test_constraint") - x = solver.IntVar(1, 10, "x") - myct = CustomConstraint(solver, x) - solver.Add(myct) - db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - solver.Solve(db) - - def test_failing_constraint(self): - print("test_failing_constraint") - solver = pywrapcp.Solver("test failing constraint") - x = solver.IntVar(1, 10, "x") - myct = DumbGreaterOrEqualToFive(solver, x) - solver.Add(myct) - db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - solver.Solve(db) - - def test_domain_iterator(self): - print("test_domain_iterator") - solver = pywrapcp.Solver("test_domain_iterator") - x = solver.IntVar([1, 2, 4, 6], "x") - for i in x.DomainIterator(): - print(i) - - def test_hole_iterator(self): - print("test_hole_iterator") - solver = pywrapcp.Solver("test_hole_iterator") - x = solver.IntVar(1, 10, "x") - myct = HoleConstraint(solver, x) - solver.Add(myct) - db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - solver.Solve(db) - - def test_sum_constraint(self): - print("test_sum_constraint") - solver = pywrapcp.Solver("test_sum_constraint") - x = solver.IntVar(1, 5, "x") - y = solver.IntVar(1, 5, "y") - z = solver.IntVar(1, 5, "z") - binary_sum = BinarySum(solver, x, y, z) - solver.Add(binary_sum) - db = solver.Phase( - [x, y, z], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE - ) - solver.NewSearch(db) - while solver.NextSolution(): - print("%d + %d == %d" % (x.Value(), y.Value(), z.Value())) - solver.EndSearch() + def test_member(self): + print("test_member") + solver = pywrapcp.Solver("test member") + x = solver.IntVar(1, 10, "x") + constraint = x.Member([1, 2, 3, 5]) + print(constraint) + + def test_sparse_var(self): + print("test_sparse_var") + solver = pywrapcp.Solver("test_sparse_var") + x = solver.IntVar([1, 3, 5], "x") + print(x) + + def test_modulo(self): + print("test_modulo") + solver = pywrapcp.Solver("test_modulo") + x = solver.IntVar(0, 10, "x") + y = solver.IntVar(2, 4, "y") + print(x % 3) + print(x % y) + + def test_limit(self): + solver = pywrapcp.Solver("test_limit") + # TODO(user): expose the proto-based MakeLimit() API in or-tools and test it + # here. + time = 10000 # ms + branches = 10 + failures = sys.maxsize + solutions = sys.maxsize + smart_time_check = True + cumulative = False + limit = solver.Limit( + time, branches, failures, solutions, smart_time_check, cumulative + ) + print(limit) + + def test_search_monitor(self): + print("test_search_monitor") + solver = pywrapcp.Solver("test search_monitor") + x = solver.IntVar(1, 10, "x") + ct = x == 3 + solver.Add(ct) + db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) + monitor = CustomSearchMonitor(solver, x) + solver.Solve(db, monitor) + + def test_constraint(self): + print("test_constraint") + solver = pywrapcp.Solver("test_constraint") + x = solver.IntVar(1, 10, "x") + myct = CustomConstraint(solver, x) + solver.Add(myct) + db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) + solver.Solve(db) + + def test_failing_constraint(self): + print("test_failing_constraint") + solver = pywrapcp.Solver("test failing constraint") + x = solver.IntVar(1, 10, "x") + myct = DumbGreaterOrEqualToFive(solver, x) + solver.Add(myct) + db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) + solver.Solve(db) + + def test_domain_iterator(self): + print("test_domain_iterator") + solver = pywrapcp.Solver("test_domain_iterator") + x = solver.IntVar([1, 2, 4, 6], "x") + for i in x.DomainIterator(): + print(i) + + def test_hole_iterator(self): + print("test_hole_iterator") + solver = pywrapcp.Solver("test_hole_iterator") + x = solver.IntVar(1, 10, "x") + myct = HoleConstraint(solver, x) + solver.Add(myct) + db = solver.Phase([x], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) + solver.Solve(db) + + def test_sum_constraint(self): + print("test_sum_constraint") + solver = pywrapcp.Solver("test_sum_constraint") + x = solver.IntVar(1, 5, "x") + y = solver.IntVar(1, 5, "y") + z = solver.IntVar(1, 5, "z") + binary_sum = BinarySum(solver, x, y, z) + solver.Add(binary_sum) + db = solver.Phase( + [x, y, z], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + solver.NewSearch(db) + while solver.NextSolution(): + print("%d + %d == %d" % (x.Value(), y.Value(), z.Value())) + solver.EndSearch() class CustomDecisionBuilder(pywrapcp.PyDecisionBuilder): - def __init__(self): - super().__init__() - self._counter = 0 + def __init__(self): + super().__init__() + self._counter = 0 - def Next(self, solver): - print("In Next", file=sys.stderr) - self._counter += 1 - return None + def Next(self, solver): + print("In Next", file=sys.stderr) + self._counter += 1 + return None - def DebugString(self): - return "CustomDecisionBuilder" + def DebugString(self): + return "CustomDecisionBuilder" class CustomDecision(pywrapcp.PyDecision): - def __init__(self): - print("In CustomDecision ctor", file=sys.stderr) - super().__init__() - self._val = 1 - print("Set value to", self._val, file=sys.stderr) + def __init__(self): + print("In CustomDecision ctor", file=sys.stderr) + super().__init__() + self._val = 1 + print("Set value to", self._val, file=sys.stderr) - def Apply(self, solver): - print("In CustomDecision.Apply()", file=sys.stderr) - print("Expect value", self._val, file=sys.stderr) - solver.Fail() + def Apply(self, solver): + print("In CustomDecision.Apply()", file=sys.stderr) + print("Expect value", self._val, file=sys.stderr) + solver.Fail() - def Refute(self, solver): - print("In CustomDecision.Refute()", file=sys.stderr) + def Refute(self, solver): + print("In CustomDecision.Refute()", file=sys.stderr) - def DebugString(self): - return "CustomDecision" + def DebugString(self): + return "CustomDecision" class CustomDecisionBuilderCustomDecision(pywrapcp.PyDecisionBuilder): - def __init__(self): - super().__init__() - self.__done = False - self._counter = 0 + def __init__(self): + super().__init__() + self.__done = False + self._counter = 0 - def Next(self, solver): - print("In CustomDecisionBuilderCustomDecision.Next()", file=sys.stderr) - self._counter += 1 - if not self.__done: - self.__done = True - self.__decision = CustomDecision() - return self.__decision - return None + def Next(self, solver): + print("In CustomDecisionBuilderCustomDecision.Next()", file=sys.stderr) + self._counter += 1 + if not self.__done: + self.__done = True + self.__decision = CustomDecision() + return self.__decision + return None - def DebugString(self): - return "CustomDecisionBuilderCustomDecision" + def DebugString(self): + return "CustomDecisionBuilderCustomDecision" class DecisionTest(absltest.TestCase): - def test_custom_decision_builder(self): - solver = pywrapcp.Solver("test_custom_decision_builder") - db = CustomDecisionBuilder() - print(str(db)) - solver.Solve(db) - self.assertEqual(db._counter, 1) + def test_custom_decision_builder(self): + solver = pywrapcp.Solver("test_custom_decision_builder") + db = CustomDecisionBuilder() + print(str(db)) + solver.Solve(db) + self.assertEqual(db._counter, 1) - def test_custom_decision(self): - solver = pywrapcp.Solver("test_custom_decision") - db = CustomDecisionBuilderCustomDecision() - print(str(db)) - solver.Solve(db) - self.assertEqual(db._counter, 2) + def test_custom_decision(self): + solver = pywrapcp.Solver("test_custom_decision") + db = CustomDecisionBuilderCustomDecision() + print(str(db)) + solver.Solve(db) + self.assertEqual(db._counter, 2) class LocalSearchTest(absltest.TestCase): - class OneVarLNS(pywrapcp.BaseLns): - """One Var LNS.""" - - def __init__(self, int_vars): - super().__init__(int_vars) - self.__index = 0 - - def InitFragments(self): - print("OneVarLNS.InitFragments()...", file=sys.stderr) - self.__index = 0 - - def NextFragment(self): - print("OneVarLNS.NextFragment()...", file=sys.stderr) - if self.__index < self.Size(): - self.AppendToFragment(self.__index) - self.__index += 1 - return True - else: - return False - - class MoveOneVar(pywrapcp.IntVarLocalSearchOperator): - """Move one var up or down.""" - - def __init__(self, int_vars): - super().__init__(int_vars) - self.__index = 0 - self.__up = False - - def OneNeighbor(self): - print("MoveOneVar.OneNeighbor()...", file=sys.stderr) - current_value = self.OldValue(self.__index) - if self.__up: - self.SetValue(self.__index, current_value + 1) - self.__index = (self.__index + 1) % self.Size() - else: - self.SetValue(self.__index, current_value - 1) - self.__up = not self.__up - return True - - def OnStart(self): - print("MoveOneVar.OnStart()...", file=sys.stderr) - pass - - def IsIncremental(self): - return False - - class SumFilter(pywrapcp.IntVarLocalSearchFilter): - """Filter to speed up LS computation.""" - - def __init__(self, int_vars): - super().__init__(int_vars) - self.__sum = 0 - - def OnSynchronize(self, delta): - self.__sum = sum(self.Value(index) for index in range(self.Size())) - - def Accept( - self, - delta, - unused_delta_delta, - unused_objective_min, - unused_objective_max, - ): - solution_delta = delta.IntVarContainer() - solution_delta_size = solution_delta.Size() - for i in range(solution_delta_size): - if not solution_delta.Element(i).Activated(): - return True - new_sum = self.__sum - for i in range(solution_delta_size): - element = solution_delta.Element(i) - int_var = element.Var() - touched_var_index = self.IndexFromVar(int_var) - old_value = self.Value(touched_var_index) - new_value = element.Value() - new_sum += new_value - old_value - - return new_sum < self.__sum - - def IsIncremental(self): - return False - - def solve(self, local_search_type): - solver = pywrapcp.Solver("Solve") - int_vars = [solver.IntVar(0, 4) for _ in range(4)] - sum_var = solver.Sum(int_vars).Var() - objective = solver.Minimize(sum_var, 1) - inner_db = solver.Phase( - int_vars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MAX_VALUE - ) - if local_search_type == 0: # LNS - print("Large Neighborhood Search", file=sys.stderr) - one_var_lns = self.OneVarLNS(int_vars) - ls_params = solver.LocalSearchPhaseParameters( - sum_var, one_var_lns, inner_db - ) - ls = solver.LocalSearchPhase(int_vars, inner_db, ls_params) - elif local_search_type == 1: # LS - print("Local Search", file=sys.stderr) - move_one_var = self.MoveOneVar(int_vars) - ls_params = solver.LocalSearchPhaseParameters( - sum_var, move_one_var, inner_db - ) - ls = solver.LocalSearchPhase(int_vars, inner_db, ls_params) - else: - print("Local Search with Filter", file=sys.stderr) - move_one_var = self.MoveOneVar(int_vars) - sum_filter = self.SumFilter(int_vars) - filter_manager = pywrapcp.LocalSearchFilterManager([sum_filter]) - ls_params = solver.LocalSearchPhaseParameters( - sum_var, move_one_var, inner_db, None, filter_manager - ) - ls = solver.LocalSearchPhase(int_vars, inner_db, ls_params) - - collector = solver.LastSolutionCollector() - collector.Add(int_vars) - collector.AddObjective(sum_var) - log = solver.SearchLog(1000, objective) - solver.Solve(ls, [collector, objective, log]) - print("Objective value = %d" % collector.ObjectiveValue(0), file=sys.stderr) - - def test_large_neighborhood_search(self): - self.solve(0) - - def test_local_search(self): - self.solve(1) - - def test_local_search_with_filter(self): - self.solve(2) + class OneVarLNS(pywrapcp.BaseLns): + """One Var LNS.""" + + def __init__(self, int_vars): + super().__init__(int_vars) + self.__index = 0 + + def InitFragments(self): + print("OneVarLNS.InitFragments()...", file=sys.stderr) + self.__index = 0 + + def NextFragment(self): + print("OneVarLNS.NextFragment()...", file=sys.stderr) + if self.__index < self.Size(): + self.AppendToFragment(self.__index) + self.__index += 1 + return True + else: + return False + + class MoveOneVar(pywrapcp.IntVarLocalSearchOperator): + """Move one var up or down.""" + + def __init__(self, int_vars): + super().__init__(int_vars) + self.__index = 0 + self.__up = False + + def OneNeighbor(self): + print("MoveOneVar.OneNeighbor()...", file=sys.stderr) + current_value = self.OldValue(self.__index) + if self.__up: + self.SetValue(self.__index, current_value + 1) + self.__index = (self.__index + 1) % self.Size() + else: + self.SetValue(self.__index, current_value - 1) + self.__up = not self.__up + return True + + def OnStart(self): + print("MoveOneVar.OnStart()...", file=sys.stderr) + pass + + def IsIncremental(self): + return False + + class SumFilter(pywrapcp.IntVarLocalSearchFilter): + """Filter to speed up LS computation.""" + + def __init__(self, int_vars): + super().__init__(int_vars) + self.__sum = 0 + + def OnSynchronize(self, delta): + self.__sum = sum(self.Value(index) for index in range(self.Size())) + + def Accept( + self, + delta, + unused_delta_delta, + unused_objective_min, + unused_objective_max, + ): + solution_delta = delta.IntVarContainer() + solution_delta_size = solution_delta.Size() + for i in range(solution_delta_size): + if not solution_delta.Element(i).Activated(): + return True + new_sum = self.__sum + for i in range(solution_delta_size): + element = solution_delta.Element(i) + int_var = element.Var() + touched_var_index = self.IndexFromVar(int_var) + old_value = self.Value(touched_var_index) + new_value = element.Value() + new_sum += new_value - old_value + + return new_sum < self.__sum + + def IsIncremental(self): + return False + + def solve(self, local_search_type): + solver = pywrapcp.Solver("Solve") + int_vars = [solver.IntVar(0, 4) for _ in range(4)] + sum_var = solver.Sum(int_vars).Var() + objective = solver.Minimize(sum_var, 1) + inner_db = solver.Phase( + int_vars, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MAX_VALUE + ) + if local_search_type == 0: # LNS + print("Large Neighborhood Search", file=sys.stderr) + one_var_lns = self.OneVarLNS(int_vars) + ls_params = solver.LocalSearchPhaseParameters( + sum_var, one_var_lns, inner_db + ) + ls = solver.LocalSearchPhase(int_vars, inner_db, ls_params) + elif local_search_type == 1: # LS + print("Local Search", file=sys.stderr) + move_one_var = self.MoveOneVar(int_vars) + ls_params = solver.LocalSearchPhaseParameters( + sum_var, move_one_var, inner_db + ) + ls = solver.LocalSearchPhase(int_vars, inner_db, ls_params) + else: + print("Local Search with Filter", file=sys.stderr) + move_one_var = self.MoveOneVar(int_vars) + sum_filter = self.SumFilter(int_vars) + filter_manager = pywrapcp.LocalSearchFilterManager([sum_filter]) + ls_params = solver.LocalSearchPhaseParameters( + sum_var, move_one_var, inner_db, None, filter_manager + ) + ls = solver.LocalSearchPhase(int_vars, inner_db, ls_params) + + collector = solver.LastSolutionCollector() + collector.Add(int_vars) + collector.AddObjective(sum_var) + log = solver.SearchLog(1000, objective) + solver.Solve(ls, [collector, objective, log]) + print("Objective value = %d" % collector.ObjectiveValue(0), file=sys.stderr) + + def test_large_neighborhood_search(self): + self.solve(0) + + def test_local_search(self): + self.solve(1) + + def test_local_search_with_filter(self): + self.solve(2) class MyDecisionBuilder(pywrapcp.PyDecisionBuilder): - def __init__(self, var, value): - super().__init__() - self.__var = var - self.__value = value + def __init__(self, var, value): + super().__init__() + self.__var = var + self.__value = value - def Next(self, solver): - if not self.__var.Bound(): - decision = solver.AssignVariableValue(self.__var, self.__value) - return decision + def Next(self, solver): + if not self.__var.Bound(): + decision = solver.AssignVariableValue(self.__var, self.__value) + return decision class MyLns(pywrapcp.BaseLns): - def __init__(self, int_vars): - super().__init__(int_vars) - self.__current = 0 + def __init__(self, int_vars): + super().__init__(int_vars) + self.__current = 0 - def InitFragments(self): - self.__current = 0 + def InitFragments(self): + self.__current = 0 - def NextFragment(self, fragment, values): - while self.__current < len(values): - if values[self.__current] == 1: - fragment.append(self.__current) - self.__current += 1 - return True - else: - self.__current += 1 + def NextFragment(self, fragment, values): + while self.__current < len(values): + if values[self.__current] == 1: + fragment.append(self.__current) + self.__current += 1 + return True + else: + self.__current += 1 class MyLnsNoValues(pywrapcp.BaseLns): - def __init__(self, int_vars): - super().__init__(int_vars) - self.__current = 0 - self.__size = len(int_vars) + def __init__(self, int_vars): + super().__init__(int_vars) + self.__current = 0 + self.__size = len(int_vars) - def InitFragments(self): - self.__current = 0 + def InitFragments(self): + self.__current = 0 - def NextFragment(self, fragment): - while self.__current < self.__size: - fragment.append(self.__current) - self.__current += 1 - return True + def NextFragment(self, fragment): + while self.__current < self.__size: + fragment.append(self.__current) + self.__current += 1 + return True class MyDecisionBuilderWithRev(pywrapcp.PyDecisionBuilder): - def __init__(self, var, value, rev): - super().__init__() - self.__var = var - self.__value = value - self.__rev = rev + def __init__(self, var, value, rev): + super().__init__() + self.__var = var + self.__value = value + self.__rev = rev - def Next(self, solver): - if not self.__var.Bound(): - if self.__var.Contains(self.__value): - decision = solver.AssignVariableValue(self.__var, self.__value) - self.__rev.SetValue(solver, self.__value) - return decision - else: - return solver.FailDecision() + def Next(self, solver): + if not self.__var.Bound(): + if self.__var.Contains(self.__value): + decision = solver.AssignVariableValue(self.__var, self.__value) + self.__rev.SetValue(solver, self.__value) + return decision + else: + return solver.FailDecision() class MyDecisionBuilderThatFailsWithRev(pywrapcp.PyDecisionBuilder): - def Next(self, solver): - solver.Fail() - return None + def Next(self, solver): + solver.Fail() + return None class PyWrapCPSearchTest(absltest.TestCase): - NUMBER_OF_VARIABLES = 10 - VARIABLE_MIN = 0 - VARIABLE_MAX = 10 - LNS_NEIGHBORS = 100 - LNS_VARIABLES = 4 - DECISION_BUILDER_VALUE = 5 - OTHER_DECISION_BUILDER_VALUE = 2 - - def testNewClassAsDecisionBuilder(self): - solver = pywrapcp.Solver("testNewClassAsDecisionBuilder") - x = solver.IntVar(self.VARIABLE_MIN, self.VARIABLE_MAX, "x") - phase = MyDecisionBuilder(x, self.DECISION_BUILDER_VALUE) - solver.NewSearch(phase) - solver.NextSolution() - self.assertTrue(x.Bound()) - self.assertEqual(self.DECISION_BUILDER_VALUE, x.Min()) - solver.EndSearch() - - def testComposeTwoDecisions(self): - solver = pywrapcp.Solver("testNewClassAsDecisionBuilder") - x = solver.IntVar(0, 10, "x") - y = solver.IntVar(0, 10, "y") - phase_x = MyDecisionBuilder(x, self.DECISION_BUILDER_VALUE) - phase_y = MyDecisionBuilder(y, self.OTHER_DECISION_BUILDER_VALUE) - phase = solver.Compose([phase_x, phase_y]) - solver.NewSearch(phase) - solver.NextSolution() - self.assertTrue(x.Bound()) - self.assertEqual(self.DECISION_BUILDER_VALUE, x.Min()) - self.assertTrue(y.Bound()) - self.assertEqual(self.OTHER_DECISION_BUILDER_VALUE, y.Min()) - solver.EndSearch() - - def testRandomLns(self): - solver = pywrapcp.Solver("testRandomLnsOperator") - x = [solver.BoolVar("x_%d" % i) for i in range(self.NUMBER_OF_VARIABLES)] - lns = solver.RandomLnsOperator(x, self.LNS_VARIABLES) - delta = solver.Assignment() - for _ in range(self.LNS_NEIGHBORS): - delta.Clear() - self.assertTrue(lns.NextNeighbor(delta, delta)) - self.assertLess(0, delta.Size()) - self.assertGreater(self.LNS_VARIABLES + 1, delta.Size()) - - def testCallbackLns(self): - solver = pywrapcp.Solver("testCallbackLNS") - x = [solver.BoolVar("x_%d" % i) for i in range(self.NUMBER_OF_VARIABLES)] - lns = MyLns(x) - solution = solver.Assignment() - solution.Add(x) - for v in x: - solution.SetValue(v, 1) - obj_var = solver.Sum(x) - objective = solver.Minimize(obj_var, 1) - collector = solver.LastSolutionCollector(solution) - inner_db = solver.Phase(x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - - ls_params = solver.LocalSearchPhaseParameters(obj_var.Var(), lns, inner_db) - ls = solver.LocalSearchPhase(x, inner_db, ls_params) - log = solver.SearchLog(1000, objective) - solver.Solve(ls, [collector, objective, log]) - for v in x: - self.assertEqual(0, collector.Solution(0).Value(v)) - - def testCallbackLnsNoValues(self): - solver = pywrapcp.Solver("testCallbackLnsNoValues") - x = [solver.BoolVar("x_%d" % i) for i in range(self.NUMBER_OF_VARIABLES)] - lns = MyLnsNoValues(x) - solution = solver.Assignment() - solution.Add(x) - for v in x: - solution.SetValue(v, 1) - obj_var = solver.Sum(x) - objective = solver.Minimize(obj_var, 1) - collector = solver.LastSolutionCollector(solution) - inner_db = solver.Phase(x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - - ls_params = solver.LocalSearchPhaseParameters(obj_var.Var(), lns, inner_db) - db = solver.LocalSearchPhase(x, inner_db, ls_params) - log = solver.SearchLog(1000, objective) - solver.Solve(db, [collector, objective, log]) - for v in x: - self.assertEqual(0, collector.Solution(0).Value(v)) - - def testConcatenateOperators(self): - solver = pywrapcp.Solver("testConcatenateOperators") - x = [solver.BoolVar("x_%d" % i) for i in range(self.NUMBER_OF_VARIABLES)] - op1 = solver.Operator(x, solver.INCREMENT) - op2 = solver.Operator(x, solver.DECREMENT) - concatenate = solver.ConcatenateOperators([op1, op2]) - solution = solver.Assignment() - solution.Add(x) - for v in x: - solution.SetValue(v, 1) - obj_var = solver.Sum(x) - objective = solver.Minimize(obj_var, 1) - collector = solver.LastSolutionCollector(solution) - ls_params = solver.LocalSearchPhaseParameters(obj_var.Var(), concatenate, None) - db = solver.LocalSearchPhase(solution, ls_params) - solver.Solve(db, [objective, collector]) - for v in x: - self.assertEqual(0, collector.Solution(0).Value(v)) - - def testRevIntegerOutsideSearch(self): - solver = pywrapcp.Solver("testRevValue") - revx = pywrapcp.RevInteger(12) - self.assertEqual(12, revx.Value()) - revx.SetValue(solver, 25) - self.assertEqual(25, revx.Value()) - - def testMemberApi(self): - solver = pywrapcp.Solver("testMemberApi") - x = solver.IntVar(0, 10, "x") - c1 = solver.MemberCt(x, [2, 5]) - c2 = x.Member([2, 5]) - self.assertEqual(str(c1), str(c2)) - c3 = solver.NotMemberCt(x, [2, 7], [4, 9]) - c4 = x.NotMember([2, 7], [4, 9]) - self.assertEqual(str(c3), str(c4)) - - def testRevIntegerInSearch(self): - solver = pywrapcp.Solver("testRevIntegerInSearch") - x = solver.IntVar(0, 10, "x") - rev = pywrapcp.RevInteger(12) - phase = MyDecisionBuilderWithRev(x, 5, rev) - solver.NewSearch(phase) - solver.NextSolution() - self.assertTrue(x.Bound()) - self.assertEqual(5, rev.Value()) - solver.NextSolution() - self.assertFalse(x.Bound()) - self.assertEqual(12, rev.Value()) - solver.EndSearch() - - def testDecisionBuilderThatFails(self): - solver = pywrapcp.Solver("testRevIntegerInSearch") - phase = MyDecisionBuilderThatFailsWithRev() - self.assertFalse(solver.Solve(phase)) - - # ----------------helper for binpacking posting---------------- - - def bin_packing_helper(self, cp, binvars, weights, loadvars): - nbins = len(loadvars) - nitems = len(binvars) - for j in range(nbins): - b = [cp.BoolVar(str(i)) for i in range(nitems)] - for i in range(nitems): - cp.Add(cp.IsEqualCstCt(binvars[i], j, b[i])) - cp.Add( - cp.Sum([b[i] * weights[i] for i in range(nitems)]) == loadvars[j] - ) - cp.Add(cp.Sum(loadvars) == sum(weights)) - - def testNoNewSearch(self): - maxcapa = 44 - weights = [4, 22, 9, 5, 8, 3, 3, 4, 7, 7, 3] - loss = [ - 0, - 11, - 10, - 9, - 8, - 7, - 6, - 5, - 4, - 3, - 2, - 1, - 0, - 1, - 0, - 2, - 1, - 0, - 0, - 0, - 0, - 2, - 1, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - 2, - 1, - 0, - 3, - 2, - 1, - 0, - 2, - 1, - 0, - 0, - 0, - ] - nbslab = 11 - - # ------------------solver and variable declaration------------- - - solver = pywrapcp.Solver("Steel Mill Slab") - x = [solver.IntVar(list(range(nbslab)), "x" + str(i)) for i in range(nbslab)] - l = [solver.IntVar(list(range(maxcapa)), "l" + str(i)) for i in range(nbslab)] - obj = solver.IntVar(list(range(nbslab * maxcapa)), "obj") - - # -------------------post of the constraints-------------- - - self.bin_packing_helper(solver, x, weights[:nbslab], l) - solver.Add(solver.Sum([l[s].IndexOf(loss) for s in range(nbslab)]) == obj) - - unused_sol = [2, 0, 0, 0, 0, 1, 2, 2, 1, 1, 2] - - # ------------start the search and optimization----------- - - unused_objective = solver.Minimize(obj, 1) - unused_db = solver.Phase(x, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT) - # solver.NewSearch(db,[objective]) #segfault if NewSearch is not called. - - while solver.NextSolution(): - print(obj, "check:", sum([loss[l[s].Min()] for s in range(nbslab)])) - print(l) - solver.EndSearch() + NUMBER_OF_VARIABLES = 10 + VARIABLE_MIN = 0 + VARIABLE_MAX = 10 + LNS_NEIGHBORS = 100 + LNS_VARIABLES = 4 + DECISION_BUILDER_VALUE = 5 + OTHER_DECISION_BUILDER_VALUE = 2 + + def testNewClassAsDecisionBuilder(self): + solver = pywrapcp.Solver("testNewClassAsDecisionBuilder") + x = solver.IntVar(self.VARIABLE_MIN, self.VARIABLE_MAX, "x") + phase = MyDecisionBuilder(x, self.DECISION_BUILDER_VALUE) + solver.NewSearch(phase) + solver.NextSolution() + self.assertTrue(x.Bound()) + self.assertEqual(self.DECISION_BUILDER_VALUE, x.Min()) + solver.EndSearch() + + def testComposeTwoDecisions(self): + solver = pywrapcp.Solver("testNewClassAsDecisionBuilder") + x = solver.IntVar(0, 10, "x") + y = solver.IntVar(0, 10, "y") + phase_x = MyDecisionBuilder(x, self.DECISION_BUILDER_VALUE) + phase_y = MyDecisionBuilder(y, self.OTHER_DECISION_BUILDER_VALUE) + phase = solver.Compose([phase_x, phase_y]) + solver.NewSearch(phase) + solver.NextSolution() + self.assertTrue(x.Bound()) + self.assertEqual(self.DECISION_BUILDER_VALUE, x.Min()) + self.assertTrue(y.Bound()) + self.assertEqual(self.OTHER_DECISION_BUILDER_VALUE, y.Min()) + solver.EndSearch() + + def testRandomLns(self): + solver = pywrapcp.Solver("testRandomLnsOperator") + x = [solver.BoolVar("x_%d" % i) for i in range(self.NUMBER_OF_VARIABLES)] + lns = solver.RandomLnsOperator(x, self.LNS_VARIABLES) + delta = solver.Assignment() + for _ in range(self.LNS_NEIGHBORS): + delta.Clear() + self.assertTrue(lns.NextNeighbor(delta, delta)) + self.assertLess(0, delta.Size()) + self.assertGreater(self.LNS_VARIABLES + 1, delta.Size()) + + def testCallbackLns(self): + solver = pywrapcp.Solver("testCallbackLNS") + x = [solver.BoolVar("x_%d" % i) for i in range(self.NUMBER_OF_VARIABLES)] + lns = MyLns(x) + solution = solver.Assignment() + solution.Add(x) + for v in x: + solution.SetValue(v, 1) + obj_var = solver.Sum(x) + objective = solver.Minimize(obj_var, 1) + collector = solver.LastSolutionCollector(solution) + inner_db = solver.Phase( + x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + + ls_params = solver.LocalSearchPhaseParameters(obj_var.Var(), lns, inner_db) + ls = solver.LocalSearchPhase(x, inner_db, ls_params) + log = solver.SearchLog(1000, objective) + solver.Solve(ls, [collector, objective, log]) + for v in x: + self.assertEqual(0, collector.Solution(0).Value(v)) + + def testCallbackLnsNoValues(self): + solver = pywrapcp.Solver("testCallbackLnsNoValues") + x = [solver.BoolVar("x_%d" % i) for i in range(self.NUMBER_OF_VARIABLES)] + lns = MyLnsNoValues(x) + solution = solver.Assignment() + solution.Add(x) + for v in x: + solution.SetValue(v, 1) + obj_var = solver.Sum(x) + objective = solver.Minimize(obj_var, 1) + collector = solver.LastSolutionCollector(solution) + inner_db = solver.Phase( + x, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + + ls_params = solver.LocalSearchPhaseParameters(obj_var.Var(), lns, inner_db) + db = solver.LocalSearchPhase(x, inner_db, ls_params) + log = solver.SearchLog(1000, objective) + solver.Solve(db, [collector, objective, log]) + for v in x: + self.assertEqual(0, collector.Solution(0).Value(v)) + + def testConcatenateOperators(self): + solver = pywrapcp.Solver("testConcatenateOperators") + x = [solver.BoolVar("x_%d" % i) for i in range(self.NUMBER_OF_VARIABLES)] + op1 = solver.Operator(x, solver.INCREMENT) + op2 = solver.Operator(x, solver.DECREMENT) + concatenate = solver.ConcatenateOperators([op1, op2]) + solution = solver.Assignment() + solution.Add(x) + for v in x: + solution.SetValue(v, 1) + obj_var = solver.Sum(x) + objective = solver.Minimize(obj_var, 1) + collector = solver.LastSolutionCollector(solution) + ls_params = solver.LocalSearchPhaseParameters( + obj_var.Var(), concatenate, None + ) + db = solver.LocalSearchPhase(solution, ls_params) + solver.Solve(db, [objective, collector]) + for v in x: + self.assertEqual(0, collector.Solution(0).Value(v)) + + def testRevIntegerOutsideSearch(self): + solver = pywrapcp.Solver("testRevValue") + revx = pywrapcp.RevInteger(12) + self.assertEqual(12, revx.Value()) + revx.SetValue(solver, 25) + self.assertEqual(25, revx.Value()) + + def testMemberApi(self): + solver = pywrapcp.Solver("testMemberApi") + x = solver.IntVar(0, 10, "x") + c1 = solver.MemberCt(x, [2, 5]) + c2 = x.Member([2, 5]) + self.assertEqual(str(c1), str(c2)) + c3 = solver.NotMemberCt(x, [2, 7], [4, 9]) + c4 = x.NotMember([2, 7], [4, 9]) + self.assertEqual(str(c3), str(c4)) + + def testRevIntegerInSearch(self): + solver = pywrapcp.Solver("testRevIntegerInSearch") + x = solver.IntVar(0, 10, "x") + rev = pywrapcp.RevInteger(12) + phase = MyDecisionBuilderWithRev(x, 5, rev) + solver.NewSearch(phase) + solver.NextSolution() + self.assertTrue(x.Bound()) + self.assertEqual(5, rev.Value()) + solver.NextSolution() + self.assertFalse(x.Bound()) + self.assertEqual(12, rev.Value()) + solver.EndSearch() + + def testDecisionBuilderThatFails(self): + solver = pywrapcp.Solver("testRevIntegerInSearch") + phase = MyDecisionBuilderThatFailsWithRev() + self.assertFalse(solver.Solve(phase)) + + # ----------------helper for binpacking posting---------------- + + def bin_packing_helper(self, cp, binvars, weights, loadvars): + nbins = len(loadvars) + nitems = len(binvars) + for j in range(nbins): + b = [cp.BoolVar(str(i)) for i in range(nitems)] + for i in range(nitems): + cp.Add(cp.IsEqualCstCt(binvars[i], j, b[i])) + cp.Add( + cp.Sum([b[i] * weights[i] for i in range(nitems)]) == loadvars[j] + ) + cp.Add(cp.Sum(loadvars) == sum(weights)) + + def testNoNewSearch(self): + maxcapa = 44 + weights = [4, 22, 9, 5, 8, 3, 3, 4, 7, 7, 3] + loss = [ + 0, + 11, + 10, + 9, + 8, + 7, + 6, + 5, + 4, + 3, + 2, + 1, + 0, + 1, + 0, + 2, + 1, + 0, + 0, + 0, + 0, + 2, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 2, + 1, + 0, + 3, + 2, + 1, + 0, + 2, + 1, + 0, + 0, + 0, + ] + nbslab = 11 + + # ------------------solver and variable declaration------------- + + solver = pywrapcp.Solver("Steel Mill Slab") + x = [ + solver.IntVar(list(range(nbslab)), "x" + str(i)) for i in range(nbslab) + ] + l = [ + solver.IntVar(list(range(maxcapa)), "l" + str(i)) for i in range(nbslab) + ] + obj = solver.IntVar(list(range(nbslab * maxcapa)), "obj") + + # -------------------post of the constraints-------------- + + self.bin_packing_helper(solver, x, weights[:nbslab], l) + solver.Add(solver.Sum([l[s].IndexOf(loss) for s in range(nbslab)]) == obj) + + unused_sol = [2, 0, 0, 0, 0, 1, 2, 2, 1, 1, 2] + + # ------------start the search and optimization----------- + + unused_objective = solver.Minimize(obj, 1) + unused_db = solver.Phase( + x, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT + ) + # solver.NewSearch(db,[objective]) #segfault if NewSearch is not called. + + while solver.NextSolution(): + print(obj, "check:", sum([loss[l[s].Min()] for s in range(nbslab)])) + print(l) + solver.EndSearch() class SplitDomainDecisionBuilder(pywrapcp.PyDecisionBuilder): - def __init__(self, var, value, lower): - super().__init__() - self.__var = var - self.__value = value - self.__lower = lower - self.__done = pywrapcp.RevBool(False) + def __init__(self, var, value, lower): + super().__init__() + self.__var = var + self.__value = value + self.__lower = lower + self.__done = pywrapcp.RevBool(False) - def Next(self, solver): - if self.__done.Value(): - return None - self.__done.SetValue(solver, True) - return solver.SplitVariableDomain(self.__var, self.__value, self.__lower) + def Next(self, solver): + if self.__done.Value(): + return None + self.__done.SetValue(solver, True) + return solver.SplitVariableDomain(self.__var, self.__value, self.__lower) class PyWrapCPDecisionTest(absltest.TestCase): - def testSplitDomainLower(self): - solver = pywrapcp.Solver("testSplitDomainLower") - x = solver.IntVar(0, 10, "x") - phase = SplitDomainDecisionBuilder(x, 3, True) - solver.NewSearch(phase) - self.assertTrue(solver.NextSolution()) - self.assertEqual(0, x.Min()) - self.assertEqual(3, x.Max()) - self.assertTrue(solver.NextSolution()) - self.assertEqual(4, x.Min()) - self.assertEqual(10, x.Max()) - self.assertFalse(solver.NextSolution()) - solver.EndSearch() - - def testSplitDomainUpper(self): - solver = pywrapcp.Solver("testSplitDomainUpper") - x = solver.IntVar(0, 10, "x") - phase = SplitDomainDecisionBuilder(x, 6, False) - solver.NewSearch(phase) - self.assertTrue(solver.NextSolution()) - self.assertEqual(7, x.Min()) - self.assertEqual(10, x.Max()) - self.assertTrue(solver.NextSolution()) - self.assertEqual(0, x.Min()) - self.assertEqual(6, x.Max()) - self.assertFalse(solver.NextSolution()) - solver.EndSearch() - - def testTrueConstraint(self): - solver = pywrapcp.Solver("test") - x1 = solver.IntVar(4, 8, "x1") - x2 = solver.IntVar(3, 7, "x2") - x3 = solver.IntVar(1, 5, "x3") - solver.Add((x1 >= 3) + (x2 >= 6) + (x3 <= 3) == 3) - db = solver.Phase( - [x1, x2, x3], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE - ) - solver.NewSearch(db) - solver.NextSolution() - solver.EndSearch() - - def testFalseConstraint(self): - solver = pywrapcp.Solver("test") - x1 = solver.IntVar(4, 8, "x1") - x2 = solver.IntVar(3, 7, "x2") - x3 = solver.IntVar(1, 5, "x3") - solver.Add((x1 <= 3) + (x2 >= 6) + (x3 <= 3) == 3) - db = solver.Phase( - [x1, x2, x3], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE - ) - solver.NewSearch(db) - solver.NextSolution() - solver.EndSearch() + def testSplitDomainLower(self): + solver = pywrapcp.Solver("testSplitDomainLower") + x = solver.IntVar(0, 10, "x") + phase = SplitDomainDecisionBuilder(x, 3, True) + solver.NewSearch(phase) + self.assertTrue(solver.NextSolution()) + self.assertEqual(0, x.Min()) + self.assertEqual(3, x.Max()) + self.assertTrue(solver.NextSolution()) + self.assertEqual(4, x.Min()) + self.assertEqual(10, x.Max()) + self.assertFalse(solver.NextSolution()) + solver.EndSearch() + + def testSplitDomainUpper(self): + solver = pywrapcp.Solver("testSplitDomainUpper") + x = solver.IntVar(0, 10, "x") + phase = SplitDomainDecisionBuilder(x, 6, False) + solver.NewSearch(phase) + self.assertTrue(solver.NextSolution()) + self.assertEqual(7, x.Min()) + self.assertEqual(10, x.Max()) + self.assertTrue(solver.NextSolution()) + self.assertEqual(0, x.Min()) + self.assertEqual(6, x.Max()) + self.assertFalse(solver.NextSolution()) + solver.EndSearch() + + def testTrueConstraint(self): + solver = pywrapcp.Solver("test") + x1 = solver.IntVar(4, 8, "x1") + x2 = solver.IntVar(3, 7, "x2") + x3 = solver.IntVar(1, 5, "x3") + solver.Add((x1 >= 3) + (x2 >= 6) + (x3 <= 3) == 3) + db = solver.Phase( + [x1, x2, x3], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + solver.NewSearch(db) + solver.NextSolution() + solver.EndSearch() + + def testFalseConstraint(self): + solver = pywrapcp.Solver("test") + x1 = solver.IntVar(4, 8, "x1") + x2 = solver.IntVar(3, 7, "x2") + x3 = solver.IntVar(1, 5, "x3") + solver.Add((x1 <= 3) + (x2 >= 6) + (x3 <= 3) == 3) + db = solver.Phase( + [x1, x2, x3], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + solver.NewSearch(db) + solver.NextSolution() + solver.EndSearch() class IntVarLocalSearchOperatorTest(absltest.TestCase): - def test_ctor(self): - solver = pywrapcp.Solver("Solve") - int_vars = [solver.IntVar(0, 4) for _ in range(4)] - ivlso = pywrapcp.IntVarLocalSearchOperator(int_vars) - self.assertIsNotNone(ivlso) + def test_ctor(self): + solver = pywrapcp.Solver("Solve") + int_vars = [solver.IntVar(0, 4) for _ in range(4)] + ivlso = pywrapcp.IntVarLocalSearchOperator(int_vars) + self.assertIsNotNone(ivlso) - def test_api(self): - # print(f"{dir(pywrapcp.IntVarLocalSearchOperator)}") - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Size")) + def test_api(self): + # print(f"{dir(pywrapcp.IntVarLocalSearchOperator)}") + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Size")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Var")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "AddVars")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "IsIncremental")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Var")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "AddVars")) + self.assertTrue( + hasattr(pywrapcp.IntVarLocalSearchOperator, "IsIncremental") + ) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Activate")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Deactivate")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Activated")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Activate")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Deactivate")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Activated")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "OldValue")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "PrevValue")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Value")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "SetValue")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "OldValue")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "PrevValue")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Value")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "SetValue")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Start")) - self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "OnStart")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "Start")) + self.assertTrue(hasattr(pywrapcp.IntVarLocalSearchOperator, "OnStart")) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/constraint_solver/python/pywraprouting_test.py b/ortools/constraint_solver/python/pywraprouting_test.py deleted file mode 100755 index 4b0841dc859..00000000000 --- a/ortools/constraint_solver/python/pywraprouting_test.py +++ /dev/null @@ -1,896 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Test Routing API.""" - -import functools - -from absl.testing import absltest -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - - -def Distance(node_i, node_j): - return node_i + node_j - - -def TransitDistance(manager, i, j): - return Distance(manager.IndexToNode(i), manager.IndexToNode(j)) - - -def UnaryTransitDistance(manager, i): - return Distance(manager.IndexToNode(i), 0) - - -def One(unused_i, unused_j): - return 1 - - -def Two(unused_i, unused_j): - return 1 - - -def Three(unused_i, unused_j): - return 1 - - -class Callback: - - def __init__(self, model): - self.model = model - self.costs = [] - - def __call__(self): - self.costs.append(self.model.CostVar().Max()) - - -class TestPyWrapRoutingIndexManager(absltest.TestCase): - - def testCtor(self): - manager = pywrapcp.RoutingIndexManager(42, 3, 7) - self.assertIsNotNone(manager) - self.assertEqual(42, manager.GetNumberOfNodes()) - self.assertEqual(3, manager.GetNumberOfVehicles()) - self.assertEqual(42 + 3 * 2 - 1, manager.GetNumberOfIndices()) - for i in range(manager.GetNumberOfVehicles()): - self.assertEqual(7, manager.IndexToNode(manager.GetStartIndex(i))) - self.assertEqual(7, manager.IndexToNode(manager.GetEndIndex(i))) - - def testCtorMultiDepotSame(self): - manager = pywrapcp.RoutingIndexManager(42, 3, [0, 0, 0], [0, 0, 0]) - self.assertIsNotNone(manager) - self.assertEqual(42, manager.GetNumberOfNodes()) - self.assertEqual(3, manager.GetNumberOfVehicles()) - self.assertEqual(42 + 3 * 2 - 1, manager.GetNumberOfIndices()) - for i in range(manager.GetNumberOfVehicles()): - self.assertEqual(0, manager.IndexToNode(manager.GetStartIndex(i))) - self.assertEqual(0, manager.IndexToNode(manager.GetEndIndex(i))) - - def testCtorMultiDepotAllDiff(self): - manager = pywrapcp.RoutingIndexManager(42, 3, [1, 2, 3], [4, 5, 6]) - self.assertIsNotNone(manager) - self.assertEqual(42, manager.GetNumberOfNodes()) - self.assertEqual(3, manager.GetNumberOfVehicles()) - self.assertEqual(42, manager.GetNumberOfIndices()) - for i in range(manager.GetNumberOfVehicles()): - self.assertEqual(i + 1, manager.IndexToNode(manager.GetStartIndex(i))) - self.assertEqual(i + 4, manager.IndexToNode(manager.GetEndIndex(i))) - - -class TestPyWrapRoutingModel(absltest.TestCase): - - def testCtor(self): - manager = pywrapcp.RoutingIndexManager(42, 3, 7) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - for i in range(manager.GetNumberOfVehicles()): - self.assertEqual(7, manager.IndexToNode(model.Start(i))) - self.assertEqual(7, manager.IndexToNode(model.End(i))) - - def testSolve(self): - manager = pywrapcp.RoutingIndexManager(42, 3, 7) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.Solve() - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_OPTIMAL, model.status()) - self.assertIsNotNone(assignment) - self.assertEqual(0, assignment.ObjectiveValue()) - - def testSolveMultiDepot(self): - manager = pywrapcp.RoutingIndexManager(42, 3, [1, 2, 3], [4, 5, 6]) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.Solve() - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_OPTIMAL, model.status()) - self.assertIsNotNone(assignment) - self.assertEqual(0, assignment.ObjectiveValue()) - - def testTransitCallback(self): - manager = pywrapcp.RoutingIndexManager(5, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - self.assertEqual(1, transit_idx) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.Solve() - self.assertTrue(assignment) - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertEqual(20, assignment.ObjectiveValue()) - - def testTransitLambda(self): - manager = pywrapcp.RoutingIndexManager(5, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - transit_id = model.RegisterTransitCallback(lambda from_index, to_index: 1) - self.assertEqual(1, transit_id) - model.SetArcCostEvaluatorOfAllVehicles(transit_id) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.Solve() - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertIsNotNone(assignment) - self.assertEqual(5, assignment.ObjectiveValue()) - - def testTransitMatrix(self): - manager = pywrapcp.RoutingIndexManager(5, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - matrix = [[i + 1 for i in range(5)] for _ in range(5)] - transit_idx = model.RegisterTransitMatrix(matrix) - self.assertEqual(1, transit_idx) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.Solve() - self.assertTrue(assignment) - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertEqual(15, assignment.ObjectiveValue()) - - def testUnaryTransitCallback(self): - manager = pywrapcp.RoutingIndexManager(5, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - transit_idx = model.RegisterUnaryTransitCallback( - functools.partial(UnaryTransitDistance, manager) - ) - self.assertEqual(1, transit_idx) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.Solve() - self.assertTrue(assignment) - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertEqual(10, assignment.ObjectiveValue()) - - def testUnaryTransitLambda(self): - manager = pywrapcp.RoutingIndexManager(5, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - transit_id = model.RegisterUnaryTransitCallback(lambda from_index: 1) - self.assertEqual(1, transit_id) - model.SetArcCostEvaluatorOfAllVehicles(transit_id) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.Solve() - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertIsNotNone(assignment) - self.assertEqual(5, assignment.ObjectiveValue()) - - def testUnaryTransitVector(self): - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - vector = list(range(10)) - transit_idx = model.RegisterUnaryTransitVector(vector) - self.assertEqual(1, transit_idx) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.Solve() - self.assertTrue(assignment) - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertEqual(45, assignment.ObjectiveValue()) - - def testTSP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertEqual(90, assignment.ObjectiveValue()) - # Inspect solution - index = model.Start(0) - visited_nodes = [] - expected_visited_nodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] - while not model.IsEnd(index): - index = assignment.Value(model.NextVar(index)) - visited_nodes.append(manager.IndexToNode(index)) - self.assertEqual(expected_visited_nodes, visited_nodes) - - def testVRP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 2, [0, 1], [1, 0]) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(89, assignment.ObjectiveValue()) - # Inspect solution - index = model.Start(1) - visited_nodes = [] - expected_visited_nodes = [2, 4, 6, 8, 3, 5, 7, 9, 0] - while not model.IsEnd(index): - index = assignment.Value(model.NextVar(index)) - visited_nodes.append(manager.IndexToNode(index)) - self.assertEqual(expected_visited_nodes, visited_nodes) - self.assertTrue(model.IsEnd(assignment.Value(model.NextVar(model.Start(0))))) - - def testDimensionTSP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Add generic dimension - model.AddDimension(transit_idx, 90, 90, True, "distance") - distance_dimension = model.GetDimensionOrDie("distance") - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(90, assignment.ObjectiveValue()) - # Inspect solution - node = model.Start(0) - cumul = 0 - while not model.IsEnd(node): - self.assertEqual(cumul, assignment.Value(distance_dimension.CumulVar(node))) - next_node = assignment.Value(model.NextVar(node)) - cumul += Distance(node, next_node) - node = next_node - - def testDimensionWithVehicleCapacitiesTSP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Add generic dimension - model.AddDimensionWithVehicleCapacity(transit_idx, 90, [90], True, "distance") - distance_dimension = model.GetDimensionOrDie("distance") - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(90, assignment.ObjectiveValue()) - # Inspect solution - node = model.Start(0) - cumul = 0 - while not model.IsEnd(node): - self.assertEqual(cumul, assignment.Value(distance_dimension.CumulVar(node))) - next_node = assignment.Value(model.NextVar(node)) - cumul += Distance(node, next_node) - node = next_node - - def testDimensionWithVehicleTransitsTSP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Add generic dimension - model.AddDimensionWithVehicleTransits([transit_idx], 90, 90, True, "distance") - distance_dimension = model.GetDimensionOrDie("distance") - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(90, assignment.ObjectiveValue()) - # Inspect solution - node = model.Start(0) - cumul = 0 - while not model.IsEnd(node): - self.assertEqual(cumul, assignment.Value(distance_dimension.CumulVar(node))) - next_node = assignment.Value(model.NextVar(node)) - cumul += Distance(node, next_node) - node = next_node - - def testDimensionWithVehicleTransitsVRP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 3, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Add generic dimension - distances = [ - model.RegisterTransitCallback(One), - model.RegisterTransitCallback(Two), - model.RegisterTransitCallback(Three), - ] - model.AddDimensionWithVehicleTransits(distances, 90, 90, True, "distance") - distance_dimension = model.GetDimensionOrDie("distance") - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(90, assignment.ObjectiveValue()) - # Inspect solution - for vehicle in range(0, model.vehicles()): - node = model.Start(vehicle) - cumul = 0 - while not model.IsEnd(node): - self.assertEqual( - cumul, assignment.Min(distance_dimension.CumulVar(node)) - ) - next_node = assignment.Value(model.NextVar(node)) - # Increment cumul by the vehicle distance which is equal to the vehicle - # index + 1, cf. distances. - cumul += vehicle + 1 - node = next_node - - def testConstantDimensionTSP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 3, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Add constant dimension - constant_id, success = model.AddConstantDimension(1, 100, True, "count") - self.assertTrue(success) - self.assertEqual(transit_idx + 1, constant_id) - count_dimension = model.GetDimensionOrDie("count") - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(90, assignment.ObjectiveValue()) - # Inspect solution - node = model.Start(0) - count = 0 - while not model.IsEnd(node): - self.assertEqual(count, assignment.Value(count_dimension.CumulVar(node))) - count += 1 - node = assignment.Value(model.NextVar(node)) - self.assertEqual(10, count) - - def testVectorDimensionTSP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Add vector dimension - values = list(range(10)) - unary_transit_id, success = model.AddVectorDimension( - values, 100, True, "vector" - ) - self.assertTrue(success) - self.assertEqual(transit_idx + 1, unary_transit_id) - vector_dimension = model.GetDimensionOrDie("vector") - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertIsNotNone(assignment) - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertEqual(90, assignment.ObjectiveValue()) - # Inspect solution - node = model.Start(0) - cumul = 0 - while not model.IsEnd(node): - self.assertEqual(cumul, assignment.Value(vector_dimension.CumulVar(node))) - cumul += values[node] - node = assignment.Value(model.NextVar(node)) - - def testMatrixDimensionTSP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(5, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - cost = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(cost) - # Add matrix dimension - values = [[j for _ in range(5)] for j in range(5)] - transit_id, success = model.AddMatrixDimension(values, 100, True, "matrix") - self.assertTrue(success) - self.assertEqual(cost + 1, transit_id) - dimension = model.GetDimensionOrDie("matrix") - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertIsNotNone(assignment) - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertEqual(20, assignment.ObjectiveValue()) - # Inspect solution - index = model.Start(0) - cumul = 0 - while not model.IsEnd(index): - self.assertEqual(cumul, assignment.Value(dimension.CumulVar(index))) - cumul += values[manager.IndexToNode(index)][manager.IndexToNode(index)] - index = assignment.Value(model.NextVar(index)) - - def testMatrixDimensionVRP(self): - manager = pywrapcp.RoutingIndexManager(5, 2, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - matrix = [[i + j for i in range(5)] for j in range(5)] - transit_idx = model.RegisterTransitMatrix(matrix) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Add matrix dimension - matrix_transit_idx, success = model.AddMatrixDimension( - matrix, 10, True, "matrix" # capacity # fix_start_cumul_to_zero - ) - self.assertTrue(success) - self.assertEqual(transit_idx + 1, matrix_transit_idx) - dimension = model.GetDimensionOrDie("matrix") - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - self.assertEqual( - routing_enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertIsNotNone(assignment) - self.assertEqual(routing_enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status()) - self.assertEqual(20, assignment.ObjectiveValue()) - # Inspect solution - for v in range(manager.GetNumberOfVehicles()): - index = model.Start(v) - cumul = 0 - while not model.IsEnd(index): - self.assertEqual(cumul, assignment.Value(dimension.CumulVar(index))) - prev_index = index - index = assignment.Value(model.NextVar(index)) - cumul += matrix[manager.IndexToNode(prev_index)][ - manager.IndexToNode(index) - ] - - def testDisjunctionTSP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Add disjunctions - disjunctions = [ - [manager.NodeToIndex(1), manager.NodeToIndex(2)], - [manager.NodeToIndex(3)], - [manager.NodeToIndex(4)], - [manager.NodeToIndex(5)], - [manager.NodeToIndex(6)], - [manager.NodeToIndex(7)], - [manager.NodeToIndex(8)], - [manager.NodeToIndex(9)], - ] - for disjunction in disjunctions: - model.AddDisjunction(disjunction) - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(86, assignment.ObjectiveValue()) - # Inspect solution - node = model.Start(0) - count = 0 - while not model.IsEnd(node): - count += 1 - node = assignment.Value(model.NextVar(node)) - self.assertEqual(9, count) - - def testDisjunctionPenaltyTSP(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Add disjunctions - disjunctions = [ - ([manager.NodeToIndex(1), manager.NodeToIndex(2)], 1000), - ([manager.NodeToIndex(3)], 1000), - ([manager.NodeToIndex(4)], 1000), - ([manager.NodeToIndex(5)], 1000), - ([manager.NodeToIndex(6)], 1000), - ([manager.NodeToIndex(7)], 1000), - ([manager.NodeToIndex(8)], 1000), - ([manager.NodeToIndex(9)], 0), - ] - for disjunction, penalty in disjunctions: - model.AddDisjunction(disjunction, penalty) - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(68, assignment.ObjectiveValue()) - # Inspect solution - node = model.Start(0) - count = 0 - while not model.IsEnd(node): - count += 1 - node = assignment.Value(model.NextVar(node)) - self.assertEqual(8, count) - - def testRoutingModelParameters(self): - # Create routing model with parameters - parameters = pywrapcp.DefaultRoutingModelParameters() - parameters.solver_parameters.CopyFrom(pywrapcp.Solver.DefaultSolverParameters()) - parameters.solver_parameters.trace_propagation = True - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager, parameters) - self.assertIsNotNone(model) - self.assertEqual(1, model.vehicles()) - self.assertTrue(model.solver().Parameters().trace_propagation) - - def testRoutingLocalSearchFiltering(self): - parameters = pywrapcp.DefaultRoutingModelParameters() - parameters.solver_parameters.profile_local_search = True - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager, parameters) - self.assertIsNotNone(model) - model.Solve() - profile = model.solver().LocalSearchProfile() - print(profile) - self.assertIsInstance(profile, str) - self.assertTrue(profile) # Verify it's not empty. - - def testRoutingSearchParameters(self): - # Create routing model - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Close with parameters - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.SAVINGS - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.local_search_operators.use_two_opt = pywrapcp.BOOL_FALSE - search_parameters.solution_limit = 20 - model.CloseModelWithParameters(search_parameters) - # Solve with parameters - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual( - 11, model.GetNumberOfDecisionsInFirstSolution(search_parameters) - ) - self.assertEqual(0, model.GetNumberOfRejectsInFirstSolution(search_parameters)) - self.assertEqual(90, assignment.ObjectiveValue()) - assignment = model.SolveFromAssignmentWithParameters( - assignment, search_parameters - ) - self.assertEqual(90, assignment.ObjectiveValue()) - - def testFindErrorInRoutingSearchParameters(self): - params = pywrapcp.DefaultRoutingSearchParameters() - params.local_search_operators.use_cross = pywrapcp.BOOL_UNSPECIFIED - self.assertIn("cross", pywrapcp.FindErrorInRoutingSearchParameters(params)) - - def testCallback(self): - manager = pywrapcp.RoutingIndexManager(10, 1, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - callback = Callback(model) - model.AddAtSolutionCallback(callback) - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - assignment = model.SolveWithParameters(search_parameters) - self.assertEqual(90, assignment.ObjectiveValue()) - self.assertEqual(len(callback.costs), 1) - self.assertEqual(90, callback.costs[0]) - - def testReadAssignment(self): - manager = pywrapcp.RoutingIndexManager(10, 2, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # TODO(user): porting this segfaults the tests. - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - routes = [ - [ - manager.NodeToIndex(1), - manager.NodeToIndex(3), - manager.NodeToIndex(5), - manager.NodeToIndex(4), - manager.NodeToIndex(2), - manager.NodeToIndex(6), - ], - [ - manager.NodeToIndex(7), - manager.NodeToIndex(9), - manager.NodeToIndex(8), - ], - ] - assignment = model.ReadAssignmentFromRoutes(routes, False) - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.solution_limit = 1 - solution = model.SolveFromAssignmentWithParameters( - assignment, search_parameters - ) - self.assertEqual(90, solution.ObjectiveValue()) - for vehicle in range(0, model.vehicles()): - node = model.Start(vehicle) - count = 0 - while not model.IsEnd(node): - node = solution.Value(model.NextVar(node)) - if not model.IsEnd(node): - self.assertEqual(routes[vehicle][count], manager.IndexToNode(node)) - count += 1 - - def testAutomaticFirstSolutionStrategy_simple(self): - manager = pywrapcp.RoutingIndexManager(31, 7, 3) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - self.assertIsNotNone(model.SolveWithParameters(search_parameters)) - self.assertEqual( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC, - model.GetAutomaticFirstSolutionStrategy(), - ) - - def testAutomaticFirstSolutionStrategy_pd(self): - manager = pywrapcp.RoutingIndexManager(31, 7, 0) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - # Add cost function - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - model.SetArcCostEvaluatorOfAllVehicles(transit_idx) - self.assertTrue(model.AddDimension(transit_idx, 0, 1000, True, "distance")) - dst_dimension = model.GetDimensionOrDie("distance") - # Add few Pickup and Delivery - for request in [[2 * i, 2 * i + 1] for i in range(1, 15)]: - pickup_index = manager.NodeToIndex(request[0]) - delivery_index = manager.NodeToIndex(request[1]) - model.AddPickupAndDelivery(pickup_index, delivery_index) - model.solver().Add( - model.VehicleVar(pickup_index) == model.VehicleVar(delivery_index) - ) - model.solver().Add( - dst_dimension.CumulVar(pickup_index) - <= dst_dimension.CumulVar(delivery_index) - ) - # Solve - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - self.assertIsNotNone(model.SolveWithParameters(search_parameters)) - self.assertEqual( - routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION, - model.GetAutomaticFirstSolutionStrategy(), - ) - - -class TestBoundCost(absltest.TestCase): - - def testCtor(self): - bound_cost = pywrapcp.BoundCost() - self.assertIsNotNone(bound_cost) - self.assertEqual(0, bound_cost.bound) - self.assertEqual(0, bound_cost.cost) - - bound_cost = pywrapcp.BoundCost(97, 43) - self.assertIsNotNone(bound_cost) - self.assertEqual(97, bound_cost.bound) - self.assertEqual(43, bound_cost.cost) - - -class TestRoutingDimension(absltest.TestCase): - - def testCtor(self): - manager = pywrapcp.RoutingIndexManager(31, 7, 3) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - self.assertTrue(model.AddDimension(transit_idx, 90, 90, True, "distance")) - model.GetDimensionOrDie("distance") - - def testSoftSpanUpperBound(self): - manager = pywrapcp.RoutingIndexManager(31, 7, 3) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - self.assertTrue(model.AddDimension(transit_idx, 100, 100, True, "distance")) - dimension = model.GetDimensionOrDie("distance") - - bound_cost = pywrapcp.BoundCost(97, 43) - self.assertIsNotNone(bound_cost) - self.assertFalse(dimension.HasSoftSpanUpperBounds()) - for v in range(manager.GetNumberOfVehicles()): - dimension.SetSoftSpanUpperBoundForVehicle(bound_cost, v) - bc = dimension.GetSoftSpanUpperBoundForVehicle(v) - self.assertIsNotNone(bc) - self.assertEqual(97, bc.bound) - self.assertEqual(43, bc.cost) - self.assertTrue(dimension.HasSoftSpanUpperBounds()) - - def testQuadraticCostSoftSpanUpperBound(self): - manager = pywrapcp.RoutingIndexManager(31, 7, 3) - self.assertIsNotNone(manager) - model = pywrapcp.RoutingModel(manager) - self.assertIsNotNone(model) - transit_idx = model.RegisterTransitCallback( - functools.partial(TransitDistance, manager) - ) - self.assertTrue(model.AddDimension(transit_idx, 100, 100, True, "distance")) - dimension = model.GetDimensionOrDie("distance") - - bound_cost = pywrapcp.BoundCost(97, 43) - self.assertIsNotNone(bound_cost) - self.assertFalse(dimension.HasQuadraticCostSoftSpanUpperBounds()) - for v in range(manager.GetNumberOfVehicles()): - dimension.SetQuadraticCostSoftSpanUpperBoundForVehicle(bound_cost, v) - bc = dimension.GetQuadraticCostSoftSpanUpperBoundForVehicle(v) - self.assertIsNotNone(bc) - self.assertEqual(97, bc.bound) - self.assertEqual(43, bc.cost) - self.assertTrue(dimension.HasQuadraticCostSoftSpanUpperBounds()) - - -# TODO(user): Add tests for Routing[Cost|Vehicle|Resource]ClassIndex - -if __name__ == "__main__": - absltest.main() diff --git a/ortools/constraint_solver/routing_breaks.cc b/ortools/constraint_solver/routing_breaks.cc deleted file mode 100644 index b316dfbc6c9..00000000000 --- a/ortools/constraint_solver/routing_breaks.cc +++ /dev/null @@ -1,1089 +0,0 @@ -// Copyright 2010-2025 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "absl/log/check.h" -#include "absl/types/span.h" -#include "ortools/base/logging.h" -#include "ortools/base/types.h" -#include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_filters.h" -#include "ortools/util/saturated_arithmetic.h" -#include "ortools/util/scheduling.h" -#include "ortools/util/sorted_interval_list.h" - -namespace operations_research { - -bool DisjunctivePropagator::Propagate(Tasks* tasks) { - DCHECK_LE(tasks->num_chain_tasks, tasks->start_min.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->start_max.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->duration_min.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->duration_max.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->end_min.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->end_max.size()); - DCHECK_EQ(tasks->start_min.size(), tasks->is_preemptible.size()); - // Do forward deductions, then backward deductions. - // All propagators are followed by Precedences(), - // except MirrorTasks() after which Precedences() would make no deductions, - // and DetectablePrecedencesWithChain() which is stronger than Precedences(). - // Precedences() is a propagator that does obvious deductions quickly (O(n)), - // so interleaving Precedences() speeds up the propagation fixed point. - if (!Precedences(tasks) || !EdgeFinding(tasks) || !Precedences(tasks) || - !DetectablePrecedencesWithChain(tasks)) { - return false; - } - if (!tasks->forbidden_intervals.empty()) { - if (!ForbiddenIntervals(tasks) || !Precedences(tasks)) return false; - } - if (!tasks->distance_duration.empty()) { - if (!DistanceDuration(tasks) || !Precedences(tasks)) return false; - } - if (!MirrorTasks(tasks) || !EdgeFinding(tasks) || !Precedences(tasks) || - !DetectablePrecedencesWithChain(tasks) || !MirrorTasks(tasks)) { - return false; - } - return true; -} - -bool DisjunctivePropagator::Precedences(Tasks* tasks) { - const int num_chain_tasks = tasks->num_chain_tasks; - if (num_chain_tasks > 0) { - // Propagate forwards. - int64_t time = tasks->start_min[0]; - for (int task = 0; task < num_chain_tasks; ++task) { - time = std::max(tasks->start_min[task], time); - tasks->start_min[task] = time; - time = CapAdd(time, tasks->duration_min[task]); - if (tasks->end_max[task] < time) return false; - time = std::max(time, tasks->end_min[task]); - tasks->end_min[task] = time; - } - // Propagate backwards. - time = tasks->end_max[num_chain_tasks - 1]; - for (int task = num_chain_tasks - 1; task >= 0; --task) { - time = std::min(tasks->end_max[task], time); - tasks->end_max[task] = time; - time = CapSub(time, tasks->duration_min[task]); - if (time < tasks->start_min[task]) return false; - time = std::min(time, tasks->start_max[task]); - tasks->start_max[task] = time; - } - } - const int num_tasks = tasks->start_min.size(); - for (int task = 0; task < num_tasks; ++task) { - // Enforce start + duration <= end. - tasks->end_min[task] = - std::max(tasks->end_min[task], - CapAdd(tasks->start_min[task], tasks->duration_min[task])); - tasks->start_max[task] = - std::min(tasks->start_max[task], - CapSub(tasks->end_max[task], tasks->duration_min[task])); - tasks->duration_max[task] = - std::min(tasks->duration_max[task], - CapSub(tasks->end_max[task], tasks->start_min[task])); - if (!tasks->is_preemptible[task]) { - // Enforce start + duration == end for nonpreemptibles. - tasks->end_max[task] = - std::min(tasks->end_max[task], - CapAdd(tasks->start_max[task], tasks->duration_max[task])); - tasks->start_min[task] = - std::max(tasks->start_min[task], - CapSub(tasks->end_min[task], tasks->duration_max[task])); - tasks->duration_min[task] = - std::max(tasks->duration_min[task], - CapSub(tasks->end_min[task], tasks->start_max[task])); - } - if (tasks->duration_min[task] > tasks->duration_max[task]) return false; - if (tasks->end_min[task] > tasks->end_max[task]) return false; - if (tasks->start_min[task] > tasks->start_max[task]) return false; - } - return true; -} - -bool DisjunctivePropagator::MirrorTasks(Tasks* tasks) { - const int num_tasks = tasks->start_min.size(); - // For all tasks, start_min := -end_max and end_max := -start_min. - for (int task = 0; task < num_tasks; ++task) { - const int64_t t = -tasks->start_min[task]; - tasks->start_min[task] = -tasks->end_max[task]; - tasks->end_max[task] = t; - } - // For all tasks, start_max := -end_min and end_min := -start_max. - for (int task = 0; task < num_tasks; ++task) { - const int64_t t = -tasks->start_max[task]; - tasks->start_max[task] = -tasks->end_min[task]; - tasks->end_min[task] = t; - } - // In the mirror problem, tasks linked by precedences are in reversed order. - const int num_chain_tasks = tasks->num_chain_tasks; - for (const auto it : - {tasks->start_min.begin(), tasks->start_max.begin(), - tasks->duration_min.begin(), tasks->duration_max.begin(), - tasks->end_min.begin(), tasks->end_max.begin()}) { - std::reverse(it, it + num_chain_tasks); - std::reverse(it + num_chain_tasks, it + num_tasks); - } - std::reverse(tasks->is_preemptible.begin(), - tasks->is_preemptible.begin() + num_chain_tasks); - std::reverse(tasks->is_preemptible.begin() + num_chain_tasks, - tasks->is_preemptible.begin() + num_tasks); - return true; -} - -bool DisjunctivePropagator::EdgeFinding(Tasks* tasks) { - const int num_tasks = tasks->start_min.size(); - // Prepare start_min events for tree. - tasks_by_start_min_.resize(num_tasks); - std::iota(tasks_by_start_min_.begin(), tasks_by_start_min_.end(), 0); - std::sort( - tasks_by_start_min_.begin(), tasks_by_start_min_.end(), - [&](int i, int j) { return tasks->start_min[i] < tasks->start_min[j]; }); - event_of_task_.resize(num_tasks); - for (int event = 0; event < num_tasks; ++event) { - event_of_task_[tasks_by_start_min_[event]] = event; - } - // Tasks will be browsed according to end_max order. - tasks_by_end_max_.resize(num_tasks); - std::iota(tasks_by_end_max_.begin(), tasks_by_end_max_.end(), 0); - std::sort( - tasks_by_end_max_.begin(), tasks_by_end_max_.end(), - [&](int i, int j) { return tasks->end_max[i] < tasks->end_max[j]; }); - - // Generic overload checking: insert tasks by end_max, - // fail if envelope > end_max. - theta_lambda_tree_.Reset(num_tasks); - for (const int task : tasks_by_end_max_) { - theta_lambda_tree_.AddOrUpdateEvent( - event_of_task_[task], tasks->start_min[task], tasks->duration_min[task], - tasks->duration_min[task]); - if (theta_lambda_tree_.GetEnvelope() > tasks->end_max[task]) { - return false; - } - } - - // Generic edge finding: from full set of tasks, at each end_max event in - // decreasing order, check lambda feasibility, then move end_max task from - // theta to lambda. - for (int i = num_tasks - 1; i >= 0; --i) { - const int task = tasks_by_end_max_[i]; - const int64_t envelope = theta_lambda_tree_.GetEnvelope(); - // If a nonpreemptible optional would overload end_max, push to envelope. - while (theta_lambda_tree_.GetOptionalEnvelope() > tasks->end_max[task]) { - int critical_event; // Dummy value. - int optional_event; - int64_t available_energy; // Dummy value. - theta_lambda_tree_.GetEventsWithOptionalEnvelopeGreaterThan( - tasks->end_max[task], &critical_event, &optional_event, - &available_energy); - const int optional_task = tasks_by_start_min_[optional_event]; - tasks->start_min[optional_task] = - std::max(tasks->start_min[optional_task], envelope); - theta_lambda_tree_.RemoveEvent(optional_event); - } - if (!tasks->is_preemptible[task]) { - theta_lambda_tree_.AddOrUpdateOptionalEvent(event_of_task_[task], - tasks->start_min[task], - tasks->duration_min[task]); - } else { - theta_lambda_tree_.RemoveEvent(event_of_task_[task]); - } - } - return true; -} - -bool DisjunctivePropagator::DetectablePrecedencesWithChain(Tasks* tasks) { - const int num_tasks = tasks->start_min.size(); - // Prepare start_min events for tree. - tasks_by_start_min_.resize(num_tasks); - std::iota(tasks_by_start_min_.begin(), tasks_by_start_min_.end(), 0); - std::sort( - tasks_by_start_min_.begin(), tasks_by_start_min_.end(), - [&](int i, int j) { return tasks->start_min[i] < tasks->start_min[j]; }); - event_of_task_.resize(num_tasks); - for (int event = 0; event < num_tasks; ++event) { - event_of_task_[tasks_by_start_min_[event]] = event; - } - theta_lambda_tree_.Reset(num_tasks); - - // Sort nonchain tasks by start max = end_max - duration_min. - const int num_chain_tasks = tasks->num_chain_tasks; - nonchain_tasks_by_start_max_.resize(num_tasks - num_chain_tasks); - std::iota(nonchain_tasks_by_start_max_.begin(), - nonchain_tasks_by_start_max_.end(), num_chain_tasks); - std::sort(nonchain_tasks_by_start_max_.begin(), - nonchain_tasks_by_start_max_.end(), [&tasks](int i, int j) { - return tasks->end_max[i] - tasks->duration_min[i] < - tasks->end_max[j] - tasks->duration_min[j]; - }); - - // Detectable precedences, specialized for routes: for every task on route, - // put all tasks before it in the tree, then push with envelope. - int index_nonchain = 0; - for (int i = 0; i < num_chain_tasks; ++i) { - if (!tasks->is_preemptible[i]) { - // Add all nonchain tasks detected before i. - while (index_nonchain < nonchain_tasks_by_start_max_.size()) { - const int task = nonchain_tasks_by_start_max_[index_nonchain]; - if (tasks->end_max[task] - tasks->duration_min[task] >= - tasks->start_min[i] + tasks->duration_min[i]) - break; - theta_lambda_tree_.AddOrUpdateEvent( - event_of_task_[task], tasks->start_min[task], - tasks->duration_min[task], tasks->duration_min[task]); - index_nonchain++; - } - } - // All chain and nonchain tasks before i are now in the tree, push i. - const int64_t new_start_min = theta_lambda_tree_.GetEnvelope(); - // Add i to the tree before updating it. - theta_lambda_tree_.AddOrUpdateEvent(event_of_task_[i], tasks->start_min[i], - tasks->duration_min[i], - tasks->duration_min[i]); - tasks->start_min[i] = std::max(tasks->start_min[i], new_start_min); - } - return true; -} - -bool DisjunctivePropagator::ForbiddenIntervals(Tasks* tasks) { - if (tasks->forbidden_intervals.empty()) return true; - const int num_tasks = tasks->start_min.size(); - for (int task = 0; task < num_tasks; ++task) { - if (tasks->duration_min[task] == 0) continue; - if (tasks->forbidden_intervals[task] == nullptr) continue; - // If start_min forbidden, push to next feasible value. - { - const auto& interval = - tasks->forbidden_intervals[task]->FirstIntervalGreaterOrEqual( - tasks->start_min[task]); - if (interval == tasks->forbidden_intervals[task]->end()) continue; - if (interval->start <= tasks->start_min[task]) { - tasks->start_min[task] = CapAdd(interval->end, 1); - } - } - // If end_max forbidden, push to next feasible value. - { - const int64_t start_max = - CapSub(tasks->end_max[task], tasks->duration_min[task]); - const auto& interval = - tasks->forbidden_intervals[task]->LastIntervalLessOrEqual(start_max); - if (interval == tasks->forbidden_intervals[task]->end()) continue; - if (interval->end >= start_max) { - tasks->end_max[task] = - CapAdd(interval->start, tasks->duration_min[task] - 1); - } - } - if (CapAdd(tasks->start_min[task], tasks->duration_min[task]) > - tasks->end_max[task]) { - return false; - } - } - return true; -} - -bool DisjunctivePropagator::DistanceDuration(Tasks* tasks) { - if (tasks->distance_duration.empty()) return true; - if (tasks->num_chain_tasks == 0) return true; - const int route_start = 0; - const int route_end = tasks->num_chain_tasks - 1; - const int num_tasks = tasks->start_min.size(); - for (int i = 0; i < tasks->distance_duration.size(); ++i) { - const int64_t max_distance = tasks->distance_duration[i].first; - const int64_t minimum_break_duration = tasks->distance_duration[i].second; - - // This is a sweeping algorithm that looks whether the union of intervals - // defined by breaks and route start/end is (-infty, +infty). - // Those intervals are: - // - route start: (-infty, start_max + distance] - // - route end: [end_min, +infty) - // - breaks: [start_min, end_max + distance) if their duration_max - // is >= min_duration, empty set otherwise. - // If sweeping finds that a time point can be covered by only one interval, - // it will force the corresponding break or route start/end to cover this - // point, which can force a break to be above minimum_break_duration. - - // We suppose break tasks are ordered, so the algorithm supposes that - // start_min(task_n) <= start_min(task_{n+1}) and - // end_max(task_n) <= end_max(task_{n+1}). - for (int task = tasks->num_chain_tasks + 1; task < num_tasks; ++task) { - tasks->start_min[task] = - std::max(tasks->start_min[task], tasks->start_min[task - 1]); - } - for (int task = num_tasks - 2; task >= tasks->num_chain_tasks; --task) { - tasks->end_max[task] = - std::min(tasks->end_max[task], tasks->end_max[task + 1]); - } - // Skip breaks that cannot be performed after start. - int index_break_by_emax = tasks->num_chain_tasks; - while (index_break_by_emax < num_tasks && - tasks->end_max[index_break_by_emax] <= tasks->end_min[route_start]) { - ++index_break_by_emax; - } - // Special case: no breaks after start. - if (index_break_by_emax == num_tasks) { - tasks->end_min[route_start] = - std::max(tasks->end_min[route_start], - CapSub(tasks->start_min[route_end], max_distance)); - tasks->start_max[route_end] = - std::min(tasks->start_max[route_end], - CapAdd(tasks->end_max[route_start], max_distance)); - continue; - } - // There will be a break after start, so route_start coverage is tested. - // Initial state: start at -inf with route_start in task_set. - // Sweep over profile, looking for time points where the number of - // covering breaks is <= 1. If it is 0, fail, otherwise force the - // unique break to cover it. - // Route start and end get a special treatment, not sure generalizing - // would be better. - int64_t xor_active_tasks = route_start; - int num_active_tasks = 1; - int64_t previous_time = std::numeric_limits::min(); - const int64_t route_start_time = - CapAdd(tasks->end_max[route_start], max_distance); - const int64_t route_end_time = tasks->start_min[route_end]; - // NOTE: all smin events must be closed by a corresponding emax event, - // otherwise num_active_tasks is wrong (too high) and the reasoning misses - // some filtering. - int index_break_by_smin = index_break_by_emax; - while (index_break_by_emax < num_tasks) { - // Find next time point among start/end of covering intervals. - int64_t current_time = - CapAdd(tasks->end_max[index_break_by_emax], max_distance); - if (index_break_by_smin < num_tasks) { - current_time = - std::min(current_time, tasks->start_min[index_break_by_smin]); - } - if (previous_time < route_start_time && route_start_time < current_time) { - current_time = route_start_time; - } - if (previous_time < route_end_time && route_end_time < current_time) { - current_time = route_end_time; - } - // If num_active_tasks was 1, the unique active task must cover from - // previous_time to current_time. - if (num_active_tasks == 1) { - // xor_active_tasks is the unique task that can cover [previous_time, - // current_time). - if (xor_active_tasks != route_end) { - tasks->end_min[xor_active_tasks] = - std::max(tasks->end_min[xor_active_tasks], - CapSub(current_time, max_distance)); - if (xor_active_tasks != route_start) { - tasks->duration_min[xor_active_tasks] = std::max( - tasks->duration_min[xor_active_tasks], - std::max( - minimum_break_duration, - CapSub(CapSub(current_time, max_distance), previous_time))); - } - } - } - // Process covering intervals that start or end at current_time. - while (index_break_by_smin < num_tasks && - current_time == tasks->start_min[index_break_by_smin]) { - if (tasks->duration_max[index_break_by_smin] >= - minimum_break_duration) { - xor_active_tasks ^= index_break_by_smin; - ++num_active_tasks; - } - ++index_break_by_smin; - } - while (index_break_by_emax < num_tasks && - current_time == - CapAdd(tasks->end_max[index_break_by_emax], max_distance)) { - if (tasks->duration_max[index_break_by_emax] >= - minimum_break_duration) { - xor_active_tasks ^= index_break_by_emax; - --num_active_tasks; - } - ++index_break_by_emax; - } - if (current_time == route_start_time) { - xor_active_tasks ^= route_start; - --num_active_tasks; - } - if (current_time == route_end_time) { - xor_active_tasks ^= route_end; - ++num_active_tasks; - } - // If num_active_tasks becomes 1, the unique active task must cover from - // current_time. - if (num_active_tasks <= 0) return false; - if (num_active_tasks == 1) { - if (xor_active_tasks != route_start) { - // xor_active_tasks is the unique task that can cover from - // current_time to the next time point. - tasks->start_max[xor_active_tasks] = - std::min(tasks->start_max[xor_active_tasks], current_time); - if (xor_active_tasks != route_end) { - tasks->duration_min[xor_active_tasks] = std::max( - tasks->duration_min[xor_active_tasks], minimum_break_duration); - } - } - } - previous_time = current_time; - } - } - return true; -} - -bool DisjunctivePropagator::ChainSpanMin(Tasks* tasks) { - const int num_chain_tasks = tasks->num_chain_tasks; - if (num_chain_tasks < 1) return true; - // TODO(user): add stronger bounds. - // The duration of the chain plus that of nonchain tasks that must be - // performed during the chain is a lower bound of the chain span. - { - int64_t sum_chain_durations = 0; - const auto duration_start = tasks->duration_min.begin(); - const auto duration_end = tasks->duration_min.begin() + num_chain_tasks; - for (auto it = duration_start; it != duration_end; ++it) { - sum_chain_durations = CapAdd(sum_chain_durations, *it); - } - int64_t sum_forced_nonchain_durations = 0; - for (int i = num_chain_tasks; i < tasks->start_min.size(); ++i) { - // Tasks that can be executed before or after are skipped. - if (tasks->end_min[i] <= tasks->start_max[0] || - tasks->end_min[num_chain_tasks - 1] <= tasks->start_max[i]) { - continue; - } - sum_forced_nonchain_durations = - CapAdd(sum_forced_nonchain_durations, tasks->duration_min[i]); - } - tasks->span_min = - std::max(tasks->span_min, - CapAdd(sum_chain_durations, sum_forced_nonchain_durations)); - } - // The difference end of the chain - start of the chain is a lower bound. - { - const int64_t end_minus_start = - CapSub(tasks->end_min[num_chain_tasks - 1], tasks->start_max[0]); - tasks->span_min = std::max(tasks->span_min, end_minus_start); - } - - return tasks->span_min <= tasks->span_max; -} - -// Computes a lower bound of the span of the chain, taking into account only -// the first nonchain task. -// TODO(user): extend to arbitrary number of nonchain tasks. -bool DisjunctivePropagator::ChainSpanMinDynamic(Tasks* tasks) { - // Do nothing if there are no chain tasks or no nonchain tasks. - const int num_chain_tasks = tasks->num_chain_tasks; - if (num_chain_tasks < 1) return true; - if (num_chain_tasks == tasks->start_min.size()) return true; - const int task_index = num_chain_tasks; - if (!Precedences(tasks)) return false; - const int64_t min_possible_chain_end = tasks->end_min[num_chain_tasks - 1]; - const int64_t max_possible_chain_start = tasks->start_max[0]; - // For each chain task i, compute cumulated duration of chain tasks before it. - int64_t total_duration = 0; - { - total_duration_before_.resize(num_chain_tasks); - for (int i = 0; i < num_chain_tasks; ++i) { - total_duration_before_[i] = total_duration; - total_duration = CapAdd(total_duration, tasks->duration_min[i]); - } - } - // Estimate span min of chain tasks. Use the schedule that ends at - // min_possible_chain_end and starts at smallest of start_max[0] or the - // threshold where pushing start[0] later does not make a difference to the - // chain span because of chain precedence constraints, - // i.e. min_possible_chain_end - total_duration. - { - const int64_t chain_span_min = - min_possible_chain_end - - std::min(tasks->start_max[0], min_possible_chain_end - total_duration); - if (chain_span_min > tasks->span_max) { - return false; - } else { - tasks->span_min = std::max(tasks->span_min, chain_span_min); - } - // If task can be performed before or after the chain, - // span_min is chain_span_min. - if (tasks->end_min[task_index] <= tasks->start_max[0] || - tasks->end_min[num_chain_tasks - 1] <= tasks->start_max[task_index]) { - return true; - } - } - // Scan all possible preemption positions of the nontask chain, - // keep the one that yields the minimum span. - int64_t span_min = std::numeric_limits::max(); - bool schedule_is_feasible = false; - for (int i = 0; i < num_chain_tasks; ++i) { - if (!tasks->is_preemptible[i]) continue; - // Estimate span min if tasks is performed during i. - // For all possible minimal-span schedules, there is a schedule where task i - // and nonchain task form a single block. Thus, we only consider those. - const int64_t block_start_min = - std::max(tasks->start_min[i], - tasks->start_min[task_index] - tasks->duration_min[i]); - const int64_t block_start_max = - std::min(tasks->start_max[task_index], - tasks->start_max[i] - tasks->duration_min[task_index]); - if (block_start_min > block_start_max) continue; - - // Compute the block start that yields the minimal span. - // Given a feasible block start, a chain of minimum span constrained to - // this particular block start can be obtained by scheduling all tasks after - // the block at their earliest, and all tasks before it at their latest. - // The span can be decomposed into two parts: the head, which are the - // tasks that are before the block, and the tail, which are the block and - // the tasks after it. - // When the block start varies, the head length of the optimal schedule - // described above decreases as much as the block start decreases, until - // an inflection point at which it stays constant. That inflection value - // is the one where the precedence constraints force the chain start to - // decrease because of durations. - const int64_t head_inflection = - max_possible_chain_start + total_duration_before_[i]; - // The map from block start to minimal tail length also has an inflection - // point, that additionally depends on the nonchain task's duration. - const int64_t tail_inflection = - min_possible_chain_end - (total_duration - total_duration_before_[i]) - - tasks->duration_min[task_index]; - // All block start values between these two yield the same minimal span. - // Indeed, first, mind that the inflection points might be in any order. - // - if head_inflection < tail_inflection, then inside the interval - // [head_inflection, tail_inflection], increasing the block start by delta - // decreases the tail length by delta and increases the head length by - // delta too. - // - if tail_inflection < head_inflection, then inside the interval - // [tail_inflection, head_inflection], head length is constantly at - // total_duration_before_[i], and tail length is also constant. - // In both cases, outside of the interval, one part is constant and the - // other increases as much as the distance to the interval. - // We can abstract inflection point to the interval they form. - const int64_t optimal_interval_min_start = - std::min(head_inflection, tail_inflection); - const int64_t optimal_interval_max_start = - std::max(head_inflection, tail_inflection); - // If the optimal interval for block start intersects the feasible interval, - // we can select any point within it, for instance the earliest one. - int64_t block_start = std::max(optimal_interval_min_start, block_start_min); - // If the intervals do not intersect, the feasible value closest to the - // optimal interval has the minimal span, because the span increases as - // much as the distance to the optimal interval. - if (optimal_interval_max_start < block_start_min) { - // Optimal interval is before feasible interval, closest is feasible min. - block_start = block_start_min; - } else if (block_start_max < optimal_interval_min_start) { - // Optimal interval is after feasible interval, closest is feasible max. - block_start = block_start_max; - } - // Compute span for the chosen block start. - const int64_t head_duration = - std::max(block_start, head_inflection) - max_possible_chain_start; - const int64_t tail_duration = - min_possible_chain_end - std::min(block_start, tail_inflection); - const int64_t optimal_span_at_i = head_duration + tail_duration; - span_min = std::min(span_min, optimal_span_at_i); - schedule_is_feasible = true; - } - if (!schedule_is_feasible || span_min > tasks->span_max) { - return false; - } else { - tasks->span_min = std::max(tasks->span_min, span_min); - return true; - } -} - -void AppendTasksFromPath(absl::Span path, - const TravelBounds& travel_bounds, - const RoutingDimension& dimension, - DisjunctivePropagator::Tasks* tasks) { - const int num_nodes = path.size(); - DCHECK_EQ(travel_bounds.pre_travels.size(), num_nodes - 1); - DCHECK_EQ(travel_bounds.post_travels.size(), num_nodes - 1); - for (int i = 0; i < num_nodes; ++i) { - const int64_t cumul_min = dimension.CumulVar(path[i])->Min(); - const int64_t cumul_max = dimension.CumulVar(path[i])->Max(); - // Add task associated to visit i. - // Visits start at Cumul(path[i]) - before_visit - // and end at Cumul(path[i]) + after_visit - { - const int64_t before_visit = - (i == 0) ? 0 : travel_bounds.post_travels[i - 1]; - const int64_t after_visit = - (i == num_nodes - 1) ? 0 : travel_bounds.pre_travels[i]; - - tasks->start_min.push_back(CapSub(cumul_min, before_visit)); - tasks->start_max.push_back(CapSub(cumul_max, before_visit)); - tasks->duration_min.push_back(CapAdd(before_visit, after_visit)); - tasks->duration_max.push_back(CapAdd(before_visit, after_visit)); - tasks->end_min.push_back(CapAdd(cumul_min, after_visit)); - tasks->end_max.push_back(CapAdd(cumul_max, after_visit)); - tasks->is_preemptible.push_back(false); - } - if (i == num_nodes - 1) break; - - // Tasks from travels. - // A travel task starts at Cumul(path[i]) + pre_travel, - // last for FixedTransitVar(path[i]) - pre_travel - post_travel, - // and must end at the latest at Cumul(path[i+1]) - post_travel. - { - const int64_t pre_travel = travel_bounds.pre_travels[i]; - const int64_t post_travel = travel_bounds.post_travels[i]; - tasks->start_min.push_back(CapAdd(cumul_min, pre_travel)); - tasks->start_max.push_back(CapAdd(cumul_max, pre_travel)); - tasks->duration_min.push_back( - std::max(0, CapSub(travel_bounds.min_travels[i], - CapAdd(pre_travel, post_travel)))); - tasks->duration_max.push_back( - travel_bounds.max_travels[i] == std::numeric_limits::max() - ? std::numeric_limits::max() - : std::max(0, CapSub(travel_bounds.max_travels[i], - CapAdd(pre_travel, post_travel)))); - tasks->end_min.push_back( - CapSub(dimension.CumulVar(path[i + 1])->Min(), post_travel)); - tasks->end_max.push_back( - CapSub(dimension.CumulVar(path[i + 1])->Max(), post_travel)); - tasks->is_preemptible.push_back(true); - } - } -} - -void FillTravelBoundsOfVehicle(int vehicle, absl::Span path, - const RoutingDimension& dimension, - TravelBounds* travel_bounds) { - // Fill path and min/max/pre/post travel bounds. - FillPathEvaluation(path, dimension.transit_evaluator(vehicle), - &travel_bounds->min_travels); - const int num_travels = travel_bounds->min_travels.size(); - travel_bounds->max_travels.assign(num_travels, - std::numeric_limits::max()); - { - const int index = dimension.GetPreTravelEvaluatorOfVehicle(vehicle); - if (index == -1) { - travel_bounds->pre_travels.assign(num_travels, 0); - } else { - FillPathEvaluation(path, dimension.model()->TransitCallback(index), - &travel_bounds->pre_travels); - } - } - { - const int index = dimension.GetPostTravelEvaluatorOfVehicle(vehicle); - if (index == -1) { - travel_bounds->post_travels.assign(num_travels, 0); - } else { - FillPathEvaluation(path, dimension.model()->TransitCallback(index), - &travel_bounds->post_travels); - } - } -} - -void AppendTasksFromIntervals(const std::vector& intervals, - DisjunctivePropagator::Tasks* tasks) { - for (IntervalVar* interval : intervals) { - if (!interval->MustBePerformed()) continue; - tasks->start_min.push_back(interval->StartMin()); - tasks->start_max.push_back(interval->StartMax()); - tasks->duration_min.push_back(interval->DurationMin()); - tasks->duration_max.push_back(interval->DurationMax()); - tasks->end_min.push_back(interval->EndMin()); - tasks->end_max.push_back(interval->EndMax()); - tasks->is_preemptible.push_back(false); - } -} - -GlobalVehicleBreaksConstraint::GlobalVehicleBreaksConstraint( - const RoutingDimension* dimension) - : Constraint(dimension->model()->solver()), - model_(dimension->model()), - dimension_(dimension) { - vehicle_demons_.resize(model_->vehicles()); -} - -void GlobalVehicleBreaksConstraint::Post() { - for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { - if (dimension_->GetBreakIntervalsOfVehicle(vehicle).empty() && - dimension_->GetBreakDistanceDurationOfVehicle(vehicle).empty()) { - continue; - } - vehicle_demons_[vehicle] = MakeDelayedConstraintDemon1( - solver(), this, &GlobalVehicleBreaksConstraint::PropagateVehicle, - "PropagateVehicle", vehicle); - for (IntervalVar* interval : - dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - interval->WhenAnything(vehicle_demons_[vehicle]); - } - } - const int num_cumuls = dimension_->cumuls().size(); - const int num_nexts = model_->Nexts().size(); - for (int node = 0; node < num_cumuls; node++) { - Demon* dimension_demon = MakeConstraintDemon1( - solver(), this, &GlobalVehicleBreaksConstraint::PropagateNode, - "PropagateNode", node); - if (node < num_nexts) { - model_->NextVar(node)->WhenBound(dimension_demon); - dimension_->SlackVar(node)->WhenRange(dimension_demon); - } - model_->VehicleVar(node)->WhenBound(dimension_demon); - dimension_->CumulVar(node)->WhenRange(dimension_demon); - } -} - -void GlobalVehicleBreaksConstraint::InitialPropagate() { - for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { - if (!dimension_->GetBreakIntervalsOfVehicle(vehicle).empty() || - !dimension_->GetBreakDistanceDurationOfVehicle(vehicle).empty()) { - PropagateVehicle(vehicle); - } - } -} - -// This dispatches node events to the right vehicle propagator. -// It also filters out a part of uninteresting events, on which the vehicle -// propagator will not find anything new. -void GlobalVehicleBreaksConstraint::PropagateNode(int node) { - if (!model_->VehicleVar(node)->Bound()) return; - const int vehicle = model_->VehicleVar(node)->Min(); - if (vehicle < 0 || vehicle_demons_[vehicle] == nullptr) return; - EnqueueDelayedDemon(vehicle_demons_[vehicle]); -} - -void GlobalVehicleBreaksConstraint::FillPartialPathOfVehicle(int vehicle) { - path_.clear(); - int current = model_->Start(vehicle); - while (!model_->IsEnd(current)) { - path_.push_back(current); - current = model_->NextVar(current)->Bound() - ? model_->NextVar(current)->Min() - : model_->End(vehicle); - } - path_.push_back(current); -} - -void GlobalVehicleBreaksConstraint::FillPathTravels( - absl::Span path) { - const int num_travels = path.size() - 1; - travel_bounds_.min_travels.resize(num_travels); - travel_bounds_.max_travels.resize(num_travels); - for (int i = 0; i < num_travels; ++i) { - travel_bounds_.min_travels[i] = dimension_->FixedTransitVar(path[i])->Min(); - travel_bounds_.max_travels[i] = dimension_->FixedTransitVar(path[i])->Max(); - } -} - -// First, perform energy-based reasoning on intervals and cumul variables. -// Then, perform reasoning on slack variables. -void GlobalVehicleBreaksConstraint::PropagateVehicle(int vehicle) { - // Fill path and pre/post travel information. - FillPartialPathOfVehicle(vehicle); - const int num_nodes = path_.size(); - FillPathTravels(path_); - { - const int index = dimension_->GetPreTravelEvaluatorOfVehicle(vehicle); - if (index == -1) { - travel_bounds_.pre_travels.assign(num_nodes - 1, 0); - } else { - FillPathEvaluation(path_, model_->TransitCallback(index), - &travel_bounds_.pre_travels); - } - } - { - const int index = dimension_->GetPostTravelEvaluatorOfVehicle(vehicle); - if (index == -1) { - travel_bounds_.post_travels.assign(num_nodes - 1, 0); - } else { - FillPathEvaluation(path_, model_->TransitCallback(index), - &travel_bounds_.post_travels); - } - } - // The last travel might not be fixed: in that case, relax its information. - if (!model_->NextVar(path_[num_nodes - 2])->Bound()) { - travel_bounds_.min_travels.back() = 0; - travel_bounds_.max_travels.back() = std::numeric_limits::max(); - travel_bounds_.pre_travels.back() = 0; - travel_bounds_.post_travels.back() = 0; - } - - // Fill tasks from path, break intervals, and break constraints. - tasks_.Clear(); - AppendTasksFromPath(path_, travel_bounds_, *dimension_, &tasks_); - tasks_.num_chain_tasks = tasks_.start_min.size(); - AppendTasksFromIntervals(dimension_->GetBreakIntervalsOfVehicle(vehicle), - &tasks_); - tasks_.distance_duration = - dimension_->GetBreakDistanceDurationOfVehicle(vehicle); - - // Do the actual reasoning, no need to continue if infeasible. - if (!disjunctive_propagator_.Propagate(&tasks_)) solver()->Fail(); - - // Make task translators to help set new bounds of CP variables. - task_translators_.clear(); - for (int i = 0; i < num_nodes; ++i) { - const int64_t before_visit = - (i == 0) ? 0 : travel_bounds_.post_travels[i - 1]; - const int64_t after_visit = - (i == num_nodes - 1) ? 0 : travel_bounds_.pre_travels[i]; - task_translators_.emplace_back(dimension_->CumulVar(path_[i]), before_visit, - after_visit); - if (i == num_nodes - 1) break; - task_translators_.emplace_back(); // Dummy translator for travel tasks. - } - for (IntervalVar* interval : - dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - if (!interval->MustBePerformed()) continue; - task_translators_.emplace_back(interval); - } - - // Push new bounds to CP variables. - const int num_tasks = tasks_.start_min.size(); - for (int task = 0; task < num_tasks; ++task) { - task_translators_[task].SetStartMin(tasks_.start_min[task]); - task_translators_[task].SetStartMax(tasks_.start_max[task]); - task_translators_[task].SetDurationMin(tasks_.duration_min[task]); - task_translators_[task].SetEndMin(tasks_.end_min[task]); - task_translators_[task].SetEndMax(tasks_.end_max[task]); - } - - // Reasoning on slack variables: when intervals must be inside an arc, - // that arc's slack must be large enough to accommodate for those. - // TODO(user): Make a version more efficient than O(n^2). - if (dimension_->GetBreakIntervalsOfVehicle(vehicle).empty()) return; - // If the last arc of the path was not bound, do not change slack. - const int64_t last_bound_arc = - num_nodes - 2 - (model_->NextVar(path_[num_nodes - 2])->Bound() ? 0 : 1); - for (int i = 0; i <= last_bound_arc; ++i) { - const int64_t arc_start_max = - CapSub(dimension_->CumulVar(path_[i])->Max(), - i > 0 ? travel_bounds_.post_travels[i - 1] : 0); - const int64_t arc_end_min = - CapAdd(dimension_->CumulVar(path_[i + 1])->Min(), - i < num_nodes - 2 ? travel_bounds_.pre_travels[i + 1] : 0); - int64_t total_break_inside_arc = 0; - for (IntervalVar* interval : - dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - if (!interval->MustBePerformed()) continue; - const int64_t interval_start_max = interval->StartMax(); - const int64_t interval_end_min = interval->EndMin(); - const int64_t interval_duration_min = interval->DurationMin(); - // If interval cannot end before the arc's from node and - // cannot start after the 'to' node, then it must be inside the arc. - if (arc_start_max < interval_end_min && - interval_start_max < arc_end_min) { - total_break_inside_arc += interval_duration_min; - } - } - dimension_->SlackVar(path_[i])->SetMin(total_break_inside_arc); - } - // Reasoning on optional intervals. - // TODO(user): merge this with energy-based reasoning. - // If there is no optional interval, skip the rest of this function. - { - bool has_optional = false; - for (const IntervalVar* interval : - dimension_->GetBreakIntervalsOfVehicle(vehicle)) { - if (interval->MayBePerformed() && !interval->MustBePerformed()) { - has_optional = true; - break; - } - } - if (!has_optional) return; - } - const std::vector& break_intervals = - dimension_->GetBreakIntervalsOfVehicle(vehicle); - for (int pos = 0; pos < num_nodes - 1; ++pos) { - const int64_t current_slack_max = dimension_->SlackVar(path_[pos])->Max(); - const int64_t visit_start_offset = - pos > 0 ? travel_bounds_.post_travels[pos - 1] : 0; - const int64_t visit_start_max = - CapSub(dimension_->CumulVar(path_[pos])->Max(), visit_start_offset); - const int64_t visit_end_offset = - (pos < num_nodes - 1) ? travel_bounds_.pre_travels[pos] : 0; - const int64_t visit_end_min = - CapAdd(dimension_->CumulVar(path_[pos])->Min(), visit_end_offset); - - for (IntervalVar* interval : break_intervals) { - if (!interval->MayBePerformed()) continue; - const bool interval_is_performed = interval->MustBePerformed(); - const int64_t interval_start_max = interval->StartMax(); - const int64_t interval_end_min = interval->EndMin(); - const int64_t interval_duration_min = interval->DurationMin(); - // When interval cannot fit inside current arc, - // do disjunctive reasoning on full arc. - if (pos < num_nodes - 1 && interval_duration_min > current_slack_max) { - // The arc lasts from CumulVar(path_[pos]) - post_travel_[pos] to - // CumulVar(path_[pos+1]) + pre_travel_[pos+1]. - const int64_t arc_start_offset = - pos > 0 ? travel_bounds_.post_travels[pos - 1] : 0; - const int64_t arc_start_max = visit_start_max; - const int64_t arc_end_offset = - (pos < num_nodes - 2) ? travel_bounds_.pre_travels[pos + 1] : 0; - const int64_t arc_end_min = - CapAdd(dimension_->CumulVar(path_[pos + 1])->Min(), arc_end_offset); - // Interval not before. - if (arc_start_max < interval_end_min) { - interval->SetStartMin(arc_end_min); - if (interval_is_performed) { - dimension_->CumulVar(path_[pos + 1]) - ->SetMax(CapSub(interval_start_max, arc_end_offset)); - } - } - // Interval not after. - if (interval_start_max < arc_end_min) { - interval->SetEndMax(arc_start_max); - if (interval_is_performed) { - dimension_->CumulVar(path_[pos]) - ->SetMin(CapAdd(interval_end_min, arc_start_offset)); - } - } - continue; - } - // Interval could fit inside arc: do disjunctive reasoning between - // interval and visit. - // Interval not before. - if (visit_start_max < interval_end_min) { - interval->SetStartMin(visit_end_min); - if (interval_is_performed) { - dimension_->CumulVar(path_[pos]) - ->SetMax(CapSub(interval_start_max, visit_end_offset)); - } - } - // Interval not after. - if (interval_start_max < visit_end_min) { - interval->SetEndMax(visit_start_max); - if (interval_is_performed) { - dimension_->CumulVar(path_[pos]) - ->SetMin(CapAdd(interval_end_min, visit_start_offset)); - } - } - } - } -} - -namespace { -class VehicleBreaksFilter : public BasePathFilter { - public: - VehicleBreaksFilter(const RoutingModel& routing_model, - const RoutingDimension& dimension); - std::string DebugString() const override { return "VehicleBreaksFilter"; } - bool AcceptPath(int64_t path_start, int64_t chain_start, - int64_t chain_end) override; - - private: - // Fills path_ with the path of vehicle, start to end. - void FillPathOfVehicle(int64_t vehicle); - std::vector path_; - // Handles to model. - const RoutingModel& model_; - const RoutingDimension& dimension_; - // Strong energy-based filtering algorithm. - DisjunctivePropagator disjunctive_propagator_; - DisjunctivePropagator::Tasks tasks_; - // Used to check whether propagation changed a vector. - std::vector old_start_min_; - std::vector old_start_max_; - std::vector old_end_min_; - std::vector old_end_max_; - - std::vector start_to_vehicle_; - TravelBounds travel_bounds_; -}; - -VehicleBreaksFilter::VehicleBreaksFilter(const RoutingModel& routing_model, - const RoutingDimension& dimension) - : BasePathFilter(routing_model.Nexts(), - routing_model.Size() + routing_model.vehicles(), - routing_model.GetPathsMetadata()), - model_(routing_model), - dimension_(dimension) { - DCHECK(dimension_.HasBreakConstraints()); - start_to_vehicle_.resize(Size(), -1); - for (int i = 0; i < routing_model.vehicles(); ++i) { - start_to_vehicle_[routing_model.Start(i)] = i; - } -} - -void VehicleBreaksFilter::FillPathOfVehicle(int64_t vehicle) { - path_.clear(); - int current = model_.Start(vehicle); - while (!model_.IsEnd(current)) { - path_.push_back(current); - current = GetNext(current); - } - path_.push_back(current); -} - -bool VehicleBreaksFilter::AcceptPath(int64_t path_start, int64_t chain_start, - int64_t chain_end) { - const int vehicle = start_to_vehicle_[path_start]; - if (dimension_.GetBreakIntervalsOfVehicle(vehicle).empty() && - dimension_.GetBreakDistanceDurationOfVehicle(vehicle).empty()) { - return true; - } - // Fill path and pre/post travel information. - FillPathOfVehicle(vehicle); - FillTravelBoundsOfVehicle(vehicle, path_, dimension_, &travel_bounds_); - // Fill tasks from path, forbidden intervals, breaks and break constraints. - tasks_.Clear(); - AppendTasksFromPath(path_, travel_bounds_, dimension_, &tasks_); - tasks_.num_chain_tasks = tasks_.start_min.size(); - AppendTasksFromIntervals(dimension_.GetBreakIntervalsOfVehicle(vehicle), - &tasks_); - // Add forbidden intervals only if a node has some. - tasks_.forbidden_intervals.clear(); - if (std::any_of(path_.begin(), path_.end(), [this](int64_t node) { - return dimension_.forbidden_intervals()[node].NumIntervals() > 0; - })) { - tasks_.forbidden_intervals.assign(tasks_.start_min.size(), nullptr); - for (int i = 0; i < path_.size(); ++i) { - tasks_.forbidden_intervals[2 * i] = - &(dimension_.forbidden_intervals()[path_[i]]); - } - } - // Max distance duration constraint. - tasks_.distance_duration = - dimension_.GetBreakDistanceDurationOfVehicle(vehicle); - - // Reduce bounds until failure or fixed point is reached. - // We set a maximum amount of iterations to avoid slow propagation. - bool is_feasible = true; - int maximum_num_iterations = 8; - while (--maximum_num_iterations >= 0) { - old_start_min_ = tasks_.start_min; - old_start_max_ = tasks_.start_max; - old_end_min_ = tasks_.end_min; - old_end_max_ = tasks_.end_max; - is_feasible = disjunctive_propagator_.Propagate(&tasks_); - if (!is_feasible) break; - // If fixed point reached, stop. - if ((old_start_min_ == tasks_.start_min) && - (old_start_max_ == tasks_.start_max) && - (old_end_min_ == tasks_.end_min) && (old_end_max_ == tasks_.end_max)) { - break; - } - } - return is_feasible; -} - -} // namespace - -IntVarLocalSearchFilter* MakeVehicleBreaksFilter( - const RoutingModel& routing_model, const RoutingDimension& dimension) { - return routing_model.solver()->RevAlloc( - new VehicleBreaksFilter(routing_model, dimension)); -} - -} // namespace operations_research diff --git a/ortools/constraint_solver/samples/BUILD.bazel b/ortools/constraint_solver/samples/BUILD.bazel index ccb64d8b73c..6b6fab6fea5 100644 --- a/ortools/constraint_solver/samples/BUILD.bazel +++ b/ortools/constraint_solver/samples/BUILD.bazel @@ -24,39 +24,3 @@ code_sample_cc(name = "rabbits_and_pheasants_cp") code_sample_cc(name = "simple_cp_program") code_sample_cc(name = "simple_ls_program") - -code_sample_cc(name = "simple_routing_program") - -code_sample_cc(name = "tsp") - -code_sample_cc(name = "tsp_circuit_board") - -code_sample_cc(name = "tsp_cities") - -code_sample_cc(name = "tsp_distance_matrix") - -code_sample_cc(name = "vrp") - -code_sample_cc(name = "vrp_breaks") - -code_sample_cc(name = "vrp_capacity") - -code_sample_cc(name = "vrp_drop_nodes") - -code_sample_cc(name = "vrp_global_span") - -code_sample_cc(name = "vrp_initial_routes") - -code_sample_cc(name = "vrp_pickup_delivery") - -code_sample_cc(name = "vrp_pickup_delivery_fifo") - -code_sample_cc(name = "vrp_pickup_delivery_lifo") - -code_sample_cc(name = "vrp_resources") - -code_sample_cc(name = "vrp_starts_ends") - -code_sample_cc(name = "vrp_time_windows") - -code_sample_cc(name = "vrp_with_time_limit") diff --git a/ortools/constraint_solver/samples/CMakeLists.txt b/ortools/constraint_solver/samples/CMakeLists.txt index ee48e3d3dae..8bb2b71af62 100644 --- a/ortools/constraint_solver/samples/CMakeLists.txt +++ b/ortools/constraint_solver/samples/CMakeLists.txt @@ -17,9 +17,6 @@ endif() if(BUILD_CXX_SAMPLES) file(GLOB CXX_SRCS "*.cc") - list(FILTER CXX_SRCS EXCLUDE REGEX "/cvrp_disjoint_tw") - list(FILTER CXX_SRCS EXCLUDE REGEX "/cvrptw\.cc") - list(FILTER CXX_SRCS EXCLUDE REGEX "/cvrptw_") foreach(SAMPLE IN LISTS CXX_SRCS) add_cxx_sample(FILE_NAME ${SAMPLE}) endforeach() diff --git a/ortools/constraint_solver/samples/code_samples.bzl b/ortools/constraint_solver/samples/code_samples.bzl index 55ceddc80c0..84bf1094687 100644 --- a/ortools/constraint_solver/samples/code_samples.bzl +++ b/ortools/constraint_solver/samples/code_samples.bzl @@ -22,8 +22,6 @@ def code_sample_cc(name): deps = [ "//ortools/base", "//ortools/constraint_solver:cp", - "//ortools/constraint_solver:routing", - "//ortools/constraint_solver:routing_enums_cc_proto", ], ) @@ -35,7 +33,5 @@ def code_sample_cc(name): ":" + name + "_cc", "//ortools/base", "//ortools/constraint_solver:cp", - "//ortools/constraint_solver:routing", - "//ortools/constraint_solver:routing_enums_cc_proto", ], ) diff --git a/ortools/constraint_solver/samples/cp_is_fun_cp.py b/ortools/constraint_solver/samples/cp_is_fun_cp.py index fbb230be132..c4e93da681e 100755 --- a/ortools/constraint_solver/samples/cp_is_fun_cp.py +++ b/ortools/constraint_solver/samples/cp_is_fun_cp.py @@ -22,76 +22,77 @@ """ # [START import] from ortools.constraint_solver import pywrapcp + # [END import] def main(): - # Constraint programming engine - # [START solver] - solver = pywrapcp.Solver("CP is fun!") - # [END solver] + # Constraint programming engine + # [START solver] + solver = pywrapcp.Solver("CP is fun!") + # [END solver] - # [START variables] - base = 10 + # [START variables] + base = 10 - # Decision variables. - digits = list(range(0, base)) - digits_without_zero = list(range(1, base)) - c = solver.IntVar(digits_without_zero, "C") - p = solver.IntVar(digits, "P") - i = solver.IntVar(digits_without_zero, "I") - s = solver.IntVar(digits, "S") - f = solver.IntVar(digits_without_zero, "F") - u = solver.IntVar(digits, "U") - n = solver.IntVar(digits, "N") - t = solver.IntVar(digits_without_zero, "T") - r = solver.IntVar(digits, "R") - e = solver.IntVar(digits, "E") + # Decision variables. + digits = list(range(0, base)) + digits_without_zero = list(range(1, base)) + c = solver.IntVar(digits_without_zero, "C") + p = solver.IntVar(digits, "P") + i = solver.IntVar(digits_without_zero, "I") + s = solver.IntVar(digits, "S") + f = solver.IntVar(digits_without_zero, "F") + u = solver.IntVar(digits, "U") + n = solver.IntVar(digits, "N") + t = solver.IntVar(digits_without_zero, "T") + r = solver.IntVar(digits, "R") + e = solver.IntVar(digits, "E") - # We need to group variables in a list to use the constraint AllDifferent. - letters = [c, p, i, s, f, u, n, t, r, e] + # We need to group variables in a list to use the constraint AllDifferent. + letters = [c, p, i, s, f, u, n, t, r, e] - # Verify that we have enough digits. - assert base >= len(letters) - # [END variables] + # Verify that we have enough digits. + assert base >= len(letters) + # [END variables] - # Define constraints. - # [START constraints] - solver.Add(solver.AllDifferent(letters)) + # Define constraints. + # [START constraints] + solver.Add(solver.AllDifferent(letters)) - # CP + IS + FUN = TRUE - solver.Add( - p + s + n + base * (c + i + u) + base * base * f - == e + base * u + base * base * r + base * base * base * t - ) - # [END constraints] + # CP + IS + FUN = TRUE + solver.Add( + p + s + n + base * (c + i + u) + base * base * f + == e + base * u + base * base * r + base * base * base * t + ) + # [END constraints] - # [START solve] - solution_count = 0 - db = solver.Phase(letters, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT) - solver.NewSearch(db) - while solver.NextSolution(): - print(letters) - # Is CP + IS + FUN = TRUE? - assert ( - base * c.Value() - + p.Value() - + base * i.Value() - + s.Value() - + base * base * f.Value() - + base * u.Value() - + n.Value() - == base * base * base * t.Value() - + base * base * r.Value() - + base * u.Value() - + e.Value() - ) - solution_count += 1 - solver.EndSearch() - print(f"Number of solutions found: {solution_count}") - # [END solve] + # [START solve] + solution_count = 0 + db = solver.Phase(letters, solver.INT_VAR_DEFAULT, solver.INT_VALUE_DEFAULT) + solver.NewSearch(db) + while solver.NextSolution(): + print(letters) + # Is CP + IS + FUN = TRUE? + assert ( + base * c.Value() + + p.Value() + + base * i.Value() + + s.Value() + + base * base * f.Value() + + base * u.Value() + + n.Value() + == base * base * base * t.Value() + + base * base * r.Value() + + base * u.Value() + + e.Value() + ) + solution_count += 1 + solver.EndSearch() + print(f"Number of solutions found: {solution_count}") + # [END solve] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/constraint_solver/samples/cvrp_reload.py b/ortools/constraint_solver/samples/cvrp_reload.py deleted file mode 100755 index fee9315eed5..00000000000 --- a/ortools/constraint_solver/samples/cvrp_reload.py +++ /dev/null @@ -1,429 +0,0 @@ -#!/usr/bin/env python3 -# This Python file uses the following encoding: utf-8 -# Copyright 2015 Tin Arm Engineering AB -# Copyright 2018 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Capacitated Vehicle Routing Problem (CVRP). - -This is a sample using the routing library python wrapper to solve a CVRP -problem while allowing multiple trips, i.e., vehicles can return to a depot -to reset their load ("reload"). - -A description of the CVRP problem can be found here: -http://en.wikipedia.org/wiki/Vehicle_routing_problem. - -Distances are in meters. - -In order to implement multiple trips, new nodes are introduced at the same -locations of the original depots. These additional nodes can be dropped -from the schedule at 0 cost. - -The max_slack parameter associated to the capacity constraints of all nodes -can be set to be the maximum of the vehicles' capacities, rather than 0 like -in a traditional CVRP. Slack is required since before a solution is found, -it is not known how much capacity will be transferred at the new nodes. For -all the other (original) nodes, the slack is then re-set to 0. - -The above two considerations are implemented in `add_capacity_constraints()`. - -Last, it is useful to set a large distance between the initial depot and the -new nodes introduced, to avoid schedules having spurious transits through -those new nodes unless it's necessary to reload. This consideration is taken -into account in `create_distance_evaluator()`. -""" - -from functools import partial - -from ortools.constraint_solver import pywrapcp -from ortools.constraint_solver import routing_enums_pb2 - - -########################### -# Problem Data Definition # -########################### -def create_data_model(): - """Stores the data for the problem""" - data = {} - _capacity = 15 - # Locations in block unit - _locations = [ - (4, 4), # depot - (4, 4), # unload depot_first - (4, 4), # unload depot_second - (4, 4), # unload depot_third - (4, 4), # unload depot_fourth - (4, 4), # unload depot_fifth - (2, 0), - (8, 0), # locations to visit - (0, 1), - (1, 1), - (5, 2), - (7, 2), - (3, 3), - (6, 3), - (5, 5), - (8, 5), - (1, 6), - (2, 6), - (3, 7), - (6, 7), - (0, 8), - (7, 8), - ] - # Compute locations in meters using the block dimension defined as follow - # Manhattan average block: 750ft x 264ft -> 228m x 80m - # here we use: 114m x 80m city block - # src: https://nyti.ms/2GDoRIe 'NY Times: Know Your distance' - data["locations"] = [(l[0] * 114, l[1] * 80) for l in _locations] - data["num_locations"] = len(data["locations"]) - data["demands"] = [ - 0, # depot - -_capacity, # unload depot_first - -_capacity, # unload depot_second - -_capacity, # unload depot_third - -_capacity, # unload depot_fourth - -_capacity, # unload depot_fifth - 3, - 3, # 1, 2 - 3, - 4, # 3, 4 - 3, - 4, # 5, 6 - 8, - 8, # 7, 8 - 3, - 3, # 9,10 - 3, - 3, # 11,12 - 4, - 4, # 13, 14 - 8, - 8, - ] # 15, 16 - data["time_per_demand_unit"] = 5 # 5 minutes/unit - data["time_windows"] = [ - (0, 0), # depot - (0, 1000), # unload depot_first - (0, 1000), # unload depot_second - (0, 1000), # unload depot_third - (0, 1000), # unload depot_fourth - (0, 1000), # unload depot_fifth - (75, 850), - (75, 850), # 1, 2 - (60, 700), - (45, 550), # 3, 4 - (0, 800), - (50, 600), # 5, 6 - (0, 1000), - (10, 200), # 7, 8 - (0, 1000), - (75, 850), # 9, 10 - (85, 950), - (5, 150), # 11, 12 - (15, 250), - (10, 200), # 13, 14 - (45, 550), - (30, 400), - ] # 15, 16 - data["num_vehicles"] = 3 - data["vehicle_capacity"] = _capacity - data["vehicle_max_distance"] = 10_000 - data["vehicle_max_time"] = 1_500 - data["vehicle_speed"] = 5 * 60 / 3.6 # Travel speed: 5km/h to convert in m/min - data["depot"] = 0 - return data - - -####################### -# Problem Constraints # -####################### -def manhattan_distance(position_1, position_2): - """Computes the Manhattan distance between two points""" - return abs(position_1[0] - position_2[0]) + abs(position_1[1] - position_2[1]) - - -def create_distance_evaluator(data): - """Creates callback to return distance between points.""" - _distances = {} - # precompute distance between location to have distance callback in O(1) - for from_node in range(data["num_locations"]): - _distances[from_node] = {} - for to_node in range(data["num_locations"]): - if from_node == to_node: - _distances[from_node][to_node] = 0 - # Forbid start/end/reload node to be consecutive. - elif from_node in range(6) and to_node in range(6): - _distances[from_node][to_node] = data["vehicle_max_distance"] - else: - _distances[from_node][to_node] = manhattan_distance( - data["locations"][from_node], data["locations"][to_node] - ) - - def distance_evaluator(manager, from_node, to_node): - """Returns the manhattan distance between the two nodes""" - return _distances[manager.IndexToNode(from_node)][manager.IndexToNode(to_node)] - - return distance_evaluator - - -def add_distance_dimension(routing, manager, data, distance_evaluator_index): - """Add Global Span constraint""" - del manager - distance = "Distance" - routing.AddDimension( - distance_evaluator_index, - 0, # null slack - data["vehicle_max_distance"], # maximum distance per vehicle - True, # start cumul to zero - distance, - ) - distance_dimension = routing.GetDimensionOrDie(distance) - # Try to minimize the max distance among vehicles. - # /!\ It doesn't mean the standard deviation is minimized - distance_dimension.SetGlobalSpanCostCoefficient(100) - - -def create_demand_evaluator(data): - """Creates callback to get demands at each location.""" - _demands = data["demands"] - - def demand_evaluator(manager, from_node): - """Returns the demand of the current node""" - return _demands[manager.IndexToNode(from_node)] - - return demand_evaluator - - -def add_capacity_constraints(routing, manager, data, demand_evaluator_index): - """Adds capacity constraint""" - vehicle_capacity = data["vehicle_capacity"] - capacity = "Capacity" - routing.AddDimension( - demand_evaluator_index, - vehicle_capacity, - vehicle_capacity, - True, # start cumul to zero - capacity, - ) - - # Add Slack for reseting to zero unload depot nodes. - # e.g. vehicle with load 10/15 arrives at node 1 (depot unload) - # so we have CumulVar = 10(current load) + -15(unload) + 5(slack) = 0. - capacity_dimension = routing.GetDimensionOrDie(capacity) - # Allow to drop reloading nodes with zero cost. - for node in [1, 2, 3, 4, 5]: - node_index = manager.NodeToIndex(node) - routing.AddDisjunction([node_index], 0) - - # Allow to drop regular node with a cost. - for node in range(6, len(data["demands"])): - node_index = manager.NodeToIndex(node) - capacity_dimension.SlackVar(node_index).SetValue(0) - routing.AddDisjunction([node_index], 100_000) - - -def create_time_evaluator(data): - """Creates callback to get total times between locations.""" - - def service_time(data, node): - """Gets the service time for the specified location.""" - return abs(data["demands"][node]) * data["time_per_demand_unit"] - - def travel_time(data, from_node, to_node): - """Gets the travel times between two locations.""" - if from_node == to_node: - travel_time = 0 - else: - travel_time = ( - manhattan_distance( - data["locations"][from_node], data["locations"][to_node] - ) - / data["vehicle_speed"] - ) - return travel_time - - _total_time = {} - # precompute total time to have time callback in O(1) - for from_node in range(data["num_locations"]): - _total_time[from_node] = {} - for to_node in range(data["num_locations"]): - if from_node == to_node: - _total_time[from_node][to_node] = 0 - else: - _total_time[from_node][to_node] = int( - service_time(data, from_node) - + travel_time(data, from_node, to_node) - ) - - def time_evaluator(manager, from_node, to_node): - """Returns the total time between the two nodes""" - return _total_time[manager.IndexToNode(from_node)][manager.IndexToNode(to_node)] - - return time_evaluator - - -def add_time_window_constraints(routing, manager, data, time_evaluator): - """Add Time windows constraint""" - time = "Time" - max_time = data["vehicle_max_time"] - routing.AddDimension( - time_evaluator, - max_time, # allow waiting time - max_time, # maximum time per vehicle - False, # don't force start cumul to zero since we are giving TW to start nodes - time, - ) - time_dimension = routing.GetDimensionOrDie(time) - # Add time window constraints for each location except depot - # and 'copy' the slack var in the solution object (aka Assignment) to print it - for location_idx, time_window in enumerate(data["time_windows"]): - if location_idx == 0: - continue - index = manager.NodeToIndex(location_idx) - time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) - routing.AddToAssignment(time_dimension.SlackVar(index)) - # Add time window constraints for each vehicle start node - # and 'copy' the slack var in the solution object (aka Assignment) to print it - for vehicle_id in range(data["num_vehicles"]): - index = routing.Start(vehicle_id) - time_dimension.CumulVar(index).SetRange( - data["time_windows"][0][0], data["time_windows"][0][1] - ) - routing.AddToAssignment(time_dimension.SlackVar(index)) - # Warning: Slack var is not defined for vehicle's end node - # routing.AddToAssignment(time_dimension.SlackVar(self.routing.End(vehicle_id))) - - -########### -# Printer # -########### -def print_solution( - data, manager, routing, assignment -): # pylint:disable=too-many-locals - """Prints assignment on console""" - print(f"Objective: {assignment.ObjectiveValue()}") - total_distance = 0 - total_load = 0 - total_time = 0 - capacity_dimension = routing.GetDimensionOrDie("Capacity") - time_dimension = routing.GetDimensionOrDie("Time") - distance_dimension = routing.GetDimensionOrDie("Distance") - dropped = [] - for order in range(6, routing.nodes()): - index = manager.NodeToIndex(order) - if assignment.Value(routing.NextVar(index)) == index: - dropped.append(order) - print(f"dropped orders: {dropped}") - dropped = [] - for reload in range(1, 6): - index = manager.NodeToIndex(reload) - if assignment.Value(routing.NextVar(index)) == index: - dropped.append(reload) - print(f"dropped reload stations: {dropped}") - - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(assignment, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - load_value = 0 - distance = 0 - while not routing.IsEnd(index): - time_var = time_dimension.CumulVar(index) - plan_output += ( - f" {manager.IndexToNode(index)} " - f"Load({assignment.Min(capacity_dimension.CumulVar(index))}) " - f"Time({assignment.Min(time_var)},{assignment.Max(time_var)}) ->" - ) - previous_index = index - index = assignment.Value(routing.NextVar(index)) - distance += distance_dimension.GetTransitValue(previous_index, index, vehicle_id) - # capacity dimension TransitVar is negative at reload stations during replenishment - # don't want to consider those values when calculating the total load of the route - # hence only considering the positive values - load_value += max(0, capacity_dimension.GetTransitValue(previous_index, index, vehicle_id)) - time_var = time_dimension.CumulVar(index) - plan_output += ( - f" {manager.IndexToNode(index)} " - f"Load({assignment.Min(capacity_dimension.CumulVar(index))}) " - f"Time({assignment.Min(time_var)},{assignment.Max(time_var)})\n" - ) - plan_output += f"Distance of the route: {distance}m\n" - plan_output += f"Load of the route: {load_value}\n" - plan_output += f"Time of the route: {assignment.Min(time_var)}min\n" - print(plan_output) - total_distance += distance - total_load += load_value - total_time += assignment.Min(time_var) - print(f"Total Distance of all routes: {total_distance}m") - print(f"Total Load of all routes: {total_load}") - print(f"Total Time of all routes: {total_time}min") - - -######## -# Main # -######## -def main(): - """Entry point of the program""" - # Instantiate the data problem. - data = create_data_model() - - # Create the routing index manager - manager = pywrapcp.RoutingIndexManager( - data["num_locations"], data["num_vehicles"], data["depot"] - ) - - # Create Routing Model - routing = pywrapcp.RoutingModel(manager) - - # Define weight of each edge - distance_evaluator_index = routing.RegisterTransitCallback( - partial(create_distance_evaluator(data), manager) - ) - routing.SetArcCostEvaluatorOfAllVehicles(distance_evaluator_index) - - # Add Distance constraint to minimize the longuest route - add_distance_dimension(routing, manager, data, distance_evaluator_index) - - # Add Capacity constraint - demand_evaluator_index = routing.RegisterUnaryTransitCallback( - partial(create_demand_evaluator(data), manager) - ) - add_capacity_constraints(routing, manager, data, demand_evaluator_index) - - # Add Time Window constraint - time_evaluator_index = routing.RegisterTransitCallback( - partial(create_time_evaluator(data), manager) - ) - add_time_window_constraints(routing, manager, data, time_evaluator_index) - - # Setting first solution heuristic (cheapest addition). - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) # pylint: disable=no-member - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.time_limit.FromSeconds(3) - - # Solve the problem. - solution = routing.SolveWithParameters(search_parameters) - if solution: - print_solution(data, manager, routing, solution) - else: - print("No solution found !") - - -if __name__ == "__main__": - main() diff --git a/ortools/constraint_solver/samples/cvrptw_break.py b/ortools/constraint_solver/samples/cvrptw_break.py deleted file mode 100755 index f0a391dbc26..00000000000 --- a/ortools/constraint_solver/samples/cvrptw_break.py +++ /dev/null @@ -1,367 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Capacitated Vehicle Routing Problem with Time Windows (CVRPTW). - -This is a sample using the routing library python wrapper to solve a CVRPTW -problem. -A description of the problem can be found here: -http://en.wikipedia.org/wiki/Vehicle_routing_problem. - -Distances are in meters and time in minutes. -""" - -# [START import] -import functools -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - # Locations in block unit - locations_ = [ - # fmt: off - (4, 4), # depot - (2, 0), (8, 0), # locations to visit - (0, 1), (1, 1), - (5, 2), (7, 2), - (3, 3), (6, 3), - (5, 5), (8, 5), - (1, 6), (2, 6), - (3, 7), (6, 7), - (0, 8), (7, 8), - # fmt: on - ] - # Compute locations in meters using the block dimension defined as follow - # Manhattan average block: 750ft x 264ft -> 228m x 80m - # here we use: 114m x 80m city block - # src: https://nyti.ms/2GDoRIe "NY Times: Know Your distance" - data["locations"] = [(l[0] * 114, l[1] * 80) for l in locations_] - data["numlocations_"] = len(data["locations"]) - data["time_windows"] = [ - # fmt: off - (0, 0), # depot - (75, 85), (75, 85), # 1, 2 - (60, 70), (45, 55), # 3, 4 - (0, 8), (50, 60), # 5, 6 - (0, 10), (10, 20), # 7, 8 - (0, 10), (75, 85), # 9, 10 - (85, 95), (5, 15), # 11, 12 - (15, 25), (10, 20), # 13, 14 - (45, 55), (30, 40), - # 15, 16 - # fmt: on - ] - data["demands"] = [ - # fmt: off - 0, # depot - 1, 1, # 1, 2 - 2, 4, # 3, 4 - 2, 4, # 5, 6 - 8, 8, # 7, 8 - 1, 2, # 9, 10 - 1, 2, # 11, 12 - 4, 4, # 13, 14 - 8, 8, - # 15, 16 - # fmt: on - ] - data["time_per_demand_unit"] = 5 # 5 minutes/unit - data["num_vehicles"] = 4 - data["breaks"] = [(2, False), (2, False), (2, False), (2, False)] - data["vehicle_capacity"] = 15 - data["vehicle_speed"] = 83 # Travel speed: 5km/h converted in m/min - data["depot"] = 0 - return data - # [END data_model] - - -def manhattan_distance(position_1, position_2): - """Computes the Manhattan distance between two points.""" - return abs(position_1[0] - position_2[0]) + abs(position_1[1] - position_2[1]) - - -def create_distance_evaluator(data): - """Creates callback to return distance between points.""" - distances_ = {} - # precompute distance between location to have distance callback in O(1) - for from_node in range(data["numlocations_"]): - distances_[from_node] = {} - for to_node in range(data["numlocations_"]): - if from_node == to_node: - distances_[from_node][to_node] = 0 - else: - distances_[from_node][to_node] = manhattan_distance( - data["locations"][from_node], data["locations"][to_node] - ) - - def distance_evaluator(manager, from_node, to_node): - """Returns the manhattan distance between the two nodes.""" - return distances_[manager.IndexToNode(from_node)][manager.IndexToNode(to_node)] - - return distance_evaluator - - -def create_demand_evaluator(data): - """Creates callback to get demands at each location.""" - demands_ = data["demands"] - - def demand_evaluator(manager, node): - """Returns the demand of the current node.""" - return demands_[manager.IndexToNode(node)] - - return demand_evaluator - - -def add_capacity_constraints(routing, data, demand_evaluator_index): - """Adds capacity constraint.""" - capacity = "Capacity" - routing.AddDimension( - demand_evaluator_index, - 0, # null capacity slack - data["vehicle_capacity"], - True, # start cumul to zero - capacity, - ) - - -def create_time_evaluator(data): - """Creates callback to get total times between locations.""" - - def service_time(data, node): - """Gets the service time for the specified location.""" - return data["demands"][node] * data["time_per_demand_unit"] - - def travel_time(data, from_node, to_node): - """Gets the travel times between two locations.""" - if from_node == to_node: - travel_time = 0 - else: - travel_time = ( - manhattan_distance( - data["locations"][from_node], data["locations"][to_node] - ) - / data["vehicle_speed"] - ) - return travel_time - - total_time_ = {} - # precompute total time to have time callback in O(1) - for from_node in range(data["numlocations_"]): - total_time_[from_node] = {} - for to_node in range(data["numlocations_"]): - if from_node == to_node: - total_time_[from_node][to_node] = 0 - else: - total_time_[from_node][to_node] = int( - service_time(data, from_node) - + travel_time(data, from_node, to_node) - ) - - def time_evaluator(manager, from_node, to_node): - """Returns the total time between the two nodes.""" - return total_time_[manager.IndexToNode(from_node)][manager.IndexToNode(to_node)] - - return time_evaluator - - -def add_time_window_constraints(routing, manager, data, time_evaluator_index): - """Add Global Span constraint.""" - time = "Time" - horizon = 120 - routing.AddDimension( - time_evaluator_index, - horizon, # allow waiting time - horizon, # maximum time per vehicle - False, # don't force start cumul to zero - time, - ) - time_dimension = routing.GetDimensionOrDie(time) - # Add time window constraints for each location except depot - # and 'copy' the slack var in the solution object (aka Assignment) to print it - for location_idx, time_window in enumerate(data["time_windows"]): - if location_idx == data["depot"]: - continue - index = manager.NodeToIndex(location_idx) - time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) - routing.AddToAssignment(time_dimension.SlackVar(index)) - # Add time window constraints for each vehicle start node - # and 'copy' the slack var in the solution object (aka Assignment) to print it - for vehicle_id in range(data["num_vehicles"]): - index = routing.Start(vehicle_id) - time_dimension.CumulVar(index).SetRange( - data["time_windows"][0][0], data["time_windows"][0][1] - ) - routing.AddToAssignment(time_dimension.SlackVar(index)) - # The time window at the end node was impliclty set in the time dimension - # definition to be [0, horizon]. - # Warning: Slack var is not defined for vehicle end nodes and should not - # be added to the assignment. - - -# [START solution_printer] -def print_solution( - data, manager, routing, assignment -): # pylint:disable=too-many-locals - """Prints assignment on console.""" - print(f"Objective: {assignment.ObjectiveValue()}") - - print("Breaks:") - intervals = assignment.IntervalVarContainer() - for i in range(intervals.Size()): - brk = intervals.Element(i) - if brk.PerformedValue() == 1: - print( - f"{brk.Var().Name()}:" - f" Start({brk.StartValue()}) Duration({brk.DurationValue()})" - ) - else: - print(f"{brk.Var().Name()}: Unperformed") - - total_distance = 0 - total_load = 0 - total_time = 0 - capacity_dimension = routing.GetDimensionOrDie("Capacity") - time_dimension = routing.GetDimensionOrDie("Time") - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(assignment, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - distance = 0 - while not routing.IsEnd(index): - load_var = capacity_dimension.CumulVar(index) - time_var = time_dimension.CumulVar(index) - slack_var = time_dimension.SlackVar(index) - node = manager.IndexToNode(index) - plan_output += ( - f" {node}" - f" Load({assignment.Value(load_var)})" - f" Time({assignment.Min(time_var)}, {assignment.Max(time_var)})" - f" Slack({assignment.Min(slack_var)}, {assignment.Max(slack_var)})" - " ->" - ) - previous_index = index - index = assignment.Value(routing.NextVar(index)) - distance += routing.GetArcCostForVehicle(previous_index, index, vehicle_id) - load_var = capacity_dimension.CumulVar(index) - time_var = time_dimension.CumulVar(index) - node = manager.IndexToNode(index) - plan_output += ( - f" {node}" - f" Load({assignment.Value(load_var)})" - f" Time({assignment.Min(time_var)}, {assignment.Max(time_var)})\n" - ) - plan_output += f"Distance of the route: {distance}m\n" - plan_output += f"Load of the route: {assignment.Value(load_var)}\n" - plan_output += f"Time of the route: {assignment.Value(time_var)}\n" - print(plan_output) - total_distance += distance - total_load += assignment.Value(load_var) - total_time += assignment.Value(time_var) - print(f"Total Distance of all routes: {total_distance}m") - print(f"Total Load of all routes: {total_load}") - print(f"Total Time of all routes: {total_time}min") - # [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager - manager = pywrapcp.RoutingIndexManager( - data["numlocations_"], data["num_vehicles"], data["depot"] - ) - - # Create Routing Model - routing = pywrapcp.RoutingModel(manager) - - # Define weight of each edge - distance_evaluator_index = routing.RegisterTransitCallback( - functools.partial(create_distance_evaluator(data), manager) - ) - routing.SetArcCostEvaluatorOfAllVehicles(distance_evaluator_index) - - # Add Capacity constraint - demand_evaluator_index = routing.RegisterUnaryTransitCallback( - functools.partial(create_demand_evaluator(data), manager) - ) - add_capacity_constraints(routing, data, demand_evaluator_index) - - # Add Time Window constraint - time_evaluator_index = routing.RegisterTransitCallback( - functools.partial(create_time_evaluator(data), manager) - ) - add_time_window_constraints(routing, manager, data, time_evaluator_index) - - # Add breaks - time_dimension = routing.GetDimensionOrDie("Time") - node_visit_transit = {} - for index in range(routing.Size()): - node = manager.IndexToNode(index) - node_visit_transit[index] = int( - data["demands"][node] * data["time_per_demand_unit"] - ) - - break_intervals = {} - for v in range(data["num_vehicles"]): - vehicle_break = data["breaks"][v] - break_intervals[v] = [ - routing.solver().FixedDurationIntervalVar( - 15, - 100, - vehicle_break[0], - vehicle_break[1], - f"Break for vehicle {v}", - ) - ] - time_dimension.SetBreakIntervalsOfVehicle( - break_intervals[v], v, node_visit_transit.values() - ) - - # Setting first solution heuristic (cheapest addition). - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) # pylint: disable=no-member - # [END parameters] - - # Solve the problem. - # [START solve] - assignment = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if assignment: - print_solution(data, manager, routing, assignment) - else: - print("No solution found!") - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/nqueens_cp.py b/ortools/constraint_solver/samples/nqueens_cp.py index eed0d217b24..2002beb4d52 100755 --- a/ortools/constraint_solver/samples/nqueens_cp.py +++ b/ortools/constraint_solver/samples/nqueens_cp.py @@ -17,68 +17,73 @@ # [START import] import sys from ortools.constraint_solver import pywrapcp + # [END import] def main(board_size): - # Creates the solver. - # [START solver] - solver = pywrapcp.Solver("n-queens") - # [END solver] + # Creates the solver. + # [START solver] + solver = pywrapcp.Solver("n-queens") + # [END solver] - # Creates the variables. - # [START variables] - # The array index is the column, and the value is the row. - queens = [solver.IntVar(0, board_size - 1, f"x{i}") for i in range(board_size)] - # [END variables] + # Creates the variables. + # [START variables] + # The array index is the column, and the value is the row. + queens = [ + solver.IntVar(0, board_size - 1, f"x{i}") for i in range(board_size) + ] + # [END variables] - # Creates the constraints. - # [START constraints] - # All rows must be different. - solver.Add(solver.AllDifferent(queens)) + # Creates the constraints. + # [START constraints] + # All rows must be different. + solver.Add(solver.AllDifferent(queens)) - # No two queens can be on the same diagonal. - solver.Add(solver.AllDifferent([queens[i] + i for i in range(board_size)])) - solver.Add(solver.AllDifferent([queens[i] - i for i in range(board_size)])) - # [END constraints] + # No two queens can be on the same diagonal. + solver.Add(solver.AllDifferent([queens[i] + i for i in range(board_size)])) + solver.Add(solver.AllDifferent([queens[i] - i for i in range(board_size)])) + # [END constraints] - # [START db] - db = solver.Phase(queens, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE) - # [END db] + # [START db] + db = solver.Phase( + queens, solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + # [END db] - # [START solve] - # Iterates through the solutions, displaying each. - num_solutions = 0 - solver.NewSearch(db) - while solver.NextSolution(): - # Displays the solution just computed. - for i in range(board_size): - for j in range(board_size): - if queens[j].Value() == i: - # There is a queen in column j, row i. - print("Q", end=" ") - else: - print("_", end=" ") - print() - print() - num_solutions += 1 - solver.EndSearch() - # [END solve] + # [START solve] + # Iterates through the solutions, displaying each. + num_solutions = 0 + solver.NewSearch(db) + while solver.NextSolution(): + # Displays the solution just computed. + for i in range(board_size): + for j in range(board_size): + if queens[j].Value() == i: + # There is a queen in column j, row i. + print("Q", end=" ") + else: + print("_", end=" ") + print() + print() + num_solutions += 1 + solver.EndSearch() + # [END solve] - # Statistics. - # [START statistics] - print("\nStatistics") - print(f" failures: {solver.Failures()}") - print(f" branches: {solver.Branches()}") - print(f" wall time: {solver.WallTime()} ms") - print(f" Solutions found: {num_solutions}") - # [END statistics] + # Statistics. + # [START statistics] + print("\nStatistics") + print(f" failures: {solver.Failures()}") + print(f" branches: {solver.Branches()}") + print(f" wall time: {solver.WallTime()} ms") + print(f" Solutions found: {num_solutions}") + # [END statistics] if __name__ == "__main__": - # By default, solve the 8x8 problem. - size = 8 - if len(sys.argv) > 1: - size = int(sys.argv[1]) - main(size) + # By default, solve the 8x8 problem. + size = 8 + if len(sys.argv) > 1: + size = int(sys.argv[1]) + main(size) # [END program] diff --git a/ortools/constraint_solver/samples/simple_cp_program.py b/ortools/constraint_solver/samples/simple_cp_program.py index 7c627995589..3e5522ad0bf 100755 --- a/ortools/constraint_solver/samples/simple_cp_program.py +++ b/ortools/constraint_solver/samples/simple_cp_program.py @@ -17,58 +17,59 @@ # [START import] from ortools.constraint_solver import pywrapcp + # [END import] def main(): - """Entry point of the program.""" - # Instantiate the solver. - # [START solver] - solver = pywrapcp.Solver("CPSimple") - # [END solver] + """Entry point of the program.""" + # Instantiate the solver. + # [START solver] + solver = pywrapcp.Solver("CPSimple") + # [END solver] - # Create the variables. - # [START variables] - num_vals = 3 - x = solver.IntVar(0, num_vals - 1, "x") - y = solver.IntVar(0, num_vals - 1, "y") - z = solver.IntVar(0, num_vals - 1, "z") - # [END variables] + # Create the variables. + # [START variables] + num_vals = 3 + x = solver.IntVar(0, num_vals - 1, "x") + y = solver.IntVar(0, num_vals - 1, "y") + z = solver.IntVar(0, num_vals - 1, "z") + # [END variables] - # Constraint 0: x != y. - # [START constraints] - solver.Add(x != y) - print("Number of constraints: ", solver.Constraints()) - # [END constraints] + # Constraint 0: x != y. + # [START constraints] + solver.Add(x != y) + print("Number of constraints: ", solver.Constraints()) + # [END constraints] - # Solve the problem. - # [START solve] - decision_builder = solver.Phase( - [x, y, z], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE - ) - # [END solve] + # Solve the problem. + # [START solve] + decision_builder = solver.Phase( + [x, y, z], solver.CHOOSE_FIRST_UNBOUND, solver.ASSIGN_MIN_VALUE + ) + # [END solve] - # Print solution on console. - # [START print_solution] - count = 0 - solver.NewSearch(decision_builder) - while solver.NextSolution(): - count += 1 - solution = f"Solution {count}:\n" - for var in [x, y, z]: - solution += f" {var.Name()} = {var.Value()}" - print(solution) - solver.EndSearch() - print(f"Number of solutions found: {count}") - # [END print_solution] + # Print solution on console. + # [START print_solution] + count = 0 + solver.NewSearch(decision_builder) + while solver.NextSolution(): + count += 1 + solution = f"Solution {count}:\n" + for var in [x, y, z]: + solution += f" {var.Name()} = {var.Value()}" + print(solution) + solver.EndSearch() + print(f"Number of solutions found: {count}") + # [END print_solution] - # [START advanced] - print("Advanced usage:") - print(f"Problem solved in {solver.WallTime()}ms") - print(f"Memory usage: {pywrapcp.Solver.MemoryUsage()}bytes") - # [END advanced] + # [START advanced] + print("Advanced usage:") + print(f"Problem solved in {solver.WallTime()}ms") + print(f"Memory usage: {pywrapcp.Solver.MemoryUsage()}bytes") + # [END advanced] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/constraint_solver/samples/simple_routing_program.py b/ortools/constraint_solver/samples/simple_routing_program.py deleted file mode 100755 index 04c7abc9884..00000000000 --- a/ortools/constraint_solver/samples/simple_routing_program.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Vehicle Routing example.""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - num_locations = 5 - num_vehicles = 1 - depot = 0 - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager(num_locations, num_vehicles, depot) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the absolute difference between the two nodes.""" - # Convert from routing variable Index to user NodeIndex. - from_node = int(manager.IndexToNode(from_index)) - to_node = int(manager.IndexToNode(to_index)) - return abs(to_node - from_node) - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) # pylint: disable=no-member - # [END parameters] - - # Solve the problem. - # [START solve] - assignment = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - print(f"Objective: {assignment.ObjectiveValue()}") - index = routing.Start(0) - plan_output = "Route for vehicle 0:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f"{manager.IndexToNode(index)} -> " - previous_index = index - index = assignment.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += f"{manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/tsp.py b/ortools/constraint_solver/samples/tsp.py deleted file mode 100755 index 6e100b8c755..00000000000 --- a/ortools/constraint_solver/samples/tsp.py +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Travelling Salesman Problem. - -A description of the problem can be found here: -http://en.wikipedia.org/wiki/Travelling_salesperson_problem. -""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - # Locations in block units - locations = [ - # fmt:off - (4, 4), # depot - (2, 0), (8, 0), # locations to visit - (0, 1), (1, 1), - (5, 2), (7, 2), - (3, 3), (6, 3), - (5, 5), (8, 5), - (1, 6), (2, 6), - (3, 7), (6, 7), - (0, 8), (7, 8) - # fmt:on - ] - # Convert locations in meters using a city block dimension of 114m x 80m. - data["locations"] = [(l[0] * 114, l[1] * 80) for l in locations] - data["num_vehicles"] = 1 - data["depot"] = 0 - return data - # [END data_model] - - -# [START distance_callback] -def create_distance_callback(data, manager): - """Creates callback to return distance between points.""" - distances_ = {} - index_manager_ = manager - # precompute distance between location to have distance callback in O(1) - for from_counter, from_node in enumerate(data["locations"]): - distances_[from_counter] = {} - for to_counter, to_node in enumerate(data["locations"]): - if from_counter == to_counter: - distances_[from_counter][to_counter] = 0 - else: - distances_[from_counter][to_counter] = abs( - from_node[0] - to_node[0] - ) + abs(from_node[1] - to_node[1]) - - def distance_callback(from_index, to_index): - """Returns the manhattan distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = index_manager_.IndexToNode(from_index) - to_node = index_manager_.IndexToNode(to_index) - return distances_[from_node][to_node] - - return distance_callback - # [END distance_callback] - - -# [START solution_printer] -def print_solution(manager, routing, assignment): - """Prints assignment on console.""" - print(f"Objective: {assignment.ObjectiveValue()}") - index = routing.Start(0) - plan_output = "Route for vehicle 0:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} ->" - previous_index = index - index = assignment.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += f" {manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - # [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["locations"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - distance_callback = create_distance_callback(data, manager) - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - assignment = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if assignment: - print_solution(manager, routing, assignment) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/tsp_cities.py b/ortools/constraint_solver/samples/tsp_cities.py deleted file mode 100755 index c17007fdb95..00000000000 --- a/ortools/constraint_solver/samples/tsp_cities.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Travelling Salesperson Problem (TSP) between cities.""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - [0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972], - [2451, 0, 1745, 1524, 831, 1240, 959, 2596, 403, 1589, 1374, 357, 579], - [713, 1745, 0, 355, 920, 803, 1737, 851, 1858, 262, 940, 1453, 1260], - [1018, 1524, 355, 0, 700, 862, 1395, 1123, 1584, 466, 1056, 1280, 987], - [1631, 831, 920, 700, 0, 663, 1021, 1769, 949, 796, 879, 586, 371], - [1374, 1240, 803, 862, 663, 0, 1681, 1551, 1765, 547, 225, 887, 999], - [2408, 959, 1737, 1395, 1021, 1681, 0, 2493, 678, 1724, 1891, 1114, 701], - [213, 2596, 851, 1123, 1769, 1551, 2493, 0, 2699, 1038, 1605, 2300, 2099], - [2571, 403, 1858, 1584, 949, 1765, 678, 2699, 0, 1744, 1645, 653, 600], - [875, 1589, 262, 466, 796, 547, 1724, 1038, 1744, 0, 679, 1272, 1162], - [1420, 1374, 940, 1056, 879, 225, 1891, 1605, 1645, 679, 0, 1017, 1200], - [2145, 357, 1453, 1280, 586, 887, 1114, 2300, 653, 1272, 1017, 0, 504], - [1972, 579, 1260, 987, 371, 999, 701, 2099, 600, 1162, 1200, 504, 0], - ] - data["num_vehicles"] = 1 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()} miles") - index = routing.Start(0) - plan_output = "Route for vehicle 0:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} ->" - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += f" {manager.IndexToNode(index)}\n" - plan_output += f"Route distance: {route_distance}miles\n" - print(plan_output) - # [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - - # [END routing_model] - - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(manager, routing, solution) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/tsp_distance_matrix.py b/ortools/constraint_solver/samples/tsp_distance_matrix.py deleted file mode 100755 index ff3c5ef2840..00000000000 --- a/ortools/constraint_solver/samples/tsp_distance_matrix.py +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Travelling Salesman Problem.""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - data["num_vehicles"] = 1 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - index = routing.Start(0) - plan_output = "Route for vehicle 0:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} ->" - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += f" {manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - # [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(manager, routing, solution) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp.py b/ortools/constraint_solver/samples/vrp.py deleted file mode 100755 index 2a4d857a344..00000000000 --- a/ortools/constraint_solver/samples/vrp.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Vehicles Routing Problem (VRP). - -This is a sample using the routing library python wrapper to solve a VRP -problem. -A description of the problem can be found here: -http://en.wikipedia.org/wiki/Vehicle_routing_problem. - -Distances are in meters. -""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - total_distance = 0 - for vehicle_index in range(manager.GetNumberOfVehicles()): - if not routing.IsVehicleUsed(solution, vehicle_index): - continue - index = routing.Start(vehicle_index) - plan_output = f"Route for vehicle {vehicle_index}:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} ->" - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_index - ) - plan_output += f" {manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - total_distance += route_distance - print(f"Total Distance of all routes: {total_distance}m") - -# [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(data, manager, routing, solution) - else: - print("No solution found !") - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_breaks.py b/ortools/constraint_solver/samples/vrp_breaks.py deleted file mode 100755 index 23139203635..00000000000 --- a/ortools/constraint_solver/samples/vrp_breaks.py +++ /dev/null @@ -1,206 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Vehicle Routing Problem (VRP) with breaks. - - This is a sample using the routing library python wrapper to solve a VRP - problem. - A description of the problem can be found here: - http://en.wikipedia.org/wiki/Vehicle_routing_problem. - - Durations are in minutes. -""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["num_vehicles"] = 4 - data["depot"] = 0 - data["time_matrix"] = [ - [0, 27, 38, 34, 29, 13, 25, 9, 15, 9, 26, 25, 19, 17, 23, 38, 33], - [27, 0, 34, 15, 9, 25, 36, 17, 34, 37, 54, 29, 24, 33, 50, 43, 60], - [38, 34, 0, 49, 43, 25, 13, 40, 23, 37, 20, 63, 58, 56, 39, 77, 37], - [34, 15, 49, 0, 5, 32, 43, 25, 42, 44, 61, 25, 31, 41, 58, 28, 67], - [29, 9, 43, 5, 0, 26, 38, 19, 36, 38, 55, 20, 25, 35, 52, 33, 62], - [13, 25, 25, 32, 26, 0, 11, 15, 9, 12, 29, 38, 33, 31, 25, 52, 35], - [25, 36, 13, 43, 38, 11, 0, 26, 9, 23, 17, 50, 44, 42, 25, 63, 24], - [9, 17, 40, 25, 19, 15, 26, 0, 17, 19, 36, 23, 17, 16, 33, 37, 42], - [15, 34, 23, 42, 36, 9, 9, 17, 0, 13, 19, 40, 34, 33, 16, 54, 25], - [9, 37, 37, 44, 38, 12, 23, 19, 13, 0, 17, 26, 21, 19, 13, 40, 23], - [26, 54, 20, 61, 55, 29, 17, 36, 19, 17, 0, 43, 38, 36, 19, 57, 17], - [25, 29, 63, 25, 20, 38, 50, 23, 40, 26, 43, 0, 5, 15, 32, 13, 42], - [19, 24, 58, 31, 25, 33, 44, 17, 34, 21, 38, 5, 0, 9, 26, 19, 36], - [17, 33, 56, 41, 35, 31, 42, 16, 33, 19, 36, 15, 9, 0, 17, 21, 26], - [23, 50, 39, 58, 52, 25, 25, 33, 16, 13, 19, 32, 26, 17, 0, 38, 9], - [38, 43, 77, 28, 33, 52, 63, 37, 54, 40, 57, 13, 19, 21, 38, 0, 39], - [33, 60, 37, 67, 62, 35, 24, 42, 25, 23, 17, 42, 36, 26, 9, 39, 0], - ] - # 15 min of service time - data["service_time"] = [15] * len(data["time_matrix"]) - data["service_time"][data["depot"]] = 0 - assert len(data["time_matrix"]) == len(data["service_time"]) - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - - print("Breaks:") - intervals = solution.IntervalVarContainer() - for i in range(intervals.Size()): - brk = intervals.Element(i) - if brk.PerformedValue(): - print( - f"{brk.Var().Name()}: " - + f"Start({brk.StartValue()}) Duration({brk.DurationValue()})" - ) - else: - print(f"{brk.Var().Name()}: Unperformed") - - time_dimension = routing.GetDimensionOrDie("Time") - total_time = 0 - for vehicle_id in range(manager.GetNumberOfVehicles()): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - while not routing.IsEnd(index): - time_var = time_dimension.CumulVar(index) - plan_output += f"{manager.IndexToNode(index)} " - plan_output += f"Time({solution.Value(time_var)}) -> " - index = solution.Value(routing.NextVar(index)) - time_var = time_dimension.CumulVar(index) - plan_output += f"{manager.IndexToNode(index)} " - plan_output += f"Time({solution.Value(time_var)})\n" - plan_output += f"Time of the route: {solution.Value(time_var)}min\n" - print(plan_output) - total_time += solution.Value(time_var) - print(f"Total time of all routes: {total_time}min") - # [END solution_printer] - - -def main(): - """Solve the VRP with time windows.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["time_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def time_callback(from_index, to_index): - """Returns the travel time + service time between the two nodes.""" - # Convert from routing variable Index to time matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["time_matrix"][from_node][to_node] + data["service_time"][from_node] - - transit_callback_index = routing.RegisterTransitCallback(time_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Time Windows constraint. - time = "Time" - routing.AddDimension( - transit_callback_index, - 10, # needed optional waiting time to place break - 180, # maximum time per vehicle - True, # Force start cumul to zero. - time, - ) - time_dimension = routing.GetDimensionOrDie(time) - time_dimension.SetGlobalSpanCostCoefficient(10) - - # Breaks - # [START break_constraint] - # warning: Need a pre-travel array using the solver's index order. - node_visit_transit = [0] * routing.Size() - for index in range(routing.Size()): - node = manager.IndexToNode(index) - node_visit_transit[index] = data["service_time"][node] - - break_intervals = {} - for v in range(manager.GetNumberOfVehicles()): - break_intervals[v] = [ - routing.solver().FixedDurationIntervalVar( - 50, # start min - 60, # start max - 10, # duration: 10 min - False, # optional: no - f"Break for vehicle {v}", - ) - ] - time_dimension.SetBreakIntervalsOfVehicle( - break_intervals[v], v, node_visit_transit # breaks # vehicle index - ) - # [END break_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - # search_parameters.log_search = True - search_parameters.time_limit.FromSeconds(2) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(manager, routing, solution) - else: - print("No solution found !") - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_breaks_from_start.py b/ortools/constraint_solver/samples/vrp_breaks_from_start.py deleted file mode 100755 index 0792b901cd7..00000000000 --- a/ortools/constraint_solver/samples/vrp_breaks_from_start.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# [START program] -"""Vehicles Routing Problem (VRP) with breaks relative to the vehicle start time. - -Each vehicles start at T:15min, T:30min, T:45min and T:60min respectively. - -Each vehicle must perform a break lasting 5 minutes, -starting between 25 and 45 minutes after route start. -e.g. vehicle 2 starting a T:45min must start a 5min breaks -between [45+25,45+45] i.e. in the range [70, 90]. - -Durations are in minutes. -""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["num_vehicles"] = 4 - data["depot"] = 0 - data["time_matrix"] = [ - [0, 27, 38, 34, 29, 13, 25, 9, 15, 9, 26, 25, 19, 17, 23, 38, 33], - [27, 0, 34, 15, 9, 25, 36, 17, 34, 37, 54, 29, 24, 33, 50, 43, 60], - [38, 34, 0, 49, 43, 25, 13, 40, 23, 37, 20, 63, 58, 56, 39, 77, 37], - [34, 15, 49, 0, 5, 32, 43, 25, 42, 44, 61, 25, 31, 41, 58, 28, 67], - [29, 9, 43, 5, 0, 26, 38, 19, 36, 38, 55, 20, 25, 35, 52, 33, 62], - [13, 25, 25, 32, 26, 0, 11, 15, 9, 12, 29, 38, 33, 31, 25, 52, 35], - [25, 36, 13, 43, 38, 11, 0, 26, 9, 23, 17, 50, 44, 42, 25, 63, 24], - [9, 17, 40, 25, 19, 15, 26, 0, 17, 19, 36, 23, 17, 16, 33, 37, 42], - [15, 34, 23, 42, 36, 9, 9, 17, 0, 13, 19, 40, 34, 33, 16, 54, 25], - [9, 37, 37, 44, 38, 12, 23, 19, 13, 0, 17, 26, 21, 19, 13, 40, 23], - [26, 54, 20, 61, 55, 29, 17, 36, 19, 17, 0, 43, 38, 36, 19, 57, 17], - [25, 29, 63, 25, 20, 38, 50, 23, 40, 26, 43, 0, 5, 15, 32, 13, 42], - [19, 24, 58, 31, 25, 33, 44, 17, 34, 21, 38, 5, 0, 9, 26, 19, 36], - [17, 33, 56, 41, 35, 31, 42, 16, 33, 19, 36, 15, 9, 0, 17, 21, 26], - [23, 50, 39, 58, 52, 25, 25, 33, 16, 13, 19, 32, 26, 17, 0, 38, 9], - [38, 43, 77, 28, 33, 52, 63, 37, 54, 40, 57, 13, 19, 21, 38, 0, 39], - [33, 60, 37, 67, 62, 35, 24, 42, 25, 23, 17, 42, 36, 26, 9, 39, 0], - ] - # 15 min of service time - data["service_time"] = [15] * len(data["time_matrix"]) - data["service_time"][data["depot"]] = 0 - assert len(data["time_matrix"]) == len(data["service_time"]) - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - - print("Breaks:") - intervals = solution.IntervalVarContainer() - for i in range(intervals.Size()): - brk = intervals.Element(i) - if brk.PerformedValue() == 1: - print( - f"{brk.Var().Name()}: " - + f"Start({brk.StartValue()}) Duration({brk.DurationValue()})" - ) - else: - print(f"{brk.Var().Name()}: Unperformed") - - time_dimension = routing.GetDimensionOrDie("Time") - total_time = 0 - for vehicle_id in range(manager.GetNumberOfVehicles()): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - while not routing.IsEnd(index): - time_var = time_dimension.CumulVar(index) - if routing.IsStart(index): - start_time = solution.Value(time_var) - plan_output += f"{manager.IndexToNode(index)} " - plan_output += f"Time({solution.Value(time_var)}) -> " - index = solution.Value(routing.NextVar(index)) - time_var = time_dimension.CumulVar(index) - plan_output += f"{manager.IndexToNode(index)} " - plan_output += f"Time({solution.Value(time_var)})" - print(plan_output) - route_time = solution.Value(time_var) - start_time - print(f"Time of the route: {route_time}min\n") - total_time += route_time - print(f"Total time of all routes: {total_time}min") - # [END solution_printer] - - -def main(): - """Solve the VRP with time windows.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["time_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def time_callback(from_index, to_index): - """Returns the travel time between the two nodes.""" - # Convert from routing variable Index to time matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["time_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(time_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Time Windows constraint. - time = "Time" - routing.AddDimension( - transit_callback_index, - 10, # need optional waiting time to place break - 180, # maximum time per vehicle - False, # Don't force start cumul to zero. - time, - ) - time_dimension = routing.GetDimensionOrDie(time) - time_dimension.SetGlobalSpanCostCoefficient(10) - - # Each vehicle start with a 15min delay - for vehicle_id in range(manager.GetNumberOfVehicles()): - index = routing.Start(vehicle_id) - time_dimension.CumulVar(index).SetValue((vehicle_id + 1) * 15) - - # Add breaks - # [START break_constraint] - # warning: Need a pre-travel array using the solver's index order. - node_visit_transit = [0] * routing.Size() - for index in range(routing.Size()): - node = manager.IndexToNode(index) - node_visit_transit[index] = data["service_time"][node] - - # Add a break lasting 5 minutes, start between 25 and 45 minutes after route start - for v in range(manager.GetNumberOfVehicles()): - start_var = time_dimension.CumulVar(routing.Start(v)) - break_start = routing.solver().Sum([routing.solver().IntVar(25, 45), start_var]) - - break_intervals = [ - routing.solver().FixedDurationIntervalVar( - break_start, 5, f"Break for vehicle {v}" - ) - ] - time_dimension.SetBreakIntervalsOfVehicle( - break_intervals, v, node_visit_transit - ) - # [END break_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - # search_parameters.log_search = True - search_parameters.time_limit.FromSeconds(2) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(manager, routing, solution) - else: - print("No solution found !") - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_capacity.py b/ortools/constraint_solver/samples/vrp_capacity.py deleted file mode 100755 index f720a7f5b7f..00000000000 --- a/ortools/constraint_solver/samples/vrp_capacity.py +++ /dev/null @@ -1,173 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Capacited Vehicles Routing Problem (CVRP).""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - # [START demands_capacities] - data["demands"] = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8] - data["vehicle_capacities"] = [15, 15, 15, 15] - # [END demands_capacities] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - total_distance = 0 - total_load = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - route_load = 0 - while not routing.IsEnd(index): - node_index = manager.IndexToNode(index) - route_load += data["demands"][node_index] - plan_output += f" {node_index} Load({route_load}) -> " - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f" {manager.IndexToNode(index)} Load({route_load})\n" - plan_output += f"Distance of the route: {route_distance}m\n" - plan_output += f"Load of the route: {route_load}\n" - print(plan_output) - total_distance += route_distance - total_load += route_load - print(f"Total distance of all routes: {total_distance}m") - print(f"Total load of all routes: {total_load}") - # [END solution_printer] - - -def main(): - """Solve the CVRP problem.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Capacity constraint. - # [START capacity_constraint] - def demand_callback(from_index): - """Returns the demand of the node.""" - # Convert from routing variable Index to demands NodeIndex. - from_node = manager.IndexToNode(from_index) - return data["demands"][from_node] - - demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback) - routing.AddDimensionWithVehicleCapacity( - demand_callback_index, - 0, # null capacity slack - data["vehicle_capacities"], # vehicle maximum capacities - True, # start cumul to zero - "Capacity", - ) - # [END capacity_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.time_limit.FromSeconds(1) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(data, manager, routing, solution) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_drop_nodes.py b/ortools/constraint_solver/samples/vrp_drop_nodes.py deleted file mode 100755 index cb62ed8e17b..00000000000 --- a/ortools/constraint_solver/samples/vrp_drop_nodes.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Capacited Vehicles Routing Problem (CVRP).""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - # [START demands_capacities] - data["demands"] = [0, 1, 1, 3, 6, 3, 6, 8, 8, 1, 2, 1, 2, 6, 6, 8, 8] - data["vehicle_capacities"] = [15, 15, 15, 15] - # [END demands_capacities] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, assignment): - """Prints assignment on console.""" - print(f"Objective: {assignment.ObjectiveValue()}") - # Display dropped nodes. - dropped_nodes = "Dropped nodes:" - for node in range(routing.Size()): - if routing.IsStart(node) or routing.IsEnd(node): - continue - if assignment.Value(routing.NextVar(node)) == node: - dropped_nodes += f" {manager.IndexToNode(node)}" - print(dropped_nodes) - # Display routes - total_distance = 0 - total_load = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(assignment, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - route_load = 0 - while not routing.IsEnd(index): - node_index = manager.IndexToNode(index) - route_load += data["demands"][node_index] - plan_output += f" {node_index} Load({route_load}) -> " - previous_index = index - index = assignment.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f" {manager.IndexToNode(index)} Load({route_load})\n" - plan_output += f"Distance of the route: {route_distance}m\n" - plan_output += f"Load of the route: {route_load}\n" - print(plan_output) - total_distance += route_distance - total_load += route_load - print(f"Total Distance of all routes: {total_distance}m") - print(f"Total Load of all routes: {total_load}") - # [END solution_printer] - - -def main(): - """Solve the CVRP problem.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Capacity constraint. - # [START capacity_constraint] - def demand_callback(from_index): - """Returns the demand of the node.""" - # Convert from routing variable Index to demands NodeIndex. - from_node = manager.IndexToNode(from_index) - return data["demands"][from_node] - - demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback) - routing.AddDimensionWithVehicleCapacity( - demand_callback_index, - 0, # null capacity slack - data["vehicle_capacities"], # vehicle maximum capacities - True, # start cumul to zero - "Capacity", - ) - # Allow to drop nodes. - penalty = 1000 - for node in range(1, len(data["distance_matrix"])): - routing.AddDisjunction([manager.NodeToIndex(node)], penalty) - # [END capacity_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.time_limit.FromSeconds(1) - # [END parameters] - - # Solve the problem. - # [START solve] - assignment = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if assignment: - print_solution(data, manager, routing, assignment) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_global_span.py b/ortools/constraint_solver/samples/vrp_global_span.py deleted file mode 100755 index cb37a2ad951..00000000000 --- a/ortools/constraint_solver/samples/vrp_global_span.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Vehicles Routing Problem (VRP). - -This is a sample using the routing library python wrapper to solve a VRP -problem. -A description of the problem can be found here: -http://en.wikipedia.org/wiki/Vehicle_routing_problem. - -Distances are in meters. -""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - max_route_distance = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} -> " - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f"{manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - max_route_distance = max(route_distance, max_route_distance) - print(f"Maximum of the route distances: {max_route_distance}m") - -# [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing.AddDimension( - transit_callback_index, - 0, # no slack - 3000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - # [END distance_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(data, manager, routing, solution) - else: - print("No solution found !") - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_initial_routes.py b/ortools/constraint_solver/samples/vrp_initial_routes.py deleted file mode 100755 index a70634205b5..00000000000 --- a/ortools/constraint_solver/samples/vrp_initial_routes.py +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Vehicles Routing Problem (VRP).""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - # [START initial_routes] - data["initial_routes"] = [ - # fmt: off - [8, 16, 14, 13, 12, 11], - [3, 4, 9, 10], - [15, 1], - [7, 5, 2, 6], - # fmt: on - ] - # [END initial_routes] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - max_route_distance = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} -> " - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f"{manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - max_route_distance = max(route_distance, max_route_distance) - print(f"Maximum of the route distances: {max_route_distance}m") - -# [END solution_printer] - - -def main(): - """Solve the CVRP problem.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing.AddDimension( - transit_callback_index, - 0, # no slack - 3000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - # [END distance_constraint] - - # Close model with the custom search parameters. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.time_limit.FromSeconds(5) - # When an initial solution is given for search, the model will be closed with - # the default search parameters unless it is explicitly closed with the custom - # search parameters. - routing.CloseModelWithParameters(search_parameters) - # [END parameters] - - # Get initial solution from routes after closing the model. - # [START print_initial_solution] - initial_solution = routing.ReadAssignmentFromRoutes(data["initial_routes"], True) - print("Initial solution:") - print_solution(data, manager, routing, initial_solution) - # [END print_initial_solution] - - # Solve the problem. - # [START solve] - solution = routing.SolveFromAssignmentWithParameters( - initial_solution, search_parameters - ) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print("Solution after search:") - print_solution(data, manager, routing, solution) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_items_to_deliver.py b/ortools/constraint_solver/samples/vrp_items_to_deliver.py deleted file mode 100755 index c2223652600..00000000000 --- a/ortools/constraint_solver/samples/vrp_items_to_deliver.py +++ /dev/null @@ -1,603 +0,0 @@ -#!/usr/bin/env python3 -# [START program] -"""Vehicles Routing Problem (VRP) for delivering items from any suppliers. - -Description: Need to deliver some item X and Y at end nodes (at least 11 X and -13 Y). Several locations provide them and even few provide both. - -fleet: - * vehicles: 2 - * x capacity: 15 - * y capacity: 15 - * start node: 0 - * end node: 1 -""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["num_vehicles"] = 2 - # [START starts_ends] - data["starts"] = [0] * data["num_vehicles"] - data["ends"] = [1] * data["num_vehicles"] - assert len(data["starts"]) == data["num_vehicles"] - assert len(data["ends"]) == data["num_vehicles"] - # [END starts_ends] - - # [START demands_capacities] - # Need 11 X and 13 Y - data["providers_x"] = [ - 0, # start - -11, # end - 2, # X supply 1 - 2, # X supply 2 - 4, # X supply 3 - 4, # X supply 4 - 4, # X supply 5 - 5, # X supply 6 - 1, # X/Y supply 1 - 2, # X/Y supply 2 - 2, # X/Y supply 3 - 0, # Y supply 1 - 0, # Y supply 2 - 0, # Y supply 3 - 0, # Y supply 4 - 0, # Y supply 5 - 0, # Y supply 6 - ] - data["providers_y"] = [ - 0, # start - -13, # ends - 0, # X supply 1 - 0, # X supply 2 - 0, # X supply 3 - 0, # X supply 4 - 0, # X supply 5 - 0, # X supply 6 - 3, # X/Y supply 1 - 2, # X/Y supply 2 - 1, # X/Y supply 3 - 3, # Y supply 1 - 3, # Y supply 2 - 3, # Y supply 3 - 3, # Y supply 4 - 3, # Y supply 5 - 5, # Y supply 6 - ] - data["vehicle_capacities_x"] = [15] * data["num_vehicles"] - data["vehicle_capacities_y"] = [15] * data["num_vehicles"] - assert len(data["vehicle_capacities_x"]) == data["num_vehicles"] - assert len(data["vehicle_capacities_y"]) == data["num_vehicles"] - # [END demands_capacities] - data["distance_matrix"] = [ - [ - 0, - 548, - 776, - 696, - 582, - 274, - 502, - 194, - 308, - 194, - 536, - 502, - 388, - 354, - 468, - 776, - 662, - ], - [ - 548, - 0, - 684, - 308, - 194, - 502, - 730, - 354, - 696, - 742, - 1084, - 594, - 480, - 674, - 1016, - 868, - 1210, - ], - [ - 776, - 684, - 0, - 992, - 878, - 502, - 274, - 810, - 468, - 742, - 400, - 1278, - 1164, - 1130, - 788, - 1552, - 754, - ], - [ - 696, - 308, - 992, - 0, - 114, - 650, - 878, - 502, - 844, - 890, - 1232, - 514, - 628, - 822, - 1164, - 560, - 1358, - ], - [ - 582, - 194, - 878, - 114, - 0, - 536, - 764, - 388, - 730, - 776, - 1118, - 400, - 514, - 708, - 1050, - 674, - 1244, - ], - [ - 274, - 502, - 502, - 650, - 536, - 0, - 228, - 308, - 194, - 240, - 582, - 776, - 662, - 628, - 514, - 1050, - 708, - ], - [ - 502, - 730, - 274, - 878, - 764, - 228, - 0, - 536, - 194, - 468, - 354, - 1004, - 890, - 856, - 514, - 1278, - 480, - ], - [ - 194, - 354, - 810, - 502, - 388, - 308, - 536, - 0, - 342, - 388, - 730, - 468, - 354, - 320, - 662, - 742, - 856, - ], - [ - 308, - 696, - 468, - 844, - 730, - 194, - 194, - 342, - 0, - 274, - 388, - 810, - 696, - 662, - 320, - 1084, - 514, - ], - [ - 194, - 742, - 742, - 890, - 776, - 240, - 468, - 388, - 274, - 0, - 342, - 536, - 422, - 388, - 274, - 810, - 468, - ], - [ - 536, - 1084, - 400, - 1232, - 1118, - 582, - 354, - 730, - 388, - 342, - 0, - 878, - 764, - 730, - 388, - 1152, - 354, - ], - [ - 502, - 594, - 1278, - 514, - 400, - 776, - 1004, - 468, - 810, - 536, - 878, - 0, - 114, - 308, - 650, - 274, - 844, - ], - [ - 388, - 480, - 1164, - 628, - 514, - 662, - 890, - 354, - 696, - 422, - 764, - 114, - 0, - 194, - 536, - 388, - 730, - ], - [ - 354, - 674, - 1130, - 822, - 708, - 628, - 856, - 320, - 662, - 388, - 730, - 308, - 194, - 0, - 342, - 422, - 536, - ], - [ - 468, - 1016, - 788, - 1164, - 1050, - 514, - 514, - 662, - 320, - 274, - 388, - 650, - 536, - 342, - 0, - 764, - 194, - ], - [ - 776, - 868, - 1552, - 560, - 674, - 1050, - 1278, - 742, - 1084, - 810, - 1152, - 274, - 388, - 422, - 764, - 0, - 798, - ], - [ - 662, - 1210, - 754, - 1358, - 1244, - 708, - 480, - 856, - 514, - 468, - 354, - 844, - 730, - 536, - 194, - 798, - 0, - ], - ] - assert len(data["providers_x"]) == len(data["distance_matrix"]) - assert len(data["providers_y"]) == len(data["distance_matrix"]) - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, assignment): - """Prints assignment on console.""" - print(f"Objective: {assignment.ObjectiveValue()}") - # Display dropped nodes. - dropped_nodes = "Dropped nodes:" - for node in range(routing.Size()): - if routing.IsStart(node) or routing.IsEnd(node): - continue - if assignment.Value(routing.NextVar(node)) == node: - dropped_nodes += f" {manager.IndexToNode(node)}" - print(dropped_nodes) - # Display routes - total_distance = 0 - total_load_x = 0 - total_load_y = 0 - for vehicle_id in range(manager.GetNumberOfVehicles()): - if not routing.IsVehicleUsed(assignment, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - route_load_x = 0 - route_load_y = 0 - while not routing.IsEnd(index): - node_index = manager.IndexToNode(index) - route_load_x += data["providers_x"][node_index] - route_load_y += data["providers_y"][node_index] - plan_output += f" {node_index} Load(X:{route_load_x}, Y:{route_load_y}) -> " - previous_index = index - previous_node_index = node_index - index = assignment.Value(routing.NextVar(index)) - node_index = manager.IndexToNode(index) - # route_distance += routing.GetArcCostForVehicle(previous_index, index, vehicle_id) - route_distance += data["distance_matrix"][previous_node_index][node_index] - node_index = manager.IndexToNode(index) - plan_output += f" {node_index} Load({route_load_x}, {route_load_y})\n" - plan_output += f"Distance of the route: {route_distance}m\n" - plan_output += f"Load of the route: X:{route_load_x}, Y:{route_load_y}\n" - print(plan_output) - total_distance += route_distance - total_load_x += route_load_x - total_load_y += route_load_y - print(f"Total Distance of all routes: {total_distance}m") - print(f"Total load of all routes: X:{total_load_x}, Y:{total_load_y}") - # [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), - data["num_vehicles"], - data["starts"], - data["ends"], - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing.AddDimension( - transit_callback_index, - 0, # no slack - 2000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - # Minimize the longest road - distance_dimension.SetGlobalSpanCostCoefficient(100) - - # [END distance_constraint] - - # Add Capacity constraint. - # [START capacity_constraint] - def demand_callback_x(from_index): - """Returns the demand of the node.""" - # Convert from routing variable Index to demands NodeIndex. - from_node = manager.IndexToNode(from_index) - return data["providers_x"][from_node] - - demand_callback_x_index = routing.RegisterUnaryTransitCallback(demand_callback_x) - routing.AddDimensionWithVehicleCapacity( - demand_callback_x_index, - 0, # null capacity slack - data["vehicle_capacities_x"], # vehicle maximum capacities - True, # start cumul to zero - "Load_x", - ) - - def demand_callback_y(from_index): - """Returns the demand of the node.""" - # Convert from routing variable Index to demands NodeIndex. - from_node = manager.IndexToNode(from_index) - return data["providers_y"][from_node] - - demand_callback_y_index = routing.RegisterUnaryTransitCallback(demand_callback_y) - routing.AddDimensionWithVehicleCapacity( - demand_callback_y_index, - 0, # null capacity slack - data["vehicle_capacities_y"], # vehicle maximum capacities - True, # start cumul to zero - "Load_y", - ) - # [END capacity_constraint] - - # Add constraint at end - solver = routing.solver() - load_x_dim = routing.GetDimensionOrDie("Load_x") - load_y_dim = routing.GetDimensionOrDie("Load_y") - ends = [] - for v in range(manager.GetNumberOfVehicles()): - ends.append(routing.End(v)) - - node_end = data["ends"][0] - solver.Add( - solver.Sum([load_x_dim.CumulVar(l) for l in ends]) - >= -data["providers_x"][node_end] - ) - solver.Add( - solver.Sum([load_y_dim.CumulVar(l) for l in ends]) - >= -data["providers_y"][node_end] - ) - # solver.Add(load_y_dim.CumulVar(end) >= -data['providers_y'][node_end]) - - # Allow to freely drop any nodes. - penalty = 0 - for node in range(0, len(data["distance_matrix"])): - if node not in data["starts"] and node not in data["ends"]: - routing.AddDisjunction([manager.NodeToIndex(node)], penalty) - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - # Sets a time limit; default is 100 milliseconds. - # search_parameters.log_search = True - search_parameters.time_limit.FromSeconds(1) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(data, manager, routing, solution) - else: - print("no solution found !") - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_node_max.py b/ortools/constraint_solver/samples/vrp_node_max.py deleted file mode 100755 index 8bead8183e9..00000000000 --- a/ortools/constraint_solver/samples/vrp_node_max.py +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Vehicles Routing Problem (VRP). - -Each route as an associated objective cost equal to the max node value along the -road multiply by a constant factor (4200) -""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - data["value"] = [ - 0, # depot - 42, # 1 - 42, # 2 - 8, # 3 - 8, # 4 - 8, # 5 - 8, # 6 - 8, # 7 - 8, # 8 - 8, # 9 - 8, # 10 - 8, # 11 - 8, # 12 - 8, # 13 - 8, # 14 - 42, # 15 - 42, # 16 - ] - assert len(data["distance_matrix"]) == len(data["value"]) - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - -# [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - max_route_distance = 0 - dim_one = routing.GetDimensionOrDie("One") - dim_two = routing.GetDimensionOrDie("Two") - - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - while not routing.IsEnd(index): - one_var = dim_one.CumulVar(index) - one_slack_var = dim_one.SlackVar(index) - two_var = dim_two.CumulVar(index) - two_slack_var = dim_two.SlackVar(index) - plan_output += ( - f" N:{manager.IndexToNode(index)}" - f" one:({solution.Value(one_var)}, {solution.Value(one_slack_var)})" - f" two:({solution.Value(two_var)}, {solution.Value(two_slack_var)})" - " -> " - ) - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - one_var = dim_one.CumulVar(index) - two_var = dim_two.CumulVar(index) - plan_output += ( - f"N:{manager.IndexToNode(index)}" - f" one:{solution.Value(one_var)}" - f" two:{solution.Value(two_var)}\n" - ) - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - max_route_distance = max(route_distance, max_route_distance) - print(f"Maximum of the route distances: {max_route_distance}m") - -# [END solution_printer] - - -def main(): - """Solve the CVRP problem.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing.AddDimension( - transit_callback_index, - 0, # no slack - 3_000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(10) - # [END distance_constraint] - - # Max Node value Constraint. - # Dimension One will be used to compute the max node value up to the node in - # the route and store the result in the SlackVar of the node. - routing.AddConstantDimensionWithSlack( - 0, # transit 0 - 42 * 16, # capacity: be able to store PEAK*ROUTE_LENGTH in worst case - 42, # slack_max: to be able to store peak in slack - True, # Fix StartCumulToZero not really matter here - "One", - ) - dim_one = routing.GetDimensionOrDie("One") - - # Dimension Two will be used to store the max node value in the route end node - # CumulVar so we can use it as an objective cost. - routing.AddConstantDimensionWithSlack( - 0, # transit 0 - 42 * 16, # capacity: be able to have PEAK value in CumulVar(End) - 42, # slack_max: to be able to store peak in slack - True, # Fix StartCumulToZero YES here - "Two", - ) - dim_two = routing.GetDimensionOrDie("Two") - - # force depot Slack to be value since we don't have any predecessor... - for v in range(manager.GetNumberOfVehicles()): - start = routing.Start(v) - dim_one.SlackVar(start).SetValue(data["value"][0]) - routing.AddToAssignment(dim_one.SlackVar(start)) - - dim_two.SlackVar(start).SetValue(data["value"][0]) - routing.AddToAssignment(dim_two.SlackVar(start)) - - # Step by step relation - # Slack(N) = max( Slack(N-1) , value(N) ) - solver = routing.solver() - for node in range(1, 17): - index = manager.NodeToIndex(node) - routing.AddToAssignment(dim_one.SlackVar(index)) - routing.AddToAssignment(dim_two.SlackVar(index)) - test = [] - for v in range(manager.GetNumberOfVehicles()): - previous_index = routing.Start(v) - cond = routing.NextVar(previous_index) == index - value = solver.Max(dim_one.SlackVar(previous_index), data["value"][node]) - test.append((cond * value).Var()) - for previous in range(1, 17): - previous_index = manager.NodeToIndex(previous) - cond = routing.NextVar(previous_index) == index - value = solver.Max(dim_one.SlackVar(previous_index), data["value"][node]) - test.append((cond * value).Var()) - solver.Add(solver.Sum(test) == dim_one.SlackVar(index)) - - # relation between dimensions, copy last node Slack from dim ONE to dim TWO - for node in range(1, 17): - index = manager.NodeToIndex(node) - values = [] - for v in range(manager.GetNumberOfVehicles()): - next_index = routing.End(v) - cond = routing.NextVar(index) == next_index - value = dim_one.SlackVar(index) - values.append((cond * value).Var()) - solver.Add(solver.Sum(values) == dim_two.SlackVar(index)) - - # Should force all others dim_two slack var to zero... - for v in range(manager.GetNumberOfVehicles()): - end = routing.End(v) - dim_two.SetCumulVarSoftUpperBound(end, 0, 4200) - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - # search_parameters.log_search = True - search_parameters.time_limit.FromSeconds(5) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(data, manager, routing, solution) - else: - print("No solution found !") - # [END print_solution] - - -if __name__ == "__main__": - main() - # [END program] diff --git a/ortools/constraint_solver/samples/vrp_nodes_indices.py b/ortools/constraint_solver/samples/vrp_nodes_indices.py deleted file mode 100755 index 94ccf776126..00000000000 --- a/ortools/constraint_solver/samples/vrp_nodes_indices.py +++ /dev/null @@ -1,120 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# [START program] -"""Sample to better understand Node/Index relation. - -This script generate few markdown tables to better understand -the relation between nodes and indices. - -Things to notice: -* Since we have two duplicates (node 5 and node 4) solver need 2 extra indices -to have an unique index for each vehicle start/stop and locations. -* Solver needs to "create" an index for a vehicle 1 start since solver need an -unique start index per vehicle. -* All end nodes are moved to the end of the index list aka [15, 16, 17, 18]. -* routing.Size() return the number of node which are not end nodes (here 15 aka -[0-14]) -note: using the two properties above, we know that any index in -range(routing.Size()) is not a vehicle end node. - -* Since end nodes are moved to the end, their respective "empty" node index are -reused so all locations indices are "shifted" -e.g. node 9 is mapped to index 6 -* Same for start nodes which are moved to "empty" space -e.g. start node 7 mapped to index 4 - -Takeaway: -* Allways use routing.Start(), routing.End(), manager.IndexToNode() or -manager.NodeToIndex(). -* Location node is not necessarily equal to its index. -* To loop through ALL indices use manager.GetNumberOfIndices() (Python) or -manager::num_indices() (C++) -""" - -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - - -def main(): - """Entry point of the program.""" - locations = 17 - starts = [5, 5, 7, 8] - ends = [1, 2, 4, 4] - vehicles = len(starts) - assert len(starts) == len(ends) - - manager = pywrapcp.RoutingIndexManager(locations, vehicles, starts, ends) - routing = pywrapcp.RoutingModel(manager) - - print("Starts/Ends:") - header = "| |" - separator = "|---|" - v_starts = "| start |" - v_ends = "| end |" - for v in range(manager.GetNumberOfVehicles()): - header += f" vehicle {v} |" - separator += "---|" - v_starts += f" {starts[v]} |" - v_ends += f" {ends[v]} |" - print(header) - print(separator) - print(v_starts) - print(v_ends) - - print("\nNodes:") - print( - "| locations | manager.GetNumberOfNodes | manager.GetNumberOfIndices |" - " routing.nodes | routing.Size |" - ) - print("|---|---|---|---|---|") - print( - f"| {locations} | {manager.GetNumberOfNodes()} |" - f" {manager.GetNumberOfIndices()} | {routing.nodes()} |" - f" {routing.Size()} |" - ) - - print("\nLocations:") - print("| node | index | routing.IsStart | routing.IsEnd |") - print("|---|---|---|---|") - for node in range(manager.GetNumberOfNodes()): - if node in starts or node in ends: - continue - index = manager.NodeToIndex(node) - print( - f"| {node} | {index} | {routing.IsStart(index)} |" - f" {routing.IsEnd(index)} |" - ) - - print("\nStart/End:") - print("| vehicle | Start/end | node | index | routing.IsStart | routing.IsEnd |") - print("|---|---|---|---|---|---|") - for v in range(manager.GetNumberOfVehicles()): - start_index = routing.Start(v) - start_node = manager.IndexToNode(start_index) - print( - f"| {v} | start | {start_node} | {start_index} |" - f" {routing.IsStart(start_index)} | {routing.IsEnd(start_index)} |" - ) - for v in range(manager.GetNumberOfVehicles()): - end_index = routing.End(v) - end_node = manager.IndexToNode(end_index) - print( - f"| {v} | end | {end_node} | {end_index} |" - f" {routing.IsStart(end_index)} | {routing.IsEnd(end_index)} |" - ) - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_pickup_delivery.py b/ortools/constraint_solver/samples/vrp_pickup_delivery.py deleted file mode 100755 index e5b7912d36f..00000000000 --- a/ortools/constraint_solver/samples/vrp_pickup_delivery.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Pickup Delivery Problem (PDP).""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - # [START pickups_deliveries] - data["pickups_deliveries"] = [ - [1, 6], - [2, 10], - [4, 3], - [5, 9], - [7, 8], - [15, 11], - [13, 12], - [16, 14], - ] - # [END pickups_deliveries] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - total_distance = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} -> " - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f"{manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - total_distance += route_distance - print(f"Total Distance of all routes: {total_distance}m") - # [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - - # [END routing_model] - - # Define cost of each arc. - # [START arc_cost] - def distance_callback(from_index, to_index): - """Returns the manhattan distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing.AddDimension( - transit_callback_index, - 0, # no slack - 3000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - # [END distance_constraint] - - # Define Transportation Requests. - # [START pickup_delivery_constraint] - for request in data["pickups_deliveries"]: - pickup_index = manager.NodeToIndex(request[0]) - delivery_index = manager.NodeToIndex(request[1]) - routing.AddPickupAndDelivery(pickup_index, delivery_index) - routing.solver().Add( - routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index) - ) - routing.solver().Add( - distance_dimension.CumulVar(pickup_index) - <= distance_dimension.CumulVar(delivery_index) - ) - # [END pickup_delivery_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(data, manager, routing, solution) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_pickup_delivery_fifo.py b/ortools/constraint_solver/samples/vrp_pickup_delivery_fifo.py deleted file mode 100755 index 83641a21e95..00000000000 --- a/ortools/constraint_solver/samples/vrp_pickup_delivery_fifo.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Pickup Delivery Problem (PDP).""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - # [START pickups_deliveries] - data["pickups_deliveries"] = [ - [1, 6], - [2, 10], - [4, 3], - [5, 9], - [7, 8], - [15, 11], - [13, 12], - [16, 14], - ] - # [END pickups_deliveries] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, assignment): - """Prints assignment on console.""" - print(f"Objective: {assignment.ObjectiveValue()}") - total_distance = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(assignment, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} -> " - previous_index = index - index = assignment.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f"{manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - total_distance += route_distance - print(f"Total Distance of all routes: {total_distance}m") - # [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - - # [END routing_model] - - # Define cost of each arc. - # [START arc_cost] - def distance_callback(from_index, to_index): - """Returns the manhattan distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing.AddDimension( - transit_callback_index, - 0, # no slack - 3000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - # [END distance_constraint] - - # Define Transportation Requests. - # [START pickup_delivery_constraint] - for request in data["pickups_deliveries"]: - pickup_index = manager.NodeToIndex(request[0]) - delivery_index = manager.NodeToIndex(request[1]) - routing.AddPickupAndDelivery(pickup_index, delivery_index) - routing.solver().Add( - routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index) - ) - routing.solver().Add( - distance_dimension.CumulVar(pickup_index) - <= distance_dimension.CumulVar(delivery_index) - ) - routing.SetPickupAndDeliveryPolicyOfAllVehicles( - pywrapcp.RoutingModel.PICKUP_AND_DELIVERY_FIFO - ) - # [END pickup_delivery_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION - ) - # [END parameters] - - # Solve the problem. - # [START solve] - assignment = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if assignment: - print_solution(data, manager, routing, assignment) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_pickup_delivery_lifo.py b/ortools/constraint_solver/samples/vrp_pickup_delivery_lifo.py deleted file mode 100755 index 3f9c144cf4a..00000000000 --- a/ortools/constraint_solver/samples/vrp_pickup_delivery_lifo.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Pickup Delivery Problem (PDP).""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - # [START pickups_deliveries] - data["pickups_deliveries"] = [ - [1, 6], - [2, 10], - [4, 3], - [5, 9], - [7, 8], - [15, 11], - [13, 12], - [16, 14], - ] - # [END pickups_deliveries] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, assignment): - """Prints assignment on console.""" - print(f"Objective: {assignment.ObjectiveValue()}") - total_distance = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(assignment, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} -> " - previous_index = index - index = assignment.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f"{manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - total_distance += route_distance - print(f"Total Distance of all routes: {total_distance}m") - # [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - - # [END routing_model] - - # Define cost of each arc. - # [START arc_cost] - def distance_callback(from_index, to_index): - """Returns the manhattan distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing.AddDimension( - transit_callback_index, - 0, # no slack - 3000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - # [END distance_constraint] - - # Define Transportation Requests. - # [START pickup_delivery_constraint] - for request in data["pickups_deliveries"]: - pickup_index = manager.NodeToIndex(request[0]) - delivery_index = manager.NodeToIndex(request[1]) - routing.AddPickupAndDelivery(pickup_index, delivery_index) - routing.solver().Add( - routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index) - ) - routing.solver().Add( - distance_dimension.CumulVar(pickup_index) - <= distance_dimension.CumulVar(delivery_index) - ) - routing.SetPickupAndDeliveryPolicyOfAllVehicles( - pywrapcp.RoutingModel.PICKUP_AND_DELIVERY_LIFO - ) - # [END pickup_delivery_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION - ) - # [END parameters] - - # Solve the problem. - # [START solve] - assignment = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if assignment: - print_solution(data, manager, routing, assignment) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_resources.py b/ortools/constraint_solver/samples/vrp_resources.py deleted file mode 100755 index ba46a5ffccb..00000000000 --- a/ortools/constraint_solver/samples/vrp_resources.py +++ /dev/null @@ -1,233 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Vehicles Routing Problem (VRP) with Resource Constraints.""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["time_matrix"] = [ - [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], - [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], - [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], - [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], - [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], - [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], - [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], - [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], - [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], - [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], - [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], - [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], - [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], - [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], - [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], - [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], - [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], - ] - data["time_windows"] = [ - (0, 5), # depot - (7, 12), # 1 - (10, 15), # 2 - (5, 14), # 3 - (5, 13), # 4 - (0, 5), # 5 - (5, 10), # 6 - (0, 10), # 7 - (5, 10), # 8 - (0, 5), # 9 - (10, 16), # 10 - (10, 15), # 11 - (0, 5), # 12 - (5, 10), # 13 - (7, 12), # 14 - (10, 15), # 15 - (5, 15), # 16 - ] - data["num_vehicles"] = 4 - # [START resources_data] - data["vehicle_load_time"] = 5 - data["vehicle_unload_time"] = 5 - data["depot_capacity"] = 2 - # [END resources_data] - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - time_dimension = routing.GetDimensionOrDie("Time") - total_time = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - while not routing.IsEnd(index): - time_var = time_dimension.CumulVar(index) - plan_output += ( - f"{manager.IndexToNode(index)}" - f" Time({solution.Min(time_var)}, {solution.Max(time_var)})" - " -> " - ) - index = solution.Value(routing.NextVar(index)) - time_var = time_dimension.CumulVar(index) - plan_output += ( - f"{manager.IndexToNode(index)}" - f" Time({solution.Min(time_var)},{solution.Max(time_var)})\n" - ) - plan_output += f"Time of the route: {solution.Min(time_var)}min\n" - print(plan_output) - total_time += solution.Min(time_var) - print(f"Total time of all routes: {total_time}min") - # [END solution_printer] - - -def main(): - """Solve the VRP with time windows.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["time_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def time_callback(from_index, to_index): - """Returns the travel time between the two nodes.""" - # Convert from routing variable Index to time matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["time_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(time_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Time Windows constraint. - # [START time_windows_constraint] - time = "Time" - routing.AddDimension( - transit_callback_index, - 60, # allow waiting time - 60, # maximum time per vehicle - False, # Don't force start cumul to zero. - time, - ) - time_dimension = routing.GetDimensionOrDie(time) - # Add time window constraints for each location except depot. - for location_idx, time_window in enumerate(data["time_windows"]): - if location_idx == 0: - continue - index = manager.NodeToIndex(location_idx) - time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) - # Add time window constraints for each vehicle start node. - for vehicle_id in range(data["num_vehicles"]): - index = routing.Start(vehicle_id) - time_dimension.CumulVar(index).SetRange( - data["time_windows"][0][0], data["time_windows"][0][1] - ) - # [END time_windows_constraint] - - # Add resource constraints at the depot. - # [START depot_load_time] - solver = routing.solver() - intervals = [] - for i in range(data["num_vehicles"]): - # Add time windows at start of routes - intervals.append( - solver.FixedDurationIntervalVar( - time_dimension.CumulVar(routing.Start(i)), - data["vehicle_load_time"], - "depot_interval", - ) - ) - # Add time windows at end of routes. - intervals.append( - solver.FixedDurationIntervalVar( - time_dimension.CumulVar(routing.End(i)), - data["vehicle_unload_time"], - "depot_interval", - ) - ) - # [END depot_load_time] - - # [START depot_capacity] - depot_usage = [1 for _ in range(len(intervals))] - solver.Add( - solver.Cumulative(intervals, depot_usage, data["depot_capacity"], "depot") - ) - # [END depot_capacity] - - # Instantiate route start and end times to produce feasible times. - # [START depot_start_end_times] - for i in range(data["num_vehicles"]): - routing.AddVariableMinimizedByFinalizer( - time_dimension.CumulVar(routing.Start(i)) - ) - routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(i))) - # [END depot_start_end_times] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(data, manager, routing, solution) - # [END print_solution] - else: - print("No solution found !") - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_solution_callback.py b/ortools/constraint_solver/samples/vrp_solution_callback.py deleted file mode 100755 index a95dcb3ac3b..00000000000 --- a/ortools/constraint_solver/samples/vrp_solution_callback.py +++ /dev/null @@ -1,213 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Vehicles Routing Problem (VRP). - -This is a sample using the routing library python wrapper to solve a VRP -problem. - -The solver stop after improving its solution 15 times or after 5 seconds. - -Distances are in meters. -""" - -# [START import] -import weakref - -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_callback_printer] -def print_solution( - routing_manager: pywrapcp.RoutingIndexManager, routing_model: pywrapcp.RoutingModel -): - """Prints solution on console.""" - print("################") - print(f"Solution objective: {routing_model.CostVar().Value()}") - total_distance = 0 - for vehicle_id in range(routing_manager.GetNumberOfVehicles()): - index = routing_model.Start(vehicle_id) - if routing_model.IsEnd(routing_model.NextVar(index).Value()): - continue - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - while not routing_model.IsEnd(index): - plan_output += f" {routing_manager.IndexToNode(index)} ->" - previous_index = index - index = routing_model.NextVar(index).Value() - route_distance += routing_model.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f" {routing_manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - total_distance += route_distance - print(f"Total Distance of all routes: {total_distance}m") - -# [END solution_callback_printer] - - -# [START solution_callback] -class SolutionCallback: - """Create a solution callback.""" - - def __init__( - self, - manager: pywrapcp.RoutingIndexManager, - model: pywrapcp.RoutingModel, - limit: int, - ): - # We need a weak ref on the routing model to avoid a cycle. - self._routing_manager_ref = weakref.ref(manager) - self._routing_model_ref = weakref.ref(model) - self._counter = 0 - self._counter_limit = limit - self.objectives = [] - - def __call__(self): - objective = int( - self._routing_model_ref().CostVar().Value() - ) # pytype: disable=attribute-error - if not self.objectives or objective < self.objectives[-1]: - self.objectives.append(objective) - print_solution(self._routing_manager_ref(), self._routing_model_ref()) - self._counter += 1 - if self._counter > self._counter_limit: - self._routing_model_ref().solver().FinishCurrentSearch() - - -# [END solution_callback] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - routing_manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing_model = pywrapcp.RoutingModel(routing_manager) - - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = routing_manager.IndexToNode(from_index) - to_node = routing_manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing_model.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing_model.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing_model.AddDimension( - transit_callback_index, - 0, # no slack - 3000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing_model.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - # [END distance_constraint] - - # Attach a solution callback. - # [START attach_callback] - solution_callback = SolutionCallback(routing_manager, routing_model, 15) - routing_model.AddAtSolutionCallback(solution_callback) - # [END attach_callback] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.time_limit.FromSeconds(5) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing_model.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print(f"Best objective: {solution_callback.objectives[-1]}") - else: - print("No solution found !") - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_starts_ends.py b/ortools/constraint_solver/samples/vrp_starts_ends.py deleted file mode 100755 index 5342a78a268..00000000000 --- a/ortools/constraint_solver/samples/vrp_starts_ends.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Simple Vehicles Routing Problem.""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["distance_matrix"] = [ - # fmt: off - [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], - [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], - [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], - [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], - [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], - [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], - [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], - [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], - [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], - [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], - [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], - [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], - [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], - [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], - [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], - [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], - [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], - # fmt: on - ] - data["num_vehicles"] = 4 - # [START starts_ends] - data["starts"] = [1, 2, 15, 16] - data["ends"] = [0, 0, 0, 0] - # [END starts_ends] - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - max_route_distance = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} -> " - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f"{manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - max_route_distance = max(route_distance, max_route_distance) - print(f"Maximum of the route distances: {max_route_distance}m") - # [END solution_printer] - - -def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["distance_matrix"]), data["num_vehicles"], data["starts"], data["ends"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["distance_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing.AddDimension( - transit_callback_index, - 0, # no slack - 2000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - # [END distance_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(data, manager, routing, solution) - # [END print_solution] - - -if __name__ == "__main__": - main() - # [END program] diff --git a/ortools/constraint_solver/samples/vrp_time_windows.py b/ortools/constraint_solver/samples/vrp_time_windows.py deleted file mode 100755 index 80fac449b24..00000000000 --- a/ortools/constraint_solver/samples/vrp_time_windows.py +++ /dev/null @@ -1,197 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Vehicles Routing Problem (VRP) with Time Windows.""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["time_matrix"] = [ - [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], - [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], - [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], - [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], - [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], - [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], - [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], - [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], - [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], - [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], - [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], - [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], - [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], - [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], - [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], - [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], - [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], - ] - data["time_windows"] = [ - (0, 5), # depot - (7, 12), # 1 - (10, 15), # 2 - (16, 18), # 3 - (10, 13), # 4 - (0, 5), # 5 - (5, 10), # 6 - (0, 4), # 7 - (5, 10), # 8 - (0, 3), # 9 - (10, 16), # 10 - (10, 15), # 11 - (0, 5), # 12 - (5, 10), # 13 - (7, 8), # 14 - (10, 15), # 15 - (11, 15), # 16 - ] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(data, manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - time_dimension = routing.GetDimensionOrDie("Time") - total_time = 0 - for vehicle_id in range(data["num_vehicles"]): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - while not routing.IsEnd(index): - time_var = time_dimension.CumulVar(index) - plan_output += ( - f"{manager.IndexToNode(index)}" - f" Time({solution.Min(time_var)},{solution.Max(time_var)})" - " -> " - ) - index = solution.Value(routing.NextVar(index)) - time_var = time_dimension.CumulVar(index) - plan_output += ( - f"{manager.IndexToNode(index)}" - f" Time({solution.Min(time_var)},{solution.Max(time_var)})\n" - ) - plan_output += f"Time of the route: {solution.Min(time_var)}min\n" - print(plan_output) - total_time += solution.Min(time_var) - print(f"Total time of all routes: {total_time}min") - # [END solution_printer] - - -def main(): - """Solve the VRP with time windows.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["time_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def time_callback(from_index, to_index): - """Returns the travel time between the two nodes.""" - # Convert from routing variable Index to time matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["time_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(time_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Time Windows constraint. - # [START time_windows_constraint] - time = "Time" - routing.AddDimension( - transit_callback_index, - 30, # allow waiting time - 30, # maximum time per vehicle - False, # Don't force start cumul to zero. - time, - ) - time_dimension = routing.GetDimensionOrDie(time) - # Add time window constraints for each location except depot. - for location_idx, time_window in enumerate(data["time_windows"]): - if location_idx == data["depot"]: - continue - index = manager.NodeToIndex(location_idx) - time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) - # Add time window constraints for each vehicle start node. - depot_idx = data["depot"] - for vehicle_id in range(data["num_vehicles"]): - index = routing.Start(vehicle_id) - time_dimension.CumulVar(index).SetRange( - data["time_windows"][depot_idx][0], data["time_windows"][depot_idx][1] - ) - # [END time_windows_constraint] - - # Instantiate route start and end times to produce feasible times. - # [START depot_start_end_times] - for i in range(data["num_vehicles"]): - routing.AddVariableMinimizedByFinalizer( - time_dimension.CumulVar(routing.Start(i)) - ) - routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(i))) - # [END depot_start_end_times] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(data, manager, routing, solution) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_time_windows_per_vehicles.py b/ortools/constraint_solver/samples/vrp_time_windows_per_vehicles.py deleted file mode 100755 index c814af54ccd..00000000000 --- a/ortools/constraint_solver/samples/vrp_time_windows_per_vehicles.py +++ /dev/null @@ -1,258 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# [START program] -"""Vehicles Routing Problem (VRP) with Time Window (TW) per vehicle. - -All time are in minutes using 0am as origin -e.g. 8am = 480, 11am = 660, 1pm = 780 ... - -We have 1 depot (0) and 16 locations (1-16). -We have a fleet of 4 vehicles (0-3) whose working time is [480, 1020] (8am-5pm) -We have the distance matrix between these locations and depot. -We have a service time of 25min at each location. - -Locations are duplicated so we can simulate a TW per vehicle. -location: [01-16] vehicle: 0 TW: [540, 660] (9am-11am) -location: [17-32] vehicle: 1 TW: [660, 780] (11am-1pm) -location: [33-48] vehicle: 2 TW: [780, 900] (1pm-3pm) -location: [49-64] vehicle: 3 TW: [900, 1020] (3pm-5pm) -""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["time_matrix"] = [ - [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], - [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], - [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], - [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], - [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], - [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], - [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], - [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], - [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], - [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], - [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], - [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], - [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], - [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], - [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], - [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], - [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], - ] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - # [END data_model] - - -# [START solution_printer] -def print_solution(manager, routing, assignment): - """Prints solution on console.""" - print(f"Objective: {assignment.ObjectiveValue()}") - # Display dropped nodes. - dropped_nodes = "Dropped nodes:" - for index in range(routing.Size()): - if routing.IsStart(index) or routing.IsEnd(index): - continue - if assignment.Value(routing.NextVar(index)) == index: - node = manager.IndexToNode(index) - if node > 16: - original = node - while original > 16: - original = original - 16 - dropped_nodes += f" {node}({original})" - else: - dropped_nodes += f" {node}" - print(dropped_nodes) - # Display routes - time_dimension = routing.GetDimensionOrDie("Time") - total_time = 0 - for vehicle_id in range(manager.GetNumberOfVehicles()): - if not routing.IsVehicleUsed(assignment, vehicle_id): - continue - plan_output = f"Route for vehicle {vehicle_id}:\n" - index = routing.Start(vehicle_id) - start_time = 0 - while not routing.IsEnd(index): - time_var = time_dimension.CumulVar(index) - node = manager.IndexToNode(index) - if node > 16: - original = node - while original > 16: - original = original - 16 - plan_output += f"{node}({original})" - else: - plan_output += f"{node}" - plan_output += f" Time:{assignment.Value(time_var)} -> " - if start_time == 0: - start_time = assignment.Value(time_var) - index = assignment.Value(routing.NextVar(index)) - time_var = time_dimension.CumulVar(index) - node = manager.IndexToNode(index) - plan_output += f"{node} Time:{assignment.Value(time_var)}\n" - end_time = assignment.Value(time_var) - duration = end_time - start_time - plan_output += f"Duration of the route:{duration}min\n" - print(plan_output) - total_time += duration - print(f"Total duration of all routes: {total_time}min") - # [END solution_printer] - - -def main(): - """Solve the VRP with time windows.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - 1 + 16 * 4, data["num_vehicles"], data["depot"] # number of locations - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def time_callback(from_index, to_index): - """Returns the travel time between the two nodes.""" - # Convert from routing variable Index to time matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - # since our matrix is 17x17 map duplicated node to original one to - # retrieve the travel time - while from_node > 16: - from_node = from_node - 16 - while to_node > 16: - to_node = to_node - 16 - # add service of 25min for each location (except depot) - service_time = 0 - if from_node != data["depot"]: - service_time = 25 - return data["time_matrix"][from_node][to_node] + service_time - - transit_callback_index = routing.RegisterTransitCallback(time_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Time Windows constraint. - # [START time_windows_constraint] - time = "Time" - routing.AddDimension( - transit_callback_index, - 0, # allow waiting time (0 min) - 1020, # maximum time per vehicle (9 hours) - False, # Don't force start cumul to zero. - time, - ) - time_dimension = routing.GetDimensionOrDie(time) - # Add time window constraints for each location except depot. - for location_idx in range(17): - if location_idx == data["depot"]: - continue - # Vehicle 0 location TW: [9am, 11am] - index_0 = manager.NodeToIndex(location_idx) - time_dimension.CumulVar(index_0).SetRange(540, 660) - routing.VehicleVar(index_0).SetValues([-1, 0]) - - # Vehicle 1 location TW: [11am, 1pm] - index_1 = manager.NodeToIndex(location_idx + 16 * 1) - time_dimension.CumulVar(index_1).SetRange(660, 780) - routing.VehicleVar(index_1).SetValues([-1, 1]) - - # Vehicle 2 location TW: [1pm, 3pm] - index_2 = manager.NodeToIndex(location_idx + 16 * 2) - time_dimension.CumulVar(index_2).SetRange(780, 900) - routing.VehicleVar(index_2).SetValues([-1, 2]) - - # Vehicle 3 location TW: [3pm, 5pm] - index_3 = manager.NodeToIndex(location_idx + 16 * 3) - time_dimension.CumulVar(index_3).SetRange(900, 1020) - routing.VehicleVar(index_3).SetValues([-1, 3]) - - # Add Disjunction so only one node among duplicate is visited - penalty = 100_000 # Give solver strong incentive to visit one node - routing.AddDisjunction([index_0, index_1, index_2, index_3], penalty, 1) - - # Add time window constraints for each vehicle start node. - depot_idx = data["depot"] - for vehicle_id in range(data["num_vehicles"]): - index = routing.Start(vehicle_id) - time_dimension.CumulVar(index).SetRange(480, 1020) # (8am, 5pm) - - # Add time window constraints for each vehicle end node. - depot_idx = data["depot"] - for vehicle_id in range(data["num_vehicles"]): - index = routing.End(vehicle_id) - time_dimension.CumulVar(index).SetRange(480, 1020) # (8am, 5pm) - # [END time_windows_constraint] - - # Instantiate route start and end times to produce feasible times. - # [START depot_start_end_times] - for i in range(data["num_vehicles"]): - routing.AddVariableMinimizedByFinalizer( - time_dimension.CumulVar(routing.Start(i)) - ) - routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(i))) - # [END depot_start_end_times] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.time_limit.FromSeconds(1) - # [END parameters] - - # Solve the problem. - # [START solve] - assignment = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if assignment: - print_solution(manager, routing, assignment) - else: - print("no solution found !") - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrp_tokens.py b/ortools/constraint_solver/samples/vrp_tokens.py deleted file mode 100755 index e5ede08333f..00000000000 --- a/ortools/constraint_solver/samples/vrp_tokens.py +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Simple VRP with special locations which need to be visited at end of the route.""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -def create_data_model(): - """Stores the data for the problem.""" - data = {} - # Special location don't consume token, while regular one consume one - data["tokens"] = [ - 0, # 0 depot - 0, # 1 special node - 0, # 2 special node - 0, # 3 special node - 0, # 4 special node - 0, # 5 special node - -1, # 6 - -1, # 7 - -1, # 8 - -1, # 9 - -1, # 10 - -1, # 11 - -1, # 12 - -1, # 13 - -1, # 14 - -1, # 15 - -1, # 16 - -1, # 17 - -1, # 18 - ] - # just need to be big enough, not a limiting factor - data["vehicle_tokens"] = [20, 20, 20, 20] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - - -def print_solution(manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - token_dimension = routing.GetDimensionOrDie("Token") - total_distance = 0 - total_token = 0 - for vehicle_id in range(manager.GetNumberOfVehicles()): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - plan_output = f"Route for vehicle {vehicle_id}:\n" - index = routing.Start(vehicle_id) - total_token += solution.Value(token_dimension.CumulVar(index)) - route_distance = 0 - route_token = 0 - while not routing.IsEnd(index): - node_index = manager.IndexToNode(index) - token_var = token_dimension.CumulVar(index) - route_token = solution.Value(token_var) - plan_output += f" {node_index} Token({route_token}) -> " - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - node_index = manager.IndexToNode(index) - token_var = token_dimension.CumulVar(index) - route_token = solution.Value(token_var) - plan_output += f" {node_index} Token({route_token})\n" - plan_output += f"Distance of the route: {route_distance}m\n" - total_distance += route_distance - print(plan_output) - print(f"Total distance of all routes: {total_distance}m") - print(f"Total token of all routes: {total_token}") - - -def main(): - """Solve the CVRP problem.""" - # Instantiate the data problem. - data = create_data_model() - - # Create the routing index manager. - manager = pywrapcp.RoutingIndexManager( - len(data["tokens"]), data["num_vehicles"], data["depot"] - ) - - # Create Routing Model. - routing = pywrapcp.RoutingModel(manager) - - # Create and register a transit callback. - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - del from_index - del to_index - return 10 - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - - routing.AddDimension( - transit_callback_index, - 0, # null slack - 3000, # maximum distance per vehicle - True, # start cumul to zero - "distance", - ) - distance_dimension = routing.GetDimensionOrDie("distance") - distance_dimension.SetGlobalSpanCostCoefficient(100) - - # Define cost of each arc. - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - - # Add Token constraint. - def token_callback(from_index): - """Returns the number of token consumed by the node.""" - # Convert from routing variable Index to tokens NodeIndex. - from_node = manager.IndexToNode(from_index) - return data["tokens"][from_node] - - token_callback_index = routing.RegisterUnaryTransitCallback(token_callback) - routing.AddDimensionWithVehicleCapacity( - token_callback_index, - 0, # null capacity slack - data["vehicle_tokens"], # vehicle maximum tokens - False, # start cumul to zero - "Token", - ) - # Add constraint: special node can only be visited if token remaining is zero - token_dimension = routing.GetDimensionOrDie("Token") - for node in range(1, 6): - index = manager.NodeToIndex(node) - routing.solver().Add(token_dimension.CumulVar(index) == 0) - - # Instantiate route start and end times to produce feasible times. - # [START depot_start_end_times] - for i in range(manager.GetNumberOfVehicles()): - routing.AddVariableMinimizedByFinalizer( - token_dimension.CumulVar(routing.Start(i)) - ) - routing.AddVariableMinimizedByFinalizer( - token_dimension.CumulVar(routing.End(i)) - ) - # [END depot_start_end_times] - - # Setting first solution heuristic. - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.time_limit.FromSeconds(1) - - # Solve the problem. - solution = routing.SolveWithParameters(search_parameters) - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(manager, routing, solution) - else: - print("No solution found !") - # [END print_solution] - - -if __name__ == "__main__": - main() diff --git a/ortools/constraint_solver/samples/vrp_with_time_limit.py b/ortools/constraint_solver/samples/vrp_with_time_limit.py deleted file mode 100755 index fb286ed2756..00000000000 --- a/ortools/constraint_solver/samples/vrp_with_time_limit.py +++ /dev/null @@ -1,127 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""Vehicles Routing Problem (VRP).""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START solution_printer] -def print_solution(manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - max_route_distance = 0 - for vehicle_id in range(manager.GetNumberOfVehicles()): - if not routing.IsVehicleUsed(solution, vehicle_id): - continue - index = routing.Start(vehicle_id) - plan_output = f"Route for vehicle {vehicle_id}:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} -> " - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle( - previous_index, index, vehicle_id - ) - plan_output += f"{manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) - max_route_distance = max(route_distance, max_route_distance) - print(f"Maximum of the route distances: {max_route_distance}m") - # [END solution_printer] - - -def main(): - """Solve the CVRP problem.""" - # Instantiate the data problem. - # [START data] - num_locations = 20 - num_vehicles = 5 - depot = 0 - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager(num_locations, num_vehicles, depot) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def distance_callback(from_index, to_index): - # pylint: disable=unused-argument - """Returns the distance between the two nodes.""" - return 1 - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Distance constraint. - # [START distance_constraint] - dimension_name = "Distance" - routing.AddDimension( - transit_callback_index, - 0, # no slack - 3000, # vehicle maximum travel distance - True, # start cumul to zero - dimension_name, - ) - distance_dimension = routing.GetDimensionOrDie(dimension_name) - distance_dimension.SetGlobalSpanCostCoefficient(100) - # [END distance_constraint] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - search_parameters.local_search_metaheuristic = ( - routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH - ) - search_parameters.log_search = True - search_parameters.time_limit.FromSeconds(5) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(manager, routing, solution) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program] diff --git a/ortools/constraint_solver/samples/vrptw_store_solution_data.py b/ortools/constraint_solver/samples/vrptw_store_solution_data.py deleted file mode 100755 index 6abc76ce837..00000000000 --- a/ortools/constraint_solver/samples/vrptw_store_solution_data.py +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env python3 -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# [START program] -"""VRPTW example that stores routes and cumulative data in an array.""" - -# [START import] -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp - -# [END import] - - -# [START program_part1] -# [START data_model] -def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["time_matrix"] = [ - [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], - [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], - [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], - [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], - [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], - [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], - [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], - [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], - [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], - [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], - [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], - [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], - [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], - [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], - [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], - [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], - [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], - ] - data["time_windows"] = [ - (0, 5), # depot - (7, 12), # 1 - (10, 15), # 2 - (16, 18), # 3 - (10, 13), # 4 - (0, 5), # 5 - (5, 10), # 6 - (0, 4), # 7 - (5, 10), # 8 - (0, 3), # 9 - (10, 16), # 10 - (10, 15), # 11 - (0, 5), # 12 - (5, 10), # 13 - (7, 8), # 14 - (10, 15), # 15 - (11, 15), # 16 - ] - data["num_vehicles"] = 4 - data["depot"] = 0 - return data - -# [END data_model] - - -# [START solution_printer] -def print_solution(routes, cumul_data): - """Print the solution.""" - total_time = 0 - route_str = "" - for i, route in enumerate(routes): - if len(route) <= 2: - continue - route_str += "Route " + str(i) + ":\n" - start_time = cumul_data[i][0][0] - end_time = cumul_data[i][0][1] - route_str += ( - " " - + str(route[0]) - + " Time(" - + str(start_time) - + ", " - + str(end_time) - + ")" - ) - for j in range(1, len(route)): - start_time = cumul_data[i][j][0] - end_time = cumul_data[i][j][1] - route_str += ( - " -> " - + str(route[j]) - + " Time(" - + str(start_time) - + ", " - + str(end_time) - + ")" - ) - route_str += f"\n Route time: {start_time}min\n\n" - total_time += cumul_data[i][len(route) - 1][0] - route_str += f"Total time: {total_time}min" - print(route_str) - -# [END solution_printer] - - -# [START get_routes] -def get_routes(solution, routing, manager): - """Get vehicle routes from a solution and store them in an array.""" - # Get vehicle routes and store them in a two dimensional array whose - # i,j entry is the jth location visited by vehicle i along its route. - routes = [] - for route_nbr in range(routing.vehicles()): - index = routing.Start(route_nbr) - route = [manager.IndexToNode(index)] - while not routing.IsEnd(index): - index = solution.Value(routing.NextVar(index)) - route.append(manager.IndexToNode(index)) - routes.append(route) - return routes - -# [END get_routes] - - -# [START get_cumulative_data] -def get_cumul_data(solution, routing, dimension): - """Get cumulative data from a dimension and store it in an array.""" - # Returns an array cumul_data whose i,j entry contains the minimum and - # maximum of CumulVar for the dimension at the jth node on route : - # - cumul_data[i][j][0] is the minimum. - # - cumul_data[i][j][1] is the maximum. - - cumul_data = [] - for route_nbr in range(routing.vehicles()): - route_data = [] - index = routing.Start(route_nbr) - dim_var = dimension.CumulVar(index) - route_data.append([solution.Min(dim_var), solution.Max(dim_var)]) - while not routing.IsEnd(index): - index = solution.Value(routing.NextVar(index)) - dim_var = dimension.CumulVar(index) - route_data.append([solution.Min(dim_var), solution.Max(dim_var)]) - cumul_data.append(route_data) - return cumul_data - -# [END get_cumulative_data] - - -def main(): - """Solve the VRP with time windows.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["time_matrix"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # Create and register a transit callback. - # [START transit_callback] - def time_callback(from_index, to_index): - """Returns the travel time between the two nodes.""" - # Convert from routing variable Index to time matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return data["time_matrix"][from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(time_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Add Time Windows constraint. - # [START time_windows_constraint] - time = "Time" - - routing.AddDimension( - transit_callback_index, - 30, # allow waiting time - 30, # maximum time per vehicle - False, # Don't force cumulative time to be 0 at start of routes. - time, - ) - time_dimension = routing.GetDimensionOrDie(time) - # Add time window constraints for each location except depot. - for location_idx, time_window in enumerate(data["time_windows"]): - if location_idx == 0: - continue - index = manager.NodeToIndex(location_idx) - time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) - # Add time window constraints for each vehicle start node. - for vehicle_id in range(data["num_vehicles"]): - index = routing.Start(vehicle_id) - time_dimension.CumulVar(index).SetRange( - data["time_windows"][0][0], data["time_windows"][0][1] - ) - # [END time_windows_constraint] - - # Instantiate route start and end times to produce feasible times. - # [START depot_start_end_times] - for i in range(data["num_vehicles"]): - routing.AddVariableMinimizedByFinalizer( - time_dimension.CumulVar(routing.Start(i)) - ) - routing.AddVariableMinimizedByFinalizer(time_dimension.CumulVar(routing.End(i))) - # [END depot_start_end_times] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution. - # [START print_solution] - if solution: - routes = get_routes(solution, routing, manager) - cumul_data = get_cumul_data(solution, routing, time_dimension) - print_solution(routes, cumul_data) - # [END print_solution] - - -if __name__ == "__main__": - main() -# [END program_part1] -# [END program] diff --git a/ortools/constraint_solver/search.cc b/ortools/constraint_solver/search.cc index 1955af19380..b3014ebf4e7 100644 --- a/ortools/constraint_solver/search.cc +++ b/ortools/constraint_solver/search.cc @@ -3013,8 +3013,8 @@ class RoundRobinCompoundObjectiveMonitor : public BaseObjectiveMonitor { bool AcceptSolution() override { return monitors_[active_monitor_]->AcceptSolution(); } - bool LocalOptimum() override { - const bool ok = monitors_[active_monitor_]->LocalOptimum(); + bool AtLocalOptimum() override { + const bool ok = monitors_[active_monitor_]->AtLocalOptimum(); if (!ok) { enabled_monitors_[active_monitor_] = false; } @@ -3388,9 +3388,17 @@ class TabuSearch : public Metaheuristic { void EnterSearch() override; void ApplyDecision(Decision* d) override; bool AtSolution() override; - bool LocalOptimum() override; + bool AcceptSolution() override; + bool AtLocalOptimum() override; bool AcceptDelta(Assignment* delta, Assignment* deltadelta) override; void AcceptNeighbor() override; + void BeginNextDecision(DecisionBuilder* const) override { + if (stop_search_) solver()->Fail(); + } + void RefuteDecision(Decision* const d) override { + Metaheuristic::RefuteDecision(d); + if (stop_search_) solver()->Fail(); + } std::string DebugString() const override { return "Tabu Search"; } protected: @@ -3425,6 +3433,11 @@ class TabuSearch : public Metaheuristic { int64_t forbid_tenure_; double tabu_factor_; int64_t stamp_; + int64_t solution_count_ = 0; + bool stop_search_ = false; + std::vector delta_values_; + SparseBitset<> delta_vars_; + std::vector var_index_to_index_; }; TabuSearch::TabuSearch(Solver* solver, const std::vector& maximize, @@ -3438,10 +3451,17 @@ TabuSearch::TabuSearch(Solver* solver, const std::vector& maximize, keep_tenure_(keep_tenure), forbid_tenure_(forbid_tenure), tabu_factor_(tabu_factor), - stamp_(0) { + stamp_(0), + delta_values_(vars.size(), 0), + delta_vars_(vars.size()) { for (int index = 0; index < vars_.size(); ++index) { assignment_container_.FastAdd(vars_[index]); DCHECK_EQ(vars_[index], assignment_container_.Element(index).Var()); + const int var_index = vars_[index]->index(); + if (var_index >= var_index_to_index_.size()) { + var_index_to_index_.resize(var_index + 1, -1); + } + var_index_to_index_[var_index] = index; } } @@ -3450,6 +3470,8 @@ void TabuSearch::EnterSearch() { solver()->SetUseFastLocalSearch(true); stamp_ = 0; has_stored_assignment_ = false; + solution_count_ = 0; + stop_search_ = false; } void TabuSearch::ApplyDecision(Decision* const d) { @@ -3482,21 +3504,19 @@ void TabuSearch::ApplyDecision(Decision* const d) { MakeMinimizationVarsLessOrEqualWithSteps( [this](int i) { return CurrentInternalValue(i); }); } - // Avoid cost plateau's which lead to tabu cycles. +} + +bool TabuSearch::AcceptSolution() { + // Avoid cost plateaus which lead to tabu cycles. if (found_initial_solution_) { - Constraint* plateau_ct = nullptr; - if (Size() == 1) { - plateau_ct = s->MakeNonEquality(MinimizationVar(0), last_values_[0]); - } else { - std::vector plateau_vars(Size()); - for (int i = 0; i < Size(); ++i) { - plateau_vars[i] = - s->MakeIsEqualCstVar(MinimizationVar(i), last_values_[i]); + for (int i = 0; i < Size(); ++i) { + if (last_values_[i] != MinimizationVar(i)->Min()) { + return true; } - plateau_ct = s->MakeSumLessOrEqual(plateau_vars, Size() - 1); } - s->AddConstraint(plateau_ct); + return false; } + return true; } std::vector TabuSearch::CreateTabuVars() { @@ -3519,6 +3539,7 @@ std::vector TabuSearch::CreateTabuVars() { } bool TabuSearch::AtSolution() { + ++solution_count_; if (!ObjectiveMonitor::AtSolution()) { return false; } @@ -3549,8 +3570,15 @@ bool TabuSearch::AtSolution() { return true; } -bool TabuSearch::LocalOptimum() { +bool TabuSearch::AtLocalOptimum() { solver()->SetUseFastLocalSearch(false); + // If no solution has been accepted since the last local optimum, and no tabu + // lists are active, stop the search. + if (stamp_ > 0 && solution_count_ == 0 && keep_tabu_list_.empty() && + forbid_tabu_list_.empty()) { + stop_search_ = true; + } + solution_count_ = 0; AgeLists(); for (int i = 0; i < Size(); ++i) { SetCurrentInternalValue(i, std::numeric_limits::max()); @@ -3569,26 +3597,32 @@ bool TabuSearch::AcceptDelta(Assignment* delta, Assignment* deltadelta) { for (const IntVarElement& element : delta_container.elements()) { if (!element.Bound()) return true; } + delta_vars_.ResetAllToFalse(); + for (const IntVarElement& element : delta_container.elements()) { + const int var_index = element.Var()->index(); + if (var_index >= var_index_to_index_.size()) continue; + const int index = var_index_to_index_[var_index]; + if (index == -1) continue; + delta_values_[index] = element.Value(); + delta_vars_.Set(index); + } int num_respected = 0; - // TODO(user): Make this O(delta). - auto get_value = [this, &delta_container](int var_index) { - const IntVarElement* element = - delta_container.ElementPtrOrNull(vars(var_index)); - return (element != nullptr) - ? element->Value() + auto get_value = [this](int var_index) { + return delta_vars_[var_index] + ? delta_values_[var_index] : assignment_container_.Element(var_index).Value(); }; + const int64_t tabu_limit = TabuLimit(); for (const auto [var_index, value, unused_stamp] : synced_keep_tabu_list_) { if (get_value(var_index) == value) { - ++num_respected; + if (++num_respected >= tabu_limit) return true; } } for (const auto [var_index, value, unused_stamp] : synced_forbid_tabu_list_) { if (get_value(var_index) != value) { - ++num_respected; + if (++num_respected >= tabu_limit) return true; } } - const int64_t tabu_limit = TabuLimit(); if (num_respected >= tabu_limit) return true; // Aspiration // TODO(user): Add proper support for lex-objectives with steps. @@ -3697,7 +3731,7 @@ class SimulatedAnnealing : public Metaheuristic { std::vector initial_temperatures); ~SimulatedAnnealing() override {} void ApplyDecision(Decision* d) override; - bool LocalOptimum() override; + bool AtLocalOptimum() override; void AcceptNeighbor() override; std::string DebugString() const override { return "Simulated Annealing"; } @@ -3756,7 +3790,7 @@ void SimulatedAnnealing::ApplyDecision(Decision* const d) { } } -bool SimulatedAnnealing::LocalOptimum() { +bool SimulatedAnnealing::AtLocalOptimum() { for (int i = 0; i < Size(); ++i) { SetCurrentInternalValue(i, std::numeric_limits::max()); } @@ -3903,7 +3937,7 @@ class GuidedLocalSearch : public Metaheuristic { void ApplyDecision(Decision* d) override; bool AtSolution() override; void EnterSearch() override; - bool LocalOptimum() override; + bool AtLocalOptimum() override; virtual int64_t AssignmentElementPenalty(int index) const = 0; virtual int64_t AssignmentPenalty(int64_t var, int64_t value) const = 0; virtual int64_t Evaluate(const Assignment* delta, int64_t current_penalty, @@ -4172,7 +4206,7 @@ bool GuidedLocalSearch

::AcceptDelta(Assignment* delta, // Penalize (var, value) pairs of maximum utility, with // utility(var, value) = cost(var, value) / (1 + penalty(var, value)) template -bool GuidedLocalSearch

::LocalOptimum() { +bool GuidedLocalSearch

::AtLocalOptimum() { solver()->SetUseFastLocalSearch(false); std::vector utilities(num_vars_); double max_utility = -std::numeric_limits::infinity(); diff --git a/ortools/constraint_solver/search_stats.proto b/ortools/constraint_solver/search_stats.proto index 82cea906459..5031a1d06fc 100644 --- a/ortools/constraint_solver/search_stats.proto +++ b/ortools/constraint_solver/search_stats.proto @@ -89,10 +89,22 @@ message ConstraintSolverStatistics { double duration_seconds = 5; } +// Statistics on sub-solvers. +message SubSolverStatistics { + // Number of calls to Glop in LP scheduling. + int64 num_glop_calls_in_lp_scheduling = 1; + // Number of calls to CP-SAT in LP scheduling. + int64 num_cp_sat_calls_in_lp_scheduling = 2; + // Number of calls to min cost flow. + int64 num_min_cost_flow_calls = 3; +} + // Search statistics. message SearchStatistics { // Local search statistics for each solver context. repeated LocalSearchStatistics local_search_statistics = 1; // Constraint solver statistics. repeated ConstraintSolverStatistics constraint_solver_statistics = 2; + // Sub-solver statistics. + repeated SubSolverStatistics sub_solver_statistics = 3; } diff --git a/ortools/dotnet/Google.OrTools-full.csproj.in b/ortools/dotnet/Google.OrTools-full.csproj.in index 727092df988..c20ce39b770 100644 --- a/ortools/dotnet/Google.OrTools-full.csproj.in +++ b/ortools/dotnet/Google.OrTools-full.csproj.in @@ -89,6 +89,10 @@ pdlp/%(Filename)%(Extension) + + routing/%(Filename)%(Extension) + + sat/%(Filename)%(Extension) @@ -171,6 +175,11 @@ true PreserveNewest + + content/routing + true + PreserveNewest + content/sat true @@ -184,7 +193,7 @@ - + diff --git a/ortools/dotnet/Google.OrTools-local.csproj.in b/ortools/dotnet/Google.OrTools-local.csproj.in index eb5a3eff5b7..a925e3e7249 100644 --- a/ortools/dotnet/Google.OrTools-local.csproj.in +++ b/ortools/dotnet/Google.OrTools-local.csproj.in @@ -89,6 +89,10 @@ pdlp/%(Filename)%(Extension) + + routing/%(Filename)%(Extension) + + sat/%(Filename)%(Extension) @@ -159,6 +163,11 @@ true PreserveNewest + + content/routing + true + PreserveNewest + content/sat true @@ -172,7 +181,7 @@ - + diff --git a/ortools/flatzinc/challenge/Makefile b/ortools/flatzinc/challenge/Makefile index 3dae17137dc..68a33b5fd2f 100644 --- a/ortools/flatzinc/challenge/Makefile +++ b/ortools/flatzinc/challenge/Makefile @@ -18,7 +18,7 @@ DOCKER_BUILD_CMD := docker build endif DOCKER_RUN_CMD := docker run --rm --init -MZN_SUFFIX=2024v5 +MZN_SUFFIX=2025v2 DOCKER_NAME=cp-sat-minizinc-challenge MZN_TAG=${DOCKER_NAME}:${MZN_SUFFIX} MZN_LS_TAG=${DOCKER_NAME}-ls:${MZN_SUFFIX} diff --git a/ortools/flatzinc/challenge/minizinc-challenge-ls.Dockerfile b/ortools/flatzinc/challenge/minizinc-challenge-ls.Dockerfile index 81db76a0ed2..d5d6d02634d 100644 --- a/ortools/flatzinc/challenge/minizinc-challenge-ls.Dockerfile +++ b/ortools/flatzinc/challenge/minizinc-challenge-ls.Dockerfile @@ -1,6 +1,6 @@ -FROM minizinc/mznc2024:latest AS env +FROM minizinc/mznc2025:latest AS env -ENV SRC_GIT_BRANCH v99bugfix +ENV SRC_GIT_BRANCH=v99bugfix ENV TZ=America/Los_Angeles @@ -29,4 +29,6 @@ RUN ln -s /root/or-tools/bazel-bin/ortools/flatzinc/fz /entry_data/fzn-exec RUN cp /root/or-tools/ortools/flatzinc/mznlib/*mzn /entry_data/mzn-lib # Patch the run scripts -RUN sed -i -e "s/-G/--fzn-flags --params=use_ls_only:true -G/g" /minizinc/mzn-exec-* \ No newline at end of file +RUN sed -i -e "s/-G/--fzn-flags --params=use_ls_only:true -p 1 -G/g" /minizinc/mzn-exec-fd +RUN sed -i -e "s/-G/--fzn-flags --params=use_ls_only:true,num_workers:3 -G/g" /minizinc/mzn-exec-free +RUN sed -i -e "s/-G/--fzn-flags --params=use_ls_only:true -G/g" /minizinc/mzn-exec-par diff --git a/ortools/flatzinc/challenge/minizinc-challenge.Dockerfile b/ortools/flatzinc/challenge/minizinc-challenge.Dockerfile index d111f1e5f88..0fdfc256e61 100644 --- a/ortools/flatzinc/challenge/minizinc-challenge.Dockerfile +++ b/ortools/flatzinc/challenge/minizinc-challenge.Dockerfile @@ -1,6 +1,6 @@ -FROM minizinc/mznc2024:latest AS env +FROM minizinc/mznc2025:latest AS env -ENV SRC_GIT_BRANCH v99bugfix +ENV SRC_GIT_BRANCH=v99bugfix ENV TZ=America/Los_Angeles diff --git a/ortools/graph/BUILD.bazel b/ortools/graph/BUILD.bazel index d8d0a5c07d6..fac523588a4 100644 --- a/ortools/graph/BUILD.bazel +++ b/ortools/graph/BUILD.bazel @@ -52,6 +52,8 @@ cc_test( ":graph", "//ortools/base:gmock_main", "//ortools/base:intops", + "//ortools/base:strong_vector", + "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random", "@abseil-cpp//absl/strings", @@ -86,7 +88,9 @@ cc_library( hdrs = ["bounded_dijkstra.h"], deps = [ ":graph", + "//ortools/base:intops", "//ortools/base:iterator_adaptors", + "//ortools/base:strong_vector", "//ortools/base:threadpool", "//ortools/base:top_n", "@abseil-cpp//absl/algorithm:container", @@ -107,6 +111,7 @@ cc_test( ":test_util", "//ortools/base:dump_vars", "//ortools/base:gmock_main", + "//ortools/base:intops", "//ortools/util:flat_matrix", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/random", @@ -858,7 +863,7 @@ cc_test( deps = [ ":iterators", "//ortools/base:gmock_main", - "//ortools/base:strong_int", + "//ortools/base:intops", ], ) diff --git a/ortools/graph/bounded_dijkstra.h b/ortools/graph/bounded_dijkstra.h index e4522e5d900..98a7fc7e5f3 100644 --- a/ortools/graph/bounded_dijkstra.h +++ b/ortools/graph/bounded_dijkstra.h @@ -15,8 +15,10 @@ #define OR_TOOLS_GRAPH_BOUNDED_DIJKSTRA_H_ #include +#include #include #include +#include #include #include @@ -25,6 +27,8 @@ #include "absl/log/check.h" #include "absl/types/span.h" #include "ortools/base/iterator_adaptors.h" +#include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.h" #include "ortools/base/top_n.h" #include "ortools/graph/graph.h" @@ -54,22 +58,40 @@ namespace operations_research { // is >= limit we will return {limit, {}}. As a consequence any arc length >= // limit is the same as no arc. The code is also overflow-safe and will behave // correctly if the limit is int64max or infinity. -template -std::pair> SimpleOneToOneShortestPath( - int source, int destination, absl::Span tails, - absl::Span heads, absl::Span lengths, +template +std::pair> SimpleOneToOneShortestPath( + NodeIndex source, NodeIndex destination, absl::Span tails, + absl::Span heads, absl::Span lengths, DistanceType limit = std::numeric_limits::max()); -template +namespace internal { + +// TODO(user): We should move `is_strong_int` to util/intops/strong_int.h. +template +struct is_strong_int : std::false_type {}; + +template +struct is_strong_int<::util_intops::StrongInt> + : std::true_type {}; + +template +using IndexedVector = + std::conditional_t::value, + ::util_intops::StrongVector, + std::vector>; + +template class ElementGetter { public: - explicit ElementGetter(const std::vector& c) : c_(c) {} - const T& operator()(int index) const { return c_[index]; } + explicit ElementGetter(const IndexedVector& c) : c_(c) {} + const T& operator()(ArcIndex index) const { return c_[index]; } private: - const std::vector& c_; + const IndexedVector& c_; }; +} // namespace internal + // A wrapper that holds the memory needed to run many bounded shortest path // computations on the given graph. The graph must implement the // interface described in graph.h (without the need for reverse arcs). @@ -92,12 +114,20 @@ class ElementGetter { // negative source_offset, arc with a length greater than the distance_limit can // still be considered! template > + class ArcLengthFunctor = internal::ElementGetter< + DistanceType, typename GraphType::ArcIndex>> class BoundedDijkstraWrapper { public: - typedef typename GraphType::NodeIndex node_type; + typedef typename GraphType::NodeIndex NodeIndex; + typedef typename GraphType::ArcIndex ArcIndex; typedef DistanceType distance_type; + // A vector of T, indexed by NodeIndex/ArcIndex. + template + using ByNode = internal::IndexedVector; + template + using ByArc = internal::IndexedVector; + // IMPORTANT: Both arguments must outlive the class. The arc lengths cannot be // negative and the vector must be of the correct size (both preconditions are // CHECKed). @@ -106,7 +136,7 @@ class BoundedDijkstraWrapper { // RunBoundedDijkstra(). That's fine. Doing so will obviously invalidate the // reader API of the last Dijkstra run, which could return junk, or crash. BoundedDijkstraWrapper(const GraphType* graph, - const std::vector* arc_lengths); + const ByArc* arc_lengths); // Variant that takes a custom arc length functor and copies it locally. BoundedDijkstraWrapper(const GraphType* graph, @@ -116,8 +146,8 @@ class BoundedDijkstraWrapper { // of the graph within the distance limit (exclusive). The first element of // the returned vector will always be the source_node with a distance of zero. // See RunBoundedDijkstraFromMultipleSources() for more information. - const std::vector& RunBoundedDijkstra(int source_node, - DistanceType distance_limit) { + const std::vector& RunBoundedDijkstra( + NodeIndex source_node, DistanceType distance_limit) { return RunBoundedDijkstraFromMultipleSources({{source_node, 0}}, distance_limit); } @@ -127,7 +157,8 @@ class BoundedDijkstraWrapper { // // If this returns true, you can get the path distance with distances()[to] // and the path with ArcPathTo(to) or NodePathTo(to). - bool OneToOneShortestPath(int from, int to, DistanceType distance_limit); + bool OneToOneShortestPath(NodeIndex from, NodeIndex to, + DistanceType distance_limit); // Returns the list of all the nodes which are under the given distance limit // (exclusive) from at least one of the given source nodes (which also have @@ -136,8 +167,8 @@ class BoundedDijkstraWrapper { // By "distance", we mean the length of the shortest path from any source // plus the source's distance offset, where the length of a path is the // sum of the length of its arcs - const std::vector& RunBoundedDijkstraFromMultipleSources( - const std::vector>& + const std::vector& RunBoundedDijkstraFromMultipleSources( + const std::vector>& sources_with_distance_offsets, DistanceType distance_limit); @@ -162,10 +193,11 @@ class BoundedDijkstraWrapper { // // Note that the distances() will take the source offsets into account, // but not the destination offsets. - std::vector RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations( - const std::vector>& + std::vector + RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations( + const std::vector>& sources_with_distance_offsets, - const std::vector>& + const std::vector>& destinations_with_distance_offsets, int num_destinations_to_reach, DistanceType distance_limit); @@ -174,19 +206,19 @@ class BoundedDijkstraWrapper { // happens at most once per node, when popping it from the Dijkstra queue, // meaning that the node has been fully 'processed'). This callback may modify // the distance limit dynamically, thus affecting the stopping criterion. - const std::vector& RunBoundedDijkstraWithSettledNodeCallback( - const std::vector>& + const std::vector& RunBoundedDijkstraWithSettledNodeCallback( + const std::vector>& sources_with_distance_offsets, - std::function settled_node_callback, DistanceType distance_limit); // Returns true if `node` was reached by the last Run*() call. - bool IsReachable(int node) const { return is_reached_[node]; } + bool IsReachable(NodeIndex node) const { return is_reached_[node]; } // Returns all the reached nodes form the previous Run*() call. - const std::vector& reached_nodes() const { return reached_nodes_; } + const ByNode& reached_nodes() const { return reached_nodes_; } // The following vectors are all indexed by graph node indices. // @@ -194,7 +226,7 @@ class BoundedDijkstraWrapper { // reached nodes are updated, the others will contain junk. // The distance of the nodes from their source. - const std::vector& distances() const { return distances_; } + const ByNode& distances() const { return distances_; } // The parent of the nodes in the shortest path from their source. // When a node doesn't have any parent (it has to be a source), its parent @@ -203,27 +235,29 @@ class BoundedDijkstraWrapper { // arcs have a length of zero. // Note also that some sources may have parents, because of the initial // distances. - const std::vector& parents() const { return parents_; } + const ByNode& parents() const { return parents_; } // The arc reaching a given node in the path from their source. // arc_from_source()[x] is undefined (i.e. junk) when parents()[x] == x. - const std::vector& arc_from_source() const { return arc_from_source_; } + const ByNode& arc_from_source() const { return arc_from_source_; } // Returns the list of all the arcs in the shortest path from the node's // source to the node. - std::vector ArcPathTo(int node) const; + std::vector ArcPathTo(NodeIndex node) const; ABSL_DEPRECATED("Use ArcPathTo() instead.") - std::vector ArcPathToNode(int node) const { return ArcPathTo(node); } + std::vector ArcPathToNode(NodeIndex node) const { + return ArcPathTo(node); + } // Returns the list of all the nodes in the shortest path from the node's // source to the node. This always start by the node's source, and end by // the given node. In the case that source == node, returns {node}. - std::vector NodePathTo(int node) const; + std::vector NodePathTo(NodeIndex node) const; // Returns the node's source. This is especially useful when running // Dijkstras from multiple sources. - int SourceOfShortestPathToNode(int node) const; + NodeIndex SourceOfShortestPathToNode(NodeIndex node) const; // Original Source/Destination index extraction, after a call to the // multi-source and/or multi-destination variants: @@ -239,16 +273,16 @@ class BoundedDijkstraWrapper { // rely on the value. // // These methods are invalidated by the next RunBoundedDijkstra*() call. - int GetSourceIndex(int node) const; - int GetDestinationIndex(int node) const; + int GetSourceIndex(NodeIndex node) const; + int GetDestinationIndex(NodeIndex node) const; // Trivial accessors to the underlying graph and arc lengths. const GraphType& graph() const { return *graph_; } - const std::vector& arc_lengths() const { + const ByArc& arc_lengths() const { CHECK(arc_lengths_); return *arc_lengths_; } - DistanceType GetArcLength(int arc) const { + DistanceType GetArcLength(ArcIndex arc) const { const DistanceType length = arc_length_functor_(arc); DCHECK_GE(length, 0); return length; @@ -262,18 +296,18 @@ class BoundedDijkstraWrapper { // The Graph and length of each arc. const GraphType* const graph_; ArcLengthFunctor arc_length_functor_; - const std::vector* const arc_lengths_; + const ByArc* const arc_lengths_; // Data about the last Dijkstra run. - std::vector distances_; - std::vector parents_; - std::vector arc_from_source_; - std::vector is_reached_; - std::vector reached_nodes_; + ByNode distances_; + ByNode parents_; + ByNode arc_from_source_; + ByNode is_reached_; + std::vector reached_nodes_; // Priority queue of nodes, ordered by their distance to the source. struct NodeDistance { - node_type node; // The target node. + NodeIndex node; // The target node. DistanceType distance; // Its distance from the source. bool operator<(const NodeDistance& other) const { @@ -287,7 +321,7 @@ class BoundedDijkstraWrapper { // or ieee754 floating-point, when the machine is little endian, and // when the total size of NodeDistance equals 16 bytes). // And here are the speeds of the BM_GridGraph benchmark (in which - // DistanceType=int64_t and node_type=int32_t), done with benchy + // DistanceType=int64_t and NodeIndex=int32_t), done with benchy // --runs=20: 0) BM_GridGraph 9.22ms ± 5% BM_GridGraph 3.19ms // ± 6% 1) BM_GridGraph 8.89ms ± 4% BM_GridGraph 3.07ms ± // 3% 2) BM_GridGraph 8.61ms ± 3% BM_GridGraph 3.13ms ± 6% @@ -303,8 +337,8 @@ class BoundedDijkstraWrapper { // The vectors are only allocated after they are first used. // Between calls, is_destination_ is all false, and the rest is junk. std::vector is_destination_; - std::vector node_to_source_index_; - std::vector node_to_destination_index_; + ByNode node_to_source_index_; + ByNode node_to_destination_index_; }; // ----------------------------------------------------------------------------- @@ -314,12 +348,12 @@ class BoundedDijkstraWrapper { template BoundedDijkstraWrapper:: BoundedDijkstraWrapper(const GraphType* graph, - const std::vector* arc_lengths) + const ByArc* arc_lengths) : graph_(graph), arc_length_functor_(*arc_lengths), arc_lengths_(arc_lengths) { CHECK(arc_lengths_ != nullptr); - CHECK_EQ(arc_lengths_->size(), graph->num_arcs()); + CHECK_EQ(ArcIndex(arc_lengths_->size()), graph->num_arcs()); for (const DistanceType length : *arc_lengths) { CHECK_GE(length, 0); } @@ -341,10 +375,10 @@ BoundedDijkstraWrapper:: arc_lengths_(other.arc_lengths_) {} template -const std::vector& +const std::vector& BoundedDijkstraWrapper:: RunBoundedDijkstraFromMultipleSources( - const std::vector>& + const std::vector>& sources_with_distance_offsets, DistanceType distance_limit) { return RunBoundedDijkstraWithSettledNodeCallback( @@ -352,12 +386,12 @@ BoundedDijkstraWrapper:: } template -std::vector +std::vector BoundedDijkstraWrapper:: RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations( - const std::vector>& + const std::vector>& sources_with_distance_offsets, - const std::vector>& + const std::vector>& destinations_with_distance_offsets, int num_destinations_to_reach, DistanceType distance_limit) { if (destinations_with_distance_offsets.empty()) return {}; @@ -368,22 +402,22 @@ BoundedDijkstraWrapper:: // to reduce the search space. DCHECK_GE(num_destinations_to_reach, 0); int num_destinations = 0; - is_destination_.resize(graph_->num_nodes(), false); + is_destination_.resize(static_cast(graph_->num_nodes()), false); node_to_destination_index_.resize(graph_->num_nodes(), -1); DistanceType min_destination_distance_offset = destinations_with_distance_offsets[0].second; for (int i = 0; i < destinations_with_distance_offsets.size(); ++i) { - const int node = destinations_with_distance_offsets[i].first; + const NodeIndex node = destinations_with_distance_offsets[i].first; const DistanceType distance = destinations_with_distance_offsets[i].second; - if (!is_destination_[node]) ++num_destinations; + if (!is_destination_[static_cast(node)]) ++num_destinations; // Skip useless repetitions. - if (is_destination_[node] && + if (is_destination_[static_cast(node)] && distance >= destinations_with_distance_offsets[node_to_destination_index_[node]] .second) { continue; } - is_destination_[node] = true; + is_destination_[static_cast(node)] = true; node_to_destination_index_[node] = i; min_destination_distance_offset = std::min(min_destination_distance_offset, distance); @@ -395,13 +429,13 @@ BoundedDijkstraWrapper:: gtl::TopN> closest_destinations( /*limit=*/num_destinations_to_reach); - std::function + std::function settled_node_callback = [this, num_destinations_to_reach, min_destination_distance_offset, &destinations_with_distance_offsets, &closest_destinations]( - node_type settled_node, DistanceType settled_distance, + NodeIndex settled_node, DistanceType settled_distance, DistanceType* distance_limit) { - if (!is_destination_[settled_node]) return; + if (!is_destination_[static_cast(settled_node)]) return; const DistanceType distance = settled_distance + destinations_with_distance_offsets @@ -423,12 +457,12 @@ BoundedDijkstraWrapper:: // Clean up, sparsely, for the next call. for (const auto& [node, _] : destinations_with_distance_offsets) { - is_destination_[node] = false; + is_destination_[static_cast(node)] = false; } // Return the closest "num_destinations_to_reach" reached destinations, // sorted by distance. - std::vector sorted_destinations; + std::vector sorted_destinations; sorted_destinations.reserve(closest_destinations.size()); for (const NodeDistance& d : closest_destinations.Take()) { sorted_destinations.push_back(d.node); @@ -438,10 +472,11 @@ BoundedDijkstraWrapper:: template bool BoundedDijkstraWrapper:: - OneToOneShortestPath(int from, int to, DistanceType distance_limit) { + OneToOneShortestPath(NodeIndex from, NodeIndex to, + DistanceType distance_limit) { bool reached = false; - std::function - settled_node_callback = [to, &reached](node_type node, + std::function + settled_node_callback = [to, &reached](NodeIndex node, DistanceType distance, DistanceType* distance_limit) { if (node != to) return; @@ -456,18 +491,18 @@ bool BoundedDijkstraWrapper:: } template -const std::vector& +const std::vector& BoundedDijkstraWrapper:: RunBoundedDijkstraWithSettledNodeCallback( - const std::vector>& + const std::vector>& sources_with_distance_offsets, - std::function settled_node_callback, DistanceType distance_limit) { // Sparse clear is_reached_ from the last call. - for (const int node : reached_nodes_) { + for (const NodeIndex node : reached_nodes_) { is_reached_[node] = false; } reached_nodes_.clear(); @@ -475,15 +510,15 @@ BoundedDijkstraWrapper:: is_reached_.resize(graph_->num_nodes(), false); distances_.resize(graph_->num_nodes(), distance_limit); - parents_.resize(graph_->num_nodes(), std::numeric_limits::min()); - arc_from_source_.resize(graph_->num_nodes(), -1); + parents_.resize(graph_->num_nodes(), std::numeric_limits::min()); + arc_from_source_.resize(graph_->num_nodes(), GraphType::kNilArc); // Initialize sources. CHECK(queue_.empty()); node_to_source_index_.resize(graph_->num_nodes(), -1); for (int i = 0; i < sources_with_distance_offsets.size(); ++i) { - const int node = sources_with_distance_offsets[i].first; - DCHECK_GE(node, 0); + const NodeIndex node = sources_with_distance_offsets[i].first; + DCHECK_GE(node, NodeIndex(0)); DCHECK_LT(node, graph_->num_nodes()); const DistanceType distance = sources_with_distance_offsets[i].second; // Sources with an initial distance ≥ limit are *not* reached. @@ -498,7 +533,7 @@ BoundedDijkstraWrapper:: node_to_source_index_[node] = i; distances_[node] = distance; } - for (const int source : reached_nodes_) { + for (const NodeIndex source : reached_nodes_) { queue_.push_back({source, distances_[source]}); } std::make_heap(queue_.begin(), queue_.end(), std::greater()); @@ -533,7 +568,8 @@ BoundedDijkstraWrapper:: // Visit the neighbors. const DistanceType limit = distance_limit - top.distance; - for (const int arc : graph_->OutgoingArcs(top.node)) { + for (const typename GraphType::ArcIndex arc : + graph_->OutgoingArcs(top.node)) { // Overflow-safe check of top.distance + arc_length >= distance_limit. // This works since we know top.distance < distance_limit, as long as we // don't have negative top.distance (which might happen with negative @@ -543,7 +579,7 @@ BoundedDijkstraWrapper:: if (arc_length >= limit) continue; const DistanceType candidate_distance = top.distance + arc_length; - const int head = graph_->Head(arc); + const NodeIndex head = graph_->Head(arc); if (is_reached_[head]) { if (candidate_distance >= distances_[head]) continue; } else { @@ -563,14 +599,14 @@ BoundedDijkstraWrapper:: } template -std::vector +std::vector BoundedDijkstraWrapper::ArcPathTo( - int node) const { - std::vector output; + NodeIndex node) const { + std::vector output; int loop_detector = 0; while (true) { - DCHECK_GE(node, 0); - DCHECK_LT(node, parents_.size()); + DCHECK_GE(node, NodeIndex(0)); + DCHECK_LT(node, NodeIndex(parents_.size())); CHECK_LT(loop_detector++, parents_.size()); if (parents_[node] == node) break; output.push_back(arc_from_source_[node]); @@ -581,14 +617,14 @@ BoundedDijkstraWrapper::ArcPathTo( } template -std::vector +std::vector BoundedDijkstraWrapper::NodePathTo( - int node) const { - std::vector output; + NodeIndex node) const { + std::vector output; int loop_detector = 0; while (true) { - DCHECK_GE(node, 0); - DCHECK_LT(node, parents_.size()); + DCHECK_GE(node, NodeIndex(0)); + DCHECK_LT(node, NodeIndex(parents_.size())); CHECK_LT(loop_detector++, parents_.size()); output.push_back(node); if (parents_[node] == node) break; @@ -599,27 +635,28 @@ BoundedDijkstraWrapper::NodePathTo( } template -int BoundedDijkstraWrapper:: - SourceOfShortestPathToNode(int node) const { - int parent = node; +typename GraphType::NodeIndex BoundedDijkstraWrapper< + GraphType, DistanceType, + ArcLengthFunctor>::SourceOfShortestPathToNode(NodeIndex node) const { + NodeIndex parent = node; while (parents_[parent] != parent) parent = parents_[parent]; return parent; } template int BoundedDijkstraWrapper::GetSourceIndex(int node) const { - DCHECK_GE(node, 0); - DCHECK_LT(node, node_to_source_index_.size()); + ArcLengthFunctor>::GetSourceIndex(NodeIndex node) + const { + DCHECK_GE(node, NodeIndex(0)); + DCHECK_LT(node, NodeIndex(node_to_source_index_.size())); return node_to_source_index_[node]; } template -int BoundedDijkstraWrapper::GetDestinationIndex(int node) - const { - DCHECK_GE(node, 0); - DCHECK_LT(node, node_to_destination_index_.size()); +int BoundedDijkstraWrapper:: + GetDestinationIndex(NodeIndex node) const { + DCHECK_GE(node, NodeIndex(0)); + DCHECK_LT(node, NodeIndex(node_to_destination_index_.size())); return node_to_destination_index_[node]; } @@ -627,37 +664,38 @@ int BoundedDijkstraWrapper -std::pair> SimpleOneToOneShortestPath( - int source, int destination, absl::Span tails, - absl::Span heads, absl::Span lengths, +template +std::pair> SimpleOneToOneShortestPath( + NodeIndex source, NodeIndex destination, absl::Span tails, + absl::Span heads, absl::Span lengths, DistanceType limit) { + using ArcIndex = NodeIndex; // Compute the number of nodes. // // This is not necessary, but is a good practice to allocate the graph size in // one go. We also do some basic validation. CHECK_GE(source, 0); CHECK_GE(destination, 0); - int num_nodes = std::max(source + 1, destination + 1); - for (const int tail : tails) { + NodeIndex num_nodes = std::max(source + 1, destination + 1); + for (const NodeIndex tail : tails) { CHECK_GE(tail, 0); num_nodes = std::max(tail + 1, num_nodes); } - for (const int head : heads) { + for (const NodeIndex head : heads) { CHECK_GE(head, 0); num_nodes = std::max(head + 1, num_nodes); } // The number of arcs. - const int num_arcs = tails.size(); + const ArcIndex num_arcs = tails.size(); CHECK_EQ(num_arcs, heads.size()); CHECK_EQ(num_arcs, lengths.size()); // Build the graph. Note that this permutes arc indices for speed, but we // don't care here since we will return a node path. - util::StaticGraph<> graph(num_nodes, num_arcs); + util::StaticGraph graph(num_nodes, num_arcs); std::vector arc_lengths(lengths.begin(), lengths.end()); - for (int a = 0; a < num_arcs; ++a) { + for (ArcIndex a = 0; a < num_arcs; ++a) { // Negative length can cause the algo to loop forever and/or use a lot of // memory. So it should be validated. CHECK_GE(lengths[a], 0); diff --git a/ortools/graph/bounded_dijkstra_test.cc b/ortools/graph/bounded_dijkstra_test.cc index 07a21f5a8d7..a5f256cce14 100644 --- a/ortools/graph/bounded_dijkstra_test.cc +++ b/ortools/graph/bounded_dijkstra_test.cc @@ -30,6 +30,7 @@ #include "gtest/gtest.h" #include "ortools/base/dump_vars.h" #include "ortools/base/gmock.h" +#include "ortools/base/strong_int.h" #include "ortools/graph/graph.h" #include "ortools/graph/graph_io.h" #include "ortools/graph/test_util.h" @@ -45,122 +46,140 @@ using ::testing::Pair; using ::testing::UnorderedElementsAreArray; using ::util::ListGraph; +DEFINE_STRONG_INT_TYPE(NodeIndex, int32_t); +DEFINE_STRONG_INT_TYPE(ArcIndex, int64_t); + +using TestGraph = ListGraph; +template +using DijkstraWrapper = BoundedDijkstraWrapper; + TEST(BoundedDijkstraWrapperDeathTest, Accessors) { - ListGraph<> graph; - graph.AddArc(1, 3); - std::vector arc_lengths = {2.5}; - BoundedDijkstraWrapper, float> dijkstra(&graph, &arc_lengths); + TestGraph graph; + graph.AddArc(NodeIndex(1), NodeIndex(3)); + DijkstraWrapper::ByArc arc_lengths = {2.5}; + DijkstraWrapper dijkstra(&graph, &arc_lengths); const std::is_same same_type; ASSERT_TRUE(same_type.value); ASSERT_EQ(&dijkstra.graph(), &graph); - ASSERT_EQ(dijkstra.GetArcLength(0), 2.5); + ASSERT_EQ(dijkstra.GetArcLength(ArcIndex(0)), 2.5); } TEST(BoundedDijkstraWrapperDeathTest, WithArcLengthFunctor) { - ListGraph<> graph; - graph.AddArc(1, 3); - BoundedDijkstraWrapper, float, std::function> - dijkstra(&graph, [](int) { return 2.34; }); - ASSERT_FLOAT_EQ(dijkstra.GetArcLength(0), 2.34f); + TestGraph graph; + graph.AddArc(NodeIndex(1), NodeIndex(3)); + BoundedDijkstraWrapper> + dijkstra(&graph, [](ArcIndex) { return 2.34; }); + ASSERT_FLOAT_EQ(dijkstra.GetArcLength(ArcIndex(0)), 2.34f); } TEST(BoundedDijkstraWrapperDeathTest, ConstructorPreconditions) { - ListGraph<> graph; - for (int i = 0; i < 50; ++i) graph.AddArc(i, i + 1); + TestGraph graph; + for (int i = 0; i < 50; ++i) graph.AddArc(NodeIndex(i), NodeIndex(i + 1)); - std::vector arc_lengths(13, 0); - typedef BoundedDijkstraWrapper, int> TestedClass; + typedef DijkstraWrapper TestedClass; + TestedClass::ByArc arc_lengths(13, 0); EXPECT_DEATH(new TestedClass(&graph, &arc_lengths), "13"); arc_lengths.resize(50, 0); - arc_lengths[20] = -132; + arc_lengths[ArcIndex(20)] = -132; EXPECT_DEATH(new TestedClass(&graph, &arc_lengths), "-132"); } TEST(BoundedDijkstraWrapper, ArcPathToAndSourceOfShortestPathToNode) { - ListGraph<> graph; - std::vector arc_lengths = {1, 2, 3, 4, 6, 5}; - graph.AddArc(0, 1); - graph.AddArc(0, 1); - graph.AddArc(1, 2); - graph.AddArc(1, 2); - graph.AddArc(2, 3); - graph.AddArc(2, 3); - - BoundedDijkstraWrapper, int> dijkstra(&graph, &arc_lengths); - const std::vector reached = dijkstra.RunBoundedDijkstra(0, 10); - EXPECT_THAT(reached, ElementsAre(0, 1, 2, 3)); - EXPECT_EQ(9, dijkstra.distances()[3]); - EXPECT_THAT(dijkstra.ArcPathTo(3), ElementsAre(0, 2, 5)); - EXPECT_THAT(dijkstra.NodePathTo(3), ElementsAre(0, 1, 2, 3)); - EXPECT_EQ(0, dijkstra.SourceOfShortestPathToNode(3)); + TestGraph graph; + DijkstraWrapper::ByArc arc_lengths = {1, 2, 3, 4, 6, 5}; + graph.AddArc(NodeIndex(0), NodeIndex(1)); + graph.AddArc(NodeIndex(0), NodeIndex(1)); + graph.AddArc(NodeIndex(1), NodeIndex(2)); + graph.AddArc(NodeIndex(1), NodeIndex(2)); + graph.AddArc(NodeIndex(2), NodeIndex(3)); + graph.AddArc(NodeIndex(2), NodeIndex(3)); + + DijkstraWrapper dijkstra(&graph, &arc_lengths); + const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(0), 10); + EXPECT_THAT(reached, ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2), + NodeIndex(3))); + EXPECT_EQ(9, dijkstra.distances()[NodeIndex(3)]); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(3)), + ElementsAre(ArcIndex(0), ArcIndex(2), ArcIndex(5))); + EXPECT_THAT( + dijkstra.NodePathTo(NodeIndex(3)), + ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2), NodeIndex(3))); + EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(3))); } TEST(BoundedDijkstraWrapper, EmptyPath) { - ListGraph<> graph; - std::vector arc_lengths = {1, 2}; - graph.AddArc(0, 1); - graph.AddArc(2, 3); - - BoundedDijkstraWrapper, int> dijkstra(&graph, &arc_lengths); - const std::vector reached = dijkstra.RunBoundedDijkstra(0, 10); - EXPECT_THAT(reached, ElementsAre(0, 1)); - - EXPECT_EQ(0, dijkstra.distances()[0]); - EXPECT_THAT(dijkstra.ArcPathTo(0), ElementsAre()); - EXPECT_THAT(dijkstra.NodePathTo(0), ElementsAre(0)); - EXPECT_EQ(0, dijkstra.SourceOfShortestPathToNode(0)); + TestGraph graph; + DijkstraWrapper::ByArc arc_lengths = {1, 2}; + graph.AddArc(NodeIndex(0), NodeIndex(1)); + graph.AddArc(NodeIndex(2), NodeIndex(3)); + + DijkstraWrapper dijkstra(&graph, &arc_lengths); + const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(0), 10); + EXPECT_THAT(reached, ElementsAre(NodeIndex(0), NodeIndex(1))); + + EXPECT_EQ(0, dijkstra.distances()[NodeIndex(0)]); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(0)), ElementsAre()); + EXPECT_THAT(dijkstra.NodePathTo(NodeIndex(0)), ElementsAre(NodeIndex(0))); + EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(0))); } TEST(BoundedDijkstraWrapper, OverflowSafe) { - ListGraph<> graph; + TestGraph graph; const int64_t int_max = std::numeric_limits::max(); - std::vector arc_lengths = {int_max, int_max / 2, int_max / 2, 1}; - graph.AddArc(0, 1); - graph.AddArc(0, 1); - graph.AddArc(1, 2); - graph.AddArc(2, 3); + DijkstraWrapper::ByArc arc_lengths = {int_max, int_max / 2, + int_max / 2, 1}; + graph.AddArc(NodeIndex(0), NodeIndex(1)); + graph.AddArc(NodeIndex(0), NodeIndex(1)); + graph.AddArc(NodeIndex(1), NodeIndex(2)); + graph.AddArc(NodeIndex(2), NodeIndex(3)); - BoundedDijkstraWrapper, int64_t> dijkstra(&graph, &arc_lengths); - const std::vector reached = dijkstra.RunBoundedDijkstra(0, int_max); + BoundedDijkstraWrapper dijkstra(&graph, &arc_lengths); + const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(0), int_max); // This works because int_max is odd, i.e. 2 * (int_max / 2) = int_max - 1 - EXPECT_THAT(reached, ElementsAre(0, 1, 2)); - EXPECT_EQ(0, dijkstra.distances()[0]); - EXPECT_EQ(int_max / 2, dijkstra.distances()[1]); - EXPECT_EQ(int_max - 1, dijkstra.distances()[2]); + EXPECT_THAT(reached, ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2))); + EXPECT_EQ(0, dijkstra.distances()[NodeIndex(0)]); + EXPECT_EQ(int_max / 2, dijkstra.distances()[NodeIndex(1)]); + EXPECT_EQ(int_max - 1, dijkstra.distances()[NodeIndex(2)]); } TEST(BoundedDijkstraWrapper, ArcPathToAndSourceOfShortestPathToNode_WithArcLengthFunction) { - ListGraph<> graph; - std::vector arc_lengths = {1, 2, 3, 4, 6, 5}; - graph.AddArc(0, 1); - graph.AddArc(0, 1); - graph.AddArc(1, 2); - graph.AddArc(1, 2); - graph.AddArc(2, 3); - graph.AddArc(2, 3); + TestGraph graph; + DijkstraWrapper::ByArc arc_lengths = {1, 2, 3, 4, 6, 5}; + graph.AddArc(NodeIndex(0), NodeIndex(1)); + graph.AddArc(NodeIndex(0), NodeIndex(1)); + graph.AddArc(NodeIndex(1), NodeIndex(2)); + graph.AddArc(NodeIndex(1), NodeIndex(2)); + graph.AddArc(NodeIndex(2), NodeIndex(3)); + graph.AddArc(NodeIndex(2), NodeIndex(3)); class MyArcLengthFunctor { public: - explicit MyArcLengthFunctor(const std::vector& arc_lengths) + explicit MyArcLengthFunctor( + const DijkstraWrapper::ByArc& arc_lengths) : arc_lengths_(arc_lengths) {} - int operator()(int arc) const { - return arc % 2 == 1 ? arc_lengths_[arc] : 100; + + int operator()(ArcIndex arc) const { + return arc.value() % 2 == 1 ? arc_lengths_[arc] : 100; } private: - const std::vector& arc_lengths_; + const DijkstraWrapper::ByArc& arc_lengths_; }; - BoundedDijkstraWrapper, int, MyArcLengthFunctor> dijkstra( + BoundedDijkstraWrapper dijkstra( &graph, MyArcLengthFunctor(arc_lengths)); - const std::vector reached = dijkstra.RunBoundedDijkstra(0, 20); - EXPECT_THAT(reached, ElementsAre(0, 1, 2, 3)); - EXPECT_EQ(11, dijkstra.distances()[3]); - EXPECT_THAT(dijkstra.ArcPathTo(3), ElementsAre(1, 3, 5)); - EXPECT_THAT(dijkstra.NodePathTo(3), ElementsAre(0, 1, 2, 3)); - EXPECT_EQ(0, dijkstra.SourceOfShortestPathToNode(3)); + const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(0), 20); + EXPECT_THAT(reached, ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2), + NodeIndex(3))); + EXPECT_EQ(11, dijkstra.distances()[NodeIndex(3)]); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(3)), + ElementsAre(ArcIndex(1), ArcIndex(3), ArcIndex(5))); + EXPECT_THAT( + dijkstra.NodePathTo(NodeIndex(3)), + ElementsAre(NodeIndex(0), NodeIndex(1), NodeIndex(2), NodeIndex(3))); + EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(3))); } TEST(BoundedDijkstraWrapperTest, RandomDenseGraph) { @@ -168,12 +187,12 @@ TEST(BoundedDijkstraWrapperTest, RandomDenseGraph) { const int num_nodes = 50; std::vector> lengths(num_nodes, std::vector(num_nodes)); - ListGraph<> graph; - std::vector arc_lengths; + TestGraph graph; + DijkstraWrapper::ByArc arc_lengths; for (int i = 0; i < num_nodes; ++i) { for (int j = 0; j < num_nodes; ++j) { lengths[i][j] = (i == j) ? 0 : absl::Uniform(random, 0, 1000); - graph.AddArc(i, j); + graph.AddArc(NodeIndex(i), NodeIndex(j)); arc_lengths.push_back(lengths[i][j]); } } @@ -191,15 +210,15 @@ TEST(BoundedDijkstraWrapperTest, RandomDenseGraph) { std::vector reached_sizes; for (int source = 0; source < num_nodes; ++source) { const int limit = 100; - BoundedDijkstraWrapper, int> dijkstra(&graph, &arc_lengths); - const std::vector reached = dijkstra.RunBoundedDijkstra(source, limit); - for (const int node : reached) { + DijkstraWrapper dijkstra(&graph, &arc_lengths); + const auto reached = dijkstra.RunBoundedDijkstra(NodeIndex(source), limit); + for (const NodeIndex node : reached) { EXPECT_LT(dijkstra.distances()[node], limit); - EXPECT_EQ(dijkstra.distances()[node], lengths[source][node]); + EXPECT_EQ(dijkstra.distances()[node], lengths[source][node.value()]); // Check that we never have the same node twice in the paths. - std::vector path = {node}; - int parent = node; + std::vector path = {node}; + NodeIndex parent = node; while (dijkstra.parents()[parent] != parent) { parent = dijkstra.parents()[parent]; path.push_back(parent); @@ -230,7 +249,7 @@ TEST(SimpleOneToOneShortestPathTest, PathTooLong) { { const auto [distance, path] = - SimpleOneToOneShortestPath(0, 3, tails, heads, lengths); + SimpleOneToOneShortestPath(0, 3, tails, heads, lengths); EXPECT_EQ(distance, std::numeric_limits::max()); EXPECT_TRUE(path.empty()); } @@ -238,7 +257,7 @@ TEST(SimpleOneToOneShortestPathTest, PathTooLong) { { // from 0 to 2 work because 2 * big_length < int_max. const auto [distance, path] = - SimpleOneToOneShortestPath(0, 2, tails, heads, lengths); + SimpleOneToOneShortestPath(0, 2, tails, heads, lengths); EXPECT_EQ(distance, std::numeric_limits::max() - 1); EXPECT_THAT(path, ElementsAre(0, 1, 2)); } @@ -256,7 +275,7 @@ TEST(SimpleOneToOneShortestPathTest, Random) { // This will be the "sparse" representation. std::vector tails; std::vector heads; - std::vector arc_lengths; + DijkstraWrapper::ByArc arc_lengths; // We permutes the arc order to properly test that it do not matter. std::vector nodes(num_nodes); @@ -292,8 +311,8 @@ TEST(SimpleOneToOneShortestPathTest, Random) { // No limit. There should always be a path with our generated data. { - const auto [distance, path] = - SimpleOneToOneShortestPath(from, to, tails, heads, arc_lengths); + const auto [distance, path] = SimpleOneToOneShortestPath( + from, to, tails, heads, arc_lengths); EXPECT_EQ(distance, shortest_distance[from][to]); EXPECT_FALSE(path.empty()); EXPECT_EQ(path.front(), from); @@ -302,7 +321,7 @@ TEST(SimpleOneToOneShortestPathTest, Random) { // A limit of shortest_distance[from][to] + 1 works too. { - const auto [distance, path] = SimpleOneToOneShortestPath( + const auto [distance, path] = SimpleOneToOneShortestPath( from, to, tails, heads, arc_lengths, shortest_distance[from][to] + 1); EXPECT_EQ(distance, shortest_distance[from][to]); EXPECT_FALSE(path.empty()); @@ -312,7 +331,7 @@ TEST(SimpleOneToOneShortestPathTest, Random) { // But a limit of shortest_distance[from][to] should fail. { - const auto [distance, path] = SimpleOneToOneShortestPath( + const auto [distance, path] = SimpleOneToOneShortestPath( from, to, tails, heads, arc_lengths, shortest_distance[from][to]); EXPECT_EQ(distance, shortest_distance[from][to]); EXPECT_TRUE(path.empty()); @@ -321,101 +340,116 @@ TEST(SimpleOneToOneShortestPathTest, Random) { } TEST(BoundedDijkstraWrapperTest, MultiRunsOverDynamicGraphAndLengths) { - ListGraph<> graph; - graph.AddArc(0, 1); - graph.AddArc(0, 1); - std::vector arc_lengths = {4, 3}; - BoundedDijkstraWrapper, int> dijkstra(&graph, &arc_lengths); - - EXPECT_THAT(dijkstra.RunBoundedDijkstra(0, 5), ElementsAre(0, 1)); - EXPECT_EQ(0, dijkstra.SourceOfShortestPathToNode(1)); - EXPECT_THAT(dijkstra.ArcPathTo(1), ElementsAre(1)); - - EXPECT_THAT(dijkstra.RunBoundedDijkstra(0, 2), ElementsAre(0)); - EXPECT_EQ(0, dijkstra.SourceOfShortestPathToNode(0)); - EXPECT_THAT(dijkstra.ArcPathTo(0), IsEmpty()); - - EXPECT_THAT(dijkstra.RunBoundedDijkstra(1, 99), ElementsAre(1)); - EXPECT_EQ(1, dijkstra.SourceOfShortestPathToNode(1)); - EXPECT_THAT(dijkstra.ArcPathTo(1), IsEmpty()); + TestGraph graph; + graph.AddArc(NodeIndex(0), NodeIndex(1)); + graph.AddArc(NodeIndex(0), NodeIndex(1)); + DijkstraWrapper::ByArc arc_lengths = {4, 3}; + DijkstraWrapper dijkstra(&graph, &arc_lengths); + + EXPECT_THAT(dijkstra.RunBoundedDijkstra(NodeIndex(0), 5), + ElementsAre(NodeIndex(0), NodeIndex(1))); + EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(1))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)), ElementsAre(ArcIndex(1))); + + EXPECT_THAT(dijkstra.RunBoundedDijkstra(NodeIndex(0), 2), + ElementsAre(NodeIndex(0))); + EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(0))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(0)), IsEmpty()); + + EXPECT_THAT(dijkstra.RunBoundedDijkstra(NodeIndex(1), 99), + ElementsAre(NodeIndex(1))); + EXPECT_EQ(NodeIndex(1), dijkstra.SourceOfShortestPathToNode(NodeIndex(1))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)), IsEmpty()); // Add some arcs and nodes... - graph.AddArc(0, 2); + graph.AddArc(NodeIndex(0), NodeIndex(2)); arc_lengths.push_back(1); - graph.AddArc(1, 2); + graph.AddArc(NodeIndex(1), NodeIndex(2)); arc_lengths.push_back(0); - graph.AddArc(2, 1); + graph.AddArc(NodeIndex(2), NodeIndex(1)); arc_lengths.push_back(1); - graph.AddArc(1, 3); + graph.AddArc(NodeIndex(1), NodeIndex(3)); arc_lengths.push_back(5); - EXPECT_THAT(dijkstra.RunBoundedDijkstra(0, 10), ElementsAre(0, 2, 1, 3)); - EXPECT_EQ(0, dijkstra.SourceOfShortestPathToNode(3)); - EXPECT_THAT(dijkstra.ArcPathTo(3), ElementsAre(2, 4, 5)); - - EXPECT_THAT(dijkstra.RunBoundedDijkstra(0, 6), ElementsAre(0, 2, 1)); - EXPECT_EQ(0, dijkstra.SourceOfShortestPathToNode(1)); - EXPECT_THAT(dijkstra.ArcPathTo(1), ElementsAre(2, 4)); + EXPECT_THAT( + dijkstra.RunBoundedDijkstra(NodeIndex(0), 10), + ElementsAre(NodeIndex(0), NodeIndex(2), NodeIndex(1), NodeIndex(3))); + EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(3))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(3)), + ElementsAre(ArcIndex(2), ArcIndex(4), ArcIndex(5))); + + EXPECT_THAT(dijkstra.RunBoundedDijkstra(NodeIndex(0), 6), + ElementsAre(NodeIndex(0), NodeIndex(2), NodeIndex(1))); + EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(1))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)), + ElementsAre(ArcIndex(2), ArcIndex(4))); } TEST(BoundedDijkstraWrapperTest, MultipleSources) { // Use this graph. Source nodes have their initial distance in [ ]. // // N1[0] --(2)--> N0[4] --(1)--> N2 --(5)--> N3 <--(4)-- N4[3] --(5)--> N5 - ListGraph<> graph; - std::vector arc_lengths; - graph.AddArc(1, 0); + TestGraph graph; + DijkstraWrapper::ByArc arc_lengths; + graph.AddArc(NodeIndex(1), NodeIndex(0)); arc_lengths.push_back(2); - graph.AddArc(0, 2); + graph.AddArc(NodeIndex(0), NodeIndex(2)); arc_lengths.push_back(1); - graph.AddArc(2, 3); + graph.AddArc(NodeIndex(2), NodeIndex(3)); arc_lengths.push_back(5); - graph.AddArc(4, 3); + graph.AddArc(NodeIndex(4), NodeIndex(3)); arc_lengths.push_back(4); - graph.AddArc(4, 5); + graph.AddArc(NodeIndex(4), NodeIndex(5)); arc_lengths.push_back(5); - BoundedDijkstraWrapper, int> dijkstra(&graph, &arc_lengths); + DijkstraWrapper dijkstra(&graph, &arc_lengths); // The distance limit is exclusive, so we can't reach Node 5. ASSERT_THAT(dijkstra.RunBoundedDijkstraFromMultipleSources( - {{1, 0}, {0, 4}, {4, 3}}, 8), + {{NodeIndex(1), 0}, {NodeIndex(0), 4}, {NodeIndex(4), 3}}, 8), // The order is deterministic: node 4 comes before node 2, despite // having equal distance and higher index, because it's a source. - ElementsAre(1, 0, 4, 2, 3)); - EXPECT_EQ(2, dijkstra.distances()[0]); - EXPECT_EQ(1, dijkstra.SourceOfShortestPathToNode(0)); - EXPECT_THAT(dijkstra.ArcPathTo(0), ElementsAre(0)); - EXPECT_EQ(0, dijkstra.distances()[1]); - EXPECT_EQ(1, dijkstra.SourceOfShortestPathToNode(1)); - EXPECT_THAT(dijkstra.ArcPathTo(1), IsEmpty()); - EXPECT_EQ(3, dijkstra.distances()[2]); - EXPECT_EQ(1, dijkstra.SourceOfShortestPathToNode(2)); - EXPECT_THAT(dijkstra.ArcPathTo(2), ElementsAre(0, 1)); - EXPECT_EQ(7, dijkstra.distances()[3]); - EXPECT_EQ(4, dijkstra.SourceOfShortestPathToNode(3)); - EXPECT_THAT(dijkstra.ArcPathTo(3), ElementsAre(3)); - EXPECT_EQ(3, dijkstra.distances()[4]); - EXPECT_EQ(4, dijkstra.SourceOfShortestPathToNode(4)); - EXPECT_THAT(dijkstra.ArcPathTo(4), IsEmpty()); + ElementsAre(NodeIndex(1), NodeIndex(0), NodeIndex(4), + NodeIndex(2), NodeIndex(3))); + EXPECT_EQ(2, dijkstra.distances()[NodeIndex(0)]); + EXPECT_EQ(NodeIndex(1), dijkstra.SourceOfShortestPathToNode(NodeIndex(0))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(0)), ElementsAre(ArcIndex(0))); + EXPECT_EQ(0, dijkstra.distances()[NodeIndex(1)]); + EXPECT_EQ(NodeIndex(1), dijkstra.SourceOfShortestPathToNode(NodeIndex(1))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)), IsEmpty()); + EXPECT_EQ(3, dijkstra.distances()[NodeIndex(2)]); + EXPECT_EQ(NodeIndex(1), dijkstra.SourceOfShortestPathToNode(NodeIndex(2))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(2)), + ElementsAre(ArcIndex(0), ArcIndex(1))); + EXPECT_EQ(7, dijkstra.distances()[NodeIndex(3)]); + EXPECT_EQ(NodeIndex(4), dijkstra.SourceOfShortestPathToNode(NodeIndex(3))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(3)), ElementsAre(ArcIndex(3))); + EXPECT_EQ(3, dijkstra.distances()[NodeIndex(4)]); + EXPECT_EQ(NodeIndex(4), dijkstra.SourceOfShortestPathToNode(NodeIndex(4))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(4)), IsEmpty()); } TEST(BoundedDijkstraWrapperTest, SourcesAtOrBeyondDistanceLimitAreNotReached) { - ListGraph<> graph(/*num_nodes=*/5, /*arc_capacity=*/0); - std::vector arc_lengths; // No arcs. - BoundedDijkstraWrapper, int> dijkstra(&graph, &arc_lengths); - EXPECT_THAT(dijkstra.RunBoundedDijkstraFromMultipleSources( - {{0, 10}, {1, 11}, {2, 12}, {3, 13}}, 12), - ElementsAre(0, 1)); + TestGraph graph(/*num_nodes=*/NodeIndex(5), /*arc_capacity=*/ArcIndex(0)); + DijkstraWrapper::ByArc arc_lengths; // No arcs. + DijkstraWrapper dijkstra(&graph, &arc_lengths); + EXPECT_THAT( + dijkstra.RunBoundedDijkstraFromMultipleSources({{NodeIndex(0), 10}, + {NodeIndex(1), 11}, + {NodeIndex(2), 12}, + {NodeIndex(3), 13}}, + 12), + ElementsAre(NodeIndex(0), NodeIndex(1))); } TEST(BoundedDijkstraWrapperTest, SourcesListedMultipleTimesKeepsMinDistance) { - ListGraph<> graph(/*num_nodes=*/5, /*arc_capacity=*/1); - graph.AddArc(1, 3); - std::vector arc_lengths = {20}; - BoundedDijkstraWrapper, int> dijkstra(&graph, &arc_lengths); - EXPECT_THAT(dijkstra.RunBoundedDijkstraFromMultipleSources( - {{1, 12}, {1, 10}, {1, 14}}, 31), - ElementsAre(1, 3)); - EXPECT_EQ(dijkstra.distances()[3], 30); + TestGraph graph(/*num_nodes=*/NodeIndex(5), /*arc_capacity=*/ArcIndex(1)); + graph.AddArc(NodeIndex(1), NodeIndex(3)); + DijkstraWrapper::ByArc arc_lengths = {20}; + DijkstraWrapper dijkstra(&graph, &arc_lengths); + EXPECT_THAT( + dijkstra.RunBoundedDijkstraFromMultipleSources( + {{NodeIndex(1), 12}, {NodeIndex(1), 10}, {NodeIndex(1), 14}}, 31), + ElementsAre(NodeIndex(1), NodeIndex(3))); + EXPECT_EQ(dijkstra.distances()[NodeIndex(3)], 30); } TEST(BoundedDijkstraWrapperTest, MultipleSourcesMultipleDestinations) { @@ -430,38 +464,45 @@ TEST(BoundedDijkstraWrapperTest, MultipleSourcesMultipleDestinations) { // `------(0)-----' // // The shortest path is S0->D1->N5->D4, of distance 2 + 3 + 1 + 1 + 1 = 8. - ListGraph<> graph; - std::vector arc_lengths; - graph.AddArc(0, 1); + TestGraph graph; + DijkstraWrapper::ByArc arc_lengths; + graph.AddArc(NodeIndex(0), NodeIndex(1)); arc_lengths.push_back(3); - graph.AddArc(2, 3); + graph.AddArc(NodeIndex(2), NodeIndex(3)); arc_lengths.push_back(3); - graph.AddArc(1, 5); + graph.AddArc(NodeIndex(1), NodeIndex(5)); arc_lengths.push_back(1); - graph.AddArc(3, 5); + graph.AddArc(NodeIndex(3), NodeIndex(5)); arc_lengths.push_back(0); - graph.AddArc(5, 3); + graph.AddArc(NodeIndex(5), NodeIndex(3)); arc_lengths.push_back(0); - graph.AddArc(5, 4); + graph.AddArc(NodeIndex(5), NodeIndex(4)); arc_lengths.push_back(1); - BoundedDijkstraWrapper, int> dijkstra(&graph, &arc_lengths); + DijkstraWrapper dijkstra(&graph, &arc_lengths); // Repeat the same source and destination multiple times, to verify that // it's supported. - std::vector> sources = {{0, 5}, {2, 4}, {0, 2}, {0, 9}}; - std::vector> destinations = { - {1, 7}, {4, 5}, {3, 3}, {4, 1}, {4, 3}}; + std::vector> sources = {{NodeIndex(0), 5}, + {NodeIndex(2), 4}, + {NodeIndex(0), 2}, + {NodeIndex(0), 9}}; + std::vector> destinations = {{NodeIndex(1), 7}, + {NodeIndex(4), 5}, + {NodeIndex(3), 3}, + {NodeIndex(4), 1}, + {NodeIndex(4), 3}}; EXPECT_THAT( dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations( sources, destinations, /*num_destinations_to_reach=*/1, /*distance_limit=*/1000), - Contains(4)); - EXPECT_EQ(2 + 3 + 1 + 1, dijkstra.distances()[4]); - EXPECT_EQ(0, dijkstra.SourceOfShortestPathToNode(4)); - EXPECT_THAT(dijkstra.ArcPathTo(4), - ElementsAre(/*0->1*/ 0, /*1->5*/ 2, /*5->4*/ 5)); - EXPECT_EQ(2, dijkstra.GetSourceIndex(0)); - EXPECT_EQ(3, dijkstra.GetDestinationIndex(4)); + Contains(NodeIndex(4))); + EXPECT_EQ(2 + 3 + 1 + 1, dijkstra.distances()[NodeIndex(4)]); + EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(4))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(4)), + ElementsAre(/*0->1*/ ArcIndex(0), /*1->5*/ ArcIndex(2), + /*5->4*/ ArcIndex(5))); + EXPECT_EQ(2, dijkstra.GetSourceIndex(NodeIndex(0))); + EXPECT_EQ(3, dijkstra.GetDestinationIndex(NodeIndex(4))); // Run it with a limit too small: it'll fail to discover any destination. EXPECT_THAT( @@ -475,18 +516,20 @@ TEST(BoundedDijkstraWrapperTest, MultipleSourcesMultipleDestinations) { dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations( sources, destinations, /*num_destinations_to_reach=*/2, /*distance_limit=*/9), // Limit is exclusive. - ElementsAre(4)); + ElementsAre(NodeIndex(4))); // Slightly modify the graph and try again. We want a case where the best // destination isn't the one with the smallest distance offset. - destinations.push_back({1, 2}); // D1 will be the closest destination now. + destinations.push_back( + {NodeIndex(1), 2}); // D1 will be the closest destination now. EXPECT_THAT( dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations( sources, destinations, /*num_destinations_to_reach=*/1, /*distance_limit=*/8), // Limit is exclusive. - ElementsAre(1)); - EXPECT_EQ(0, dijkstra.SourceOfShortestPathToNode(1)); - EXPECT_THAT(dijkstra.ArcPathTo(1), ElementsAre(/*0->1*/ 0)); + ElementsAre(NodeIndex(1))); + EXPECT_EQ(NodeIndex(0), dijkstra.SourceOfShortestPathToNode(NodeIndex(1))); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(1)), + ElementsAre(/*0->1*/ ArcIndex(0))); // Corner case: run with no destinations. EXPECT_THAT( @@ -505,8 +548,8 @@ TEST(BoundedDijkstraWrapperTest, MultipleSourcesMultipleDestinations) { // Call Get{Source,Destination}Index() on nodes that aren't sources or // destinations. This returns junk; so we don't check the returned values, // but we do check that it doesn't crash. - dijkstra.GetDestinationIndex(4); - dijkstra.GetSourceIndex(1); + dijkstra.GetDestinationIndex(NodeIndex(4)); + dijkstra.GetSourceIndex(NodeIndex(1)); // Setting num_reached_destinations=1 now should make '1' the only reachable // destination, even if the limit is infinite. @@ -514,85 +557,88 @@ TEST(BoundedDijkstraWrapperTest, MultipleSourcesMultipleDestinations) { dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations( sources, destinations, /*num_destinations_to_reach=*/1, /*distance_limit=*/1000), - ElementsAre(1)); + ElementsAre(NodeIndex(1))); // Verify that if we set the number of destinations to infinity, they're all // explored, and the search still stops before exploring the whole graph. To // do that, we add one extra arc that's beyond the farthest destination's // distance (including its destination offset), i.e. 1 (distance 2+3+7 = 12). - graph.AddArc(5, 6); + graph.AddArc(NodeIndex(5), NodeIndex(6)); arc_lengths.push_back(2); - graph.AddArc(6, 7); + graph.AddArc(NodeIndex(6), NodeIndex(7)); arc_lengths.push_back(0); EXPECT_THAT( dijkstra.RunBoundedDijkstraFromMultipleSourcesToMultipleDestinations( sources, destinations, /*num_destinations_to_reach=*/1000, /*distance_limit=*/1000), - ElementsAre(1, 4, 3)); - EXPECT_GE(dijkstra.distances()[1], 5); - EXPECT_GE(dijkstra.distances()[4], 7); - EXPECT_GE(dijkstra.distances()[3], 6); + ElementsAre(NodeIndex(1), NodeIndex(4), NodeIndex(3))); + EXPECT_GE(dijkstra.distances()[NodeIndex(1)], 5); + EXPECT_GE(dijkstra.distances()[NodeIndex(4)], 7); + EXPECT_GE(dijkstra.distances()[NodeIndex(3)], 6); // To verify that node #7 isn't reached, we can check its distance, which will // still be set to the initialized "distance_limit - min_destination_offset". - EXPECT_GE(dijkstra.distances()[7], 1000 - 1); + EXPECT_GE(dijkstra.distances()[NodeIndex(7)], 1000 - 1); } TEST(BoundedDijkstraWrapperTest, OneToOneShortestPath) { // Since we already tested the multiple sources - multiple destinations // variant, we only need to test the "plumbing" here. - ListGraph<> graph; - std::vector arc_lengths; - graph.AddArc(0, 1); + TestGraph graph; + DijkstraWrapper::ByArc arc_lengths; + graph.AddArc(NodeIndex(0), NodeIndex(1)); arc_lengths.push_back(3); - graph.AddArc(1, 2); + graph.AddArc(NodeIndex(1), NodeIndex(2)); arc_lengths.push_back(2); - BoundedDijkstraWrapper, int> dijkstra(&graph, &arc_lengths); + DijkstraWrapper dijkstra(&graph, &arc_lengths); - EXPECT_TRUE(dijkstra.OneToOneShortestPath(0, 2, 6)); - EXPECT_THAT(dijkstra.ArcPathTo(2), ElementsAre(0, 1)); + EXPECT_TRUE(dijkstra.OneToOneShortestPath(NodeIndex(0), NodeIndex(2), 6)); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(2)), + ElementsAre(ArcIndex(0), ArcIndex(1))); - EXPECT_TRUE(dijkstra.OneToOneShortestPath(0, 0, 1)); - EXPECT_THAT(dijkstra.ArcPathTo(0), ElementsAre()); + EXPECT_TRUE(dijkstra.OneToOneShortestPath(NodeIndex(0), NodeIndex(0), 1)); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(0)), ElementsAre()); - EXPECT_TRUE(dijkstra.OneToOneShortestPath(1, 2, 3)); - EXPECT_THAT(dijkstra.ArcPathTo(2), ElementsAre(1)); + EXPECT_TRUE(dijkstra.OneToOneShortestPath(NodeIndex(1), NodeIndex(2), 3)); + EXPECT_THAT(dijkstra.ArcPathTo(NodeIndex(2)), ElementsAre(ArcIndex(1))); - EXPECT_FALSE(dijkstra.OneToOneShortestPath(0, 2, 5)); - EXPECT_FALSE(dijkstra.OneToOneShortestPath(0, 0, 0)); - EXPECT_FALSE(dijkstra.OneToOneShortestPath(1, 2, 2)); - EXPECT_FALSE(dijkstra.OneToOneShortestPath(2, 1, 1000)); + EXPECT_FALSE(dijkstra.OneToOneShortestPath(NodeIndex(0), NodeIndex(2), 5)); + EXPECT_FALSE(dijkstra.OneToOneShortestPath(NodeIndex(0), NodeIndex(0), 0)); + EXPECT_FALSE(dijkstra.OneToOneShortestPath(NodeIndex(1), NodeIndex(2), 2)); + EXPECT_FALSE(dijkstra.OneToOneShortestPath(NodeIndex(2), NodeIndex(0), 1000)); } TEST(BoundedDijkstraWrapperTest, CustomSettledNodeCallback) { // A small chain: 8 --[3]--> 1 --[2]--> 42 --[3]--> 3 --[2]--> 4. - ListGraph<> graph; - std::vector arc_lengths; - graph.AddArc(8, 1); + TestGraph graph; + DijkstraWrapper::ByArc arc_lengths; + graph.AddArc(NodeIndex(8), NodeIndex(1)); arc_lengths.push_back(3); - graph.AddArc(1, 42); + graph.AddArc(NodeIndex(1), NodeIndex(42)); arc_lengths.push_back(2); - graph.AddArc(42, 3); + graph.AddArc(NodeIndex(42), NodeIndex(3)); arc_lengths.push_back(3); - graph.AddArc(3, 4); + graph.AddArc(NodeIndex(3), NodeIndex(4)); arc_lengths.push_back(2); - typedef BoundedDijkstraWrapper, int> DijkstraType; + typedef DijkstraWrapper DijkstraType; DijkstraType dijkstra(&graph, &arc_lengths); // Tracks each NodeDistance it's called on, and sets the distance limit // to 10 if it gets called on node 42. - std::vector> settled_node_dists; - auto callback = [&settled_node_dists](int node, int distance, + std::vector> settled_node_dists; + auto callback = [&settled_node_dists](NodeIndex node, int distance, int* distance_limit) { settled_node_dists.push_back({node, distance}); - if (node == 42) *distance_limit = 10; + if (node == NodeIndex(42)) *distance_limit = 10; }; - EXPECT_THAT(dijkstra.RunBoundedDijkstraWithSettledNodeCallback({{8, 0}}, - callback, 999), - ElementsAre(8, 1, 42, 3)); + EXPECT_THAT( + dijkstra.RunBoundedDijkstraWithSettledNodeCallback({{NodeIndex(8), 0}}, + callback, 999), + ElementsAre(NodeIndex(8), NodeIndex(1), NodeIndex(42), NodeIndex(3))); EXPECT_THAT(settled_node_dists, - ElementsAre(Pair(8, 0), Pair(1, 3), Pair(42, 5), Pair(3, 8))); + ElementsAre(Pair(NodeIndex(8), 0), Pair(NodeIndex(1), 3), + Pair(NodeIndex(42), 5), Pair(NodeIndex(3), 8))); } TEST(BoundedDisjktraTest, RandomizedStressTest) { @@ -601,49 +647,51 @@ TEST(BoundedDisjktraTest, RandomizedStressTest) { constexpr int kint32max = std::numeric_limits::max(); for (int test = 0; test < kNumTests; ++test) { // Generate a random graph with random weights. - const int num_nodes = absl::Uniform(random, 1, 12); - const int num_arcs = - absl::Uniform(absl::IntervalClosed, random, 0, - std::min(num_nodes * (num_nodes - 1), 15)); - ListGraph<> graph(num_nodes, num_arcs); - for (int a = 0; a < num_arcs; ++a) { - graph.AddArc(absl::Uniform(random, 0, num_nodes), - absl::Uniform(random, 0, num_nodes)); + const NodeIndex num_nodes(absl::Uniform(random, 1, 12)); + const ArcIndex num_arcs(absl::Uniform( + absl::IntervalClosed, random, 0, + std::min(num_nodes.value() * (num_nodes.value() - 1), 15))); + TestGraph graph(num_nodes, num_arcs); + for (ArcIndex a(0); a < num_arcs; ++a) { + graph.AddArc(NodeIndex(absl::Uniform(random, 0, num_nodes.value())), + NodeIndex(absl::Uniform(random, 0, num_nodes.value()))); } - std::vector lengths(num_arcs); + DijkstraWrapper::ByArc lengths(num_arcs); for (int& w : lengths) w = absl::Uniform(random, 0, 5); // Run Floyd-Warshall as a 'reference' shortest path algorithm. - FlatMatrix ref_dist(num_nodes, num_nodes, kint32max); - for (int a = 0; a < num_arcs; ++a) { - int& d = ref_dist[graph.Tail(a)][graph.Head(a)]; + FlatMatrix ref_dist(num_nodes.value(), num_nodes.value(), kint32max); + for (ArcIndex a(0); a < num_arcs; ++a) { + int& d = ref_dist[graph.Tail(a).value()][graph.Head(a).value()]; if (lengths[a] < d) d = lengths[a]; } - for (int node = 0; node < num_nodes; ++node) { - ref_dist[node][node] = 0; + for (NodeIndex node(0); node < num_nodes; ++node) { + ref_dist[node.value()][node.value()] = 0; } - for (int k = 0; k < num_nodes; ++k) { - for (int i = 0; i < num_nodes; ++i) { - for (int j = 0; j < num_nodes; ++j) { + for (NodeIndex k(0); k < num_nodes; ++k) { + for (NodeIndex i(0); i < num_nodes; ++i) { + for (NodeIndex j(0); j < num_nodes; ++j) { const int64_t dist_through_k = - static_cast(ref_dist[i][k]) + ref_dist[k][j]; - if (dist_through_k < ref_dist[i][j]) ref_dist[i][j] = dist_through_k; + static_cast(ref_dist[i.value()][k.value()]) + + ref_dist[k.value()][j.value()]; + if (dist_through_k < ref_dist[i.value()][j.value()]) + ref_dist[i.value()][j.value()] = dist_through_k; } } } // Compute the graph's largest distance below kint32max. int max_distance = 0; - for (int i = 0; i < num_nodes; ++i) { - for (int j = 0; j < num_nodes; ++j) { - const int d = ref_dist[i][j]; + for (NodeIndex i(0); i < num_nodes; ++i) { + for (NodeIndex j(0); j < num_nodes; ++j) { + const int d = ref_dist[i.value()][j.value()]; if (d != kint32max && d > max_distance) max_distance = d; } } // Now, run some Dijkstras and verify that they match. To balance out the // FW (Floyd-Warshall) which is O(N³), we run more than one Dijkstra per FW. - BoundedDijkstraWrapper, int> dijkstra(&graph, &lengths); + DijkstraWrapper dijkstra(&graph, &lengths); for (int num_dijkstra = 0; num_dijkstra < 20; ++num_dijkstra) { // Draw the distance limit. const int limit = @@ -652,33 +700,34 @@ TEST(BoundedDisjktraTest, RandomizedStressTest) { : absl::Uniform(absl::IntervalClosed, random, 0, max_distance); // Draw sources (*with* repetition) with initial distances. const int num_sources = absl::Uniform(random, 1, 5); - std::vector> sources(num_sources); + std::vector> sources(num_sources); for (auto& [s, dist] : sources) { - s = absl::Uniform(random, 0, num_nodes); + s = NodeIndex(absl::Uniform(random, 0, num_nodes.value())); dist = absl::Uniform(absl::IntervalClosed, random, 0, max_distance + 1); } // Precompute the reference minimum distance to each node (using any of // the sources), and the expected reached nodes: any node whose distance // is < limit. That includes the sources: if a source's initial distance // is ≥ limit, it won't be reached.That includes the source themselves. - std::vector node_min_dist(num_nodes, kint32max); - std::vector expected_reached_nodes; - for (int node = 0; node < num_nodes; ++node) { + DijkstraWrapper::ByNode node_min_dist(num_nodes, kint32max); + DijkstraWrapper::ByNode expected_reached_nodes; + for (NodeIndex node(0); node < num_nodes; ++node) { int min_dist = kint32max; for (const auto& [src, dist] : sources) { // Cast to int64_t to avoid overflows. min_dist = std::min( - min_dist, static_cast(ref_dist[src][node]) + dist); + min_dist, + static_cast(ref_dist[src.value()][node.value()]) + dist); } node_min_dist[node] = min_dist; if (min_dist < limit) expected_reached_nodes.push_back(node); } - const std::vector reached_nodes = + const auto reached_nodes = dijkstra.RunBoundedDijkstraFromMultipleSources(sources, limit); EXPECT_THAT(reached_nodes, UnorderedElementsAreArray(expected_reached_nodes)); - for (const int node : reached_nodes) { + for (const NodeIndex node : reached_nodes) { EXPECT_EQ(dijkstra.distances()[node], node_min_dist[node]) << node; } ASSERT_FALSE(HasFailure()) @@ -697,7 +746,8 @@ void BM_GridGraph(benchmark::State& state) { const int kSourceNode = static_cast(kWidth * kHeight / 2); std::unique_ptr graph = util::Create2DGridGraph(/*width=*/kWidth, /*height=*/kHeight); - std::vector arc_lengths(graph->num_arcs(), 0); + BoundedDijkstraWrapper::ByArc arc_lengths( + graph->num_arcs(), 0); const int64_t min_length = arc_lengths_are_discrete ? 0 : 1; const int64_t max_length = arc_lengths_are_discrete ? 2 : 1000000000000000L; std::mt19937 random(12345); diff --git a/ortools/graph/graph.h b/ortools/graph/graph.h index 73af4c944ad..db3f0e2bcb1 100644 --- a/ortools/graph/graph.h +++ b/ortools/graph/graph.h @@ -315,7 +315,7 @@ class BaseGraph { template class ArcPropertyIterator -#if __cplusplus < 202002L +#if __cplusplus < 201703L : public std::iterator #endif { @@ -324,6 +324,11 @@ class ArcPropertyIterator // TODO(b/385094969): This should be `NodeIndex` for integers, // `NodeIndex::value_type` for strong signed integer types. using difference_type = std::ptrdiff_t; +#if __cplusplus >= 201703L && __cplusplus < 202002L + using iterator_category = std::input_iterator_tag; + using pointer = PropertyT*; + using reference = PropertyT&; +#endif ArcPropertyIterator() = default; @@ -346,6 +351,7 @@ class ArcPropertyIterator const ArcPropertyIterator& r) { return l.arc_it_ == r.arc_it_; } + friend bool operator!=(const ArcPropertyIterator& l, const ArcPropertyIterator& r) { return !(l == r); @@ -420,6 +426,8 @@ class Vector : public std::vector { template class SVector { public: + using value_type = T; + SVector() : base_(nullptr), size_(0), capacity_(0) {} ~SVector() { clear_and_dealloc(); } @@ -434,7 +442,7 @@ class SVector { capacity_ = other.size_; base_ = Allocate(capacity_); CHECK(base_ != nullptr); - base_ += capacity_; + base_ += static_cast(capacity_); } else { // capacity_ >= other.size clear(); } @@ -488,6 +496,9 @@ class SVector { T* data() const { return base_; } + const T* begin() const { return base_; } + const T* end() const { return base_ + static_cast(size_); } + void swap(SVector& x) noexcept { std::swap(base_, x.base_); std::swap(size_, x.size_); @@ -564,8 +575,9 @@ class SVector { // Copies other.base_ to base_ in this SVector. Safe for all types as it uses // constructor for each entry. void CopyInternal(const SVector& other, std::false_type) { - for (int i = -size_; i < size_; ++i) { - new (base_ + i) T(other.base_[i]); + for (IndexT i = -size_; i < size_; ++i) { + new (base_ + static_cast(i)) + T(other.base_[static_cast(i)]); } } @@ -1091,41 +1103,21 @@ class ReverseArcStaticGraph // TODO(user): consider slower but more memory efficient implementations that // follow the cycles of the permutation and use a bitmap to indicate what has // been permuted or to mark the beginning of each cycle. - -// Some compiler do not know typeof(), so we have to use this extra function -// internally. -template -void PermuteWithExplicitElementType(const IntVector& permutation, - Array& array_to_permute, - ElementType unused) { - std::vector temp(permutation.size()); - for (size_t i = 0; i < permutation.size(); ++i) { - temp[i] = array_to_permute[i]; - } - for (size_t i = 0; i < permutation.size(); ++i) { - array_to_permute[static_cast(permutation[i])] = temp[i]; - } -} - template void Permute(const IntVector& permutation, Array* array_to_permute) { if (permutation.empty()) { return; } - PermuteWithExplicitElementType(permutation, *array_to_permute, - (*array_to_permute)[0]); -} - -// We need a specialization for vector, because the default code uses -// (*array_to_permute)[0] as ElementType, which isn't 'bool' in that case. -template -void Permute(const IntVector& permutation, - std::vector* array_to_permute) { - if (permutation.empty()) { - return; + const auto size = permutation.size(); + auto& array = *array_to_permute; + using ElementType = + typename std::iterator_traits::value_type; + std::vector temp(size); + auto array_begin = std::begin(array); + std::copy_n(array_begin, size, temp.begin()); + for (size_t i = 0; i < permutation.size(); ++i) { + *(array_begin + static_cast(permutation[i])) = temp[i]; } - bool unused = false; - PermuteWithExplicitElementType(permutation, *array_to_permute, unused); } // BaseGraph implementation ---------------------------------------------------- diff --git a/ortools/graph/graph_io.h b/ortools/graph/graph_io.h index ffe455028e9..82a2002a5e5 100644 --- a/ortools/graph/graph_io.h +++ b/ortools/graph/graph_io.h @@ -97,12 +97,12 @@ std::string GraphToString(const Graph& graph, GraphToStringFormat format) { } else { // PRINT_GRAPH_ADJACENCY_LISTS[_SORTED] adj.clear(); for (const typename Graph::ArcIndex arc : graph.OutgoingArcs(node)) { - adj.push_back(graph.Head(arc)); + adj.push_back(static_cast(graph.Head(arc))); } if (format == PRINT_GRAPH_ADJACENCY_LISTS_SORTED) { std::sort(adj.begin(), adj.end()); } - if (node != 0) out += '\n'; + if (node != typename Graph::NodeIndex(0)) out += '\n'; absl::StrAppend(&out, static_cast(node), ": ", absl::StrJoin(adj, " ")); } diff --git a/ortools/graph/graph_test.cc b/ortools/graph/graph_test.cc index 29e85240e28..e6901973853 100644 --- a/ortools/graph/graph_test.cc +++ b/ortools/graph/graph_test.cc @@ -24,6 +24,7 @@ #include #include +#include "absl/algorithm/container.h" #include "absl/log/check.h" #include "absl/random/random.h" #include "absl/strings/str_cat.h" @@ -32,9 +33,11 @@ #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/base/strong_int.h" +#include "ortools/base/strong_vector.h" namespace util { +using testing::ElementsAre; using testing::Pair; using testing::UnorderedElementsAre; @@ -289,98 +292,144 @@ void ConstructAndCheckGraph( // Return the size of the memory block allocated by malloc when asking for x // bytes. -inline int UpperBoundOfMallocBlockSizeOf(int x) { +template +inline IndexType UpperBoundOfMallocBlockSizeOf(IndexType x) { // Note(user): as of 2012-09, the rule seems to be: round x up to the // next multiple of 16. // WARNING: This may change, and may already be wrong for small values. - return 16 * ((x + 15) / 16); + return IndexType((16 * (static_cast(x) + 15)) / 16); } -TEST(SVectorTest, DynamicGrowth) { - internal::SVector v; - EXPECT_EQ(0, v.size()); - EXPECT_EQ(0, v.capacity()); - for (int i = 0; i < 100; i++) { +template +class SVectorTest : public ::testing::Test {}; + +typedef ::testing::Types, std::pair, + std::pair, + std::pair> + TestSVectorIndexTypes; + +TYPED_TEST_SUITE(SVectorTest, TestSVectorIndexTypes); + +TYPED_TEST(SVectorTest, CopyMoveIterate) { + using IndexT = typename TypeParam::first_type; + using ValueT = typename TypeParam::second_type; + using VectorT = internal::SVector; + VectorT v; + v.resize(IndexT(2)); + v[IndexT(0)] = ValueT(1); + v[IndexT(1)] = ValueT(2); + + { + EXPECT_THAT(VectorT(v), ElementsAre(ValueT(1), ValueT(2))); + VectorT v2 = v; + EXPECT_THAT(v2, ElementsAre(ValueT(1), ValueT(2))); + EXPECT_THAT(v, ElementsAre(ValueT(1), ValueT(2))); + } + + { + VectorT v2 = std::move(v); + EXPECT_THAT(v2, ElementsAre(ValueT(1), ValueT(2))); + EXPECT_THAT(VectorT(std::move(v2)), ElementsAre(ValueT(1), ValueT(2))); + } +} + +TYPED_TEST(SVectorTest, DynamicGrowth) { + using IndexT = typename TypeParam::first_type; + using ValueT = typename TypeParam::second_type; + internal::SVector v; + EXPECT_EQ(IndexT(0), v.size()); + EXPECT_EQ(IndexT(0), v.capacity()); + for (ValueT i(0); i < ValueT(100); i++) { v.grow(-i, i); } - EXPECT_EQ(100, v.size()); - EXPECT_GE(v.capacity(), 100); - EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(100)); - for (int i = 0; i < 100; i++) { - EXPECT_EQ(-i, v[~i]); - EXPECT_EQ(i, v[i]); + EXPECT_EQ(IndexT(100), v.size()); + EXPECT_GE(v.capacity(), IndexT(100)); + EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(IndexT(100))); + for (IndexT i(0); i < IndexT(100); ++i) { + EXPECT_EQ(ValueT(static_cast(-i)), v[~i]); + EXPECT_EQ(ValueT(static_cast(i)), v[i]); } } -TEST(SVectorTest, Reserve) { - internal::SVector v; - v.reserve(100); - EXPECT_EQ(0, v.size()); - EXPECT_GE(v.capacity(), 100); - EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(100)); - for (int i = 0; i < 100; i++) { +TYPED_TEST(SVectorTest, Reserve) { + using IndexT = typename TypeParam::first_type; + using ValueT = typename TypeParam::second_type; + internal::SVector v; + v.reserve(IndexT(100)); + EXPECT_EQ(IndexT(0), v.size()); + EXPECT_GE(v.capacity(), IndexT(100)); + EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(IndexT(100))); + for (ValueT i(0); i < ValueT(100); i++) { v.grow(-i, i); } - EXPECT_EQ(100, v.size()); - EXPECT_GE(v.capacity(), 100); - EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(100)); - for (int i = 0; i < 10; i++) { - EXPECT_EQ(-i, v[~i]); - EXPECT_EQ(i, v[i]); + EXPECT_EQ(IndexT(100), v.size()); + EXPECT_GE(v.capacity(), IndexT(100)); + EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(IndexT(100))); + for (IndexT i(0); i < IndexT(10); i++) { + EXPECT_EQ(ValueT(static_cast(-i)), v[~i]); + EXPECT_EQ(ValueT(static_cast(i)), v[i]); } } -TEST(SVectorTest, Resize) { - internal::SVector v; - v.resize(100); - EXPECT_EQ(100, v.size()); - EXPECT_GE(v.capacity(), 100); - EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(100)); - for (int i = 0; i < 100; i++) { - EXPECT_EQ(0, v[-i - 1]); - EXPECT_EQ(0, v[i]); +TYPED_TEST(SVectorTest, Resize) { + using IndexT = typename TypeParam::first_type; + using ValueT = typename TypeParam::second_type; + internal::SVector v; + v.resize(IndexT(100)); + EXPECT_EQ(IndexT(100), v.size()); + EXPECT_GE(v.capacity(), IndexT(100)); + EXPECT_LE(v.capacity(), UpperBoundOfMallocBlockSizeOf(IndexT(100))); + for (IndexT i(0); i < IndexT(100); ++i) { + EXPECT_EQ(ValueT(0), v[-i - IndexT(1)]); + EXPECT_EQ(ValueT(0), v[i]); } } -TEST(SVectorTest, ResizeToZero) { - internal::SVector s; - s.resize(1); - s.resize(0); - EXPECT_EQ(0, s.size()); +TYPED_TEST(SVectorTest, ResizeToZero) { + using IndexT = typename TypeParam::first_type; + using ValueT = typename TypeParam::second_type; + internal::SVector v; + v.resize(IndexT(1)); + v.resize(IndexT(0)); + EXPECT_EQ(IndexT(0), v.size()); } -TEST(SVectorTest, Swap) { - internal::SVector s; - internal::SVector t; - s.resize(1); - s[0] = 's'; - s[-1] = 's'; - t.resize(2); - for (int i = -2; i <= 1; ++i) { - t[i] = 't'; +TYPED_TEST(SVectorTest, Swap) { + using IndexT = typename TypeParam::first_type; + using ValueT = typename TypeParam::second_type; + internal::SVector s; + internal::SVector t; + s.resize(IndexT(1)); + s[IndexT(0)] = ValueT('s'); + s[IndexT(-1)] = ValueT('s'); + t.resize(IndexT(2)); + for (IndexT i(-2); i <= IndexT(1); ++i) { + t[i] = ValueT('t'); } s.swap(t); - EXPECT_EQ(1, t.size()); - EXPECT_EQ('s', t[-1]); - EXPECT_EQ('s', t[0]); - EXPECT_EQ(2, s.size()); - EXPECT_EQ('t', s[-2]); - EXPECT_EQ('t', s[-1]); - EXPECT_EQ('t', s[0]); - EXPECT_EQ('t', s[1]); + EXPECT_EQ(IndexT(1), t.size()); + EXPECT_EQ(ValueT('s'), t[IndexT(-1)]); + EXPECT_EQ(ValueT('s'), t[IndexT(0)]); + EXPECT_EQ(IndexT(2), s.size()); + EXPECT_EQ(ValueT('t'), s[IndexT(-2)]); + EXPECT_EQ(ValueT('t'), s[IndexT(-1)]); + EXPECT_EQ(ValueT('t'), s[IndexT(0)]); + EXPECT_EQ(ValueT('t'), s[IndexT(1)]); } -TEST(SVectorTest, SwapAndDestroy) { - internal::SVector s; +TYPED_TEST(SVectorTest, SwapAndDestroy) { + using IndexT = typename TypeParam::first_type; + using ValueT = typename TypeParam::second_type; + internal::SVector s; { - internal::SVector t; - t.resize(2); - t[-2] = 42; + internal::SVector t; + t.resize(IndexT(2)); + t[IndexT(-2)] = ValueT(42); t.swap(s); } - EXPECT_EQ(2, s.size()); - EXPECT_EQ(42, s[-2]); - EXPECT_EQ(0, s[1]); + EXPECT_EQ(IndexT(2), s.size()); + EXPECT_EQ(ValueT(42), s[IndexT(-2)]); + EXPECT_EQ(ValueT(0), s[IndexT(1)]); } // Use a more complex type to better check the invocations of @@ -458,7 +507,7 @@ class MoveOnlyObject { int MoveOnlyObject::sequence_ = 1; int MoveOnlyObject::object_count_ = 0; -TEST(SVectorTest, MoveWithMoveOnlyObject) { +TEST(SVectorMoveOnlyTest, MoveWithMoveOnlyObject) { EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); internal::SVector a; a.resize(10); @@ -472,7 +521,7 @@ TEST(SVectorTest, MoveWithMoveOnlyObject) { EXPECT_EQ(0, a.size()); // NOLINT } -TEST(SVectorTest, ShrinkWithMoveOnlyObject) { +TEST(SVectorMoveOnlyTest, ShrinkWithMoveOnlyObject) { EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); { internal::SVector a; @@ -484,7 +533,7 @@ TEST(SVectorTest, ShrinkWithMoveOnlyObject) { EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); } -TEST(SVectorTest, GrowMoveOnlyObject) { +TEST(SVectorMoveOnlyTest, GrowMoveOnlyObject) { EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); { internal::SVector a; @@ -501,7 +550,7 @@ TEST(SVectorTest, GrowMoveOnlyObject) { EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); } -TEST(SVectorTest, ReserveMoveOnlyObject) { +TEST(SVectorMoveOnlyTest, ReserveMoveOnlyObject) { EXPECT_EQ(0, MoveOnlyObject::GetObjectCount()); { internal::SVector a; @@ -554,7 +603,7 @@ int TrackedObject::num_destructions = 0; int TrackedObject::num_moves = 0; int TrackedObject::num_copies = 0; -TEST(SVectorTest, CopyConstructor) { +TEST(SVectorTrackingTest, CopyConstructor) { TrackedObject::ResetCounters(); ASSERT_EQ(TrackedObject::Counters(), "constructions: 0, destructions: 0, moves: 0, copies: 0"); @@ -573,7 +622,7 @@ TEST(SVectorTest, CopyConstructor) { ASSERT_EQ(v_copy.size(), 5); } -TEST(SVectorTest, AssignmentOperator) { +TEST(SVectorTrackingTest, AssignmentOperator) { TrackedObject::ResetCounters(); ASSERT_EQ(TrackedObject::Counters(), "constructions: 0, destructions: 0, moves: 0, copies: 0"); @@ -595,7 +644,7 @@ TEST(SVectorTest, AssignmentOperator) { ASSERT_EQ(other.size(), 5); } -TEST(SVectorTest, CopyConstructorIntegralType) { +TEST(SVectorTrackingTest, CopyConstructorIntegralType) { auto v = internal::SVector(); v.resize(3); v[-3] = 1; @@ -613,7 +662,7 @@ TEST(SVectorTest, CopyConstructorIntegralType) { } } -TEST(SVectorTest, AssignmentOperatorIntegralType) { +TEST(SVectorTrackingTest, AssignmentOperatorIntegralType) { internal::SVector other; auto v = internal::SVector(); v.resize(3); @@ -632,7 +681,7 @@ TEST(SVectorTest, AssignmentOperatorIntegralType) { } } -TEST(SVectorTest, MoveConstructor) { +TEST(SVectorTrackingTest, MoveConstructor) { TrackedObject::ResetCounters(); ASSERT_EQ(TrackedObject::Counters(), "constructions: 0, destructions: 0, moves: 0, copies: 0"); @@ -650,7 +699,7 @@ TEST(SVectorTest, MoveConstructor) { ASSERT_EQ(b.size(), 5); } -TEST(SVectorTest, MoveAssignmentOperator) { +TEST(SVectorTrackingTest, MoveAssignmentOperator) { TrackedObject::ResetCounters(); ASSERT_EQ(TrackedObject::Counters(), "constructions: 0, destructions: 0, moves: 0, copies: 0"); @@ -1011,6 +1060,28 @@ TEST(SVector, NoHeapCheckerFalsePositive) { EXPECT_EQ(kVector->size(), 5000); } +TEST(Permute, IntArray) { + int array[] = {4, 5, 6}; + std::vector permutation = {0, 2, 1}; + util::Permute(permutation, &array); + EXPECT_THAT(array, ElementsAre(4, 6, 5)); +} + +TEST(Permute, BoolVector) { + std::vector array = {true, false, true}; + std::vector permutation = {0, 2, 1}; + util::Permute(permutation, &array); + EXPECT_THAT(array, ElementsAre(true, true, false)); +} + +TEST(Permute, StrongVector) { + util_intops::StrongVector array = {4, 5, 6}; + std::vector permutation = {StrongArcId(0), StrongArcId(2), + StrongArcId(1)}; + util::Permute(permutation, &array); + EXPECT_THAT(array, ElementsAre(4, 6, 5)); +} + template static void BM_RandomArcs(benchmark::State& state) { const int kRandomSeed = 0; @@ -1304,4 +1375,23 @@ static void BM_CompleteBipartiteGraphTailHead(benchmark::State& state) { BENCHMARK_TEMPLATE(BM_CompleteBipartiteGraphTailHead, int32_t); BENCHMARK_TEMPLATE(BM_CompleteBipartiteGraphTailHead, int16_t); +template +void BM_Permute(benchmark::State& state) { + const int size = state.range(0); + ArrayT array(size); + + std::vector permutation(size); + absl::c_iota(permutation, IndexT(0)); + + for (const auto s : state) { + util::Permute(permutation, &array); + benchmark::DoNotOptimize(array); + benchmark::DoNotOptimize(permutation); + } +} +BENCHMARK(BM_Permute, StrongArcId>) + ->Arg(128); +BENCHMARK(BM_Permute, int>)->Arg(128); +BENCHMARK(BM_Permute, int>)->Arg(128); + } // namespace util diff --git a/ortools/graph/iterators.h b/ortools/graph/iterators.h index 73f67a07bdc..50fd5335b07 100644 --- a/ortools/graph/iterators.h +++ b/ortools/graph/iterators.h @@ -124,13 +124,18 @@ class IntegerRangeIterator // TODO(b/385094969): In C++17, `std::iterator_traits` required // explicitly specifying the iterator category. Remove this when backwards // compatibility with C++17 is no longer needed. -#if __cplusplus < 202002L +#if __cplusplus < 201703L : public std::iterator #endif { public: using difference_type = ptrdiff_t; using value_type = IntegerType; +#if __cplusplus >= 201703L && __cplusplus < 202002L + using iterator_category = std::input_iterator_tag; + using pointer = IntegerType*; + using reference = IntegerType&; +#endif IntegerRangeIterator() : index_{} {} @@ -243,13 +248,18 @@ class IntegerRange : public BeginEndWrapper> { // different iterators with the same index type and sentinel. template class ChasingIterator -#if __cplusplus < 202002L +#if __cplusplus < 201703L : public std::iterator #endif { public: using difference_type = ptrdiff_t; using value_type = IndexT; +#if __cplusplus >= 201703L && __cplusplus < 202002L + using iterator_category = std::input_iterator_tag; + using pointer = IndexT*; + using reference = IndexT&; +#endif ChasingIterator() : index_(sentinel), next_(nullptr) {} diff --git a/ortools/graph/samples/assignment_linear_sum_assignment.py b/ortools/graph/samples/assignment_linear_sum_assignment.py index 82af30d5607..178d65d230a 100755 --- a/ortools/graph/samples/assignment_linear_sum_assignment.py +++ b/ortools/graph/samples/assignment_linear_sum_assignment.py @@ -18,58 +18,57 @@ import numpy as np from ortools.graph.python import linear_sum_assignment + # [END import] def main(): - """Linear Sum Assignment example.""" - # [START solver] - assignment = linear_sum_assignment.SimpleLinearSumAssignment() - # [END solver] + """Linear Sum Assignment example.""" + # [START solver] + assignment = linear_sum_assignment.SimpleLinearSumAssignment() + # [END solver] - # [START data] - costs = np.array( - [ - [90, 76, 75, 70], - [35, 85, 55, 65], - [125, 95, 90, 105], - [45, 110, 95, 115], - ] - ) + # [START data] + costs = np.array([ + [90, 76, 75, 70], + [35, 85, 55, 65], + [125, 95, 90, 105], + [45, 110, 95, 115], + ]) - # Let's transform this into 3 parallel vectors (start_nodes, end_nodes, - # arc_costs) - end_nodes_unraveled, start_nodes_unraveled = np.meshgrid( - np.arange(costs.shape[1]), np.arange(costs.shape[0]) - ) - start_nodes = start_nodes_unraveled.ravel() - end_nodes = end_nodes_unraveled.ravel() - arc_costs = costs.ravel() - # [END data] + # Let's transform this into 3 parallel vectors (start_nodes, end_nodes, + # arc_costs) + end_nodes_unraveled, start_nodes_unraveled = np.meshgrid( + np.arange(costs.shape[1]), np.arange(costs.shape[0]) + ) + start_nodes = start_nodes_unraveled.ravel() + end_nodes = end_nodes_unraveled.ravel() + arc_costs = costs.ravel() + # [END data] - # [START constraints] - assignment.add_arcs_with_cost(start_nodes, end_nodes, arc_costs) - # [END constraints] + # [START constraints] + assignment.add_arcs_with_cost(start_nodes, end_nodes, arc_costs) + # [END constraints] - # [START solve] - status = assignment.solve() - # [END solve] + # [START solve] + status = assignment.solve() + # [END solve] - # [START print_solution] - if status == assignment.OPTIMAL: - print(f"Total cost = {assignment.optimal_cost()}\n") - for i in range(0, assignment.num_nodes()): - print( - f"Worker {i} assigned to task {assignment.right_mate(i)}." - + f" Cost = {assignment.assignment_cost(i)}" - ) - elif status == assignment.INFEASIBLE: - print("No assignment is possible.") - elif status == assignment.POSSIBLE_OVERFLOW: - print("Some input costs are too large and may cause an integer overflow.") - # [END print_solution] + # [START print_solution] + if status == assignment.OPTIMAL: + print(f"Total cost = {assignment.optimal_cost()}\n") + for i in range(0, assignment.num_nodes()): + print( + f"Worker {i} assigned to task {assignment.right_mate(i)}." + + f" Cost = {assignment.assignment_cost(i)}" + ) + elif status == assignment.INFEASIBLE: + print("No assignment is possible.") + elif status == assignment.POSSIBLE_OVERFLOW: + print("Some input costs are too large and may cause an integer overflow.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END Program] diff --git a/ortools/graph/samples/assignment_min_flow.py b/ortools/graph/samples/assignment_min_flow.py index 0d55ed20c94..47ac7b01ec3 100755 --- a/ortools/graph/samples/assignment_min_flow.py +++ b/ortools/graph/samples/assignment_min_flow.py @@ -16,75 +16,84 @@ """Linear assignment example.""" # [START import] from ortools.graph.python import min_cost_flow + # [END import] def main(): - """Solving an Assignment Problem with MinCostFlow.""" - # [START solver] - # Instantiate a SimpleMinCostFlow solver. - smcf = min_cost_flow.SimpleMinCostFlow() - # [END solver] + """Solving an Assignment Problem with MinCostFlow.""" + # [START solver] + # Instantiate a SimpleMinCostFlow solver. + smcf = min_cost_flow.SimpleMinCostFlow() + # [END solver] - # [START data] - # Define the directed graph for the flow. - start_nodes = ( - [0, 0, 0, 0] + [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] + [5, 6, 7, 8] - ) - end_nodes = ( - [1, 2, 3, 4] + [5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8] + [9, 9, 9, 9] - ) - capacities = ( - [1, 1, 1, 1] + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + [1, 1, 1, 1] - ) - costs = ( - [0, 0, 0, 0] - + [90, 76, 75, 70, 35, 85, 55, 65, 125, 95, 90, 105, 45, 110, 95, 115] - + [0, 0, 0, 0] - ) + # [START data] + # Define the directed graph for the flow. + start_nodes = ( + [0, 0, 0, 0] + + [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4] + + [5, 6, 7, 8] + ) + end_nodes = ( + [1, 2, 3, 4] + + [5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8, 5, 6, 7, 8] + + [9, 9, 9, 9] + ) + capacities = ( + [1, 1, 1, 1] + + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + + [1, 1, 1, 1] + ) + costs = ( + [0, 0, 0, 0] + + [90, 76, 75, 70, 35, 85, 55, 65, 125, 95, 90, 105, 45, 110, 95, 115] + + [0, 0, 0, 0] + ) - source = 0 - sink = 9 - tasks = 4 - supplies = [tasks, 0, 0, 0, 0, 0, 0, 0, 0, -tasks] - # [END data] + source = 0 + sink = 9 + tasks = 4 + supplies = [tasks, 0, 0, 0, 0, 0, 0, 0, 0, -tasks] + # [END data] - # [START constraints] - # Add each arc. - for start_node, end_node, capacity, cost in zip( - start_nodes, end_nodes, capacities, costs - ): - smcf.add_arc_with_capacity_and_unit_cost(start_node, end_node, capacity, cost) - # Add node supplies. - for idx, supply in enumerate(supplies): - smcf.set_node_supply(idx, supply) - # [END constraints] + # [START constraints] + # Add each arc. + for start_node, end_node, capacity, cost in zip( + start_nodes, end_nodes, capacities, costs + ): + smcf.add_arc_with_capacity_and_unit_cost( + start_node, end_node, capacity, cost + ) + # Add node supplies. + for idx, supply in enumerate(supplies): + smcf.set_node_supply(idx, supply) + # [END constraints] - # [START solve] - # Find the minimum cost flow between node 0 and node 10. - status = smcf.solve() - # [END solve] + # [START solve] + # Find the minimum cost flow between node 0 and node 10. + status = smcf.solve() + # [END solve] - # [START print_solution] - if status == smcf.OPTIMAL: - print(f"Total cost = {smcf.optimal_cost()}") - for arc in range(smcf.num_arcs()): - # Can ignore arcs leading out of source or into sink. - if smcf.tail(arc) != source and smcf.head(arc) != sink: + # [START print_solution] + if status == smcf.OPTIMAL: + print(f"Total cost = {smcf.optimal_cost()}") + for arc in range(smcf.num_arcs()): + # Can ignore arcs leading out of source or into sink. + if smcf.tail(arc) != source and smcf.head(arc) != sink: - # Arcs in the solution have a flow value of 1. Their start and end nodes - # give an assignment of worker to task. - if smcf.flow(arc) > 0: - print( - f"Worker {smcf.tail(arc)} assigned to task {smcf.head(arc)}. " - f"Cost = {smcf.unit_cost(arc)}" - ) - else: - print("There was an issue with the min cost flow input.") - print(f"Status: {status}") - # [END print_solution] + # Arcs in the solution have a flow value of 1. Their start and end nodes + # give an assignment of worker to task. + if smcf.flow(arc) > 0: + print( + f"Worker {smcf.tail(arc)} assigned to task {smcf.head(arc)}. " + f"Cost = {smcf.unit_cost(arc)}" + ) + else: + print("There was an issue with the min cost flow input.") + print(f"Status: {status}") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/graph/samples/balance_min_flow.py b/ortools/graph/samples/balance_min_flow.py index 923ff22a859..5514a5a9448 100755 --- a/ortools/graph/samples/balance_min_flow.py +++ b/ortools/graph/samples/balance_min_flow.py @@ -16,107 +16,108 @@ """Assignment with teams of workers.""" # [START import] from ortools.graph.python import min_cost_flow + # [END import] def main(): - """Solving an Assignment with teams of worker.""" - # [START solver] - smcf = min_cost_flow.SimpleMinCostFlow() - # [END solver] + """Solving an Assignment with teams of worker.""" + # [START solver] + smcf = min_cost_flow.SimpleMinCostFlow() + # [END solver] - # [START data] - # Define the directed graph for the flow. - team_a = [1, 3, 5] - team_b = [2, 4, 6] + # [START data] + # Define the directed graph for the flow. + team_a = [1, 3, 5] + team_b = [2, 4, 6] - start_nodes = ( - # fmt: off + start_nodes = ( + # fmt: off [0, 0] + [11, 11, 11] + [12, 12, 12] + [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6] + [7, 8, 9, 10] - # fmt: on - ) - end_nodes = ( - # fmt: off + # fmt: on + ) + end_nodes = ( + # fmt: off [11, 12] + team_a + team_b + [7, 8, 9, 10, 7, 8, 9, 10, 7, 8, 9, 10, 7, 8, 9, 10, 7, 8, 9, 10, 7, 8, 9, 10] + [13, 13, 13, 13] - # fmt: on - ) - capacities = ( - # fmt: off + # fmt: on + ) + capacities = ( + # fmt: off [2, 2] + [1, 1, 1] + [1, 1, 1] + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] + [1, 1, 1, 1] - # fmt: on - ) - costs = ( - # fmt: off + # fmt: on + ) + costs = ( + # fmt: off [0, 0] + [0, 0, 0] + [0, 0, 0] + [90, 76, 75, 70, 35, 85, 55, 65, 125, 95, 90, 105, 45, 110, 95, 115, 60, 105, 80, 75, 45, 65, 110, 95] + [0, 0, 0, 0] - # fmt: on - ) + # fmt: on + ) - source = 0 - sink = 13 - tasks = 4 - # Define an array of supplies at each node. - supplies = [tasks, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -tasks] - # [END data] + source = 0 + sink = 13 + tasks = 4 + # Define an array of supplies at each node. + supplies = [tasks, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -tasks] + # [END data] - # [START constraints] - # Add each arc. - for i in range(0, len(start_nodes)): - smcf.add_arc_with_capacity_and_unit_cost( - start_nodes[i], end_nodes[i], capacities[i], costs[i] - ) + # [START constraints] + # Add each arc. + for i in range(0, len(start_nodes)): + smcf.add_arc_with_capacity_and_unit_cost( + start_nodes[i], end_nodes[i], capacities[i], costs[i] + ) - # Add node supplies. - for i in range(0, len(supplies)): - smcf.set_node_supply(i, supplies[i]) - # [END constraints] + # Add node supplies. + for i in range(0, len(supplies)): + smcf.set_node_supply(i, supplies[i]) + # [END constraints] - # [START solve] - # Find the minimum cost flow between node 0 and node 10. - status = smcf.solve() - # [END solve] + # [START solve] + # Find the minimum cost flow between node 0 and node 10. + status = smcf.solve() + # [END solve] - # [START print_solution] - if status == smcf.OPTIMAL: - print("Total cost = ", smcf.optimal_cost()) - print() - for arc in range(smcf.num_arcs()): - # Can ignore arcs leading out of source or intermediate, or into sink. - if ( - smcf.tail(arc) != source - and smcf.tail(arc) != 11 - and smcf.tail(arc) != 12 - and smcf.head(arc) != sink - ): + # [START print_solution] + if status == smcf.OPTIMAL: + print("Total cost = ", smcf.optimal_cost()) + print() + for arc in range(smcf.num_arcs()): + # Can ignore arcs leading out of source or intermediate, or into sink. + if ( + smcf.tail(arc) != source + and smcf.tail(arc) != 11 + and smcf.tail(arc) != 12 + and smcf.head(arc) != sink + ): - # Arcs in the solution will have a flow value of 1. - # There start and end nodes give an assignment of worker to task. - if smcf.flow(arc) > 0: - print( - "Worker %d assigned to task %d. Cost = %d" - % (smcf.tail(arc), smcf.head(arc), smcf.unit_cost(arc)) - ) - else: - print("There was an issue with the min cost flow input.") - print(f"Status: {status}") - # [END print_solution] + # Arcs in the solution will have a flow value of 1. + # There start and end nodes give an assignment of worker to task. + if smcf.flow(arc) > 0: + print( + "Worker %d assigned to task %d. Cost = %d" + % (smcf.tail(arc), smcf.head(arc), smcf.unit_cost(arc)) + ) + else: + print("There was an issue with the min cost flow input.") + print(f"Status: {status}") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/graph/samples/dijkstra_directed.cc b/ortools/graph/samples/dijkstra_directed.cc index 046f15d1d94..20c8a508c82 100644 --- a/ortools/graph/samples/dijkstra_directed.cc +++ b/ortools/graph/samples/dijkstra_directed.cc @@ -50,8 +50,8 @@ int main(int argc, char** argv) { // Solve the shortest path problem from 0 to 5. std::pair> result = - operations_research::SimpleOneToOneShortestPath(0, 5, tails, heads, - lengths); + operations_research::SimpleOneToOneShortestPath(0, 5, tails, + heads, lengths); // Print to length of the path and then the nodes in the path. std::cout << "Shortest path length: " << result.first << std::endl; diff --git a/ortools/graph/samples/dijkstra_undirected.cc b/ortools/graph/samples/dijkstra_undirected.cc index f51645691b9..84bef36fee8 100644 --- a/ortools/graph/samples/dijkstra_undirected.cc +++ b/ortools/graph/samples/dijkstra_undirected.cc @@ -59,8 +59,8 @@ int main(int argc, char** argv) { // Solve the shortest path problem from 0 to 4. std::pair> result = - operations_research::SimpleOneToOneShortestPath(0, 4, tails, heads, - lengths); + operations_research::SimpleOneToOneShortestPath(0, 4, tails, + heads, lengths); // Print to length of the path and then the nodes in the path. std::cout << "Shortest path length: " << result.first << std::endl; diff --git a/ortools/graph/samples/simple_max_flow_program.py b/ortools/graph/samples/simple_max_flow_program.py index 38bd192247f..ee7facd62fe 100755 --- a/ortools/graph/samples/simple_max_flow_program.py +++ b/ortools/graph/samples/simple_max_flow_program.py @@ -18,52 +18,53 @@ import numpy as np from ortools.graph.python import max_flow + # [END import] def main(): - """MaxFlow simple interface example.""" - # [START solver] - # Instantiate a SimpleMaxFlow solver. - smf = max_flow.SimpleMaxFlow() - # [END solver] + """MaxFlow simple interface example.""" + # [START solver] + # Instantiate a SimpleMaxFlow solver. + smf = max_flow.SimpleMaxFlow() + # [END solver] - # [START data] - # Define three parallel arrays: start_nodes, end_nodes, and the capacities - # between each pair. For instance, the arc from node 0 to node 1 has a - # capacity of 20. - start_nodes = np.array([0, 0, 0, 1, 1, 2, 2, 3, 3]) - end_nodes = np.array([1, 2, 3, 2, 4, 3, 4, 2, 4]) - capacities = np.array([20, 30, 10, 40, 30, 10, 20, 5, 20]) - # [END data] + # [START data] + # Define three parallel arrays: start_nodes, end_nodes, and the capacities + # between each pair. For instance, the arc from node 0 to node 1 has a + # capacity of 20. + start_nodes = np.array([0, 0, 0, 1, 1, 2, 2, 3, 3]) + end_nodes = np.array([1, 2, 3, 2, 4, 3, 4, 2, 4]) + capacities = np.array([20, 30, 10, 40, 30, 10, 20, 5, 20]) + # [END data] - # [START constraints] - # Add arcs in bulk. - # note: we could have used add_arc_with_capacity(start, end, capacity) - all_arcs = smf.add_arcs_with_capacity(start_nodes, end_nodes, capacities) - # [END constraints] + # [START constraints] + # Add arcs in bulk. + # note: we could have used add_arc_with_capacity(start, end, capacity) + all_arcs = smf.add_arcs_with_capacity(start_nodes, end_nodes, capacities) + # [END constraints] - # [START solve] - # Find the maximum flow between node 0 and node 4. - status = smf.solve(0, 4) - # [END solve] + # [START solve] + # Find the maximum flow between node 0 and node 4. + status = smf.solve(0, 4) + # [END solve] - # [START print_solution] - if status != smf.OPTIMAL: - print("There was an issue with the max flow input.") - print(f"Status: {status}") - exit(1) - print("Max flow:", smf.optimal_flow()) - print("") - print(" Arc Flow / Capacity") - solution_flows = smf.flows(all_arcs) - for arc, flow, capacity in zip(all_arcs, solution_flows, capacities): - print(f"{smf.tail(arc)} / {smf.head(arc)} {flow:3} / {capacity:3}") - print("Source side min-cut:", smf.get_source_side_min_cut()) - print("Sink side min-cut:", smf.get_sink_side_min_cut()) - # [END print_solution] + # [START print_solution] + if status != smf.OPTIMAL: + print("There was an issue with the max flow input.") + print(f"Status: {status}") + exit(1) + print("Max flow:", smf.optimal_flow()) + print("") + print(" Arc Flow / Capacity") + solution_flows = smf.flows(all_arcs) + for arc, flow, capacity in zip(all_arcs, solution_flows, capacities): + print(f"{smf.tail(arc)} / {smf.head(arc)} {flow:3} / {capacity:3}") + print("Source side min-cut:", smf.get_source_side_min_cut()) + print("Sink side min-cut:", smf.get_sink_side_min_cut()) + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/graph/samples/simple_min_cost_flow_program.py b/ortools/graph/samples/simple_min_cost_flow_program.py index 4e0e0afd563..93c6aecb09f 100755 --- a/ortools/graph/samples/simple_min_cost_flow_program.py +++ b/ortools/graph/samples/simple_min_cost_flow_program.py @@ -18,62 +18,63 @@ import numpy as np from ortools.graph.python import min_cost_flow + # [END import] def main(): - """MinCostFlow simple interface example.""" - # [START solver] - # Instantiate a SimpleMinCostFlow solver. - smcf = min_cost_flow.SimpleMinCostFlow() - # [END solver] + """MinCostFlow simple interface example.""" + # [START solver] + # Instantiate a SimpleMinCostFlow solver. + smcf = min_cost_flow.SimpleMinCostFlow() + # [END solver] - # [START data] - # Define four parallel arrays: sources, destinations, capacities, - # and unit costs between each pair. For instance, the arc from node 0 - # to node 1 has a capacity of 15. - start_nodes = np.array([0, 0, 1, 1, 1, 2, 2, 3, 4]) - end_nodes = np.array([1, 2, 2, 3, 4, 3, 4, 4, 2]) - capacities = np.array([15, 8, 20, 4, 10, 15, 4, 20, 5]) - unit_costs = np.array([4, 4, 2, 2, 6, 1, 3, 2, 3]) + # [START data] + # Define four parallel arrays: sources, destinations, capacities, + # and unit costs between each pair. For instance, the arc from node 0 + # to node 1 has a capacity of 15. + start_nodes = np.array([0, 0, 1, 1, 1, 2, 2, 3, 4]) + end_nodes = np.array([1, 2, 2, 3, 4, 3, 4, 4, 2]) + capacities = np.array([15, 8, 20, 4, 10, 15, 4, 20, 5]) + unit_costs = np.array([4, 4, 2, 2, 6, 1, 3, 2, 3]) - # Define an array of supplies at each node. - supplies = [20, 0, 0, -5, -15] - # [END data] + # Define an array of supplies at each node. + supplies = [20, 0, 0, -5, -15] + # [END data] - # [START constraints] - # Add arcs, capacities and costs in bulk using numpy. - all_arcs = smcf.add_arcs_with_capacity_and_unit_cost( - start_nodes, end_nodes, capacities, unit_costs - ) + # [START constraints] + # Add arcs, capacities and costs in bulk using numpy. + all_arcs = smcf.add_arcs_with_capacity_and_unit_cost( + start_nodes, end_nodes, capacities, unit_costs + ) - # Add supply for each nodes. - smcf.set_nodes_supplies(np.arange(0, len(supplies)), supplies) - # [END constraints] + # Add supply for each nodes. + smcf.set_nodes_supplies(np.arange(0, len(supplies)), supplies) + # [END constraints] - # [START solve] - # Find the min cost flow. - status = smcf.solve() - # [END solve] + # [START solve] + # Find the min cost flow. + status = smcf.solve() + # [END solve] - # [START print_solution] - if status != smcf.OPTIMAL: - print("There was an issue with the min cost flow input.") - print(f"Status: {status}") - exit(1) - print(f"Minimum cost: {smcf.optimal_cost()}") - print("") - print(" Arc Flow / Capacity Cost") - solution_flows = smcf.flows(all_arcs) - costs = solution_flows * unit_costs - for arc, flow, cost in zip(all_arcs, solution_flows, costs): - print( - f"{smcf.tail(arc):1} -> " - f"{smcf.head(arc)} {flow:3} / {smcf.capacity(arc):3} {cost}" - ) - # [END print_solution] + # [START print_solution] + if status != smcf.OPTIMAL: + print("There was an issue with the min cost flow input.") + print(f"Status: {status}") + exit(1) + print(f"Minimum cost: {smcf.optimal_cost()}") + print("") + print(" Arc Flow / Capacity Cost") + solution_flows = smcf.flows(all_arcs) + costs = solution_flows * unit_costs + for arc, flow, cost in zip(all_arcs, solution_flows, costs): + print( + f"{smcf.tail(arc):1} -> " + f"{smcf.head(arc)} {flow:3} / {smcf.capacity(arc):3} {cost}" + ) + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/gscip/BUILD.bazel b/ortools/gscip/BUILD.bazel index 1c7e016f10e..00bbd080db7 100644 --- a/ortools/gscip/BUILD.bazel +++ b/ortools/gscip/BUILD.bazel @@ -49,19 +49,6 @@ cc_library( ], ) -cc_library( - name = "legacy_scip_params", - srcs = ["legacy_scip_params.cc"], - hdrs = ["legacy_scip_params.h"], - deps = [ - "//ortools/linear_solver:scip_helper_macros", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/strings:str_format", - "@scip", - ], -) - cc_library( name = "gscip", srcs = [ @@ -76,7 +63,6 @@ cc_library( ":gscip_cc_proto", ":gscip_message_handler", ":gscip_parameters", - ":legacy_scip_params", "//ortools/base", "//ortools/base:status_builder", "//ortools/base:status_macros", diff --git a/ortools/gscip/gscip.cc b/ortools/gscip/gscip.cc index bba9b3fc8a6..58eb771694e 100644 --- a/ortools/gscip/gscip.cc +++ b/ortools/gscip/gscip.cc @@ -40,7 +40,6 @@ #include "ortools/gscip/gscip.pb.h" #include "ortools/gscip/gscip_event_handler.h" #include "ortools/gscip/gscip_parameters.h" -#include "ortools/gscip/legacy_scip_params.h" #include "ortools/linear_solver/scip_helper_macros.h" #include "ortools/port/proto_utils.h" #include "ortools/util/status_macros.h" @@ -294,8 +293,7 @@ const GScipConstraintOptions& DefaultGScipConstraintOptions() { return constraint_options; } -absl::Status GScip::SetParams(const GScipParameters& params, - absl::string_view legacy_params) { +absl::Status GScip::SetParams(const GScipParameters& params) { if (params.has_silence_output()) { SCIPsetMessagehdlrQuiet(scip_, params.silence_output()); } @@ -350,10 +348,6 @@ absl::Status GScip::SetParams(const GScipParameters& params, RETURN_IF_SCIP_ERROR( SCIPsetRealParam(scip_, real_param.first.c_str(), real_param.second)); } - if (!legacy_params.empty()) { - RETURN_IF_ERROR( - LegacyScipSetSolverSpecificParameters(legacy_params, scip_)); - } return absl::OkStatus(); } @@ -929,8 +923,7 @@ absl::StatusOr GScip::SuggestHint( } absl::StatusOr GScip::Solve( - const GScipParameters& params, absl::string_view legacy_params, - const GScipMessageHandler message_handler, + const GScipParameters& params, const GScipMessageHandler message_handler, const Interrupter* const interrupter) { if (InErrorState()) { return absl::InvalidArgumentError( @@ -950,7 +943,7 @@ absl::StatusOr GScip::Solve( GScipResult result; // Step 1: apply parameters. - const absl::Status param_status = SetParams(params, legacy_params); + const absl::Status param_status = SetParams(params); if (!param_status.ok()) { result.gscip_output.set_status(GScipOutput::INVALID_SOLVER_PARAMETERS); // Conversion to std::string for open source build. diff --git a/ortools/gscip/gscip.h b/ortools/gscip/gscip.h index 722fc45dffd..e6f9aa08ca9 100644 --- a/ortools/gscip/gscip.h +++ b/ortools/gscip/gscip.h @@ -178,8 +178,7 @@ class GScip { static std::string ScipVersion(); // After Solve() the parameters are reset and SCIP stage is restored to - // PROBLEM. "legacy_params" are in the format of legacy_scip_params.h and are - // applied after "params". Use of "legacy_params" is discouraged. + // PROBLEM. // // The returned StatusOr will contain an error only if an: // * An underlying function from SCIP fails. @@ -192,7 +191,6 @@ class GScip { // returns. absl::StatusOr Solve( const GScipParameters& params = GScipParameters(), - absl::string_view legacy_params = "", GScipMessageHandler message_handler = nullptr, const Interrupter* interrupter = nullptr); @@ -480,8 +478,7 @@ class GScip { // Releases SCIP memory. absl::Status CleanUp(); - absl::Status SetParams(const GScipParameters& params, - absl::string_view legacy_params); + absl::Status SetParams(const GScipParameters& params); absl::Status FreeTransform(); // Returns an error if |d| >= ScipInf(). diff --git a/ortools/gurobi/BUILD.bazel b/ortools/gurobi/BUILD.bazel index f1369871078..07070d46d56 100644 --- a/ortools/gurobi/BUILD.bazel +++ b/ortools/gurobi/BUILD.bazel @@ -13,39 +13,6 @@ package(default_visibility = ["//visibility:public"]) -cc_library( - name = "environment", - srcs = [ - "environment.cc", - ], - hdrs = [ - "environment.h", - ], - deps = [ - "//ortools/base", - "//ortools/base:dynamic_library", - "//ortools/base:file", - "//ortools/base:status_macros", - "@abseil-cpp//absl/flags:flag", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:statusor", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/synchronization", - "@abseil-cpp//absl/types:optional", - ], -) - -cc_library( - name = "gurobi_util", - srcs = ["gurobi_util.cc"], - hdrs = ["gurobi_util.h"], - deps = [ - ":environment", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/strings:str_format", - ], -) - cc_library( name = "gurobi_stdout_matchers", testonly = True, diff --git a/ortools/gurobi/isv_public/BUILD.bazel b/ortools/gurobi/isv_public/BUILD.bazel index d50111b386e..cabab7cc8b7 100644 --- a/ortools/gurobi/isv_public/BUILD.bazel +++ b/ortools/gurobi/isv_public/BUILD.bazel @@ -18,8 +18,8 @@ cc_library( srcs = ["gurobi_isv.cc"], hdrs = ["gurobi_isv.h"], deps = [ - "//ortools/gurobi:environment", "//ortools/math_opt/solvers:gurobi_cc_proto", + "//ortools/third_party_solvers:gurobi_environment", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", diff --git a/ortools/gurobi/isv_public/gurobi_isv.h b/ortools/gurobi/isv_public/gurobi_isv.h index 1850e914185..ce6946db2b4 100644 --- a/ortools/gurobi/isv_public/gurobi_isv.h +++ b/ortools/gurobi/isv_public/gurobi_isv.h @@ -18,7 +18,7 @@ #include #include "absl/status/statusor.h" -#include "ortools/gurobi/environment.h" +#include "ortools/third_party_solvers/gurobi_environment.h" namespace operations_research::math_opt { diff --git a/ortools/init/BUILD.bazel b/ortools/init/BUILD.bazel index a6121f8f765..c3f0cc42901 100644 --- a/ortools/init/BUILD.bazel +++ b/ortools/init/BUILD.bazel @@ -19,9 +19,9 @@ cc_library( hdrs = ["init.h"], deps = [ "//ortools/base", - "//ortools/gurobi:environment", "//ortools/sat:cp_model_solver", "//ortools/sat:cp_model_solver_helpers", + "//ortools/third_party_solvers:gurobi_environment", "@abseil-cpp//absl/flags:flag", "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:globals", diff --git a/ortools/init/init.cc b/ortools/init/init.cc index aefe9ccfbe3..58b1f73eadb 100644 --- a/ortools/init/init.cc +++ b/ortools/init/init.cc @@ -20,9 +20,9 @@ #include "absl/log/globals.h" #include "absl/strings/string_view.h" #include "ortools/base/init_google.h" -#include "ortools/gurobi/environment.h" #include "ortools/sat/cp_model_solver.h" #include "ortools/sat/cp_model_solver_helpers.h" +#include "ortools/third_party_solvers/gurobi_environment.h" namespace operations_research { void CppBridge::InitLogging(absl::string_view usage) { diff --git a/ortools/init/python/init_test.py b/ortools/init/python/init_test.py index 18708e4ffae..80ed541fcfa 100755 --- a/ortools/init/python/init_test.py +++ b/ortools/init/python/init_test.py @@ -20,35 +20,35 @@ class InitTest(absltest.TestCase): - def test_logging(self): - print("test_logging") - init.CppBridge.init_logging("pywrapinit_test.py") - init.CppBridge.shutdown_logging() - - def test_flags(self): - print("test_cpp_flags") - cpp_flags = init.CppFlags() - assert hasattr(cpp_flags, "stderrthreshold") - assert hasattr(cpp_flags, "log_prefix") - assert hasattr(cpp_flags, "cp_model_dump_prefix") - assert hasattr(cpp_flags, "cp_model_dump_models") - assert hasattr(cpp_flags, "cp_model_dump_submodels") - assert hasattr(cpp_flags, "cp_model_dump_response") - init.CppBridge.set_flags(cpp_flags) - - def test_version(self): - print("test_version") - major = init.OrToolsVersion.major_number() - self.assertIsInstance(major, int) - minor = init.OrToolsVersion.minor_number() - self.assertIsInstance(minor, int) - patch = init.OrToolsVersion.patch_number() - self.assertIsInstance(patch, int) - version = init.OrToolsVersion.version_string() - self.assertIsInstance(version, str) - string = f"{major}.{minor}.{patch}" - self.assertEqual(version, string) + def test_logging(self): + print("test_logging") + init.CppBridge.init_logging("pywrapinit_test.py") + init.CppBridge.shutdown_logging() + + def test_flags(self): + print("test_cpp_flags") + cpp_flags = init.CppFlags() + assert hasattr(cpp_flags, "stderrthreshold") + assert hasattr(cpp_flags, "log_prefix") + assert hasattr(cpp_flags, "cp_model_dump_prefix") + assert hasattr(cpp_flags, "cp_model_dump_models") + assert hasattr(cpp_flags, "cp_model_dump_submodels") + assert hasattr(cpp_flags, "cp_model_dump_response") + init.CppBridge.set_flags(cpp_flags) + + def test_version(self): + print("test_version") + major = init.OrToolsVersion.major_number() + self.assertIsInstance(major, int) + minor = init.OrToolsVersion.minor_number() + self.assertIsInstance(minor, int) + patch = init.OrToolsVersion.patch_number() + self.assertIsInstance(patch, int) + version = init.OrToolsVersion.version_string() + self.assertIsInstance(version, str) + string = f"{major}.{minor}.{patch}" + self.assertEqual(version, string) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/java/pom-full.xml.in b/ortools/java/pom-full.xml.in index ffde245eac3..791f7b6f3fd 100644 --- a/ortools/java/pom-full.xml.in +++ b/ortools/java/pom-full.xml.in @@ -109,7 +109,7 @@ com.google.protobuf protobuf-java - 4.31.0 + 4.31.1 diff --git a/ortools/java/pom-local.xml.in b/ortools/java/pom-local.xml.in index d03b19413bb..64b2c512211 100644 --- a/ortools/java/pom-local.xml.in +++ b/ortools/java/pom-local.xml.in @@ -81,7 +81,7 @@ com.google.protobuf protobuf-java - 4.31.0 + 4.31.1 diff --git a/ortools/linear_solver/BUILD.bazel b/ortools/linear_solver/BUILD.bazel index 4a7ed5e0ad6..e8103e9968b 100644 --- a/ortools/linear_solver/BUILD.bazel +++ b/ortools/linear_solver/BUILD.bazel @@ -12,7 +12,6 @@ # limitations under the License. load("@bazel_skylib//rules:common_settings.bzl", "bool_flag") -load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("@protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") load("@protobuf//bazel:proto_library.bzl", "proto_library") load("@protobuf//bazel:py_proto_library.bzl", "py_proto_library") @@ -171,6 +170,7 @@ cc_library( name = "linear_solver", srcs = [ "gurobi_interface.cc", + "gurobi_util.cc", "linear_expr.cc", "linear_solver.cc", "linear_solver_callback.cc", @@ -211,6 +211,7 @@ cc_library( "//conditions:default": [], }), hdrs = [ + "gurobi_util.h", "linear_expr.h", "linear_solver.h", "linear_solver_callback.h", @@ -258,15 +259,12 @@ cc_library( ":model_validator", "//ortools/base", "//ortools/base:accurate_sum", - "//ortools/base:dynamic_library", "//ortools/base:hash", "//ortools/base:logging", "//ortools/base:map_util", "//ortools/base:status_macros", "//ortools/base:stl_util", "//ortools/base:timer", - "//ortools/gurobi:environment", - "//ortools/gurobi:gurobi_util", "//ortools/linear_solver/proto_solver:gurobi_proto_solver", "//ortools/linear_solver/proto_solver:sat_proto_solver", "//ortools/port:file", @@ -274,9 +272,10 @@ cc_library( "//ortools/sat:cp_model_cc_proto", "//ortools/sat:cp_model_solver", "//ortools/sat:lp_utils", + "//ortools/third_party_solvers:gurobi_environment", + "//ortools/third_party_solvers:xpress_environment", "//ortools/util:fp_utils", "//ortools/util:lazy_mutable_copy", - "//ortools/xpress:environment", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", @@ -311,7 +310,7 @@ cc_library( "//conditions:default": [], }) + select({ ":use_scip": [ - "//ortools/gscip:legacy_scip_params", + "//ortools/linear_solver/proto_solver:scip_params", "//ortools/linear_solver/proto_solver:scip_proto_solver", "@scip", ], @@ -343,6 +342,21 @@ cc_library( ], ) +cc_library( + name = "gurobi_util", + srcs = ["gurobi_util.cc"], + hdrs = ["gurobi_util.h"], + deps = [ + "//ortools/third_party_solvers:gurobi_environment", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/status:statusor", + ], +) + cc_library( name = "scip_helper_macros", hdrs = ["scip_helper_macros.h"], diff --git a/ortools/linear_solver/glpk_interface.cc b/ortools/linear_solver/glpk_interface.cc index aa76d173973..95549b7b456 100644 --- a/ortools/linear_solver/glpk_interface.cc +++ b/ortools/linear_solver/glpk_interface.cc @@ -15,7 +15,6 @@ #include #include -#include #include #include #include @@ -24,10 +23,7 @@ #include #include "absl/base/attributes.h" -#include "absl/memory/memory.h" #include "absl/strings/str_format.h" -#include "ortools/base/commandlineflags.h" -#include "ortools/base/hash.h" #include "ortools/base/logging.h" #include "ortools/base/timer.h" #include "ortools/glpk/glpk_env_deleter.h" diff --git a/ortools/linear_solver/gurobi_interface.cc b/ortools/linear_solver/gurobi_interface.cc index 2efe7f66356..35d5ace103a 100644 --- a/ortools/linear_solver/gurobi_interface.cc +++ b/ortools/linear_solver/gurobi_interface.cc @@ -66,12 +66,12 @@ #include "absl/time/time.h" #include "ortools/base/logging.h" #include "ortools/base/timer.h" -#include "ortools/gurobi/environment.h" -#include "ortools/gurobi/gurobi_util.h" +#include "ortools/linear_solver/gurobi_util.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver_callback.h" #include "ortools/linear_solver/proto_solver/gurobi_proto_solver.h" #include "ortools/linear_solver/proto_solver/proto_utils.h" +#include "ortools/third_party_solvers/gurobi_environment.h" #include "ortools/util/lazy_mutable_copy.h" #include "ortools/util/time_limit.h" @@ -543,6 +543,13 @@ struct MPCallbackWithGurobiContext { // NOTE(user): This function must have this exact API, because we are passing // it to Gurobi as a callback. + +#if defined(_MSC_VER) +#define GUROBI_STDCALL __stdcall +#else +#define GUROBI_STDCALL +#endif + int GUROBI_STDCALL CallbackImpl(GRBmodel* model, void* gurobi_internal_callback_data, int where, void* raw_model_and_callback) { diff --git a/ortools/gurobi/gurobi_util.cc b/ortools/linear_solver/gurobi_util.cc similarity index 79% rename from ortools/gurobi/gurobi_util.cc rename to ortools/linear_solver/gurobi_util.cc index f26c094d750..0fc5ba6a593 100644 --- a/ortools/gurobi/gurobi_util.cc +++ b/ortools/linear_solver/gurobi_util.cc @@ -11,19 +11,50 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/gurobi/gurobi_util.h" +#include "ortools/linear_solver/gurobi_util.h" #include #include #include +#include "absl/log/log.h" +#include "absl/status/status.h" +#include "absl/status/statusor.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/str_join.h" -#include "ortools/gurobi/environment.h" +#include "ortools/base/status_macros.h" +#include "ortools/third_party_solvers/gurobi_environment.h" namespace operations_research { +bool GurobiIsCorrectlyInstalled() { + absl::StatusOr status = GetGurobiEnv(); + if (!status.ok() || status.value() == nullptr) { + LOG(WARNING) << status.status(); + return false; + } + + GRBfreeenv(status.value()); + + return true; +} + +absl::StatusOr GetGurobiEnv() { + GRBenv* env = nullptr; + + RETURN_IF_ERROR(LoadGurobiDynamicLibrary({})); + + if (GRBloadenv(&env, nullptr) != 0 || env == nullptr) { + return absl::FailedPreconditionError( + absl::StrCat("Found the Gurobi shared library, but could not create " + "Gurobi environment: is Gurobi licensed on this machine?", + GRBgeterrormsg(env))); + } + + return env; +} + std::string GurobiParamInfoForLogging(GRBenv* grb, bool one_liner_output) { const absl::ParsedFormat<'s', 's', 's'> kExtendedFormat( " Parameter: '%s' value: %s default: %s"); diff --git a/ortools/gurobi/gurobi_util.h b/ortools/linear_solver/gurobi_util.h similarity index 65% rename from ortools/gurobi/gurobi_util.h rename to ortools/linear_solver/gurobi_util.h index 85c79bbdaf5..2a258b2696a 100644 --- a/ortools/gurobi/gurobi_util.h +++ b/ortools/linear_solver/gurobi_util.h @@ -11,15 +11,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_GUROBI_GUROBI_UTIL_H_ -#define OR_TOOLS_GUROBI_GUROBI_UTIL_H_ +#ifndef OR_TOOLS_LINEAR_SOLVER_GUROBI_UTIL_H_ +#define OR_TOOLS_LINEAR_SOLVER_GUROBI_UTIL_H_ #include -#include "ortools/gurobi/environment.h" +#include "absl/flags/declare.h" +#include "absl/status/statusor.h" +#include "ortools/third_party_solvers/gurobi_environment.h" namespace operations_research { +absl::StatusOr GetGurobiEnv(); + +// This returns true if the Gurobi shared library is properly loaded (otherwise, +// tries to find it and load it) and if a Gurobi license can be obtained (it +// does that by trying to grab a license and then release it). +bool GurobiIsCorrectlyInstalled(); + // Returns a human-readable listing of all gurobi parameters that are set to // non-default values, and their current value in the given environment. If all // parameters are at their default value, returns the empty string. @@ -28,4 +37,4 @@ std::string GurobiParamInfoForLogging(GRBenv* grb, bool one_liner_output = false); } // namespace operations_research -#endif // OR_TOOLS_GUROBI_GUROBI_UTIL_H_ +#endif // OR_TOOLS_LINEAR_SOLVER_GUROBI_UTIL_H_ diff --git a/ortools/linear_solver/model_exporter.cc b/ortools/linear_solver/model_exporter.cc index 51a37276772..3554e1961da 100644 --- a/ortools/linear_solver/model_exporter.cc +++ b/ortools/linear_solver/model_exporter.cc @@ -57,7 +57,7 @@ class LineBreaker { // Returns true if string s will fit on the current line without adding a // carriage return. - bool WillFit(const std::string& s) { + bool WillFit(absl::string_view s) { return line_size_ + static_cast(s.size()) < max_line_size_; } diff --git a/ortools/linear_solver/proto_solver/BUILD.bazel b/ortools/linear_solver/proto_solver/BUILD.bazel index 8b01c79af5f..eea9aa0f122 100644 --- a/ortools/linear_solver/proto_solver/BUILD.bazel +++ b/ortools/linear_solver/proto_solver/BUILD.bazel @@ -110,6 +110,19 @@ cc_library( ], ) +cc_library( + name = "scip_params", + srcs = ["scip_params.cc"], + hdrs = ["scip_params.h"], + deps = [ + "//ortools/linear_solver:scip_helper_macros", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@scip", + ], +) + cc_library( name = "scip_proto_solver", srcs = ["scip_proto_solver.cc"], @@ -121,10 +134,10 @@ cc_library( deps = [ "//ortools/base", "//ortools/base:timer", - "//ortools/gscip:legacy_scip_params", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/linear_solver:model_validator", "//ortools/linear_solver:scip_helper_macros", + "//ortools/linear_solver/proto_solver:scip_params", "//ortools/util:lazy_mutable_copy", "@abseil-cpp//absl/cleanup", "@abseil-cpp//absl/container:btree", @@ -146,9 +159,10 @@ cc_library( hdrs = ["gurobi_proto_solver.h"], deps = [ "//ortools/base:timer", - "//ortools/gurobi:environment", + "//ortools/linear_solver:gurobi_util", "//ortools/linear_solver:linear_solver_cc_proto", "//ortools/linear_solver:model_validator", + "//ortools/third_party_solvers:gurobi_environment", "//ortools/util:lazy_mutable_copy", "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/cleanup", @@ -181,26 +195,3 @@ cc_library( "@highs", ], ) - -cc_library( - name = "xpress_proto_solver", - srcs = ["xpress_proto_solver.cc"], - hdrs = ["xpress_proto_solver.h"], - deps = [ - "//ortools/base:timer", - "//ortools/linear_solver:linear_solver_cc_proto", - "//ortools/linear_solver:model_validator", - "//ortools/util:lazy_mutable_copy", - "//ortools/xpress:environment", - "@abseil-cpp//absl/base:core_headers", - "@abseil-cpp//absl/cleanup", - "@abseil-cpp//absl/log", - "@abseil-cpp//absl/log:check", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:statusor", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/strings:str_format", - "@abseil-cpp//absl/time", - "@abseil-cpp//absl/types:optional", - ], -) diff --git a/ortools/linear_solver/proto_solver/gurobi_proto_solver.cc b/ortools/linear_solver/proto_solver/gurobi_proto_solver.cc index aba0d9ded51..4e58dcdd801 100644 --- a/ortools/linear_solver/proto_solver/gurobi_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/gurobi_proto_solver.cc @@ -36,12 +36,12 @@ #include "absl/strings/string_view.h" #include "absl/time/clock.h" #include "absl/time/time.h" -#include "absl/types/optional.h" #include "ortools/base/status_macros.h" #include "ortools/base/timer.h" -#include "ortools/gurobi/environment.h" +#include "ortools/linear_solver/gurobi_util.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_validator.h" +#include "ortools/third_party_solvers/gurobi_environment.h" #include "ortools/util/lazy_mutable_copy.h" namespace operations_research { diff --git a/ortools/linear_solver/proto_solver/gurobi_proto_solver.h b/ortools/linear_solver/proto_solver/gurobi_proto_solver.h index e9aae72af34..4ad96852cb9 100644 --- a/ortools/linear_solver/proto_solver/gurobi_proto_solver.h +++ b/ortools/linear_solver/proto_solver/gurobi_proto_solver.h @@ -14,13 +14,11 @@ #ifndef OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_GUROBI_PROTO_SOLVER_H_ #define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_GUROBI_PROTO_SOLVER_H_ -#include - #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/string_view.h" -#include "ortools/gurobi/environment.h" #include "ortools/linear_solver/linear_solver.pb.h" +#include "ortools/third_party_solvers/gurobi_environment.h" #include "ortools/util/lazy_mutable_copy.h" namespace operations_research { diff --git a/ortools/gscip/legacy_scip_params.cc b/ortools/linear_solver/proto_solver/scip_params.cc similarity index 98% rename from ortools/gscip/legacy_scip_params.cc rename to ortools/linear_solver/proto_solver/scip_params.cc index 13f015a39b7..282b5d303ac 100644 --- a/ortools/gscip/legacy_scip_params.cc +++ b/ortools/linear_solver/proto_solver/scip_params.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/gscip/legacy_scip_params.h" +#include "ortools/linear_solver/proto_solver/scip_params.h" #include #include diff --git a/ortools/gscip/legacy_scip_params.h b/ortools/linear_solver/proto_solver/scip_params.h similarity index 83% rename from ortools/gscip/legacy_scip_params.h rename to ortools/linear_solver/proto_solver/scip_params.h index d50d5072b20..fecd699c089 100644 --- a/ortools/gscip/legacy_scip_params.h +++ b/ortools/linear_solver/proto_solver/scip_params.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_GSCIP_LEGACY_SCIP_PARAMS_H_ -#define OR_TOOLS_GSCIP_LEGACY_SCIP_PARAMS_H_ +#ifndef OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_SCIP_PARAMS_H_ +#define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_SCIP_PARAMS_H_ #include @@ -27,4 +27,4 @@ absl::Status LegacyScipSetSolverSpecificParameters(absl::string_view parameters, SCIP* scip); } -#endif // OR_TOOLS_GSCIP_LEGACY_SCIP_PARAMS_H_ +#endif // OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_SCIP_PARAMS_H_ diff --git a/ortools/linear_solver/proto_solver/scip_proto_solver.cc b/ortools/linear_solver/proto_solver/scip_proto_solver.cc index 3829b732041..8e36df5dfb2 100644 --- a/ortools/linear_solver/proto_solver/scip_proto_solver.cc +++ b/ortools/linear_solver/proto_solver/scip_proto_solver.cc @@ -41,9 +41,9 @@ #include "absl/time/time.h" #include "ortools/base/status_macros.h" #include "ortools/base/timer.h" -#include "ortools/gscip/legacy_scip_params.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_validator.h" +#include "ortools/linear_solver/proto_solver/scip_params.h" #include "ortools/linear_solver/scip_helper_macros.h" #include "ortools/util/lazy_mutable_copy.h" #include "scip/cons_and.h" diff --git a/ortools/linear_solver/proto_solver/xpress_proto_solver.cc b/ortools/linear_solver/proto_solver/xpress_proto_solver.cc deleted file mode 100644 index 2e22b17bb2b..00000000000 --- a/ortools/linear_solver/proto_solver/xpress_proto_solver.cc +++ /dev/null @@ -1,970 +0,0 @@ -// Copyright 2010-2025 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#include "ortools/linear_solver/proto_solver/xpress_proto_solver.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#include "absl/base/attributes.h" -#include "absl/cleanup/cleanup.h" -#include "absl/log/check.h" -#include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" -#include "absl/strings/str_join.h" -#include "absl/strings/str_split.h" -#include "absl/strings/string_view.h" -#include "absl/time/clock.h" -#include "absl/time/time.h" -#include "absl/types/optional.h" -#include "ortools/base/logging.h" -#include "ortools/base/status_macros.h" -#include "ortools/base/timer.h" -#include "ortools/linear_solver/linear_solver.pb.h" -#include "ortools/linear_solver/model_validator.h" -#include "ortools/util/lazy_mutable_copy.h" -#include "ortools/xpress/environment.h" - -namespace operations_research { - -// namespace { -// constexpr int XPRS_OK = 0; - -// bool XPressCodeToInvalidResponse(int error_code, const char* source_file, -// int source_line, const char* statement, -// XPRSprob prob, MPSolutionResponse* response) -// { -// if (error_code == XPRS_OK) return true; -// response->set_status(); -// response->set_status_message(absl::StrFormat( -// "XPress error code %d (file '%s', line %d) on '%s': %s", error_code, -// source_file, source_line, statement, XPRSgeterrormsg(prob))); -// return false; -// } - -// int AddIndicatorConstraint(const MPGeneralConstraintProto& gen_cst, -// XPRSprob xpress_model, -// std::vector* tmp_variables, -// std::vector* tmp_coefficients) { -// CHECK(xpress_model != nullptr); -// CHECK(tmp_variables != nullptr); -// CHECK(tmp_coefficients != nullptr); - -// const auto& ind_cst = gen_cst.indicator_constraint(); -// MPConstraintProto cst = ind_cst.constraint(); -// if (cst.lower_bound() > -std::numeric_limits::infinity()) { -// int status = XPRSaddgenconstrIndicator( -// xpress_model, gen_cst.name().c_str(), ind_cst.var_index(), -// ind_cst.var_value(), cst.var_index_size(), -// cst.mutable_var_index()->mutable_data(), -// cst.mutable_coefficient()->mutable_data(), -// cst.upper_bound() == cst.lower_bound() ? XPRS_EQUAL -// : XPRS_GREATER_EQUAL, -// cst.lower_bound()); -// if (status != XPRS_OK) return status; -// } -// if (cst.upper_bound() < std::numeric_limits::infinity() && -// cst.lower_bound() != cst.upper_bound()) { -// return XPRSaddgenconstrIndicator(xpress_model, gen_cst.name().c_str(), -// ind_cst.var_index(), -// ind_cst.var_value(), -// cst.var_index_size(), -// cst.mutable_var_index()->mutable_data(), -// cst.mutable_coefficient()->mutable_data(), -// XPRS_LESS_EQUAL, cst.upper_bound()); -// } - -// return XPRS_OK; -// } - -// int AddSosConstraint(const MPSosConstraint& sos_cst, XPRSprob xpress_model, -// std::vector* tmp_variables, -// std::vector* tmp_weights) { -// CHECK(xpress_model != nullptr); -// CHECK(tmp_variables != nullptr); -// CHECK(tmp_weights != nullptr); - -// tmp_variables->resize(sos_cst.var_index_size(), 0); -// for (int v = 0; v < sos_cst.var_index_size(); ++v) { -// (*tmp_variables)[v] = sos_cst.var_index(v); -// } -// tmp_weights->resize(sos_cst.var_index_size(), 0); -// if (sos_cst.weight_size() == sos_cst.var_index_size()) { -// for (int w = 0; w < sos_cst.weight_size(); ++w) { -// (*tmp_weights)[w] = sos_cst.weight(w); -// } -// } else { -// DCHECK_EQ(sos_cst.weight_size(), 0); -// // XPress requires variable weights in their SOS constraints. -// std::iota(tmp_weights->begin(), tmp_weights->end(), 1); -// } - -// std::vector types = {sos_cst.type() == MPSosConstraint::SOS1_DEFAULT -// ? XPRS_SOS_TYPE1 -// : XPRS_SOS_TYPE2}; -// std::vector begins = {0}; -// return XPRSaddsos(xpress_model, /*numsos=*/1, -// /*nummembers=*/sos_cst.var_index_size(), -// /*types=*/types.data(), -// /*beg=*/begins.data(), /*ind=*/tmp_variables->data(), -// /*weight*/ tmp_weights->data()); -// } - -// int AddQuadraticConstraint(const MPGeneralConstraintProto& gen_cst, -// XPRSprob xpress_model) { -// CHECK(xpress_model != nullptr); -// constexpr double kInfinity = std::numeric_limits::infinity(); - -// CHECK(gen_cst.has_quadratic_constraint()); -// const MPQuadraticConstraint& quad_cst = gen_cst.quadratic_constraint(); - -// auto addqconstr = [](XPRSprob xpress_model, MPQuadraticConstraint quad_cst, -// char sense, double rhs, const std::string& name) { -// return XPRSaddqconstr( -// xpress_model, -// /*numlnz=*/quad_cst.var_index_size(), -// /*lind=*/quad_cst.mutable_var_index()->mutable_data(), -// /*lval=*/quad_cst.mutable_coefficient()->mutable_data(), -// /*numqnz=*/quad_cst.qvar1_index_size(), -// /*qrow=*/quad_cst.mutable_qvar1_index()->mutable_data(), -// /*qcol=*/quad_cst.mutable_qvar2_index()->mutable_data(), -// /*qval=*/quad_cst.mutable_qcoefficient()->mutable_data(), -// /*sense=*/sense, -// /*rhs=*/rhs, -// /*QCname=*/name.c_str()); -// }; - -// if (quad_cst.has_lower_bound() && quad_cst.lower_bound() > -kInfinity) { -// const int xprs_status = -// addqconstr(xpress_model, gen_cst.quadratic_constraint(), -// XPRS_GREATER_EQUAL, quad_cst.lower_bound(), -// gen_cst.has_name() ? gen_cst.name() + "_lb" : ""); -// if (xprs_status != XPRS_OK) return xprs_status; -// } -// if (quad_cst.has_upper_bound() && quad_cst.upper_bound() < kInfinity) { -// const int xprs_status = -// addqconstr(xpress_model, gen_cst.quadratic_constraint(), -// XPRS_LESS_EQUAL, quad_cst.upper_bound(), -// gen_cst.has_name() ? gen_cst.name() + "_ub" : ""); -// if (xprs_status != XPRS_OK) return xprs_status; -// } - -// return XPRS_OK; -// } - -// int AddAndConstraint(const MPGeneralConstraintProto& gen_cst, -// XPRSprob xpress_model, std::vector* tmp_variables) -// { -// CHECK(xpress_model != nullptr); -// CHECK(tmp_variables != nullptr); - -// auto and_cst = gen_cst.and_constraint(); -// return XPRSaddgenconstrAnd( -// xpress_model, -// /*name=*/gen_cst.name().c_str(), -// /*resvar=*/and_cst.resultant_var_index(), -// /*nvars=*/and_cst.var_index_size(), -// /*vars=*/and_cst.mutable_var_index()->mutable_data()); -// } - -// int AddOrConstraint(const MPGeneralConstraintProto& gen_cst, -// XPRSprob xpress_model, std::vector* tmp_variables) { -// CHECK(xpress_model != nullptr); -// CHECK(tmp_variables != nullptr); - -// auto or_cst = gen_cst.or_constraint(); -// return XPRSaddgenconstrOr( -// xpress_model, -// /*name=*/gen_cst.name().c_str(), -// /*resvar=*/or_cst.resultant_var_index(), -// /*nvars=*/or_cst.var_index_size(), -// /*vars=*/or_cst.mutable_var_index()->mutable_data()); -// } - -// int AddMinConstraint(const MPGeneralConstraintProto& gen_cst, -// XPRSprob xpress_model, std::vector* tmp_variables) -// { -// CHECK(xpress_model != nullptr); -// CHECK(tmp_variables != nullptr); - -// auto min_cst = gen_cst.min_constraint(); -// return XPRSaddgenconstrMin( -// xpress_model, -// /*name=*/gen_cst.name().c_str(), -// /*resvar=*/min_cst.resultant_var_index(), -// /*nvars=*/min_cst.var_index_size(), -// /*vars=*/min_cst.mutable_var_index()->mutable_data(), -// /*constant=*/min_cst.has_constant() -// ? min_cst.constant() -// : std::numeric_limits::infinity()); -// } - -// int AddMaxConstraint(const MPGeneralConstraintProto& gen_cst, -// XPRSprob xpress_model, std::vector* tmp_variables) -// { -// CHECK(xpress_model != nullptr); -// CHECK(tmp_variables != nullptr); - -// auto max_cst = gen_cst.max_constraint(); -// return XPRSaddgenconstrMax( -// xpress_model, -// /*name=*/gen_cst.name().c_str(), -// /*resvar=*/max_cst.resultant_var_index(), -// /*nvars=*/max_cst.var_index_size(), -// /*vars=*/max_cst.mutable_var_index()->mutable_data(), -// /*constant=*/max_cst.has_constant() -// ? max_cst.constant() -// : -std::numeric_limits::infinity()); -// } -// } // namespace - -// std::string SetSolverSpecificParameters(absl::string_view parameters, -// XPRSprob xpress) { -// if (parameters.empty()) return absl::OkStatus(); -// std::vector error_messages; -// for (absl::string_view line : absl::StrSplit(parameters, '\n')) { -// // Empty lines are simply ignored. -// if (line.empty()) continue; -// // Comment tokens end at the next new-line, or the end of the string. -// // The first character must be '#' -// if (line[0] == '#') continue; -// for (absl::string_view token : -// absl::StrSplit(line, ',', absl::SkipWhitespace())) { -// if (token.empty()) continue; -// std::vector key_value = -// absl::StrSplit(token, absl::ByAnyChar(" ="), -// absl::SkipWhitespace()); -// // If one parameter fails, we keep processing the list of parameters. -// if (key_value.size() != 2) { -// const std::string current_message = -// absl::StrCat("Cannot parse parameter '", token, -// "'. Expected format is 'ParameterName value' or " -// "'ParameterName=value'"); -// error_messages.push_back(current_message); -// continue; -// } -// const int xpress_code = -// XPRSsetparam(xpress, key_value[0].c_str(), key_value[1].c_str()); -// if (xpress_code != XPRS_OK) { -// const std::string current_message = absl::StrCat( -// "Error setting parameter '", key_value[0], "' to value '", -// key_value[1], "': ", XPRSgeterrormsg(xpress)); -// error_messages.push_back(current_message); -// continue; -// } -// VLOG(2) << absl::StrCat("Set parameter '", key_value[0], "' to value -// '", -// key_value[1]); -// } -// } - -// if (error_messages.empty()) return ""; -// return absl::StrJoin(error_messages, "\n"); -// } - -MPSolutionResponse XPressSolveProto(LazyMutableCopy request) { - MPSolutionResponse response; - response.set_status(MPSolverResponseStatus::MPSOLVER_SOLVER_TYPE_UNAVAILABLE); - - // const absl::optional> optional_model = - // ExtractValidMPModelOrPopulateResponseStatus(request, &response); - // if (!optional_model) return response; - // const MPModelProto& model = optional_model->get(); - - // // We set `xpress_env` to point to a new environment if no existing one - // is - // // provided. We must make sure that we free this environment when we exit - // this - // // function. - // bool xpress_env_was_created = false; - // auto xpress_env_deleter = absl::MakeCleanup([&]() { - // if (xpress_env_was_created && xpress_env != nullptr) { - // XPRSfreeenv(xpress_env); - // } - // }); - // if (xpress_env == nullptr) { - // ASSIGN_OR_RETURN(xpress_env, GetXPressEnv()); - // xpress_env_was_created = true; - // } - - // XPRSprob xpress_model = nullptr; - // auto xpress_model_deleter = absl::MakeCleanup([&]() { - // const int error_code = XPRSfreemodel(xpress_model); - // LOG_IF(DFATAL, error_code != XPRS_OK) - // << "XPRSfreemodel failed with error " << error_code << ": " - // << XPRSgeterrormsg(xpress_env); - // }); - - // // `xpress_env` references ther XPRSenv argument. - // #define RETURN_IF_XPRESS_ERROR(x) \ -// RETURN_IF_ERROR( \ -// if (!XPressCodeToInvalidResponse(x, __FILE__, __LINE__, #x, xpress, - // &response)) { \ -// return response; \ -// }) - - // RETURN_IF_XPRESS_ERROR(XPRSnewmodel(xpress_env, &xpress_model, - // model.name().c_str(), - // /*numvars=*/0, - // /*obj=*/nullptr, - // /*lb=*/nullptr, - // /*ub=*/nullptr, - // /*vtype=*/nullptr, - // /*varnames=*/nullptr)); - // XPRSprob const model_env = XPRSgetenv(xpress_model); - - // if (request.has_solver_specific_parameters()) { - // const auto parameters_status = SetSolverSpecificParameters( - // request.solver_specific_parameters(), model_env); - // if (!parameters_status.ok()) { - // response.set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); - // response.set_status_str( - // std::string(parameters_status.message())); // NOLINT - // return response; - // } - // } - // if (request.solver_time_limit_seconds() > 0) { - // RETURN_IF_XPRESS_ERROR( - // XPRSsetdblparam(model_env, XPRS_DBL_PAR_TIMELIMIT, - // request.solver_time_limit_seconds())); - // } - // RETURN_IF_XPRESS_ERROR( - // XPRSsetintparam(model_env, XPRS_INT_PAR_OUTPUTFLAG, - // request.enable_internal_solver_output())); - - // const int variable_size = model.variable_size(); - // bool has_integer_variables = false; - // { - // std::vector obj_coeffs(variable_size, 0); - // std::vector lb(variable_size); - // std::vector ub(variable_size); - // std::vector ctype(variable_size); - // std::vector varnames(variable_size); - // for (int v = 0; v < variable_size; ++v) { - // const MPVariableProto& variable = model.variable(v); - // obj_coeffs[v] = variable.objective_coefficient(); - // lb[v] = variable.lower_bound(); - // ub[v] = variable.upper_bound(); - // ctype[v] = variable.is_integer() && - // request.solver_type() ==SolutionRes - // : XPRS_CONTINUOUS; - // if (variable.is_integer()) has_integer_variables = true; - // if (!variable.name().empty()) varnames[v] = variable.name().c_str(); - // } - - // RETURN_IF_XPRESS_ERROR( - // XPRSaddvars(xpress_model, variable_size, 0, nullptr, nullptr, - // nullptr, - // /*obj=*/obj_coeffs.data(), - // /*lb=*/lb.data(), /*ub=*/ub.data(), - // /*vtype=*/ctype.data(), - // /*varnames=*/const_cast(varnames.data()))); - - // // Set solution hints if any. - // for (int i = 0; i < model.solution_hint().var_index_size(); ++i) { - // RETURN_IF_XPRESS_ERROR(XPRSsetdblattrelement( - // xpress_model, XPRS_DBL_ATTR_START, model.solution_hint().var_inde - // const absl::optional> - // optional_model = - // ExtractValidMPModelOrPopulateResponseStatus(request, &response); - // if (!optional_model) return response; - // const MPModelProto& model = optional_model->get(); - - // // We set `xpress_env` to point to a new environment if no existing one - // is - // // provided. We must make sure that we free this environment when we exit - // this - // // function. - // bool xpress_env_was_created = false; - // auto xpress_env_deleter = absl::MakeCleanup([&]() { - // if (xpress_env_was_created && xpress_env != nullptr) { - // XPRSfreeenv(xpress_env); - // } - // }); - // if (xpress_env == nullptr) { - // ASSIGN_OR_RETURN(xpress_env, GetXPressEnv()); - // xpress_env_was_created = true; - // } - - // XPRSprob xpress_model = nullptr; - // auto xpress_model_deleter = absl::MakeCleanup([&]() { - // const int error_code = XPRSfreemodel(xpress_model); - // LOG_IF(DFATAL, error_code != XPRS_OK) - // << "XPRSfreemodel failed with error " << error_code << ": " - // << XPRSgeterrormsg(xpress_env); - // }); - - // // `xpress_env` references ther XPRSenv argument. - // #define RETURN_IF_XPRESS_ERROR(x) \ -// RETURN_IF_ERROR( \ -// XPressCodeToUtilStatus(x, __FILE__, __LINE__, #x, xpress_env)); - - // RETURN_IF_XPRESS_ERROR(XPRSnewmodel(xpress_env, &xpress_model, - // model.name().c_str(), - // /*numvars=*/0, - // /*obj=*/nullptr, - // /*lb=*/nullptr, - // /*ub=*/nullptr, - // /*vtype=*/nullptr, - // /*varnames=*/nullptr)); - // XPRSprob const model_env = XPRSgetenv(xpress_model); - - // if (request.has_solver_specific_parameters()) { - // const auto parameters_status = SetSolverSpecificParameters( - // request.solver_specific_parameters(), model_env); - // if (!parameters_status.ok()) { - // response.set_status(MPSOLVER_MODEL_INVALID_SOLVER_PARAMETERS); - // response.set_status_str( - // std::string(parameters_status.message())); // NOLINT - // return response; - // } - // } - // if (request.solver_time_limit_seconds() > 0) { - // RETURN_IF_XPRESS_ERROR( - // XPRSsetdblparam(model_env, XPRS_DBL_PAR_TIMELIMIT, - // request.solver_time_limit_seconds())); - // } - // RETURN_IF_XPRESS_ERROR( - // XPRSsetintparam(model_env, XPRS_INT_PAR_OUTPUTFLAG, - // request.enable_internal_solver_output())); - - // const int variable_size = model.variable_size(); - // bool has_integer_variables = false; - // { - // std::vector obj_coeffs(variable_size, 0); - // std::vector lb(variable_size); - // std::vector ub(variable_size); - // std::vector ctype(variable_size); - // std::vector varnames(variable_size); - // for (int v = 0; v < variable_size; ++v) { - // const MPVariableProto& variable = model.variable(v); - // obj_coeffs[v] = variable.objective_coefficient(); - // lb[v] = variable.lower_bound(); - // ub[v] = variable.upper_bound(); - // ctype[v] = variable.is_integer() && - // request.solver_type() == - // MPModelRequest::XPRESS_MIXED_INTEGER_PROGRAMMING - // ? XPRS_INTEGER - // : XPRS_CONTINUOUS; - // if (variable.is_integer()) has_integer_variables = true; - // if (!variable.name().empty()) varnames[v] = variable.name().c_str(); - // } - - // RETURN_IF_XPRESS_ERROR( - // XPRSaddvars(xpress_model, variable_size, 0, nullptr, nullptr, - // nullptr, - // /*obj=*/obj_coeffs.data(), - // /*lb=*/lb.data(), /*ub=*/ub.data(), - // /*vtype=*/ctype.data(), - // /*varnames=*/const_cast(varnames.data()))); - - // // Set solution hints if any. - // for (int i = 0; i < model.solution_hint().var_index_size(); ++i) { - // RETURN_IF_XPRESS_ERROR(XPRSsetdblattrelement( - // xpress_model, XPRS_DBL_ATTR_START, - // model.solution_hint().var_index(i), - // model.solution_hint().var_value(i))); - // } - // } - - // { - // std::vector ct_variables; - // std::vector ct_coefficients; - // for (int c = 0; c < model.constraint_size(); ++c) { - // const MPConstraintProto& constraint = model.constraint(c); - // const int size = constraint.var_index_size(); - // ct_variables.resize(size, 0); - // ct_coefficients.resize(size, 0); - // for (int i = 0; i < size; ++i) { - // ct_variables[i] = constraint.var_index(i); - // ct_coefficients[i] = constraint.coefficient(i); - // } - // // Using XPRSaddrangeconstr for constraints that don't require it - // adds - // // a slack which is not always removed by presolve. - // if (constraint.lower_bound() == constraint.upper_bound()) { - // RETURN_IF_XPRESS_ERROR(XPRSaddconstr( - // xpress_model, /*numnz=*/size, /*cind=*/ct_variables.data(), - // /*cval=*/ct_coefficients.data(), - // /*sense=*/XPRS_EQUAL, /*rhs=*/constraint.lower_bound(), - // /*constrname=*/constraint.name().c_str())); - // } else if (constraint.lower_bound() == - // -std::numeric_limits::infinity()) { - // RETURN_IF_XPRESS_ERROR(XPRSaddconstr( - // xpress_model, /*numnz=*/size, /*cind=*/ct_variables.data(), - // /*cval=*/ct_coefficients.data(), - // /*sense=*/XPRS_LESS_EQUAL, /*rhs=*/constraint.upper_bound(), - // /*constrname=*/constraint.name().c_str())); - // } else if (constraint.upper_bound() == - // std::numeric_limits::infinity()) { - // RETURN_IF_XPRESS_ERROR(XPRSaddconstr( - // xpress_model, /*numnz=*/size, /*cind=*/ct_variables.data(), - // /*cval=*/ct_coefficients.data(), - // /*sense=*/XPRS_GREATER_EQUAL, /*rhs=*/constraint.lower_bound(), - // /*constrname=*/constraint.name().c_str())); - // } else { - // RETURN_IF_XPRESS_ERROR(XPRSaddrangeconstr( - // xpress_model, /*numnz=*/size, /*cind=*/ct_variables.data(), - // /*cval=*/ct_coefficients.data(), - // /*lower=*/constraint.lower_bound(), - // /*upper=*/constraint.upper_bound(), - // /*constrname=*/constraint.name().c_str())); - // } - // } - - // for (const auto& gen_cst : model.general_constraint()) { - // switch (gen_cst.general_constraint_case()) { - // case MPGeneralConstraintProto::kIndicatorConstraint: { - // RETURN_IF_XPRESS_ERROR(AddIndicatorConstraint( - // gen_cst, xpress_model, &ct_variables, &ct_coefficients)); - // break; - // } - // case MPGeneralConstraintProto::kSosConstraint: { - // RETURN_IF_XPRESS_ERROR(AddSosConstraint(gen_cst.sos_constraint(), - // xpress_model, - // &ct_variables, - // &ct_coefficients)); - // break; - // } - // case MPGeneralConstraintProto::kQuadraticConstraint: { - // RETURN_IF_XPRESS_ERROR(AddQuadraticConstraint(gen_cst, - // xpress_model)); break; - // } - // case MPGeneralConstraintProto::kAbsConstraint: { - // RETURN_IF_XPRESS_ERROR(XPRSaddgenconstrAbs( - // xpress_model, - // /*name=*/gen_cst.name().c_str(), - // /*resvar=*/gen_cst.abs_constraint().resultant_var_index(), - // /*argvar=*/gen_cst.abs_constraint().var_index())); - // break; - // } - // case MPGeneralConstraintProto::kAndConstraint: { - // RETURN_IF_XPRESS_ERROR( - // AddAndConstraint(gen_cst, xpress_model, &ct_variables)); - // break; - // } - // case MPGeneralConstraintProto::kOrConstraint: { - // RETURN_IF_XPRESS_ERROR( - // AddOrConstraint(gen_cst, xpress_model, &ct_variables)); - // break; - // } - // case MPGeneralConstraintProto::kMinConstraint: { - // RETURN_IF_XPRESS_ERROR( - // AddMinConstraint(gen_cst, xpress_model, &ct_variables)); - // break; - // } - // case MPGeneralConstraintProto::kMaxConstraint: { - // RETURN_IF_XPRESS_ERROR( - // AddMaxConstraint(gen_cst, xpress_model, &ct_variables)); - // break; - // } - // default: - // return absl::UnimplementedError( - // absl::StrFormat("General constraints of type %i not - // supported.", - // gen_cst.general_constraint_case())); - // } - // } - // } - - // RETURN_IF_XPRESS_ERROR(XPRSsetintattr(xpress_model, - // XPRS_INT_ATTR_MODELSENSE, - // model.maximize() ? -1 : 1)); - // RETURN_IF_XPRESS_ERROR(XPRSsetdblattr(xpress_model, XPRS_DBL_ATTR_OBJCON, - // model.objective_offset())); - // if (model.has_quadratic_objective()) { - // MPQuadraticObjective qobj = model.quadratic_objective(); - // if (qobj.coefficient_size() > 0) { - // RETURN_IF_XPRESS_ERROR( - // XPRSaddqpterms(xpress_model, /*numqnz=*/qobj.coefficient_size(), - // /*qrow=*/qobj.mutable_qvar1_index()->mutable_data(), - // /*qcol=*/qobj.mutable_qvar2_index()->mutable_data(), - // /*qval=*/qobj.mutable_coefficient()->mutable_data())); - // } - // } - - // RETURN_IF_XPRESS_ERROR(XPRSupdatemodel(xpress_model)); - - // const absl::Time time_before = absl::Now(); - // UserTimer user_timer; - // user_timer.Start(); - - // RETURN_IF_XPRESS_ERROR(XPRSoptimize(xpress_model)); - - // const absl::Duration solving_duration = absl::Now() - time_before; - // user_timer.Stop(); - // VLOG(1) << "Finished solving in XPressSolveProto(), walltime = " - // << solving_duration << ", usertime = " << - // user_timer.GetDuration(); - // response.mutable_solve_info()->set_solve_wall_time_seconds( - // absl::ToDoubleSeconds(solving_duration)); - // response.mutable_solve_info()->set_solve_user_time_seconds( - // absl::ToDoubleSeconds(user_timer.GetDuration())); - - // int optimization_status = 0; - // RETURN_IF_XPRESS_ERROR( - // XPRSgetintattr(xpress_model, XPRS_INT_ATTR_STATUS, - // &optimization_status)); - // int solution_count = 0; - // RETURN_IF_XPRESS_ERROR( - // XPRSgetintattr(xpress_model, XPRS_INT_ATTR_SOLCOUNT, - // &solution_count)); - // switch (optimization_status) { - // case XPRS_OPTIMAL: - // response.set_status(MPSOLVER_OPTIMAL); - // break; - // case XPRS_INF_OR_UNBD: - // DLOG(INFO) << "XPress solve returned XPRS_INF_OR_UNBD, which we treat - // as " - // "INFEASIBLE even though it may mean UNBOUNDED."; - // response.set_status_str( - // "The model may actually be unbounded: XPress returned " - // "XPRS_INF_OR_UNBD"); - // ABSL_FALLTHROUGH_INTENDED; - // case XPRS_INFEASIBLE: - // response.set_status(MPSOLVER_INFEASIBLE); - // break; - // case XPRS_UNBOUNDED: - // response.set_status(MPSOLVER_UNBOUNDED); - // break; - // default: { - // if (solution_count > 0) { - // response.set_status(MPSOLVER_FEASIBLE); - // } else { - // response.set_status(MPSOLVER_NOT_SOLVED); - // response.set_status_str( - // absl::StrFormat("XPress status code %d", optimization_status)); - // } - // break; - // } - // } - - // if (solution_count > 0 && (response.status() == MPSOLVER_FEASIBLE || - // response.status() == MPSOLVER_OPTIMAL)) { - // double objective_value = 0; - // RETURN_IF_XPRESS_ERROR( - // XPRSgetdblattr(xpress_model, XPRS_DBL_ATTR_OBJVAL, - // &objective_value)); - // response.set_objective_value(objective_value); - // double best_objective_bound = 0; - // const int error = XPRSgetdblattr(xpress_model, XPRS_DBL_ATTR_OBJBOUND, - // &best_objective_bound); - // if (response.status() == MPSOLVER_OPTIMAL && - // error == XPRS_ERROR_DATA_NOT_AVAILABLE) { - // // If the presolve deletes all variables, there's no best bound. - // response.set_best_objective_bound(objective_value); - // } else { - // RETURN_IF_XPRESS_ERROR(error); - // response.set_best_objective_bound(best_objective_bound); - // } - - // response.mutable_variable_value()->Resize(variable_size, 0); - // RETURN_IF_XPRESS_ERROR( - // XPRSgetdblattrarray(xpress_model, XPRS_DBL_ATTR_X, 0, - // variable_size, - // response.mutable_variable_value()->mutable_data())); - // // NOTE, XPressSolveProto() is exposed to external clients via MPSolver - // API, - // // which assumes the solution values of integer variables are rounded - // to - // // integer values. - // auto round_values_of_integer_variables_fn = - // [&](google::protobuf::RepeatedField* values) { - // for (int v = 0; v < variable_size; ++v) { - // if (model.variable(v).is_integer()) { - // (*values)[v] = std::round((*values)[v]); - // } - // } - // }; - // round_values_of_integer_variables_fn(response.mutable_variable_value()); - // if (!has_integer_variables && model.general_constraint_size() == 0) { - // response.mutable_dual_value()->Resize(model.constraint_size(), 0); - // RETURN_IF_XPRESS_ERROR(XPRSgetdblattrarray( - // xpress_model, XPRS_DBL_ATTR_PI, 0, model.constraint_size(), - // response.mutable_dual_value()->mutable_data())); - // } - // const int additional_solutions = std::min( - // solution_count, - // std::min(request.populate_additional_solutions_up_to(), - // std::numeric_limits::max() - 1) + - // 1); - // for (int i = 1; i < additional_solutions; ++i) { - // RETURN_IF_XPRESS_ERROR( - // XPRSsetintparam(model_env, XPRS_INT_PAR_SOLUTIONNUMBER, i)); - // MPSolution* solution = response.add_additional_solutions(); - // solution->mutable_variable_value()->Resize(variable_size, 0); - // double objective_value = 0; - // RETURN_IF_XPRESS_ERROR(XPRSgetdblattr( - // xpress_model, XPRS_DBL_ATTR_POOLOBJVAL, &objective_value)); - // solution->set_objective_value(objective_value); - // RETURN_IF_XPRESS_ERROR(XPRSgetdblattrarray( - // xpress_model, XPRS_DBL_ATTR_XN, 0, variable_size, - // solution->mutable_variable_value()->mutable_data())); - // round_values_of_integer_variables_fn(solution->mutable_variable_value()); - // } - // } - // #undef RETURN_IF_XPRESS_ERRORx(i), - // model.solution_hint().var_value(i))); - // } - // } - - // { - // std::vector ct_variables; - // std::vector ct_coefficients; - // for (int c = 0; c < model.constraint_size(); ++c) { - // const MPConstraintProto& constraint = model.constraint(c); - // const int size = constraint.var_index_size(); - // ct_variables.resize(size, 0); - // ct_coefficients.resize(size, 0); - // for (int i = 0; i < size; ++i) { - // ct_variables[i] = constraint.var_index(i); - // ct_coefficients[i] = constraint.coefficient(i); - // } - // // Using XPRSaddrangeconstr for constraints that don't require it - // adds - // // a slack which is not always removed by presolve. - // if (constraint.lower_bound() == constraint.upper_bound()) { - // RETURN_IF_XPRESS_ERROR(XPRSaddconstr( - // xpress_model, /*numnz=*/size, /*cind=*/ct_variables.data(), - // /*cval=*/ct_coefficients.data(), - // /*sense=*/XPRS_EQUAL, /*rhs=*/constraint.lower_bound(), - // /*constrname=*/constraint.name().c_str())); - // } else if (constraint.lower_bound() == - // -std::numeric_limits::infinity()) { - // RETURN_IF_XPRESS_ERROR(XPRSaddconstr( - // xpress_model, /*numnz=*/size, /*cind=*/ct_variables.data(), - // /*cval=*/ct_coefficients.data(), - // /*sense=*/XPRS_LESS_EQUAL, /*rhs=*/constraint.upper_bound(), - // /*constrname=*/constraint.name().c_str())); - // } else if (constraint.upper_bound() == - // std::numeric_limits::infinity()) { - // RETURN_IF_XPRESS_ERROR(XPRSaddconstr( - // xpress_model, /*numnz=*/size, /*cind=*/ct_variables.data(), - // /*cval=*/ct_coefficients.data(), - // /*sense=*/XPRS_GREATER_EQUAL, /*rhs=*/constraint.lower_bound(), - // /*constrname=*/constraint.name().c_str())); - // } else { - // RETURN_IF_XPRESS_ERROR(XPRSaddrangeconstr( - // xpress_model, /*numnz=*/size, /*cind=*/ct_variables.data(), - // /*cval=*/ct_coefficients.data(), - // /*lower=*/constraint.lower_bound(), - // /*upper=*/constraint.upper_bound(), - // /*constrname=*/constraint.name().c_str())); - // } - // } - - // for (const auto& gen_cst : model.general_constraint()) { - // switch (gen_cst.general_constraint_case()) { - // case MPGeneralConstraintProto::kIndicatorConstraint: { - // RETURN_IF_XPRESS_ERROR(AddIndicatorConstraint( - // gen_cst, xpress_model, &ct_variables, &ct_coefficients)); - // break; - // } - // case MPGeneralConstraintProto::kSosConstraint: { - // RETURN_IF_XPRESS_ERROR(AddSosConstraint(gen_cst.sos_constraint(), - // xpress_model, - // &ct_variables, - // &ct_coefficients)); - // break; - // } - // case MPGeneralConstraintProto::kQuadraticConstraint: { - // RETURN_IF_XPRESS_ERROR(AddQuadraticConstraint(gen_cst, - // xpress_model)); break; - // } - // case MPGeneralConstraintProto::kAbsConstraint: { - // RETURN_IF_XPRESS_ERROR(XPRSaddgenconstrAbs( - // xpress_model, - // /*name=*/gen_cst.name().c_str(), - // /*resvar=*/gen_cst.abs_constraint().resultant_var_index(), - // /*argvar=*/gen_cst.abs_constraint().var_index())); - // break; - // } - // case MPGeneralConstraintProto::kAndConstraint: { - // RETURN_IF_XPRESS_ERROR( - // AddAndConstraint(gen_cst, xpress_model, &ct_variables)); - // break; - // } - // case MPGeneralConstraintProto::kOrConstraint: { - // RETURN_IF_XPRESS_ERROR( - // AddOrConstraint(gen_cst, xpress_model, &ct_variables)); - // break; - // } - // case MPGeneralConstraintProto::kMinConstraint: { - // RETURN_IF_XPRESS_ERROR( - // AddMinConstraint(gen_cst, xpress_model, &ct_variables)); - // break; - // } - // case MPGeneralConstraintProto::kMaxConstraint: { - // RETURN_IF_XPRESS_ERROR( - // AddMaxConstraint(gen_cst, xpress_model, &ct_variables)); - // break; - // } - // default: - // return absl::UnimplementedError( - // absl::StrFormat("General constraints of type %i not - // supported.", - // gen_cst.general_constraint_case())); - // } - // } - // } - - // RETURN_IF_XPRESS_ERROR(XPRSsetintattr(xpress_model, - // XPRS_INT_ATTR_MODELSENSE, - // model.maximize() ? -1 : 1)); - // RETURN_IF_XPRESS_ERROR(XPRSsetdblattr(xpress_model, XPRS_DBL_ATTR_OBJCON, - // model.objective_offset())); - // if (model.has_quadratic_objective()) { - // MPQuadraticObjective qobj = model.quadratic_objective(); - // if (qobj.coefficient_size() > 0) { - // RETURN_IF_XPRESS_ERROR( - // XPRSaddqpterms(xpress_model, /*numqnz=*/qobj.coefficient_size(), - // /*qrow=*/qobj.mutable_qvar1_index()->mutable_data(), - // /*qcol=*/qobj.mutable_qvar2_index()->mutable_data(), - // /*qval=*/qobj.mutable_coefficient()->mutable_data())); - // } - // } - - // RETURN_IF_XPRESS_ERROR(XPRSupdatemodel(xpress_model)); - - // const absl::Time time_before = absl::Now(); - // UserTimer user_timer; - // user_timer.Start(); - - // RETURN_IF_XPRESS_ERROR(XPRSoptimize(xpress_model)); - - // const absl::Duration solving_duration = absl::Now() - time_before; - // user_timer.Stop(); - // VLOG(1) << "Finished solving in XPressSolveProto(), walltime = " - // << solving_duration << ", usertime = " << - // user_timer.GetDuration(); - // response.mutable_solve_info()->set_solve_wall_time_seconds( - // absl::ToDoubleSeconds(solving_duration)); - // response.mutable_solve_info()->set_solve_user_time_seconds( - // absl::ToDoubleSeconds(user_timer.GetDuration())); - - // int optimization_status = 0; - // RETURN_IF_XPRESS_ERROR( - // XPRSgetintattr(xpress_model, XPRS_INT_ATTR_STATUS, - // &optimization_status)); - // int solution_count = 0; - // RETURN_IF_XPRESS_ERROR( - // XPRSgetintattr(xpress_model, XPRS_INT_ATTR_SOLCOUNT, - // &solution_count)); - // switch (optimization_status) { - // case XPRS_OPTIMAL: - // response.set_status(MPSOLVER_OPTIMAL); - // break; - // case XPRS_INF_OR_UNBD: - // DLOG(INFO) << "XPress solve returned XPRS_INF_OR_UNBD, which we treat - // as " - // "INFEASIBLE even though it may mean UNBOUNDED."; - // response.set_status_str( - // "The model may actually be unbounded: XPress returned " - // "XPRS_INF_OR_UNBD"); - // ABSL_FALLTHROUGH_INTENDED; - // case XPRS_INFEASIBLE: - // response.set_status(MPSOLVER_INFEASIBLE); - // break; - // case XPRS_UNBOUNDED: - // response.set_status(MPSOLVER_UNBOUNDED); - // break; - // default: { - // if (solution_count > 0) { - // response.set_status(MPSOLVER_FEASIBLE); - // } else { - // response.set_status(MPSOLVER_NOT_SOLVED); - // response.set_status_str( - // absl::StrFormat("XPress status code %d", optimization_status)); - // } - // break; - // } - // } - - // if (solution_count > 0 && (response.status() == MPSOLVER_FEASIBLE || - // response.status() == MPSOLVER_OPTIMAL)) { - // double objective_value = 0; - // RETURN_IF_XPRESS_ERROR( - // XPRSgetdblattr(xpress_model, XPRS_DBL_ATTR_OBJVAL, - // &objective_value)); - // response.set_objective_value(objective_value); - // double best_objective_bound = 0; - // const int error = XPRSgetdblattr(xpress_model, XPRS_DBL_ATTR_OBJBOUND, - // &best_objective_bound); - // if (response.status() == MPSOLVER_OPTIMAL && - // error == XPRS_ERROR_DATA_NOT_AVAILABLE) { - // // If the presolve deletes all variables, there's no best bound. - // response.set_best_objective_bound(objective_value); - // } else { - // RETURN_IF_XPRESS_ERROR(error); - // response.set_best_objective_bound(best_objective_bound); - // } - - // response.mutable_variable_value()->Resize(variable_size, 0); - // RETURN_IF_XPRESS_ERROR( - // XPRSgetdblattrarray(xpress_model, XPRS_DBL_ATTR_X, 0, - // variable_size, - // response.mutable_variable_value()->mutable_data())); - // // NOTE, XPressSolveProto() is exposed to external clients via MPSolver - // API, - // // which assumes the solution values of integer variables are rounded - // to - // // integer values. - // auto round_values_of_integer_variables_fn = - // [&](google::protobuf::RepeatedField* values) { - // for (int v = 0; v < variable_size; ++v) { - // if (model.variable(v).is_integer()) { - // (*values)[v] = std::round((*values)[v]); - // } - // } - // }; - // round_values_of_integer_variables_fn(response.mutable_variable_value()); - // if (!has_integer_variables && model.general_constraint_size() == 0) { - // response.mutable_dual_value()->Resize(model.constraint_size(), 0); - // RETURN_IF_XPRESS_ERROR(XPRSgetdblattrarray( - // xpress_model, XPRS_DBL_ATTR_PI, 0, model.constraint_size(), - // response.mutable_dual_value()->mutable_data())); - // } - // const int additional_solutions = std::min( - // solution_count, - // std::min(request.populate_additional_solutions_up_to(), - // std::numeric_limits::max() - 1) + - // 1); - // for (int i = 1; i < additional_solutions; ++i) { - // RETURN_IF_XPRESS_ERROR( - // XPRSsetintparam(model_env, XPRS_INT_PAR_SOLUTIONNUMBER, i)); - // MPSolution* solution = response.add_additional_solutions(); - // solution->mutable_variable_value()->Resize(variable_size, 0); - // double objective_value = 0; - // RETURN_IF_XPRESS_ERROR(XPRSgetdblattr( - // xpress_model, XPRS_DBL_ATTR_POOLOBJVAL, &objective_value)); - // solution->set_objective_value(objective_value); - // RETURN_IF_XPRESS_ERROR(XPRSgetdblattrarray( - // xpress_model, XPRS_DBL_ATTR_XN, 0, variable_size, - // solution->mutable_variable_value()->mutable_data())); - // round_values_of_integer_variables_fn(solution->mutable_variable_value()); - // } - // } - // #undef RETURN_IF_XPRESS_ERROR - - return response; -} - -} // namespace operations_research diff --git a/ortools/linear_solver/proto_solver/xpress_proto_solver.h b/ortools/linear_solver/proto_solver/xpress_proto_solver.h deleted file mode 100644 index c56e5acc881..00000000000 --- a/ortools/linear_solver/proto_solver/xpress_proto_solver.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2010-2025 Google LLC -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#ifndef OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_XPRESS_PROTO_SOLVER_H_ -#define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_XPRESS_PROTO_SOLVER_H_ - -#include "ortools/linear_solver/linear_solver.pb.h" -#include "ortools/util/lazy_mutable_copy.h" - -namespace operations_research { - -// Solves the input request. -MPSolutionResponse XPressSolveProto(LazyMutableCopy request); - -} // namespace operations_research - -#endif // OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_XPRESS_PROTO_SOLVER_H_ diff --git a/ortools/linear_solver/python/linear_solver.i b/ortools/linear_solver/python/linear_solver.i index 087622250b2..2309ea3e4c1 100644 --- a/ortools/linear_solver/python/linear_solver.i +++ b/ortools/linear_solver/python/linear_solver.i @@ -24,7 +24,7 @@ // solver.Maximize(10 * x1 + 6 * x2) // // USAGE EXAMPLES: -// - ortools/python/linear_programming.py +// - examples/python/linear_programming.py // - ./pywraplp_test.py // // TODO(user): test all the APIs that are currently marked as 'untested'. diff --git a/ortools/linear_solver/python/linear_solver_natural_api.py b/ortools/linear_solver/python/linear_solver_natural_api.py index 45e92652897..a5956500b12 100644 --- a/ortools/linear_solver/python/linear_solver_natural_api.py +++ b/ortools/linear_solver/python/linear_solver_natural_api.py @@ -31,266 +31,270 @@ class _FakeMPVariableRepresentingTheConstantOffset: - """A dummy class for a singleton instance used to represent the constant. + """A dummy class for a singleton instance used to represent the constant. - To represent linear expressions, we store a dictionary - MPVariable->coefficient. To represent the constant offset of the expression, - we use this class as a substitute: its coefficient will be the offset. To - properly be evaluated, its solution_value() needs to be 1. - """ + To represent linear expressions, we store a dictionary + MPVariable->coefficient. To represent the constant offset of the expression, + we use this class as a substitute: its coefficient will be the offset. To + properly be evaluated, its solution_value() needs to be 1. + """ - def solution_value(self): # pylint: disable=invalid-name - return 1 + def solution_value(self): # pylint: disable=invalid-name + return 1 - def __repr__(self): - return "OFFSET_KEY" + def __repr__(self): + return "OFFSET_KEY" OFFSET_KEY = _FakeMPVariableRepresentingTheConstantOffset() def CastToLinExp(v): - if isinstance(v, numbers.Number): - return Constant(v) - else: - return v + if isinstance(v, numbers.Number): + return Constant(v) + else: + return v class LinearExpr: - """Holds linear expressions. - - A linear expression is essentially an offset (floating-point value), and a - dictionary mapping MPVariable objects to their coefficient (which is also a - floating-point value). + """Holds linear expressions. + + A linear expression is essentially an offset (floating-point value), and a + dictionary mapping MPVariable objects to their coefficient (which is also a + floating-point value). + """ + + OVERRIDDEN_OPERATOR_METHODS = [ + "__%s__" % opname + for opname in [ + "add", + "radd", + "sub", + "rsub", + "mul", + "rmul", + "div", + "truediv", + "neg", + "eq", + "ge", + "le", + "gt", + "lt", + "ne", + ] + ] + + def solution_value(self): # pylint: disable=invalid-name + """Value of this linear expr, using the solution_value of its vars.""" + coeffs = self.GetCoeffs() + return sum(var.solution_value() * coeff for var, coeff in coeffs.items()) + + def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): + """Private function used by GetCoeffs() to delegate processing. + + Implementation must either update coeffs or push to the stack a + sub-expression and the accumulated multiplier that applies to it. + + Args: + coeffs: A dictionary of variables' coefficients. It is a defaultdict that + initializes the new values to 0 by default. + multiplier: The current accumulated multiplier to apply to this + expression. + stack: A list to append to if the current expression is composed of + sub-expressions. The elements of the stack are pair tuples + (multiplier, linear_expression). """ + raise NotImplementedError + + def GetCoeffs(self): + coeffs = collections.defaultdict(float) + stack = [(1.0, self)] + while stack: + current_multiplier, current_expression = stack.pop() + current_expression.AddSelfToCoeffMapOrStack( + coeffs, current_multiplier, stack + ) + return coeffs + + def __add__(self, expr): + return Sum(self, expr) + + def __radd__(self, cst): + return Sum(self, cst) + + def __sub__(self, expr): + return Sum(self, -expr) + + def __rsub__(self, cst): + return Sum(-self, cst) + + def __mul__(self, cst): + return ProductCst(self, cst) + + def __rmul__(self, cst): + return ProductCst(self, cst) + + def __div__(self, cst): + return ProductCst(self, 1.0 / cst) + + def __truediv__(self, cst): + return ProductCst(self, 1.0 / cst) - OVERRIDDEN_OPERATOR_METHODS = [ - "__%s__" % opname - for opname in [ - "add", - "radd", - "sub", - "rsub", - "mul", - "rmul", - "div", - "truediv", - "neg", - "eq", - "ge", - "le", - "gt", - "lt", - "ne", - ] - ] - - def solution_value(self): # pylint: disable=invalid-name - """Value of this linear expr, using the solution_value of its vars.""" - coeffs = self.GetCoeffs() - return sum(var.solution_value() * coeff for var, coeff in coeffs.items()) - - def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): - """Private function used by GetCoeffs() to delegate processing. - - Implementation must either update coeffs or push to the stack a - sub-expression and the accumulated multiplier that applies to it. - - Args: - coeffs: A dictionary of variables' coefficients. It is a defaultdict that - initializes the new values to 0 by default. - multiplier: The current accumulated multiplier to apply to this - expression. - stack: A list to append to if the current expression is composed of - sub-expressions. The elements of the stack are pair tuples - (multiplier, linear_expression). - """ - raise NotImplementedError - - def GetCoeffs(self): - coeffs = collections.defaultdict(float) - stack = [(1.0, self)] - while stack: - current_multiplier, current_expression = stack.pop() - current_expression.AddSelfToCoeffMapOrStack( - coeffs, current_multiplier, stack - ) - return coeffs - - def __add__(self, expr): - return Sum(self, expr) - - def __radd__(self, cst): - return Sum(self, cst) - - def __sub__(self, expr): - return Sum(self, -expr) - - def __rsub__(self, cst): - return Sum(-self, cst) - - def __mul__(self, cst): - return ProductCst(self, cst) - - def __rmul__(self, cst): - return ProductCst(self, cst) - - def __div__(self, cst): - return ProductCst(self, 1.0 / cst) - - def __truediv__(self, cst): - return ProductCst(self, 1.0 / cst) - - def __neg__(self): - return ProductCst(self, -1) - - def __eq__(self, arg): - if isinstance(arg, numbers.Number): - return LinearConstraint(self, arg, arg) - else: - return LinearConstraint(self - arg, 0.0, 0.0) - - def __ge__(self, arg): - if isinstance(arg, numbers.Number): - return LinearConstraint(self, arg, inf) - else: - return LinearConstraint(self - arg, 0.0, inf) - - def __le__(self, arg): - if isinstance(arg, numbers.Number): - return LinearConstraint(self, -inf, arg) - else: - return LinearConstraint(self - arg, -inf, 0.0) - - def __lt__(self, arg): - raise ValueError('Operators "<" and ">" not supported with the linear solver') - - def __gt__(self, arg): - raise ValueError('Operators "<" and ">" not supported with the linear solver') - - def __ne__(self, arg): - raise ValueError('Operator "!=" not supported with the linear solver') + def __neg__(self): + return ProductCst(self, -1) + + def __eq__(self, arg): + if isinstance(arg, numbers.Number): + return LinearConstraint(self, arg, arg) + else: + return LinearConstraint(self - arg, 0.0, 0.0) + + def __ge__(self, arg): + if isinstance(arg, numbers.Number): + return LinearConstraint(self, arg, inf) + else: + return LinearConstraint(self - arg, 0.0, inf) + + def __le__(self, arg): + if isinstance(arg, numbers.Number): + return LinearConstraint(self, -inf, arg) + else: + return LinearConstraint(self - arg, -inf, 0.0) + + def __lt__(self, arg): + raise ValueError( + 'Operators "<" and ">" not supported with the linear solver' + ) + + def __gt__(self, arg): + raise ValueError( + 'Operators "<" and ">" not supported with the linear solver' + ) + + def __ne__(self, arg): + raise ValueError('Operator "!=" not supported with the linear solver') class VariableExpr(LinearExpr): - """Represents a LinearExpr containing only a single variable.""" + """Represents a LinearExpr containing only a single variable.""" - def __init__(self, mpvar): - self.__var = mpvar + def __init__(self, mpvar): + self.__var = mpvar - def __str__(self): - return str(self.__var) + def __str__(self): + return str(self.__var) - def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): - coeffs[self.__var] += multiplier + def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): + coeffs[self.__var] += multiplier class ProductCst(LinearExpr): - """Represents the product of a LinearExpr by a constant.""" + """Represents the product of a LinearExpr by a constant.""" - def __init__(self, expr, coef): - self.__expr = CastToLinExp(expr) - if isinstance(coef, numbers.Number): - self.__coef = coef - else: - raise TypeError + def __init__(self, expr, coef): + self.__expr = CastToLinExp(expr) + if isinstance(coef, numbers.Number): + self.__coef = coef + else: + raise TypeError - def __str__(self): - if self.__coef == -1: - return "-" + str(self.__expr) - else: - return "(" + str(self.__coef) + " * " + str(self.__expr) + ")" + def __str__(self): + if self.__coef == -1: + return "-" + str(self.__expr) + else: + return "(" + str(self.__coef) + " * " + str(self.__expr) + ")" - def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): - current_multiplier = multiplier * self.__coef - if current_multiplier: - stack.append((current_multiplier, self.__expr)) + def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): + current_multiplier = multiplier * self.__coef + if current_multiplier: + stack.append((current_multiplier, self.__expr)) class Constant(LinearExpr): - def __init__(self, val): - self.__val = val + def __init__(self, val): + self.__val = val - def __str__(self): - return str(self.__val) + def __str__(self): + return str(self.__val) - def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): - coeffs[OFFSET_KEY] += self.__val * multiplier + def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): + coeffs[OFFSET_KEY] += self.__val * multiplier class SumArray(LinearExpr): - """Represents the sum of a list of LinearExpr.""" - - def __init__(self, array): - self.__array = [CastToLinExp(elem) for elem in array] - - def __str__(self): - parts = [] - for term in map(str, self.__array): - if not parts: - parts.append(term) - continue - if term[0] == "-": - parts.append(" - " + term[1:]) - else: - parts.append(" + " + term) - return f'({"".join(parts)})' - - def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): - # Append elements in reversed order so that the first popped from the stack - # in the next iteration of the evaluation loop will be the first item of the - # array. This keeps the end result of the floating point computation - # predictable from user perspective. - for arg in reversed(self.__array): - stack.append((multiplier, arg)) + """Represents the sum of a list of LinearExpr.""" + + def __init__(self, array): + self.__array = [CastToLinExp(elem) for elem in array] + + def __str__(self): + parts = [] + for term in map(str, self.__array): + if not parts: + parts.append(term) + continue + if term[0] == "-": + parts.append(" - " + term[1:]) + else: + parts.append(" + " + term) + return f'({"".join(parts)})' + + def AddSelfToCoeffMapOrStack(self, coeffs, multiplier, stack): + # Append elements in reversed order so that the first popped from the stack + # in the next iteration of the evaluation loop will be the first item of the + # array. This keeps the end result of the floating point computation + # predictable from user perspective. + for arg in reversed(self.__array): + stack.append((multiplier, arg)) def Sum(*args): - return SumArray(args) + return SumArray(args) SumCst = Sum # pylint: disable=invalid-name class LinearConstraint: - """Represents a linear constraint: LowerBound <= LinearExpr <= UpperBound.""" - - def __init__(self, expr, lb, ub): - self.__expr = expr - self.__lb = lb - self.__ub = ub - - def __str__(self): - if self.__lb > -inf and self.__ub < inf: - if self.__lb == self.__ub: - return str(self.__expr) + " == " + str(self.__lb) - else: - return ( - str(self.__lb) + " <= " + str(self.__expr) + " <= " + str(self.__ub) - ) - elif self.__lb > -inf: - return str(self.__expr) + " >= " + str(self.__lb) - elif self.__ub < inf: - return str(self.__expr) + " <= " + str(self.__ub) - else: - return "Trivial inequality (always true)" - - def Extract(self, solver, name=""): - """Performs the actual creation of the constraint object.""" - coeffs = self.__expr.GetCoeffs() - constant = coeffs.pop(OFFSET_KEY, 0.0) - lb = -solver.infinity() - ub = solver.infinity() - if self.__lb > -inf: - lb = self.__lb - constant - if self.__ub < inf: - ub = self.__ub - constant - - constraint = solver.RowConstraint(lb, ub, name) - for ( - v, - c, - ) in coeffs.items(): - constraint.SetCoefficient(v, float(c)) - return constraint + """Represents a linear constraint: LowerBound <= LinearExpr <= UpperBound.""" + + def __init__(self, expr, lb, ub): + self.__expr = expr + self.__lb = lb + self.__ub = ub + + def __str__(self): + if self.__lb > -inf and self.__ub < inf: + if self.__lb == self.__ub: + return str(self.__expr) + " == " + str(self.__lb) + else: + return ( + str(self.__lb) + " <= " + str(self.__expr) + " <= " + str(self.__ub) + ) + elif self.__lb > -inf: + return str(self.__expr) + " >= " + str(self.__lb) + elif self.__ub < inf: + return str(self.__expr) + " <= " + str(self.__ub) + else: + return "Trivial inequality (always true)" + + def Extract(self, solver, name=""): + """Performs the actual creation of the constraint object.""" + coeffs = self.__expr.GetCoeffs() + constant = coeffs.pop(OFFSET_KEY, 0.0) + lb = -solver.infinity() + ub = solver.infinity() + if self.__lb > -inf: + lb = self.__lb - constant + if self.__ub < inf: + ub = self.__ub - constant + + constraint = solver.RowConstraint(lb, ub, name) + for ( + v, + c, + ) in coeffs.items(): + constraint.SetCoefficient(v, float(c)) + return constraint diff --git a/ortools/linear_solver/python/lp_api_test.py b/ortools/linear_solver/python/lp_api_test.py index 6f2879e4ff7..a4bf4ca92bb 100755 --- a/ortools/linear_solver/python/lp_api_test.py +++ b/ortools/linear_solver/python/lp_api_test.py @@ -7,17 +7,17 @@ def Sum(arg): - if type(arg) is types.GeneratorType: - arg = [x for x in arg] - sum = 0 - for i in arg: - sum += i - print("sum(%s) = %d" % (str(arg), sum)) + if type(arg) is types.GeneratorType: + arg = [x for x in arg] + sum = 0 + for i in arg: + sum += i + print("sum(%s) = %d" % (str(arg), sum)) def test_sum_no_brackets(): - Sum(x for x in range(10) if x % 2 == 0) - Sum([x for x in range(10) if x % 2 == 0]) + Sum(x for x in range(10) if x % 2 == 0) + Sum([x for x in range(10) if x % 2 == 0]) text_model = """ @@ -37,24 +37,24 @@ def test_sum_no_brackets(): def test_proto(): - input_proto = linear_solver_pb2.MPModelRequest() - text_format.Merge(text_model, input_proto) - solver = pywraplp.Solver.CreateSolver("glop") - print(input_proto) - # For now, create the model from the proto by parsing the proto - solver.LoadModelFromProto(input_proto.model) - solver.EnableOutput() - solver.Solve() - # Fill solution - solution = linear_solver_pb2.MPSolutionResponse() - solver.FillSolutionResponseProto(solution) - print(solution) + input_proto = linear_solver_pb2.MPModelRequest() + text_format.Merge(text_model, input_proto) + solver = pywraplp.Solver.CreateSolver("glop") + print(input_proto) + # For now, create the model from the proto by parsing the proto + solver.LoadModelFromProto(input_proto.model) + solver.EnableOutput() + solver.Solve() + # Fill solution + solution = linear_solver_pb2.MPSolutionResponse() + solver.FillSolutionResponseProto(solution) + print(solution) def main(): - test_sum_no_brackets() - test_proto() + test_sum_no_brackets() + test_proto() if __name__ == "__main__": - main() + main() diff --git a/ortools/linear_solver/python/lp_test.py b/ortools/linear_solver/python/lp_test.py index 8c1e99f9cac..1eb7c81d284 100755 --- a/ortools/linear_solver/python/lp_test.py +++ b/ortools/linear_solver/python/lp_test.py @@ -20,253 +20,247 @@ class PyWrapLpTest(unittest.TestCase): - def RunLinearExampleNaturalLanguageAPI(self, optimization_problem_type): - """Example of simple linear program with natural language API.""" - solver = pywraplp.Solver( - "RunLinearExampleNaturalLanguageAPI", optimization_problem_type - ) - infinity = solver.infinity() - # x1, x2 and x3 are continuous non-negative variables. - x1 = solver.NumVar(0.0, infinity, "x1") - x2 = solver.NumVar(0.0, infinity, "x2") - x3 = solver.NumVar(0.0, infinity, "x3") - - solver.Maximize(10 * x1 + 6 * x2 + 4 * x3) - c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, "ConstraintName0") - c1 = solver.Add(2 * x1 + 2 * x2 + 6 * x3 <= 300) - sum_of_vars = sum([x1, x2, x3]) - c2 = solver.Add(sum_of_vars <= 100.0, "OtherConstraintName") - - self.SolveAndPrint( - solver, - [x1, x2, x3], - [c0, c1, c2], - optimization_problem_type != pywraplp.Solver.PDLP_LINEAR_PROGRAMMING, - ) - - # Print a linear expression's solution value. - print(("Sum of vars: %s = %s" % (sum_of_vars, sum_of_vars.solution_value()))) - - def RunLinearExampleCppStyleAPI(self, optimization_problem_type): - """Example of simple linear program with the C++ style API.""" - solver = pywraplp.Solver("RunLinearExampleCppStyle", optimization_problem_type) - infinity = solver.infinity() - # x1, x2 and x3 are continuous non-negative variables. - x1 = solver.NumVar(0.0, infinity, "x1") - x2 = solver.NumVar(0.0, infinity, "x2") - x3 = solver.NumVar(0.0, infinity, "x3") - - # Maximize 10 * x1 + 6 * x2 + 4 * x3. - objective = solver.Objective() - objective.SetCoefficient(x1, 10) - objective.SetCoefficient(x2, 6) - objective.SetCoefficient(x3, 4) - objective.SetMaximization() - - # x1 + x2 + x3 <= 100. - c0 = solver.Constraint(-infinity, 100.0, "c0") - c0.SetCoefficient(x1, 1) - c0.SetCoefficient(x2, 1) - c0.SetCoefficient(x3, 1) - - # 10 * x1 + 4 * x2 + 5 * x3 <= 600. - c1 = solver.Constraint(-infinity, 600.0, "c1") - c1.SetCoefficient(x1, 10) - c1.SetCoefficient(x2, 4) - c1.SetCoefficient(x3, 5) - - # 2 * x1 + 2 * x2 + 6 * x3 <= 300. - c2 = solver.Constraint(-infinity, 300.0, "c2") - c2.SetCoefficient(x1, 2) - c2.SetCoefficient(x2, 2) - c2.SetCoefficient(x3, 6) - - self.SolveAndPrint( - solver, - [x1, x2, x3], - [c0, c1, c2], - optimization_problem_type != pywraplp.Solver.PDLP_LINEAR_PROGRAMMING, - ) - - def RunMixedIntegerExampleCppStyleAPI(self, optimization_problem_type): - """Example of simple mixed integer program with the C++ style API.""" - solver = pywraplp.Solver( - "RunMixedIntegerExampleCppStyle", optimization_problem_type - ) - infinity = solver.infinity() - # x1 and x2 are integer non-negative variables. - x1 = solver.IntVar(0.0, infinity, "x1") - x2 = solver.IntVar(0.0, infinity, "x2") - - # Maximize x1 + 10 * x2. - objective = solver.Objective() - objective.SetCoefficient(x1, 1) - objective.SetCoefficient(x2, 10) - objective.SetMaximization() - - # x1 + 7 * x2 <= 17.5. - c0 = solver.Constraint(-infinity, 17.5, "c0") - c0.SetCoefficient(x1, 1) - c0.SetCoefficient(x2, 7) - - # x1 <= 3.5. - c1 = solver.Constraint(-infinity, 3.5, "c1") - c1.SetCoefficient(x1, 1) - c1.SetCoefficient(x2, 0) - - self.SolveAndPrint(solver, [x1, x2], [c0, c1], True) - - def RunBooleanExampleCppStyleAPI(self, optimization_problem_type): - """Example of simple boolean program with the C++ style API.""" - solver = pywraplp.Solver("RunBooleanExampleCppStyle", optimization_problem_type) - # x1 and x2 are integer non-negative variables. - x1 = solver.BoolVar("x1") - x2 = solver.BoolVar("x2") - - # Minimize 2 * x1 + x2. - objective = solver.Objective() - objective.SetCoefficient(x1, 2) - objective.SetCoefficient(x2, 1) - objective.SetMinimization() - - # 1 <= x1 + 2 * x2 <= 3. - c0 = solver.Constraint(1, 3, "c0") - c0.SetCoefficient(x1, 1) - c0.SetCoefficient(x2, 2) - - self.SolveAndPrint(solver, [x1, x2], [c0], True) - - def SolveAndPrint(self, solver, variable_list, constraint_list, is_precise): - """Solve the problem and print the solution.""" - print(("Number of variables = %d" % solver.NumVariables())) - self.assertEqual(solver.NumVariables(), len(variable_list)) - - print(("Number of constraints = %d" % solver.NumConstraints())) - self.assertEqual(solver.NumConstraints(), len(constraint_list)) - - result_status = solver.Solve() - - # The problem has an optimal solution. - self.assertEqual(result_status, pywraplp.Solver.OPTIMAL) - - # The solution looks legit (when using solvers others than - # GLOP_LINEAR_PROGRAMMING, verifying the solution is highly recommended!). - if is_precise: - self.assertTrue(solver.VerifySolution(1e-7, True)) - - print(("Problem solved in %f milliseconds" % solver.wall_time())) - - # The objective value of the solution. - print(("Optimal objective value = %f" % solver.Objective().Value())) - - # The value of each variable in the solution. - for variable in variable_list: - print(("%s = %f" % (variable.name(), variable.solution_value()))) - - print("Advanced usage:") - print(("Problem solved in %d iterations" % solver.iterations())) - if not solver.IsMip(): - for variable in variable_list: - print( - ( - "%s: reduced cost = %f" - % (variable.name(), variable.reduced_cost()) - ) - ) - activities = solver.ComputeConstraintActivities() - for i, constraint in enumerate(constraint_list): - print( - ( - "constraint %d: dual value = %f\n" - " activity = %f" - % (i, constraint.dual_value(), activities[constraint.index()]) - ) - ) - - def testApi(self): - print("testApi", flush=True) - all_names_and_problem_types = list( - linear_solver_pb2.MPModelRequest.SolverType.items() - ) - for name, problem_type in all_names_and_problem_types: - with self.subTest(f"{name}: {problem_type}"): - print(f"######## {name}:{problem_type} #######", flush=True) - if not pywraplp.Solver.SupportsProblemType(problem_type): - continue - if name.startswith("GUROBI"): - continue - if name.startswith("KNAPSACK"): - continue - if not name.startswith("SCIP"): - continue - if name.endswith("LINEAR_PROGRAMMING"): - print(("\n------ Linear programming example with %s ------" % name)) - print("\n*** Natural language API ***") - self.RunLinearExampleNaturalLanguageAPI(problem_type) - print("\n*** C++ style API ***") - self.RunLinearExampleCppStyleAPI(problem_type) - elif name.endswith("MIXED_INTEGER_PROGRAMMING"): - print( - ( - "\n------ Mixed Integer programming example with %s ------" - % name - ) - ) - print("\n*** C++ style API ***") - self.RunMixedIntegerExampleCppStyleAPI(problem_type) - elif name.endswith("INTEGER_PROGRAMMING"): - print( - ("\n------ Boolean programming example with %s ------" % name) - ) - print("\n*** C++ style API ***") - self.RunBooleanExampleCppStyleAPI(problem_type) - else: - print("ERROR: %s unsupported" % name) - - def testSetHint(self): - print("testSetHint", flush=True) - solver = pywraplp.Solver( - "RunBooleanExampleCppStyle", pywraplp.Solver.GLOP_LINEAR_PROGRAMMING - ) - infinity = solver.infinity() - # x1 and x2 are integer non-negative variables. - x1 = solver.BoolVar("x1") - x2 = solver.BoolVar("x2") - - # Minimize 2 * x1 + x2. - objective = solver.Objective() - objective.SetCoefficient(x1, 2) - objective.SetCoefficient(x2, 1) - objective.SetMinimization() - - # 1 <= x1 + 2 * x2 <= 3. - c0 = solver.Constraint(1, 3, "c0") - c0.SetCoefficient(x1, 1) - c0.SetCoefficient(x2, 2) - - solver.SetHint([x1, x2], [1.0, 0.0]) - self.assertEqual(2, len(solver.variables())) - self.assertEqual(1, len(solver.constraints())) - - def testBopInfeasible(self): - print("testBopInfeasible", flush=True) - solver = pywraplp.Solver("test", pywraplp.Solver.BOP_INTEGER_PROGRAMMING) - solver.EnableOutput() - - x = solver.IntVar(0, 10, "") - solver.Add(x >= 20) - - result_status = solver.Solve() - print(result_status) # outputs: 0 - - def testLoadSolutionFromProto(self): - print("testLoadSolutionFromProto", flush=True) - solver = pywraplp.Solver("", pywraplp.Solver.GLOP_LINEAR_PROGRAMMING) - solver.LoadSolutionFromProto(linear_solver_pb2.MPSolutionResponse()) - - def testSolveFromProto(self): - print("testSolveFromProto", flush=True) - request_str = """ + + def RunLinearExampleNaturalLanguageAPI(self, optimization_problem_type): + """Example of simple linear program with natural language API.""" + solver = pywraplp.Solver( + "RunLinearExampleNaturalLanguageAPI", optimization_problem_type + ) + infinity = solver.infinity() + # x1, x2 and x3 are continuous non-negative variables. + x1 = solver.NumVar(0.0, infinity, "x1") + x2 = solver.NumVar(0.0, infinity, "x2") + x3 = solver.NumVar(0.0, infinity, "x3") + + solver.Maximize(10 * x1 + 6 * x2 + 4 * x3) + c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, "ConstraintName0") + c1 = solver.Add(2 * x1 + 2 * x2 + 6 * x3 <= 300) + sum_of_vars = sum([x1, x2, x3]) + c2 = solver.Add(sum_of_vars <= 100.0, "OtherConstraintName") + + self.SolveAndPrint( + solver, + [x1, x2, x3], + [c0, c1, c2], + optimization_problem_type != pywraplp.Solver.PDLP_LINEAR_PROGRAMMING, + ) + + # Print a linear expression's solution value. + print("Sum of vars: %s = %s" % (sum_of_vars, sum_of_vars.solution_value())) + + def RunLinearExampleCppStyleAPI(self, optimization_problem_type): + """Example of simple linear program with the C++ style API.""" + solver = pywraplp.Solver( + "RunLinearExampleCppStyle", optimization_problem_type + ) + infinity = solver.infinity() + # x1, x2 and x3 are continuous non-negative variables. + x1 = solver.NumVar(0.0, infinity, "x1") + x2 = solver.NumVar(0.0, infinity, "x2") + x3 = solver.NumVar(0.0, infinity, "x3") + + # Maximize 10 * x1 + 6 * x2 + 4 * x3. + objective = solver.Objective() + objective.SetCoefficient(x1, 10) + objective.SetCoefficient(x2, 6) + objective.SetCoefficient(x3, 4) + objective.SetMaximization() + + # x1 + x2 + x3 <= 100. + c0 = solver.Constraint(-infinity, 100.0, "c0") + c0.SetCoefficient(x1, 1) + c0.SetCoefficient(x2, 1) + c0.SetCoefficient(x3, 1) + + # 10 * x1 + 4 * x2 + 5 * x3 <= 600. + c1 = solver.Constraint(-infinity, 600.0, "c1") + c1.SetCoefficient(x1, 10) + c1.SetCoefficient(x2, 4) + c1.SetCoefficient(x3, 5) + + # 2 * x1 + 2 * x2 + 6 * x3 <= 300. + c2 = solver.Constraint(-infinity, 300.0, "c2") + c2.SetCoefficient(x1, 2) + c2.SetCoefficient(x2, 2) + c2.SetCoefficient(x3, 6) + + self.SolveAndPrint( + solver, + [x1, x2, x3], + [c0, c1, c2], + optimization_problem_type != pywraplp.Solver.PDLP_LINEAR_PROGRAMMING, + ) + + def RunMixedIntegerExampleCppStyleAPI(self, optimization_problem_type): + """Example of simple mixed integer program with the C++ style API.""" + solver = pywraplp.Solver( + "RunMixedIntegerExampleCppStyle", optimization_problem_type + ) + infinity = solver.infinity() + # x1 and x2 are integer non-negative variables. + x1 = solver.IntVar(0.0, infinity, "x1") + x2 = solver.IntVar(0.0, infinity, "x2") + + # Maximize x1 + 10 * x2. + objective = solver.Objective() + objective.SetCoefficient(x1, 1) + objective.SetCoefficient(x2, 10) + objective.SetMaximization() + + # x1 + 7 * x2 <= 17.5. + c0 = solver.Constraint(-infinity, 17.5, "c0") + c0.SetCoefficient(x1, 1) + c0.SetCoefficient(x2, 7) + + # x1 <= 3.5. + c1 = solver.Constraint(-infinity, 3.5, "c1") + c1.SetCoefficient(x1, 1) + c1.SetCoefficient(x2, 0) + + self.SolveAndPrint(solver, [x1, x2], [c0, c1], True) + + def RunBooleanExampleCppStyleAPI(self, optimization_problem_type): + """Example of simple boolean program with the C++ style API.""" + solver = pywraplp.Solver( + "RunBooleanExampleCppStyle", optimization_problem_type + ) + # x1 and x2 are integer non-negative variables. + x1 = solver.BoolVar("x1") + x2 = solver.BoolVar("x2") + + # Minimize 2 * x1 + x2. + objective = solver.Objective() + objective.SetCoefficient(x1, 2) + objective.SetCoefficient(x2, 1) + objective.SetMinimization() + + # 1 <= x1 + 2 * x2 <= 3. + c0 = solver.Constraint(1, 3, "c0") + c0.SetCoefficient(x1, 1) + c0.SetCoefficient(x2, 2) + + self.SolveAndPrint(solver, [x1, x2], [c0], True) + + def SolveAndPrint(self, solver, variable_list, constraint_list, is_precise): + """Solve the problem and print the solution.""" + print(("Number of variables = %d" % solver.NumVariables())) + self.assertEqual(solver.NumVariables(), len(variable_list)) + + print(("Number of constraints = %d" % solver.NumConstraints())) + self.assertEqual(solver.NumConstraints(), len(constraint_list)) + + result_status = solver.Solve() + + # The problem has an optimal solution. + self.assertEqual(result_status, pywraplp.Solver.OPTIMAL) + + # The solution looks legit (when using solvers others than + # GLOP_LINEAR_PROGRAMMING, verifying the solution is highly recommended!). + if is_precise: + self.assertTrue(solver.VerifySolution(1e-7, True)) + + print(("Problem solved in %f milliseconds" % solver.wall_time())) + + # The objective value of the solution. + print(("Optimal objective value = %f" % solver.Objective().Value())) + + # The value of each variable in the solution. + for variable in variable_list: + print(("%s = %f" % (variable.name(), variable.solution_value()))) + + print("Advanced usage:") + print(("Problem solved in %d iterations" % solver.iterations())) + if not solver.IsMip(): + for variable in variable_list: + print(( + "%s: reduced cost = %f" % (variable.name(), variable.reduced_cost()) + )) + activities = solver.ComputeConstraintActivities() + for i, constraint in enumerate(constraint_list): + print(( + "constraint %d: dual value = %f\n activity = %f" + % (i, constraint.dual_value(), activities[constraint.index()]) + )) + + def testApi(self): + print("testApi", flush=True) + all_names_and_problem_types = list( + linear_solver_pb2.MPModelRequest.SolverType.items() + ) + for name, problem_type in all_names_and_problem_types: + with self.subTest(f"{name}: {problem_type}"): + print(f"######## {name}:{problem_type} #######", flush=True) + if not pywraplp.Solver.SupportsProblemType(problem_type): + continue + if name.startswith("GUROBI"): + continue + if name.startswith("KNAPSACK"): + continue + if not name.startswith("SCIP"): + continue + if name.endswith("LINEAR_PROGRAMMING"): + print(("\n------ Linear programming example with %s ------" % name)) + print("\n*** Natural language API ***") + self.RunLinearExampleNaturalLanguageAPI(problem_type) + print("\n*** C++ style API ***") + self.RunLinearExampleCppStyleAPI(problem_type) + elif name.endswith("MIXED_INTEGER_PROGRAMMING"): + print(( + "\n------ Mixed Integer programming example with %s ------" % name + )) + print("\n*** C++ style API ***") + self.RunMixedIntegerExampleCppStyleAPI(problem_type) + elif name.endswith("INTEGER_PROGRAMMING"): + print(("\n------ Boolean programming example with %s ------" % name)) + print("\n*** C++ style API ***") + self.RunBooleanExampleCppStyleAPI(problem_type) + else: + print("ERROR: %s unsupported" % name) + + def testSetHint(self): + print("testSetHint", flush=True) + solver = pywraplp.Solver( + "RunBooleanExampleCppStyle", pywraplp.Solver.GLOP_LINEAR_PROGRAMMING + ) + infinity = solver.infinity() + # x1 and x2 are integer non-negative variables. + x1 = solver.BoolVar("x1") + x2 = solver.BoolVar("x2") + + # Minimize 2 * x1 + x2. + objective = solver.Objective() + objective.SetCoefficient(x1, 2) + objective.SetCoefficient(x2, 1) + objective.SetMinimization() + + # 1 <= x1 + 2 * x2 <= 3. + c0 = solver.Constraint(1, 3, "c0") + c0.SetCoefficient(x1, 1) + c0.SetCoefficient(x2, 2) + + solver.SetHint([x1, x2], [1.0, 0.0]) + self.assertEqual(2, len(solver.variables())) + self.assertEqual(1, len(solver.constraints())) + + def testBopInfeasible(self): + print("testBopInfeasible", flush=True) + solver = pywraplp.Solver("test", pywraplp.Solver.BOP_INTEGER_PROGRAMMING) + solver.EnableOutput() + + x = solver.IntVar(0, 10, "") + solver.Add(x >= 20) + + result_status = solver.Solve() + print(result_status) # outputs: 0 + + def testLoadSolutionFromProto(self): + print("testLoadSolutionFromProto", flush=True) + solver = pywraplp.Solver("", pywraplp.Solver.GLOP_LINEAR_PROGRAMMING) + solver.LoadSolutionFromProto(linear_solver_pb2.MPSolutionResponse()) + + def testSolveFromProto(self): + print("testSolveFromProto", flush=True) + request_str = """ model { maximize: false objective_offset: 0 @@ -324,34 +318,37 @@ def testSolveFromProto(self): solver_time_limit_seconds: 1.0 solver_specific_parameters: "" """ - request = linear_solver_pb2.MPModelRequest() - text_format.Parse(request_str, request) - response = linear_solver_pb2.MPSolutionResponse() - self.assertEqual(len(request.model.variable), 3) - pywraplp.Solver.SolveWithProto(model_request=request, response=response) - self.assertEqual( - linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL, response.status - ) - - def testExportToMps(self): - """Test MPS export.""" - print("testExportToMps", flush=True) - solver = pywraplp.Solver("ExportMps", pywraplp.Solver.GLOP_LINEAR_PROGRAMMING) - infinity = solver.infinity() - # x1, x2 and x3 are continuous non-negative variables. - x1 = solver.NumVar(0.0, infinity, "x1") - x2 = solver.NumVar(0.0, infinity, "x2") - x3 = solver.NumVar(0.0, infinity, "x3") - - solver.Maximize(10 * x1 + 6 * x2 + 4 * x3) - c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, "ConstraintName0") - c1 = solver.Add(2 * x1 + 2 * x2 + 6 * x3 <= 300) - sum_of_vars = sum([x1, x2, x3]) - c2 = solver.Add(sum_of_vars <= 100.0, "OtherConstraintName") - - mps_str = solver.ExportModelAsMpsFormat(fixed_format=False, obfuscate=False) - self.assertIn("ExportMps", mps_str) + request = linear_solver_pb2.MPModelRequest() + text_format.Parse(request_str, request) + response = linear_solver_pb2.MPSolutionResponse() + self.assertEqual(len(request.model.variable), 3) + pywraplp.Solver.SolveWithProto(model_request=request, response=response) + self.assertEqual( + linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL, + response.status, + ) + + def testExportToMps(self): + """Test MPS export.""" + print("testExportToMps", flush=True) + solver = pywraplp.Solver( + "ExportMps", pywraplp.Solver.GLOP_LINEAR_PROGRAMMING + ) + infinity = solver.infinity() + # x1, x2 and x3 are continuous non-negative variables. + x1 = solver.NumVar(0.0, infinity, "x1") + x2 = solver.NumVar(0.0, infinity, "x2") + x3 = solver.NumVar(0.0, infinity, "x3") + + solver.Maximize(10 * x1 + 6 * x2 + 4 * x3) + c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, "ConstraintName0") + c1 = solver.Add(2 * x1 + 2 * x2 + 6 * x3 <= 300) + sum_of_vars = sum([x1, x2, x3]) + c2 = solver.Add(sum_of_vars <= 100.0, "OtherConstraintName") + + mps_str = solver.ExportModelAsMpsFormat(fixed_format=False, obfuscate=False) + self.assertIn("ExportMps", mps_str) if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/ortools/linear_solver/python/model_builder.py b/ortools/linear_solver/python/model_builder.py index 0b59c665fc8..dbccf71b6c7 100644 --- a/ortools/linear_solver/python/model_builder.py +++ b/ortools/linear_solver/python/model_builder.py @@ -66,50 +66,52 @@ def _add_linear_constraint_to_helper( helper: mbh.ModelBuilderHelper, name: Optional[str], ): - """Creates a new linear constraint in the helper. - - It handles boolean values (which might arise in the construction of - BoundedLinearExpressions). - - If bounded_expr is a Boolean value, the created constraint is different. - In that case, the constraint will be immutable and marked as under-specified. - It will be always feasible or infeasible whether the value is True or False. - - Args: - bounded_expr: The bounded expression used to create the constraint. - helper: The helper to create the constraint. - name: The name of the constraint to be created. - - Returns: - LinearConstraint: a constraint in the helper corresponding to the input. - - Raises: - TypeError: If constraint is an invalid type. - """ - if isinstance(bounded_expr, bool): - c = LinearConstraint(helper, is_under_specified=True) - if name is not None: - helper.set_constraint_name(c.index, name) - if bounded_expr: - # constraint that is always feasible: 0.0 <= nothing <= 0.0 - helper.set_constraint_lower_bound(c.index, 0.0) - helper.set_constraint_upper_bound(c.index, 0.0) - else: - # constraint that is always infeasible: +oo <= nothing <= -oo - helper.set_constraint_lower_bound(c.index, 1) - helper.set_constraint_upper_bound(c.index, -1) - return c - if isinstance(bounded_expr, mbh.BoundedLinearExpression): - c = LinearConstraint(helper) - # pylint: disable=protected-access - helper.add_terms_to_constraint(c.index, bounded_expr.vars, bounded_expr.coeffs) - helper.set_constraint_lower_bound(c.index, bounded_expr.lower_bound) - helper.set_constraint_upper_bound(c.index, bounded_expr.upper_bound) - # pylint: enable=protected-access - if name is not None: - helper.set_constraint_name(c.index, name) - return c - raise TypeError(f"invalid type={type(bounded_expr).__name__!r}") + """Creates a new linear constraint in the helper. + + It handles boolean values (which might arise in the construction of + BoundedLinearExpressions). + + If bounded_expr is a Boolean value, the created constraint is different. + In that case, the constraint will be immutable and marked as under-specified. + It will be always feasible or infeasible whether the value is True or False. + + Args: + bounded_expr: The bounded expression used to create the constraint. + helper: The helper to create the constraint. + name: The name of the constraint to be created. + + Returns: + LinearConstraint: a constraint in the helper corresponding to the input. + + Raises: + TypeError: If constraint is an invalid type. + """ + if isinstance(bounded_expr, bool): + c = LinearConstraint(helper, is_under_specified=True) + if name is not None: + helper.set_constraint_name(c.index, name) + if bounded_expr: + # constraint that is always feasible: 0.0 <= nothing <= 0.0 + helper.set_constraint_lower_bound(c.index, 0.0) + helper.set_constraint_upper_bound(c.index, 0.0) + else: + # constraint that is always infeasible: +oo <= nothing <= -oo + helper.set_constraint_lower_bound(c.index, 1) + helper.set_constraint_upper_bound(c.index, -1) + return c + if isinstance(bounded_expr, mbh.BoundedLinearExpression): + c = LinearConstraint(helper) + # pylint: disable=protected-access + helper.add_terms_to_constraint( + c.index, bounded_expr.vars, bounded_expr.coeffs + ) + helper.set_constraint_lower_bound(c.index, bounded_expr.lower_bound) + helper.set_constraint_upper_bound(c.index, bounded_expr.upper_bound) + # pylint: enable=protected-access + if name is not None: + helper.set_constraint_name(c.index, name) + return c + raise TypeError(f"invalid type={type(bounded_expr).__name__!r}") def _add_enforced_linear_constraint_to_helper( @@ -119,1322 +121,1341 @@ def _add_enforced_linear_constraint_to_helper( value: bool, name: Optional[str], ): - """Creates a new enforced linear constraint in the helper. + """Creates a new enforced linear constraint in the helper. + + It handles boolean values (which might arise in the construction of + BoundedLinearExpressions). + + If bounded_expr is a Boolean value, the linear part of the constraint is + different. + In that case, the constraint will be immutable and marked as under-specified. + Its linear part will be always feasible or infeasible whether the value is + True or False. + + Args: + bounded_expr: The bounded expression used to create the constraint. + helper: The helper to create the constraint. + var: the variable used in the indicator + value: the value used in the indicator + name: The name of the constraint to be created. + + Returns: + EnforcedLinearConstraint: a constraint in the helper corresponding to the + input. + + Raises: + TypeError: If constraint is an invalid type. + """ + if isinstance(bounded_expr, bool): + # TODO(user): create indicator variable assignment instead ? + c = EnforcedLinearConstraint(helper, is_under_specified=True) + c.indicator_variable = var + c.indicator_value = value + if name is not None: + helper.set_enforced_constraint_name(c.index, name) + if bounded_expr: + # constraint that is always feasible: 0.0 <= nothing <= 0.0 + helper.set_enforced_constraint_lower_bound(c.index, 0.0) + helper.set_enforced_constraint_upper_bound(c.index, 0.0) + else: + # constraint that is always infeasible: +oo <= nothing <= -oo + helper.set_enforced_constraint_lower_bound(c.index, 1) + helper.set_enforced_constraint_upper_bound(c.index, -1) + return c + if isinstance(bounded_expr, mbh.BoundedLinearExpression): + c = EnforcedLinearConstraint(helper) + c.indicator_variable = var + c.indicator_value = value + helper.add_terms_to_enforced_constraint( + c.index, bounded_expr.vars, bounded_expr.coeffs + ) + helper.set_enforced_constraint_lower_bound( + c.index, bounded_expr.lower_bound + ) + helper.set_enforced_constraint_upper_bound( + c.index, bounded_expr.upper_bound + ) + if name is not None: + helper.set_constraint_name(c.index, name) + return c + + raise TypeError(f"invalid type={type(bounded_expr).__name__!r}") - It handles boolean values (which might arise in the construction of - BoundedLinearExpressions). - If bounded_expr is a Boolean value, the linear part of the constraint is - different. - In that case, the constraint will be immutable and marked as under-specified. - Its linear part will be always feasible or infeasible whether the value is - True or False. +class LinearConstraint: + """Stores a linear equation. + + Example: + x = model.new_num_var(0, 10, 'x') + y = model.new_num_var(0, 10, 'y') + + linear_constraint = model.add(x + 2 * y == 5) + """ + + def __init__( + self, + helper: mbh.ModelBuilderHelper, + *, + index: Optional[IntegerT] = None, + is_under_specified: bool = False, + ) -> None: + """LinearConstraint constructor. Args: - bounded_expr: The bounded expression used to create the constraint. - helper: The helper to create the constraint. - var: the variable used in the indicator - value: the value used in the indicator - name: The name of the constraint to be created. + helper: The pybind11 ModelBuilderHelper. + index: If specified, recreates a wrapper to an existing linear constraint. + is_under_specified: indicates if the constraint was created by + model.add(bool). + """ + if index is None: + self.__index = helper.add_linear_constraint() + else: + self.__index = index + self.__helper: mbh.ModelBuilderHelper = helper + self.__is_under_specified = is_under_specified + + def __hash__(self): + return hash((self.__helper, self.__index)) + + @property + def index(self) -> IntegerT: + """Returns the index of the constraint in the helper.""" + return self.__index + + @property + def helper(self) -> mbh.ModelBuilderHelper: + """Returns the ModelBuilderHelper instance.""" + return self.__helper + + @property + def lower_bound(self) -> np.double: + return self.__helper.constraint_lower_bound(self.__index) + + @lower_bound.setter + def lower_bound(self, bound: NumberT) -> None: + self.assert_constraint_is_well_defined() + self.__helper.set_constraint_lower_bound(self.__index, bound) + + @property + def upper_bound(self) -> np.double: + return self.__helper.constraint_upper_bound(self.__index) + + @upper_bound.setter + def upper_bound(self, bound: NumberT) -> None: + self.assert_constraint_is_well_defined() + self.__helper.set_constraint_upper_bound(self.__index, bound) + + @property + def name(self) -> str: + constraint_name = self.__helper.constraint_name(self.__index) + if constraint_name: + return constraint_name + return f"linear_constraint#{self.__index}" + + @name.setter + def name(self, name: str) -> None: + return self.__helper.set_constraint_name(self.__index, name) + + @property + def is_under_specified(self) -> bool: + """Returns True if the constraint is under specified. + + Usually, it means that it was created by model.add(False) or model.add(True) + The effect is that modifying the constraint will raise an exception. + """ + return self.__is_under_specified + + def assert_constraint_is_well_defined(self) -> None: + """Raises an exception if the constraint is under specified.""" + if self.__is_under_specified: + raise ValueError( + f"Constraint {self.index} is under specified and cannot be modified" + ) + + def __str__(self): + return self.name + + def __repr__(self): + return ( + f"LinearConstraint({self.name}, lb={self.lower_bound}," + f" ub={self.upper_bound}," + f" var_indices={self.helper.constraint_var_indices(self.index)}," + f" coefficients={self.helper.constraint_coefficients(self.index)})" + ) - Returns: - EnforcedLinearConstraint: a constraint in the helper corresponding to the - input. + def set_coefficient(self, var: Variable, coeff: NumberT) -> None: + """Sets the coefficient of the variable in the constraint.""" + self.assert_constraint_is_well_defined() + self.__helper.set_constraint_coefficient(self.__index, var.index, coeff) - Raises: - TypeError: If constraint is an invalid type. + def add_term(self, var: Variable, coeff: NumberT) -> None: + """Adds var * coeff to the constraint.""" + self.assert_constraint_is_well_defined() + self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff) + + def clear_terms(self) -> None: + """Clear all terms of the constraint.""" + self.assert_constraint_is_well_defined() + self.__helper.clear_constraint_terms(self.__index) + + +class EnforcedLinearConstraint: + """Stores an enforced linear equation, also name indicator constraint. + + Example: + x = model.new_num_var(0, 10, 'x') + y = model.new_num_var(0, 10, 'y') + z = model.new_bool_var('z') + + enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False) + """ + + def __init__( + self, + helper: mbh.ModelBuilderHelper, + *, + index: Optional[IntegerT] = None, + is_under_specified: bool = False, + ) -> None: + """EnforcedLinearConstraint constructor. + + Args: + helper: The pybind11 ModelBuilderHelper. + index: If specified, recreates a wrapper to an existing linear constraint. + is_under_specified: indicates if the constraint was created by + model.add(bool). """ - if isinstance(bounded_expr, bool): - # TODO(user): create indicator variable assignment instead ? - c = EnforcedLinearConstraint(helper, is_under_specified=True) - c.indicator_variable = var - c.indicator_value = value - if name is not None: - helper.set_enforced_constraint_name(c.index, name) - if bounded_expr: - # constraint that is always feasible: 0.0 <= nothing <= 0.0 - helper.set_enforced_constraint_lower_bound(c.index, 0.0) - helper.set_enforced_constraint_upper_bound(c.index, 0.0) - else: - # constraint that is always infeasible: +oo <= nothing <= -oo - helper.set_enforced_constraint_lower_bound(c.index, 1) - helper.set_enforced_constraint_upper_bound(c.index, -1) - return c - if isinstance(bounded_expr, mbh.BoundedLinearExpression): - c = EnforcedLinearConstraint(helper) - c.indicator_variable = var - c.indicator_value = value - helper.add_terms_to_enforced_constraint( - c.index, bounded_expr.vars, bounded_expr.coeffs + if index is None: + self.__index = helper.add_enforced_linear_constraint() + else: + if not helper.is_enforced_linear_constraint(index): + raise ValueError( + f"the given index {index} does not refer to an enforced linear" + " constraint" ) - helper.set_enforced_constraint_lower_bound(c.index, bounded_expr.lower_bound) - helper.set_enforced_constraint_upper_bound(c.index, bounded_expr.upper_bound) - if name is not None: - helper.set_constraint_name(c.index, name) - return c - raise TypeError(f"invalid type={type(bounded_expr).__name__!r}") + self.__index = index + self.__helper: mbh.ModelBuilderHelper = helper + self.__is_under_specified = is_under_specified + + @property + def index(self) -> IntegerT: + """Returns the index of the constraint in the helper.""" + return self.__index + + @property + def helper(self) -> mbh.ModelBuilderHelper: + """Returns the ModelBuilderHelper instance.""" + return self.__helper + + @property + def lower_bound(self) -> np.double: + return self.__helper.enforced_constraint_lower_bound(self.__index) + + @lower_bound.setter + def lower_bound(self, bound: NumberT) -> None: + self.assert_constraint_is_well_defined() + self.__helper.set_enforced_constraint_lower_bound(self.__index, bound) + + @property + def upper_bound(self) -> np.double: + return self.__helper.enforced_constraint_upper_bound(self.__index) + + @upper_bound.setter + def upper_bound(self, bound: NumberT) -> None: + self.assert_constraint_is_well_defined() + self.__helper.set_enforced_constraint_upper_bound(self.__index, bound) + + @property + def indicator_variable(self) -> "Variable": + enforcement_var_index = ( + self.__helper.enforced_constraint_indicator_variable_index(self.__index) + ) + return Variable(self.__helper, enforcement_var_index) + + @indicator_variable.setter + def indicator_variable(self, var: "Variable") -> None: + self.__helper.set_enforced_constraint_indicator_variable_index( + self.__index, var.index + ) + @property + def indicator_value(self) -> bool: + return self.__helper.enforced_constraint_indicator_value(self.__index) -class LinearConstraint: - """Stores a linear equation. + @indicator_value.setter + def indicator_value(self, value: bool) -> None: + self.__helper.set_enforced_constraint_indicator_value(self.__index, value) - Example: - x = model.new_num_var(0, 10, 'x') - y = model.new_num_var(0, 10, 'y') + @property + def name(self) -> str: + constraint_name = self.__helper.enforced_constraint_name(self.__index) + if constraint_name: + return constraint_name + return f"enforced_linear_constraint#{self.__index}" - linear_constraint = model.add(x + 2 * y == 5) + @name.setter + def name(self, name: str) -> None: + return self.__helper.set_enforced_constraint_name(self.__index, name) + + @property + def is_under_specified(self) -> bool: + """Returns True if the constraint is under specified. + + Usually, it means that it was created by model.add(False) or model.add(True) + The effect is that modifying the constraint will raise an exception. """ + return self.__is_under_specified + + def assert_constraint_is_well_defined(self) -> None: + """Raises an exception if the constraint is under specified.""" + if self.__is_under_specified: + raise ValueError( + f"Constraint {self.index} is under specified and cannot be modified" + ) + + def __str__(self): + return self.name + + def __repr__(self): + return ( + f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound}," + f" ub={self.upper_bound}," + f" var_indices={self.helper.enforced_constraint_var_indices(self.index)}," + f" coefficients={self.helper.enforced_constraint_coefficients(self.index)}," + f" indicator_variable={self.indicator_variable}" + f" indicator_value={self.indicator_value})" + ) - def __init__( - self, - helper: mbh.ModelBuilderHelper, - *, - index: Optional[IntegerT] = None, - is_under_specified: bool = False, - ) -> None: - """LinearConstraint constructor. - - Args: - helper: The pybind11 ModelBuilderHelper. - index: If specified, recreates a wrapper to an existing linear constraint. - is_under_specified: indicates if the constraint was created by - model.add(bool). - """ - if index is None: - self.__index = helper.add_linear_constraint() - else: - self.__index = index - self.__helper: mbh.ModelBuilderHelper = helper - self.__is_under_specified = is_under_specified - - def __hash__(self): - return hash((self.__helper, self.__index)) - - @property - def index(self) -> IntegerT: - """Returns the index of the constraint in the helper.""" - return self.__index - - @property - def helper(self) -> mbh.ModelBuilderHelper: - """Returns the ModelBuilderHelper instance.""" - return self.__helper - - @property - def lower_bound(self) -> np.double: - return self.__helper.constraint_lower_bound(self.__index) - - @lower_bound.setter - def lower_bound(self, bound: NumberT) -> None: - self.assert_constraint_is_well_defined() - self.__helper.set_constraint_lower_bound(self.__index, bound) - - @property - def upper_bound(self) -> np.double: - return self.__helper.constraint_upper_bound(self.__index) - - @upper_bound.setter - def upper_bound(self, bound: NumberT) -> None: - self.assert_constraint_is_well_defined() - self.__helper.set_constraint_upper_bound(self.__index, bound) - - @property - def name(self) -> str: - constraint_name = self.__helper.constraint_name(self.__index) - if constraint_name: - return constraint_name - return f"linear_constraint#{self.__index}" - - @name.setter - def name(self, name: str) -> None: - return self.__helper.set_constraint_name(self.__index, name) - - @property - def is_under_specified(self) -> bool: - """Returns True if the constraint is under specified. - - Usually, it means that it was created by model.add(False) or model.add(True) - The effect is that modifying the constraint will raise an exception. - """ - return self.__is_under_specified - - def assert_constraint_is_well_defined(self) -> None: - """Raises an exception if the constraint is under specified.""" - if self.__is_under_specified: - raise ValueError( - f"Constraint {self.index} is under specified and cannot be modified" - ) + def set_coefficient(self, var: Variable, coeff: NumberT) -> None: + """Sets the coefficient of the variable in the constraint.""" + self.assert_constraint_is_well_defined() + self.__helper.set_enforced_constraint_coefficient( + self.__index, var.index, coeff + ) + + def add_term(self, var: Variable, coeff: NumberT) -> None: + """Adds var * coeff to the constraint.""" + self.assert_constraint_is_well_defined() + self.__helper.safe_add_term_to_enforced_constraint( + self.__index, var.index, coeff + ) - def __str__(self): - return self.name + def clear_terms(self) -> None: + """Clear all terms of the constraint.""" + self.assert_constraint_is_well_defined() + self.__helper.clear_enforced_constraint_terms(self.__index) - def __repr__(self): - return ( - f"LinearConstraint({self.name}, lb={self.lower_bound}," - f" ub={self.upper_bound}," - f" var_indices={self.helper.constraint_var_indices(self.index)}," - f" coefficients={self.helper.constraint_coefficients(self.index)})" - ) - def set_coefficient(self, var: Variable, coeff: NumberT) -> None: - """Sets the coefficient of the variable in the constraint.""" - self.assert_constraint_is_well_defined() - self.__helper.set_constraint_coefficient(self.__index, var.index, coeff) +class Model: + """Methods for building a linear model. + + Methods beginning with: + + * ```new_``` create integer, boolean, or interval variables. + * ```add_``` create new constraints and add them to the model. + """ + + def __init__(self): + self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper() + + def clone(self) -> "Model": + """Returns a clone of the current model.""" + clone = Model() + clone.helper.overwrite_model(self.helper) + return clone + + @typing.overload + def _get_linear_constraints( + self, constraints: Optional[pd.Index] + ) -> pd.Index: + ... + + @typing.overload + def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: + ... + + def _get_linear_constraints( + self, constraints: Optional[_IndexOrSeries] = None + ) -> _IndexOrSeries: + if constraints is None: + return self.get_linear_constraints() + return constraints + + @typing.overload + def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: + ... + + @typing.overload + def _get_variables(self, variables: pd.Series) -> pd.Series: + ... + + def _get_variables( + self, variables: Optional[_IndexOrSeries] = None + ) -> _IndexOrSeries: + if variables is None: + return self.get_variables() + return variables + + def get_linear_constraints(self) -> pd.Index: + """Gets all linear constraints in the model.""" + return pd.Index( + [ + self.linear_constraint_from_index(i) + for i in range(self.num_constraints) + ], + name="linear_constraint", + ) - def add_term(self, var: Variable, coeff: NumberT) -> None: - """Adds var * coeff to the constraint.""" - self.assert_constraint_is_well_defined() - self.__helper.safe_add_term_to_constraint(self.__index, var.index, coeff) + def get_linear_constraint_expressions( + self, constraints: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the expressions of all linear constraints in the set. - def clear_terms(self) -> None: - """Clear all terms of the constraint.""" - self.assert_constraint_is_well_defined() - self.__helper.clear_constraint_terms(self.__index) + If `constraints` is a `pd.Index`, then the output will be indexed by the + constraints. If `constraints` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + Args: + constraints (Union[pd.Index, pd.Series]): Optional. The set of linear + constraints from which to get the expressions. If unspecified, all + linear constraints will be in scope. -class EnforcedLinearConstraint: - """Stores an enforced linear equation, also name indicator constraint. + Returns: + pd.Series: The expressions of all linear constraints in the set. + """ + return _attribute_series( + # pylint: disable=g-long-lambda + func=lambda c: mbh.FlatExpr( + # pylint: disable=g-complex-comprehension + [ + Variable(self.__helper, var_id) + for var_id in c.helper.constraint_var_indices(c.index) + ], + c.helper.constraint_coefficients(c.index), + 0.0, + ), + values=self._get_linear_constraints(constraints), + ) - Example: - x = model.new_num_var(0, 10, 'x') - y = model.new_num_var(0, 10, 'y') - z = model.new_bool_var('z') + def get_linear_constraint_lower_bounds( + self, constraints: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the lower bounds of all linear constraints in the set. - enforced_linear_constraint = model.add_enforced(x + 2 * y == 5, z, False) + If `constraints` is a `pd.Index`, then the output will be indexed by the + constraints. If `constraints` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + + Args: + constraints (Union[pd.Index, pd.Series]): Optional. The set of linear + constraints from which to get the lower bounds. If unspecified, all + linear constraints will be in scope. + + Returns: + pd.Series: The lower bounds of all linear constraints in the set. """ + return _attribute_series( + func=lambda c: c.lower_bound, # pylint: disable=protected-access + values=self._get_linear_constraints(constraints), + ) - def __init__( - self, - helper: mbh.ModelBuilderHelper, - *, - index: Optional[IntegerT] = None, - is_under_specified: bool = False, - ) -> None: - """EnforcedLinearConstraint constructor. - - Args: - helper: The pybind11 ModelBuilderHelper. - index: If specified, recreates a wrapper to an existing linear constraint. - is_under_specified: indicates if the constraint was created by - model.add(bool). - """ - if index is None: - self.__index = helper.add_enforced_linear_constraint() - else: - if not helper.is_enforced_linear_constraint(index): - raise ValueError( - f"the given index {index} does not refer to an enforced linear" - " constraint" - ) - - self.__index = index - self.__helper: mbh.ModelBuilderHelper = helper - self.__is_under_specified = is_under_specified - - @property - def index(self) -> IntegerT: - """Returns the index of the constraint in the helper.""" - return self.__index - - @property - def helper(self) -> mbh.ModelBuilderHelper: - """Returns the ModelBuilderHelper instance.""" - return self.__helper - - @property - def lower_bound(self) -> np.double: - return self.__helper.enforced_constraint_lower_bound(self.__index) - - @lower_bound.setter - def lower_bound(self, bound: NumberT) -> None: - self.assert_constraint_is_well_defined() - self.__helper.set_enforced_constraint_lower_bound(self.__index, bound) - - @property - def upper_bound(self) -> np.double: - return self.__helper.enforced_constraint_upper_bound(self.__index) - - @upper_bound.setter - def upper_bound(self, bound: NumberT) -> None: - self.assert_constraint_is_well_defined() - self.__helper.set_enforced_constraint_upper_bound(self.__index, bound) - - @property - def indicator_variable(self) -> "Variable": - enforcement_var_index = ( - self.__helper.enforced_constraint_indicator_variable_index(self.__index) - ) - return Variable(self.__helper, enforcement_var_index) + def get_linear_constraint_upper_bounds( + self, constraints: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the upper bounds of all linear constraints in the set. - @indicator_variable.setter - def indicator_variable(self, var: "Variable") -> None: - self.__helper.set_enforced_constraint_indicator_variable_index( - self.__index, var.index - ) + If `constraints` is a `pd.Index`, then the output will be indexed by the + constraints. If `constraints` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. - @property - def indicator_value(self) -> bool: - return self.__helper.enforced_constraint_indicator_value(self.__index) - - @indicator_value.setter - def indicator_value(self, value: bool) -> None: - self.__helper.set_enforced_constraint_indicator_value(self.__index, value) - - @property - def name(self) -> str: - constraint_name = self.__helper.enforced_constraint_name(self.__index) - if constraint_name: - return constraint_name - return f"enforced_linear_constraint#{self.__index}" - - @name.setter - def name(self, name: str) -> None: - return self.__helper.set_enforced_constraint_name(self.__index, name) - - @property - def is_under_specified(self) -> bool: - """Returns True if the constraint is under specified. - - Usually, it means that it was created by model.add(False) or model.add(True) - The effect is that modifying the constraint will raise an exception. - """ - return self.__is_under_specified - - def assert_constraint_is_well_defined(self) -> None: - """Raises an exception if the constraint is under specified.""" - if self.__is_under_specified: - raise ValueError( - f"Constraint {self.index} is under specified and cannot be modified" - ) + Args: + constraints (Union[pd.Index, pd.Series]): Optional. The set of linear + constraints. If unspecified, all linear constraints will be in scope. - def __str__(self): - return self.name - - def __repr__(self): - return ( - f"EnforcedLinearConstraint({self.name}, lb={self.lower_bound}," - f" ub={self.upper_bound}," - f" var_indices={self.helper.enforced_constraint_var_indices(self.index)}," - f" coefficients={self.helper.enforced_constraint_coefficients(self.index)}," - f" indicator_variable={self.indicator_variable}" - f" indicator_value={self.indicator_value})" - ) + Returns: + pd.Series: The upper bounds of all linear constraints in the set. + """ + return _attribute_series( + func=lambda c: c.upper_bound, # pylint: disable=protected-access + values=self._get_linear_constraints(constraints), + ) - def set_coefficient(self, var: Variable, coeff: NumberT) -> None: - """Sets the coefficient of the variable in the constraint.""" - self.assert_constraint_is_well_defined() - self.__helper.set_enforced_constraint_coefficient( - self.__index, var.index, coeff - ) + def get_variables(self) -> pd.Index: + """Gets all variables in the model.""" + return pd.Index( + [self.var_from_index(i) for i in range(self.num_variables)], + name="variable", + ) - def add_term(self, var: Variable, coeff: NumberT) -> None: - """Adds var * coeff to the constraint.""" - self.assert_constraint_is_well_defined() - self.__helper.safe_add_term_to_enforced_constraint( - self.__index, var.index, coeff - ) + def get_variable_lower_bounds( + self, variables: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the lower bounds of all variables in the set. - def clear_terms(self) -> None: - """Clear all terms of the constraint.""" - self.assert_constraint_is_well_defined() - self.__helper.clear_enforced_constraint_terms(self.__index) + If `variables` is a `pd.Index`, then the output will be indexed by the + variables. If `variables` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + Args: + variables (Union[pd.Index, pd.Series]): Optional. The set of variables + from which to get the lower bounds. If unspecified, all variables will + be in scope. -class Model: - """Methods for building a linear model. + Returns: + pd.Series: The lower bounds of all variables in the set. + """ + return _attribute_series( + func=lambda v: v.lower_bound, # pylint: disable=protected-access + values=self._get_variables(variables), + ) + + def get_variable_upper_bounds( + self, variables: Optional[_IndexOrSeries] = None + ) -> pd.Series: + """Gets the upper bounds of all variables in the set. - Methods beginning with: + Args: + variables (Union[pd.Index, pd.Series]): Optional. The set of variables + from which to get the upper bounds. If unspecified, all variables will + be in scope. - * ```new_``` create integer, boolean, or interval variables. - * ```add_``` create new constraints and add them to the model. + Returns: + pd.Series: The upper bounds of all variables in the set. """ + return _attribute_series( + func=lambda v: v.upper_bound, # pylint: disable=protected-access + values=self._get_variables(variables), + ) - def __init__(self): - self.__helper: mbh.ModelBuilderHelper = mbh.ModelBuilderHelper() - - def clone(self) -> "Model": - """Returns a clone of the current model.""" - clone = Model() - clone.helper.overwrite_model(self.helper) - return clone - - @typing.overload - def _get_linear_constraints(self, constraints: Optional[pd.Index]) -> pd.Index: ... - - @typing.overload - def _get_linear_constraints(self, constraints: pd.Series) -> pd.Series: ... - - def _get_linear_constraints( - self, constraints: Optional[_IndexOrSeries] = None - ) -> _IndexOrSeries: - if constraints is None: - return self.get_linear_constraints() - return constraints - - @typing.overload - def _get_variables(self, variables: Optional[pd.Index]) -> pd.Index: ... - - @typing.overload - def _get_variables(self, variables: pd.Series) -> pd.Series: ... - - def _get_variables( - self, variables: Optional[_IndexOrSeries] = None - ) -> _IndexOrSeries: - if variables is None: - return self.get_variables() - return variables - - def get_linear_constraints(self) -> pd.Index: - """Gets all linear constraints in the model.""" - return pd.Index( - [self.linear_constraint_from_index(i) for i in range(self.num_constraints)], - name="linear_constraint", - ) + # Integer variable. - def get_linear_constraint_expressions( - self, constraints: Optional[_IndexOrSeries] = None - ) -> pd.Series: - """Gets the expressions of all linear constraints in the set. - - If `constraints` is a `pd.Index`, then the output will be indexed by the - constraints. If `constraints` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. - - Args: - constraints (Union[pd.Index, pd.Series]): Optional. The set of linear - constraints from which to get the expressions. If unspecified, all - linear constraints will be in scope. - - Returns: - pd.Series: The expressions of all linear constraints in the set. - """ - return _attribute_series( - # pylint: disable=g-long-lambda - func=lambda c: mbh.FlatExpr( - # pylint: disable=g-complex-comprehension - [ - Variable(self.__helper, var_id) - for var_id in c.helper.constraint_var_indices(c.index) - ], - c.helper.constraint_coefficients(c.index), - 0.0, - ), - values=self._get_linear_constraints(constraints), - ) + def new_var( + self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str] + ) -> Variable: + """Create an integer variable with domain [lb, ub]. - def get_linear_constraint_lower_bounds( - self, constraints: Optional[_IndexOrSeries] = None - ) -> pd.Series: - """Gets the lower bounds of all linear constraints in the set. - - If `constraints` is a `pd.Index`, then the output will be indexed by the - constraints. If `constraints` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. - - Args: - constraints (Union[pd.Index, pd.Series]): Optional. The set of linear - constraints from which to get the lower bounds. If unspecified, all - linear constraints will be in scope. - - Returns: - pd.Series: The lower bounds of all linear constraints in the set. - """ - return _attribute_series( - func=lambda c: c.lower_bound, # pylint: disable=protected-access - values=self._get_linear_constraints(constraints), - ) + Args: + lb: Lower bound of the variable. + ub: Upper bound of the variable. + is_integer: Indicates if the variable must take integral values. + name: The name of the variable. - def get_linear_constraint_upper_bounds( - self, constraints: Optional[_IndexOrSeries] = None - ) -> pd.Series: - """Gets the upper bounds of all linear constraints in the set. - - If `constraints` is a `pd.Index`, then the output will be indexed by the - constraints. If `constraints` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. - - Args: - constraints (Union[pd.Index, pd.Series]): Optional. The set of linear - constraints. If unspecified, all linear constraints will be in scope. - - Returns: - pd.Series: The upper bounds of all linear constraints in the set. - """ - return _attribute_series( - func=lambda c: c.upper_bound, # pylint: disable=protected-access - values=self._get_linear_constraints(constraints), - ) + Returns: + a variable whose domain is [lb, ub]. + """ + if name: + return Variable(self.__helper, lb, ub, is_integer, name) + else: + return Variable(self.__helper, lb, ub, is_integer) - def get_variables(self) -> pd.Index: - """Gets all variables in the model.""" - return pd.Index( - [self.var_from_index(i) for i in range(self.num_variables)], - name="variable", - ) + def new_int_var( + self, lb: NumberT, ub: NumberT, name: Optional[str] = None + ) -> Variable: + """Create an integer variable with domain [lb, ub]. - def get_variable_lower_bounds( - self, variables: Optional[_IndexOrSeries] = None - ) -> pd.Series: - """Gets the lower bounds of all variables in the set. - - If `variables` is a `pd.Index`, then the output will be indexed by the - variables. If `variables` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. - - Args: - variables (Union[pd.Index, pd.Series]): Optional. The set of variables - from which to get the lower bounds. If unspecified, all variables will - be in scope. - - Returns: - pd.Series: The lower bounds of all variables in the set. - """ - return _attribute_series( - func=lambda v: v.lower_bound, # pylint: disable=protected-access - values=self._get_variables(variables), - ) + Args: + lb: Lower bound of the variable. + ub: Upper bound of the variable. + name: The name of the variable. - def get_variable_upper_bounds( - self, variables: Optional[_IndexOrSeries] = None - ) -> pd.Series: - """Gets the upper bounds of all variables in the set. - - Args: - variables (Union[pd.Index, pd.Series]): Optional. The set of variables - from which to get the upper bounds. If unspecified, all variables will - be in scope. - - Returns: - pd.Series: The upper bounds of all variables in the set. - """ - return _attribute_series( - func=lambda v: v.upper_bound, # pylint: disable=protected-access - values=self._get_variables(variables), - ) + Returns: + a variable whose domain is [lb, ub]. + """ - # Integer variable. - - def new_var( - self, lb: NumberT, ub: NumberT, is_integer: bool, name: Optional[str] - ) -> Variable: - """Create an integer variable with domain [lb, ub]. - - Args: - lb: Lower bound of the variable. - ub: Upper bound of the variable. - is_integer: Indicates if the variable must take integral values. - name: The name of the variable. - - Returns: - a variable whose domain is [lb, ub]. - """ - if name: - return Variable(self.__helper, lb, ub, is_integer, name) - else: - return Variable(self.__helper, lb, ub, is_integer) - - def new_int_var( - self, lb: NumberT, ub: NumberT, name: Optional[str] = None - ) -> Variable: - """Create an integer variable with domain [lb, ub]. - - Args: - lb: Lower bound of the variable. - ub: Upper bound of the variable. - name: The name of the variable. - - Returns: - a variable whose domain is [lb, ub]. - """ - - return self.new_var(lb, ub, True, name) - - def new_num_var( - self, lb: NumberT, ub: NumberT, name: Optional[str] = None - ) -> Variable: - """Create an integer variable with domain [lb, ub]. - - Args: - lb: Lower bound of the variable. - ub: Upper bound of the variable. - name: The name of the variable. - - Returns: - a variable whose domain is [lb, ub]. - """ - - return self.new_var(lb, ub, False, name) - - def new_bool_var(self, name: Optional[str] = None) -> Variable: - """Creates a 0-1 variable with the given name.""" - return self.new_var( - 0, 1, True, name - ) # pytype: disable=wrong-arg-types # numpy-scalars - - def new_constant(self, value: NumberT) -> Variable: - """Declares a constant variable.""" - return self.new_var(value, value, False, None) - - def new_var_series( - self, - name: str, - index: pd.Index, - lower_bounds: Union[NumberT, pd.Series] = -math.inf, - upper_bounds: Union[NumberT, pd.Series] = math.inf, - is_integral: Union[bool, pd.Series] = False, - ) -> pd.Series: - """Creates a series of (scalar-valued) variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for - variables in the set. If a `pd.Series` is passed in, it will be based on - the corresponding values of the pd.Series. Defaults to -inf. - upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for - variables in the set. If a `pd.Series` is passed in, it will be based on - the corresponding values of the pd.Series. Defaults to +inf. - is_integral (bool, pd.Series): Optional. Indicates if the variable can - only take integer values. If a `pd.Series` is passed in, it will be - based on the corresponding values of the pd.Series. Defaults to False. - - Returns: - pd.Series: The variable set indexed by its corresponding dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - ValueError: if the `lowerbound` is greater than the `upperbound`. - ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` - does not match the input index. - """ - if not isinstance(index, pd.Index): - raise TypeError("Non-index object is used as index") - if not name.isidentifier(): - raise ValueError(f"name={name!r} is not a valid identifier") - if ( - mbn.is_a_number(lower_bounds) - and mbn.is_a_number(upper_bounds) - and lower_bounds > upper_bounds - ): - raise ValueError( - f"lower_bound={lower_bounds} is greater than" - f" upper_bound={upper_bounds} for variable set={name!r}" - ) - if ( - isinstance(is_integral, bool) - and is_integral - and mbn.is_a_number(lower_bounds) - and mbn.is_a_number(upper_bounds) - and math.isfinite(lower_bounds) - and math.isfinite(upper_bounds) - and math.ceil(lower_bounds) > math.floor(upper_bounds) - ): - raise ValueError( - f"ceil(lower_bound={lower_bounds})={math.ceil(lower_bounds)}" - f" is greater than floor({upper_bounds}) = {math.floor(upper_bounds)}" - f" for variable set={name!r}" - ) - lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index) - upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index) - is_integrals = _convert_to_series_and_validate_index(is_integral, index) - return pd.Series( - index=index, - data=[ - # pylint: disable=g-complex-comprehension - Variable( - self.__helper, - lower_bounds[i], - upper_bounds[i], - is_integrals[i], - f"{name}[{i}]", - ) - for i in index - ], - ) + return self.new_var(lb, ub, True, name) - def new_num_var_series( - self, - name: str, - index: pd.Index, - lower_bounds: Union[NumberT, pd.Series] = -math.inf, - upper_bounds: Union[NumberT, pd.Series] = math.inf, - ) -> pd.Series: - """Creates a series of continuous variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for - variables in the set. If a `pd.Series` is passed in, it will be based on - the corresponding values of the pd.Series. Defaults to -inf. - upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for - variables in the set. If a `pd.Series` is passed in, it will be based on - the corresponding values of the pd.Series. Defaults to +inf. - - Returns: - pd.Series: The variable set indexed by its corresponding dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - ValueError: if the `lowerbound` is greater than the `upperbound`. - ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` - does not match the input index. - """ - return self.new_var_series(name, index, lower_bounds, upper_bounds, False) - - def new_int_var_series( - self, - name: str, - index: pd.Index, - lower_bounds: Union[NumberT, pd.Series] = -math.inf, - upper_bounds: Union[NumberT, pd.Series] = math.inf, - ) -> pd.Series: - """Creates a series of integer variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for - variables in the set. If a `pd.Series` is passed in, it will be based on - the corresponding values of the pd.Series. Defaults to -inf. - upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for - variables in the set. If a `pd.Series` is passed in, it will be based on - the corresponding values of the pd.Series. Defaults to +inf. - - Returns: - pd.Series: The variable set indexed by its corresponding dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - ValueError: if the `lowerbound` is greater than the `upperbound`. - ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` - does not match the input index. - """ - return self.new_var_series(name, index, lower_bounds, upper_bounds, True) - - def new_bool_var_series( - self, - name: str, - index: pd.Index, - ) -> pd.Series: - """Creates a series of Boolean variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - - Returns: - pd.Series: The variable set indexed by its corresponding dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - ValueError: if the `lowerbound` is greater than the `upperbound`. - ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` - does not match the input index. - """ - return self.new_var_series(name, index, 0, 1, True) - - def var_from_index(self, index: IntegerT) -> Variable: - """Rebuilds a variable object from the model and its index.""" - return Variable(self.__helper, index) - - # Linear constraints. - - def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars - self, - linear_expr: LinearExprT, - lb: NumberT = -math.inf, - ub: NumberT = math.inf, - name: Optional[str] = None, - ) -> LinearConstraint: - """Adds the constraint: `lb <= linear_expr <= ub` with the given name.""" - ct = LinearConstraint(self.__helper) - if name: - self.__helper.set_constraint_name(ct.index, name) - if mbn.is_a_number(linear_expr): - self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) - self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) - elif isinstance(linear_expr, LinearExpr): - flat_expr = mbh.FlatExpr(linear_expr) - # pylint: disable=protected-access - self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) - self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) - self.__helper.add_terms_to_constraint( - ct.index, flat_expr.vars, flat_expr.coeffs - ) - else: - raise TypeError( - "Not supported:" - f" Model.add_linear_constraint({type(linear_expr).__name__!r})" - ) - return ct - - def add( - self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None - ) -> Union[LinearConstraint, pd.Series]: - """Adds a `BoundedLinearExpression` to the model. - - Args: - ct: A [`BoundedLinearExpression`](#boundedlinearexpression). - name: An optional name. - - Returns: - An instance of the `Constraint` class. - - Note that a special treatment is done when the argument does not contain any - variable, and thus evaluates to True or False. - - `model.add(True)` will create a constraint 0 <= empty sum <= 0. - The constraint will be marked as under specified, and cannot be modified - thereafter. - - `model.add(False)` will create a constraint inf <= empty sum <= -inf. The - constraint will be marked as under specified, and cannot be modified - thereafter. - - you can check the if a constraint is under specified by reading the - `LinearConstraint.is_under_specified` property. - """ - if isinstance(ct, mbh.BoundedLinearExpression): - return _add_linear_constraint_to_helper(ct, self.__helper, name) - elif isinstance(ct, bool): - return _add_linear_constraint_to_helper(ct, self.__helper, name) - elif isinstance(ct, pd.Series): - return pd.Series( - index=ct.index, - data=[ - _add_linear_constraint_to_helper( - expr, self.__helper, f"{name}[{i}]" - ) - for (i, expr) in zip(ct.index, ct) - ], - ) - else: - raise TypeError(f"Not supported: Model.add({type(ct).__name__!r})") - - def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint: - """Rebuilds a linear constraint object from the model and its index.""" - return LinearConstraint(self.__helper, index=index) - - # Enforced Linear constraints. - - def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars - self, - linear_expr: LinearExprT, - ivar: "Variable", - ivalue: bool, - lb: NumberT = -math.inf, - ub: NumberT = math.inf, - name: Optional[str] = None, - ) -> EnforcedLinearConstraint: - """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name.""" - ct = EnforcedLinearConstraint(self.__helper) - ct.indicator_variable = ivar - ct.indicator_value = ivalue - if name: - self.__helper.set_constraint_name(ct.index, name) - if mbn.is_a_number(linear_expr): - self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) - self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) - elif isinstance(linear_expr, LinearExpr): - flat_expr = mbh.FlatExpr(linear_expr) - # pylint: disable=protected-access - self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) - self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) - self.__helper.add_terms_to_constraint( - ct.index, flat_expr.vars, flat_expr.coeffs - ) - else: - raise TypeError( - "Not supported:" - f" Model.add_enforced_linear_constraint({type(linear_expr).__name__!r})" - ) - return ct - - def add_enforced( - self, - ct: Union[ConstraintT, pd.Series], - var: Union[Variable, pd.Series], - value: Union[bool, pd.Series], - name: Optional[str] = None, - ) -> Union[EnforcedLinearConstraint, pd.Series]: - """Adds a `ivar == ivalue => BoundedLinearExpression` to the model. - - Args: - ct: A [`BoundedLinearExpression`](#boundedlinearexpression). - var: The indicator variable - value: the indicator value - name: An optional name. - - Returns: - An instance of the `Constraint` class. - - Note that a special treatment is done when the argument does not contain any - variable, and thus evaluates to True or False. - - model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty - sum <= 0 - - model.add_enforced(False, var, value) will create a constraint inf <= - empty sum <= -inf - - you can check the if a constraint is always false (lb=inf, ub=-inf) by - calling EnforcedLinearConstraint.is_always_false() - """ - if isinstance(ct, mbh.BoundedLinearExpression): - return _add_enforced_linear_constraint_to_helper( - ct, self.__helper, var, value, name - ) - elif ( - isinstance(ct, bool) - and isinstance(var, Variable) - and isinstance(value, bool) - ): - return _add_enforced_linear_constraint_to_helper( - ct, self.__helper, var, value, name - ) - elif isinstance(ct, pd.Series): - ivar_series = _convert_to_var_series_and_validate_index(var, ct.index) - ivalue_series = _convert_to_series_and_validate_index(value, ct.index) - return pd.Series( - index=ct.index, - data=[ - _add_enforced_linear_constraint_to_helper( - expr, - self.__helper, - ivar_series[i], - ivalue_series[i], - f"{name}[{i}]", - ) - for (i, expr) in zip(ct.index, ct) - ], - ) - else: - raise TypeError(f"Not supported: Model.add_enforced({type(ct).__name__!r}") - - def enforced_linear_constraint_from_index( - self, index: IntegerT - ) -> EnforcedLinearConstraint: - """Rebuilds an enforced linear constraint object from the model and its index.""" - return EnforcedLinearConstraint(self.__helper, index=index) - - # Objective. - def minimize(self, linear_expr: LinearExprT) -> None: - """Minimizes the given objective.""" - self.__optimize(linear_expr, False) - - def maximize(self, linear_expr: LinearExprT) -> None: - """Maximizes the given objective.""" - self.__optimize(linear_expr, True) - - def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None: - """Defines the objective.""" - self.helper.clear_objective() - self.__helper.set_maximize(maximize) - if mbn.is_a_number(linear_expr): - self.helper.set_objective_offset(linear_expr) - elif isinstance(linear_expr, Variable): - self.helper.set_var_objective_coefficient(linear_expr.index, 1.0) - elif isinstance(linear_expr, LinearExpr): - flat_expr = mbh.FlatExpr(linear_expr) - # pylint: disable=protected-access - self.helper.set_objective_offset(flat_expr.offset) - var_indices = [var.index for var in flat_expr.vars] - self.helper.set_objective_coefficients(var_indices, flat_expr.coeffs) - else: - raise TypeError( - "Not supported:" - f" Model.minimize/maximize({type(linear_expr).__name__!r})" + def new_num_var( + self, lb: NumberT, ub: NumberT, name: Optional[str] = None + ) -> Variable: + """Create an integer variable with domain [lb, ub]. + + Args: + lb: Lower bound of the variable. + ub: Upper bound of the variable. + name: The name of the variable. + + Returns: + a variable whose domain is [lb, ub]. + """ + + return self.new_var(lb, ub, False, name) + + def new_bool_var(self, name: Optional[str] = None) -> Variable: + """Creates a 0-1 variable with the given name.""" + return self.new_var( + 0, 1, True, name + ) # pytype: disable=wrong-arg-types # numpy-scalars + + def new_constant(self, value: NumberT) -> Variable: + """Declares a constant variable.""" + return self.new_var(value, value, False, None) + + def new_var_series( + self, + name: str, + index: pd.Index, + lower_bounds: Union[NumberT, pd.Series] = -math.inf, + upper_bounds: Union[NumberT, pd.Series] = math.inf, + is_integral: Union[bool, pd.Series] = False, + ) -> pd.Series: + """Creates a series of (scalar-valued) variables with the given name. + + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. + lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for + variables in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. Defaults to -inf. + upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for + variables in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. Defaults to +inf. + is_integral (bool, pd.Series): Optional. Indicates if the variable can + only take integer values. If a `pd.Series` is passed in, it will be + based on the corresponding values of the pd.Series. Defaults to False. + + Returns: + pd.Series: The variable set indexed by its corresponding dimensions. + + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the `lowerbound` is greater than the `upperbound`. + ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` + does not match the input index. + """ + if not isinstance(index, pd.Index): + raise TypeError("Non-index object is used as index") + if not name.isidentifier(): + raise ValueError(f"name={name!r} is not a valid identifier") + if ( + mbn.is_a_number(lower_bounds) + and mbn.is_a_number(upper_bounds) + and lower_bounds > upper_bounds + ): + raise ValueError( + f"lower_bound={lower_bounds} is greater than" + f" upper_bound={upper_bounds} for variable set={name!r}" + ) + if ( + isinstance(is_integral, bool) + and is_integral + and mbn.is_a_number(lower_bounds) + and mbn.is_a_number(upper_bounds) + and math.isfinite(lower_bounds) + and math.isfinite(upper_bounds) + and math.ceil(lower_bounds) > math.floor(upper_bounds) + ): + raise ValueError( + f"ceil(lower_bound={lower_bounds})={math.ceil(lower_bounds)}" + f" is greater than floor({upper_bounds}) = {math.floor(upper_bounds)}" + f" for variable set={name!r}" + ) + lower_bounds = _convert_to_series_and_validate_index(lower_bounds, index) + upper_bounds = _convert_to_series_and_validate_index(upper_bounds, index) + is_integrals = _convert_to_series_and_validate_index(is_integral, index) + return pd.Series( + index=index, + data=[ + # pylint: disable=g-complex-comprehension + Variable( + self.__helper, + lower_bounds[i], + upper_bounds[i], + is_integrals[i], + f"{name}[{i}]", ) + for i in index + ], + ) + + def new_num_var_series( + self, + name: str, + index: pd.Index, + lower_bounds: Union[NumberT, pd.Series] = -math.inf, + upper_bounds: Union[NumberT, pd.Series] = math.inf, + ) -> pd.Series: + """Creates a series of continuous variables with the given name. - @property - def objective_offset(self) -> np.double: - """Returns the fixed offset of the objective.""" - return self.__helper.objective_offset() - - @objective_offset.setter - def objective_offset(self, value: NumberT) -> None: - self.__helper.set_objective_offset(value) - - def objective_expression(self) -> "LinearExpr": - """Returns the expression to optimize.""" - variables: list[Variable] = [] - coefficients: list[numbers.Real] = [] - for variable in self.get_variables(): - coeff = self.__helper.var_objective_coefficient(variable.index) - if coeff != 0.0: - variables.append(variable) - coefficients.append(coeff) - return mbh.FlatExpr(variables, coefficients, self.__helper.objective_offset()) - - # Hints. - def clear_hints(self): - """Clears all solution hints.""" - self.__helper.clear_hints() - - def add_hint(self, var: Variable, value: NumberT) -> None: - """Adds var == value as a hint to the model. - - Args: - var: The variable of the hint - value: The value of the hint - - Note that variables must not appear more than once in the list of hints. - """ - self.__helper.add_hint(var.index, value) - - # Input/Output - def export_to_lp_string(self, obfuscate: bool = False) -> str: - options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() - options.obfuscate = obfuscate - return self.__helper.export_to_lp_string(options) - - def export_to_mps_string(self, obfuscate: bool = False) -> str: - options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() - options.obfuscate = obfuscate - return self.__helper.export_to_mps_string(options) - - def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool: - options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() - options.obfuscate = obfuscate - return self.__helper.write_to_mps_file(filename, options) - - def export_to_proto(self) -> linear_solver_pb2.MPModelProto: - """Exports the optimization model to a ProtoBuf format.""" - return mbh.to_mpmodel_proto(self.__helper) - - def import_from_mps_string(self, mps_string: str) -> bool: - """Reads a model from a MPS string.""" - return self.__helper.import_from_mps_string(mps_string) - - def import_from_mps_file(self, mps_file: str) -> bool: - """Reads a model from a .mps file.""" - return self.__helper.import_from_mps_file(mps_file) - - def import_from_lp_string(self, lp_string: str) -> bool: - """Reads a model from a LP string. - - Note that this code is very limited, and will not support any real lp. - It is only intented to be use to parse test lp problems. - - Args: - lp_string: The LP string to import. - - Returns: - True if the import was successful. - """ - return self.__helper.import_from_lp_string(lp_string) - - def import_from_lp_file(self, lp_file: str) -> bool: - """Reads a model from a .lp file. - - Note that this code is very limited, and will not support any real lp. - It is only intented to be use to parse test lp problems. - - Args: - lp_file: The LP file to import. - - Returns: - True if the import was successful. - """ - return self.__helper.import_from_lp_file(lp_file) - - def import_from_proto_file(self, proto_file: str) -> bool: - """Reads a model from a proto file.""" - return self.__helper.read_model_from_proto_file(proto_file) - - def export_to_proto_file(self, proto_file: str) -> bool: - """Writes a model to a proto file.""" - return self.__helper.write_model_to_proto_file(proto_file) - - # Model getters and Setters - - @property - def num_variables(self) -> int: - """Returns the number of variables in the model.""" - return self.__helper.num_variables() - - @property - def num_constraints(self) -> int: - """The number of constraints in the model.""" - return self.__helper.num_constraints() - - @property - def name(self) -> str: - """The name of the model.""" - return self.__helper.name() + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. + lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for + variables in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. Defaults to -inf. + upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for + variables in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. Defaults to +inf. - @name.setter - def name(self, name: str): - self.__helper.set_name(name) + Returns: + pd.Series: The variable set indexed by its corresponding dimensions. - @property - def helper(self) -> mbh.ModelBuilderHelper: - """Returns the model builder helper.""" - return self.__helper + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the `lowerbound` is greater than the `upperbound`. + ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` + does not match the input index. + """ + return self.new_var_series(name, index, lower_bounds, upper_bounds, False) + def new_int_var_series( + self, + name: str, + index: pd.Index, + lower_bounds: Union[NumberT, pd.Series] = -math.inf, + upper_bounds: Union[NumberT, pd.Series] = math.inf, + ) -> pd.Series: + """Creates a series of integer variables with the given name. -class Solver: - """Main solver class. + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. + lower_bounds (Union[int, float, pd.Series]): Optional. A lower bound for + variables in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. Defaults to -inf. + upper_bounds (Union[int, float, pd.Series]): Optional. An upper bound for + variables in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. Defaults to +inf. - The purpose of this class is to search for a solution to the model provided - to the solve() method. + Returns: + pd.Series: The variable set indexed by its corresponding dimensions. - Once solve() is called, this class allows inspecting the solution found - with the value() method, as well as general statistics about the solve - procedure. + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the `lowerbound` is greater than the `upperbound`. + ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` + does not match the input index. """ + return self.new_var_series(name, index, lower_bounds, upper_bounds, True) - def __init__(self, solver_name: str): - self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper(solver_name) - self.log_callback: Optional[Callable[[str], None]] = None - - def solver_is_supported(self) -> bool: - """Checks whether the requested solver backend was found.""" - return self.__solve_helper.solver_is_supported() - - # Solver backend and parameters. - def set_time_limit_in_seconds(self, limit: NumberT) -> None: - """Sets a time limit for the solve() call.""" - self.__solve_helper.set_time_limit_in_seconds(limit) - - def set_solver_specific_parameters(self, parameters: str) -> None: - """Sets parameters specific to the solver backend.""" - self.__solve_helper.set_solver_specific_parameters(parameters) - - def enable_output(self, enabled: bool) -> None: - """Controls the solver backend logs.""" - self.__solve_helper.enable_output(enabled) - - def solve(self, model: Model) -> SolveStatus: - """Solves a problem and passes each solution to the callback if not null.""" - if self.log_callback is not None: - self.__solve_helper.set_log_callback(self.log_callback) - else: - self.__solve_helper.clear_log_callback() - self.__solve_helper.solve(model.helper) - return SolveStatus(self.__solve_helper.status()) - - def stop_search(self): - """Stops the current search asynchronously.""" - self.__solve_helper.interrupt_solve() - - def value(self, expr: LinearExprT) -> np.double: - """Returns the value of a linear expression after solve.""" - if not self.__solve_helper.has_solution(): - return pd.NA - if mbn.is_a_number(expr): - return expr - elif isinstance(expr, LinearExpr): - return self.__solve_helper.expression_value(expr) - else: - raise TypeError(f"Unknown expression {type(expr).__name__!r}") - - def values(self, variables: _IndexOrSeries) -> pd.Series: - """Returns the values of the input variables. - - If `variables` is a `pd.Index`, then the output will be indexed by the - variables. If `variables` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. - - Args: - variables (Union[pd.Index, pd.Series]): The set of variables from which to - get the values. - - Returns: - pd.Series: The values of all variables in the set. - """ - if not self.__solve_helper.has_solution(): - return _attribute_series(func=lambda v: pd.NA, values=variables) - return _attribute_series( - func=lambda v: self.__solve_helper.variable_value(v.index), - values=variables, - ) + def new_bool_var_series( + self, + name: str, + index: pd.Index, + ) -> pd.Series: + """Creates a series of Boolean variables with the given name. - def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series: - """Returns the reduced cost of the input variables. - - If `variables` is a `pd.Index`, then the output will be indexed by the - variables. If `variables` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. - - Args: - variables (Union[pd.Index, pd.Series]): The set of variables from which to - get the values. - - Returns: - pd.Series: The reduced cost of all variables in the set. - """ - if not self.__solve_helper.has_solution(): - return _attribute_series(func=lambda v: pd.NA, values=variables) - return _attribute_series( - func=lambda v: self.__solve_helper.reduced_cost(v.index), - values=variables, - ) + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. - def reduced_cost(self, var: Variable) -> np.double: - """Returns the reduced cost of a linear expression after solve.""" - if not self.__solve_helper.has_solution(): - return pd.NA - return self.__solve_helper.reduced_cost(var.index) - - def dual_values(self, constraints: _IndexOrSeries) -> pd.Series: - """Returns the dual values of the input constraints. - - If `constraints` is a `pd.Index`, then the output will be indexed by the - constraints. If `constraints` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. - - Args: - constraints (Union[pd.Index, pd.Series]): The set of constraints from - which to get the dual values. - - Returns: - pd.Series: The dual_values of all constraints in the set. - """ - if not self.__solve_helper.has_solution(): - return _attribute_series(func=lambda v: pd.NA, values=constraints) - return _attribute_series( - func=lambda v: self.__solve_helper.dual_value(v.index), - values=constraints, - ) + Returns: + pd.Series: The variable set indexed by its corresponding dimensions. - def dual_value(self, ct: LinearConstraint) -> np.double: - """Returns the dual value of a linear constraint after solve.""" - if not self.__solve_helper.has_solution(): - return pd.NA - return self.__solve_helper.dual_value(ct.index) - - def activity(self, ct: LinearConstraint) -> np.double: - """Returns the activity of a linear constraint after solve.""" - if not self.__solve_helper.has_solution(): - return pd.NA - return self.__solve_helper.activity(ct.index) - - @property - def objective_value(self) -> np.double: - """Returns the value of the objective after solve.""" - if not self.__solve_helper.has_solution(): - return pd.NA - return self.__solve_helper.objective_value() - - @property - def best_objective_bound(self) -> np.double: - """Returns the best lower (upper) bound found when min(max)imizing.""" - if not self.__solve_helper.has_solution(): - return pd.NA - return self.__solve_helper.best_objective_bound() - - @property - def status_string(self) -> str: - """Returns additional information of the last solve. - - It can describe why the model is invalid. - """ - return self.__solve_helper.status_string() - - @property - def wall_time(self) -> np.double: - return self.__solve_helper.wall_time() - - @property - def user_time(self) -> np.double: - return self.__solve_helper.user_time() + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the `lowerbound` is greater than the `upperbound`. + ValueError: if the index of `lower_bound`, `upper_bound`, or `is_integer` + does not match the input index. + """ + return self.new_var_series(name, index, 0, 1, True) + + def var_from_index(self, index: IntegerT) -> Variable: + """Rebuilds a variable object from the model and its index.""" + return Variable(self.__helper, index) + + # Linear constraints. + + def add_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars + self, + linear_expr: LinearExprT, + lb: NumberT = -math.inf, + ub: NumberT = math.inf, + name: Optional[str] = None, + ) -> LinearConstraint: + """Adds the constraint: `lb <= linear_expr <= ub` with the given name.""" + ct = LinearConstraint(self.__helper) + if name: + self.__helper.set_constraint_name(ct.index, name) + if mbn.is_a_number(linear_expr): + self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) + self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) + elif isinstance(linear_expr, LinearExpr): + flat_expr = mbh.FlatExpr(linear_expr) + # pylint: disable=protected-access + self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) + self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) + self.__helper.add_terms_to_constraint( + ct.index, flat_expr.vars, flat_expr.coeffs + ) + else: + raise TypeError( + "Not supported:" + f" Model.add_linear_constraint({type(linear_expr).__name__!r})" + ) + return ct + def add( + self, ct: Union[ConstraintT, pd.Series], name: Optional[str] = None + ) -> Union[LinearConstraint, pd.Series]: + """Adds a `BoundedLinearExpression` to the model. -def _get_index(obj: _IndexOrSeries) -> pd.Index: - """Returns the indices of `obj` as a `pd.Index`.""" - if isinstance(obj, pd.Series): - return obj.index - return obj + Args: + ct: A [`BoundedLinearExpression`](#boundedlinearexpression). + name: An optional name. + Returns: + An instance of the `Constraint` class. -def _attribute_series( - *, - func: Callable[[_VariableOrConstraint], NumberT], - values: _IndexOrSeries, -) -> pd.Series: - """Returns the attributes of `values`. + Note that a special treatment is done when the argument does not contain any + variable, and thus evaluates to True or False. + + `model.add(True)` will create a constraint 0 <= empty sum <= 0. + The constraint will be marked as under specified, and cannot be modified + thereafter. + + `model.add(False)` will create a constraint inf <= empty sum <= -inf. The + constraint will be marked as under specified, and cannot be modified + thereafter. + + you can check the if a constraint is under specified by reading the + `LinearConstraint.is_under_specified` property. + """ + if isinstance(ct, mbh.BoundedLinearExpression): + return _add_linear_constraint_to_helper(ct, self.__helper, name) + elif isinstance(ct, bool): + return _add_linear_constraint_to_helper(ct, self.__helper, name) + elif isinstance(ct, pd.Series): + return pd.Series( + index=ct.index, + data=[ + _add_linear_constraint_to_helper( + expr, self.__helper, f"{name}[{i}]" + ) + for (i, expr) in zip(ct.index, ct) + ], + ) + else: + raise TypeError(f"Not supported: Model.add({type(ct).__name__!r})") + + def linear_constraint_from_index(self, index: IntegerT) -> LinearConstraint: + """Rebuilds a linear constraint object from the model and its index.""" + return LinearConstraint(self.__helper, index=index) + + # Enforced Linear constraints. + + def add_enforced_linear_constraint( # pytype: disable=annotation-type-mismatch # numpy-scalars + self, + linear_expr: LinearExprT, + ivar: "Variable", + ivalue: bool, + lb: NumberT = -math.inf, + ub: NumberT = math.inf, + name: Optional[str] = None, + ) -> EnforcedLinearConstraint: + """Adds the constraint: `ivar == ivalue => lb <= linear_expr <= ub` with the given name.""" + ct = EnforcedLinearConstraint(self.__helper) + ct.indicator_variable = ivar + ct.indicator_value = ivalue + if name: + self.__helper.set_constraint_name(ct.index, name) + if mbn.is_a_number(linear_expr): + self.__helper.set_constraint_lower_bound(ct.index, lb - linear_expr) + self.__helper.set_constraint_upper_bound(ct.index, ub - linear_expr) + elif isinstance(linear_expr, LinearExpr): + flat_expr = mbh.FlatExpr(linear_expr) + # pylint: disable=protected-access + self.__helper.set_constraint_lower_bound(ct.index, lb - flat_expr.offset) + self.__helper.set_constraint_upper_bound(ct.index, ub - flat_expr.offset) + self.__helper.add_terms_to_constraint( + ct.index, flat_expr.vars, flat_expr.coeffs + ) + else: + raise TypeError( + "Not supported:" + f" Model.add_enforced_linear_constraint({type(linear_expr).__name__!r})" + ) + return ct + + def add_enforced( + self, + ct: Union[ConstraintT, pd.Series], + var: Union[Variable, pd.Series], + value: Union[bool, pd.Series], + name: Optional[str] = None, + ) -> Union[EnforcedLinearConstraint, pd.Series]: + """Adds a `ivar == ivalue => BoundedLinearExpression` to the model. Args: - func: The function to call for getting the attribute data. - values: The values that the function will be applied (element-wise) to. + ct: A [`BoundedLinearExpression`](#boundedlinearexpression). + var: The indicator variable + value: the indicator value + name: An optional name. Returns: - pd.Series: The attribute values. + An instance of the `Constraint` class. + + Note that a special treatment is done when the argument does not contain any + variable, and thus evaluates to True or False. + + model.add_enforced(True, ivar, ivalue) will create a constraint 0 <= empty + sum <= 0 + + model.add_enforced(False, var, value) will create a constraint inf <= + empty sum <= -inf + + you can check the if a constraint is always false (lb=inf, ub=-inf) by + calling EnforcedLinearConstraint.is_always_false() """ - return pd.Series( - data=[func(v) for v in values], - index=_get_index(values), + if isinstance(ct, mbh.BoundedLinearExpression): + return _add_enforced_linear_constraint_to_helper( + ct, self.__helper, var, value, name + ) + elif ( + isinstance(ct, bool) + and isinstance(var, Variable) + and isinstance(value, bool) + ): + return _add_enforced_linear_constraint_to_helper( + ct, self.__helper, var, value, name + ) + elif isinstance(ct, pd.Series): + ivar_series = _convert_to_var_series_and_validate_index(var, ct.index) + ivalue_series = _convert_to_series_and_validate_index(value, ct.index) + return pd.Series( + index=ct.index, + data=[ + _add_enforced_linear_constraint_to_helper( + expr, + self.__helper, + ivar_series[i], + ivalue_series[i], + f"{name}[{i}]", + ) + for (i, expr) in zip(ct.index, ct) + ], + ) + else: + raise TypeError( + f"Not supported: Model.add_enforced({type(ct).__name__!r}" + ) + + def enforced_linear_constraint_from_index( + self, index: IntegerT + ) -> EnforcedLinearConstraint: + """Rebuilds an enforced linear constraint object from the model and its index.""" + return EnforcedLinearConstraint(self.__helper, index=index) + + # Objective. + def minimize(self, linear_expr: LinearExprT) -> None: + """Minimizes the given objective.""" + self.__optimize(linear_expr, False) + + def maximize(self, linear_expr: LinearExprT) -> None: + """Maximizes the given objective.""" + self.__optimize(linear_expr, True) + + def __optimize(self, linear_expr: LinearExprT, maximize: bool) -> None: + """Defines the objective.""" + self.helper.clear_objective() + self.__helper.set_maximize(maximize) + if mbn.is_a_number(linear_expr): + self.helper.set_objective_offset(linear_expr) + elif isinstance(linear_expr, Variable): + self.helper.set_var_objective_coefficient(linear_expr.index, 1.0) + elif isinstance(linear_expr, LinearExpr): + flat_expr = mbh.FlatExpr(linear_expr) + # pylint: disable=protected-access + self.helper.set_objective_offset(flat_expr.offset) + var_indices = [var.index for var in flat_expr.vars] + self.helper.set_objective_coefficients(var_indices, flat_expr.coeffs) + else: + raise TypeError( + "Not supported:" + f" Model.minimize/maximize({type(linear_expr).__name__!r})" + ) + + @property + def objective_offset(self) -> np.double: + """Returns the fixed offset of the objective.""" + return self.__helper.objective_offset() + + @objective_offset.setter + def objective_offset(self, value: NumberT) -> None: + self.__helper.set_objective_offset(value) + + def objective_expression(self) -> "LinearExpr": + """Returns the expression to optimize.""" + variables: list[Variable] = [] + coefficients: list[numbers.Real] = [] + for variable in self.get_variables(): + coeff = self.__helper.var_objective_coefficient(variable.index) + if coeff != 0.0: + variables.append(variable) + coefficients.append(coeff) + return mbh.FlatExpr( + variables, coefficients, self.__helper.objective_offset() ) + # Hints. + def clear_hints(self): + """Clears all solution hints.""" + self.__helper.clear_hints() -def _convert_to_series_and_validate_index( - value_or_series: Union[bool, NumberT, pd.Series], index: pd.Index -) -> pd.Series: - """Returns a pd.Series of the given index with the corresponding values. + def add_hint(self, var: Variable, value: NumberT) -> None: + """Adds var == value as a hint to the model. + + Args: + var: The variable of the hint + value: The value of the hint + + Note that variables must not appear more than once in the list of hints. + """ + self.__helper.add_hint(var.index, value) + + # Input/Output + def export_to_lp_string(self, obfuscate: bool = False) -> str: + options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() + options.obfuscate = obfuscate + return self.__helper.export_to_lp_string(options) + + def export_to_mps_string(self, obfuscate: bool = False) -> str: + options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() + options.obfuscate = obfuscate + return self.__helper.export_to_mps_string(options) + + def write_to_mps_file(self, filename: str, obfuscate: bool = False) -> bool: + options: mbh.MPModelExportOptions = mbh.MPModelExportOptions() + options.obfuscate = obfuscate + return self.__helper.write_to_mps_file(filename, options) + + def export_to_proto(self) -> linear_solver_pb2.MPModelProto: + """Exports the optimization model to a ProtoBuf format.""" + return mbh.to_mpmodel_proto(self.__helper) + + def import_from_mps_string(self, mps_string: str) -> bool: + """Reads a model from a MPS string.""" + return self.__helper.import_from_mps_string(mps_string) + + def import_from_mps_file(self, mps_file: str) -> bool: + """Reads a model from a .mps file.""" + return self.__helper.import_from_mps_file(mps_file) + + def import_from_lp_string(self, lp_string: str) -> bool: + """Reads a model from a LP string. + + Note that this code is very limited, and will not support any real lp. + It is only intented to be use to parse test lp problems. Args: - value_or_series: the values to be converted (if applicable). - index: the index of the resulting pd.Series. + lp_string: The LP string to import. Returns: - pd.Series: The set of values with the given index. + True if the import was successful. + """ + return self.__helper.import_from_lp_string(lp_string) - Raises: - TypeError: If the type of `value_or_series` is not recognized. - ValueError: If the index does not match. + def import_from_lp_file(self, lp_file: str) -> bool: + """Reads a model from a .lp file. + + Note that this code is very limited, and will not support any real lp. + It is only intented to be use to parse test lp problems. + + Args: + lp_file: The LP file to import. + + Returns: + True if the import was successful. """ - if mbn.is_a_number(value_or_series) or isinstance(value_or_series, bool): - result = pd.Series(data=value_or_series, index=index) - elif isinstance(value_or_series, pd.Series): - if value_or_series.index.equals(index): - result = value_or_series - else: - raise ValueError("index does not match") + return self.__helper.import_from_lp_file(lp_file) + + def import_from_proto_file(self, proto_file: str) -> bool: + """Reads a model from a proto file.""" + return self.__helper.read_model_from_proto_file(proto_file) + + def export_to_proto_file(self, proto_file: str) -> bool: + """Writes a model to a proto file.""" + return self.__helper.write_model_to_proto_file(proto_file) + + # Model getters and Setters + + @property + def num_variables(self) -> int: + """Returns the number of variables in the model.""" + return self.__helper.num_variables() + + @property + def num_constraints(self) -> int: + """The number of constraints in the model.""" + return self.__helper.num_constraints() + + @property + def name(self) -> str: + """The name of the model.""" + return self.__helper.name() + + @name.setter + def name(self, name: str): + self.__helper.set_name(name) + + @property + def helper(self) -> mbh.ModelBuilderHelper: + """Returns the model builder helper.""" + return self.__helper + + +class Solver: + """Main solver class. + + The purpose of this class is to search for a solution to the model provided + to the solve() method. + + Once solve() is called, this class allows inspecting the solution found + with the value() method, as well as general statistics about the solve + procedure. + """ + + def __init__(self, solver_name: str): + self.__solve_helper: mbh.ModelSolverHelper = mbh.ModelSolverHelper( + solver_name + ) + self.log_callback: Optional[Callable[[str], None]] = None + + def solver_is_supported(self) -> bool: + """Checks whether the requested solver backend was found.""" + return self.__solve_helper.solver_is_supported() + + # Solver backend and parameters. + def set_time_limit_in_seconds(self, limit: NumberT) -> None: + """Sets a time limit for the solve() call.""" + self.__solve_helper.set_time_limit_in_seconds(limit) + + def set_solver_specific_parameters(self, parameters: str) -> None: + """Sets parameters specific to the solver backend.""" + self.__solve_helper.set_solver_specific_parameters(parameters) + + def enable_output(self, enabled: bool) -> None: + """Controls the solver backend logs.""" + self.__solve_helper.enable_output(enabled) + + def solve(self, model: Model) -> SolveStatus: + """Solves a problem and passes each solution to the callback if not null.""" + if self.log_callback is not None: + self.__solve_helper.set_log_callback(self.log_callback) else: - raise TypeError("invalid type={type(value_or_series).__name!r}") - return result + self.__solve_helper.clear_log_callback() + self.__solve_helper.solve(model.helper) + return SolveStatus(self.__solve_helper.status()) + + def stop_search(self): + """Stops the current search asynchronously.""" + self.__solve_helper.interrupt_solve() + + def value(self, expr: LinearExprT) -> np.double: + """Returns the value of a linear expression after solve.""" + if not self.__solve_helper.has_solution(): + return pd.NA + if mbn.is_a_number(expr): + return expr + elif isinstance(expr, LinearExpr): + return self.__solve_helper.expression_value(expr) + else: + raise TypeError(f"Unknown expression {type(expr).__name__!r}") + def values(self, variables: _IndexOrSeries) -> pd.Series: + """Returns the values of the input variables. -def _convert_to_var_series_and_validate_index( - var_or_series: Union["Variable", pd.Series], index: pd.Index -) -> pd.Series: - """Returns a pd.Series of the given index with the corresponding values. + If `variables` is a `pd.Index`, then the output will be indexed by the + variables. If `variables` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. Args: - var_or_series: the variables to be converted (if applicable). - index: the index of the resulting pd.Series. + variables (Union[pd.Index, pd.Series]): The set of variables from which to + get the values. Returns: - pd.Series: The set of values with the given index. + pd.Series: The values of all variables in the set. + """ + if not self.__solve_helper.has_solution(): + return _attribute_series(func=lambda v: pd.NA, values=variables) + return _attribute_series( + func=lambda v: self.__solve_helper.variable_value(v.index), + values=variables, + ) - Raises: - TypeError: If the type of `value_or_series` is not recognized. - ValueError: If the index does not match. + def reduced_costs(self, variables: _IndexOrSeries) -> pd.Series: + """Returns the reduced cost of the input variables. + + If `variables` is a `pd.Index`, then the output will be indexed by the + variables. If `variables` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + + Args: + variables (Union[pd.Index, pd.Series]): The set of variables from which to + get the values. + + Returns: + pd.Series: The reduced cost of all variables in the set. """ - if isinstance(var_or_series, Variable): - result = pd.Series(data=var_or_series, index=index) - elif isinstance(var_or_series, pd.Series): - if var_or_series.index.equals(index): - result = var_or_series - else: - raise ValueError("index does not match") + if not self.__solve_helper.has_solution(): + return _attribute_series(func=lambda v: pd.NA, values=variables) + return _attribute_series( + func=lambda v: self.__solve_helper.reduced_cost(v.index), + values=variables, + ) + + def reduced_cost(self, var: Variable) -> np.double: + """Returns the reduced cost of a linear expression after solve.""" + if not self.__solve_helper.has_solution(): + return pd.NA + return self.__solve_helper.reduced_cost(var.index) + + def dual_values(self, constraints: _IndexOrSeries) -> pd.Series: + """Returns the dual values of the input constraints. + + If `constraints` is a `pd.Index`, then the output will be indexed by the + constraints. If `constraints` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. + + Args: + constraints (Union[pd.Index, pd.Series]): The set of constraints from + which to get the dual values. + + Returns: + pd.Series: The dual_values of all constraints in the set. + """ + if not self.__solve_helper.has_solution(): + return _attribute_series(func=lambda v: pd.NA, values=constraints) + return _attribute_series( + func=lambda v: self.__solve_helper.dual_value(v.index), + values=constraints, + ) + + def dual_value(self, ct: LinearConstraint) -> np.double: + """Returns the dual value of a linear constraint after solve.""" + if not self.__solve_helper.has_solution(): + return pd.NA + return self.__solve_helper.dual_value(ct.index) + + def activity(self, ct: LinearConstraint) -> np.double: + """Returns the activity of a linear constraint after solve.""" + if not self.__solve_helper.has_solution(): + return pd.NA + return self.__solve_helper.activity(ct.index) + + @property + def objective_value(self) -> np.double: + """Returns the value of the objective after solve.""" + if not self.__solve_helper.has_solution(): + return pd.NA + return self.__solve_helper.objective_value() + + @property + def best_objective_bound(self) -> np.double: + """Returns the best lower (upper) bound found when min(max)imizing.""" + if not self.__solve_helper.has_solution(): + return pd.NA + return self.__solve_helper.best_objective_bound() + + @property + def status_string(self) -> str: + """Returns additional information of the last solve. + + It can describe why the model is invalid. + """ + return self.__solve_helper.status_string() + + @property + def wall_time(self) -> np.double: + return self.__solve_helper.wall_time() + + @property + def user_time(self) -> np.double: + return self.__solve_helper.user_time() + + +def _get_index(obj: _IndexOrSeries) -> pd.Index: + """Returns the indices of `obj` as a `pd.Index`.""" + if isinstance(obj, pd.Series): + return obj.index + return obj + + +def _attribute_series( + *, + func: Callable[[_VariableOrConstraint], NumberT], + values: _IndexOrSeries, +) -> pd.Series: + """Returns the attributes of `values`. + + Args: + func: The function to call for getting the attribute data. + values: The values that the function will be applied (element-wise) to. + + Returns: + pd.Series: The attribute values. + """ + return pd.Series( + data=[func(v) for v in values], + index=_get_index(values), + ) + + +def _convert_to_series_and_validate_index( + value_or_series: Union[bool, NumberT, pd.Series], index: pd.Index +) -> pd.Series: + """Returns a pd.Series of the given index with the corresponding values. + + Args: + value_or_series: the values to be converted (if applicable). + index: the index of the resulting pd.Series. + + Returns: + pd.Series: The set of values with the given index. + + Raises: + TypeError: If the type of `value_or_series` is not recognized. + ValueError: If the index does not match. + """ + if mbn.is_a_number(value_or_series) or isinstance(value_or_series, bool): + result = pd.Series(data=value_or_series, index=index) + elif isinstance(value_or_series, pd.Series): + if value_or_series.index.equals(index): + result = value_or_series + else: + raise ValueError("index does not match") + else: + raise TypeError("invalid type={type(value_or_series).__name!r}") + return result + + +def _convert_to_var_series_and_validate_index( + var_or_series: Union["Variable", pd.Series], index: pd.Index +) -> pd.Series: + """Returns a pd.Series of the given index with the corresponding values. + + Args: + var_or_series: the variables to be converted (if applicable). + index: the index of the resulting pd.Series. + + Returns: + pd.Series: The set of values with the given index. + + Raises: + TypeError: If the type of `value_or_series` is not recognized. + ValueError: If the index does not match. + """ + if isinstance(var_or_series, Variable): + result = pd.Series(data=var_or_series, index=index) + elif isinstance(var_or_series, pd.Series): + if var_or_series.index.equals(index): + result = var_or_series else: - raise TypeError("invalid type={type(value_or_series).__name!r}") - return result + raise ValueError("index does not match") + else: + raise TypeError("invalid type={type(value_or_series).__name!r}") + return result # Compatibility. diff --git a/ortools/linear_solver/python/model_builder_helper.cc b/ortools/linear_solver/python/model_builder_helper.cc index f085b0fe6ad..80368281597 100644 --- a/ortools/linear_solver/python/model_builder_helper.cc +++ b/ortools/linear_solver/python/model_builder_helper.cc @@ -439,11 +439,7 @@ PYBIND11_MODULE(model_builder_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddInPlace(other); - return expr; - } - return expr->Add(other); + return (num_uses == 4) ? expr->AddInPlace(other) : expr->Add(other); }, py::arg("other").none(false), "Returns the sum of `self` and `other`.") @@ -453,26 +449,43 @@ PYBIND11_MODULE(model_builder_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddFloatInPlace(cst); - return expr; - } - return expr->AddFloat(cst); + return (num_uses == 4) ? expr->AddFloatInPlace(cst) + : expr->AddFloat(cst); + }, + py::arg("cst"), "Returns `self` + `cst`.") + .def( + "__radd__", + [](py::object self, + std::shared_ptr other) -> std::shared_ptr { + const int num_uses = Py_REFCNT(self.ptr()); + std::shared_ptr expr = + self.cast>(); + return (num_uses == 4) ? expr->AddInPlace(other) : expr->Add(other); }, py::arg("cst"), "Returns `self` + `cst`.") - .def("__radd__", &LinearExpr::Add, py::arg("other").none(false), - "Returns `self` + `other`.") .def( "__radd__", [](py::object self, double cst) -> std::shared_ptr { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddFloatInPlace(cst); - return expr; - } - return expr->AddFloat(cst); + return (num_uses == 4) ? expr->AddFloatInPlace(cst) + : expr->AddFloat(cst); + }, + py::arg("cst"), "Returns `self` + `cst`.") + .def( + "__iadd__", + [](std::shared_ptr expr, + std::shared_ptr other) -> std::shared_ptr { + return expr->AddInPlace(other); + }, + py::arg("other").none(false), + "Returns the sum of `self` and `other`.") + .def( + "__iadd__", + [](std::shared_ptr expr, + double cst) -> std::shared_ptr { + return expr->AddFloatInPlace(cst); }, py::arg("cst"), "Returns `self` + `cst`.") .def( @@ -482,11 +495,8 @@ PYBIND11_MODULE(model_builder_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddInPlace(other->Neg()); - return expr; - } - return expr->Sub(other); + return (num_uses == 4) ? expr->AddInPlace(other->Neg()) + : expr->Sub(other); }, py::arg("other").none(false), "Returns `self` - `other`.") .def( @@ -495,11 +505,23 @@ PYBIND11_MODULE(model_builder_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddFloatInPlace(-cst); - return expr; - } - return expr->SubFloat(cst); + return (num_uses == 4) ? expr->AddFloatInPlace(-cst) + : expr->SubFloat(cst); + }, + py::arg("cst"), "Returns `self` - `cst`.") + .def( + "__isub__", + [](std::shared_ptr expr, + std::shared_ptr other) -> std::shared_ptr { + expr->AddInPlace(other->Neg()); + return expr->AddInPlace(other->Neg()); + }, + py::arg("other").none(false), "Returns `self` - `other`.") + .def( + "__isub__", + [](std::shared_ptr expr, + double cst) -> std::shared_ptr { + return expr->AddFloatInPlace(-cst); }, py::arg("cst"), "Returns `self` - `cst`.") .def_property_readonly( @@ -511,25 +533,6 @@ PYBIND11_MODULE(model_builder_helper, m) { py::class_, LinearExpr>(m, "AffineExpr") .def(py::init, double, double>()) - .def("__add__", &AffineExpr::Add, py::arg("other").none(false), - "Returns `self` + `other`.") - .def("__add__", &AffineExpr::AddFloat, py::arg("cst"), - "Returns `self` + `cst`.") - .def("__radd__", &AffineExpr::Add, py::arg("other").none(false), - "Returns `self` + `other`.") - .def("__radd__", &AffineExpr::AddFloat, py::arg("cst"), - "Returns `self` + `cst`.") - .def("__sub__", &AffineExpr::Sub, py::arg("other").none(false), - "Returns `self` - `other`.") - .def("__sub__", &AffineExpr::SubFloat, py::arg("cst"), - "Returns `self` - `cst`.") - .def("__rsub__", &AffineExpr::RSubFloat, py::arg("cst"), - "Returns `cst` - `self`.") - .def("__mul__", &AffineExpr::MulFloat, py::arg("cst"), - "Returns `self` * `cst`.") - .def("__rmul__", &AffineExpr::MulFloat, py::arg("cst"), - "Returns `self` * `cst`.") - .def("__neg__", &AffineExpr::Neg, "Returns -`self`.") .def_property_readonly("expression", &AffineExpr ::expression) .def_property_readonly("coefficient", &AffineExpr::coefficient) .def_property_readonly("offset", &AffineExpr::offset); diff --git a/ortools/linear_solver/python/model_builder_helper_test.py b/ortools/linear_solver/python/model_builder_helper_test.py index 664cb6ab6ea..7309e35c07e 100644 --- a/ortools/linear_solver/python/model_builder_helper_test.py +++ b/ortools/linear_solver/python/model_builder_helper_test.py @@ -26,187 +26,187 @@ class PywrapModelBuilderHelperTest(absltest.TestCase): - def test_export_model_proto_to_mps_string(self): - model = model_builder_helper.ModelBuilderHelper() - model.set_name("testmodel") - result = model.export_to_mps_string() - self.assertIn("testmodel", result) - self.assertIn("ENDATA", result) - - def test_export_model_proto_to_lp_string(self): - model = model_builder_helper.ModelBuilderHelper() - model.set_maximize(True) - lp_string = model.export_to_lp_string() - self.assertIn("Maximize", lp_string) - - def test_import_from_mps_string(self): - model = model_builder_helper.ModelBuilderHelper() - self.assertTrue(model.import_from_mps_string("NAME testmodel")) - self.assertEqual(model.name(), "testmodel") - - # ImportFromMpsFile doesn't read from files yet - def test_import_from_mps_file(self): - path = os.path.dirname(__file__) - mps_path = f"{path}/../testdata/maximization.mps" - model = model_builder_helper.ModelBuilderHelper() - self.assertTrue(model.import_from_mps_file(mps_path)) - self.assertEqual(model.name(), "SupportedMaximizationProblem") - - def test_import_from_lp_string(self): - model = model_builder_helper.ModelBuilderHelper() - model.import_from_lp_string("max:") - self.assertTrue(model.maximize()) - - # TODO(user): Add test_import_from_lp_file after the implementation is fixed - - def test_solve_with_glop(self): - model = linear_solver_pb2.MPModelProto() - model.variable.append( - linear_solver_pb2.MPVariableProto( - lower_bound=0.0, upper_bound=1.0, objective_coefficient=1.0 - ) + def test_export_model_proto_to_mps_string(self): + model = model_builder_helper.ModelBuilderHelper() + model.set_name("testmodel") + result = model.export_to_mps_string() + self.assertIn("testmodel", result) + self.assertIn("ENDATA", result) + + def test_export_model_proto_to_lp_string(self): + model = model_builder_helper.ModelBuilderHelper() + model.set_maximize(True) + lp_string = model.export_to_lp_string() + self.assertIn("Maximize", lp_string) + + def test_import_from_mps_string(self): + model = model_builder_helper.ModelBuilderHelper() + self.assertTrue(model.import_from_mps_string("NAME testmodel")) + self.assertEqual(model.name(), "testmodel") + + # ImportFromMpsFile doesn't read from files yet + def test_import_from_mps_file(self): + path = os.path.dirname(__file__) + mps_path = f"{path}/../testdata/maximization.mps" + model = model_builder_helper.ModelBuilderHelper() + self.assertTrue(model.import_from_mps_file(mps_path)) + self.assertEqual(model.name(), "SupportedMaximizationProblem") + + def test_import_from_lp_string(self): + model = model_builder_helper.ModelBuilderHelper() + model.import_from_lp_string("max:") + self.assertTrue(model.maximize()) + + # TODO(user): Add test_import_from_lp_file after the implementation is fixed + + def test_solve_with_glop(self): + model = linear_solver_pb2.MPModelProto() + model.variable.append( + linear_solver_pb2.MPVariableProto( + lower_bound=0.0, upper_bound=1.0, objective_coefficient=1.0 ) - model.maximize = True - request = linear_solver_pb2.MPModelRequest( - model=model, - solver_type=linear_solver_pb2.MPModelRequest.GLOP_LINEAR_PROGRAMMING, + ) + model.maximize = True + request = linear_solver_pb2.MPModelRequest( + model=model, + solver_type=linear_solver_pb2.MPModelRequest.GLOP_LINEAR_PROGRAMMING, + ) + solver_helper = model_builder_helper.ModelSolverHelper("") + result = solver_helper.solve_serialized_request(request.SerializeToString()) + response = linear_solver_pb2.MPSolutionResponse().FromString(result) + self.assertEqual( + response.status, + linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL, + ) + self.assertAlmostEqual(response.objective_value, 1.0) + + def test_solve_with_glop_direct(self): + model = model_builder_helper.ModelBuilderHelper() + self.assertEqual(0, model.add_var()) + model.set_var_lower_bound(0, 0.0) + model.set_var_upper_bound(0, 1.0) + model.set_var_objective_coefficient(0, 1.0) + model.set_maximize(True) + + solver = model_builder_helper.ModelSolverHelper("glop") + solver.solve(model) + self.assertEqual( + solver.status(), + linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL, + ) + self.assertAlmostEqual(solver.objective_value(), 1.0) + self.assertAlmostEqual(solver.variable_value(0), 1.0) + values = solver.variable_values() + self.assertEqual(1, len(values)) + self.assertAlmostEqual(1.0, values[0]) + + def test_solve_with_pdlp(self): + model = linear_solver_pb2.MPModelProto() + model.variable.append( + linear_solver_pb2.MPVariableProto( + lower_bound=0.0, upper_bound=1.0, objective_coefficient=1.0 ) - solver_helper = model_builder_helper.ModelSolverHelper("") - result = solver_helper.solve_serialized_request(request.SerializeToString()) - response = linear_solver_pb2.MPSolutionResponse().FromString(result) - self.assertEqual( - response.status, - linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL, - ) - self.assertAlmostEqual(response.objective_value, 1.0) - - def test_solve_with_glop_direct(self): - model = model_builder_helper.ModelBuilderHelper() - self.assertEqual(0, model.add_var()) - model.set_var_lower_bound(0, 0.0) - model.set_var_upper_bound(0, 1.0) - model.set_var_objective_coefficient(0, 1.0) - model.set_maximize(True) - - solver = model_builder_helper.ModelSolverHelper("glop") - solver.solve(model) - self.assertEqual( - solver.status(), - linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL, - ) - self.assertAlmostEqual(solver.objective_value(), 1.0) - self.assertAlmostEqual(solver.variable_value(0), 1.0) - values = solver.variable_values() - self.assertEqual(1, len(values)) - self.assertAlmostEqual(1.0, values[0]) - - def test_solve_with_pdlp(self): - model = linear_solver_pb2.MPModelProto() - model.variable.append( - linear_solver_pb2.MPVariableProto( - lower_bound=0.0, upper_bound=1.0, objective_coefficient=1.0 - ) - ) - model.maximize = True - request = linear_solver_pb2.MPModelRequest( - model=model, - solver_type=linear_solver_pb2.MPModelRequest.PDLP_LINEAR_PROGRAMMING, - ) - solver_helper = model_builder_helper.ModelSolverHelper("") - result = solver_helper.solve_serialized_request(request.SerializeToString()) - if result: - response = linear_solver_pb2.MPSolutionResponse().FromString(result) - self.assertEqual( - response.status, - linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL, - ) - self.assertAlmostEqual(response.objective_value, 1.0) - else: - print("Solver not supported.") - - # TODO(user): Test the log callback after the implementation is completed. - - def test_interrupt_solve(self): - # This is an instance that we know Glop won't solve quickly. - path = os.path.dirname(__file__) - mps_path = f"{path}/../testdata/large_model.mps.gz" - with gzip.open(mps_path, "r") as f: - mps_data = f.read() - model_helper = model_builder_helper.ModelBuilderHelper() - self.assertTrue(model_helper.import_from_mps_string(mps_data)) - solver_helper = model_builder_helper.ModelSolverHelper("glop") - - result = [] - solve_thread = threading.Thread( - target=lambda: result.append(solver_helper.solve(model_helper)) - ) - solve_thread.start() - self.assertTrue(solver_helper.interrupt_solve()) - solve_thread.join(timeout=30.0) - self.assertTrue(solver_helper.has_response()) - self.assertEqual( - solver_helper.status(), - model_builder_helper.SolveStatus.CANCELLED_BY_USER, - ) - - def test_build_model(self): - var_lb = np.array([-1.0]) - var_ub = np.array([np.inf]) - obj = np.array([10.0]) - con_lb = np.array([-5.0, -6.0]) - con_ub = np.array([5.0, 6.0]) - constraint_matrix = sparse.csr_matrix(np.array([[1.0], [2.0]])) - - model = model_builder_helper.ModelBuilderHelper() - model.fill_model_from_sparse_data( - var_lb, var_ub, obj, con_lb, con_ub, constraint_matrix - ) - self.assertEqual(1, model.num_variables()) - self.assertEqual(-1.0, model.var_lower_bound(0)) - self.assertEqual(np.inf, model.var_upper_bound(0)) - self.assertEqual(10.0, model.var_objective_coefficient(0)) - - self.assertEqual(2, model.num_constraints()) - self.assertEqual(-5.0, model.constraint_lower_bound(0)) - self.assertEqual(5.0, model.constraint_upper_bound(0)) - self.assertEqual([0], model.constraint_var_indices(0)) - self.assertEqual([1.0], model.constraint_coefficients(0)) - - self.assertEqual(-6.0, model.constraint_lower_bound(1)) - self.assertEqual(6.0, model.constraint_upper_bound(1)) - self.assertEqual([0], model.constraint_var_indices(1)) - self.assertEqual([2.0], model.constraint_coefficients(1)) - - var_array = model.add_var_array([10], 1.0, 5.0, True, "var_") - self.assertEqual(1, var_array.ndim) - self.assertEqual(10, var_array.size) - self.assertEqual((10,), var_array.shape) - self.assertEqual(model.var_name(var_array[3]), "var_3") - - def test_set_coefficient(self): - var_lb = np.array([-1.0, -2.0]) - var_ub = np.array([np.inf, np.inf]) - obj = np.array([10.0, 20.0]) - con_lb = np.array([-5.0, -6.0]) - con_ub = np.array([5.0, 6.0]) - constraint_matrix = sparse.csr_matrix(np.array([[1.0, 3.0], [2.0, 4.0]])) - - model = model_builder_helper.ModelBuilderHelper() - model.fill_model_from_sparse_data( - var_lb, var_ub, obj, con_lb, con_ub, constraint_matrix - ) - # Here, we add new variables to test that we are able to set coefficients - # for variables that are not yet in the constraint. - var_index1 = model.add_var() - var_index2 = model.add_var() - model.set_constraint_coefficient(0, var_index2, 5.0) - model.set_constraint_coefficient(0, var_index1, 6.0) - self.assertEqual([1.0, 3.0, 5.0, 6.0], model.constraint_coefficients(0)) - # Here, we test that we are able to set coefficients for variables whose - # index in the constraint is different from its index in the model. - model.set_constraint_coefficient(0, var_index2, 7.0) - self.assertEqual([1.0, 3.0, 7.0, 6.0], model.constraint_coefficients(0)) + ) + model.maximize = True + request = linear_solver_pb2.MPModelRequest( + model=model, + solver_type=linear_solver_pb2.MPModelRequest.PDLP_LINEAR_PROGRAMMING, + ) + solver_helper = model_builder_helper.ModelSolverHelper("") + result = solver_helper.solve_serialized_request(request.SerializeToString()) + if result: + response = linear_solver_pb2.MPSolutionResponse().FromString(result) + self.assertEqual( + response.status, + linear_solver_pb2.MPSolverResponseStatus.MPSOLVER_OPTIMAL, + ) + self.assertAlmostEqual(response.objective_value, 1.0) + else: + print("Solver not supported.") + + # TODO(user): Test the log callback after the implementation is completed. + + def test_interrupt_solve(self): + # This is an instance that we know Glop won't solve quickly. + path = os.path.dirname(__file__) + mps_path = f"{path}/../testdata/large_model.mps.gz" + with gzip.open(mps_path, "r") as f: + mps_data = f.read() + model_helper = model_builder_helper.ModelBuilderHelper() + self.assertTrue(model_helper.import_from_mps_string(mps_data)) + solver_helper = model_builder_helper.ModelSolverHelper("glop") + + result = [] + solve_thread = threading.Thread( + target=lambda: result.append(solver_helper.solve(model_helper)) + ) + solve_thread.start() + self.assertTrue(solver_helper.interrupt_solve()) + solve_thread.join(timeout=30.0) + self.assertTrue(solver_helper.has_response()) + self.assertEqual( + solver_helper.status(), + model_builder_helper.SolveStatus.CANCELLED_BY_USER, + ) + + def test_build_model(self): + var_lb = np.array([-1.0]) + var_ub = np.array([np.inf]) + obj = np.array([10.0]) + con_lb = np.array([-5.0, -6.0]) + con_ub = np.array([5.0, 6.0]) + constraint_matrix = sparse.csr_matrix(np.array([[1.0], [2.0]])) + + model = model_builder_helper.ModelBuilderHelper() + model.fill_model_from_sparse_data( + var_lb, var_ub, obj, con_lb, con_ub, constraint_matrix + ) + self.assertEqual(1, model.num_variables()) + self.assertEqual(-1.0, model.var_lower_bound(0)) + self.assertEqual(np.inf, model.var_upper_bound(0)) + self.assertEqual(10.0, model.var_objective_coefficient(0)) + + self.assertEqual(2, model.num_constraints()) + self.assertEqual(-5.0, model.constraint_lower_bound(0)) + self.assertEqual(5.0, model.constraint_upper_bound(0)) + self.assertEqual([0], model.constraint_var_indices(0)) + self.assertEqual([1.0], model.constraint_coefficients(0)) + + self.assertEqual(-6.0, model.constraint_lower_bound(1)) + self.assertEqual(6.0, model.constraint_upper_bound(1)) + self.assertEqual([0], model.constraint_var_indices(1)) + self.assertEqual([2.0], model.constraint_coefficients(1)) + + var_array = model.add_var_array([10], 1.0, 5.0, True, "var_") + self.assertEqual(1, var_array.ndim) + self.assertEqual(10, var_array.size) + self.assertEqual((10,), var_array.shape) + self.assertEqual(model.var_name(var_array[3]), "var_3") + + def test_set_coefficient(self): + var_lb = np.array([-1.0, -2.0]) + var_ub = np.array([np.inf, np.inf]) + obj = np.array([10.0, 20.0]) + con_lb = np.array([-5.0, -6.0]) + con_ub = np.array([5.0, 6.0]) + constraint_matrix = sparse.csr_matrix(np.array([[1.0, 3.0], [2.0, 4.0]])) + + model = model_builder_helper.ModelBuilderHelper() + model.fill_model_from_sparse_data( + var_lb, var_ub, obj, con_lb, con_ub, constraint_matrix + ) + # Here, we add new variables to test that we are able to set coefficients + # for variables that are not yet in the constraint. + var_index1 = model.add_var() + var_index2 = model.add_var() + model.set_constraint_coefficient(0, var_index2, 5.0) + model.set_constraint_coefficient(0, var_index1, 6.0) + self.assertEqual([1.0, 3.0, 5.0, 6.0], model.constraint_coefficients(0)) + # Here, we test that we are able to set coefficients for variables whose + # index in the constraint is different from its index in the model. + model.set_constraint_coefficient(0, var_index2, 7.0) + self.assertEqual([1.0, 3.0, 7.0, 6.0], model.constraint_coefficients(0)) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/linear_solver/python/model_builder_numbers.py b/ortools/linear_solver/python/model_builder_numbers.py index 21131378c9b..f9aa1e68dae 100644 --- a/ortools/linear_solver/python/model_builder_numbers.py +++ b/ortools/linear_solver/python/model_builder_numbers.py @@ -23,47 +23,53 @@ def is_integral(x: Any) -> bool: - """Checks if x has either a number.Integral or a np.integer type.""" - return isinstance(x, numbers.Integral) or isinstance(x, np.integer) + """Checks if x has either a number.Integral or a np.integer type.""" + return isinstance(x, numbers.Integral) or isinstance(x, np.integer) def is_a_number(x: Any) -> bool: - """Checks if x has either a number.Number or a np.double type.""" - return ( - isinstance(x, numbers.Number) - or isinstance(x, np.double) - or isinstance(x, np.integer) - ) + """Checks if x has either a number.Number or a np.double type.""" + return ( + isinstance(x, numbers.Number) + or isinstance(x, np.double) + or isinstance(x, np.integer) + ) def is_zero(x: Any) -> bool: - """Checks if the x is 0 or 0.0.""" - return (is_integral(x) and int(x) == 0) or (is_a_number(x) and float(x) == 0.0) + """Checks if the x is 0 or 0.0.""" + return (is_integral(x) and int(x) == 0) or ( + is_a_number(x) and float(x) == 0.0 + ) def is_one(x: Any) -> bool: - """Checks if x is 1 or 1.0.""" - return (is_integral(x) and int(x) == 1) or (is_a_number(x) and float(x) == 1.0) + """Checks if x is 1 or 1.0.""" + return (is_integral(x) and int(x) == 1) or ( + is_a_number(x) and float(x) == 1.0 + ) def is_minus_one(x: Any) -> bool: - """Checks if x is -1 or -1.0.""" - return (is_integral(x) and int(x) == -1) or (is_a_number(x) and float(x) == -1.0) + """Checks if x is -1 or -1.0.""" + return (is_integral(x) and int(x) == -1) or ( + is_a_number(x) and float(x) == -1.0 + ) def assert_is_a_number(x: NumberT) -> np.double: - """Asserts that x is a number and converts to a np.double.""" - if not is_a_number(x): - raise TypeError("Not a number: %s" % x) - return np.double(x) + """Asserts that x is a number and converts to a np.double.""" + if not is_a_number(x): + raise TypeError("Not a number: %s" % x) + return np.double(x) def assert_is_a_number_array(x: Sequence[NumberT]) -> npt.NDArray[np.double]: - """Asserts x is a list of numbers and converts it to np.array(np.double).""" - result = np.empty(len(x), dtype=np.double) - pos = 0 - for c in x: - result[pos] = assert_is_a_number(c) - pos += 1 - assert pos == len(x) - return result + """Asserts x is a list of numbers and converts it to np.array(np.double).""" + result = np.empty(len(x), dtype=np.double) + pos = 0 + for c in x: + result[pos] = assert_is_a_number(c) + pos += 1 + assert pos == len(x) + return result diff --git a/ortools/linear_solver/python/model_builder_test.py b/ortools/linear_solver/python/model_builder_test.py index 475289c4a2c..7dc5a8ef1f9 100644 --- a/ortools/linear_solver/python/model_builder_test.py +++ b/ortools/linear_solver/python/model_builder_test.py @@ -31,121 +31,121 @@ def build_dict(expr: mb.LinearExprT) -> Dict[mbh.Variable, float]: - res = {} - flat_expr = mbh.FlatExpr(expr) - for var, coeff in zip(flat_expr.vars, flat_expr.coeffs): - if not coeff: - continue - res[var] = coeff - return res + res = {} + flat_expr = mbh.FlatExpr(expr) + for var, coeff in zip(flat_expr.vars, flat_expr.coeffs): + if not coeff: + continue + res[var] = coeff + return res class ModelBuilderTest(absltest.TestCase): - # Number of decimal places to use for numerical tolerance for - # checking primal, dual, objective values and other values. - NUM_PLACES = 5 - - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() - - # pylint: disable=too-many-statements - def run_minimal_linear_example(self, solver_name): - """Minimal Linear Example.""" - model = mb.Model() - model.name = "minimal_linear_example" - x1 = model.new_num_var(0.0, math.inf, "x1") - x2 = model.new_num_var(0.0, math.inf, "x2") - x3 = model.new_num_var(0.0, math.inf, "x3") - self.assertEqual(3, model.num_variables) - self.assertFalse(x1.is_integral) - self.assertEqual(0.0, x1.lower_bound) - self.assertEqual(math.inf, x2.upper_bound) - x1.lower_bound = 1.0 - self.assertEqual(1.0, x1.lower_bound) - - model.maximize(10.0 * x1 + 6 * x2 + 4.0 * x3 - 3.5) - self.assertEqual(4.0, x3.objective_coefficient) - self.assertEqual(-3.5, model.objective_offset) - model.objective_offset = -5.5 - self.assertEqual(-5.5, model.objective_offset) - - c0 = model.add(x1 + x2 + x3 <= 100.0) - self.assertEqual(100, c0.upper_bound) - c1 = model.add(10 * x1 + 4.0 * x2 + 5.0 * x3 <= 600.0, "c1") - self.assertEqual("c1", c1.name) - c2 = model.add(2.0 * x1 + 2.0 * x2 + 6.0 * x3 <= 300.0) - self.assertEqual(-math.inf, c2.lower_bound) - - solver = mb.Solver(solver_name) - if not solver.solver_is_supported(): - print(f"Solver {solver_name} is not supported") - return - self.assertTrue(pd.isna(solver.value(x1))) - self.assertTrue(pd.isna(solver.value(x2))) - self.assertTrue(pd.isna(solver.value(x3))) - self.assertTrue(pd.isna(solver.reduced_cost(x1))) - self.assertTrue(pd.isna(solver.reduced_cost(x2))) - self.assertTrue(pd.isna(solver.dual_value(c0))) - self.assertTrue(pd.isna(solver.dual_value(c1))) - self.assertTrue(pd.isna(solver.dual_value(c2))) - self.assertTrue(pd.isna(solver.activity(c0))) - self.assertTrue(pd.isna(solver.activity(c1))) - self.assertTrue(pd.isna(solver.activity(c2))) - self.assertTrue(pd.isna(solver.objective_value)) - self.assertTrue(pd.isna(solver.best_objective_bound)) - self.assertEqual(mb.SolveStatus.OPTIMAL, solver.solve(model)) - - # The problem has an optimal solution. - self.assertAlmostEqual( - 733.333333 + model.objective_offset, - solver.objective_value, - places=self.NUM_PLACES, - ) - self.assertAlmostEqual( - solver.value(10.0 * x1 + 6 * x2 + 4.0 * x3 - 5.5), - solver.objective_value, - places=self.NUM_PLACES, - ) - self.assertAlmostEqual(33.333333, solver.value(x1), places=self.NUM_PLACES) - self.assertAlmostEqual(66.666667, solver.value(x2), places=self.NUM_PLACES) - self.assertAlmostEqual(0.0, solver.value(x3), places=self.NUM_PLACES) - - dual_objective_value = ( - solver.dual_value(c0) * c0.upper_bound - + solver.dual_value(c1) * c1.upper_bound - + solver.dual_value(c2) * c2.upper_bound - + model.objective_offset - ) - self.assertAlmostEqual( - solver.objective_value, dual_objective_value, places=self.NUM_PLACES - ) + # Number of decimal places to use for numerical tolerance for + # checking primal, dual, objective values and other values. + NUM_PLACES = 5 + + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() + + # pylint: disable=too-many-statements + def run_minimal_linear_example(self, solver_name): + """Minimal Linear Example.""" + model = mb.Model() + model.name = "minimal_linear_example" + x1 = model.new_num_var(0.0, math.inf, "x1") + x2 = model.new_num_var(0.0, math.inf, "x2") + x3 = model.new_num_var(0.0, math.inf, "x3") + self.assertEqual(3, model.num_variables) + self.assertFalse(x1.is_integral) + self.assertEqual(0.0, x1.lower_bound) + self.assertEqual(math.inf, x2.upper_bound) + x1.lower_bound = 1.0 + self.assertEqual(1.0, x1.lower_bound) + + model.maximize(10.0 * x1 + 6 * x2 + 4.0 * x3 - 3.5) + self.assertEqual(4.0, x3.objective_coefficient) + self.assertEqual(-3.5, model.objective_offset) + model.objective_offset = -5.5 + self.assertEqual(-5.5, model.objective_offset) + + c0 = model.add(x1 + x2 + x3 <= 100.0) + self.assertEqual(100, c0.upper_bound) + c1 = model.add(10 * x1 + 4.0 * x2 + 5.0 * x3 <= 600.0, "c1") + self.assertEqual("c1", c1.name) + c2 = model.add(2.0 * x1 + 2.0 * x2 + 6.0 * x3 <= 300.0) + self.assertEqual(-math.inf, c2.lower_bound) + + solver = mb.Solver(solver_name) + if not solver.solver_is_supported(): + print(f"Solver {solver_name} is not supported") + return + self.assertTrue(pd.isna(solver.value(x1))) + self.assertTrue(pd.isna(solver.value(x2))) + self.assertTrue(pd.isna(solver.value(x3))) + self.assertTrue(pd.isna(solver.reduced_cost(x1))) + self.assertTrue(pd.isna(solver.reduced_cost(x2))) + self.assertTrue(pd.isna(solver.dual_value(c0))) + self.assertTrue(pd.isna(solver.dual_value(c1))) + self.assertTrue(pd.isna(solver.dual_value(c2))) + self.assertTrue(pd.isna(solver.activity(c0))) + self.assertTrue(pd.isna(solver.activity(c1))) + self.assertTrue(pd.isna(solver.activity(c2))) + self.assertTrue(pd.isna(solver.objective_value)) + self.assertTrue(pd.isna(solver.best_objective_bound)) + self.assertEqual(mb.SolveStatus.OPTIMAL, solver.solve(model)) + + # The problem has an optimal solution. + self.assertAlmostEqual( + 733.333333 + model.objective_offset, + solver.objective_value, + places=self.NUM_PLACES, + ) + self.assertAlmostEqual( + solver.value(10.0 * x1 + 6 * x2 + 4.0 * x3 - 5.5), + solver.objective_value, + places=self.NUM_PLACES, + ) + self.assertAlmostEqual(33.333333, solver.value(x1), places=self.NUM_PLACES) + self.assertAlmostEqual(66.666667, solver.value(x2), places=self.NUM_PLACES) + self.assertAlmostEqual(0.0, solver.value(x3), places=self.NUM_PLACES) + + dual_objective_value = ( + solver.dual_value(c0) * c0.upper_bound + + solver.dual_value(c1) * c1.upper_bound + + solver.dual_value(c2) * c2.upper_bound + + model.objective_offset + ) + self.assertAlmostEqual( + solver.objective_value, dual_objective_value, places=self.NUM_PLACES + ) - # x1 and x2 are basic - self.assertAlmostEqual(0.0, solver.reduced_cost(x1), places=self.NUM_PLACES) - self.assertAlmostEqual(0.0, solver.reduced_cost(x2), places=self.NUM_PLACES) - # x3 is non-basic - x3_expected_reduced_cost = ( - 4.0 - 1.0 * solver.dual_value(c0) - 5.0 * solver.dual_value(c1) - ) - self.assertAlmostEqual( - x3_expected_reduced_cost, - solver.reduced_cost(x3), - places=self.NUM_PLACES, - ) + # x1 and x2 are basic + self.assertAlmostEqual(0.0, solver.reduced_cost(x1), places=self.NUM_PLACES) + self.assertAlmostEqual(0.0, solver.reduced_cost(x2), places=self.NUM_PLACES) + # x3 is non-basic + x3_expected_reduced_cost = ( + 4.0 - 1.0 * solver.dual_value(c0) - 5.0 * solver.dual_value(c1) + ) + self.assertAlmostEqual( + x3_expected_reduced_cost, + solver.reduced_cost(x3), + places=self.NUM_PLACES, + ) - self.assertAlmostEqual(100.0, solver.activity(c0), places=self.NUM_PLACES) - self.assertAlmostEqual(600.0, solver.activity(c1), places=self.NUM_PLACES) - self.assertAlmostEqual(200.0, solver.activity(c2), places=self.NUM_PLACES) + self.assertAlmostEqual(100.0, solver.activity(c0), places=self.NUM_PLACES) + self.assertAlmostEqual(600.0, solver.activity(c1), places=self.NUM_PLACES) + self.assertAlmostEqual(200.0, solver.activity(c2), places=self.NUM_PLACES) - self.assertIn("minimal_linear_example", model.export_to_lp_string(False)) - self.assertIn("minimal_linear_example", model.export_to_mps_string(False)) + self.assertIn("minimal_linear_example", model.export_to_lp_string(False)) + self.assertIn("minimal_linear_example", model.export_to_mps_string(False)) - def test_minimal_linear_example(self): - self.run_minimal_linear_example("glop") + def test_minimal_linear_example(self): + self.run_minimal_linear_example("glop") - def test_import_from_mps_string(self): - mps_data = """ + def test_import_from_mps_string(self): + mps_data = """ * Generated by MPModelProtoExporter * Name : SupportedMaximizationProblem * Format : Free @@ -165,19 +165,19 @@ def test_import_from_mps_string(self): UP BOUND X_ONE 4 ENDATA """ - model = mb.Model() - self.assertTrue(model.import_from_mps_string(mps_data)) - self.assertEqual(model.name, "SupportedMaximizationProblem") - - def test_import_from_mps_file(self): - path = os.path.dirname(__file__) - mps_path = f"{path}/../testdata/maximization.mps" - model = mb.Model() - self.assertTrue(model.import_from_mps_file(mps_path)) - self.assertEqual(model.name, "SupportedMaximizationProblem") - - def test_import_from_lp_string(self): - lp_data = """ + model = mb.Model() + self.assertTrue(model.import_from_mps_string(mps_data)) + self.assertEqual(model.name, "SupportedMaximizationProblem") + + def test_import_from_mps_file(self): + path = os.path.dirname(__file__) + mps_path = f"{path}/../testdata/maximization.mps" + model = mb.Model() + self.assertTrue(model.import_from_mps_file(mps_path)) + self.assertEqual(model.name, "SupportedMaximizationProblem") + + def test_import_from_lp_string(self): + lp_data = """ min: x + y; bin: b1, b2, b3; 1 <= x <= 42; @@ -185,1502 +185,1524 @@ def test_import_from_lp_string(self): 4 y + b2 - 3 b3 <= 2; constraint_num2: -4 b1 + b2 - 3 z <= -2; """ - model = mb.Model() - self.assertTrue(model.import_from_lp_string(lp_data)) - self.assertEqual(6, model.num_variables) - self.assertEqual(3, model.num_constraints) - self.assertEqual(1, model.var_from_index(0).lower_bound) - self.assertEqual(42, model.var_from_index(0).upper_bound) - self.assertEqual("x", model.var_from_index(0).name) - - def test_import_from_lp_file(self): - path = os.path.dirname(__file__) - lp_path = f"{path}/../testdata/small_model.lp" - model = mb.Model() - self.assertTrue(model.import_from_lp_file(lp_path)) - self.assertEqual(6, model.num_variables) - self.assertEqual(3, model.num_constraints) - self.assertEqual(1, model.var_from_index(0).lower_bound) - self.assertEqual(42, model.var_from_index(0).upper_bound) - self.assertEqual("x", model.var_from_index(0).name) - - def test_class_api(self): - model = mb.Model() - x = model.new_int_var(0, 10, "x") - y = model.new_int_var(1, 10, "y") - z = model.new_int_var(2, 10, "z") - t = model.new_int_var(3, 10, "t") - - e1 = mb.LinearExpr.sum([x, y, z]) - flat_e1 = mbh.FlatExpr(e1) - expected_vars = np.array([0, 1, 2], dtype=np.int32) - np_testing.assert_array_equal(expected_vars, flat_e1.variable_indices()) - np_testing.assert_array_equal( - np.array([1, 1, 1], dtype=np.double), flat_e1.coeffs - ) - self.assertEqual(flat_e1.offset, 0.0) - self.assertEqual(e1.__str__(), "(x + y + z)") - - e2 = mb.LinearExpr.sum([e1, 4.0]) - flat_e2 = mbh.FlatExpr(e2) - np_testing.assert_array_equal(expected_vars, flat_e2.variable_indices()) - np_testing.assert_array_equal( - np.array([1, 1, 1], dtype=np.double), flat_e2.coeffs - ) - self.assertEqual(flat_e2.offset, 4.0) - self.assertEqual(e2.__str__(), "((x + y + z) + 4)") - self.assertEqual(flat_e2.__str__(), "x + y + z + 4") - - e3 = mb.LinearExpr.term(e2, 2) - flat_e3 = mbh.FlatExpr(e3) - np_testing.assert_array_equal(expected_vars, flat_e3.variable_indices()) - np_testing.assert_array_equal( - np.array([2, 2, 2], dtype=np.double), flat_e3.coeffs - ) - self.assertEqual(flat_e3.offset, 8.0) - self.assertEqual(e3.__str__(), "(2 * ((x + y + z) + 4))") - self.assertEqual(flat_e3.__str__(), "2 * x + 2 * y + 2 * z + 8") - - e4 = mb.LinearExpr.weighted_sum([x, t], [-1, 1], constant=2) - flat_e4 = mbh.FlatExpr(e4) - np_testing.assert_array_equal( - np.array([0, 3], dtype=np.int32), flat_e4.variable_indices() - ) - np_testing.assert_array_equal( - np.array([-1, 1], dtype=np.double), flat_e4.coeffs - ) - self.assertEqual(flat_e4.offset, 2.0) - self.assertEqual(e4.__str__(), "(-x + t + 2)") + model = mb.Model() + self.assertTrue(model.import_from_lp_string(lp_data)) + self.assertEqual(6, model.num_variables) + self.assertEqual(3, model.num_constraints) + self.assertEqual(1, model.var_from_index(0).lower_bound) + self.assertEqual(42, model.var_from_index(0).upper_bound) + self.assertEqual("x", model.var_from_index(0).name) + + def test_import_from_lp_file(self): + path = os.path.dirname(__file__) + lp_path = f"{path}/../testdata/small_model.lp" + model = mb.Model() + self.assertTrue(model.import_from_lp_file(lp_path)) + self.assertEqual(6, model.num_variables) + self.assertEqual(3, model.num_constraints) + self.assertEqual(1, model.var_from_index(0).lower_bound) + self.assertEqual(42, model.var_from_index(0).upper_bound) + self.assertEqual("x", model.var_from_index(0).name) + + def test_class_api(self): + model = mb.Model() + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(1, 10, "y") + z = model.new_int_var(2, 10, "z") + t = model.new_int_var(3, 10, "t") + + e1 = mb.LinearExpr.sum([x, y, z]) + flat_e1 = mbh.FlatExpr(e1) + expected_vars = np.array([0, 1, 2], dtype=np.int32) + np_testing.assert_array_equal(expected_vars, flat_e1.variable_indices()) + np_testing.assert_array_equal( + np.array([1, 1, 1], dtype=np.double), flat_e1.coeffs + ) + self.assertEqual(flat_e1.offset, 0.0) + self.assertEqual(e1.__str__(), "(x + y + z)") + + e2 = mb.LinearExpr.sum([e1, 4.0]) + flat_e2 = mbh.FlatExpr(e2) + np_testing.assert_array_equal(expected_vars, flat_e2.variable_indices()) + np_testing.assert_array_equal( + np.array([1, 1, 1], dtype=np.double), flat_e2.coeffs + ) + self.assertEqual(flat_e2.offset, 4.0) + self.assertEqual(e2.__str__(), "((x + y + z) + 4)") + self.assertEqual(flat_e2.__str__(), "x + y + z + 4") + + e3 = mb.LinearExpr.term(e2, 2) + flat_e3 = mbh.FlatExpr(e3) + np_testing.assert_array_equal(expected_vars, flat_e3.variable_indices()) + np_testing.assert_array_equal( + np.array([2, 2, 2], dtype=np.double), flat_e3.coeffs + ) + self.assertEqual(flat_e3.offset, 8.0) + self.assertEqual(e3.__str__(), "(2 * ((x + y + z) + 4))") + self.assertEqual(flat_e3.__str__(), "2 * x + 2 * y + 2 * z + 8") + + e4 = mb.LinearExpr.weighted_sum([x, t], [-1, 1], constant=2) + flat_e4 = mbh.FlatExpr(e4) + np_testing.assert_array_equal( + np.array([0, 3], dtype=np.int32), flat_e4.variable_indices() + ) + np_testing.assert_array_equal( + np.array([-1, 1], dtype=np.double), flat_e4.coeffs + ) + self.assertEqual(flat_e4.offset, 2.0) + self.assertEqual(e4.__str__(), "(-x + t + 2)") - e4b = mb.LinearExpr.weighted_sum([e4 * 3], [1]) - flat_e4b = mbh.FlatExpr(e4b) - np_testing.assert_array_equal( - np.array([0, 3], dtype=np.int32), flat_e4b.variable_indices() - ) - np_testing.assert_array_equal( - np.array([-3, 3], dtype=np.double), flat_e4b.coeffs - ) - self.assertEqual(flat_e4b.offset, 6.0) - self.assertEqual(e4b.__str__(), "(3 * (-x + t + 2))") + e4b = mb.LinearExpr.weighted_sum([e4 * 3], [1]) + flat_e4b = mbh.FlatExpr(e4b) + np_testing.assert_array_equal( + np.array([0, 3], dtype=np.int32), flat_e4b.variable_indices() + ) + np_testing.assert_array_equal( + np.array([-3, 3], dtype=np.double), flat_e4b.coeffs + ) + self.assertEqual(flat_e4b.offset, 6.0) + self.assertEqual(e4b.__str__(), "(3 * (-x + t + 2))") - e5 = mb.LinearExpr.sum([e1, -3, e4]) - flat_e5 = mbh.FlatExpr(e5) - np_testing.assert_array_equal( - np.array([1, 2, 3], dtype=np.int32), flat_e5.variable_indices() - ) - np_testing.assert_array_equal( - np.array([1, 1, 1], dtype=np.double), flat_e5.coeffs - ) - self.assertEqual(flat_e5.offset, -1.0) - self.assertEqual(flat_e5.__str__(), "y + z + t - 1") + e5 = mb.LinearExpr.sum([e1, -3, e4]) + flat_e5 = mbh.FlatExpr(e5) + np_testing.assert_array_equal( + np.array([1, 2, 3], dtype=np.int32), flat_e5.variable_indices() + ) + np_testing.assert_array_equal( + np.array([1, 1, 1], dtype=np.double), flat_e5.coeffs + ) + self.assertEqual(flat_e5.offset, -1.0) + self.assertEqual(flat_e5.__str__(), "y + z + t - 1") - e6 = mb.LinearExpr.term(x, 2.0, constant=1.0) - flat_e6 = mbh.FlatExpr(e6) - np_testing.assert_array_equal( - np.array([0], dtype=np.int32), flat_e6.variable_indices() - ) - np_testing.assert_array_equal(np.array([2], dtype=np.double), flat_e6.coeffs) - self.assertEqual(flat_e6.offset, 1.0) + e6 = mb.LinearExpr.term(x, 2.0, constant=1.0) + flat_e6 = mbh.FlatExpr(e6) + np_testing.assert_array_equal( + np.array([0], dtype=np.int32), flat_e6.variable_indices() + ) + np_testing.assert_array_equal( + np.array([2], dtype=np.double), flat_e6.coeffs + ) + self.assertEqual(flat_e6.offset, 1.0) - e7 = mb.LinearExpr.term(x, 1.0, constant=0.0) - self.assertEqual(x, e7) + e7 = mb.LinearExpr.term(x, 1.0, constant=0.0) + self.assertEqual(x, e7) - e8 = mb.LinearExpr.term(2, 3, constant=4) - self.assertEqual(e8, 10) + e8 = mb.LinearExpr.term(2, 3, constant=4) + self.assertEqual(e8, 10) - e9 = mb.LinearExpr.term(x * 2 + 3, 1, constant=0) - e10 = mb.LinearExpr.term(x, 2, constant=3) - self.assertEqual( - str(mbh.FlatExpr(e9)), - str(mbh.FlatExpr(e10)), - ) + e9 = mb.LinearExpr.term(x * 2 + 3, 1, constant=0) + e10 = mb.LinearExpr.term(x, 2, constant=3) + self.assertEqual( + str(mbh.FlatExpr(e9)), + str(mbh.FlatExpr(e10)), + ) - e10 = mb.LinearExpr.sum() - self.assertEqual(str(e10), "0") + e10 = mb.LinearExpr.sum() + self.assertEqual(str(e10), "0") - e11 = mb.LinearExpr.sum(x) - self.assertIsInstance(e11, mb.Variable) - self.assertEqual(x.index, e11.index) + e11 = mb.LinearExpr.sum(x) + self.assertIsInstance(e11, mb.Variable) + self.assertEqual(x.index, e11.index) - e12 = mb.LinearExpr.sum(-1.0, x, 1.0) - self.assertIsInstance(e12, mb.Variable) - self.assertEqual(x.index, e12.index) + e12 = mb.LinearExpr.sum(-1.0, x, 1.0) + self.assertIsInstance(e12, mb.Variable) + self.assertEqual(x.index, e12.index) - e13 = mb.LinearExpr.sum(-1.0, x, constant=1.0) - self.assertIsInstance(e13, mb.Variable) - self.assertEqual(x.index, e13.index) + e13 = mb.LinearExpr.sum(-1.0, x, constant=1.0) + self.assertIsInstance(e13, mb.Variable) + self.assertEqual(x.index, e13.index) - e14 = mb.LinearExpr.weighted_sum([x, t, 1.2], [1, -1, -1.0], constant=2) - flat_e14 = mbh.FlatExpr(e14) - np_testing.assert_array_equal( - np.array([0, 3], dtype=np.int32), flat_e14.variable_indices() - ) - np_testing.assert_array_equal( - np.array([1, -1], dtype=np.double), flat_e14.coeffs - ) - self.assertEqual(flat_e14.offset, 0.8) - self.assertEqual(e14.__str__(), "(x - t + 0.8)") - - e15 = mb.LinearExpr.weighted_sum([1, x, 1], [1, 1, -1]) - self.assertIsInstance(e15, mb.Variable) - self.assertEqual(x.index, e15.index) - - e16 = mb.LinearExpr.affine(x, 1.0, 0.0) - self.assertIsInstance(e16, mb.Variable) - self.assertEqual(x.index, e16.index) - - e17 = -x - self.assertIsInstance(e17, mb.AffineExpr) - self.assertEqual(str(e17), "(-x)") - - e18 = mb.LinearExpr.affine(x, 1.0, -2.0) - self.assertIsInstance(e18, mb.AffineExpr) - self.assertEqual(str(e18), "(x - 2)") - - e19 = mb.LinearExpr.weighted_sum([1, x, 1], [1, 1, -2]) - self.assertIsInstance(e19, mb.AffineExpr) - self.assertEqual(str(e19), "(x - 1)") - - e20 = mb.LinearExpr.affine(x, -2.0, 0.0) - self.assertIsInstance(e20, mb.AffineExpr) - self.assertEqual(str(e20), "(-2 * x)") - - e21 = mb.LinearExpr.weighted_sum([1, x, 1], [1, 2, -1]) - self.assertIsInstance(e21, mb.AffineExpr) - self.assertEqual(str(e21), "(2 * x)") - - c1 = x == 2 - self.assertEqual(str(c1), "x == 2") - - c2 = -x == 3 - self.assertEqual(str(c2), "-x == 3") - - c3 = x + y == 3 - self.assertEqual(str(c3), "(x + y) == 3") - - c4 = -x + y == 3 - self.assertEqual(str(c4), "(-x + y) == 3") - - c5 = x - y == 3 - self.assertEqual(str(c5), "(x - y) == 3") - - def test_variables(self): - model = mb.Model() - x = model.new_int_var(0.0, 4.0, "x") - self.assertEqual(0, x.index) - self.assertEqual(0.0, x.lower_bound) - self.assertEqual(4.0, x.upper_bound) - self.assertEqual("x", x.name) - x.lower_bound = 1.0 - x.upper_bound = 3.0 - self.assertEqual(1.0, x.lower_bound) - self.assertEqual(3.0, x.upper_bound) - self.assertTrue(x.is_integral) - n1 = model.new_int_var(0, 4) - self.assertEqual(n1.name, "variable#1") - n2 = model.new_int_var(0, 4, None) - self.assertEqual(n2.name, "variable#2") - - # Tests the equality operator. - y = model.new_int_var(0.0, 4.0, "y") - x_copy = model.var_from_index(0) - self.assertEqual(x, x) - self.assertEqual(x, x_copy) - self.assertNotEqual(x, y) - - # Tests the hash method. - var_set = set() - var_set.add(x) - self.assertIn(x, var_set) - self.assertIn(x_copy, var_set) - self.assertNotIn(y, var_set) - - def test_duplicate_variables(self): - model = mb.Model() - x = model.new_int_var(0.0, 4.0, "x") - y = model.new_int_var(0.0, 4.0, "y") - z = model.new_int_var(0.0, 4.0, "z") - model.add(x + 2 * y == x - z) - model.minimize(x + y + z) - solver = mb.Solver("sat") - self.assertEqual(mb.SolveStatus.OPTIMAL, solver.solve(model)) - - def test_add_term(self): - model = mb.Model() - x = model.new_int_var(0.0, 4.0, "x") - y = model.new_int_var(0.0, 4.0, "y") - z = model.new_int_var(0.0, 4.0, "z") - t = model.new_int_var(0.0, 4.0, "t") - ct = model.add(x + 2 * y == 3) - self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1]) - self.assertEqual(ct.helper.constraint_coefficients(ct.index), [1, 2]) - ct.add_term(x, 2) - self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1]) - self.assertEqual(ct.helper.constraint_coefficients(ct.index), [3, 2]) - ct.set_coefficient(x, 5) - self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1]) - self.assertEqual(ct.helper.constraint_coefficients(ct.index), [5, 2]) - ct.add_term(z, 4) - self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1, 2]) - self.assertEqual(ct.helper.constraint_coefficients(ct.index), [5, 2, 4]) - ct.set_coefficient(t, -1) - self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1, 2, 3]) - self.assertEqual(ct.helper.constraint_coefficients(ct.index), [5, 2, 4, -1]) - - def test_issue_3614(self): - total_number_of_choices = 5 + 1 - total_unique_products = 3 - standalone_features = list(range(5)) - feature_bundle_incidence_matrix = {} - for idx in range(len(standalone_features)): - feature_bundle_incidence_matrix[idx, 0] = 0 - feature_bundle_incidence_matrix[0, 0] = 1 - feature_bundle_incidence_matrix[1, 0] = 1 - - bundle_start_idx = len(standalone_features) - # Model - model = mb.Model() - y = {} - v = {} - for i in range(total_number_of_choices): - y[i] = model.new_bool_var(f"y_{i}") - - for j in range(total_unique_products): - for i in range(len(standalone_features)): - v[i, j] = model.new_bool_var(f"v_{(i,j)}") - model.add( - v[i, j] - == ( - y[i] - + ( - feature_bundle_incidence_matrix[(i, 0)] - * y[bundle_start_idx] - ) - ) + e14 = mb.LinearExpr.weighted_sum([x, t, 1.2], [1, -1, -1.0], constant=2) + flat_e14 = mbh.FlatExpr(e14) + np_testing.assert_array_equal( + np.array([0, 3], dtype=np.int32), flat_e14.variable_indices() + ) + np_testing.assert_array_equal( + np.array([1, -1], dtype=np.double), flat_e14.coeffs + ) + self.assertEqual(flat_e14.offset, 0.8) + self.assertEqual(e14.__str__(), "(x - t + 0.8)") + + e15 = mb.LinearExpr.weighted_sum([1, x, 1], [1, 1, -1]) + self.assertIsInstance(e15, mb.Variable) + self.assertEqual(x.index, e15.index) + + e16 = mb.LinearExpr.affine(x, 1.0, 0.0) + self.assertIsInstance(e16, mb.Variable) + self.assertEqual(x.index, e16.index) + + e17 = -x + self.assertIsInstance(e17, mb.AffineExpr) + self.assertEqual(str(e17), "(-x)") + + e18 = mb.LinearExpr.affine(x, 1.0, -2.0) + self.assertIsInstance(e18, mb.AffineExpr) + self.assertEqual(str(e18), "(x - 2)") + + e19 = mb.LinearExpr.weighted_sum([1, x, 1], [1, 1, -2]) + self.assertIsInstance(e19, mb.AffineExpr) + self.assertEqual(str(e19), "(x - 1)") + + e20 = mb.LinearExpr.affine(x, -2.0, 0.0) + self.assertIsInstance(e20, mb.AffineExpr) + self.assertEqual(str(e20), "(-2 * x)") + + e21 = mb.LinearExpr.weighted_sum([1, x, 1], [1, 2, -1]) + self.assertIsInstance(e21, mb.AffineExpr) + self.assertEqual(str(e21), "(2 * x)") + + c1 = x == 2 + self.assertEqual(str(c1), "x == 2") + + c2 = -x == 3 + self.assertEqual(str(c2), "-x == 3") + + c3 = x + y == 3 + self.assertEqual(str(c3), "(x + y) == 3") + + c4 = -x + y == 3 + self.assertEqual(str(c4), "(-x + y) == 3") + + c5 = x - y == 3 + self.assertEqual(str(c5), "(x - y) == 3") + + def test_large_iadd(self): + model = mb.Model() + s = 0 + for _ in range(300000): + s += model.new_bool_var("") + model.add(s == 10) + + def test_large_isub(self): + model = mb.Model() + s = 0 + for _ in range(300000): + s -= model.new_bool_var("") + model.add(s == 10) + + def test_variables(self): + model = mb.Model() + x = model.new_int_var(0.0, 4.0, "x") + self.assertEqual(0, x.index) + self.assertEqual(0.0, x.lower_bound) + self.assertEqual(4.0, x.upper_bound) + self.assertEqual("x", x.name) + x.lower_bound = 1.0 + x.upper_bound = 3.0 + self.assertEqual(1.0, x.lower_bound) + self.assertEqual(3.0, x.upper_bound) + self.assertTrue(x.is_integral) + n1 = model.new_int_var(0, 4) + self.assertEqual(n1.name, "variable#1") + n2 = model.new_int_var(0, 4, None) + self.assertEqual(n2.name, "variable#2") + + # Tests the equality operator. + y = model.new_int_var(0.0, 4.0, "y") + x_copy = model.var_from_index(0) + self.assertEqual(x, x) + self.assertEqual(x, x_copy) + self.assertNotEqual(x, y) + + # Tests the hash method. + var_set = set() + var_set.add(x) + self.assertIn(x, var_set) + self.assertIn(x_copy, var_set) + self.assertNotIn(y, var_set) + + def test_duplicate_variables(self): + model = mb.Model() + x = model.new_int_var(0.0, 4.0, "x") + y = model.new_int_var(0.0, 4.0, "y") + z = model.new_int_var(0.0, 4.0, "z") + model.add(x + 2 * y == x - z) + model.minimize(x + y + z) + solver = mb.Solver("sat") + self.assertEqual(mb.SolveStatus.OPTIMAL, solver.solve(model)) + + def test_add_term(self): + model = mb.Model() + x = model.new_int_var(0.0, 4.0, "x") + y = model.new_int_var(0.0, 4.0, "y") + z = model.new_int_var(0.0, 4.0, "z") + t = model.new_int_var(0.0, 4.0, "t") + ct = model.add(x + 2 * y == 3) + self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1]) + self.assertEqual(ct.helper.constraint_coefficients(ct.index), [1, 2]) + ct.add_term(x, 2) + self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1]) + self.assertEqual(ct.helper.constraint_coefficients(ct.index), [3, 2]) + ct.set_coefficient(x, 5) + self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1]) + self.assertEqual(ct.helper.constraint_coefficients(ct.index), [5, 2]) + ct.add_term(z, 4) + self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1, 2]) + self.assertEqual(ct.helper.constraint_coefficients(ct.index), [5, 2, 4]) + ct.set_coefficient(t, -1) + self.assertEqual(ct.helper.constraint_var_indices(ct.index), [0, 1, 2, 3]) + self.assertEqual(ct.helper.constraint_coefficients(ct.index), [5, 2, 4, -1]) + + def test_issue_3614(self): + total_number_of_choices = 5 + 1 + total_unique_products = 3 + standalone_features = list(range(5)) + feature_bundle_incidence_matrix = {} + for idx in range(len(standalone_features)): + feature_bundle_incidence_matrix[idx, 0] = 0 + feature_bundle_incidence_matrix[0, 0] = 1 + feature_bundle_incidence_matrix[1, 0] = 1 + + bundle_start_idx = len(standalone_features) + # Model + model = mb.Model() + y = {} + v = {} + for i in range(total_number_of_choices): + y[i] = model.new_bool_var(f"y_{i}") + + for j in range(total_unique_products): + for i in range(len(standalone_features)): + v[i, j] = model.new_bool_var(f"v_{(i,j)}") + model.add( + v[i, j] + == ( + y[i] + + ( + feature_bundle_incidence_matrix[(i, 0)] + * y[bundle_start_idx] ) + ) + ) - solver = mb.Solver("sat") - status = solver.solve(model) - self.assertEqual(mb.SolveStatus.OPTIMAL, status) + solver = mb.Solver("sat") + status = solver.solve(model) + self.assertEqual(mb.SolveStatus.OPTIMAL, status) - def test_create_false_ct(self): - # Create the model. - model = mb.Model() - x = model.new_num_var(0.0, math.inf, "x") + def test_create_false_ct(self): + # Create the model. + model = mb.Model() + x = model.new_num_var(0.0, math.inf, "x") - ct = model.add(False) - self.assertTrue(ct.is_under_specified) - self.assertRaises(ValueError, ct.add_term, x, 1) + ct = model.add(False) + self.assertTrue(ct.is_under_specified) + self.assertRaises(ValueError, ct.add_term, x, 1) - model.maximize(x) + model.maximize(x) - solver = mb.Solver("glop") - status = solver.solve(model) - self.assertEqual(status, mb.SolveStatus.INFEASIBLE) + solver = mb.Solver("glop") + status = solver.solve(model) + self.assertEqual(status, mb.SolveStatus.INFEASIBLE) - def test_create_true_ct(self): - # Create the model. - model = mb.Model() - x = model.new_num_var(0.0, 5.0, "x") + def test_create_true_ct(self): + # Create the model. + model = mb.Model() + x = model.new_num_var(0.0, 5.0, "x") - ct = model.add(True) - self.assertEqual(ct.lower_bound, 0.0) - self.assertEqual(ct.upper_bound, 0.0) - self.assertTrue(ct.is_under_specified) - self.assertRaises(ValueError, ct.add_term, x, 1) + ct = model.add(True) + self.assertEqual(ct.lower_bound, 0.0) + self.assertEqual(ct.upper_bound, 0.0) + self.assertTrue(ct.is_under_specified) + self.assertRaises(ValueError, ct.add_term, x, 1) - model.maximize(x) + model.maximize(x) - solver = mb.Solver("glop") - status = solver.solve(model) + solver = mb.Solver("glop") + status = solver.solve(model) - self.assertEqual(status, mb.SolveStatus.OPTIMAL) + self.assertEqual(status, mb.SolveStatus.OPTIMAL) class InternalHelperTest(absltest.TestCase): - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() - def test_anonymous_variables(self): - helper = mb.Model().helper - index = helper.add_var() - variable = mb.Variable(helper, index) - self.assertEqual(variable.name, f"variable#{index}") + def test_anonymous_variables(self): + helper = mb.Model().helper + index = helper.add_var() + variable = mb.Variable(helper, index) + self.assertEqual(variable.name, f"variable#{index}") - def test_anonymous_constraints(self): - helper = mb.Model().helper - index = helper.add_linear_constraint() - constraint = mb.LinearConstraint(helper, index=index) - self.assertEqual(constraint.name, f"linear_constraint#{index}") + def test_anonymous_constraints(self): + helper = mb.Model().helper + index = helper.add_linear_constraint() + constraint = mb.LinearConstraint(helper, index=index) + self.assertEqual(constraint.name, f"linear_constraint#{index}") class LinearBaseTest(parameterized.TestCase): - def setUp(self): - super().setUp() - simple_model = mb.Model() - self.x = simple_model.new_var_series( - name="x", index=pd.Index(range(3), name="i") - ) - self.y = simple_model.new_var_series( - name="y", index=pd.Index(range(5), name="i") - ) - self.simple_model = simple_model - - @parameterized.named_parameters( - # Variable / Indexing - dict( - testcase_name="x[0]", - expr=lambda x, y: x[0], - expected_str="x[0]", - ), - dict( - testcase_name="x[1]", - expr=lambda x, y: x[1], - expected_str="x[1]", - ), - dict( - testcase_name="x[2]", - expr=lambda x, y: x[2], - expected_str="x[2]", - ), - dict( - testcase_name="y[0]", - expr=lambda x, y: y[0], - expected_str="y[0]", - ), - dict( - testcase_name="y[4]", - expr=lambda x, y: y[4], - expected_str="y[4]", - ), - # Sum - dict( - testcase_name="x[0] + 5", - expr=lambda x, y: x[0] + 5, - expected_str="x[0] + 5", - ), - dict( - testcase_name="x[0] - 5", - expr=lambda x, y: x[0] - 5, - expected_str="x[0] - 5", - ), - dict( - testcase_name="5 - x[0]", - expr=lambda x, y: 5 - x[0], - expected_str="-x[0] + 5", - ), - dict( - testcase_name="5 + x[0]", - expr=lambda x, y: 5 + x[0], - expected_str="x[0] + 5", - ), - dict( - testcase_name="x[0] + y[0]", - expr=lambda x, y: x[0] + y[0], - expected_str="x[0] + y[0]", - ), - dict( - testcase_name="x[0] + y[0] + 5", - expr=lambda x, y: x[0] + y[0] + 5, - expected_str="x[0] + y[0] + 5", - ), - dict( - testcase_name="5 + x[0] + y[0]", - expr=lambda x, y: 5 + x[0] + y[0], - expected_str="x[0] + y[0] + 5", - ), - dict( - testcase_name="5 + x[0] - x[0]", - expr=lambda x, y: 5 + x[0] - x[0], - expected_str="5", - ), - dict( - testcase_name="5 + x[0] - y[0]", - expr=lambda x, y: 5 + x[0] - y[0], - expected_str="x[0] - y[0] + 5", - ), - dict( - testcase_name="x.sum()", - expr=lambda x, y: x.sum(), - expected_str="x[0] + x[1] + x[2]", - ), - dict( - testcase_name="x.add(y, fill_value=0).sum() + 5", - expr=lambda x, y: x.add(y, fill_value=0).sum() + 5, - expected_str="x[0] + x[1] + x[2] + y[0] + y[1] + ... + 5", - ), - dict( - testcase_name="sum(x, y + 5)", - expr=lambda x, y: mb.LinearExpr.sum([x.sum(), y.sum() + 5]), - expected_str="x[0] + x[1] + x[2] + y[0] + y[1] + ... + 5", - ), - # Product - dict( - testcase_name="- x.sum()", - expr=lambda x, y: -x.sum(), - expected_str="-x[0] - x[1] - x[2]", - ), - dict( - testcase_name="5 - x.sum()", - expr=lambda x, y: 5 - x.sum(), - expected_str="-x[0] - x[1] - x[2] + 5", - ), - dict( - testcase_name="x.sum() / 2", - expr=lambda x, y: x.sum() / 2, - expected_str="0.5 * x[0] + 0.5 * x[1] + 0.5 * x[2]", - ), - dict( - testcase_name="(3 * x).sum()", - expr=lambda x, y: (3 * x).sum(), - expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]", - ), - dict( - testcase_name="(x * 3).sum()", - expr=lambda x, y: (x * 3).sum(), - expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]", - ), - dict( - testcase_name="x.sum() * 3", - expr=lambda x, y: x.sum() * 3, - expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]", - ), - dict( - testcase_name="3 * x.sum()", - expr=lambda x, y: 3 * x.sum(), - expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]", - ), - dict( - testcase_name="0 * x.sum() + y.sum()", - expr=lambda x, y: 0 * x.sum() + y.sum(), - expected_str="y[0] + y[1] + y[2] + y[3] + y[4]", - ), - # LinearExpression - dict( - testcase_name="FlatExpr(x.sum())", - expr=lambda x, y: mbh.FlatExpr(x.sum()), - expected_str="x[0] + x[1] + x[2]", - ), - dict( - testcase_name="FlatExpr(FlatExpr(x.sum()))", - # pylint: disable=g-long-lambda - expr=lambda x, y: mbh.FlatExpr(mbh.FlatExpr(x.sum())), - expected_str="x[0] + x[1] + x[2]", - ), - dict( - testcase_name="""FlatExpr(sum([ + def setUp(self): + super().setUp() + simple_model = mb.Model() + self.x = simple_model.new_var_series( + name="x", index=pd.Index(range(3), name="i") + ) + self.y = simple_model.new_var_series( + name="y", index=pd.Index(range(5), name="i") + ) + self.simple_model = simple_model + + @parameterized.named_parameters( + # Variable / Indexing + dict( + testcase_name="x[0]", + expr=lambda x, y: x[0], + expected_str="x[0]", + ), + dict( + testcase_name="x[1]", + expr=lambda x, y: x[1], + expected_str="x[1]", + ), + dict( + testcase_name="x[2]", + expr=lambda x, y: x[2], + expected_str="x[2]", + ), + dict( + testcase_name="y[0]", + expr=lambda x, y: y[0], + expected_str="y[0]", + ), + dict( + testcase_name="y[4]", + expr=lambda x, y: y[4], + expected_str="y[4]", + ), + # Sum + dict( + testcase_name="x[0] + 5", + expr=lambda x, y: x[0] + 5, + expected_str="x[0] + 5", + ), + dict( + testcase_name="x[0] - 5", + expr=lambda x, y: x[0] - 5, + expected_str="x[0] - 5", + ), + dict( + testcase_name="5 - x[0]", + expr=lambda x, y: 5 - x[0], + expected_str="-x[0] + 5", + ), + dict( + testcase_name="5 + x[0]", + expr=lambda x, y: 5 + x[0], + expected_str="x[0] + 5", + ), + dict( + testcase_name="x[0] + y[0]", + expr=lambda x, y: x[0] + y[0], + expected_str="x[0] + y[0]", + ), + dict( + testcase_name="x[0] + y[0] + 5", + expr=lambda x, y: x[0] + y[0] + 5, + expected_str="x[0] + y[0] + 5", + ), + dict( + testcase_name="5 + x[0] + y[0]", + expr=lambda x, y: 5 + x[0] + y[0], + expected_str="x[0] + y[0] + 5", + ), + dict( + testcase_name="5 + x[0] - x[0]", + expr=lambda x, y: 5 + x[0] - x[0], + expected_str="5", + ), + dict( + testcase_name="5 + x[0] - y[0]", + expr=lambda x, y: 5 + x[0] - y[0], + expected_str="x[0] - y[0] + 5", + ), + dict( + testcase_name="x.sum()", + expr=lambda x, y: x.sum(), + expected_str="x[0] + x[1] + x[2]", + ), + dict( + testcase_name="x.add(y, fill_value=0).sum() + 5", + expr=lambda x, y: x.add(y, fill_value=0).sum() + 5, + expected_str="x[0] + x[1] + x[2] + y[0] + y[1] + ... + 5", + ), + dict( + testcase_name="sum(x, y + 5)", + expr=lambda x, y: mb.LinearExpr.sum([x.sum(), y.sum() + 5]), + expected_str="x[0] + x[1] + x[2] + y[0] + y[1] + ... + 5", + ), + # Product + dict( + testcase_name="- x.sum()", + expr=lambda x, y: -x.sum(), + expected_str="-x[0] - x[1] - x[2]", + ), + dict( + testcase_name="5 - x.sum()", + expr=lambda x, y: 5 - x.sum(), + expected_str="-x[0] - x[1] - x[2] + 5", + ), + dict( + testcase_name="x.sum() / 2", + expr=lambda x, y: x.sum() / 2, + expected_str="0.5 * x[0] + 0.5 * x[1] + 0.5 * x[2]", + ), + dict( + testcase_name="(3 * x).sum()", + expr=lambda x, y: (3 * x).sum(), + expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]", + ), + dict( + testcase_name="(x * 3).sum()", + expr=lambda x, y: (x * 3).sum(), + expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]", + ), + dict( + testcase_name="x.sum() * 3", + expr=lambda x, y: x.sum() * 3, + expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]", + ), + dict( + testcase_name="3 * x.sum()", + expr=lambda x, y: 3 * x.sum(), + expected_str="3 * x[0] + 3 * x[1] + 3 * x[2]", + ), + dict( + testcase_name="0 * x.sum() + y.sum()", + expr=lambda x, y: 0 * x.sum() + y.sum(), + expected_str="y[0] + y[1] + y[2] + y[3] + y[4]", + ), + # LinearExpression + dict( + testcase_name="FlatExpr(x.sum())", + expr=lambda x, y: mbh.FlatExpr(x.sum()), + expected_str="x[0] + x[1] + x[2]", + ), + dict( + testcase_name="FlatExpr(FlatExpr(x.sum()))", + # pylint: disable=g-long-lambda + expr=lambda x, y: mbh.FlatExpr(mbh.FlatExpr(x.sum())), + expected_str="x[0] + x[1] + x[2]", + ), + dict( + testcase_name="""FlatExpr(sum([ FlatExpr(x.sum()), FlatExpr(x.sum()), ]))""", - # pylint: disable=g-long-lambda - expr=lambda x, y: mbh.FlatExpr( - sum( - [ - mbh.FlatExpr(x.sum()), - mbh.FlatExpr(x.sum()), - ] - ) - ), - expected_str="2 * x[0] + 2 * x[1] + 2 * x[2]", - ), - ) - def test_str(self, expr, expected_str): - x = self.x - y = self.y - self.assertEqual(str(mbh.FlatExpr(expr(x, y))), expected_str) + # pylint: disable=g-long-lambda + expr=lambda x, y: mbh.FlatExpr( + sum([ + mbh.FlatExpr(x.sum()), + mbh.FlatExpr(x.sum()), + ]) + ), + expected_str="2 * x[0] + 2 * x[1] + 2 * x[2]", + ), + ) + def test_str(self, expr, expected_str): + x = self.x + y = self.y + self.assertEqual(str(mbh.FlatExpr(expr(x, y))), expected_str) class LinearBaseErrorsTest(absltest.TestCase): - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() - def test_unknown_linear_type(self): - with self.assertRaises(TypeError): + def test_unknown_linear_type(self): + with self.assertRaises(TypeError): - class UnknownLinearType(mb.LinearExpr): + class UnknownLinearType(mb.LinearExpr): - def __init__(self): - mb.LinearExpr.__init__(self) + def __init__(self): + mb.LinearExpr.__init__(self) - mbh.FlatExpr(UnknownLinearType()) + mbh.FlatExpr(UnknownLinearType()) - def test_division_by_zero(self): - with self.assertRaises(ZeroDivisionError): - model = mb.Model() - x = model.new_var_series(name="x", index=pd.Index(range(1))) - print(x / 0) + def test_division_by_zero(self): + with self.assertRaises(ZeroDivisionError): + model = mb.Model() + x = model.new_var_series(name="x", index=pd.Index(range(1))) + print(x / 0) - def test_boolean_expression(self): - with self.assertRaisesRegex(NotImplementedError, r"instance as a Boolean"): - model = mb.Model() - x = model.new_var_series(name="x", index=pd.Index(range(1))) - bool(x.sum()) + def test_boolean_expression(self): + with self.assertRaisesRegex(NotImplementedError, r"instance as a Boolean"): + model = mb.Model() + x = model.new_var_series(name="x", index=pd.Index(range(1))) + bool(x.sum()) class BoundedLinearBaseTest(parameterized.TestCase): - def setUp(self): - super().setUp() - simple_model = mb.Model() - self.x = simple_model.new_var_series( - name="x", index=pd.Index(range(3), name="i") - ) - self.y = simple_model.new_var_series( - name="y", index=pd.Index(range(5), name="i") - ) - self.simple_model = simple_model - - @parameterized.product( - lhs=( - lambda x, y: x.sum(), - lambda x, y: -x.sum(), - lambda x, y: x.sum() * 0, - lambda x, y: x.sum() * 3, - lambda x, y: x[0], - lambda x, y: x[1], - lambda x, y: x[2], - lambda x, y: -math.inf, - lambda x, y: -1, - lambda x, y: 0, - lambda x, y: 1, - lambda x, y: 1.1, - lambda x, y: math.inf, - ), - rhs=( - lambda x, y: y.sum(), - lambda x, y: -y.sum(), - lambda x, y: y.sum() * 0, - lambda x, y: y.sum() * 3, - lambda x, y: y[0], - lambda x, y: y[1], - lambda x, y: y[2], - lambda x, y: -math.inf, - lambda x, y: -1, - lambda x, y: 0, - lambda x, y: 1, - lambda x, y: 1.1, - lambda x, y: math.inf, - ), - op=( - lambda lhs, rhs: lhs == rhs, - lambda lhs, rhs: lhs <= rhs, - lambda lhs, rhs: lhs >= rhs, - ), + def setUp(self): + super().setUp() + simple_model = mb.Model() + self.x = simple_model.new_var_series( + name="x", index=pd.Index(range(3), name="i") + ) + self.y = simple_model.new_var_series( + name="y", index=pd.Index(range(5), name="i") + ) + self.simple_model = simple_model + + @parameterized.product( + lhs=( + lambda x, y: x.sum(), + lambda x, y: -x.sum(), + lambda x, y: x.sum() * 0, + lambda x, y: x.sum() * 3, + lambda x, y: x[0], + lambda x, y: x[1], + lambda x, y: x[2], + lambda x, y: -math.inf, + lambda x, y: -1, + lambda x, y: 0, + lambda x, y: 1, + lambda x, y: 1.1, + lambda x, y: math.inf, + ), + rhs=( + lambda x, y: y.sum(), + lambda x, y: -y.sum(), + lambda x, y: y.sum() * 0, + lambda x, y: y.sum() * 3, + lambda x, y: y[0], + lambda x, y: y[1], + lambda x, y: y[2], + lambda x, y: -math.inf, + lambda x, y: -1, + lambda x, y: 0, + lambda x, y: 1, + lambda x, y: 1.1, + lambda x, y: math.inf, + ), + op=( + lambda lhs, rhs: lhs == rhs, + lambda lhs, rhs: lhs <= rhs, + lambda lhs, rhs: lhs >= rhs, + ), + ) + def test_str(self, lhs, rhs, op): + x = self.x + y = self.y + l: mb.LinearExprT = lhs(x, y) + r: mb.LinearExprT = rhs(x, y) + result = op(l, r) + if isinstance(l, mb.LinearExpr) or isinstance(r, mb.LinearExpr): + self.assertIsInstance(result, mbh.BoundedLinearExpression) + self.assertIn("=", str(result), msg="is one of ==, <=, or >=") + else: + self.assertIsInstance(result, bool) + + def test_doublesided_bounded_expressions(self): + x = self.x + self.assertEqual( + "0 <= x[0] <= 1", str(mb.BoundedLinearExpression(x[0], 0, 1)) + ) + + def test_free_bounded_expressions(self): + self.assertEqual( + "-inf <= x[0] <= inf", + str(mb.BoundedLinearExpression(self.x[0], -math.inf, math.inf)), ) - def test_str(self, lhs, rhs, op): - x = self.x - y = self.y - l: mb.LinearExprT = lhs(x, y) - r: mb.LinearExprT = rhs(x, y) - result = op(l, r) - if isinstance(l, mb.LinearExpr) or isinstance(r, mb.LinearExpr): - self.assertIsInstance(result, mbh.BoundedLinearExpression) - self.assertIn("=", str(result), msg="is one of ==, <=, or >=") - else: - self.assertIsInstance(result, bool) - - def test_doublesided_bounded_expressions(self): - x = self.x - self.assertEqual("0 <= x[0] <= 1", str(mb.BoundedLinearExpression(x[0], 0, 1))) - - def test_free_bounded_expressions(self): - self.assertEqual( - "-inf <= x[0] <= inf", - str(mb.BoundedLinearExpression(self.x[0], -math.inf, math.inf)), - ) - def test_var_eq_var_as_bool(self): - x = self.x - y = self.y - self.assertEqual(x[0], x[0]) - self.assertNotEqual(x[0], x[1]) - self.assertNotEqual(x[0], y[0]) + def test_var_eq_var_as_bool(self): + x = self.x + y = self.y + self.assertEqual(x[0], x[0]) + self.assertNotEqual(x[0], x[1]) + self.assertNotEqual(x[0], y[0]) - self.assertEqual(x[1], x[1]) - self.assertNotEqual(x[1], x[0]) - self.assertNotEqual(x[1], y[1]) + self.assertEqual(x[1], x[1]) + self.assertNotEqual(x[1], x[0]) + self.assertNotEqual(x[1], y[1]) - self.assertEqual(y[0], y[0]) - self.assertNotEqual(y[0], y[1]) - self.assertNotEqual(y[0], x[0]) + self.assertEqual(y[0], y[0]) + self.assertNotEqual(y[0], y[1]) + self.assertNotEqual(y[0], x[0]) - self.assertEqual(y[1], y[1]) - self.assertNotEqual(y[1], y[0]) - self.assertNotEqual(y[1], x[1]) + self.assertEqual(y[1], y[1]) + self.assertNotEqual(y[1], y[0]) + self.assertNotEqual(y[1], x[1]) class BoundedLinearBaseErrorsTest(absltest.TestCase): - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() - def test_single_var_bounded_linear_expression_as_bool(self): - with self.assertRaisesRegex( - NotImplementedError, "Evaluating a BoundedLinearExpression" - ): - model = mb.Model() - x = model.new_bool_var(name="x") - bool(mb.BoundedLinearExpression(x, 0, 1)) + def test_single_var_bounded_linear_expression_as_bool(self): + with self.assertRaisesRegex( + NotImplementedError, "Evaluating a BoundedLinearExpression" + ): + model = mb.Model() + x = model.new_bool_var(name="x") + bool(mb.BoundedLinearExpression(x, 0, 1)) - def test_bounded_linear_expression_as_bool(self): - with self.assertRaisesRegex(TypeError, "incompatible constructor arguments"): - model = mb.Model() - x = model.new_var_series(name="x", index=pd.Index(range(1))) - bool(mb.BoundedLinearExpression(x, 0, 1)) + def test_bounded_linear_expression_as_bool(self): + with self.assertRaisesRegex( + TypeError, "incompatible constructor arguments" + ): + model = mb.Model() + x = model.new_var_series(name="x", index=pd.Index(range(1))) + bool(mb.BoundedLinearExpression(x, 0, 1)) class ModelBuilderErrorsTest(absltest.TestCase): - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() - - def test_new_var_series_errors(self): - with self.assertRaisesRegex(TypeError, r"Non-index object"): - model = mb.Model() - model.new_var_series(name="", index=pd.DataFrame()) - with self.assertRaisesRegex(TypeError, r"invalid type"): - model = mb.Model() - model.new_var_series(name="x", index=pd.Index([0]), lower_bounds="0") - with self.assertRaisesRegex(TypeError, r"invalid type"): - model = mb.Model() - model.new_var_series(name="x", index=pd.Index([0]), upper_bounds="0") - with self.assertRaisesRegex(TypeError, r"invalid type"): - model = mb.Model() - model.new_var_series(name="x", index=pd.Index([0]), is_integral="True") - with self.assertRaisesRegex(ValueError, r"not a valid identifier"): - model = mb.Model() - model.new_var_series(name="", index=pd.Index([0])) - with self.assertRaisesRegex(ValueError, r"is greater than"): - model = mb.Model() - model.new_var_series( - name="x", - index=pd.Index([0]), - lower_bounds=0.2, - upper_bounds=0.1, - ) - with self.assertRaisesRegex(ValueError, r"is greater than"): - model = mb.Model() - model.new_var_series( - name="x", - index=pd.Index([0]), - lower_bounds=0.1, - upper_bounds=0.2, - is_integral=True, - ) - with self.assertRaisesRegex(ValueError, r"index does not match"): - model = mb.Model() - model.new_var_series( - name="x", index=pd.Index([0]), lower_bounds=pd.Series([1, 2]) - ) - with self.assertRaisesRegex(ValueError, r"index does not match"): - model = mb.Model() - model.new_var_series( - name="x", index=pd.Index([0]), upper_bounds=pd.Series([1, 2]) - ) - with self.assertRaisesRegex(ValueError, r"index does not match"): - model = mb.Model() - model.new_var_series( - name="x", index=pd.Index([0]), is_integral=pd.Series([False, True]) - ) - - def test_add_linear_constraints_errors(self): - with self.assertRaisesRegex(TypeError, r"Not supported"): - model = mb.Model() - model.add("True", name="c") - with self.assertRaisesRegex(TypeError, r"invalid type="): - model = mb.Model() - model.add(pd.Series(["T"]), name="c") + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() + + def test_new_var_series_errors(self): + with self.assertRaisesRegex(TypeError, r"Non-index object"): + model = mb.Model() + model.new_var_series(name="", index=pd.DataFrame()) + with self.assertRaisesRegex(TypeError, r"invalid type"): + model = mb.Model() + model.new_var_series(name="x", index=pd.Index([0]), lower_bounds="0") + with self.assertRaisesRegex(TypeError, r"invalid type"): + model = mb.Model() + model.new_var_series(name="x", index=pd.Index([0]), upper_bounds="0") + with self.assertRaisesRegex(TypeError, r"invalid type"): + model = mb.Model() + model.new_var_series(name="x", index=pd.Index([0]), is_integral="True") + with self.assertRaisesRegex(ValueError, r"not a valid identifier"): + model = mb.Model() + model.new_var_series(name="", index=pd.Index([0])) + with self.assertRaisesRegex(ValueError, r"is greater than"): + model = mb.Model() + model.new_var_series( + name="x", + index=pd.Index([0]), + lower_bounds=0.2, + upper_bounds=0.1, + ) + with self.assertRaisesRegex(ValueError, r"is greater than"): + model = mb.Model() + model.new_var_series( + name="x", + index=pd.Index([0]), + lower_bounds=0.1, + upper_bounds=0.2, + is_integral=True, + ) + with self.assertRaisesRegex(ValueError, r"index does not match"): + model = mb.Model() + model.new_var_series( + name="x", index=pd.Index([0]), lower_bounds=pd.Series([1, 2]) + ) + with self.assertRaisesRegex(ValueError, r"index does not match"): + model = mb.Model() + model.new_var_series( + name="x", index=pd.Index([0]), upper_bounds=pd.Series([1, 2]) + ) + with self.assertRaisesRegex(ValueError, r"index does not match"): + model = mb.Model() + model.new_var_series( + name="x", index=pd.Index([0]), is_integral=pd.Series([False, True]) + ) + + def test_add_linear_constraints_errors(self): + with self.assertRaisesRegex(TypeError, r"Not supported"): + model = mb.Model() + model.add("True", name="c") + with self.assertRaisesRegex(TypeError, r"invalid type="): + model = mb.Model() + model.add(pd.Series(["T"]), name="c") class ModelBuilderVariablesTest(parameterized.TestCase): - _variable_indices = ( - pd.Index(range(3)), - pd.Index(range(5), name="i"), - pd.MultiIndex.from_product(((1, 2), ("a", "b", "c")), names=["i", "j"]), - pd.MultiIndex.from_product((("a", "b"), (1, 2, 3))), + _variable_indices = ( + pd.Index(range(3)), + pd.Index(range(5), name="i"), + pd.MultiIndex.from_product(((1, 2), ("a", "b", "c")), names=["i", "j"]), + pd.MultiIndex.from_product((("a", "b"), (1, 2, 3))), + ) + _bounds = ( + lambda index: (-math.inf, -10.5), + lambda index: (-math.inf, -1), + lambda index: (-math.inf, 0), + lambda index: (-math.inf, 10), + lambda index: (-math.inf, math.inf), + lambda index: (-10, -1.1), + lambda index: (-10, 0), + lambda index: (-10, -10), + lambda index: (-10, 3), + lambda index: (-9, math.inf), + lambda index: (-1, 1), + lambda index: (0, 0), + lambda index: (0, 1), + lambda index: (0, math.inf), + lambda index: (1, 1), + lambda index: (1, 10.1), + lambda index: (1, math.inf), + lambda index: (100.1, math.inf), + # pylint: disable=g-long-lambda + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(-10.5, index=index), + ), + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(-1, index=index), + ), + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(0, index=index), + ), + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(10, index=index), + ), + lambda index: ( + pd.Series(-math.inf, index=index), + pd.Series(math.inf, index=index), + ), + lambda index: (pd.Series(-10, index=index), pd.Series(-1.1, index=index)), + lambda index: (pd.Series(-10, index=index), pd.Series(0, index=index)), + lambda index: (pd.Series(-10, index=index), pd.Series(-10, index=index)), + lambda index: (pd.Series(-10, index=index), pd.Series(3, index=index)), + lambda index: ( + pd.Series(-9, index=index), + pd.Series(math.inf, index=index), + ), + lambda index: (pd.Series(-1, index=index), pd.Series(1, index=index)), + lambda index: (pd.Series(0, index=index), pd.Series(0, index=index)), + lambda index: (pd.Series(0, index=index), pd.Series(1, index=index)), + lambda index: ( + pd.Series(0, index=index), + pd.Series(math.inf, index=index), + ), + lambda index: (pd.Series(1, index=index), pd.Series(1, index=index)), + lambda index: (pd.Series(1, index=index), pd.Series(10.1, index=index)), + lambda index: ( + pd.Series(1, index=index), + pd.Series(math.inf, index=index), + ), + lambda index: ( + pd.Series(100.1, index=index), + pd.Series(math.inf, index=index), + ), + ) + _is_integer = ( + lambda index: False, + lambda index: True, + lambda index: pd.Series(False, index=index), + lambda index: pd.Series(True, index=index), + ) + + @parameterized.product( + index=_variable_indices, bounds=_bounds, is_integer=_is_integer + ) + def test_new_var_series(self, index, bounds, is_integer): + model = mb.Model() + variables = model.new_var_series( + name="test_variable", + index=index, + lower_bounds=bounds(index)[0], + upper_bounds=bounds(index)[1], + is_integral=is_integer(index), ) - _bounds = ( - lambda index: (-math.inf, -10.5), - lambda index: (-math.inf, -1), - lambda index: (-math.inf, 0), - lambda index: (-math.inf, 10), - lambda index: (-math.inf, math.inf), - lambda index: (-10, -1.1), - lambda index: (-10, 0), - lambda index: (-10, -10), - lambda index: (-10, 3), - lambda index: (-9, math.inf), - lambda index: (-1, 1), - lambda index: (0, 0), - lambda index: (0, 1), - lambda index: (0, math.inf), - lambda index: (1, 1), - lambda index: (1, 10.1), - lambda index: (1, math.inf), - lambda index: (100.1, math.inf), - # pylint: disable=g-long-lambda - lambda index: ( - pd.Series(-math.inf, index=index), - pd.Series(-10.5, index=index), - ), - lambda index: ( - pd.Series(-math.inf, index=index), - pd.Series(-1, index=index), - ), - lambda index: ( - pd.Series(-math.inf, index=index), - pd.Series(0, index=index), - ), - lambda index: ( - pd.Series(-math.inf, index=index), - pd.Series(10, index=index), - ), - lambda index: ( - pd.Series(-math.inf, index=index), - pd.Series(math.inf, index=index), - ), - lambda index: (pd.Series(-10, index=index), pd.Series(-1.1, index=index)), - lambda index: (pd.Series(-10, index=index), pd.Series(0, index=index)), - lambda index: (pd.Series(-10, index=index), pd.Series(-10, index=index)), - lambda index: (pd.Series(-10, index=index), pd.Series(3, index=index)), - lambda index: ( - pd.Series(-9, index=index), - pd.Series(math.inf, index=index), - ), - lambda index: (pd.Series(-1, index=index), pd.Series(1, index=index)), - lambda index: (pd.Series(0, index=index), pd.Series(0, index=index)), - lambda index: (pd.Series(0, index=index), pd.Series(1, index=index)), - lambda index: ( - pd.Series(0, index=index), - pd.Series(math.inf, index=index), - ), - lambda index: (pd.Series(1, index=index), pd.Series(1, index=index)), - lambda index: (pd.Series(1, index=index), pd.Series(10.1, index=index)), - lambda index: ( - pd.Series(1, index=index), - pd.Series(math.inf, index=index), - ), - lambda index: ( - pd.Series(100.1, index=index), - pd.Series(math.inf, index=index), - ), + self.assertLen(variables, len(index)) + self.assertLen(set(variables), len(index)) + for i in index: + self.assertEqual(variables[i].name, f"test_variable[{i}]") + + @parameterized.product( + index=_variable_indices, bounds=_bounds, is_integer=_is_integer + ) + def test_get_variable_lower_bounds(self, index, bounds, is_integer): + lower_bound, upper_bound = bounds(index) + model = mb.Model() + x = model.new_var_series( + name="x", + index=index, + lower_bounds=lower_bound, + upper_bounds=upper_bound, + is_integral=is_integer(index), ) - _is_integer = ( - lambda index: False, - lambda index: True, - lambda index: pd.Series(False, index=index), - lambda index: pd.Series(True, index=index), + y = model.new_var_series( + name="y", + index=index, + lower_bounds=lower_bound, + upper_bounds=upper_bound, + is_integral=is_integer(index), ) - - @parameterized.product( - index=_variable_indices, bounds=_bounds, is_integer=_is_integer - ) - def test_new_var_series(self, index, bounds, is_integer): - model = mb.Model() - variables = model.new_var_series( - name="test_variable", - index=index, - lower_bounds=bounds(index)[0], - upper_bounds=bounds(index)[1], - is_integral=is_integer(index), - ) - self.assertLen(variables, len(index)) - self.assertLen(set(variables), len(index)) - for i in index: - self.assertEqual(variables[i].name, f"test_variable[{i}]") - - @parameterized.product( - index=_variable_indices, bounds=_bounds, is_integer=_is_integer - ) - def test_get_variable_lower_bounds(self, index, bounds, is_integer): - lower_bound, upper_bound = bounds(index) - model = mb.Model() - x = model.new_var_series( - name="x", - index=index, - lower_bounds=lower_bound, - upper_bounds=upper_bound, - is_integral=is_integer(index), - ) - y = model.new_var_series( - name="y", - index=index, - lower_bounds=lower_bound, - upper_bounds=upper_bound, - is_integral=is_integer(index), - ) - for lower_bounds in ( + for lower_bounds in ( + model.get_variable_lower_bounds(x), + model.get_variable_lower_bounds(y), + ): + self.assertSequenceAlmostEqual( + lower_bounds, + mb._convert_to_series_and_validate_index(lower_bound, index), + ) + self.assertSequenceAlmostEqual( + model.get_variable_lower_bounds(), + pd.concat([ model.get_variable_lower_bounds(x), model.get_variable_lower_bounds(y), - ): - self.assertSequenceAlmostEqual( - lower_bounds, - mb._convert_to_series_and_validate_index(lower_bound, index), - ) - self.assertSequenceAlmostEqual( - model.get_variable_lower_bounds(), - pd.concat( - [ - model.get_variable_lower_bounds(x), - model.get_variable_lower_bounds(y), - ] - ), - ) - variables = model.get_variables() - lower_bounds = model.get_variable_lower_bounds(variables) - self.assertSequenceAlmostEqual(lower_bounds.index, variables) - - @parameterized.product( - index=_variable_indices, bounds=_bounds, is_integer=_is_integer + ]), ) - def test_get_variable_upper_bounds(self, index, bounds, is_integer): - lower_bound, upper_bound = bounds(index) - model = mb.Model() - x = model.new_var_series( - name="x", - index=index, - lower_bounds=lower_bound, - upper_bounds=upper_bound, - is_integral=is_integer(index), - ) - y = model.new_var_series( - name="y", - index=index, - lower_bounds=lower_bound, - upper_bounds=upper_bound, - is_integral=is_integer(index), - ) - for upper_bounds in ( + variables = model.get_variables() + lower_bounds = model.get_variable_lower_bounds(variables) + self.assertSequenceAlmostEqual(lower_bounds.index, variables) + + @parameterized.product( + index=_variable_indices, bounds=_bounds, is_integer=_is_integer + ) + def test_get_variable_upper_bounds(self, index, bounds, is_integer): + lower_bound, upper_bound = bounds(index) + model = mb.Model() + x = model.new_var_series( + name="x", + index=index, + lower_bounds=lower_bound, + upper_bounds=upper_bound, + is_integral=is_integer(index), + ) + y = model.new_var_series( + name="y", + index=index, + lower_bounds=lower_bound, + upper_bounds=upper_bound, + is_integral=is_integer(index), + ) + for upper_bounds in ( + model.get_variable_upper_bounds(x), + model.get_variable_upper_bounds(y), + ): + self.assertSequenceAlmostEqual( + upper_bounds, + mb._convert_to_series_and_validate_index(upper_bound, index), + ) + self.assertSequenceAlmostEqual( + model.get_variable_upper_bounds(), + pd.concat([ model.get_variable_upper_bounds(x), model.get_variable_upper_bounds(y), - ): - self.assertSequenceAlmostEqual( - upper_bounds, - mb._convert_to_series_and_validate_index(upper_bound, index), - ) - self.assertSequenceAlmostEqual( - model.get_variable_upper_bounds(), - pd.concat( - [ - model.get_variable_upper_bounds(x), - model.get_variable_upper_bounds(y), - ] - ), - ) - variables = model.get_variables() - upper_bounds = model.get_variable_upper_bounds(variables) - self.assertSequenceAlmostEqual(upper_bounds.index, variables) + ]), + ) + variables = model.get_variables() + upper_bounds = model.get_variable_upper_bounds(variables) + self.assertSequenceAlmostEqual(upper_bounds.index, variables) class ModelBuilderLinearConstraintsTest(parameterized.TestCase): - constraint_test_cases = [ - # pylint: disable=g-long-lambda - dict( - testcase_name="True", - name="true", - bounded_exprs=lambda x, y: True, - constraint_count=1, - lower_bounds=[0.0], - upper_bounds=[0.0], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - testcase_name="pd.Series(True)", - name="true", - bounded_exprs=lambda x, y: pd.Series(True), - constraint_count=1, - lower_bounds=[0.0], - upper_bounds=[0.0], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - testcase_name="False", - name="false", - bounded_exprs=lambda x, y: False, - constraint_count=1, - lower_bounds=[1.0], - upper_bounds=[-1.0], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - testcase_name="pd.Series(False)", - name="false", - bounded_exprs=lambda x, y: pd.Series(False), - constraint_count=1, - lower_bounds=[1.0], - upper_bounds=[-1.0], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - testcase_name="x[0] <= 1.5", - name="x0_le_c", - bounded_exprs=lambda x, y: x[0] <= 1.5, - constraint_count=1, - lower_bounds=[-math.inf], - upper_bounds=[1.5], - expression_terms=lambda x, y: [{x[0]: 1}], - expression_offsets=[0], - ), - dict( - testcase_name="x[0] == 1", - name="x0_eq_c", - bounded_exprs=lambda x, y: x[0] == 1, - constraint_count=1, - lower_bounds=[1], - upper_bounds=[1], - expression_terms=lambda x, y: [{x[0]: 1}], - expression_offsets=[0], - ), - dict( - testcase_name="x[0] >= -1", - name="x0_ge_c", - bounded_exprs=lambda x, y: x[0] >= -1, - constraint_count=1, - lower_bounds=[-1], - upper_bounds=[math.inf], - expression_terms=lambda x, y: [{x[0]: 1}], - expression_offsets=[0], - ), - dict( - testcase_name="-1.5 <= x[0]", - name="c_le_x0", - bounded_exprs=lambda x, y: -1.5 <= x[0], - constraint_count=1, - lower_bounds=[-1.5], - upper_bounds=[math.inf], - expression_terms=lambda x, y: [{x[0]: 1}], - expression_offsets=[0], - ), - dict( - testcase_name="0 == x[0]", - name="c_eq_x0", - bounded_exprs=lambda x, y: 0 == x[0], - constraint_count=1, - lower_bounds=[0], - upper_bounds=[0], - expression_terms=lambda x, y: [{x[0]: 1}], - expression_offsets=[0], - ), - dict( - testcase_name="10 >= x[0]", - name="c_ge_x0", - bounded_exprs=lambda x, y: 10 >= x[0], - constraint_count=1, - lower_bounds=[-math.inf], - upper_bounds=[10], - expression_terms=lambda x, y: [{x[0]: 1}], - expression_offsets=[0], - ), - dict( - testcase_name="x[0] <= x[0]", - name="x0_le_x0", - bounded_exprs=lambda x, y: x[0] <= x[0], - constraint_count=1, - lower_bounds=[-math.inf], - upper_bounds=[0], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - testcase_name="x[0] == x[0]", - name="x0_eq_x0", - bounded_exprs=lambda x, y: x[0] == x[0], - constraint_count=1, - lower_bounds=[0], - upper_bounds=[0], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - testcase_name="pd.Series(x[0] == x[0])", - name="x0_eq_x0_series", - bounded_exprs=lambda x, y: pd.Series(x[0] == x[0]), - constraint_count=1, - lower_bounds=[0], - upper_bounds=[0], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - testcase_name="x[0] >= x[0]", - name="x0_ge_x0", - bounded_exprs=lambda x, y: x[0] >= x[0], - constraint_count=1, - lower_bounds=[0], - upper_bounds=[math.inf], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - # x[0] - x[0] <= 3 - testcase_name="x[0] - 1 <= x[0] + 2", - name="x0c_le_x0c", - bounded_exprs=lambda x, y: pd.Series(x[0] - 1 <= x[0] + 2), - constraint_count=1, - lower_bounds=[-math.inf], - upper_bounds=[3], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - # x[0] - x[0] == 3 - testcase_name="x[0] - 1 == x[0] + 2", - name="x0c_eq_x0c", - bounded_exprs=lambda x, y: pd.Series(x[0] - 1 == x[0] + 2), - constraint_count=1, - lower_bounds=[3], - upper_bounds=[3], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - # x[0] - x[0] >= 3 - testcase_name="x[0] - 1 >= x[0] + 2", - name="x0c_ge_x0c", - bounded_exprs=lambda x, y: pd.Series(x[0] - 1 >= x[0] + 2), - constraint_count=1, - lower_bounds=[3], - upper_bounds=[math.inf], - expression_terms=lambda x, y: [{}], - expression_offsets=[0], - ), - dict( - testcase_name="x[0] <= x[1]", - name="x0_le_x1", - bounded_exprs=lambda x, y: x[0] <= x[1], - constraint_count=1, - lower_bounds=[-math.inf], - upper_bounds=[0], - expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], - expression_offsets=[0], - ), - dict( - testcase_name="x[0] == x[1]", - name="x0_eq_x1", - bounded_exprs=lambda x, y: x[0] == x[1], - constraint_count=1, - lower_bounds=[0], - upper_bounds=[0], - expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], - expression_offsets=[0], - ), - dict( - testcase_name="x[0] >= x[1]", - name="x0_ge_x1", - bounded_exprs=lambda x, y: x[0] >= x[1], - constraint_count=1, - lower_bounds=[0], - upper_bounds=[math.inf], - expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], - expression_offsets=[0], - ), - dict( - # x[0] - x[1] <= -3 - testcase_name="x[0] + 1 <= x[1] - 2", - name="x0c_le_x1c", - bounded_exprs=lambda x, y: x[0] + 1 <= x[1] - 2, - constraint_count=1, - lower_bounds=[-math.inf], - upper_bounds=[-3], - expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], - expression_offsets=[0], - ), - dict( - # x[0] - x[1] == -3 - testcase_name="x[0] + 1 == x[1] - 2", - name="x0c_eq_x1c", - bounded_exprs=lambda x, y: x[0] + 1 == x[1] - 2, - constraint_count=1, - lower_bounds=[-3], - upper_bounds=[-3], - expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], - expression_offsets=[0], - ), - dict( - # x[0] - x[1] >= -3 - testcase_name="x[0] + 1 >= x[1] - 2", - name="x0c_ge_x1c", - bounded_exprs=lambda x, y: pd.Series(x[0] + 1 >= x[1] - 2), - constraint_count=1, - lower_bounds=[-3], - upper_bounds=[math.inf], - expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], - expression_offsets=[0], - ), - dict( - testcase_name="x <= 0", - name="x_le_c", - bounded_exprs=lambda x, y: x.apply(lambda expr: expr <= 0), - constraint_count=3, - lower_bounds=[-math.inf] * 3, - upper_bounds=[0] * 3, - expression_terms=lambda x, y: [{xi: 1} for xi in x], - expression_offsets=[0] * 3, - ), - dict( - testcase_name="x >= 0", - name="x_ge_c", - bounded_exprs=lambda x, y: x.apply(lambda expr: expr >= 0), - constraint_count=3, - lower_bounds=[0] * 3, - upper_bounds=[math.inf] * 3, - expression_terms=lambda x, y: [{xi: 1} for xi in x], - expression_offsets=[0] * 3, - ), - dict( - testcase_name="x == 0", - name="x_eq_c", - bounded_exprs=lambda x, y: x.apply(lambda expr: expr == 0), - constraint_count=3, - lower_bounds=[0] * 3, - upper_bounds=[0] * 3, - expression_terms=lambda x, y: [{xi: 1} for xi in x], - expression_offsets=[0] * 3, - ), - dict( - testcase_name="y == 0", - name="y_eq_c", - bounded_exprs=(lambda x, y: y.apply(lambda expr: expr == 0)), - constraint_count=2 * 3, - lower_bounds=[0] * 2 * 3, - upper_bounds=[0] * 2 * 3, - expression_terms=lambda x, y: [{yi: 1} for yi in y], - expression_offsets=[0] * 3 * 2, - ), - dict( - testcase_name='y.groupby("i").sum() == 0', - name="ygroupbyi_eq_c", - bounded_exprs=( - lambda x, y: y.groupby("i").sum().apply(lambda expr: expr == 0) - ), - constraint_count=2, - lower_bounds=[0] * 2, - upper_bounds=[0] * 2, - expression_terms=lambda x, y: [ - {y[1, "a"]: 1, y[1, "b"]: 1, y[1, "c"]: 1}, - {y[2, "a"]: 1, y[2, "b"]: 1, y[2, "c"]: 1}, - ], - expression_offsets=[0] * 2, - ), - dict( - testcase_name='y.groupby("j").sum() == 0', - name="ygroupbyj_eq_c", - bounded_exprs=( - lambda x, y: y.groupby("j").sum().apply(lambda expr: expr == 0) - ), - constraint_count=3, - lower_bounds=[0] * 3, - upper_bounds=[0] * 3, - expression_terms=lambda x, y: [ - {y[1, "a"]: 1, y[2, "a"]: 1}, - {y[1, "b"]: 1, y[2, "b"]: 1}, - {y[1, "c"]: 1, y[2, "c"]: 1}, - ], - expression_offsets=[0] * 3, - ), - dict( - testcase_name='3 * x + y.groupby("i").sum() <= 0', - name="broadcast_align_fill", - bounded_exprs=( - lambda x, y: (3 * x) - .add(y.groupby("i").sum(), fill_value=0) - .apply(lambda expr: expr <= 0) - ), - constraint_count=3, - lower_bounds=[-math.inf] * 3, - upper_bounds=[0] * 3, - expression_terms=lambda x, y: [ - {x[0]: 3}, - {x[1]: 3, y[1, "a"]: 1, y[1, "b"]: 1, y[1, "c"]: 1}, - {x[2]: 3, y[2, "a"]: 1, y[2, "b"]: 1, y[2, "c"]: 1}, - ], - expression_offsets=[0] * 3, - ), - ] - - def create_test_model(self, name, bounded_exprs): - model = mb.Model() - x = model.new_var_series( - name="x", - index=pd.Index(range(3), name="i"), - ) - y = model.new_var_series( - name="y", - index=pd.MultiIndex.from_product( - ((1, 2), ("a", "b", "c")), names=["i", "j"] - ), - ) - model.add(name=name, ct=bounded_exprs(x, y)) - return model, {"x": x, "y": y} - - @parameterized.named_parameters( - # pylint: disable=g-complex-comprehension - { - f: tc[f] - for f in [ - "testcase_name", - "name", - "bounded_exprs", - "constraint_count", - ] - } - for tc in constraint_test_cases + constraint_test_cases = [ + # pylint: disable=g-long-lambda + dict( + testcase_name="True", + name="true", + bounded_exprs=lambda x, y: True, + constraint_count=1, + lower_bounds=[0.0], + upper_bounds=[0.0], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="pd.Series(True)", + name="true", + bounded_exprs=lambda x, y: pd.Series(True), + constraint_count=1, + lower_bounds=[0.0], + upper_bounds=[0.0], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="False", + name="false", + bounded_exprs=lambda x, y: False, + constraint_count=1, + lower_bounds=[1.0], + upper_bounds=[-1.0], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="pd.Series(False)", + name="false", + bounded_exprs=lambda x, y: pd.Series(False), + constraint_count=1, + lower_bounds=[1.0], + upper_bounds=[-1.0], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] <= 1.5", + name="x0_le_c", + bounded_exprs=lambda x, y: x[0] <= 1.5, + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[1.5], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] == 1", + name="x0_eq_c", + bounded_exprs=lambda x, y: x[0] == 1, + constraint_count=1, + lower_bounds=[1], + upper_bounds=[1], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] >= -1", + name="x0_ge_c", + bounded_exprs=lambda x, y: x[0] >= -1, + constraint_count=1, + lower_bounds=[-1], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="-1.5 <= x[0]", + name="c_le_x0", + bounded_exprs=lambda x, y: -1.5 <= x[0], + constraint_count=1, + lower_bounds=[-1.5], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="0 == x[0]", + name="c_eq_x0", + bounded_exprs=lambda x, y: 0 == x[0], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[0], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="10 >= x[0]", + name="c_ge_x0", + bounded_exprs=lambda x, y: 10 >= x[0], + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[10], + expression_terms=lambda x, y: [{x[0]: 1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] <= x[0]", + name="x0_le_x0", + bounded_exprs=lambda x, y: x[0] <= x[0], + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[0], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] == x[0]", + name="x0_eq_x0", + bounded_exprs=lambda x, y: x[0] == x[0], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[0], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="pd.Series(x[0] == x[0])", + name="x0_eq_x0_series", + bounded_exprs=lambda x, y: pd.Series(x[0] == x[0]), + constraint_count=1, + lower_bounds=[0], + upper_bounds=[0], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] >= x[0]", + name="x0_ge_x0", + bounded_exprs=lambda x, y: x[0] >= x[0], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + # x[0] - x[0] <= 3 + testcase_name="x[0] - 1 <= x[0] + 2", + name="x0c_le_x0c", + bounded_exprs=lambda x, y: pd.Series(x[0] - 1 <= x[0] + 2), + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[3], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + # x[0] - x[0] == 3 + testcase_name="x[0] - 1 == x[0] + 2", + name="x0c_eq_x0c", + bounded_exprs=lambda x, y: pd.Series(x[0] - 1 == x[0] + 2), + constraint_count=1, + lower_bounds=[3], + upper_bounds=[3], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + # x[0] - x[0] >= 3 + testcase_name="x[0] - 1 >= x[0] + 2", + name="x0c_ge_x0c", + bounded_exprs=lambda x, y: pd.Series(x[0] - 1 >= x[0] + 2), + constraint_count=1, + lower_bounds=[3], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] <= x[1]", + name="x0_le_x1", + bounded_exprs=lambda x, y: x[0] <= x[1], + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[0], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] == x[1]", + name="x0_eq_x1", + bounded_exprs=lambda x, y: x[0] == x[1], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[0], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + testcase_name="x[0] >= x[1]", + name="x0_ge_x1", + bounded_exprs=lambda x, y: x[0] >= x[1], + constraint_count=1, + lower_bounds=[0], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + # x[0] - x[1] <= -3 + testcase_name="x[0] + 1 <= x[1] - 2", + name="x0c_le_x1c", + bounded_exprs=lambda x, y: x[0] + 1 <= x[1] - 2, + constraint_count=1, + lower_bounds=[-math.inf], + upper_bounds=[-3], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + # x[0] - x[1] == -3 + testcase_name="x[0] + 1 == x[1] - 2", + name="x0c_eq_x1c", + bounded_exprs=lambda x, y: x[0] + 1 == x[1] - 2, + constraint_count=1, + lower_bounds=[-3], + upper_bounds=[-3], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + # x[0] - x[1] >= -3 + testcase_name="x[0] + 1 >= x[1] - 2", + name="x0c_ge_x1c", + bounded_exprs=lambda x, y: pd.Series(x[0] + 1 >= x[1] - 2), + constraint_count=1, + lower_bounds=[-3], + upper_bounds=[math.inf], + expression_terms=lambda x, y: [{x[0]: 1, x[1]: -1}], + expression_offsets=[0], + ), + dict( + testcase_name="x <= 0", + name="x_le_c", + bounded_exprs=lambda x, y: x.apply(lambda expr: expr <= 0), + constraint_count=3, + lower_bounds=[-math.inf] * 3, + upper_bounds=[0] * 3, + expression_terms=lambda x, y: [{xi: 1} for xi in x], + expression_offsets=[0] * 3, + ), + dict( + testcase_name="x >= 0", + name="x_ge_c", + bounded_exprs=lambda x, y: x.apply(lambda expr: expr >= 0), + constraint_count=3, + lower_bounds=[0] * 3, + upper_bounds=[math.inf] * 3, + expression_terms=lambda x, y: [{xi: 1} for xi in x], + expression_offsets=[0] * 3, + ), + dict( + testcase_name="x == 0", + name="x_eq_c", + bounded_exprs=lambda x, y: x.apply(lambda expr: expr == 0), + constraint_count=3, + lower_bounds=[0] * 3, + upper_bounds=[0] * 3, + expression_terms=lambda x, y: [{xi: 1} for xi in x], + expression_offsets=[0] * 3, + ), + dict( + testcase_name="y == 0", + name="y_eq_c", + bounded_exprs=(lambda x, y: y.apply(lambda expr: expr == 0)), + constraint_count=2 * 3, + lower_bounds=[0] * 2 * 3, + upper_bounds=[0] * 2 * 3, + expression_terms=lambda x, y: [{yi: 1} for yi in y], + expression_offsets=[0] * 3 * 2, + ), + dict( + testcase_name='y.groupby("i").sum() == 0', + name="ygroupbyi_eq_c", + bounded_exprs=( + lambda x, y: y.groupby("i").sum().apply(lambda expr: expr == 0) + ), + constraint_count=2, + lower_bounds=[0] * 2, + upper_bounds=[0] * 2, + expression_terms=lambda x, y: [ + {y[1, "a"]: 1, y[1, "b"]: 1, y[1, "c"]: 1}, + {y[2, "a"]: 1, y[2, "b"]: 1, y[2, "c"]: 1}, + ], + expression_offsets=[0] * 2, + ), + dict( + testcase_name='y.groupby("j").sum() == 0', + name="ygroupbyj_eq_c", + bounded_exprs=( + lambda x, y: y.groupby("j").sum().apply(lambda expr: expr == 0) + ), + constraint_count=3, + lower_bounds=[0] * 3, + upper_bounds=[0] * 3, + expression_terms=lambda x, y: [ + {y[1, "a"]: 1, y[2, "a"]: 1}, + {y[1, "b"]: 1, y[2, "b"]: 1}, + {y[1, "c"]: 1, y[2, "c"]: 1}, + ], + expression_offsets=[0] * 3, + ), + dict( + testcase_name='3 * x + y.groupby("i").sum() <= 0', + name="broadcast_align_fill", + bounded_exprs=( + lambda x, y: (3 * x) + .add(y.groupby("i").sum(), fill_value=0) + .apply(lambda expr: expr <= 0) + ), + constraint_count=3, + lower_bounds=[-math.inf] * 3, + upper_bounds=[0] * 3, + expression_terms=lambda x, y: [ + {x[0]: 3}, + {x[1]: 3, y[1, "a"]: 1, y[1, "b"]: 1, y[1, "c"]: 1}, + {x[2]: 3, y[2, "a"]: 1, y[2, "b"]: 1, y[2, "c"]: 1}, + ], + expression_offsets=[0] * 3, + ), + ] + + def create_test_model(self, name, bounded_exprs): + model = mb.Model() + x = model.new_var_series( + name="x", + index=pd.Index(range(3), name="i"), ) - def test_get_linear_constraints( - self, - name, - bounded_exprs, - constraint_count, - ): - model, _ = self.create_test_model(name, bounded_exprs) - linear_constraints = model.get_linear_constraints() - self.assertIsInstance(linear_constraints, pd.Index) - self.assertLen(linear_constraints, constraint_count) - - def test_get_linear_constraints_empty(self): - linear_constraints = mb.Model().get_linear_constraints() - self.assertIsInstance(linear_constraints, pd.Index) - self.assertEmpty(linear_constraints) - - @parameterized.named_parameters( - # pylint: disable=g-complex-comprehension - { - f: tc[f] - for f in [ - "testcase_name", - "name", - "bounded_exprs", - "lower_bounds", - ] - } - for tc in constraint_test_cases + y = model.new_var_series( + name="y", + index=pd.MultiIndex.from_product( + ((1, 2), ("a", "b", "c")), names=["i", "j"] + ), ) - def test_get_linear_constraint_lower_bounds( - self, - name, - bounded_exprs, - lower_bounds, + model.add(name=name, ct=bounded_exprs(x, y)) + return model, {"x": x, "y": y} + + @parameterized.named_parameters( + # pylint: disable=g-complex-comprehension + { + f: tc[f] + for f in [ + "testcase_name", + "name", + "bounded_exprs", + "constraint_count", + ] + } + for tc in constraint_test_cases + ) + def test_get_linear_constraints( + self, + name, + bounded_exprs, + constraint_count, + ): + model, _ = self.create_test_model(name, bounded_exprs) + linear_constraints = model.get_linear_constraints() + self.assertIsInstance(linear_constraints, pd.Index) + self.assertLen(linear_constraints, constraint_count) + + def test_get_linear_constraints_empty(self): + linear_constraints = mb.Model().get_linear_constraints() + self.assertIsInstance(linear_constraints, pd.Index) + self.assertEmpty(linear_constraints) + + @parameterized.named_parameters( + # pylint: disable=g-complex-comprehension + { + f: tc[f] + for f in [ + "testcase_name", + "name", + "bounded_exprs", + "lower_bounds", + ] + } + for tc in constraint_test_cases + ) + def test_get_linear_constraint_lower_bounds( + self, + name, + bounded_exprs, + lower_bounds, + ): + model, _ = self.create_test_model(name, bounded_exprs) + for linear_constraint_lower_bounds in ( + model.get_linear_constraint_lower_bounds(), + model.get_linear_constraint_lower_bounds( + model.get_linear_constraints() + ), ): - model, _ = self.create_test_model(name, bounded_exprs) - for linear_constraint_lower_bounds in ( - model.get_linear_constraint_lower_bounds(), - model.get_linear_constraint_lower_bounds(model.get_linear_constraints()), - ): - self.assertSequenceAlmostEqual(linear_constraint_lower_bounds, lower_bounds) - - @parameterized.named_parameters( - # pylint: disable=g-complex-comprehension - { - f: tc[f] - for f in [ - "testcase_name", - "name", - "bounded_exprs", - "upper_bounds", - ] - } - for tc in constraint_test_cases - ) - def test_get_linear_constraint_upper_bounds( - self, - name, - bounded_exprs, - upper_bounds, + self.assertSequenceAlmostEqual( + linear_constraint_lower_bounds, lower_bounds + ) + + @parameterized.named_parameters( + # pylint: disable=g-complex-comprehension + { + f: tc[f] + for f in [ + "testcase_name", + "name", + "bounded_exprs", + "upper_bounds", + ] + } + for tc in constraint_test_cases + ) + def test_get_linear_constraint_upper_bounds( + self, + name, + bounded_exprs, + upper_bounds, + ): + model, _ = self.create_test_model(name, bounded_exprs) + for linear_constraint_upper_bounds in ( + model.get_linear_constraint_upper_bounds(), + model.get_linear_constraint_upper_bounds( + model.get_linear_constraints() + ), ): - model, _ = self.create_test_model(name, bounded_exprs) - for linear_constraint_upper_bounds in ( - model.get_linear_constraint_upper_bounds(), - model.get_linear_constraint_upper_bounds(model.get_linear_constraints()), - ): - self.assertSequenceAlmostEqual(linear_constraint_upper_bounds, upper_bounds) - - @parameterized.named_parameters( - # pylint: disable=g-complex-comprehension - { - f: tc[f] - for f in [ - "testcase_name", - "name", - "bounded_exprs", - "expression_terms", - "expression_offsets", - ] - } - for tc in constraint_test_cases - ) - def test_get_linear_constraint_expressions( - self, - name, - bounded_exprs, - expression_terms, - expression_offsets, + self.assertSequenceAlmostEqual( + linear_constraint_upper_bounds, upper_bounds + ) + + @parameterized.named_parameters( + # pylint: disable=g-complex-comprehension + { + f: tc[f] + for f in [ + "testcase_name", + "name", + "bounded_exprs", + "expression_terms", + "expression_offsets", + ] + } + for tc in constraint_test_cases + ) + def test_get_linear_constraint_expressions( + self, + name, + bounded_exprs, + expression_terms, + expression_offsets, + ): + model, variables = self.create_test_model(name, bounded_exprs) + x = variables["x"] + y = variables["y"] + for linear_constraint_expressions in ( + model.get_linear_constraint_expressions(), + model.get_linear_constraint_expressions(model.get_linear_constraints()), ): - model, variables = self.create_test_model(name, bounded_exprs) - x = variables["x"] - y = variables["y"] - for linear_constraint_expressions in ( - model.get_linear_constraint_expressions(), - model.get_linear_constraint_expressions(model.get_linear_constraints()), - ): - expr_terms = expression_terms(x, y) - self.assertLen(linear_constraint_expressions, len(expr_terms)) - for expr, expr_term in zip(linear_constraint_expressions, expr_terms): - self.assertDictEqual(build_dict(expr), expr_term) - self.assertSequenceAlmostEqual( - [expr.offset for expr in linear_constraint_expressions], - expression_offsets, - ) + expr_terms = expression_terms(x, y) + self.assertLen(linear_constraint_expressions, len(expr_terms)) + for expr, expr_term in zip(linear_constraint_expressions, expr_terms): + self.assertDictEqual(build_dict(expr), expr_term) + self.assertSequenceAlmostEqual( + [expr.offset for expr in linear_constraint_expressions], + expression_offsets, + ) class ModelBuilderObjectiveTest(parameterized.TestCase): - _expressions = ( - lambda x, y: -3, - lambda x, y: 0, - lambda x, y: 10, - lambda x, y: x[0], - lambda x, y: x[1], - lambda x, y: x[2], - lambda x, y: y[0], - lambda x, y: y[1], - lambda x, y: x[0] + 5, - lambda x, y: -3 + y[1], - lambda x, y: 3 * x[0], - lambda x, y: x[0] * 3 * 5 - 3, - lambda x, y: x.sum(), - lambda x, y: 101 + 2 * 3 * x.sum(), - lambda x, y: x.sum() * 2, - lambda x, y: sum(y), - lambda x, y: x.sum() + 2 * y.sum() + 3, - ) - _variable_indices = ( - pd.Index(range(3)), - pd.Index(range(3), name="i"), - pd.Index(range(10), name="i"), + _expressions = ( + lambda x, y: -3, + lambda x, y: 0, + lambda x, y: 10, + lambda x, y: x[0], + lambda x, y: x[1], + lambda x, y: x[2], + lambda x, y: y[0], + lambda x, y: y[1], + lambda x, y: x[0] + 5, + lambda x, y: -3 + y[1], + lambda x, y: 3 * x[0], + lambda x, y: x[0] * 3 * 5 - 3, + lambda x, y: x.sum(), + lambda x, y: 101 + 2 * 3 * x.sum(), + lambda x, y: x.sum() * 2, + lambda x, y: sum(y), + lambda x, y: x.sum() + 2 * y.sum() + 3, + ) + _variable_indices = ( + pd.Index(range(3)), + pd.Index(range(3), name="i"), + pd.Index(range(10), name="i"), + ) + + def assertLinearExpressionAlmostEqual( + self, + expr1: mbh.LinearExpr, + expr2: mbh.LinearExpr, + ) -> None: + """Test that the two linear expressions are almost equal.""" + flat_expr1 = mbh.FlatExpr(expr1) + flat_expr2 = mbh.FlatExpr(expr2) + self.assertEqual(len(flat_expr1.vars), len(flat_expr2.vars)) + if len(flat_expr1.vars) > 0: # pylint: disable=g-explicit-length-test + self.assertSequenceEqual(flat_expr1.vars, flat_expr2.vars) + self.assertSequenceAlmostEqual( + flat_expr1.coeffs, flat_expr2.coeffs, places=5 + ) + else: + self.assertEmpty(flat_expr1.coeffs) + self.assertEmpty(flat_expr2.coeffs) + self.assertAlmostEqual(flat_expr1.offset, flat_expr2.offset) + + @parameterized.product( + expression=_expressions, + variable_indices=_variable_indices, + is_maximize=(True, False), + ) + def test_set_objective( + self, + expression: Callable[[pd.Series, pd.Series], mb.LinearExprT], + variable_indices: pd.Index, + is_maximize: bool, + ): + model = mb.Model() + x = model.new_var_series(name="x", index=variable_indices) + y = model.new_var_series(name="y", index=variable_indices) + objective_expression = expression(x, y) + if is_maximize: + model.maximize(objective_expression) + else: + model.minimize(objective_expression) + got_objective_expression = model.objective_expression() + self.assertLinearExpressionAlmostEqual( + got_objective_expression, objective_expression ) - def assertLinearExpressionAlmostEqual( - self, - expr1: mbh.LinearExpr, - expr2: mbh.LinearExpr, - ) -> None: - """Test that the two linear expressions are almost equal.""" - flat_expr1 = mbh.FlatExpr(expr1) - flat_expr2 = mbh.FlatExpr(expr2) - self.assertEqual(len(flat_expr1.vars), len(flat_expr2.vars)) - if len(flat_expr1.vars) > 0: # pylint: disable=g-explicit-length-test - self.assertSequenceEqual(flat_expr1.vars, flat_expr2.vars) - self.assertSequenceAlmostEqual( - flat_expr1.coeffs, flat_expr2.coeffs, places=5 - ) - else: - self.assertEmpty(flat_expr1.coeffs) - self.assertEmpty(flat_expr2.coeffs) - self.assertAlmostEqual(flat_expr1.offset, flat_expr2.offset) - - @parameterized.product( - expression=_expressions, - variable_indices=_variable_indices, - is_maximize=(True, False), + def test_set_new_objective(self): + model = mb.Model() + x = model.new_var_series(name="x", index=pd.Index(range(3))) + old_objective_expression = 1 + new_objective_expression = x.sum() - 2.3 + + # Set and check for old objective. + model.maximize(old_objective_expression) + flat_got_objective_expression = mbh.FlatExpr(model.objective_expression()) + self.assertEmpty(flat_got_objective_expression.vars) + self.assertEmpty(flat_got_objective_expression.coeffs) + self.assertAlmostEqual(flat_got_objective_expression.offset, 1) + + # Set to a new objective and check that it is different. + model.minimize(new_objective_expression) + got_objective_expression = model.objective_expression() + self.assertLinearExpressionAlmostEqual( + got_objective_expression, new_objective_expression ) - def test_set_objective( - self, - expression: Callable[[pd.Series, pd.Series], mb.LinearExprT], - variable_indices: pd.Index, - is_maximize: bool, - ): - model = mb.Model() - x = model.new_var_series(name="x", index=variable_indices) - y = model.new_var_series(name="y", index=variable_indices) - objective_expression = expression(x, y) - if is_maximize: - model.maximize(objective_expression) - else: - model.minimize(objective_expression) - got_objective_expression = model.objective_expression() - self.assertLinearExpressionAlmostEqual( - got_objective_expression, objective_expression - ) - - def test_set_new_objective(self): - model = mb.Model() - x = model.new_var_series(name="x", index=pd.Index(range(3))) - old_objective_expression = 1 - new_objective_expression = x.sum() - 2.3 - - # Set and check for old objective. - model.maximize(old_objective_expression) - flat_got_objective_expression = mbh.FlatExpr(model.objective_expression()) - self.assertEmpty(flat_got_objective_expression.vars) - self.assertEmpty(flat_got_objective_expression.coeffs) - self.assertAlmostEqual(flat_got_objective_expression.offset, 1) - - # Set to a new objective and check that it is different. - model.minimize(new_objective_expression) - got_objective_expression = model.objective_expression() - self.assertLinearExpressionAlmostEqual( - got_objective_expression, new_objective_expression - ) - @parameterized.product( - expression=_expressions, - variable_indices=_variable_indices, + @parameterized.product( + expression=_expressions, + variable_indices=_variable_indices, + ) + def test_minimize( + self, + expression: Callable[[pd.Series, pd.Series], mb.LinearExprT], + variable_indices: pd.Index, + ): + model = mb.Model() + x = model.new_var_series(name="x", index=variable_indices) + y = model.new_var_series(name="y", index=variable_indices) + objective_expression = mbh.FlatExpr(expression(x, y)) + model.minimize(objective_expression) + got_objective_expression = model.objective_expression() + self.assertLinearExpressionAlmostEqual( + got_objective_expression, objective_expression ) - def test_minimize( - self, - expression: Callable[[pd.Series, pd.Series], mb.LinearExprT], - variable_indices: pd.Index, - ): - model = mb.Model() - x = model.new_var_series(name="x", index=variable_indices) - y = model.new_var_series(name="y", index=variable_indices) - objective_expression = mbh.FlatExpr(expression(x, y)) - model.minimize(objective_expression) - got_objective_expression = model.objective_expression() - self.assertLinearExpressionAlmostEqual( - got_objective_expression, objective_expression - ) - @parameterized.product( - expression=_expressions, - variable_indices=_variable_indices, + @parameterized.product( + expression=_expressions, + variable_indices=_variable_indices, + ) + def test_maximize( + self, + expression: Callable[[pd.Series, pd.Series], float], + variable_indices: pd.Index, + ): + model = mb.Model() + x = model.new_var_series(name="x", index=variable_indices) + y = model.new_var_series(name="y", index=variable_indices) + objective_expression = mbh.FlatExpr(expression(x, y)) + model.maximize(objective_expression) + got_objective_expression = model.objective_expression() + self.assertLinearExpressionAlmostEqual( + got_objective_expression, objective_expression ) - def test_maximize( - self, - expression: Callable[[pd.Series, pd.Series], float], - variable_indices: pd.Index, - ): - model = mb.Model() - x = model.new_var_series(name="x", index=variable_indices) - y = model.new_var_series(name="y", index=variable_indices) - objective_expression = mbh.FlatExpr(expression(x, y)) - model.maximize(objective_expression) - got_objective_expression = model.objective_expression() - self.assertLinearExpressionAlmostEqual( - got_objective_expression, objective_expression - ) class ModelBuilderProtoTest(absltest.TestCase): - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() - def test_export_to_proto(self): - expected = linear_solver_pb2.MPModelProto() - text_format.Parse( - """ + def test_export_to_proto(self): + expected = linear_solver_pb2.MPModelProto() + text_format.Parse( + """ name: "test_name" maximize: true objective_offset: 0 @@ -1713,692 +1735,690 @@ def test_export_to_proto(self): name: "Ct[1]" } """, - expected, - ) - model = mb.Model() - model.name = "test_name" - x = model.new_var_series("x", pd.Index(range(2)), 0, 1000) - model.add(ct=x.apply(lambda expr: expr <= 10), name="Ct") - model.maximize(x.sum()) - self.assertEqual(str(expected), str(model.export_to_proto())) + expected, + ) + model = mb.Model() + model.name = "test_name" + x = model.new_var_series("x", pd.Index(range(2)), 0, 1000) + model.add(ct=x.apply(lambda expr: expr <= 10), name="Ct") + model.maximize(x.sum()) + self.assertEqual(str(expected), str(model.export_to_proto())) class SolverTest(parameterized.TestCase): - _solvers = ( - { - "name": "sat", - "is_integer": True, # CP-SAT supports only pure integer variables. - }, - { - "name": "glop", - "solver_specific_parameters": "use_preprocessing: False", - "is_integer": False, # GLOP does not properly support integers. - }, - { - "name": "scip", - "is_integer": False, - }, - { - "name": "scip", - "is_integer": True, - }, - { - "name": "highs_lp", - "is_integer": False, - }, - { - "name": "highs", - "is_integer": True, - }, + _solvers = ( + { + "name": "sat", + "is_integer": True, # CP-SAT supports only pure integer variables. + }, + { + "name": "glop", + "solver_specific_parameters": "use_preprocessing: False", + "is_integer": False, # GLOP does not properly support integers. + }, + { + "name": "scip", + "is_integer": False, + }, + { + "name": "scip", + "is_integer": True, + }, + { + "name": "highs_lp", + "is_integer": False, + }, + { + "name": "highs", + "is_integer": True, + }, + ) + _variable_indices = ( + pd.Index(range(0)), # No variables. + pd.Index(range(1)), # Single variable. + pd.Index(range(3)), # Multiple variables. + ) + _variable_bounds = (-1, 0, 10.1) + _solve_statuses = ( + mb.SolveStatus.OPTIMAL, + mb.SolveStatus.INFEASIBLE, + mb.SolveStatus.UNBOUNDED, + ) + _set_objectives = (True, False) + _objective_senses = (True, False) + _objective_expressions = ( + sum, + lambda x: sum(x) + 5.2, + lambda x: -10.1, + lambda x: 0, + ) + + def _create_model( + self, + variable_indices: pd.Index = pd.Index(range(3)), + variable_bound: float = 0, + is_integer: bool = False, + solve_status: mb.SolveStatus = mb.SolveStatus.OPTIMAL, + set_objective: bool = True, + is_maximize: bool = True, + objective_expression: Callable[[pd.Series], float] = lambda x: x.sum(), + ) -> mb.ModelBuilder: + """Constructs an optimization problem. + + It has the following formulation: + + ``` + maximize / minimize objective_expression(x) + satisfying constraints + (if solve_status != UNBOUNDED and objective_sense == MAXIMIZE) + x[variable_indices] <= variable_bound + (if solve_status != UNBOUNDED and objective_sense == MINIMIZE) + x[variable_indices] >= variable_bound + x[variable_indices] is_integer + False (if solve_status == INFEASIBLE) + ``` + + Args: + variable_indices (pd.Index): The indices of the variable(s). + variable_bound (float): The upper- or lower-bound(s) of the variable(s). + is_integer (bool): Whether the variables should be integer. + solve_status (mb.SolveStatus): The solve status to target. + set_objective (bool): Whether to set the objective of the model. + is_maximize (bool): Whether to maximize the objective of the model. + objective_expression (Callable[[pd.Series], float]): The expression to + maximize or minimize if set_objective=True. + + Returns: + mb.ModelBuilder: The resulting problem. + """ + model = mb.Model() + # Variable(s) + x = model.new_var_series( + name="x", + index=pd.Index(variable_indices), + is_integral=is_integer, ) - _variable_indices = ( - pd.Index(range(0)), # No variables. - pd.Index(range(1)), # Single variable. - pd.Index(range(3)), # Multiple variables. + # Constraint(s) + if solve_status == mb.SolveStatus.INFEASIBLE: + # Force infeasibility here to test that we get pd.NA later. + model.add(False, name="bool") + elif solve_status != mb.SolveStatus.UNBOUNDED: + if is_maximize: + model.add(x.apply(lambda xi: xi <= variable_bound), "upper_bound") + else: + model.add(x.apply(lambda xi: xi >= variable_bound), "lower_bound") + # Objective + if set_objective: + if is_maximize: + model.maximize(objective_expression(x)) + else: + model.minimize(objective_expression(x)) + return model + + @parameterized.product( + solver=_solvers, + variable_indices=_variable_indices, + variable_bound=_variable_bounds, + solve_status=_solve_statuses, + set_objective=_set_objectives, + is_maximize=_objective_senses, + objective_expression=_objective_expressions, + ) + def test_solve_status( + self, + solver: Dict[str, Union[str, Mapping[str, Any], bool]], + variable_indices: pd.Index, + variable_bound: float, + solve_status: mb.SolveStatus, + set_objective: bool, + is_maximize: bool, + objective_expression: Callable[[pd.Series], float], + ): + model = self._create_model( + variable_indices=variable_indices, + variable_bound=variable_bound, + is_integer=solver["is_integer"], + solve_status=solve_status, + set_objective=set_objective, + is_maximize=is_maximize, + objective_expression=objective_expression, ) - _variable_bounds = (-1, 0, 10.1) - _solve_statuses = ( - mb.SolveStatus.OPTIMAL, - mb.SolveStatus.INFEASIBLE, - mb.SolveStatus.UNBOUNDED, - ) - _set_objectives = (True, False) - _objective_senses = (True, False) - _objective_expressions = ( - sum, - lambda x: sum(x) + 5.2, - lambda x: -10.1, - lambda x: 0, - ) - - def _create_model( - self, - variable_indices: pd.Index = pd.Index(range(3)), - variable_bound: float = 0, - is_integer: bool = False, - solve_status: mb.SolveStatus = mb.SolveStatus.OPTIMAL, - set_objective: bool = True, - is_maximize: bool = True, - objective_expression: Callable[[pd.Series], float] = lambda x: x.sum(), - ) -> mb.ModelBuilder: - """Constructs an optimization problem. - - It has the following formulation: - - ``` - maximize / minimize objective_expression(x) - satisfying constraints - (if solve_status != UNBOUNDED and objective_sense == MAXIMIZE) - x[variable_indices] <= variable_bound - (if solve_status != UNBOUNDED and objective_sense == MINIMIZE) - x[variable_indices] >= variable_bound - x[variable_indices] is_integer - False (if solve_status == INFEASIBLE) - ``` - - Args: - variable_indices (pd.Index): The indices of the variable(s). - variable_bound (float): The upper- or lower-bound(s) of the variable(s). - is_integer (bool): Whether the variables should be integer. - solve_status (mb.SolveStatus): The solve status to target. - set_objective (bool): Whether to set the objective of the model. - is_maximize (bool): Whether to maximize the objective of the model. - objective_expression (Callable[[pd.Series], float]): The expression to - maximize or minimize if set_objective=True. - - Returns: - mb.ModelBuilder: The resulting problem. - """ - model = mb.Model() - # Variable(s) - x = model.new_var_series( - name="x", - index=pd.Index(variable_indices), - is_integral=is_integer, + model_solver = mb.Solver(solver["name"]) + if not model_solver.solver_is_supported(): + print(f'Solver {solver["name"]} is not supported') + return + if solver.get("solver_specific_parameters"): + model_solver.set_solver_specific_parameters( + solver.get("solver_specific_parameters") + ) + got_solve_status = model_solver.solve(model) + + # pylint: disable=g-explicit-length-test + # (we disable explicit-length-test here because `variable_indices: pd.Index` + # evaluates to an ambiguous boolean value.) + if len(variable_indices) > 0: # Test cases with >=1 variable. + self.assertNotEmpty(variable_indices) + if ( + isinstance( + objective_expression(model.get_variables()), + (int, float), + ) + and solve_status != mb.SolveStatus.INFEASIBLE + ): + # Feasibility implies optimality when objective is a constant term. + self.assertEqual(got_solve_status, mb.SolveStatus.OPTIMAL) + elif not set_objective and solve_status != mb.SolveStatus.INFEASIBLE: + # Feasibility implies optimality when objective is not set. + self.assertEqual(got_solve_status, mb.SolveStatus.OPTIMAL) + elif solver["name"] == "sat" and got_solve_status == 8: + # CP_SAT returns status=8 for some infeasible and unbounded cases. + self.assertIn( + solve_status, + (mb.SolveStatus.INFEASIBLE, mb.SolveStatus.UNBOUNDED), ) - # Constraint(s) - if solve_status == mb.SolveStatus.INFEASIBLE: - # Force infeasibility here to test that we get pd.NA later. - model.add(False, name="bool") - elif solve_status != mb.SolveStatus.UNBOUNDED: - if is_maximize: - model.add(x.apply(lambda xi: xi <= variable_bound), "upper_bound") - else: - model.add(x.apply(lambda xi: xi >= variable_bound), "lower_bound") - # Objective - if set_objective: - if is_maximize: - model.maximize(objective_expression(x)) - else: - model.minimize(objective_expression(x)) - return model - - @parameterized.product( - solver=_solvers, - variable_indices=_variable_indices, - variable_bound=_variable_bounds, - solve_status=_solve_statuses, - set_objective=_set_objectives, - is_maximize=_objective_senses, - objective_expression=_objective_expressions, + elif ( + solver["name"] == "highs" + and got_solve_status == mb.SolveStatus.INFEASIBLE + and solve_status == mb.SolveStatus.UNBOUNDED + ): + # Highs is can return INFEASIBLE when UNBOUNDED is expected. + pass + else: + self.assertEqual(got_solve_status, solve_status) + elif solve_status == mb.SolveStatus.UNBOUNDED: + # Unbounded problems are optimal when there are no variables. + self.assertEqual(got_solve_status, mb.SolveStatus.OPTIMAL) + else: + self.assertEqual(got_solve_status, solve_status) + + @parameterized.product( + solver=_solvers, + variable_indices=_variable_indices, + variable_bound=_variable_bounds, + solve_status=_solve_statuses, + set_objective=_set_objectives, + is_maximize=_objective_senses, + objective_expression=_objective_expressions, + ) + def test_get_variable_values( + self, + solver: Dict[str, Union[str, Mapping[str, Any], bool]], + variable_indices: pd.Index, + variable_bound: float, + solve_status: mb.SolveStatus, + set_objective: bool, + is_maximize: bool, + objective_expression: Callable[[pd.Series], float], + ): + model = self._create_model( + variable_indices=variable_indices, + variable_bound=variable_bound, + is_integer=solver["is_integer"], + solve_status=solve_status, + set_objective=set_objective, + is_maximize=is_maximize, + objective_expression=objective_expression, ) - def test_solve_status( - self, - solver: Dict[str, Union[str, Mapping[str, Any], bool]], - variable_indices: pd.Index, - variable_bound: float, - solve_status: mb.SolveStatus, - set_objective: bool, - is_maximize: bool, - objective_expression: Callable[[pd.Series], float], - ): - model = self._create_model( - variable_indices=variable_indices, - variable_bound=variable_bound, - is_integer=solver["is_integer"], - solve_status=solve_status, - set_objective=set_objective, - is_maximize=is_maximize, - objective_expression=objective_expression, - ) - model_solver = mb.Solver(solver["name"]) - if not model_solver.solver_is_supported(): - print(f'Solver {solver["name"]} is not supported') - return - if solver.get("solver_specific_parameters"): - model_solver.set_solver_specific_parameters( - solver.get("solver_specific_parameters") - ) - got_solve_status = model_solver.solve(model) - - # pylint: disable=g-explicit-length-test - # (we disable explicit-length-test here because `variable_indices: pd.Index` - # evaluates to an ambiguous boolean value.) - if len(variable_indices) > 0: # Test cases with >=1 variable. - self.assertNotEmpty(variable_indices) - if ( - isinstance( - objective_expression(model.get_variables()), - (int, float), - ) - and solve_status != mb.SolveStatus.INFEASIBLE - ): - # Feasibility implies optimality when objective is a constant term. - self.assertEqual(got_solve_status, mb.SolveStatus.OPTIMAL) - elif not set_objective and solve_status != mb.SolveStatus.INFEASIBLE: - # Feasibility implies optimality when objective is not set. - self.assertEqual(got_solve_status, mb.SolveStatus.OPTIMAL) - elif solver["name"] == "sat" and got_solve_status == 8: - # CP_SAT returns status=8 for some infeasible and unbounded cases. - self.assertIn( - solve_status, - (mb.SolveStatus.INFEASIBLE, mb.SolveStatus.UNBOUNDED), - ) - elif ( - solver["name"] == "highs" - and got_solve_status == mb.SolveStatus.INFEASIBLE - and solve_status == mb.SolveStatus.UNBOUNDED - ): - # Highs is can return INFEASIBLE when UNBOUNDED is expected. - pass - else: - self.assertEqual(got_solve_status, solve_status) - elif solve_status == mb.SolveStatus.UNBOUNDED: - # Unbounded problems are optimal when there are no variables. - self.assertEqual(got_solve_status, mb.SolveStatus.OPTIMAL) - else: - self.assertEqual(got_solve_status, solve_status) - - @parameterized.product( - solver=_solvers, - variable_indices=_variable_indices, - variable_bound=_variable_bounds, - solve_status=_solve_statuses, - set_objective=_set_objectives, - is_maximize=_objective_senses, - objective_expression=_objective_expressions, + model_solver = mb.Solver(solver["name"]) + if not model_solver.solver_is_supported(): + print(f'Solver {solver["name"]} is not supported') + return + if solver.get("solver_specific_parameters"): + model_solver.set_solver_specific_parameters( + solver.get("solver_specific_parameters") + ) + got_solve_status = model_solver.solve(model) + variables = model.get_variables() + variable_values = model_solver.values(variables) + # Test the type of `variable_values` (we always get pd.Series) + self.assertIsInstance(variable_values, pd.Series) + # Test the index of `variable_values` (match the input variables [if any]) + self.assertSequenceAlmostEqual( + variable_values.index, + mb._get_index(model._get_variables(variables)), ) - def test_get_variable_values( - self, - solver: Dict[str, Union[str, Mapping[str, Any], bool]], - variable_indices: pd.Index, - variable_bound: float, - solve_status: mb.SolveStatus, - set_objective: bool, - is_maximize: bool, - objective_expression: Callable[[pd.Series], float], + if got_solve_status not in ( + mb.SolveStatus.OPTIMAL, + mb.SolveStatus.FEASIBLE, + ): + # self.assertSequenceAlmostEqual does not work here because we cannot do + # equality comparison for NA values (NAs will propagate and we will get + # 'TypeError: boolean value of NA is ambiguous') + for variable_value in variable_values: + self.assertTrue(pd.isna(variable_value)) + elif set_objective and not isinstance( + objective_expression(model.get_variables()), + (int, float), ): - model = self._create_model( - variable_indices=variable_indices, - variable_bound=variable_bound, - is_integer=solver["is_integer"], - solve_status=solve_status, - set_objective=set_objective, - is_maximize=is_maximize, - objective_expression=objective_expression, + # The variable values are only well-defined when the objective is set + # and depends on the variable(s). + if not solver["is_integer"]: + self.assertSequenceAlmostEqual( + variable_values, [variable_bound] * len(variable_values) ) - model_solver = mb.Solver(solver["name"]) - if not model_solver.solver_is_supported(): - print(f'Solver {solver["name"]} is not supported') - return - if solver.get("solver_specific_parameters"): - model_solver.set_solver_specific_parameters( - solver.get("solver_specific_parameters") - ) - got_solve_status = model_solver.solve(model) - variables = model.get_variables() - variable_values = model_solver.values(variables) - # Test the type of `variable_values` (we always get pd.Series) - self.assertIsInstance(variable_values, pd.Series) - # Test the index of `variable_values` (match the input variables [if any]) + elif is_maximize: + self.assertTrue(solver["is_integer"]) # Assert a known assumption. self.assertSequenceAlmostEqual( - variable_values.index, - mb._get_index(model._get_variables(variables)), + variable_values, + [math.floor(variable_bound)] * len(variable_values), + ) + else: + self.assertTrue(solver["is_integer"]) # Assert a known assumption. + self.assertSequenceAlmostEqual( + variable_values, + [math.ceil(variable_bound)] * len(variable_values), ) - if got_solve_status not in ( - mb.SolveStatus.OPTIMAL, - mb.SolveStatus.FEASIBLE, - ): - # self.assertSequenceAlmostEqual does not work here because we cannot do - # equality comparison for NA values (NAs will propagate and we will get - # 'TypeError: boolean value of NA is ambiguous') - for variable_value in variable_values: - self.assertTrue(pd.isna(variable_value)) - elif set_objective and not isinstance( - objective_expression(model.get_variables()), - (int, float), - ): - # The variable values are only well-defined when the objective is set - # and depends on the variable(s). - if not solver["is_integer"]: - self.assertSequenceAlmostEqual( - variable_values, [variable_bound] * len(variable_values) - ) - elif is_maximize: - self.assertTrue(solver["is_integer"]) # Assert a known assumption. - self.assertSequenceAlmostEqual( - variable_values, - [math.floor(variable_bound)] * len(variable_values), - ) - else: - self.assertTrue(solver["is_integer"]) # Assert a known assumption. - self.assertSequenceAlmostEqual( - variable_values, - [math.ceil(variable_bound)] * len(variable_values), - ) - @parameterized.product( - solver=_solvers, - variable_indices=_variable_indices, - variable_bound=_variable_bounds, - solve_status=_solve_statuses, - set_objective=_set_objectives, - is_maximize=_objective_senses, - objective_expression=_objective_expressions, + @parameterized.product( + solver=_solvers, + variable_indices=_variable_indices, + variable_bound=_variable_bounds, + solve_status=_solve_statuses, + set_objective=_set_objectives, + is_maximize=_objective_senses, + objective_expression=_objective_expressions, + ) + def test_get_objective_value( + self, + solver: Dict[str, Union[str, Mapping[str, Any], bool]], + variable_indices: pd.Index, + variable_bound: float, + solve_status: mb.SolveStatus, + set_objective: bool, + is_maximize: bool, + objective_expression: Callable[[pd.Series], float], + ): + model = self._create_model( + variable_indices=variable_indices, + variable_bound=variable_bound, + is_integer=solver["is_integer"], + solve_status=solve_status, + set_objective=set_objective, + is_maximize=is_maximize, + objective_expression=objective_expression, ) - def test_get_objective_value( - self, - solver: Dict[str, Union[str, Mapping[str, Any], bool]], - variable_indices: pd.Index, - variable_bound: float, - solve_status: mb.SolveStatus, - set_objective: bool, - is_maximize: bool, - objective_expression: Callable[[pd.Series], float], - ): - model = self._create_model( - variable_indices=variable_indices, - variable_bound=variable_bound, - is_integer=solver["is_integer"], - solve_status=solve_status, - set_objective=set_objective, - is_maximize=is_maximize, - objective_expression=objective_expression, - ) - model_solver = mb.Solver(solver["name"]) - if not model_solver.solver_is_supported(): - print(f'Solver {solver["name"]} is not supported') - return - if solver.get("solver_specific_parameters"): - model_solver.set_solver_specific_parameters( - solver.get("solver_specific_parameters") - ) - got_status = model_solver.solve(model) - - # Test objective value - if got_status not in (mb.SolveStatus.OPTIMAL, mb.SolveStatus.FEASIBLE): - self.assertTrue(pd.isna(model_solver.objective_value)) - return - if set_objective: - variable_values = model_solver.values(model.get_variables()) - self.assertAlmostEqual( - model_solver.objective_value, - objective_expression(variable_values), - ) - else: - self.assertAlmostEqual(model_solver.objective_value, 0) + model_solver = mb.Solver(solver["name"]) + if not model_solver.solver_is_supported(): + print(f'Solver {solver["name"]} is not supported') + return + if solver.get("solver_specific_parameters"): + model_solver.set_solver_specific_parameters( + solver.get("solver_specific_parameters") + ) + got_status = model_solver.solve(model) + + # Test objective value + if got_status not in (mb.SolveStatus.OPTIMAL, mb.SolveStatus.FEASIBLE): + self.assertTrue(pd.isna(model_solver.objective_value)) + return + if set_objective: + variable_values = model_solver.values(model.get_variables()) + self.assertAlmostEqual( + model_solver.objective_value, + objective_expression(variable_values), + ) + else: + self.assertAlmostEqual(model_solver.objective_value, 0) class ModelBuilderExamplesTest(absltest.TestCase): - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() - - def test_simple_problem(self): - # max 5x1 + 4x2 + 3x3 - # s.t 2x1 + 3x2 + x3 <= 5 - # 4x1 + x2 + 2x3 <= 11 - # 3x1 + 4x2 + 2x3 <= 8 - # x1, x2, x3 >= 0 - # Values = (2,0,1) - # Reduced Costs = (0,-3,0) - model = mb.Model() - x = model.new_var_series( - "x", pd.Index(range(3)), lower_bounds=0, is_integral=True - ) - self.assertLen(model.get_variables(), 3) - model.maximize(x.dot([5, 4, 3])) - model.add(x.dot([2, 3, 1]) <= 5) - model.add(x.dot([4, 1, 2]) <= 11) - model.add(x.dot([3, 4, 2]) <= 8) - self.assertLen(model.get_linear_constraints(), 3) - solver = mb.Solver("glop") - test_red_cost = solver.reduced_costs(model.get_variables()) - test_dual_values = solver.dual_values(model.get_variables()) - self.assertLen(test_red_cost, 3) - self.assertLen(test_dual_values, 3) - for reduced_cost in test_red_cost: - self.assertTrue(pd.isna(reduced_cost)) - for dual_value in test_dual_values: - self.assertTrue(pd.isna(dual_value)) - run = solver.solve(model) - self.assertEqual(run, mb.SolveStatus.OPTIMAL) - i = solver.values(model.get_variables()) - self.assertSequenceAlmostEqual(i, [2, 0, 1]) - red_cost = solver.reduced_costs(model.get_variables()) - dual_val = solver.dual_values(model.get_linear_constraints()) - self.assertSequenceAlmostEqual(red_cost, [0, -3, 0]) - self.assertSequenceAlmostEqual(dual_val, [1, 0, 1]) - self.assertAlmostEqual(2, solver.value(x[0])) - self.assertAlmostEqual(0, solver.reduced_cost((x[0]))) - self.assertAlmostEqual(-3, solver.reduced_cost((x[1]))) - self.assertAlmostEqual(0, solver.reduced_cost((x[2]))) - self.assertAlmostEqual(1, solver.dual_value((x[0]))) - self.assertAlmostEqual(0, solver.dual_value((x[1]))) - self.assertAlmostEqual(1, solver.dual_value((x[2]))) - - def test_graph_k_color(self): - # Assign a color to each vertex of graph, s.t that no two adjacent - # vertices share the same color. Assume N vertices, and max number of - # colors is K - # Consider graph with edges: - # Edge 1: (0,1) - # Edge 2: (0,2) - # Edge 3: (1,3) - # Trying to color graph with at most 3 colors (0, 1, 2) - # Two sets of variables: - # x - pandas series representing coloring status of nodes - # y - pandas series indicating whether color has been used - # Min: y0 + y1 + y2 - # s.t: Every vertex must be assigned exactly one color - # if two vertices are adjacent they cannot have the same color - - model = mb.Model() - num_colors = 3 - num_nodes = 4 - x = model.new_var_series( - "x", - pd.MultiIndex.from_product( - (range(num_nodes), range(num_colors)), - names=["node", "color"], - ), - lower_bounds=0, - upper_bounds=1, - is_integral=True, - ) - y = model.new_var_series( - "y", - pd.Index(range(num_colors), name="color"), - lower_bounds=0, - upper_bounds=1, - is_integral=True, - ) - model.minimize(y.dot([1, 1, 1])) - # Every vertex must be assigned exactly one color - model.add(x.groupby("node").sum().apply(lambda expr: expr == 1)) - # If a vertex i is assigned to a color j then color j is used: - # namely, we re-arrange the terms to express: "x - y <= 0". - model.add(x.sub(y, fill_value=0).apply(lambda expr: expr <= 0)) - # if two vertices are adjacent they cannot have the same color j - for j in range(num_colors): - model.add(x[0, j] + x[1, j] <= 1) - model.add(x[0, j] + x[2, j] <= 1) - model.add(x[1, j] + x[3, j] <= 1) - solver = mb.Solver("sat") - run = solver.solve(model) - self.assertEqual(run, mb.SolveStatus.OPTIMAL) - self.assertEqual(solver.objective_value, 2) - - def test_knapsack_problem(self): - # Basic Knapsack Problem: Given N items, - # with N different weights and values, find the maximum possible value of - # the Items while meeting a weight requirement - # We have 3 items: - # Item x1: Weight = 10, Value = 60 - # Item x2: Weight = 20, Value = 100 - # Item x3: Weight = 30, Value = 120 - # Max: 60x1 + 100x2 + 120x3 - # s.t: 10x1 + 20x2 + 30x3 <= 50 - model = mb.Model() - x = model.new_bool_var_series("x", pd.Index(range(3))) - self.assertLen(model.get_variables(), 3) - model.maximize(x.dot([60, 100, 120])) - model.add(x.dot([10, 20, 30]) <= 50) - self.assertLen(model.get_linear_constraints(), 1) - solver = mb.Solver("sat") - run = solver.solve(model) - self.assertEqual(run, mb.SolveStatus.OPTIMAL) - i = solver.values(model.get_variables()) - self.assertEqual(solver.objective_value, 220) - self.assertSequenceAlmostEqual(i, [0, 1, 1]) - - def test_max_flow_problem(self): - # Testing max flow problem with 8 nodes - # 0-8, source 0, target 8 - # Edges: - # (0,1), (0,2), (0,3), (1,4), (2,4),(2,5), (2,6), (3,5), (4,7), (5,7), (6,7) - # Variables: flow_var- pandas series of flows, indexed by edges - # Max: flow[(0,1)] + flow[(0,2)] + flow[(0,3)] - # S.t: flow[(0,1)] <= 3 - # flow[(0,2)] <= 2 - # flow[(0,3)] <= 2 - # flow[(1,4)] <= 5 - # flow[(1,5)] <= 1 - # flow[(2,4)] <= 1 - # flow[(2,5)] <= 3 - # flow[(2,6)] <= 1 - # flow[(3,5)] <= 1 - # flow[(4,7)] <= 4 - # flow[(5,7)] <= 2 - # flow[(6,7)] <= 4 - # Flow conservation constraints: - # flow[(0,1)] = flow[(1,4)] + flow[(1,5)] - # flow[(0,2)] = flow[(2,4)] + flow[(2,5)] + flow[(2,6)] - # flow[(0,3)] = flow[(3,5)] - # flow[(1,4)] + flow[(2,4)] = flow[(4,7)] - # flow[(1,5)] + flow[(2,5)] + flow[(3,5)] = X[(5,7)] - # flow[(2,6)] = flow[(6,7)] - - model = mb.Model() - nodes = [1, 2, 3, 4, 5, 6] - edge_capacities = pd.Series( - { - (0, 1): 3, - (0, 2): 2, - (0, 3): 2, - (1, 4): 5, - (1, 5): 1, - (2, 4): 1, - (2, 5): 3, - (2, 6): 1, - (3, 5): 1, - (4, 7): 4, - (5, 7): 2, - (6, 7): 4, - } - ) - flow_var = model.new_var_series( - "flow_var", - pd.MultiIndex.from_tuples( - edge_capacities.index, names=("source", "target") - ), - lower_bounds=0, - is_integral=True, - ) - self.assertLen(model.get_variables(), 12) - model.maximize(flow_var[0, :].sum()) - model.add( - (flow_var - edge_capacities).apply(lambda expr: expr <= 0), - name="capacity_constraint", - ) - for node in nodes: - # must specify constraint name when directly comparing two variables - model.add( - flow_var.xs(node, level=0).sum() == flow_var.xs(node, level=1).sum(), - name="flow_conservation", - ) - solver = mb.Solver("sat") - run = solver.solve(model) - self.assertEqual(run, mb.SolveStatus.OPTIMAL) - self.assertEqual(solver.objective_value, 6) - - def test_add_enforced(self): - model = mb.Model() - x = model.new_int_var(0, 10, "x") - y = model.new_int_var(0, 10, "y") - z = model.new_bool_var("z") - ct = model.add_enforced(x + 2 * y >= 10, z, False) - self.assertEqual(ct.lower_bound, 10.0) - self.assertEqual(z.index, ct.indicator_variable.index) - self.assertFalse(ct.indicator_value) - - def testInPlaceSumModifications(self) -> None: - model = mb.Model() - x = [model.new_int_var(0, 10, f"x{i}") for i in range(5)] - y = [model.new_int_var(0, 10, f"y{i}") for i in range(5)] - e1 = sum(x) - self.assertIsInstance(e1, mbh.SumArray) - self.assertEqual(e1.offset, 0) - self.assertEqual(e1.num_exprs, 5) - e1_str = str(e1) - _ = e1 + y[0] - _ = sum(y) + e1 - self.assertEqual(e1_str, str(e1)) - - e2 = sum(x) - 2 - y[0] - 0.1 - e2_str = str(e2) - self.assertIsInstance(e2, mbh.SumArray) - self.assertEqual(e2.offset, -2.1) - self.assertEqual(e2.num_exprs, 6) - _ = e2 + 2.5 - self.assertEqual(str(e2), e2_str) - - e3 = 1.2 + sum(x) + 0.3 - self.assertIsInstance(e3, mbh.SumArray) - self.assertEqual(e3.offset, 1.5) - self.assertEqual(e3.num_exprs, 5) - - def testLargeSum(self) -> None: - model = mb.Model() - x = [model.new_int_var(0, 10, f"x{i}") for i in range(100000)] - model.add(sum(x) == 10) - - def testSimplification1(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = (x * 2) * 2 - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(4, prod.coefficient) - self.assertEqual(0, prod.offset) - - def testSimplification2(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (x * 2) - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(4, prod.coefficient) - self.assertEqual(0, prod.offset) - - def testSimplification3(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = (2 * x) * 2 - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(4, prod.coefficient) - self.assertEqual(0, prod.offset) - - def testSimplification4(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (2 * x) - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(4, prod.coefficient) - self.assertEqual(0, prod.offset) - - def testSimplification5(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (x + 1) - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(2, prod.coefficient) - self.assertEqual(2, prod.offset) - - def testSimplification6(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = (x + 1) * 2 - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(2, prod.coefficient) - self.assertEqual(2, prod.offset) - - def testSimplification7(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (x - 1) - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(2, prod.coefficient) - self.assertEqual(-2, prod.offset) - - def testSimplification8(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = (x - 1) * 2 - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(2, prod.coefficient) - self.assertEqual(-2, prod.offset) - - def testSimplification9(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (1 - x) - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(-2, prod.coefficient) - self.assertEqual(2, prod.offset) - - def testSimplification10(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod = (1 - x) * 2 - self.assertIsInstance(prod, mbh.AffineExpr) - self.assertEqual(x, prod.expression) - self.assertEqual(-2, prod.coefficient) - self.assertEqual(2, prod.offset) - - def testSimplification11(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - prod = (x + y) * 0 - self.assertEqual(repr(prod), "FixedValue(0)") - s = x + y - self.assertIs(s, s + 0.0) - self.assertIs(s, s - 0.0) - - affine = x + 2 - self.assertIs(affine, affine + 0.0) - self.assertIs(affine, affine - 0.0) - self.assertIs(affine, affine * 1.0) - - def testSimplification12(self): - model = mb.Model() - x = model.new_int_var(-10, 10, "x") - prod1 = 2 * x - 0.0 - self.assertEqual( - repr(prod1), - "AffineExpr(expr=Variable(index=0, lb=-10, ub=10, is_integral=1," - " name='x'), coeff=2, offset=0)", - ) + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() + + def test_simple_problem(self): + # max 5x1 + 4x2 + 3x3 + # s.t 2x1 + 3x2 + x3 <= 5 + # 4x1 + x2 + 2x3 <= 11 + # 3x1 + 4x2 + 2x3 <= 8 + # x1, x2, x3 >= 0 + # Values = (2,0,1) + # Reduced Costs = (0,-3,0) + model = mb.Model() + x = model.new_var_series( + "x", pd.Index(range(3)), lower_bounds=0, is_integral=True + ) + self.assertLen(model.get_variables(), 3) + model.maximize(x.dot([5, 4, 3])) + model.add(x.dot([2, 3, 1]) <= 5) + model.add(x.dot([4, 1, 2]) <= 11) + model.add(x.dot([3, 4, 2]) <= 8) + self.assertLen(model.get_linear_constraints(), 3) + solver = mb.Solver("glop") + test_red_cost = solver.reduced_costs(model.get_variables()) + test_dual_values = solver.dual_values(model.get_variables()) + self.assertLen(test_red_cost, 3) + self.assertLen(test_dual_values, 3) + for reduced_cost in test_red_cost: + self.assertTrue(pd.isna(reduced_cost)) + for dual_value in test_dual_values: + self.assertTrue(pd.isna(dual_value)) + run = solver.solve(model) + self.assertEqual(run, mb.SolveStatus.OPTIMAL) + i = solver.values(model.get_variables()) + self.assertSequenceAlmostEqual(i, [2, 0, 1]) + red_cost = solver.reduced_costs(model.get_variables()) + dual_val = solver.dual_values(model.get_linear_constraints()) + self.assertSequenceAlmostEqual(red_cost, [0, -3, 0]) + self.assertSequenceAlmostEqual(dual_val, [1, 0, 1]) + self.assertAlmostEqual(2, solver.value(x[0])) + self.assertAlmostEqual(0, solver.reduced_cost((x[0]))) + self.assertAlmostEqual(-3, solver.reduced_cost((x[1]))) + self.assertAlmostEqual(0, solver.reduced_cost((x[2]))) + self.assertAlmostEqual(1, solver.dual_value((x[0]))) + self.assertAlmostEqual(0, solver.dual_value((x[1]))) + self.assertAlmostEqual(1, solver.dual_value((x[2]))) + + def test_graph_k_color(self): + # Assign a color to each vertex of graph, s.t that no two adjacent + # vertices share the same color. Assume N vertices, and max number of + # colors is K + # Consider graph with edges: + # Edge 1: (0,1) + # Edge 2: (0,2) + # Edge 3: (1,3) + # Trying to color graph with at most 3 colors (0, 1, 2) + # Two sets of variables: + # x - pandas series representing coloring status of nodes + # y - pandas series indicating whether color has been used + # Min: y0 + y1 + y2 + # s.t: Every vertex must be assigned exactly one color + # if two vertices are adjacent they cannot have the same color + + model = mb.Model() + num_colors = 3 + num_nodes = 4 + x = model.new_var_series( + "x", + pd.MultiIndex.from_product( + (range(num_nodes), range(num_colors)), + names=["node", "color"], + ), + lower_bounds=0, + upper_bounds=1, + is_integral=True, + ) + y = model.new_var_series( + "y", + pd.Index(range(num_colors), name="color"), + lower_bounds=0, + upper_bounds=1, + is_integral=True, + ) + model.minimize(y.dot([1, 1, 1])) + # Every vertex must be assigned exactly one color + model.add(x.groupby("node").sum().apply(lambda expr: expr == 1)) + # If a vertex i is assigned to a color j then color j is used: + # namely, we re-arrange the terms to express: "x - y <= 0". + model.add(x.sub(y, fill_value=0).apply(lambda expr: expr <= 0)) + # if two vertices are adjacent they cannot have the same color j + for j in range(num_colors): + model.add(x[0, j] + x[1, j] <= 1) + model.add(x[0, j] + x[2, j] <= 1) + model.add(x[1, j] + x[3, j] <= 1) + solver = mb.Solver("sat") + run = solver.solve(model) + self.assertEqual(run, mb.SolveStatus.OPTIMAL) + self.assertEqual(solver.objective_value, 2) + + def test_knapsack_problem(self): + # Basic Knapsack Problem: Given N items, + # with N different weights and values, find the maximum possible value of + # the Items while meeting a weight requirement + # We have 3 items: + # Item x1: Weight = 10, Value = 60 + # Item x2: Weight = 20, Value = 100 + # Item x3: Weight = 30, Value = 120 + # Max: 60x1 + 100x2 + 120x3 + # s.t: 10x1 + 20x2 + 30x3 <= 50 + model = mb.Model() + x = model.new_bool_var_series("x", pd.Index(range(3))) + self.assertLen(model.get_variables(), 3) + model.maximize(x.dot([60, 100, 120])) + model.add(x.dot([10, 20, 30]) <= 50) + self.assertLen(model.get_linear_constraints(), 1) + solver = mb.Solver("sat") + run = solver.solve(model) + self.assertEqual(run, mb.SolveStatus.OPTIMAL) + i = solver.values(model.get_variables()) + self.assertEqual(solver.objective_value, 220) + self.assertSequenceAlmostEqual(i, [0, 1, 1]) + + def test_max_flow_problem(self): + # Testing max flow problem with 8 nodes + # 0-8, source 0, target 8 + # Edges: + # (0,1), (0,2), (0,3), (1,4), (2,4),(2,5), (2,6), (3,5), (4,7), (5,7), (6,7) + # Variables: flow_var- pandas series of flows, indexed by edges + # Max: flow[(0,1)] + flow[(0,2)] + flow[(0,3)] + # S.t: flow[(0,1)] <= 3 + # flow[(0,2)] <= 2 + # flow[(0,3)] <= 2 + # flow[(1,4)] <= 5 + # flow[(1,5)] <= 1 + # flow[(2,4)] <= 1 + # flow[(2,5)] <= 3 + # flow[(2,6)] <= 1 + # flow[(3,5)] <= 1 + # flow[(4,7)] <= 4 + # flow[(5,7)] <= 2 + # flow[(6,7)] <= 4 + # Flow conservation constraints: + # flow[(0,1)] = flow[(1,4)] + flow[(1,5)] + # flow[(0,2)] = flow[(2,4)] + flow[(2,5)] + flow[(2,6)] + # flow[(0,3)] = flow[(3,5)] + # flow[(1,4)] + flow[(2,4)] = flow[(4,7)] + # flow[(1,5)] + flow[(2,5)] + flow[(3,5)] = X[(5,7)] + # flow[(2,6)] = flow[(6,7)] + + model = mb.Model() + nodes = [1, 2, 3, 4, 5, 6] + edge_capacities = pd.Series({ + (0, 1): 3, + (0, 2): 2, + (0, 3): 2, + (1, 4): 5, + (1, 5): 1, + (2, 4): 1, + (2, 5): 3, + (2, 6): 1, + (3, 5): 1, + (4, 7): 4, + (5, 7): 2, + (6, 7): 4, + }) + flow_var = model.new_var_series( + "flow_var", + pd.MultiIndex.from_tuples( + edge_capacities.index, names=("source", "target") + ), + lower_bounds=0, + is_integral=True, + ) + self.assertLen(model.get_variables(), 12) + model.maximize(flow_var[0, :].sum()) + model.add( + (flow_var - edge_capacities).apply(lambda expr: expr <= 0), + name="capacity_constraint", + ) + for node in nodes: + # must specify constraint name when directly comparing two variables + model.add( + flow_var.xs(node, level=0).sum() == flow_var.xs(node, level=1).sum(), + name="flow_conservation", + ) + solver = mb.Solver("sat") + run = solver.solve(model) + self.assertEqual(run, mb.SolveStatus.OPTIMAL) + self.assertEqual(solver.objective_value, 6) + + def test_add_enforced(self): + model = mb.Model() + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(0, 10, "y") + z = model.new_bool_var("z") + ct = model.add_enforced(x + 2 * y >= 10, z, False) + self.assertEqual(ct.lower_bound, 10.0) + self.assertEqual(z.index, ct.indicator_variable.index) + self.assertFalse(ct.indicator_value) + + def testInPlaceSumModifications(self) -> None: + model = mb.Model() + x = [model.new_int_var(0, 10, f"x{i}") for i in range(5)] + y = [model.new_int_var(0, 10, f"y{i}") for i in range(5)] + e1 = sum(x) + self.assertIsInstance(e1, mbh.SumArray) + self.assertEqual(e1.offset, 0) + self.assertEqual(e1.num_exprs, 5) + e1_str = str(e1) + _ = e1 + y[0] + _ = sum(y) + e1 + self.assertEqual(e1_str, str(e1)) + + e2 = sum(x) - 2 - y[0] - 0.1 + e2_str = str(e2) + self.assertIsInstance(e2, mbh.SumArray) + self.assertEqual(e2.offset, -2.1) + self.assertEqual(e2.num_exprs, 6) + _ = e2 + 2.5 + self.assertEqual(str(e2), e2_str) + + e3 = 1.2 + sum(x) + 0.3 + self.assertIsInstance(e3, mbh.SumArray) + self.assertEqual(e3.offset, 1.5) + self.assertEqual(e3.num_exprs, 5) + + def testLargeSum(self) -> None: + model = mb.Model() + x = [model.new_int_var(0, 10, f"x{i}") for i in range(100000)] + model.add(sum(x) == 10) + + def testSimplification1(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = (x * 2) * 2 + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(4, prod.coefficient) + self.assertEqual(0, prod.offset) + + def testSimplification2(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (x * 2) + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(4, prod.coefficient) + self.assertEqual(0, prod.offset) + + def testSimplification3(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = (2 * x) * 2 + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(4, prod.coefficient) + self.assertEqual(0, prod.offset) + + def testSimplification4(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (2 * x) + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(4, prod.coefficient) + self.assertEqual(0, prod.offset) + + def testSimplification5(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (x + 1) + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(2, prod.coefficient) + self.assertEqual(2, prod.offset) + + def testSimplification6(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = (x + 1) * 2 + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(2, prod.coefficient) + self.assertEqual(2, prod.offset) + + def testSimplification7(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (x - 1) + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(2, prod.coefficient) + self.assertEqual(-2, prod.offset) + + def testSimplification8(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = (x - 1) * 2 + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(2, prod.coefficient) + self.assertEqual(-2, prod.offset) + + def testSimplification9(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (1 - x) + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(-2, prod.coefficient) + self.assertEqual(2, prod.offset) + + def testSimplification10(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod = (1 - x) * 2 + self.assertIsInstance(prod, mbh.AffineExpr) + self.assertEqual(x, prod.expression) + self.assertEqual(-2, prod.coefficient) + self.assertEqual(2, prod.offset) + + def testSimplification11(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + prod = (x + y) * 0 + self.assertEqual(repr(prod), "FixedValue(0)") + s = x + y + self.assertIs(s, s + 0.0) + self.assertIs(s, s - 0.0) + + affine = x + 2 + self.assertIs(affine, affine + 0.0) + self.assertIs(affine, affine - 0.0) + self.assertIs(affine, affine * 1.0) + + def testSimplification12(self): + model = mb.Model() + x = model.new_int_var(-10, 10, "x") + prod1 = 2 * x - 0.0 + self.assertEqual( + repr(prod1), + "AffineExpr(expr=Variable(index=0, lb=-10, ub=10, is_integral=1," + " name='x'), coeff=2, offset=0)", + ) - prod2 = 2 * x - 1.0 - self.assertEqual( - repr(prod2), - "AffineExpr(expr=Variable(index=0, lb=-10, ub=10, is_integral=1," - " name='x'), coeff=2, offset=-1)", - ) + prod2 = 2 * x - 1.0 + self.assertEqual( + repr(prod2), + "AffineExpr(expr=Variable(index=0, lb=-10, ub=10, is_integral=1," + " name='x'), coeff=2, offset=-1)", + ) - prod3 = 2 * x * 0.0 - self.assertEqual(repr(prod3), "FixedValue(0)") + prod3 = 2 * x * 0.0 + self.assertEqual(repr(prod3), "FixedValue(0)") if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/linear_solver/python/pywraplp_test.py b/ortools/linear_solver/python/pywraplp_test.py index 1f98234375a..c0b3cbb7fae 100644 --- a/ortools/linear_solver/python/pywraplp_test.py +++ b/ortools/linear_solver/python/pywraplp_test.py @@ -43,57 +43,57 @@ class PyWrapLp(unittest.TestCase): - def test_proto(self): - input_proto = linear_solver_pb2.MPModelProto() - text_format.Merge(TEXT_MODEL, input_proto) - solver = pywraplp.Solver.CreateSolver("CBC") - if not solver: - return - # For now, create the model from the proto by parsing the proto - errors = solver.LoadModelFromProto(input_proto) - self.assertFalse(errors) - solver.Solve() - # Fill solution - solution = linear_solver_pb2.MPSolutionResponse() - solver.FillSolutionResponseProto(solution) - self.assertEqual(solution.objective_value, 3.0) - self.assertEqual(solution.variable_value[0], 1.0) - self.assertEqual(solution.variable_value[1], 1.0) - self.assertEqual(solution.best_objective_bound, 3.0) + def test_proto(self): + input_proto = linear_solver_pb2.MPModelProto() + text_format.Merge(TEXT_MODEL, input_proto) + solver = pywraplp.Solver.CreateSolver("CBC") + if not solver: + return + # For now, create the model from the proto by parsing the proto + errors = solver.LoadModelFromProto(input_proto) + self.assertFalse(errors) + solver.Solve() + # Fill solution + solution = linear_solver_pb2.MPSolutionResponse() + solver.FillSolutionResponseProto(solution) + self.assertEqual(solution.objective_value, 3.0) + self.assertEqual(solution.variable_value[0], 1.0) + self.assertEqual(solution.variable_value[1], 1.0) + self.assertEqual(solution.best_objective_bound, 3.0) - def test_external_api(self): - solver = pywraplp.Solver.CreateSolver("GLOP") - infinity = solver.Infinity() - infinity2 = solver.infinity() - self.assertEqual(infinity, infinity2) - # x1, x2 and x3 are continuous non-negative variables. - x1 = solver.NumVar(0.0, infinity, "x1") - x2 = solver.NumVar(0.0, infinity, "x2") - x3 = solver.NumVar(0.0, infinity, "x3") - self.assertEqual(x1.Lb(), 0) - self.assertEqual(x1.Ub(), infinity) - self.assertFalse(x1.Integer()) - solver.Maximize(10 * x1 + 6 * x2 + 4 * x3 + 5) - self.assertEqual(solver.Objective().Offset(), 5) - c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, "ConstraintName0") - c1 = solver.Add(2 * x1 + 2 * x2 + 6 * x3 <= 300) - sum_of_vars = sum([x1, x2, x3]) - solver.Add(sum_of_vars <= 100.0, "OtherConstraintName") - self.assertEqual(c1.Lb(), -infinity) - self.assertEqual(c1.Ub(), 300) - c1.SetLb(-100000) - self.assertEqual(c1.Lb(), -100000) - c1.SetUb(301) - self.assertEqual(c1.Ub(), 301) + def test_external_api(self): + solver = pywraplp.Solver.CreateSolver("GLOP") + infinity = solver.Infinity() + infinity2 = solver.infinity() + self.assertEqual(infinity, infinity2) + # x1, x2 and x3 are continuous non-negative variables. + x1 = solver.NumVar(0.0, infinity, "x1") + x2 = solver.NumVar(0.0, infinity, "x2") + x3 = solver.NumVar(0.0, infinity, "x3") + self.assertEqual(x1.Lb(), 0) + self.assertEqual(x1.Ub(), infinity) + self.assertFalse(x1.Integer()) + solver.Maximize(10 * x1 + 6 * x2 + 4 * x3 + 5) + self.assertEqual(solver.Objective().Offset(), 5) + c0 = solver.Add(10 * x1 + 4 * x2 + 5 * x3 <= 600, "ConstraintName0") + c1 = solver.Add(2 * x1 + 2 * x2 + 6 * x3 <= 300) + sum_of_vars = sum([x1, x2, x3]) + solver.Add(sum_of_vars <= 100.0, "OtherConstraintName") + self.assertEqual(c1.Lb(), -infinity) + self.assertEqual(c1.Ub(), 300) + c1.SetLb(-100000) + self.assertEqual(c1.Lb(), -100000) + c1.SetUb(301) + self.assertEqual(c1.Ub(), 301) - solver.SetTimeLimit(10000) - result_status = solver.Solve() + solver.SetTimeLimit(10000) + result_status = solver.Solve() - # The problem has an optimal solution. - self.assertEqual(result_status, pywraplp.Solver.OPTIMAL) - self.assertAlmostEqual(x1.ReducedCost(), 0.0) - self.assertAlmostEqual(c0.DualValue(), 0.6666666666666667) + # The problem has an optimal solution. + self.assertEqual(result_status, pywraplp.Solver.OPTIMAL) + self.assertAlmostEqual(x1.ReducedCost(), 0.0) + self.assertAlmostEqual(c0.DualValue(), 0.6666666666666667) if __name__ == "__main__": - unittest.main() + unittest.main() diff --git a/ortools/linear_solver/python/solve_model.py b/ortools/linear_solver/python/solve_model.py index 5cded45323f..99ba86959e8 100644 --- a/ortools/linear_solver/python/solve_model.py +++ b/ortools/linear_solver/python/solve_model.py @@ -21,42 +21,46 @@ from ortools.linear_solver.python import model_builder _INPUT = flags.DEFINE_string("input", "", "Input file to load and solve.") -_PARAMS = flags.DEFINE_string("params", "", "Solver parameters in string format.") -_SOLVER = flags.DEFINE_string("solver", "sat", "Solver type to solve the model with.") +_PARAMS = flags.DEFINE_string( + "params", "", "Solver parameters in string format." +) +_SOLVER = flags.DEFINE_string( + "solver", "sat", "Solver type to solve the model with." +) def main(argv: Sequence[str]) -> None: - """Load a model and solves it.""" - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") + """Load a model and solves it.""" + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") - model = model_builder.ModelBuilder() + model = model_builder.ModelBuilder() - # Load MPS or proto file. - if _INPUT.value.endswith(".mps"): - if not model.import_from_mps_file(_INPUT.value): - print(f"Cannot import MPS file: '{_INPUT.value}'") - return - elif not model.import_from_proto_file(_INPUT.value): - print(f"Cannot import Proto file: '{_INPUT.value}'") - return + # Load MPS or proto file. + if _INPUT.value.endswith(".mps"): + if not model.import_from_mps_file(_INPUT.value): + print(f"Cannot import MPS file: '{_INPUT.value}'") + return + elif not model.import_from_proto_file(_INPUT.value): + print(f"Cannot import Proto file: '{_INPUT.value}'") + return - # Create solver. - solver = model_builder.ModelSolver(_SOLVER.value) - if not solver.solver_is_supported(): - print(f"Cannot create solver with name '{_SOLVER.value}'") - return + # Create solver. + solver = model_builder.ModelSolver(_SOLVER.value) + if not solver.solver_is_supported(): + print(f"Cannot create solver with name '{_SOLVER.value}'") + return - # Set parameters. - if _PARAMS.value: - solver.set_solver_specific_parameters(_PARAMS.value) + # Set parameters. + if _PARAMS.value: + solver.set_solver_specific_parameters(_PARAMS.value) - # Enable the output of the solver. - solver.enable_output(True) + # Enable the output of the solver. + solver.enable_output(True) - # And solve. - solver.solve(model) + # And solve. + solver.solve(model) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/linear_solver/samples/assignment_groups_mip.py b/ortools/linear_solver/samples/assignment_groups_mip.py index 62e18572f62..34ed286d698 100644 --- a/ortools/linear_solver/samples/assignment_groups_mip.py +++ b/ortools/linear_solver/samples/assignment_groups_mip.py @@ -16,168 +16,172 @@ """Solve assignment problem for given group of workers.""" # [START import] from ortools.linear_solver import pywraplp + # [END import] def main(): - # Data - # [START data] - costs = [ - [90, 76, 75, 70, 50, 74], - [35, 85, 55, 65, 48, 101], - [125, 95, 90, 105, 59, 120], - [45, 110, 95, 115, 104, 83], - [60, 105, 80, 75, 59, 62], - [45, 65, 110, 95, 47, 31], - [38, 51, 107, 41, 69, 99], - [47, 85, 57, 71, 92, 77], - [39, 63, 97, 49, 118, 56], - [47, 101, 71, 60, 88, 109], - [17, 39, 103, 64, 61, 92], - [101, 45, 83, 59, 92, 27], - ] - num_workers = len(costs) - num_tasks = len(costs[0]) - # [END data] - - # Allowed groups of workers: - # [START allowed_groups] - group1 = [ # Subgroups of workers 0 - 3 - [2, 3], - [1, 3], - [1, 2], - [0, 1], - [0, 2], - ] - - group2 = [ # Subgroups of workers 4 - 7 - [6, 7], - [5, 7], - [5, 6], - [4, 5], - [4, 7], - ] - - group3 = [ # Subgroups of workers 8 - 11 - [10, 11], - [9, 11], - [9, 10], - [8, 10], - [8, 11], - ] - # [END allowed_groups] - - # Solver. - # [START solver] - # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver("SCIP") - if not solver: - return - # [END solver] - - # Variables - # [START variables] - # x[worker, task] is an array of 0-1 variables, which will be 1 - # if the worker is assigned to the task. - x = {} - for worker in range(num_workers): - for task in range(num_tasks): - x[worker, task] = solver.BoolVar(f"x[{worker},{task}]") - # [END variables] - - # Constraints - # [START constraints] - # The total size of the tasks each worker takes on is at most total_size_max. - for worker in range(num_workers): - solver.Add(solver.Sum([x[worker, task] for task in range(num_tasks)]) <= 1) - - # Each task is assigned to exactly one worker. + # Data + # [START data] + costs = [ + [90, 76, 75, 70, 50, 74], + [35, 85, 55, 65, 48, 101], + [125, 95, 90, 105, 59, 120], + [45, 110, 95, 115, 104, 83], + [60, 105, 80, 75, 59, 62], + [45, 65, 110, 95, 47, 31], + [38, 51, 107, 41, 69, 99], + [47, 85, 57, 71, 92, 77], + [39, 63, 97, 49, 118, 56], + [47, 101, 71, 60, 88, 109], + [17, 39, 103, 64, 61, 92], + [101, 45, 83, 59, 92, 27], + ] + num_workers = len(costs) + num_tasks = len(costs[0]) + # [END data] + + # Allowed groups of workers: + # [START allowed_groups] + group1 = [ # Subgroups of workers 0 - 3 + [2, 3], + [1, 3], + [1, 2], + [0, 1], + [0, 2], + ] + + group2 = [ # Subgroups of workers 4 - 7 + [6, 7], + [5, 7], + [5, 6], + [4, 5], + [4, 7], + ] + + group3 = [ # Subgroups of workers 8 - 11 + [10, 11], + [9, 11], + [9, 10], + [8, 10], + [8, 11], + ] + # [END allowed_groups] + + # Solver. + # [START solver] + # Create the mip solver with the SCIP backend. + solver = pywraplp.Solver.CreateSolver("SCIP") + if not solver: + return + # [END solver] + + # Variables + # [START variables] + # x[worker, task] is an array of 0-1 variables, which will be 1 + # if the worker is assigned to the task. + x = {} + for worker in range(num_workers): for task in range(num_tasks): - solver.Add(solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1) - # [END constraints] - - # [START assignments] - # Create variables for each worker, indicating whether they work on some task. - work = {} - for worker in range(num_workers): - work[worker] = solver.BoolVar(f"work[{worker}]") - - for worker in range(num_workers): - solver.Add( - work[worker] == solver.Sum([x[worker, task] for task in range(num_tasks)]) - ) - - # Group1 - constraint_g1 = solver.Constraint(1, 1) - for index, _ in enumerate(group1): - # a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1] - # p is True if a AND b, False otherwise - constraint = solver.Constraint(0, 1) - constraint.SetCoefficient(work[group1[index][0]], 1) - constraint.SetCoefficient(work[group1[index][1]], 1) - p = solver.BoolVar(f"g1_p{index}") - constraint.SetCoefficient(p, -2) - - constraint_g1.SetCoefficient(p, 1) - - # Group2 - constraint_g2 = solver.Constraint(1, 1) - for index, _ in enumerate(group2): - # a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1] - # p is True if a AND b, False otherwise - constraint = solver.Constraint(0, 1) - constraint.SetCoefficient(work[group2[index][0]], 1) - constraint.SetCoefficient(work[group2[index][1]], 1) - p = solver.BoolVar(f"g2_p{index}") - constraint.SetCoefficient(p, -2) - - constraint_g2.SetCoefficient(p, 1) - - # Group3 - constraint_g3 = solver.Constraint(1, 1) - for index, _ in enumerate(group3): - # a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1] - # p is True if a AND b, False otherwise - constraint = solver.Constraint(0, 1) - constraint.SetCoefficient(work[group3[index][0]], 1) - constraint.SetCoefficient(work[group3[index][1]], 1) - p = solver.BoolVar(f"g3_p{index}") - constraint.SetCoefficient(p, -2) - - constraint_g3.SetCoefficient(p, 1) - # [END assignments] - - # Objective - # [START objective] - objective_terms = [] + x[worker, task] = solver.BoolVar(f"x[{worker},{task}]") + # [END variables] + + # Constraints + # [START constraints] + # The total size of the tasks each worker takes on is at most total_size_max. + for worker in range(num_workers): + solver.Add(solver.Sum([x[worker, task] for task in range(num_tasks)]) <= 1) + + # Each task is assigned to exactly one worker. + for task in range(num_tasks): + solver.Add( + solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1 + ) + # [END constraints] + + # [START assignments] + # Create variables for each worker, indicating whether they work on some task. + work = {} + for worker in range(num_workers): + work[worker] = solver.BoolVar(f"work[{worker}]") + + for worker in range(num_workers): + solver.Add( + work[worker] + == solver.Sum([x[worker, task] for task in range(num_tasks)]) + ) + + # Group1 + constraint_g1 = solver.Constraint(1, 1) + for index, _ in enumerate(group1): + # a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1] + # p is True if a AND b, False otherwise + constraint = solver.Constraint(0, 1) + constraint.SetCoefficient(work[group1[index][0]], 1) + constraint.SetCoefficient(work[group1[index][1]], 1) + p = solver.BoolVar(f"g1_p{index}") + constraint.SetCoefficient(p, -2) + + constraint_g1.SetCoefficient(p, 1) + + # Group2 + constraint_g2 = solver.Constraint(1, 1) + for index, _ in enumerate(group2): + # a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1] + # p is True if a AND b, False otherwise + constraint = solver.Constraint(0, 1) + constraint.SetCoefficient(work[group2[index][0]], 1) + constraint.SetCoefficient(work[group2[index][1]], 1) + p = solver.BoolVar(f"g2_p{index}") + constraint.SetCoefficient(p, -2) + + constraint_g2.SetCoefficient(p, 1) + + # Group3 + constraint_g3 = solver.Constraint(1, 1) + for index, _ in enumerate(group3): + # a*b can be transformed into 0 <= a + b - 2*p <= 1 with p in [0,1] + # p is True if a AND b, False otherwise + constraint = solver.Constraint(0, 1) + constraint.SetCoefficient(work[group3[index][0]], 1) + constraint.SetCoefficient(work[group3[index][1]], 1) + p = solver.BoolVar(f"g3_p{index}") + constraint.SetCoefficient(p, -2) + + constraint_g3.SetCoefficient(p, 1) + # [END assignments] + + # Objective + # [START objective] + objective_terms = [] + for worker in range(num_workers): + for task in range(num_tasks): + objective_terms.append(costs[worker][task] * x[worker, task]) + solver.Minimize(solver.Sum(objective_terms)) + # [END objective] + + # Solve + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] + + # Print solution. + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: + print(f"Total cost = {solver.Objective().Value()}\n") for worker in range(num_workers): - for task in range(num_tasks): - objective_terms.append(costs[worker][task] * x[worker, task]) - solver.Minimize(solver.Sum(objective_terms)) - # [END objective] - - # Solve - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] - - # Print solution. - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: - print(f"Total cost = {solver.Objective().Value()}\n") - for worker in range(num_workers): - for task in range(num_tasks): - if x[worker, task].solution_value() > 0.5: - print( - f"Worker {worker} assigned to task {task}." - + f" Cost: {costs[worker][task]}" - ) - else: - print("No solution found.") - # [END print_solution] + for task in range(num_tasks): + if x[worker, task].solution_value() > 0.5: + print( + f"Worker {worker} assigned to task {task}." + + f" Cost: {costs[worker][task]}" + ) + else: + print("No solution found.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/assignment_mb.py b/ortools/linear_solver/samples/assignment_mb.py index d366b204bde..cb70b436560 100644 --- a/ortools/linear_solver/samples/assignment_mb.py +++ b/ortools/linear_solver/samples/assignment_mb.py @@ -20,13 +20,14 @@ import pandas as pd from ortools.linear_solver.python import model_builder + # [END import] def main(): - # Data - # [START data_model] - data_str = """ + # Data + # [START data_model] + data_str = """ worker task cost w1 t1 90 w1 t2 80 @@ -50,60 +51,60 @@ def main(): w5 t4 100 """ - data = pd.read_table(io.StringIO(data_str), sep=r"\s+") - # [END data_model] - - # Create the model. - # [START model] - model = model_builder.Model() - # [END model] - - # Variables - # [START variables] - # x[i, j] is an array of 0-1 variables, which will be 1 - # if worker i is assigned to task j. - x = model.new_bool_var_series(name="x", index=data.index) - # [END variables] - - # Constraints - # [START constraints] - # Each worker is assigned to at most 1 task. - for unused_name, tasks in data.groupby("worker"): - model.add(x[tasks.index].sum() <= 1) - - # Each task is assigned to exactly one worker. - for unused_name, workers in data.groupby("task"): - model.add(x[workers.index].sum() == 1) - # [END constraints] - - # Objective - # [START objective] - model.minimize(data.cost.dot(x)) - # [END objective] - - # [START solve] - # Create the solver with the CP-SAT backend, and solve the model. - solver = model_builder.Solver("sat") - if not solver.solver_is_supported(): - return - status = solver.solve(model) - # [END solve] - - # Print solution. - # [START print_solution] - if ( - status == model_builder.SolveStatus.OPTIMAL - or status == model_builder.SolveStatus.FEASIBLE - ): - print(f"Total cost = {solver.objective_value}\n") - selected = data.loc[solver.values(x).loc[lambda x: x == 1].index] - for unused_index, row in selected.iterrows(): - print(f"{row.task} assigned to {row.worker} with a cost of {row.cost}") - else: - print("No solution found.") - # [END print_solution] + data = pd.read_table(io.StringIO(data_str), sep=r"\s+") + # [END data_model] + + # Create the model. + # [START model] + model = model_builder.Model() + # [END model] + + # Variables + # [START variables] + # x[i, j] is an array of 0-1 variables, which will be 1 + # if worker i is assigned to task j. + x = model.new_bool_var_series(name="x", index=data.index) + # [END variables] + + # Constraints + # [START constraints] + # Each worker is assigned to at most 1 task. + for unused_name, tasks in data.groupby("worker"): + model.add(x[tasks.index].sum() <= 1) + + # Each task is assigned to exactly one worker. + for unused_name, workers in data.groupby("task"): + model.add(x[workers.index].sum() == 1) + # [END constraints] + + # Objective + # [START objective] + model.minimize(data.cost.dot(x)) + # [END objective] + + # [START solve] + # Create the solver with the CP-SAT backend, and solve the model. + solver = model_builder.Solver("sat") + if not solver.solver_is_supported(): + return + status = solver.solve(model) + # [END solve] + + # Print solution. + # [START print_solution] + if ( + status == model_builder.SolveStatus.OPTIMAL + or status == model_builder.SolveStatus.FEASIBLE + ): + print(f"Total cost = {solver.objective_value}\n") + selected = data.loc[solver.values(x).loc[lambda x: x == 1].index] + for unused_index, row in selected.iterrows(): + print(f"{row.task} assigned to {row.worker} with a cost of {row.cost}") + else: + print("No solution found.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/assignment_mip.py b/ortools/linear_solver/samples/assignment_mip.py index 7f8a3154300..4014e97ac90 100644 --- a/ortools/linear_solver/samples/assignment_mip.py +++ b/ortools/linear_solver/samples/assignment_mip.py @@ -16,82 +16,83 @@ # [START program] # [START import] from ortools.linear_solver import pywraplp + # [END import] def main(): - # Data - # [START data_model] - costs = [ - [90, 80, 75, 70], - [35, 85, 55, 65], - [125, 95, 90, 95], - [45, 110, 95, 115], - [50, 100, 90, 100], - ] - num_workers = len(costs) - num_tasks = len(costs[0]) - # [END data_model] + # Data + # [START data_model] + costs = [ + [90, 80, 75, 70], + [35, 85, 55, 65], + [125, 95, 90, 95], + [45, 110, 95, 115], + [50, 100, 90, 100], + ] + num_workers = len(costs) + num_tasks = len(costs[0]) + # [END data_model] - # Solver - # [START solver] - # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver("SCIP") + # Solver + # [START solver] + # Create the mip solver with the SCIP backend. + solver = pywraplp.Solver.CreateSolver("SCIP") - if not solver: - return - # [END solver] + if not solver: + return + # [END solver] - # Variables - # [START variables] - # x[i, j] is an array of 0-1 variables, which will be 1 - # if worker i is assigned to task j. - x = {} - for i in range(num_workers): - for j in range(num_tasks): - x[i, j] = solver.IntVar(0, 1, "") - # [END variables] + # Variables + # [START variables] + # x[i, j] is an array of 0-1 variables, which will be 1 + # if worker i is assigned to task j. + x = {} + for i in range(num_workers): + for j in range(num_tasks): + x[i, j] = solver.IntVar(0, 1, "") + # [END variables] - # Constraints - # [START constraints] - # Each worker is assigned to at most 1 task. - for i in range(num_workers): - solver.Add(solver.Sum([x[i, j] for j in range(num_tasks)]) <= 1) + # Constraints + # [START constraints] + # Each worker is assigned to at most 1 task. + for i in range(num_workers): + solver.Add(solver.Sum([x[i, j] for j in range(num_tasks)]) <= 1) - # Each task is assigned to exactly one worker. - for j in range(num_tasks): - solver.Add(solver.Sum([x[i, j] for i in range(num_workers)]) == 1) - # [END constraints] + # Each task is assigned to exactly one worker. + for j in range(num_tasks): + solver.Add(solver.Sum([x[i, j] for i in range(num_workers)]) == 1) + # [END constraints] - # Objective - # [START objective] - objective_terms = [] - for i in range(num_workers): - for j in range(num_tasks): - objective_terms.append(costs[i][j] * x[i, j]) - solver.Minimize(solver.Sum(objective_terms)) - # [END objective] + # Objective + # [START objective] + objective_terms = [] + for i in range(num_workers): + for j in range(num_tasks): + objective_terms.append(costs[i][j] * x[i, j]) + solver.Minimize(solver.Sum(objective_terms)) + # [END objective] - # Solve - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] + # Solve + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] - # Print solution. - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: - print(f"Total cost = {solver.Objective().Value()}\n") - for i in range(num_workers): - for j in range(num_tasks): - # Test if x[i,j] is 1 (with tolerance for floating point arithmetic). - if x[i, j].solution_value() > 0.5: - print(f"Worker {i} assigned to task {j}." + f" Cost: {costs[i][j]}") - else: - print("No solution found.") - # [END print_solution] + # Print solution. + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: + print(f"Total cost = {solver.Objective().Value()}\n") + for i in range(num_workers): + for j in range(num_tasks): + # Test if x[i,j] is 1 (with tolerance for floating point arithmetic). + if x[i, j].solution_value() > 0.5: + print(f"Worker {i} assigned to task {j}." + f" Cost: {costs[i][j]}") + else: + print("No solution found.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/assignment_task_sizes_mip.py b/ortools/linear_solver/samples/assignment_task_sizes_mip.py index da15fd5acff..7d3fdabe8c2 100644 --- a/ortools/linear_solver/samples/assignment_task_sizes_mip.py +++ b/ortools/linear_solver/samples/assignment_task_sizes_mip.py @@ -16,98 +16,101 @@ """MIP example that solves an assignment problem.""" # [START import] from ortools.linear_solver import pywraplp + # [END import] def main(): - # Data - # [START data] - costs = [ - [90, 76, 75, 70, 50, 74, 12, 68], - [35, 85, 55, 65, 48, 101, 70, 83], - [125, 95, 90, 105, 59, 120, 36, 73], - [45, 110, 95, 115, 104, 83, 37, 71], - [60, 105, 80, 75, 59, 62, 93, 88], - [45, 65, 110, 95, 47, 31, 81, 34], - [38, 51, 107, 41, 69, 99, 115, 48], - [47, 85, 57, 71, 92, 77, 109, 36], - [39, 63, 97, 49, 118, 56, 92, 61], - [47, 101, 71, 60, 88, 109, 52, 90], - ] - num_workers = len(costs) - num_tasks = len(costs[0]) + # Data + # [START data] + costs = [ + [90, 76, 75, 70, 50, 74, 12, 68], + [35, 85, 55, 65, 48, 101, 70, 83], + [125, 95, 90, 105, 59, 120, 36, 73], + [45, 110, 95, 115, 104, 83, 37, 71], + [60, 105, 80, 75, 59, 62, 93, 88], + [45, 65, 110, 95, 47, 31, 81, 34], + [38, 51, 107, 41, 69, 99, 115, 48], + [47, 85, 57, 71, 92, 77, 109, 36], + [39, 63, 97, 49, 118, 56, 92, 61], + [47, 101, 71, 60, 88, 109, 52, 90], + ] + num_workers = len(costs) + num_tasks = len(costs[0]) - task_sizes = [10, 7, 3, 12, 15, 4, 11, 5] - # Maximum total of task sizes for any worker - total_size_max = 15 - # [END data] + task_sizes = [10, 7, 3, 12, 15, 4, 11, 5] + # Maximum total of task sizes for any worker + total_size_max = 15 + # [END data] - # Solver - # [START solver] - # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver("SCIP") + # Solver + # [START solver] + # Create the mip solver with the SCIP backend. + solver = pywraplp.Solver.CreateSolver("SCIP") - if not solver: - return - # [END solver] + if not solver: + return + # [END solver] - # Variables - # [START variables] - # x[i, j] is an array of 0-1 variables, which will be 1 - # if worker i is assigned to task j. - x = {} - for worker in range(num_workers): - for task in range(num_tasks): - x[worker, task] = solver.BoolVar(f"x[{worker},{task}]") - # [END variables] + # Variables + # [START variables] + # x[i, j] is an array of 0-1 variables, which will be 1 + # if worker i is assigned to task j. + x = {} + for worker in range(num_workers): + for task in range(num_tasks): + x[worker, task] = solver.BoolVar(f"x[{worker},{task}]") + # [END variables] - # Constraints - # [START constraints] - # The total size of the tasks each worker takes on is at most total_size_max. - for worker in range(num_workers): - solver.Add( - solver.Sum( - [task_sizes[task] * x[worker, task] for task in range(num_tasks)] - ) - <= total_size_max + # Constraints + # [START constraints] + # The total size of the tasks each worker takes on is at most total_size_max. + for worker in range(num_workers): + solver.Add( + solver.Sum( + [task_sizes[task] * x[worker, task] for task in range(num_tasks)] ) + <= total_size_max + ) - # Each task is assigned to exactly one worker. - for task in range(num_tasks): - solver.Add(solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1) - # [END constraints] + # Each task is assigned to exactly one worker. + for task in range(num_tasks): + solver.Add( + solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1 + ) + # [END constraints] - # Objective - # [START objective] - objective_terms = [] - for worker in range(num_workers): - for task in range(num_tasks): - objective_terms.append(costs[worker][task] * x[worker, task]) - solver.Minimize(solver.Sum(objective_terms)) - # [END objective] + # Objective + # [START objective] + objective_terms = [] + for worker in range(num_workers): + for task in range(num_tasks): + objective_terms.append(costs[worker][task] * x[worker, task]) + solver.Minimize(solver.Sum(objective_terms)) + # [END objective] - # Solve - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] + # Solve + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] - # Print solution. - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: - print(f"Total cost = {solver.Objective().Value()}\n") - for worker in range(num_workers): - for task in range(num_tasks): - if x[worker, task].solution_value() > 0.5: - print( - f"Worker {worker} assigned to task {task}." - + f" Cost: {costs[worker][task]}" - ) - else: - print("No solution found.") - # [END print_solution] + # Print solution. + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: + print(f"Total cost = {solver.Objective().Value()}\n") + for worker in range(num_workers): + for task in range(num_tasks): + if x[worker, task].solution_value() > 0.5: + print( + f"Worker {worker} assigned to task {task}." + + f" Cost: {costs[worker][task]}" + ) + else: + print("No solution found.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/assignment_teams_mip.py b/ortools/linear_solver/samples/assignment_teams_mip.py index 3e27b2e9e0a..6b0f9803f13 100644 --- a/ortools/linear_solver/samples/assignment_teams_mip.py +++ b/ortools/linear_solver/samples/assignment_teams_mip.py @@ -16,103 +16,106 @@ """MIP example that solves an assignment problem.""" # [START import] from ortools.linear_solver import pywraplp + # [END import] def main(): - # Data - # [START data] - costs = [ - [90, 76, 75, 70], - [35, 85, 55, 65], - [125, 95, 90, 105], - [45, 110, 95, 115], - [60, 105, 80, 75], - [45, 65, 110, 95], - ] - num_workers = len(costs) - num_tasks = len(costs[0]) - - team1 = [0, 2, 4] - team2 = [1, 3, 5] - # Maximum total of tasks for any team - team_max = 2 - # [END data] - - # Solver - # [START solver] - # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver("SCIP") - if not solver: - return - # [END solver] - - # Variables - # [START variables] - # x[i, j] is an array of 0-1 variables, which will be 1 - # if worker i is assigned to task j. - x = {} - for worker in range(num_workers): - for task in range(num_tasks): - x[worker, task] = solver.BoolVar(f"x[{worker},{task}]") - # [END variables] + # Data + # [START data] + costs = [ + [90, 76, 75, 70], + [35, 85, 55, 65], + [125, 95, 90, 105], + [45, 110, 95, 115], + [60, 105, 80, 75], + [45, 65, 110, 95], + ] + num_workers = len(costs) + num_tasks = len(costs[0]) - # Constraints - # [START constraints] - # Each worker is assigned at most 1 task. - for worker in range(num_workers): - solver.Add(solver.Sum([x[worker, task] for task in range(num_tasks)]) <= 1) + team1 = [0, 2, 4] + team2 = [1, 3, 5] + # Maximum total of tasks for any team + team_max = 2 + # [END data] + + # Solver + # [START solver] + # Create the mip solver with the SCIP backend. + solver = pywraplp.Solver.CreateSolver("SCIP") + if not solver: + return + # [END solver] + + # Variables + # [START variables] + # x[i, j] is an array of 0-1 variables, which will be 1 + # if worker i is assigned to task j. + x = {} + for worker in range(num_workers): + for task in range(num_tasks): + x[worker, task] = solver.BoolVar(f"x[{worker},{task}]") + # [END variables] + + # Constraints + # [START constraints] + # Each worker is assigned at most 1 task. + for worker in range(num_workers): + solver.Add(solver.Sum([x[worker, task] for task in range(num_tasks)]) <= 1) - # Each task is assigned to exactly one worker. + # Each task is assigned to exactly one worker. + for task in range(num_tasks): + solver.Add( + solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1 + ) + + # Each team takes at most two tasks. + team1_tasks = [] + for worker in team1: + for task in range(num_tasks): + team1_tasks.append(x[worker, task]) + solver.Add(solver.Sum(team1_tasks) <= team_max) + + team2_tasks = [] + for worker in team2: for task in range(num_tasks): - solver.Add(solver.Sum([x[worker, task] for worker in range(num_workers)]) == 1) - - # Each team takes at most two tasks. - team1_tasks = [] - for worker in team1: - for task in range(num_tasks): - team1_tasks.append(x[worker, task]) - solver.Add(solver.Sum(team1_tasks) <= team_max) - - team2_tasks = [] - for worker in team2: - for task in range(num_tasks): - team2_tasks.append(x[worker, task]) - solver.Add(solver.Sum(team2_tasks) <= team_max) - # [END constraints] - - # Objective - # [START objective] - objective_terms = [] + team2_tasks.append(x[worker, task]) + solver.Add(solver.Sum(team2_tasks) <= team_max) + # [END constraints] + + # Objective + # [START objective] + objective_terms = [] + for worker in range(num_workers): + for task in range(num_tasks): + objective_terms.append(costs[worker][task] * x[worker, task]) + solver.Minimize(solver.Sum(objective_terms)) + # [END objective] + + # Solve + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] + + # Print solution. + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: + print(f"Total cost = {solver.Objective().Value()}\n") for worker in range(num_workers): - for task in range(num_tasks): - objective_terms.append(costs[worker][task] * x[worker, task]) - solver.Minimize(solver.Sum(objective_terms)) - # [END objective] - - # Solve - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] - - # Print solution. - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL or status == pywraplp.Solver.FEASIBLE: - print(f"Total cost = {solver.Objective().Value()}\n") - for worker in range(num_workers): - for task in range(num_tasks): - if x[worker, task].solution_value() > 0.5: - print( - f"Worker {worker} assigned to task {task}." - + f" Cost = {costs[worker][task]}" - ) - else: - print("No solution found.") - print(f"Time = {solver.WallTime()} ms") - # [END print_solution] + for task in range(num_tasks): + if x[worker, task].solution_value() > 0.5: + print( + f"Worker {worker} assigned to task {task}." + + f" Cost = {costs[worker][task]}" + ) + else: + print("No solution found.") + print(f"Time = {solver.WallTime()} ms") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/basic_example.py b/ortools/linear_solver/samples/basic_example.py index bdf5b570aec..e10320dd9ec 100644 --- a/ortools/linear_solver/samples/basic_example.py +++ b/ortools/linear_solver/samples/basic_example.py @@ -17,79 +17,80 @@ # [START import] from ortools.init.python import init from ortools.linear_solver import pywraplp + # [END import] def main(): - print("Google OR-Tools version:", init.OrToolsVersion.version_string()) - - # [START solver] - # Create the linear solver with the GLOP backend. - solver = pywraplp.Solver.CreateSolver("GLOP") - if not solver: - print("Could not create solver GLOP") - return - # [END solver] - - # [START variables] - # Create the variables x and y. - x_var = solver.NumVar(0, 1, "x") - y_var = solver.NumVar(0, 2, "y") - - print("Number of variables =", solver.NumVariables()) - # [END variables] - - # [START constraints] - infinity = solver.infinity() - # Create a linear constraint, x + y <= 2. - constraint = solver.Constraint(-infinity, 2, "ct") - constraint.SetCoefficient(x_var, 1) - constraint.SetCoefficient(y_var, 1) - - print("Number of constraints =", solver.NumConstraints()) - # [END constraints] - - # [START objective] - # Create the objective function, 3 * x + y. - objective = solver.Objective() - objective.SetCoefficient(x_var, 3) - objective.SetCoefficient(y_var, 1) - objective.SetMaximization() - # [END objective] - - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - result_status = solver.Solve() - # [END solve] - - # [START print_solution] - print(f"Status: {result_status}") - if result_status != pywraplp.Solver.OPTIMAL: - print("The problem does not have an optimal solution!") - if result_status == pywraplp.Solver.FEASIBLE: - print("A potentially suboptimal solution was found") - else: - print("The solver could not solve the problem.") - return - - print("Solution:") - print("Objective value =", objective.Value()) - print("x =", x_var.solution_value()) - print("y =", y_var.solution_value()) - # [END print_solution] - - # [START advanced] - print("Advanced usage:") - print(f"Problem solved in {solver.wall_time():d} milliseconds") - print(f"Problem solved in {solver.iterations():d} iterations") - # [END advanced] + print("Google OR-Tools version:", init.OrToolsVersion.version_string()) + + # [START solver] + # Create the linear solver with the GLOP backend. + solver = pywraplp.Solver.CreateSolver("GLOP") + if not solver: + print("Could not create solver GLOP") + return + # [END solver] + + # [START variables] + # Create the variables x and y. + x_var = solver.NumVar(0, 1, "x") + y_var = solver.NumVar(0, 2, "y") + + print("Number of variables =", solver.NumVariables()) + # [END variables] + + # [START constraints] + infinity = solver.infinity() + # Create a linear constraint, x + y <= 2. + constraint = solver.Constraint(-infinity, 2, "ct") + constraint.SetCoefficient(x_var, 1) + constraint.SetCoefficient(y_var, 1) + + print("Number of constraints =", solver.NumConstraints()) + # [END constraints] + + # [START objective] + # Create the objective function, 3 * x + y. + objective = solver.Objective() + objective.SetCoefficient(x_var, 3) + objective.SetCoefficient(y_var, 1) + objective.SetMaximization() + # [END objective] + + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + result_status = solver.Solve() + # [END solve] + + # [START print_solution] + print(f"Status: {result_status}") + if result_status != pywraplp.Solver.OPTIMAL: + print("The problem does not have an optimal solution!") + if result_status == pywraplp.Solver.FEASIBLE: + print("A potentially suboptimal solution was found") + else: + print("The solver could not solve the problem.") + return + + print("Solution:") + print("Objective value =", objective.Value()) + print("x =", x_var.solution_value()) + print("y =", y_var.solution_value()) + # [END print_solution] + + # [START advanced] + print("Advanced usage:") + print(f"Problem solved in {solver.wall_time():d} milliseconds") + print(f"Problem solved in {solver.iterations():d} iterations") + # [END advanced] if __name__ == "__main__": - init.CppBridge.init_logging("basic_example.py") - cpp_flags = init.CppFlags() - cpp_flags.stderrthreshold = True - cpp_flags.log_prefix = False - init.CppBridge.set_flags(cpp_flags) - main() + init.CppBridge.init_logging("basic_example.py") + cpp_flags = init.CppFlags() + cpp_flags.stderrthreshold = True + cpp_flags.log_prefix = False + init.CppBridge.set_flags(cpp_flags) + main() # [END program] diff --git a/ortools/linear_solver/samples/bin_packing_mb.py b/ortools/linear_solver/samples/bin_packing_mb.py index 1882771a95b..3b1d03d67aa 100644 --- a/ortools/linear_solver/samples/bin_packing_mb.py +++ b/ortools/linear_solver/samples/bin_packing_mb.py @@ -20,15 +20,16 @@ import pandas as pd from ortools.linear_solver.python import model_builder + # [END import] # [START program_part1] # [START data_model] def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame]: - """Create the data for the example.""" + """Create the data for the example.""" - items_str = """ + items_str = """ item weight i1 48 i2 30 @@ -43,7 +44,7 @@ def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame]: i11 30 """ - bins_str = """ + bins_str = """ bin capacity b1 100 b2 100 @@ -54,89 +55,91 @@ def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame]: b7 100 """ - items = pd.read_table(io.StringIO(items_str), index_col=0, sep=r"\s+") - bins = pd.read_table(io.StringIO(bins_str), index_col=0, sep=r"\s+") - return items, bins - # [END data_model] + items = pd.read_table(io.StringIO(items_str), index_col=0, sep=r"\s+") + bins = pd.read_table(io.StringIO(bins_str), index_col=0, sep=r"\s+") + return items, bins + # [END data_model] def main(): - # [START data] - items, bins = create_data_model() - # [END data] - # [END program_part1] - - # [START model] - # Create the model. - model = model_builder.Model() - # [END model] - - # [START program_part2] - # [START variables] - # Variables - # x[i, j] = 1 if item i is packed in bin j. - items_x_bins = pd.MultiIndex.from_product( - [items.index, bins.index], names=["item", "bin"] + # [START data] + items, bins = create_data_model() + # [END data] + # [END program_part1] + + # [START model] + # Create the model. + model = model_builder.Model() + # [END model] + + # [START program_part2] + # [START variables] + # Variables + # x[i, j] = 1 if item i is packed in bin j. + items_x_bins = pd.MultiIndex.from_product( + [items.index, bins.index], names=["item", "bin"] + ) + x = model.new_bool_var_series(name="x", index=items_x_bins) + + # y[j] = 1 if bin j is used. + y = model.new_bool_var_series(name="y", index=bins.index) + # [END variables] + + # [START constraints] + # Constraints + # Each item must be in exactly one bin. + for unused_name, all_copies in x.groupby("item"): + model.add(x[all_copies.index].sum() == 1) + + # The amount packed in each bin cannot exceed its capacity. + for selected_bin in bins.index: + items_in_bin = x.xs(selected_bin, level="bin") + model.add( + items_in_bin.dot(items.weight) + <= bins.loc[selected_bin].capacity * y[selected_bin] ) - x = model.new_bool_var_series(name="x", index=items_x_bins) - - # y[j] = 1 if bin j is used. - y = model.new_bool_var_series(name="y", index=bins.index) - # [END variables] - - # [START constraints] - # Constraints - # Each item must be in exactly one bin. - for unused_name, all_copies in x.groupby("item"): - model.add(x[all_copies.index].sum() == 1) - - # The amount packed in each bin cannot exceed its capacity. - for selected_bin in bins.index: - items_in_bin = x.xs(selected_bin, level="bin") - model.add( - items_in_bin.dot(items.weight) - <= bins.loc[selected_bin].capacity * y[selected_bin] - ) - # [END constraints] - - # [START objective] - # Objective: minimize the number of bins used. - model.minimize(y.sum()) - # [END objective] - - # [START solve] - # Create the solver with the CP-SAT backend, and solve the model. - solver = model_builder.Solver("sat") - if not solver.solver_is_supported(): - return - status = solver.solve(model) - # [END solve] - - # [START print_solution] - if status == model_builder.SolveStatus.OPTIMAL: - print(f"Number of bins used = {solver.objective_value}") - - x_values = solver.values(x) - y_values = solver.values(y) - active_bins = y_values.loc[lambda x: x == 1].index - - for b in active_bins: - print(f"Bin {b}") - items_in_bin = x_values.xs(b, level="bin").loc[lambda x: x == 1].index - for item in items_in_bin: - print(f" Item {item} - weight {items.loc[item].weight}") - print(f" Packed items weight: {items.loc[items_in_bin].sum().to_string()}") - print() - - print(f"Total packed weight: {items.weight.sum()}") - print() - print(f"Time = {solver.wall_time} seconds") - else: - print("The problem does not have an optimal solution.") - # [END print_solution] + # [END constraints] + + # [START objective] + # Objective: minimize the number of bins used. + model.minimize(y.sum()) + # [END objective] + + # [START solve] + # Create the solver with the CP-SAT backend, and solve the model. + solver = model_builder.Solver("sat") + if not solver.solver_is_supported(): + return + status = solver.solve(model) + # [END solve] + + # [START print_solution] + if status == model_builder.SolveStatus.OPTIMAL: + print(f"Number of bins used = {solver.objective_value}") + + x_values = solver.values(x) + y_values = solver.values(y) + active_bins = y_values.loc[lambda x: x == 1].index + + for b in active_bins: + print(f"Bin {b}") + items_in_bin = x_values.xs(b, level="bin").loc[lambda x: x == 1].index + for item in items_in_bin: + print(f" Item {item} - weight {items.loc[item].weight}") + print( + f" Packed items weight: {items.loc[items_in_bin].sum().to_string()}" + ) + print() + + print(f"Total packed weight: {items.weight.sum()}") + print() + print(f"Time = {solver.wall_time} seconds") + else: + print("The problem does not have an optimal solution.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program_part2] # [END program] diff --git a/ortools/linear_solver/samples/bin_packing_mip.py b/ortools/linear_solver/samples/bin_packing_mip.py index 4f49aabcaf5..abe35500ae8 100755 --- a/ortools/linear_solver/samples/bin_packing_mip.py +++ b/ortools/linear_solver/samples/bin_packing_mip.py @@ -16,103 +16,105 @@ # [START program] # [START import] from ortools.linear_solver import pywraplp + # [END import] # [START program_part1] # [START data_model] def create_data_model(): - """Create the data for the example.""" - data = {} - weights = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30] - data["weights"] = weights - data["items"] = list(range(len(weights))) - data["bins"] = data["items"] - data["bin_capacity"] = 100 - return data + """Create the data for the example.""" + data = {} + weights = [48, 30, 19, 36, 36, 27, 42, 42, 36, 24, 30] + data["weights"] = weights + data["items"] = list(range(len(weights))) + data["bins"] = data["items"] + data["bin_capacity"] = 100 + return data + # [END data_model] def main(): - # [START data] - data = create_data_model() - # [END data] - # [END program_part1] - - # [START solver] - # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver("SCIP") - - if not solver: - return - # [END solver] - - # [START program_part2] - # [START variables] - # Variables - # x[i, j] = 1 if item i is packed in bin j. - x = {} - for i in data["items"]: - for j in data["bins"]: - x[(i, j)] = solver.IntVar(0, 1, "x_%i_%i" % (i, j)) - - # y[j] = 1 if bin j is used. - y = {} + # [START data] + data = create_data_model() + # [END data] + # [END program_part1] + + # [START solver] + # Create the mip solver with the SCIP backend. + solver = pywraplp.Solver.CreateSolver("SCIP") + + if not solver: + return + # [END solver] + + # [START program_part2] + # [START variables] + # Variables + # x[i, j] = 1 if item i is packed in bin j. + x = {} + for i in data["items"]: for j in data["bins"]: - y[j] = solver.IntVar(0, 1, "y[%i]" % j) - # [END variables] - - # [START constraints] - # Constraints - # Each item must be in exactly one bin. - for i in data["items"]: - solver.Add(sum(x[i, j] for j in data["bins"]) == 1) - - # The amount packed in each bin cannot exceed its capacity. + x[(i, j)] = solver.IntVar(0, 1, "x_%i_%i" % (i, j)) + + # y[j] = 1 if bin j is used. + y = {} + for j in data["bins"]: + y[j] = solver.IntVar(0, 1, "y[%i]" % j) + # [END variables] + + # [START constraints] + # Constraints + # Each item must be in exactly one bin. + for i in data["items"]: + solver.Add(sum(x[i, j] for j in data["bins"]) == 1) + + # The amount packed in each bin cannot exceed its capacity. + for j in data["bins"]: + solver.Add( + sum(x[(i, j)] * data["weights"][i] for i in data["items"]) + <= y[j] * data["bin_capacity"] + ) + # [END constraints] + + # [START objective] + # Objective: minimize the number of bins used. + solver.Minimize(solver.Sum([y[j] for j in data["bins"]])) + # [END objective] + + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] + + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL: + num_bins = 0 for j in data["bins"]: - solver.Add( - sum(x[(i, j)] * data["weights"][i] for i in data["items"]) - <= y[j] * data["bin_capacity"] - ) - # [END constraints] - - # [START objective] - # Objective: minimize the number of bins used. - solver.Minimize(solver.Sum([y[j] for j in data["bins"]])) - # [END objective] - - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] - - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL: - num_bins = 0 - for j in data["bins"]: - if y[j].solution_value() == 1: - bin_items = [] - bin_weight = 0 - for i in data["items"]: - if x[i, j].solution_value() > 0: - bin_items.append(i) - bin_weight += data["weights"][i] - if bin_items: - num_bins += 1 - print("Bin number", j) - print(" Items packed:", bin_items) - print(" Total weight:", bin_weight) - print() - print() - print("Number of bins used:", num_bins) - print("Time = ", solver.WallTime(), " milliseconds") - else: - print("The problem does not have an optimal solution.") - # [END print_solution] + if y[j].solution_value() == 1: + bin_items = [] + bin_weight = 0 + for i in data["items"]: + if x[i, j].solution_value() > 0: + bin_items.append(i) + bin_weight += data["weights"][i] + if bin_items: + num_bins += 1 + print("Bin number", j) + print(" Items packed:", bin_items) + print(" Total weight:", bin_weight) + print() + print() + print("Number of bins used:", num_bins) + print("Time = ", solver.WallTime(), " milliseconds") + else: + print("The problem does not have an optimal solution.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program_part2] # [END program] diff --git a/ortools/linear_solver/samples/clone_model_mb.py b/ortools/linear_solver/samples/clone_model_mb.py index ce27b6e1b90..716ab446d44 100644 --- a/ortools/linear_solver/samples/clone_model_mb.py +++ b/ortools/linear_solver/samples/clone_model_mb.py @@ -18,77 +18,78 @@ import math from ortools.linear_solver.python import model_builder + # [END import] def main(): - # [START model] - # Create the model. - model = model_builder.Model() - # [END model] - - # [START variables] - # x and y are integer non-negative variables. - x = model.new_int_var(0.0, math.inf, "x") - y = model.new_int_var(0.0, math.inf, "y") - # [END variables] - - # [START constraints] - # x + 7 * y <= 17.5. - unused_c1 = model.add(x + 7 * y <= 17.5) - - # x <= 3.5. - c2 = model.add(x <= 3.5) - # [END constraints] - - # [START objective] - # Maximize x + 10 * y. - model.maximize(x + 10 * y) - # [END objective] - - # [Start clone] - # Clone the model. - print("Cloning the model.") - model_copy = model.clone() - x_copy = model_copy.var_from_index(x.index) - y_copy = model_copy.var_from_index(y.index) - z_copy = model_copy.new_bool_var("z") - c2_copy = model_copy.linear_constraint_from_index(c2.index) - - # Add new constraint. - model_copy.add(x_copy >= 1) - print(f"Number of constraints in original model ={model.num_constraints}") - print(f"Number of constraints in cloned model = {model_copy.num_constraints}") - - # Modify a constraint. - c2_copy.add_term(z_copy, 2.0) - # [END clone] - - # [START solve] - # Create the solver with the SCIP backend, and solve the model. - solver = model_builder.Solver("scip") - if not solver.solver_is_supported(): - return - status = solver.solve(model_copy) - # [END solve] - - # [START print_solution] - if status == model_builder.SolveStatus.OPTIMAL: - print("Solution:") - print(f"Objective value = {solver.objective_value}") - print(f"x = {solver.value(x_copy)}") - print(f"y = {solver.value(y_copy)}") - print(f"z = {solver.value(z_copy)}") - else: - print("The problem does not have an optimal solution.") - # [END print_solution] - - # [START advanced] - print("\nAdvanced usage:") - print(f"Problem solved in {solver.wall_time} seconds") - # [END advanced] + # [START model] + # Create the model. + model = model_builder.Model() + # [END model] + + # [START variables] + # x and y are integer non-negative variables. + x = model.new_int_var(0.0, math.inf, "x") + y = model.new_int_var(0.0, math.inf, "y") + # [END variables] + + # [START constraints] + # x + 7 * y <= 17.5. + unused_c1 = model.add(x + 7 * y <= 17.5) + + # x <= 3.5. + c2 = model.add(x <= 3.5) + # [END constraints] + + # [START objective] + # Maximize x + 10 * y. + model.maximize(x + 10 * y) + # [END objective] + + # [Start clone] + # Clone the model. + print("Cloning the model.") + model_copy = model.clone() + x_copy = model_copy.var_from_index(x.index) + y_copy = model_copy.var_from_index(y.index) + z_copy = model_copy.new_bool_var("z") + c2_copy = model_copy.linear_constraint_from_index(c2.index) + + # Add new constraint. + model_copy.add(x_copy >= 1) + print(f"Number of constraints in original model ={model.num_constraints}") + print(f"Number of constraints in cloned model = {model_copy.num_constraints}") + + # Modify a constraint. + c2_copy.add_term(z_copy, 2.0) + # [END clone] + + # [START solve] + # Create the solver with the SCIP backend, and solve the model. + solver = model_builder.Solver("scip") + if not solver.solver_is_supported(): + return + status = solver.solve(model_copy) + # [END solve] + + # [START print_solution] + if status == model_builder.SolveStatus.OPTIMAL: + print("Solution:") + print(f"Objective value = {solver.objective_value}") + print(f"x = {solver.value(x_copy)}") + print(f"y = {solver.value(y_copy)}") + print(f"z = {solver.value(z_copy)}") + else: + print("The problem does not have an optimal solution.") + # [END print_solution] + + # [START advanced] + print("\nAdvanced usage:") + print(f"Problem solved in {solver.wall_time} seconds") + # [END advanced] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/integer_programming_example.py b/ortools/linear_solver/samples/integer_programming_example.py index 35a9264b843..c3e039aeef9 100644 --- a/ortools/linear_solver/samples/integer_programming_example.py +++ b/ortools/linear_solver/samples/integer_programming_example.py @@ -16,81 +16,82 @@ # [START program] # [START import] from ortools.linear_solver import pywraplp + # [END import] def IntegerProgrammingExample(): - """Integer programming sample.""" - # [START solver] - # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver("SCIP") - if not solver: - return - # [END solver] - - # [START variables] - # x, y, and z are non-negative integer variables. - x = solver.IntVar(0.0, solver.infinity(), "x") - y = solver.IntVar(0.0, solver.infinity(), "y") - z = solver.IntVar(0.0, solver.infinity(), "z") - - print("Number of variables =", solver.NumVariables()) - # [END variables] - - # [START constraints] - # 2*x + 7*y + 3*z <= 50 - constraint0 = solver.Constraint(-solver.infinity(), 50) - constraint0.SetCoefficient(x, 2) - constraint0.SetCoefficient(y, 7) - constraint0.SetCoefficient(z, 3) - - # 3*x - 5*y + 7*z <= 45 - constraint1 = solver.Constraint(-solver.infinity(), 45) - constraint1.SetCoefficient(x, 3) - constraint1.SetCoefficient(y, -5) - constraint1.SetCoefficient(z, 7) - - # 5*x + 2*y - 6*z <= 37 - constraint2 = solver.Constraint(-solver.infinity(), 37) - constraint2.SetCoefficient(x, 5) - constraint2.SetCoefficient(y, 2) - constraint2.SetCoefficient(z, -6) - - print("Number of constraints =", solver.NumConstraints()) - # [END constraints] - - # [START objective] - # Maximize 2*x + 2*y + 3*z - objective = solver.Objective() - objective.SetCoefficient(x, 2) - objective.SetCoefficient(y, 2) - objective.SetCoefficient(z, 3) - objective.SetMaximization() - # [END objective] - - # Solve the problem. - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] - - # Print the solution. - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL: - print("Solution:") - print(f"Objective value = {solver.Objective().Value()}") - # Print the value of each variable in the solution. - for variable in [x, y, z]: - print(f"{variable.name()} = {variable.solution_value()}") - else: - print("The problem does not have an optimal solution.") - # [END print_solution] - - # [START advanced] - print("\nAdvanced usage:") - print(f"Problem solved in {solver.wall_time():d} milliseconds") - print(f"Problem solved in {solver.iterations():d} iterations") - # [END advanced] + """Integer programming sample.""" + # [START solver] + # Create the mip solver with the SCIP backend. + solver = pywraplp.Solver.CreateSolver("SCIP") + if not solver: + return + # [END solver] + + # [START variables] + # x, y, and z are non-negative integer variables. + x = solver.IntVar(0.0, solver.infinity(), "x") + y = solver.IntVar(0.0, solver.infinity(), "y") + z = solver.IntVar(0.0, solver.infinity(), "z") + + print("Number of variables =", solver.NumVariables()) + # [END variables] + + # [START constraints] + # 2*x + 7*y + 3*z <= 50 + constraint0 = solver.Constraint(-solver.infinity(), 50) + constraint0.SetCoefficient(x, 2) + constraint0.SetCoefficient(y, 7) + constraint0.SetCoefficient(z, 3) + + # 3*x - 5*y + 7*z <= 45 + constraint1 = solver.Constraint(-solver.infinity(), 45) + constraint1.SetCoefficient(x, 3) + constraint1.SetCoefficient(y, -5) + constraint1.SetCoefficient(z, 7) + + # 5*x + 2*y - 6*z <= 37 + constraint2 = solver.Constraint(-solver.infinity(), 37) + constraint2.SetCoefficient(x, 5) + constraint2.SetCoefficient(y, 2) + constraint2.SetCoefficient(z, -6) + + print("Number of constraints =", solver.NumConstraints()) + # [END constraints] + + # [START objective] + # Maximize 2*x + 2*y + 3*z + objective = solver.Objective() + objective.SetCoefficient(x, 2) + objective.SetCoefficient(y, 2) + objective.SetCoefficient(z, 3) + objective.SetMaximization() + # [END objective] + + # Solve the problem. + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] + + # Print the solution. + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL: + print("Solution:") + print(f"Objective value = {solver.Objective().Value()}") + # Print the value of each variable in the solution. + for variable in [x, y, z]: + print(f"{variable.name()} = {variable.solution_value()}") + else: + print("The problem does not have an optimal solution.") + # [END print_solution] + + # [START advanced] + print("\nAdvanced usage:") + print(f"Problem solved in {solver.wall_time():d} milliseconds") + print(f"Problem solved in {solver.iterations():d} iterations") + # [END advanced] IntegerProgrammingExample() diff --git a/ortools/linear_solver/samples/linear_programming_example.py b/ortools/linear_solver/samples/linear_programming_example.py index a4bbd1b2543..6fe5c0edc04 100644 --- a/ortools/linear_solver/samples/linear_programming_example.py +++ b/ortools/linear_solver/samples/linear_programming_example.py @@ -16,65 +16,66 @@ # [START program] # [START import] from ortools.linear_solver import pywraplp + # [END import] def LinearProgrammingExample(): - """Linear programming sample.""" - # Instantiate a Glop solver, naming it LinearExample. - # [START solver] - solver = pywraplp.Solver.CreateSolver("GLOP") - if not solver: - return - # [END solver] - - # Create the two variables and let them take on any non-negative value. - # [START variables] - x = solver.NumVar(0, solver.infinity(), "x") - y = solver.NumVar(0, solver.infinity(), "y") - - print("Number of variables =", solver.NumVariables()) - # [END variables] - - # [START constraints] - # Constraint 0: x + 2y <= 14. - solver.Add(x + 2 * y <= 14.0) - - # Constraint 1: 3x - y >= 0. - solver.Add(3 * x - y >= 0.0) - - # Constraint 2: x - y <= 2. - solver.Add(x - y <= 2.0) - - print("Number of constraints =", solver.NumConstraints()) - # [END constraints] - - # [START objective] - # Objective function: 3x + 4y. - solver.Maximize(3 * x + 4 * y) - # [END objective] - - # Solve the system. - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] - - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL: - print("Solution:") - print(f"Objective value = {solver.Objective().Value():0.1f}") - print(f"x = {x.solution_value():0.1f}") - print(f"y = {y.solution_value():0.1f}") - else: - print("The problem does not have an optimal solution.") - # [END print_solution] - - # [START advanced] - print("\nAdvanced usage:") - print(f"Problem solved in {solver.wall_time():d} milliseconds") - print(f"Problem solved in {solver.iterations():d} iterations") - # [END advanced] + """Linear programming sample.""" + # Instantiate a Glop solver, naming it LinearExample. + # [START solver] + solver = pywraplp.Solver.CreateSolver("GLOP") + if not solver: + return + # [END solver] + + # Create the two variables and let them take on any non-negative value. + # [START variables] + x = solver.NumVar(0, solver.infinity(), "x") + y = solver.NumVar(0, solver.infinity(), "y") + + print("Number of variables =", solver.NumVariables()) + # [END variables] + + # [START constraints] + # Constraint 0: x + 2y <= 14. + solver.Add(x + 2 * y <= 14.0) + + # Constraint 1: 3x - y >= 0. + solver.Add(3 * x - y >= 0.0) + + # Constraint 2: x - y <= 2. + solver.Add(x - y <= 2.0) + + print("Number of constraints =", solver.NumConstraints()) + # [END constraints] + + # [START objective] + # Objective function: 3x + 4y. + solver.Maximize(3 * x + 4 * y) + # [END objective] + + # Solve the system. + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] + + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL: + print("Solution:") + print(f"Objective value = {solver.Objective().Value():0.1f}") + print(f"x = {x.solution_value():0.1f}") + print(f"y = {y.solution_value():0.1f}") + else: + print("The problem does not have an optimal solution.") + # [END print_solution] + + # [START advanced] + print("\nAdvanced usage:") + print(f"Problem solved in {solver.wall_time():d} milliseconds") + print(f"Problem solved in {solver.iterations():d} iterations") + # [END advanced] LinearProgrammingExample() diff --git a/ortools/linear_solver/samples/mip_var_array.py b/ortools/linear_solver/samples/mip_var_array.py index 2fc61626fad..5133920dbce 100644 --- a/ortools/linear_solver/samples/mip_var_array.py +++ b/ortools/linear_solver/samples/mip_var_array.py @@ -17,93 +17,95 @@ # [START program] # [START import] from ortools.linear_solver import pywraplp + # [END import] # [START program_part1] # [START data_model] def create_data_model(): - """Stores the data for the problem.""" - data = {} - data["constraint_coeffs"] = [ - [5, 7, 9, 2, 1], - [18, 4, -9, 10, 12], - [4, 7, 3, 8, 5], - [5, 13, 16, 3, -7], - ] - data["bounds"] = [250, 285, 211, 315] - data["obj_coeffs"] = [7, 8, 2, 9, 6] - data["num_vars"] = 5 - data["num_constraints"] = 4 - return data + """Stores the data for the problem.""" + data = {} + data["constraint_coeffs"] = [ + [5, 7, 9, 2, 1], + [18, 4, -9, 10, 12], + [4, 7, 3, 8, 5], + [5, 13, 16, 3, -7], + ] + data["bounds"] = [250, 285, 211, 315] + data["obj_coeffs"] = [7, 8, 2, 9, 6] + data["num_vars"] = 5 + data["num_constraints"] = 4 + return data + # [END data_model] def main(): - # [START data] - data = create_data_model() - # [END data] - # [END program_part1] - # [START solver] - # Create the mip solver with the SCIP backend. - solver = pywraplp.Solver.CreateSolver("SCIP") - if not solver: - return - # [END solver] - - # [START program_part2] - # [START variables] - infinity = solver.infinity() - x = {} + # [START data] + data = create_data_model() + # [END data] + # [END program_part1] + # [START solver] + # Create the mip solver with the SCIP backend. + solver = pywraplp.Solver.CreateSolver("SCIP") + if not solver: + return + # [END solver] + + # [START program_part2] + # [START variables] + infinity = solver.infinity() + x = {} + for j in range(data["num_vars"]): + x[j] = solver.IntVar(0, infinity, "x[%i]" % j) + print("Number of variables =", solver.NumVariables()) + # [END variables] + + # [START constraints] + for i in range(data["num_constraints"]): + constraint = solver.RowConstraint(0, data["bounds"][i], "") for j in range(data["num_vars"]): - x[j] = solver.IntVar(0, infinity, "x[%i]" % j) - print("Number of variables =", solver.NumVariables()) - # [END variables] - - # [START constraints] - for i in range(data["num_constraints"]): - constraint = solver.RowConstraint(0, data["bounds"][i], "") - for j in range(data["num_vars"]): - constraint.SetCoefficient(x[j], data["constraint_coeffs"][i][j]) - print("Number of constraints =", solver.NumConstraints()) - # In Python, you can also set the constraints as follows. - # for i in range(data['num_constraints']): - # constraint_expr = \ - # [data['constraint_coeffs'][i][j] * x[j] for j in range(data['num_vars'])] - # solver.Add(sum(constraint_expr) <= data['bounds'][i]) - # [END constraints] - - # [START objective] - objective = solver.Objective() + constraint.SetCoefficient(x[j], data["constraint_coeffs"][i][j]) + print("Number of constraints =", solver.NumConstraints()) + # In Python, you can also set the constraints as follows. + # for i in range(data['num_constraints']): + # constraint_expr = \ + # [data['constraint_coeffs'][i][j] * x[j] for j in range(data['num_vars'])] + # solver.Add(sum(constraint_expr) <= data['bounds'][i]) + # [END constraints] + + # [START objective] + objective = solver.Objective() + for j in range(data["num_vars"]): + objective.SetCoefficient(x[j], data["obj_coeffs"][j]) + objective.SetMaximization() + # In Python, you can also set the objective as follows. + # obj_expr = [data['obj_coeffs'][j] * x[j] for j in range(data['num_vars'])] + # solver.Maximize(solver.Sum(obj_expr)) + # [END objective] + + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] + + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL: + print("Objective value =", solver.Objective().Value()) for j in range(data["num_vars"]): - objective.SetCoefficient(x[j], data["obj_coeffs"][j]) - objective.SetMaximization() - # In Python, you can also set the objective as follows. - # obj_expr = [data['obj_coeffs'][j] * x[j] for j in range(data['num_vars'])] - # solver.Maximize(solver.Sum(obj_expr)) - # [END objective] - - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] - - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL: - print("Objective value =", solver.Objective().Value()) - for j in range(data["num_vars"]): - print(x[j].name(), " = ", x[j].solution_value()) - print() - print(f"Problem solved in {solver.wall_time():d} milliseconds") - print(f"Problem solved in {solver.iterations():d} iterations") - print(f"Problem solved in {solver.nodes():d} branch-and-bound nodes") - else: - print("The problem does not have an optimal solution.") - # [END print_solution] + print(x[j].name(), " = ", x[j].solution_value()) + print() + print(f"Problem solved in {solver.wall_time():d} milliseconds") + print(f"Problem solved in {solver.iterations():d} iterations") + print(f"Problem solved in {solver.nodes():d} branch-and-bound nodes") + else: + print("The problem does not have an optimal solution.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program_part2] # [END program] diff --git a/ortools/linear_solver/samples/multiple_knapsack_mip.py b/ortools/linear_solver/samples/multiple_knapsack_mip.py index 29dd59155c8..57a0bd23ff2 100644 --- a/ortools/linear_solver/samples/multiple_knapsack_mip.py +++ b/ortools/linear_solver/samples/multiple_knapsack_mip.py @@ -16,94 +16,95 @@ """Solve a multiple knapsack problem using a MIP solver.""" # [START import] from ortools.linear_solver import pywraplp + # [END import] def main(): - # [START data] - data = {} - data["weights"] = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36] - data["values"] = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25] - assert len(data["weights"]) == len(data["values"]) - data["num_items"] = len(data["weights"]) - data["all_items"] = range(data["num_items"]) + # [START data] + data = {} + data["weights"] = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36] + data["values"] = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25] + assert len(data["weights"]) == len(data["values"]) + data["num_items"] = len(data["weights"]) + data["all_items"] = range(data["num_items"]) - data["bin_capacities"] = [100, 100, 100, 100, 100] - data["num_bins"] = len(data["bin_capacities"]) - data["all_bins"] = range(data["num_bins"]) - # [END data] + data["bin_capacities"] = [100, 100, 100, 100, 100] + data["num_bins"] = len(data["bin_capacities"]) + data["all_bins"] = range(data["num_bins"]) + # [END data] - # Create the mip solver with the SCIP backend. - # [START solver] - solver = pywraplp.Solver.CreateSolver("SCIP") - if solver is None: - print("SCIP solver unavailable.") - return - # [END solver] + # Create the mip solver with the SCIP backend. + # [START solver] + solver = pywraplp.Solver.CreateSolver("SCIP") + if solver is None: + print("SCIP solver unavailable.") + return + # [END solver] - # Variables. - # [START variables] - # x[i, b] = 1 if item i is packed in bin b. - x = {} - for i in data["all_items"]: - for b in data["all_bins"]: - x[i, b] = solver.BoolVar(f"x_{i}_{b}") - # [END variables] + # Variables. + # [START variables] + # x[i, b] = 1 if item i is packed in bin b. + x = {} + for i in data["all_items"]: + for b in data["all_bins"]: + x[i, b] = solver.BoolVar(f"x_{i}_{b}") + # [END variables] - # Constraints. - # [START constraints] - # Each item is assigned to at most one bin. - for i in data["all_items"]: - solver.Add(sum(x[i, b] for b in data["all_bins"]) <= 1) + # Constraints. + # [START constraints] + # Each item is assigned to at most one bin. + for i in data["all_items"]: + solver.Add(sum(x[i, b] for b in data["all_bins"]) <= 1) - # The amount packed in each bin cannot exceed its capacity. - for b in data["all_bins"]: - solver.Add( - sum(x[i, b] * data["weights"][i] for i in data["all_items"]) - <= data["bin_capacities"][b] - ) - # [END constraints] + # The amount packed in each bin cannot exceed its capacity. + for b in data["all_bins"]: + solver.Add( + sum(x[i, b] * data["weights"][i] for i in data["all_items"]) + <= data["bin_capacities"][b] + ) + # [END constraints] - # Objective. - # [START objective] - # Maximize total value of packed items. - objective = solver.Objective() - for i in data["all_items"]: - for b in data["all_bins"]: - objective.SetCoefficient(x[i, b], data["values"][i]) - objective.SetMaximization() - # [END objective] + # Objective. + # [START objective] + # Maximize total value of packed items. + objective = solver.Objective() + for i in data["all_items"]: + for b in data["all_bins"]: + objective.SetCoefficient(x[i, b], data["values"][i]) + objective.SetMaximization() + # [END objective] - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL: - print(f"Total packed value: {objective.Value()}") - total_weight = 0 - for b in data["all_bins"]: - print(f"Bin {b}") - bin_weight = 0 - bin_value = 0 - for i in data["all_items"]: - if x[i, b].solution_value() > 0: - print( - f"Item {i} weight: {data['weights'][i]} value:" - f" {data['values'][i]}" - ) - bin_weight += data["weights"][i] - bin_value += data["values"][i] - print(f"Packed bin weight: {bin_weight}") - print(f"Packed bin value: {bin_value}\n") - total_weight += bin_weight - print(f"Total packed weight: {total_weight}") - else: - print("The problem does not have an optimal solution.") - # [END print_solution] + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL: + print(f"Total packed value: {objective.Value()}") + total_weight = 0 + for b in data["all_bins"]: + print(f"Bin {b}") + bin_weight = 0 + bin_value = 0 + for i in data["all_items"]: + if x[i, b].solution_value() > 0: + print( + f"Item {i} weight: {data['weights'][i]} value:" + f" {data['values'][i]}" + ) + bin_weight += data["weights"][i] + bin_value += data["values"][i] + print(f"Packed bin weight: {bin_weight}") + print(f"Packed bin value: {bin_value}\n") + total_weight += bin_weight + print(f"Total packed weight: {total_weight}") + else: + print("The problem does not have an optimal solution.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/simple_lp_program.py b/ortools/linear_solver/samples/simple_lp_program.py index 2612581fe74..1e84dcf8b87 100644 --- a/ortools/linear_solver/samples/simple_lp_program.py +++ b/ortools/linear_solver/samples/simple_lp_program.py @@ -16,63 +16,64 @@ # [START program] # [START import] from ortools.linear_solver import pywraplp + # [END import] def main(): - # [START solver] - # Create the linear solver with the GLOP backend. - solver = pywraplp.Solver.CreateSolver("GLOP") - if not solver: - return - # [END solver] + # [START solver] + # Create the linear solver with the GLOP backend. + solver = pywraplp.Solver.CreateSolver("GLOP") + if not solver: + return + # [END solver] - # [START variables] - infinity = solver.infinity() - # Create the variables x and y. - x = solver.NumVar(0.0, infinity, "x") - y = solver.NumVar(0.0, infinity, "y") + # [START variables] + infinity = solver.infinity() + # Create the variables x and y. + x = solver.NumVar(0.0, infinity, "x") + y = solver.NumVar(0.0, infinity, "y") - print("Number of variables =", solver.NumVariables()) - # [END variables] + print("Number of variables =", solver.NumVariables()) + # [END variables] - # [START constraints] - # x + 7 * y <= 17.5. - solver.Add(x + 7 * y <= 17.5) + # [START constraints] + # x + 7 * y <= 17.5. + solver.Add(x + 7 * y <= 17.5) - # x <= 3.5. - solver.Add(x <= 3.5) + # x <= 3.5. + solver.Add(x <= 3.5) - print("Number of constraints =", solver.NumConstraints()) - # [END constraints] + print("Number of constraints =", solver.NumConstraints()) + # [END constraints] - # [START objective] - # Maximize x + 10 * y. - solver.Maximize(x + 10 * y) - # [END objective] + # [START objective] + # Maximize x + 10 * y. + solver.Maximize(x + 10 * y) + # [END objective] - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL: - print("Solution:") - print("Objective value =", solver.Objective().Value()) - print("x =", x.solution_value()) - print("y =", y.solution_value()) - else: - print("The problem does not have an optimal solution.") - # [END print_solution] + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL: + print("Solution:") + print("Objective value =", solver.Objective().Value()) + print("x =", x.solution_value()) + print("y =", y.solution_value()) + else: + print("The problem does not have an optimal solution.") + # [END print_solution] - # [START advanced] - print("\nAdvanced usage:") - print(f"Problem solved in {solver.wall_time():d} milliseconds") - print(f"Problem solved in {solver.iterations():d} iterations") - # [END advanced] + # [START advanced] + print("\nAdvanced usage:") + print(f"Problem solved in {solver.wall_time():d} milliseconds") + print(f"Problem solved in {solver.iterations():d} iterations") + # [END advanced] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/simple_lp_program_mb.py b/ortools/linear_solver/samples/simple_lp_program_mb.py index d69e27395bd..01097539320 100644 --- a/ortools/linear_solver/samples/simple_lp_program_mb.py +++ b/ortools/linear_solver/samples/simple_lp_program_mb.py @@ -18,65 +18,66 @@ import math from ortools.linear_solver.python import model_builder + # [END import] def main(): - # [START model] - # Create the model. - model = model_builder.Model() - # [END model] - - # [START variables] - # Create the variables x and y. - x = model.new_num_var(0.0, math.inf, "x") - y = model.new_num_var(0.0, math.inf, "y") - - print("Number of variables =", model.num_variables) - # [END variables] - - # [START constraints] - # x + 7 * y <= 17.5. - ct = model.add(x + 7 * y <= 17.5) - - # x <= 3.5. - model.add(x <= 3.5) - - print("Number of constraints =", model.num_constraints) - # [END constraints] - - # [START objective] - # Maximize x + 10 * y. - model.maximize(x + 10 * y) - # [END objective] - - # [START solve] - # Create the solver with the GLOP backend, and solve the model. - solver = model_builder.Solver("glop") - if not solver.solver_is_supported(): - return - status = solver.solve(model) - # [END solve] - - # [START print_solution] - if status == model_builder.SolveStatus.OPTIMAL: - print("Solution:") - print("Objective value =", solver.objective_value) - print("x =", solver.value(x)) - print("y =", solver.value(y)) - - print("dual_value(ct) =", solver.dual_value(ct)) - print("reduced_cost(x) =", solver.reduced_cost(x)) - else: - print("The problem does not have an optimal solution.") - # [END print_solution] - - # [START advanced] - print("\nAdvanced usage:") - print("Problem solved in %f seconds" % solver.wall_time) - # [END advanced] + # [START model] + # Create the model. + model = model_builder.Model() + # [END model] + + # [START variables] + # Create the variables x and y. + x = model.new_num_var(0.0, math.inf, "x") + y = model.new_num_var(0.0, math.inf, "y") + + print("Number of variables =", model.num_variables) + # [END variables] + + # [START constraints] + # x + 7 * y <= 17.5. + ct = model.add(x + 7 * y <= 17.5) + + # x <= 3.5. + model.add(x <= 3.5) + + print("Number of constraints =", model.num_constraints) + # [END constraints] + + # [START objective] + # Maximize x + 10 * y. + model.maximize(x + 10 * y) + # [END objective] + + # [START solve] + # Create the solver with the GLOP backend, and solve the model. + solver = model_builder.Solver("glop") + if not solver.solver_is_supported(): + return + status = solver.solve(model) + # [END solve] + + # [START print_solution] + if status == model_builder.SolveStatus.OPTIMAL: + print("Solution:") + print("Objective value =", solver.objective_value) + print("x =", solver.value(x)) + print("y =", solver.value(y)) + + print("dual_value(ct) =", solver.dual_value(ct)) + print("reduced_cost(x) =", solver.reduced_cost(x)) + else: + print("The problem does not have an optimal solution.") + # [END print_solution] + + # [START advanced] + print("\nAdvanced usage:") + print("Problem solved in %f seconds" % solver.wall_time) + # [END advanced] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/simple_mip_program.py b/ortools/linear_solver/samples/simple_mip_program.py index 8b37801434f..ac9e36c28dd 100644 --- a/ortools/linear_solver/samples/simple_mip_program.py +++ b/ortools/linear_solver/samples/simple_mip_program.py @@ -16,64 +16,65 @@ # [START program] # [START import] from ortools.linear_solver import pywraplp + # [END import] def main(): - # [START solver] - # Create the mip solver with the CP-SAT backend. - solver = pywraplp.Solver.CreateSolver("SAT") - if not solver: - return - # [END solver] + # [START solver] + # Create the mip solver with the CP-SAT backend. + solver = pywraplp.Solver.CreateSolver("SAT") + if not solver: + return + # [END solver] - # [START variables] - infinity = solver.infinity() - # x and y are integer non-negative variables. - x = solver.IntVar(0.0, infinity, "x") - y = solver.IntVar(0.0, infinity, "y") + # [START variables] + infinity = solver.infinity() + # x and y are integer non-negative variables. + x = solver.IntVar(0.0, infinity, "x") + y = solver.IntVar(0.0, infinity, "y") - print("Number of variables =", solver.NumVariables()) - # [END variables] + print("Number of variables =", solver.NumVariables()) + # [END variables] - # [START constraints] - # x + 7 * y <= 17.5. - solver.Add(x + 7 * y <= 17.5) + # [START constraints] + # x + 7 * y <= 17.5. + solver.Add(x + 7 * y <= 17.5) - # x <= 3.5. - solver.Add(x <= 3.5) + # x <= 3.5. + solver.Add(x <= 3.5) - print("Number of constraints =", solver.NumConstraints()) - # [END constraints] + print("Number of constraints =", solver.NumConstraints()) + # [END constraints] - # [START objective] - # Maximize x + 10 * y. - solver.Maximize(x + 10 * y) - # [END objective] + # [START objective] + # Maximize x + 10 * y. + solver.Maximize(x + 10 * y) + # [END objective] - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] + # [START solve] + print(f"Solving with {solver.SolverVersion()}") + status = solver.Solve() + # [END solve] - # [START print_solution] - if status == pywraplp.Solver.OPTIMAL: - print("Solution:") - print("Objective value =", solver.Objective().Value()) - print("x =", x.solution_value()) - print("y =", y.solution_value()) - else: - print("The problem does not have an optimal solution.") - # [END print_solution] + # [START print_solution] + if status == pywraplp.Solver.OPTIMAL: + print("Solution:") + print("Objective value =", solver.Objective().Value()) + print("x =", x.solution_value()) + print("y =", y.solution_value()) + else: + print("The problem does not have an optimal solution.") + # [END print_solution] - # [START advanced] - print("\nAdvanced usage:") - print(f"Problem solved in {solver.wall_time():d} milliseconds") - print(f"Problem solved in {solver.iterations():d} iterations") - print(f"Problem solved in {solver.nodes():d} branch-and-bound nodes") - # [END advanced] + # [START advanced] + print("\nAdvanced usage:") + print(f"Problem solved in {solver.wall_time():d} milliseconds") + print(f"Problem solved in {solver.iterations():d} iterations") + print(f"Problem solved in {solver.nodes():d} branch-and-bound nodes") + # [END advanced] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/simple_mip_program_mb.py b/ortools/linear_solver/samples/simple_mip_program_mb.py index 61f487b955d..f968f0f59c5 100644 --- a/ortools/linear_solver/samples/simple_mip_program_mb.py +++ b/ortools/linear_solver/samples/simple_mip_program_mb.py @@ -18,62 +18,63 @@ import math from ortools.linear_solver.python import model_builder + # [END import] def main(): - # [START model] - # Create the model. - model = model_builder.Model() - # [END model] - - # [START variables] - # x and y are integer non-negative variables. - x = model.new_int_var(0.0, math.inf, "x") - y = model.new_int_var(0.0, math.inf, "y") - - print("Number of variables =", model.num_variables) - # [END variables] - - # [START constraints] - # x + 7 * y <= 17.5. - model.add(x + 7 * y <= 17.5) - - # x <= 3.5. - model.add(x <= 3.5) - - print("Number of constraints =", model.num_constraints) - # [END constraints] - - # [START objective] - # Maximize x + 10 * y. - model.maximize(x + 10 * y) - # [END objective] - - # [START solve] - # Create the solver with the SCIP backend, and solve the model. - solver = model_builder.Solver("scip") - if not solver.solver_is_supported(): - return - status = solver.solve(model) - # [END solve] - - # [START print_solution] - if status == model_builder.SolveStatus.OPTIMAL: - print("Solution:") - print("Objective value =", solver.objective_value) - print("x =", solver.value(x)) - print("y =", solver.value(y)) - else: - print("The problem does not have an optimal solution.") - # [END print_solution] - - # [START advanced] - print("\nAdvanced usage:") - print("Problem solved in %f seconds" % solver.wall_time) - # [END advanced] + # [START model] + # Create the model. + model = model_builder.Model() + # [END model] + + # [START variables] + # x and y are integer non-negative variables. + x = model.new_int_var(0.0, math.inf, "x") + y = model.new_int_var(0.0, math.inf, "y") + + print("Number of variables =", model.num_variables) + # [END variables] + + # [START constraints] + # x + 7 * y <= 17.5. + model.add(x + 7 * y <= 17.5) + + # x <= 3.5. + model.add(x <= 3.5) + + print("Number of constraints =", model.num_constraints) + # [END constraints] + + # [START objective] + # Maximize x + 10 * y. + model.maximize(x + 10 * y) + # [END objective] + + # [START solve] + # Create the solver with the SCIP backend, and solve the model. + solver = model_builder.Solver("scip") + if not solver.solver_is_supported(): + return + status = solver.solve(model) + # [END solve] + + # [START print_solution] + if status == model_builder.SolveStatus.OPTIMAL: + print("Solution:") + print("Objective value =", solver.objective_value) + print("x =", solver.value(x)) + print("y =", solver.value(y)) + else: + print("The problem does not have an optimal solution.") + # [END print_solution] + + # [START advanced] + print("\nAdvanced usage:") + print("Problem solved in %f seconds" % solver.wall_time) + # [END advanced] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/linear_solver/samples/stigler_diet.py b/ortools/linear_solver/samples/stigler_diet.py index 15bdc2c4c3b..3fa51aa9aab 100755 --- a/ortools/linear_solver/samples/stigler_diet.py +++ b/ortools/linear_solver/samples/stigler_diet.py @@ -20,31 +20,32 @@ """ # [START import] from ortools.linear_solver import pywraplp + # [END import] def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data_model] - # Nutrient minimums. - nutrients = [ - ["Calories (kcal)", 3], - ["Protein (g)", 70], - ["Calcium (g)", 0.8], - ["Iron (mg)", 12], - ["Vitamin A (KIU)", 5], - ["Vitamin B1 (mg)", 1.8], - ["Vitamin B2 (mg)", 2.7], - ["Niacin (mg)", 18], - ["Vitamin C (mg)", 75], - ] - - # Commodity, Unit, 1939 price (cents), Calories (kcal), Protein (g), - # Calcium (g), Iron (mg), Vitamin A (KIU), Vitamin B1 (mg), Vitamin B2 (mg), - # Niacin (mg), Vitamin C (mg) - data = [ - # fmt: off + """Entry point of the program.""" + # Instantiate the data problem. + # [START data_model] + # Nutrient minimums. + nutrients = [ + ['Calories (kcal)', 3], + ['Protein (g)', 70], + ['Calcium (g)', 0.8], + ['Iron (mg)', 12], + ['Vitamin A (KIU)', 5], + ['Vitamin B1 (mg)', 1.8], + ['Vitamin B2 (mg)', 2.7], + ['Niacin (mg)', 18], + ['Vitamin C (mg)', 75], + ] + + # Commodity, Unit, 1939 price (cents), Calories (kcal), Protein (g), + # Calcium (g), Iron (mg), Vitamin A (KIU), Vitamin B1 (mg), Vitamin B2 (mg), + # Niacin (mg), Vitamin C (mg) + data = [ + # fmt: off ['Wheat Flour (Enriched)', '10 lb.', 36, 44.7, 1411, 2, 365, 0, 55.4, 33.3, 441, 0], ['Macaroni', '1 lb.', 14.1, 11.6, 418, 0.7, 54, 0, 3.2, 1.9, 68, 0], ['Wheat Cereal (Enriched)', '28 oz.', 24.2, 11.8, 377, 14.4, 175, 0, 14.4, 8.8, 114, 0], @@ -122,82 +123,84 @@ def main(): ['Corn Syrup', '24 oz.', 13.7, 14.7, 0, 0.5, 74, 0, 0, 0, 5, 0], ['Molasses', '18 oz.', 13.6, 9.0, 0, 10.3, 244, 0, 1.9, 7.5, 146, 0], ['Strawberry Preserves', '1 lb.', 20.5, 6.4, 11, 0.4, 7, 0.2, 0.2, 0.4, 3, 0], - # fmt: on - ] - # [END data_model] - - # [START solver] - # Instantiate a Glop solver and naming it. - solver = pywraplp.Solver.CreateSolver("GLOP") - if not solver: - return - # [END solver] - - # [START variables] - # Declare an array to hold our variables. - foods = [solver.NumVar(0.0, solver.infinity(), item[0]) for item in data] - - print("Number of variables =", solver.NumVariables()) - # [END variables] - - # [START constraints] - # Create the constraints, one per nutrient. - constraints = [] - for i, nutrient in enumerate(nutrients): - constraints.append(solver.Constraint(nutrient[1], solver.infinity())) - for j, item in enumerate(data): - constraints[i].SetCoefficient(foods[j], item[i + 3]) - - print("Number of constraints =", solver.NumConstraints()) - # [END constraints] - - # [START objective] - # Objective function: Minimize the sum of (price-normalized) foods. - objective = solver.Objective() - for food in foods: - objective.SetCoefficient(food, 1) - objective.SetMinimization() - # [END objective] - - # [START solve] - print(f"Solving with {solver.SolverVersion()}") - status = solver.Solve() - # [END solve] - - # [START print_solution] - # Check that the problem has an optimal solution. - if status != solver.OPTIMAL: - print("The problem does not have an optimal solution!") - if status == solver.FEASIBLE: - print("A potentially suboptimal solution was found.") - else: - print("The solver could not solve the problem.") - exit(1) - - # Display the amounts (in dollars) to purchase of each food. - nutrients_result = [0] * len(nutrients) - print("\nAnnual Foods:") - for i, food in enumerate(foods): - if food.solution_value() > 0.0: - print("{}: ${}".format(data[i][0], 365.0 * food.solution_value())) - for j, _ in enumerate(nutrients): - nutrients_result[j] += data[i][j + 3] * food.solution_value() - print("\nOptimal annual price: ${:.4f}".format(365.0 * objective.Value())) - - print("\nNutrients per day:") - for i, nutrient in enumerate(nutrients): - print( - "{}: {:.2f} (min {})".format(nutrient[0], nutrients_result[i], nutrient[1]) + # fmt: on + ] + # [END data_model] + + # [START solver] + # Instantiate a Glop solver and naming it. + solver = pywraplp.Solver.CreateSolver('GLOP') + if not solver: + return + # [END solver] + + # [START variables] + # Declare an array to hold our variables. + foods = [solver.NumVar(0.0, solver.infinity(), item[0]) for item in data] + + print('Number of variables =', solver.NumVariables()) + # [END variables] + + # [START constraints] + # Create the constraints, one per nutrient. + constraints = [] + for i, nutrient in enumerate(nutrients): + constraints.append(solver.Constraint(nutrient[1], solver.infinity())) + for j, item in enumerate(data): + constraints[i].SetCoefficient(foods[j], item[i + 3]) + + print('Number of constraints =', solver.NumConstraints()) + # [END constraints] + + # [START objective] + # Objective function: Minimize the sum of (price-normalized) foods. + objective = solver.Objective() + for food in foods: + objective.SetCoefficient(food, 1) + objective.SetMinimization() + # [END objective] + + # [START solve] + print(f'Solving with {solver.SolverVersion()}') + status = solver.Solve() + # [END solve] + + # [START print_solution] + # Check that the problem has an optimal solution. + if status != solver.OPTIMAL: + print('The problem does not have an optimal solution!') + if status == solver.FEASIBLE: + print('A potentially suboptimal solution was found.') + else: + print('The solver could not solve the problem.') + exit(1) + + # Display the amounts (in dollars) to purchase of each food. + nutrients_result = [0] * len(nutrients) + print('\nAnnual Foods:') + for i, food in enumerate(foods): + if food.solution_value() > 0.0: + print('{}: ${}'.format(data[i][0], 365.0 * food.solution_value())) + for j, _ in enumerate(nutrients): + nutrients_result[j] += data[i][j + 3] * food.solution_value() + print('\nOptimal annual price: ${:.4f}'.format(365.0 * objective.Value())) + + print('\nNutrients per day:') + for i, nutrient in enumerate(nutrients): + print( + '{}: {:.2f} (min {})'.format( + nutrient[0], nutrients_result[i], nutrient[1] ) - # [END print_solution] + ) + # [END print_solution] - # [START advanced] - print("\nAdvanced usage:") - print(f"Problem solved in {solver.wall_time():d} milliseconds") - print(f"Problem solved in {solver.iterations():d} iterations") - # [END advanced] + # [START advanced] + print('\nAdvanced usage:') + print(f'Problem solved in {solver.wall_time():d} milliseconds') + print(f'Problem solved in {solver.iterations():d} iterations') + # [END advanced] -if __name__ == "__main__": - main() +if __name__ == '__main__': + main() # [END program] diff --git a/ortools/linear_solver/scip_interface.cc b/ortools/linear_solver/scip_interface.cc index cd3b5390d6a..582a4409ecb 100644 --- a/ortools/linear_solver/scip_interface.cc +++ b/ortools/linear_solver/scip_interface.cc @@ -35,11 +35,11 @@ #include "ortools/base/logging.h" #include "ortools/base/status_macros.h" #include "ortools/base/timer.h" -#include "ortools/gscip/legacy_scip_params.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/linear_solver_callback.h" #include "ortools/linear_solver/proto_solver/proto_utils.h" +#include "ortools/linear_solver/proto_solver/scip_params.h" #include "ortools/linear_solver/proto_solver/scip_proto_solver.h" #include "ortools/linear_solver/scip_callback.h" #include "ortools/linear_solver/scip_helper_macros.h" diff --git a/ortools/linear_solver/solve.cc b/ortools/linear_solver/solve.cc index 3d77bb37e94..52c29575297 100644 --- a/ortools/linear_solver/solve.cc +++ b/ortools/linear_solver/solve.cc @@ -32,7 +32,7 @@ // CP-SAT parameters: // // solve --solver=sat \ -// --params="max_time_in_seconds:600, num_search_workers:8" +// --params="max_time_in_seconds:600, num_workers:8" // --stderrthreshold=0 \ // --input=/tmp/foo.mps \ // 2>/tmp/foo.err diff --git a/ortools/linear_solver/wrappers/BUILD.bazel b/ortools/linear_solver/wrappers/BUILD.bazel index eb3bb8db257..3d7a471396b 100644 --- a/ortools/linear_solver/wrappers/BUILD.bazel +++ b/ortools/linear_solver/wrappers/BUILD.bazel @@ -46,10 +46,8 @@ cc_library( "//ortools/linear_solver/proto_solver:pdlp_proto_solver", "//ortools/linear_solver/proto_solver:sat_proto_solver", "//ortools/linear_solver/proto_solver:scip_proto_solver", - "//ortools/linear_solver/proto_solver:xpress_proto_solver", "//ortools/lp_data:lp_parser", "//ortools/lp_data:mps_reader", "//ortools/util:logging", - "//ortools/xpress:environment", ], ) diff --git a/ortools/linear_solver/wrappers/model_builder_helper.cc b/ortools/linear_solver/wrappers/model_builder_helper.cc index def8d929524..64e810c118e 100644 --- a/ortools/linear_solver/wrappers/model_builder_helper.cc +++ b/ortools/linear_solver/wrappers/model_builder_helper.cc @@ -30,14 +30,13 @@ #include "absl/strings/str_join.h" #include "ortools/base/helpers.h" #include "ortools/base/options.h" -#include "ortools/gurobi/environment.h" +#include "ortools/linear_solver/gurobi_util.h" #include "ortools/linear_solver/linear_solver.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_exporter.h" #include "ortools/linear_solver/proto_solver/glop_proto_solver.h" #include "ortools/linear_solver/proto_solver/gurobi_proto_solver.h" #include "ortools/linear_solver/proto_solver/sat_proto_solver.h" -#include "ortools/linear_solver/proto_solver/xpress_proto_solver.h" #include "ortools/linear_solver/solve_mp_model.h" #if defined(USE_SCIP) #include "ortools/linear_solver/proto_solver/scip_proto_solver.h" @@ -50,7 +49,6 @@ #endif // defined(USE_PDLP) #include "ortools/lp_data/lp_parser.h" #include "ortools/lp_data/mps_reader.h" -#include "ortools/xpress/environment.h" namespace operations_research { namespace mb { @@ -557,11 +555,6 @@ bool ModelSolverHelper::SolverIsSupported() const { solver_type_.value() == MPModelRequest::GUROBI_LINEAR_PROGRAMMING) { return GurobiIsCorrectlyInstalled(); } - if (solver_type_.value() == - MPModelRequest::XPRESS_MIXED_INTEGER_PROGRAMMING || - solver_type_.value() == MPModelRequest::XPRESS_LINEAR_PROGRAMMING) { - return XpressIsCorrectlyInstalled(); - } return false; } @@ -635,12 +628,6 @@ void ModelSolverHelper::Solve(const ModelBuilderHelper& model) { break; } #endif // defined(USE_HIGHS) - case MPModelRequest:: - XPRESS_LINEAR_PROGRAMMING: // ABSL_FALLTHROUGH_INTENDED - case MPModelRequest::XPRESS_MIXED_INTEGER_PROGRAMMING: { - response_ = XPressSolveProto(request); - break; - } default: { response_->set_status( MPSolverResponseStatus::MPSOLVER_SOLVER_TYPE_UNAVAILABLE); @@ -816,9 +803,8 @@ std::shared_ptr LinearExpr::AddFloat(double cst) { std::shared_ptr LinearExpr::Sub(std::shared_ptr expr) { std::vector> exprs; exprs.push_back(shared_from_this()); - exprs.push_back(expr); - std::vector coeffs = {1.0, -1.0}; - return std::make_shared(exprs, coeffs, 0.0); + exprs.push_back(expr->MulFloat(-1.0)); + return std::make_shared(exprs, 0.0); } std::shared_ptr LinearExpr::SubFloat(double cst) { @@ -989,6 +975,72 @@ std::string FlatExpr::DebugString() const { return s; } +SumArray::SumArray(std::vector> exprs, + double offset) + : exprs_(std::move(exprs)), offset_(offset) {} + +void SumArray::Visit(ExprVisitor& lin, double c) { + for (int i = 0; i < exprs_.size(); ++i) { + lin.AddToProcess(exprs_[i], c); + } + if (offset_ != 0.0) { + lin.AddConstant(offset_ * c); + } +} + +std::string SumArray::ToString() const { + if (exprs_.empty()) { + if (offset_ != 0.0) { + return absl::StrCat(offset_); + } + } + std::string s = "("; + for (int i = 0; i < exprs_.size(); ++i) { + if (i > 0) { + absl::StrAppend(&s, " + "); + } + absl::StrAppend(&s, exprs_[i]->ToString()); + } + if (offset_ != 0.0) { + if (offset_ > 0.0) { + absl::StrAppend(&s, " + ", offset_); + } else { + absl::StrAppend(&s, " - ", -offset_); + } + } + absl::StrAppend(&s, ")"); + return s; +} + +std::string SumArray::DebugString() const { + std::string s = absl::StrCat( + "SumArray(", + absl::StrJoin(exprs_, ", ", + [](std::string* out, std::shared_ptr expr) { + absl::StrAppend(out, expr->DebugString()); + })); + if (offset_ != 0.0) { + absl::StrAppend(&s, ", offset=", offset_); + } + absl::StrAppend(&s, ")"); + return s; +} + +std::shared_ptr SumArray::AddInPlace( + std::shared_ptr expr) { + exprs_.push_back(std::move(expr)); + return shared_from_this(); +} + +std::shared_ptr SumArray::AddFloatInPlace(double cst) { + offset_ += cst; + return shared_from_this(); +} + +int SumArray::num_exprs() const { return exprs_.size(); } + +double SumArray::offset() const { return offset_; } + void FixedValue::Visit(ExprVisitor& lin, double c) { lin.AddConstant(value_ * c); } diff --git a/ortools/linear_solver/wrappers/model_builder_helper.h b/ortools/linear_solver/wrappers/model_builder_helper.h index 7c6e4f026f0..4f2371da2cc 100644 --- a/ortools/linear_solver/wrappers/model_builder_helper.h +++ b/ortools/linear_solver/wrappers/model_builder_helper.h @@ -26,8 +26,6 @@ #include "absl/container/btree_map.h" #include "absl/container/fixed_array.h" -#include "absl/strings/str_cat.h" -#include "absl/strings/str_join.h" #include "ortools/linear_solver/linear_solver.pb.h" #include "ortools/linear_solver/model_exporter.h" #include "ortools/util/solve_interrupter.h" @@ -63,12 +61,12 @@ class LinearExpr : public std::enable_shared_from_this { static std::shared_ptr Constant(double value); std::shared_ptr Add(std::shared_ptr expr); - std::shared_ptr AddFloat(double cst); + virtual std::shared_ptr AddFloat(double cst); std::shared_ptr Sub(std::shared_ptr expr); - std::shared_ptr SubFloat(double cst); - std::shared_ptr RSubFloat(double cst); - std::shared_ptr MulFloat(double cst); - std::shared_ptr Neg(); + virtual std::shared_ptr SubFloat(double cst); + virtual std::shared_ptr RSubFloat(double cst); + virtual std::shared_ptr MulFloat(double cst); + virtual std::shared_ptr Neg(); std::shared_ptr Eq(std::shared_ptr rhs); std::shared_ptr EqCst(double rhs); @@ -150,61 +148,17 @@ class FlatExpr : public LinearExpr { class SumArray : public LinearExpr { public: explicit SumArray(std::vector> exprs, - double offset) - : exprs_(std::move(exprs)), offset_(offset) {} + double offset); ~SumArray() override = default; - void Visit(ExprVisitor& lin, double c) override { - for (int i = 0; i < exprs_.size(); ++i) { - lin.AddToProcess(exprs_[i], c); - } - if (offset_ != 0.0) { - lin.AddConstant(offset_ * c); - } - } - - std::string ToString() const override { - if (exprs_.empty()) { - if (offset_ != 0.0) { - return absl::StrCat(offset_); - } - } - std::string s = "("; - for (int i = 0; i < exprs_.size(); ++i) { - if (i > 0) { - absl::StrAppend(&s, " + "); - } - absl::StrAppend(&s, exprs_[i]->ToString()); - } - if (offset_ != 0.0) { - if (offset_ > 0.0) { - absl::StrAppend(&s, " + ", offset_); - } else { - absl::StrAppend(&s, " - ", -offset_); - } - } - absl::StrAppend(&s, ")"); - return s; - } - - std::string DebugString() const override { - std::string s = absl::StrCat( - "SumArray(", - absl::StrJoin(exprs_, ", ", - [](std::string* out, std::shared_ptr expr) { - absl::StrAppend(out, expr->DebugString()); - })); - if (offset_ != 0.0) { - absl::StrAppend(&s, ", offset=", offset_); - } - absl::StrAppend(&s, ")"); - return s; - } + void Visit(ExprVisitor& lin, double c) override; - void AddInPlace(std::shared_ptr expr) { exprs_.push_back(expr); } - void AddFloatInPlace(double cst) { offset_ += cst; } - int num_exprs() const { return exprs_.size(); } - double offset() const { return offset_; } + std::string ToString() const override; + std::string DebugString() const override; + std::shared_ptr AddInPlace(std::shared_ptr expr); + std::shared_ptr AddFloatInPlace(double cst); + int num_exprs() const; + double offset() const; private: std::vector> exprs_; @@ -243,11 +197,11 @@ class AffineExpr : public LinearExpr { double coefficient() const { return coeff_; } double offset() const { return offset_; } - std::shared_ptr AddFloat(double cst); - std::shared_ptr SubFloat(double cst); - std::shared_ptr RSubFloat(double cst); - std::shared_ptr MulFloat(double cst); - std::shared_ptr Neg(); + std::shared_ptr AddFloat(double cst) override; + std::shared_ptr SubFloat(double cst) override; + std::shared_ptr RSubFloat(double cst) override; + std::shared_ptr MulFloat(double cst) override; + std::shared_ptr Neg() override; private: std::shared_ptr expr_; diff --git a/ortools/linear_solver/xpress_interface.cc b/ortools/linear_solver/xpress_interface.cc index 0f88bc861c8..c93f5e8caaf 100644 --- a/ortools/linear_solver/xpress_interface.cc +++ b/ortools/linear_solver/xpress_interface.cc @@ -20,15 +20,15 @@ #include #include #include -#include #include +#include #include "absl/strings/numbers.h" #include "absl/strings/str_format.h" #include "ortools/base/logging.h" #include "ortools/base/timer.h" #include "ortools/linear_solver/linear_solver.h" -#include "ortools/xpress/environment.h" +#include "ortools/third_party_solvers/xpress_environment.h" #define XPRS_INTEGER 'I' #define XPRS_CONTINUOUS 'C' @@ -1094,7 +1094,8 @@ void XpressInterface::SetCoefficient(MPConstraint* const constraint, double new_value, double) { InvalidateSolutionSynchronization(); - fixedOrderCoefficientsPerConstraint[constraint->index()][variable->index()] = new_value; + fixedOrderCoefficientsPerConstraint[constraint->index()][variable->index()] = + new_value; // Changing a single coefficient in the matrix is potentially pretty // slow since that coefficient has to be found in the sparse matrix diff --git a/ortools/linear_solver/xpress_interface_test.cc b/ortools/linear_solver/xpress_interface_test.cc index 16925a1e365..4aa21c23636 100644 --- a/ortools/linear_solver/xpress_interface_test.cc +++ b/ortools/linear_solver/xpress_interface_test.cc @@ -22,7 +22,7 @@ #include "gtest/gtest.h" #include "ortools/base/init_google.h" #include "ortools/linear_solver/linear_solver.h" -#include "ortools/xpress/environment.h" +#include "ortools/third_party_solvers/xpress_environment.h" #define XPRS_NAMELENGTH 1028 namespace operations_research { @@ -159,7 +159,7 @@ class XPRSGetter { std::string value(280, '\0'); int valueSize; EXPECT_STATUS(XPRSgetstringattrib(prob(), attrib, &value[0], value.size(), - &valueSize)); + &valueSize)); value.resize(valueSize - 1); return value; } diff --git a/ortools/math_opt/core/c_api/cpp_example_test.py b/ortools/math_opt/core/c_api/cpp_example_test.py index d09ec513fba..216e78b45d0 100644 --- a/ortools/math_opt/core/c_api/cpp_example_test.py +++ b/ortools/math_opt/core/c_api/cpp_example_test.py @@ -28,17 +28,19 @@ class CppExampleTest( absltest.TestCase, ): - def test_regression(self): - result = self.assert_binary_succeeds("ortools/math_opt/core/c_api/cpp_example") - is_optimal = self.assert_has_line_with_prefixed_number( - "Termination is optimal: ", result.stdout - ) - self.assertEqual(is_optimal, 1) - objective_value = self.assert_has_line_with_prefixed_number( - "Objective value: ", result.stdout - ) - self.assertAlmostEqual(objective_value, 1.0) + def test_regression(self): + result = self.assert_binary_succeeds( + "ortools/math_opt/core/c_api/cpp_example" + ) + is_optimal = self.assert_has_line_with_prefixed_number( + "Termination is optimal: ", result.stdout + ) + self.assertEqual(is_optimal, 1) + objective_value = self.assert_has_line_with_prefixed_number( + "Objective value: ", result.stdout + ) + self.assertAlmostEqual(objective_value, 1.0) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/core/python/solver_gurobi_test.py b/ortools/math_opt/core/python/solver_gurobi_test.py index 3e5b3aaf648..59e9aec0453 100644 --- a/ortools/math_opt/core/python/solver_gurobi_test.py +++ b/ortools/math_opt/core/python/solver_gurobi_test.py @@ -31,18 +31,18 @@ # x, y, z in [0, 1] # The IIS is x upper bound, z upper bound, (d) lower bound def _simple_infeasible_model() -> model_pb2.ModelProto: - model = model_pb2.ModelProto() - model.variables.ids[:] = [0, 1, 2] - model.variables.lower_bounds[:] = [0.0, 0.0, 0.0] - model.variables.upper_bounds[:] = [1.0, 1.0, 1.0] - model.variables.integers[:] = [False, False, False] - model.linear_constraints.ids[:] = [0, 1] - model.linear_constraints.lower_bounds[:] = [-math.inf, 3.0] - model.linear_constraints.upper_bounds[:] = [4.0, math.inf] - model.linear_constraint_matrix.row_ids[:] = [0, 0, 1, 1] - model.linear_constraint_matrix.column_ids[:] = [0, 2, 0, 2] - model.linear_constraint_matrix.coefficients[:] = [1.0, 1.0, 1.0, 1.0] - return model + model = model_pb2.ModelProto() + model.variables.ids[:] = [0, 1, 2] + model.variables.lower_bounds[:] = [0.0, 0.0, 0.0] + model.variables.upper_bounds[:] = [1.0, 1.0, 1.0] + model.variables.integers[:] = [False, False, False] + model.linear_constraints.ids[:] = [0, 1] + model.linear_constraints.lower_bounds[:] = [-math.inf, 3.0] + model.linear_constraints.upper_bounds[:] = [4.0, math.inf] + model.linear_constraint_matrix.row_ids[:] = [0, 0, 1, 1] + model.linear_constraint_matrix.column_ids[:] = [0, 2, 0, 2] + model.linear_constraint_matrix.coefficients[:] = [1.0, 1.0, 1.0, 1.0] + return model # The model is @@ -52,130 +52,132 @@ def _simple_infeasible_model() -> model_pb2.ModelProto: # x + z <= 1 # x, y, z in {0, 1} def _nontrivial_infeasible_model() -> model_pb2.ModelProto: - model = model_pb2.ModelProto() - model.variables.ids[:] = [0, 1, 2] - model.variables.lower_bounds[:] = [0.0, 0.0, 0.0] - model.variables.upper_bounds[:] = [1.0, 1.0, 1.0] - model.variables.integers[:] = [True, True, True] - model.linear_constraints.ids[:] = [0, 1, 2, 3] - model.linear_constraints.lower_bounds[:] = [ - 3.0, - -math.inf, - -math.inf, - -math.inf, - ] - model.linear_constraints.upper_bounds[:] = [math.inf, 1.0, 1.0, 1.0] - model.linear_constraint_matrix.row_ids[:] = [0, 0, 0, 1, 1, 2, 2, 3, 3] - model.linear_constraint_matrix.column_ids[:] = [0, 1, 2, 0, 1, 1, 2, 0, 2] - model.linear_constraint_matrix.coefficients[:] = [ - 2.0, - 2.0, - 2.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - 1.0, - ] - return model + model = model_pb2.ModelProto() + model.variables.ids[:] = [0, 1, 2] + model.variables.lower_bounds[:] = [0.0, 0.0, 0.0] + model.variables.upper_bounds[:] = [1.0, 1.0, 1.0] + model.variables.integers[:] = [True, True, True] + model.linear_constraints.ids[:] = [0, 1, 2, 3] + model.linear_constraints.lower_bounds[:] = [ + 3.0, + -math.inf, + -math.inf, + -math.inf, + ] + model.linear_constraints.upper_bounds[:] = [math.inf, 1.0, 1.0, 1.0] + model.linear_constraint_matrix.row_ids[:] = [0, 0, 0, 1, 1, 2, 2, 3, 3] + model.linear_constraint_matrix.column_ids[:] = [0, 1, 2, 0, 1, 1, 2, 0, 2] + model.linear_constraint_matrix.coefficients[:] = [ + 2.0, + 2.0, + 2.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + ] + return model def _expected_iis_success() -> ( infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto ): - expected = infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto( - is_minimal=True, feasibility=result_pb2.FEASIBILITY_STATUS_INFEASIBLE - ) - expected.infeasible_subsystem.variable_bounds[0].upper = True - expected.infeasible_subsystem.variable_bounds[2].upper = True - expected.infeasible_subsystem.linear_constraints[1].lower = True - return expected + expected = infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto( + is_minimal=True, feasibility=result_pb2.FEASIBILITY_STATUS_INFEASIBLE + ) + expected.infeasible_subsystem.variable_bounds[0].upper = True + expected.infeasible_subsystem.variable_bounds[2].upper = True + expected.infeasible_subsystem.linear_constraints[1].lower = True + return expected class PybindComputeInfeasibleSubsystemTest( compare_proto.MathOptProtoAssertions, absltest.TestCase ): - def test_compute_infeasible_subsystem_infeasible(self) -> None: - iis_result = solver.compute_infeasible_subsystem( - _simple_infeasible_model(), - parameters_pb2.SOLVER_TYPE_GUROBI, - parameters_pb2.SolverInitializerProto(), - parameters_pb2.SolveParametersProto(), - None, - None, - ) - self.assert_protos_equiv(iis_result, _expected_iis_success()) - - def test_compute_infeasible_subsystem_infeasible_uninterrupted(self) -> None: - interrupter = solver.SolveInterrupter() - iis_result = solver.compute_infeasible_subsystem( - _simple_infeasible_model(), - parameters_pb2.SOLVER_TYPE_GUROBI, - parameters_pb2.SolverInitializerProto(), - parameters_pb2.SolveParametersProto(), - None, - interrupter, - ) - self.assert_protos_equiv(iis_result, _expected_iis_success()) - - def test_compute_infeasible_subsystem_interrupted(self) -> None: - interrupter = solver.SolveInterrupter() - interrupter.interrupt() - iis_result = solver.compute_infeasible_subsystem( - _nontrivial_infeasible_model(), - parameters_pb2.SOLVER_TYPE_GUROBI, - parameters_pb2.SolverInitializerProto(), - parameters_pb2.SolveParametersProto(), - None, - interrupter, - ) - expected = infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto( - feasibility=result_pb2.FEASIBILITY_STATUS_UNDETERMINED - ) - self.assert_protos_equiv(iis_result, expected) - - def test_compute_infeasible_subsystem_time_limit(self) -> None: - params = parameters_pb2.SolveParametersProto() - params.time_limit.FromTimedelta(datetime.timedelta(seconds=0.0)) - iis_result = solver.compute_infeasible_subsystem( - _nontrivial_infeasible_model(), - parameters_pb2.SOLVER_TYPE_GUROBI, - parameters_pb2.SolverInitializerProto(), - params, - None, - None, - ) - expected = infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto( - feasibility=result_pb2.FEASIBILITY_STATUS_UNDETERMINED - ) - self.assert_protos_equiv(iis_result, expected) - - def test_compute_infeasible_subsystem_infeasible_message_cb(self) -> None: - messages = [] - iis_result = solver.compute_infeasible_subsystem( - _simple_infeasible_model(), - parameters_pb2.SOLVER_TYPE_GUROBI, - parameters_pb2.SolverInitializerProto(), - parameters_pb2.SolveParametersProto(), - messages.extend, - None, - ) - self.assert_protos_equiv(iis_result, _expected_iis_success()) - self.assertIn("IIS computed", "\n".join(messages)) - - def test_compute_infeasible_subsystem_error_wrong_solver(self) -> None: - with self.assertRaisesRegex(StatusNotOk, "SOLVER_TYPE_GLPK is not registered"): - solver.compute_infeasible_subsystem( - _simple_infeasible_model(), - parameters_pb2.SOLVER_TYPE_GLPK, - parameters_pb2.SolverInitializerProto(), - parameters_pb2.SolveParametersProto(), - None, - None, - ) + def test_compute_infeasible_subsystem_infeasible(self) -> None: + iis_result = solver.compute_infeasible_subsystem( + _simple_infeasible_model(), + parameters_pb2.SOLVER_TYPE_GUROBI, + parameters_pb2.SolverInitializerProto(), + parameters_pb2.SolveParametersProto(), + None, + None, + ) + self.assert_protos_equiv(iis_result, _expected_iis_success()) + + def test_compute_infeasible_subsystem_infeasible_uninterrupted(self) -> None: + interrupter = solver.SolveInterrupter() + iis_result = solver.compute_infeasible_subsystem( + _simple_infeasible_model(), + parameters_pb2.SOLVER_TYPE_GUROBI, + parameters_pb2.SolverInitializerProto(), + parameters_pb2.SolveParametersProto(), + None, + interrupter, + ) + self.assert_protos_equiv(iis_result, _expected_iis_success()) + + def test_compute_infeasible_subsystem_interrupted(self) -> None: + interrupter = solver.SolveInterrupter() + interrupter.interrupt() + iis_result = solver.compute_infeasible_subsystem( + _nontrivial_infeasible_model(), + parameters_pb2.SOLVER_TYPE_GUROBI, + parameters_pb2.SolverInitializerProto(), + parameters_pb2.SolveParametersProto(), + None, + interrupter, + ) + expected = infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto( + feasibility=result_pb2.FEASIBILITY_STATUS_UNDETERMINED + ) + self.assert_protos_equiv(iis_result, expected) + + def test_compute_infeasible_subsystem_time_limit(self) -> None: + params = parameters_pb2.SolveParametersProto() + params.time_limit.FromTimedelta(datetime.timedelta(seconds=0.0)) + iis_result = solver.compute_infeasible_subsystem( + _nontrivial_infeasible_model(), + parameters_pb2.SOLVER_TYPE_GUROBI, + parameters_pb2.SolverInitializerProto(), + params, + None, + None, + ) + expected = infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto( + feasibility=result_pb2.FEASIBILITY_STATUS_UNDETERMINED + ) + self.assert_protos_equiv(iis_result, expected) + + def test_compute_infeasible_subsystem_infeasible_message_cb(self) -> None: + messages = [] + iis_result = solver.compute_infeasible_subsystem( + _simple_infeasible_model(), + parameters_pb2.SOLVER_TYPE_GUROBI, + parameters_pb2.SolverInitializerProto(), + parameters_pb2.SolveParametersProto(), + messages.extend, + None, + ) + self.assert_protos_equiv(iis_result, _expected_iis_success()) + self.assertIn("IIS computed", "\n".join(messages)) + + def test_compute_infeasible_subsystem_error_wrong_solver(self) -> None: + with self.assertRaisesRegex( + StatusNotOk, "SOLVER_TYPE_GLPK is not registered" + ): + solver.compute_infeasible_subsystem( + _simple_infeasible_model(), + parameters_pb2.SOLVER_TYPE_GLPK, + parameters_pb2.SolverInitializerProto(), + parameters_pb2.SolveParametersProto(), + None, + None, + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/core/python/solver_test.py b/ortools/math_opt/core/python/solver_test.py index f74f98f0ef6..e7609021595 100644 --- a/ortools/math_opt/core/python/solver_test.py +++ b/ortools/math_opt/core/python/solver_test.py @@ -28,16 +28,16 @@ def _build_simple_model() -> model_pb2.ModelProto: - model = model_pb2.ModelProto() - model.variables.ids.append(0) - model.variables.lower_bounds.append(1.0) - model.variables.upper_bounds.append(2.0) - model.variables.integers.append(False) - model.variables.names.append("x") - model.objective.maximize = True - model.objective.linear_coefficients.ids.append(0) - model.objective.linear_coefficients.values.append(1.0) - return model + model = model_pb2.ModelProto() + model.variables.ids.append(0) + model.variables.lower_bounds.append(1.0) + model.variables.upper_bounds.append(2.0) + model.variables.integers.append(False) + model.variables.names.append("x") + model.objective.maximize = True + model.objective.linear_coefficients.ids.append(0) + model.objective.linear_coefficients.values.append(1.0) + return model def _solve_model( @@ -51,230 +51,238 @@ def _solve_model( message_callback: Optional[Callable[[Sequence[str]], None]] = None, callback_registration: callback_pb2.CallbackRegistrationProto = callback_pb2.CallbackRegistrationProto(), user_cb: Optional[ - Callable[[callback_pb2.CallbackDataProto], callback_pb2.CallbackResultProto] + Callable[ + [callback_pb2.CallbackDataProto], callback_pb2.CallbackResultProto + ] ] = None, interrupter: Optional[solver.SolveInterrupter] = None, ) -> result_pb2.SolveResultProto: - """Convenience function for both types of solve with parameter defaults.""" - if use_solver_class: - pybind_solver = solver.new( - solver_type, - model, - solver_initializer, - ) - return pybind_solver.solve( - parameters, - model_parameters, - message_callback, - callback_registration, - user_cb, - interrupter, - ) - else: - return solver.solve( - model, - solver_type, - solver_initializer, - parameters, - model_parameters, - message_callback, - callback_registration, - user_cb, - interrupter, - ) + """Convenience function for both types of solve with parameter defaults.""" + if use_solver_class: + pybind_solver = solver.new( + solver_type, + model, + solver_initializer, + ) + return pybind_solver.solve( + parameters, + model_parameters, + message_callback, + callback_registration, + user_cb, + interrupter, + ) + else: + return solver.solve( + model, + solver_type, + solver_initializer, + parameters, + model_parameters, + message_callback, + callback_registration, + user_cb, + interrupter, + ) class PybindSolverTest(parameterized.TestCase): - def tearDown(self): - super().tearDown() - self.assertEqual(solver.debug_num_solver(), 0) + def tearDown(self): + super().tearDown() + self.assertEqual(solver.debug_num_solver(), 0) - @parameterized.named_parameters( - dict(testcase_name="without_solver", use_solver_class=False), - dict(testcase_name="with_solver", use_solver_class=True), + @parameterized.named_parameters( + dict(testcase_name="without_solver", use_solver_class=False), + dict(testcase_name="with_solver", use_solver_class=True), + ) + def test_valid_solve(self, use_solver_class: bool) -> None: + model = _build_simple_model() + result = _solve_model(model, use_solver_class=use_solver_class) + self.assertEqual( + result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL ) - def test_valid_solve(self, use_solver_class: bool) -> None: - model = _build_simple_model() - result = _solve_model(model, use_solver_class=use_solver_class) - self.assertEqual( - result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL - ) - self.assertTrue(result.solutions) - self.assertAlmostEqual(result.solutions[0].primal_solution.objective_value, 2.0) - - @parameterized.named_parameters( - dict(testcase_name="without_solver", use_solver_class=False), - dict(testcase_name="with_solver", use_solver_class=True), + self.assertTrue(result.solutions) + self.assertAlmostEqual( + result.solutions[0].primal_solution.objective_value, 2.0 ) - def test_invalid_input_throws_error(self, use_solver_class: bool) -> None: - model = _build_simple_model() - # Add invalid variable id to cause MathOpt model validation error. - model.objective.linear_coefficients.ids.append(7) - model.objective.linear_coefficients.values.append(2.0) - if sys.platform in ("darwin", "win32"): - with self.assertRaisesRegex(RuntimeError, "id 7 not found"): - _solve_model(model, use_solver_class=use_solver_class) - else: - with self.assertRaisesRegex(status.StatusNotOk, "id 7 not found"): - _solve_model(model, use_solver_class=use_solver_class) - @parameterized.named_parameters( - dict(testcase_name="without_solver", use_solver_class=False), - dict(testcase_name="with_solver", use_solver_class=True), - ) - def test_solve_interrupter_interrupts_solve(self, use_solver_class: bool) -> None: - model = _build_simple_model() - interrupter = solver.SolveInterrupter() - interrupter.interrupt() - result = _solve_model( - model, use_solver_class=use_solver_class, interrupter=interrupter - ) - self.assertEqual( - result.termination.reason, - result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND, - ) - self.assertEqual(result.termination.limit, result_pb2.LIMIT_INTERRUPTED) + @parameterized.named_parameters( + dict(testcase_name="without_solver", use_solver_class=False), + dict(testcase_name="with_solver", use_solver_class=True), + ) + def test_invalid_input_throws_error(self, use_solver_class: bool) -> None: + model = _build_simple_model() + # Add invalid variable id to cause MathOpt model validation error. + model.objective.linear_coefficients.ids.append(7) + model.objective.linear_coefficients.values.append(2.0) + if sys.platform in ("darwin", "win32"): + with self.assertRaisesRegex(RuntimeError, "id 7 not found"): + _solve_model(model, use_solver_class=use_solver_class) + else: + with self.assertRaisesRegex(status.StatusNotOk, "id 7 not found"): + _solve_model(model, use_solver_class=use_solver_class) - @parameterized.named_parameters( - dict(testcase_name="without_solver", use_solver_class=False), - dict(testcase_name="with_solver", use_solver_class=True), + @parameterized.named_parameters( + dict(testcase_name="without_solver", use_solver_class=False), + dict(testcase_name="with_solver", use_solver_class=True), + ) + def test_solve_interrupter_interrupts_solve( + self, use_solver_class: bool + ) -> None: + model = _build_simple_model() + interrupter = solver.SolveInterrupter() + interrupter.interrupt() + result = _solve_model( + model, use_solver_class=use_solver_class, interrupter=interrupter + ) + self.assertEqual( + result.termination.reason, + result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND, ) - def test_message_callback_is_invoked(self, use_solver_class: bool) -> None: - model = _build_simple_model() - messages = [] - # Message callback extends `messages` with solver output. - _solve_model( - model, - use_solver_class=use_solver_class, - parameters=parameters_pb2.SolveParametersProto( - enable_output=True, threads=1 - ), - message_callback=messages.extend, - ) - self.assertIn("status:", "\n".join(messages)) + self.assertEqual(result.termination.limit, result_pb2.LIMIT_INTERRUPTED) - @parameterized.named_parameters( - dict(testcase_name="without_solver", use_solver_class=False), - dict(testcase_name="with_solver", use_solver_class=True), + @parameterized.named_parameters( + dict(testcase_name="without_solver", use_solver_class=False), + dict(testcase_name="with_solver", use_solver_class=True), + ) + def test_message_callback_is_invoked(self, use_solver_class: bool) -> None: + model = _build_simple_model() + messages = [] + # Message callback extends `messages` with solver output. + _solve_model( + model, + use_solver_class=use_solver_class, + parameters=parameters_pb2.SolveParametersProto( + enable_output=True, threads=1 + ), + message_callback=messages.extend, ) - def test_user_callback_is_invoked(self, use_solver_class: bool) -> None: - model = _build_simple_model() - solution_values = [] - mutex = threading.Lock() + self.assertIn("status:", "\n".join(messages)) - # Callback stores solution values found during solve in `solution_values`. - def collect_solution_values_user_callback( - cb_data: callback_pb2.CallbackDataProto, - ) -> callback_pb2.CallbackResultProto: - with mutex: - assert cb_data.event == callback_pb2.CALLBACK_EVENT_MIP_SOLUTION - solution_values.extend(cb_data.primal_solution_vector.values) - return callback_pb2.CallbackResultProto() + @parameterized.named_parameters( + dict(testcase_name="without_solver", use_solver_class=False), + dict(testcase_name="with_solver", use_solver_class=True), + ) + def test_user_callback_is_invoked(self, use_solver_class: bool) -> None: + model = _build_simple_model() + solution_values = [] + mutex = threading.Lock() - # This implicitly tests that the GIL is released, since the solve below can - # deadlock otherwise. - result = _solve_model( - model, - use_solver_class=use_solver_class, - solver_type=parameters_pb2.SOLVER_TYPE_CP_SAT, - callback_registration=callback_pb2.CallbackRegistrationProto( - request_registration=[callback_pb2.CALLBACK_EVENT_MIP_SOLUTION] - ), - user_cb=collect_solution_values_user_callback, - ) - self.assertEqual( - result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL - ) - # `solution_values` should at least contain the optimal value 2.0. - self.assertContainsSubset(solution_values, [2.0]) + # Callback stores solution values found during solve in `solution_values`. + def collect_solution_values_user_callback( + cb_data: callback_pb2.CallbackDataProto, + ) -> callback_pb2.CallbackResultProto: + with mutex: + assert cb_data.event == callback_pb2.CALLBACK_EVENT_MIP_SOLUTION + solution_values.extend(cb_data.primal_solution_vector.values) + return callback_pb2.CallbackResultProto() - @parameterized.named_parameters( - dict(testcase_name="without_solver", use_solver_class=False), - dict(testcase_name="with_solver", use_solver_class=True), + # This implicitly tests that the GIL is released, since the solve below can + # deadlock otherwise. + result = _solve_model( + model, + use_solver_class=use_solver_class, + solver_type=parameters_pb2.SOLVER_TYPE_CP_SAT, + callback_registration=callback_pb2.CallbackRegistrationProto( + request_registration=[callback_pb2.CALLBACK_EVENT_MIP_SOLUTION] + ), + user_cb=collect_solution_values_user_callback, ) - def test_solution_hint_is_used(self, use_solver_class: bool) -> None: - model = _build_simple_model() - solution_hint = model_parameters_pb2.SolutionHintProto() - solution_hint.variable_values.ids.append(0) - solution_hint.variable_values.values.append(1.0) - # Limit the solver so that it does not find a solution other than provided. - result = _solve_model( - model, - use_solver_class=use_solver_class, - solver_type=parameters_pb2.SOLVER_TYPE_GSCIP, - parameters=parameters_pb2.SolveParametersProto( - node_limit=0, - heuristics=parameters_pb2.EMPHASIS_OFF, - presolve=parameters_pb2.EMPHASIS_OFF, - ), - model_parameters=model_parameters_pb2.ModelSolveParametersProto( - solution_hints=[solution_hint] - ), - ) - self.assertEqual( - result.termination.reason, result_pb2.TERMINATION_REASON_FEASIBLE - ) - self.assertTrue(result.solutions) - self.assertAlmostEqual(result.solutions[0].primal_solution.objective_value, 1.0) + self.assertEqual( + result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL + ) + # `solution_values` should at least contain the optimal value 2.0. + self.assertContainsSubset(solution_values, [2.0]) - def test_debug_num_solver(self) -> None: - self.assertEqual(solver.debug_num_solver(), 0) - pybind_solver = solver.new( - parameters_pb2.SOLVER_TYPE_GLOP, - model_pb2.ModelProto(), - parameters_pb2.SolverInitializerProto(), - ) - self.assertEqual(solver.debug_num_solver(), 1) - del pybind_solver - self.assertEqual(solver.debug_num_solver(), 0) + @parameterized.named_parameters( + dict(testcase_name="without_solver", use_solver_class=False), + dict(testcase_name="with_solver", use_solver_class=True), + ) + def test_solution_hint_is_used(self, use_solver_class: bool) -> None: + model = _build_simple_model() + solution_hint = model_parameters_pb2.SolutionHintProto() + solution_hint.variable_values.ids.append(0) + solution_hint.variable_values.values.append(1.0) + # Limit the solver so that it does not find a solution other than provided. + result = _solve_model( + model, + use_solver_class=use_solver_class, + solver_type=parameters_pb2.SOLVER_TYPE_GSCIP, + parameters=parameters_pb2.SolveParametersProto( + node_limit=0, + heuristics=parameters_pb2.EMPHASIS_OFF, + presolve=parameters_pb2.EMPHASIS_OFF, + ), + model_parameters=model_parameters_pb2.ModelSolveParametersProto( + solution_hints=[solution_hint] + ), + ) + self.assertEqual( + result.termination.reason, result_pb2.TERMINATION_REASON_FEASIBLE + ) + self.assertTrue(result.solutions) + self.assertAlmostEqual( + result.solutions[0].primal_solution.objective_value, 1.0 + ) - def test_incremental_solver_update(self) -> None: - model = _build_simple_model() - incremental_solver = solver.new( - parameters_pb2.SOLVER_TYPE_GLOP, - model, - parameters_pb2.SolverInitializerProto(), - ) - result = incremental_solver.solve( - parameters_pb2.SolveParametersProto(), - model_parameters_pb2.ModelSolveParametersProto(), - None, - callback_pb2.CallbackRegistrationProto(), - None, - None, - ) - self.assertEqual( - result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL - ) - self.assertAlmostEqual(result.solve_stats.best_primal_bound, 2.0) - update = model_update_pb2.ModelUpdateProto() - update.variable_updates.upper_bounds.ids.append(0) - update.variable_updates.upper_bounds.values.append(2.5) - self.assertTrue(incremental_solver.update(update)) - result = incremental_solver.solve( - parameters_pb2.SolveParametersProto(), - model_parameters_pb2.ModelSolveParametersProto(), - None, - callback_pb2.CallbackRegistrationProto(), - None, - None, - ) - self.assertEqual( - result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL - ) - self.assertAlmostEqual(result.solve_stats.best_primal_bound, 2.5) + def test_debug_num_solver(self) -> None: + self.assertEqual(solver.debug_num_solver(), 0) + pybind_solver = solver.new( + parameters_pb2.SOLVER_TYPE_GLOP, + model_pb2.ModelProto(), + parameters_pb2.SolverInitializerProto(), + ) + self.assertEqual(solver.debug_num_solver(), 1) + del pybind_solver + self.assertEqual(solver.debug_num_solver(), 0) + + def test_incremental_solver_update(self) -> None: + model = _build_simple_model() + incremental_solver = solver.new( + parameters_pb2.SOLVER_TYPE_GLOP, + model, + parameters_pb2.SolverInitializerProto(), + ) + result = incremental_solver.solve( + parameters_pb2.SolveParametersProto(), + model_parameters_pb2.ModelSolveParametersProto(), + None, + callback_pb2.CallbackRegistrationProto(), + None, + None, + ) + self.assertEqual( + result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL + ) + self.assertAlmostEqual(result.solve_stats.best_primal_bound, 2.0) + update = model_update_pb2.ModelUpdateProto() + update.variable_updates.upper_bounds.ids.append(0) + update.variable_updates.upper_bounds.values.append(2.5) + self.assertTrue(incremental_solver.update(update)) + result = incremental_solver.solve( + parameters_pb2.SolveParametersProto(), + model_parameters_pb2.ModelSolveParametersProto(), + None, + callback_pb2.CallbackRegistrationProto(), + None, + None, + ) + self.assertEqual( + result.termination.reason, result_pb2.TERMINATION_REASON_OPTIMAL + ) + self.assertAlmostEqual(result.solve_stats.best_primal_bound, 2.5) class PybindSolveInterrupterTest(parameterized.TestCase): - def test_solve_interrupter_is_interrupted(self) -> None: - interrupter = solver.SolveInterrupter() - self.assertFalse(interrupter.is_interrupted()) - interrupter.interrupt() - self.assertTrue(interrupter.is_interrupted()) - del interrupter + def test_solve_interrupter_is_interrupted(self) -> None: + interrupter = solver.SolveInterrupter() + self.assertFalse(interrupter.is_interrupted()) + interrupter.interrupt() + self.assertTrue(interrupter.is_interrupted()) + del interrupter if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/elemental/python/elemental_test.py b/ortools/math_opt/elemental/python/elemental_test.py index 355db2de935..c9601c0309e 100644 --- a/ortools/math_opt/elemental/python/elemental_test.py +++ b/ortools/math_opt/elemental/python/elemental_test.py @@ -35,509 +35,535 @@ _OBJECTIVE_QUADRATIC_COEFFICIENT = ( enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT ) -_INDICATOR_CONSTRAINT_INDICATOR = enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR +_INDICATOR_CONSTRAINT_INDICATOR = ( + enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR +) def _sort_attr_keys( attr_keys: np.typing.NDArray[np.int64], ) -> np.typing.NDArray[np.int64]: - """Sorts attr_keys lexicographically.""" - return attr_keys[np.lexsort(np.rot90(attr_keys))] + """Sorts attr_keys lexicographically.""" + return attr_keys[np.lexsort(np.rot90(attr_keys))] class BindingsTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_init_names_not_set(self): - e = cpp_elemental.CppElemental() - self.assertEqual(e.model_name, "") - self.assertEqual(e.primary_objective_name, "") - - def test_init_names_set(self): - e = cpp_elemental.CppElemental(model_name="abc", primary_objective_name="123") - self.assertEqual(e.model_name, "abc") - self.assertEqual(e.primary_objective_name, "123") - - def test_element_operations(self): - e = cpp_elemental.CppElemental() - - # Add two variables. - xs = e.add_elements(_VARIABLE, 2) - - self.assertEqual(e.get_num_elements(_VARIABLE), 2) - self.assertEqual(e.get_next_element_id(_VARIABLE), xs[-1] + 1) - np.testing.assert_array_equal( - e.elements_exist(_VARIABLE, xs), [True, True], strict=True - ) - - # Delete first variable. - e.delete_elements(_VARIABLE, xs[0:1]) - - np.testing.assert_array_equal( - e.elements_exist(_VARIABLE, xs), [False, True], strict=True - ) - - # Add constraint c. - c = e.add_element(_LINEAR_CONSTRAINT, "c") - - self.assertEqual(e.get_num_elements(_LINEAR_CONSTRAINT), 1) - self.assertEqual(e.element_exists(_LINEAR_CONSTRAINT, c), True) - self.assertEqual(e.get_element_name(_LINEAR_CONSTRAINT, c), "c") - np.testing.assert_array_equal(e.get_elements(_VARIABLE), [xs[1]], strict=True) - np.testing.assert_array_equal( - e.get_elements(_LINEAR_CONSTRAINT), - np.array([c], dtype=np.int64), - strict=True, - ) - - def test_ensure_next_element_id_at_least(self): - e = cpp_elemental.CppElemental() - e.ensure_next_element_id_at_least(_VARIABLE, 4) - self.assertEqual(e.add_element(_VARIABLE, "x"), 4) - - def test_name_handling(self): - e = cpp_elemental.CppElemental() - ids = e.add_named_elements( - _LINEAR_CONSTRAINT, - np.array(["c", "name", "a somewhat long name", "a 💩 name"]), - ) - np.testing.assert_array_equal( - e.get_element_names(_LINEAR_CONSTRAINT, ids), - np.array(["c", "name", "a somewhat long name", "a 💩 name"]), - strict=True, - ) - with self.assertRaisesRegex(ValueError, "got 1d array of dtype l"): - e.add_named_elements(_LINEAR_CONSTRAINT, np.array([1, 2, 3])) - with self.assertRaisesRegex(ValueError, "got 2d array of dtype U"): - e.add_named_elements(_LINEAR_CONSTRAINT, np.array([["a", "b"], ["c", "d"]])) - - def test_delete_with_duplicates_raises(self): - e = cpp_elemental.CppElemental() - xs = e.add_elements(_VARIABLE, 1) - with self.assertRaisesRegex(ValueError, "duplicates"): - e.delete_elements(_VARIABLE, np.array([xs[0], xs[0]])) - - def test_element_operations_bad_shape(self): - e = cpp_elemental.CppElemental() - ids = e.add_elements(_VARIABLE, 2) - with self.assertRaisesRegex( - ValueError, "array has incorrect number of dimensions: 2; expected 1" - ): - e.delete_elements(_VARIABLE, np.full((1, 1), ids[0])) - - def test_bad_element_type_raises(self): - e = cpp_elemental.CppElemental() - with self.assertRaisesRegex(TypeError, "incompatible function arguments"): - e.add_elements(-42, 1) # pytype: disable=wrong-arg-types - - def test_attr0(self): - e = cpp_elemental.CppElemental() - keys = np.empty((1, 0), np.int64) - default_value = e.get_attrs(_MAXIMIZE, keys) - self.assertFalse(e.is_attr_non_default(_MAXIMIZE, keys[0])) - self.assertEqual(e.get_attr_num_non_defaults(_MAXIMIZE), 0) - np.testing.assert_array_equal( - e.get_attr_non_defaults(_MAXIMIZE), - np.empty((0, 0), np.int64), - strict=True, - ) - - new_value = np.invert(default_value) - e.set_attrs(_MAXIMIZE, keys, new_value) - - self.assertEqual(e.get_attrs(_MAXIMIZE, keys), new_value) - np.testing.assert_array_equal( - e.bulk_is_attr_non_default(_MAXIMIZE, keys), - np.array([True]), - strict=True, - ) - self.assertEqual(e.get_attr_num_non_defaults(_MAXIMIZE), 1) - np.testing.assert_array_equal( - e.get_attr_non_defaults(_MAXIMIZE), keys, strict=True - ) - - def test_attr1(self): - e = cpp_elemental.CppElemental() - x = e.add_elements(_VARIABLE, 3) - keys = np.column_stack([x]) - np.testing.assert_array_equal( - e.get_attrs(_VARIABLE_LOWER_BOUND, keys), - np.array([-np.inf, -np.inf, -np.inf]), - ) - self.assertEqual(e.get_attr(_VARIABLE_LOWER_BOUND, keys[0]), -math.inf) - np.testing.assert_array_equal( - e.bulk_is_attr_non_default(_VARIABLE_LOWER_BOUND, keys), - np.array([False, False, False]), - strict=True, - ) - self.assertEqual(e.is_attr_non_default(_VARIABLE_LOWER_BOUND, keys[0]), False) - np.testing.assert_array_equal( - e.get_attr_non_defaults(_VARIABLE_LOWER_BOUND), - np.empty((0, 1), np.int64), - strict=True, - ) - - e.set_attrs( - _VARIABLE_LOWER_BOUND, - keys[[0, 2]], - np.array([42.0, 44.0]), - ) - np.testing.assert_array_equal( - e.get_attrs(_VARIABLE_LOWER_BOUND, keys), - np.array([42.0, -np.inf, 44.0]), - strict=True, - ) - e.set_attr(_VARIABLE_LOWER_BOUND, keys[0], 45.0) - np.testing.assert_array_equal( - e.get_attrs(_VARIABLE_LOWER_BOUND, keys), - np.array([45.0, -np.inf, 44.0]), - strict=True, - ) - np.testing.assert_array_equal( - e.bulk_is_attr_non_default(_VARIABLE_LOWER_BOUND, keys), - np.array([True, False, True]), - strict=True, - ) - self.assertEqual(e.get_attr_num_non_defaults(_VARIABLE_LOWER_BOUND), 2) - # Note: sorting the result because ordering is not guaranteed. - np.testing.assert_array_equal( - np.sort(e.get_attr_non_defaults(_VARIABLE_LOWER_BOUND), axis=0), - np.array([[x[0]], [x[2]]]), - strict=True, - ) - - def test_attr2(self): - e = cpp_elemental.CppElemental() - x = e.add_elements(_VARIABLE, 1) - c = e.add_elements(_LINEAR_CONSTRAINT, 1) - keys = np.column_stack([x, c]) - np.testing.assert_array_equal( - e.get_attrs(_LINEAR_CONSTRAINT_COEFFICIENT, keys), - np.array([0.0]), - strict=True, - ) - np.testing.assert_array_equal( - e.bulk_is_attr_non_default(_LINEAR_CONSTRAINT_COEFFICIENT, keys), - np.array([False]), - strict=True, - ) - self.assertEqual(e.get_attr_num_non_defaults(_LINEAR_CONSTRAINT_COEFFICIENT), 0) - np.testing.assert_array_equal( - e.get_attr_non_defaults(_LINEAR_CONSTRAINT_COEFFICIENT), - np.empty((0, 2), np.int64), - strict=True, - ) - - e.set_attrs(_LINEAR_CONSTRAINT_COEFFICIENT, keys, np.array([42.0])) - np.testing.assert_array_equal( - e.get_attrs(_LINEAR_CONSTRAINT_COEFFICIENT, keys), - np.array([42.0]), - strict=True, - ) - np.testing.assert_array_equal( - e.bulk_is_attr_non_default(_LINEAR_CONSTRAINT_COEFFICIENT, keys), - np.array([True]), - strict=True, - ) - self.assertEqual(e.get_attr_num_non_defaults(_LINEAR_CONSTRAINT_COEFFICIENT), 1) - np.testing.assert_array_equal( - e.get_attr_non_defaults(_LINEAR_CONSTRAINT_COEFFICIENT), - keys, - strict=True, - ) - - def test_attr2_symmetric(self): - e = cpp_elemental.CppElemental() - xs = e.add_elements(_VARIABLE, 3) - q01 = [xs[0], xs[1]] - q21 = [xs[2], xs[1]] - q12 = [xs[1], xs[2]] - - e.set_attr(_OBJECTIVE_QUADRATIC_COEFFICIENT, q01, 42.0) - e.set_attr(_OBJECTIVE_QUADRATIC_COEFFICIENT, q21, 43.0) - e.set_attr(_OBJECTIVE_QUADRATIC_COEFFICIENT, q12, 44.0) - self.assertEqual( - e.get_attr_num_non_defaults(_OBJECTIVE_QUADRATIC_COEFFICIENT), 2 - ) - - # Note: sorting the result because ordering is not guaranteed. - np.testing.assert_array_equal( - np.sort(e.get_attr_non_defaults(_OBJECTIVE_QUADRATIC_COEFFICIENT), axis=0), - np.array([q01, q12]), - strict=True, - ) - - def test_attr1_element_valued(self): - e = cpp_elemental.CppElemental() - x = e.add_element(_VARIABLE, "x") - ic = e.add_element(_INDICATOR_CONSTRAINT, "ic") - - e.set_attr(_INDICATOR_CONSTRAINT_INDICATOR, [ic], x) - self.assertEqual( - e.get_attr_num_non_defaults(_INDICATOR_CONSTRAINT_INDICATOR), 1 - ) - - def test_clear_attr0(self): - e = cpp_elemental.CppElemental() - e.set_attr(_MAXIMIZE, (), True) - self.assertTrue(e.get_attr(_MAXIMIZE, ())) - e.clear_attr(_MAXIMIZE) - self.assertFalse(e.get_attr(_MAXIMIZE, ())) - - def test_clear_attr1(self): - e = cpp_elemental.CppElemental() - x = e.add_element(_VARIABLE, "x") - e.set_attr(_VARIABLE_LOWER_BOUND, (x,), 4.0) - self.assertEqual(e.get_attr(_VARIABLE_LOWER_BOUND, (x,)), 4.0) - e.clear_attr(_VARIABLE_LOWER_BOUND) - self.assertEqual(e.get_attr(_VARIABLE_LOWER_BOUND, (x,)), -math.inf) - - def test_attr0_bad_attr_id_raises(self): - e = cpp_elemental.CppElemental() - with self.assertRaisesRegex(TypeError, "incompatible function arguments"): - e.get_attrs(-42, np.array([1])) # pytype: disable=wrong-arg-types - # Note: `assertRaisesRegex` does not seem to work with multiline regexps. - with self.assertRaisesRegex(TypeError, "incompatible function arguments"): - e.get_attrs(_VARIABLE, ()) # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, "attr: BoolAttr0"): - e.get_attrs(_VARIABLE, ()) # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, "attr: DoubleAttr1"): - e.get_attrs(_VARIABLE, ()) # pytype: disable=wrong-arg-types - - def test_attr1_bad_element_id_raises(self): - e = cpp_elemental.CppElemental() - with self.assertRaisesRegex(ValueError, "-1.*variable"): - e.get_attrs(_VARIABLE_LOWER_BOUND, np.array([[-1]])) - - def test_set_attr_with_duplicates_raises(self): - e = cpp_elemental.CppElemental() - x = e.add_elements(_VARIABLE, 2) - with self.assertRaisesRegex(ValueError, "array has duplicates"): - e.set_attrs( - _VARIABLE_LOWER_BOUND, - np.array([[x[0]], [x[1]], [x[1]]]), - np.array([42.0, 44.0, 46.0]), - ) - # We should not have modified any attribute. - self.assertEqual(e.get_attr_num_non_defaults(_VARIABLE_LOWER_BOUND), 0) - - def test_set_attr_with_nonexistent_raises(self): - e = cpp_elemental.CppElemental() - x = e.add_elements(_VARIABLE, 1) - with self.assertRaisesRegex( - ValueError, "linear_constraint id 0 does not exist" - ): - e.set_attrs( - _LINEAR_CONSTRAINT_COEFFICIENT, - np.array([[x[0], -1]]), - np.array([42.0]), - ) - - def test_slice_attr1_success(self): - e = cpp_elemental.CppElemental() - x = e.add_element(_VARIABLE, "x") - y = e.add_element(_VARIABLE, "y") - e.set_attr(_VARIABLE_LOWER_BOUND, (x,), 2.0) - np.testing.assert_array_equal( - e.slice_attr(_VARIABLE_LOWER_BOUND, 0, x), - np.array([[x]], dtype=np.int64), - strict=True, - ) - self.assertEqual(e.get_attr_slice_size(_VARIABLE_LOWER_BOUND, 0, x), 1) - np.testing.assert_array_equal( - e.slice_attr(_VARIABLE_LOWER_BOUND, 0, y), - np.zeros((0, 1), dtype=np.int64), - strict=True, - ) - self.assertEqual(e.get_attr_slice_size(_VARIABLE_LOWER_BOUND, 0, y), 0) - - def test_slice_attr1_invalid_key_index(self): - e = cpp_elemental.CppElemental() - x = e.add_element(_VARIABLE, "x") - with self.assertRaisesRegex(ValueError, "key_index was: -1"): - e.slice_attr(_VARIABLE_LOWER_BOUND, -1, x) - with self.assertRaisesRegex(ValueError, "key_index was: 1"): - e.slice_attr(_VARIABLE_LOWER_BOUND, 1, x) - - def test_slice_attr1_invalid_element_index(self): - e = cpp_elemental.CppElemental() - e.add_element(_VARIABLE, "x") - with self.assertRaisesRegex(ValueError, "no element with id -1"): - e.slice_attr(_VARIABLE_LOWER_BOUND, 0, -1) - with self.assertRaisesRegex(ValueError, "no element with id 4"): - e.slice_attr(_VARIABLE_LOWER_BOUND, 0, 4) - - def test_slice_attr2_success(self): - e = cpp_elemental.CppElemental() - # The first two variables are unused so that all variable and constraint - # indices are all different. - e.add_element(_VARIABLE, "") - e.add_element(_VARIABLE, "") - x = e.add_element(_VARIABLE, "x") - y = e.add_element(_VARIABLE, "y") - z = e.add_element(_VARIABLE, "z") - - c = e.add_element(_LINEAR_CONSTRAINT, "c") - d = e.add_element(_LINEAR_CONSTRAINT, "d") - e.set_attr(_LINEAR_CONSTRAINT_COEFFICIENT, (c, x), 2.0) - e.set_attr(_LINEAR_CONSTRAINT_COEFFICIENT, (d, x), 3.0) - e.set_attr(_LINEAR_CONSTRAINT_COEFFICIENT, (d, y), 4.0) - np.testing.assert_array_equal( - e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 0, c), - np.array([[c, x]], dtype=np.int64), - strict=True, - ) - self.assertEqual(e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 0, c), 1) - - np.testing.assert_array_equal( - _sort_attr_keys(e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 0, d)), - np.array([[d, x], [d, y]], dtype=np.int64), - strict=True, - ) - self.assertEqual(e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 0, d), 2) - - np.testing.assert_array_equal( - _sort_attr_keys(e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 1, x)), - np.array([[c, x], [d, x]], dtype=np.int64), - strict=True, - ) - self.assertEqual(e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 1, x), 2) - - np.testing.assert_array_equal( - e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 1, y), - np.array([[d, y]], dtype=np.int64), - strict=True, - ) - self.assertEqual(e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 1, y), 1) - - np.testing.assert_array_equal( - e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 1, z), - np.zeros((0, 2), dtype=np.int64), - strict=True, - ) - self.assertEqual(e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 1, z), 0) - - def test_clone(self): - e = cpp_elemental.CppElemental(model_name="mmm") - x = e.add_element(_VARIABLE, "x") - e.set_attr(_VARIABLE_LOWER_BOUND, (x,), 4.0) - - e2 = e.clone() - self.assertEqual(e2.model_name, "mmm") - np.testing.assert_array_equal( - e2.get_elements(_VARIABLE), np.array([x], dtype=np.int64), strict=True - ) - np.testing.assert_array_equal( - e2.get_attr_non_defaults(_VARIABLE_LOWER_BOUND), - np.array([[x]], dtype=np.int64), - strict=True, - ) - self.assertEqual(e2.get_attr(_VARIABLE_LOWER_BOUND, (x,)), 4.0) - - def test_clone_with_rename(self): - e = cpp_elemental.CppElemental(model_name="mmm") - x = e.add_element(_VARIABLE, "x") - e2 = e.clone(new_model_name="yyy") - self.assertEqual(e2.model_name, "yyy") - np.testing.assert_array_equal( - e2.get_elements(_VARIABLE), np.array([x], dtype=np.int64), strict=True - ) - - def test_export_model(self): - e = cpp_elemental.CppElemental() - x = e.add_element(_VARIABLE, "x") - e.set_attr(_VARIABLE_LOWER_BOUND, (x,), 4.0) - - expected = model_pb2.ModelProto( - variables=model_pb2.VariablesProto( - ids=[0], - lower_bounds=[4.0], - upper_bounds=[math.inf], - integers=[False], - names=["x"], - ) - ) - self.assert_protos_equal(e.export_model(), expected) - - expected.variables.names[:] = [] - self.assert_protos_equal(e.export_model(remove_names=True), expected) - - def test_repr(self): - e = cpp_elemental.CppElemental() - e.add_element(_VARIABLE, "xyz") - self.assertEqual( - repr(e), - """Model: + def test_init_names_not_set(self): + e = cpp_elemental.CppElemental() + self.assertEqual(e.model_name, "") + self.assertEqual(e.primary_objective_name, "") + + def test_init_names_set(self): + e = cpp_elemental.CppElemental( + model_name="abc", primary_objective_name="123" + ) + self.assertEqual(e.model_name, "abc") + self.assertEqual(e.primary_objective_name, "123") + + def test_element_operations(self): + e = cpp_elemental.CppElemental() + + # Add two variables. + xs = e.add_elements(_VARIABLE, 2) + + self.assertEqual(e.get_num_elements(_VARIABLE), 2) + self.assertEqual(e.get_next_element_id(_VARIABLE), xs[-1] + 1) + np.testing.assert_array_equal( + e.elements_exist(_VARIABLE, xs), [True, True], strict=True + ) + + # Delete first variable. + e.delete_elements(_VARIABLE, xs[0:1]) + + np.testing.assert_array_equal( + e.elements_exist(_VARIABLE, xs), [False, True], strict=True + ) + + # Add constraint c. + c = e.add_element(_LINEAR_CONSTRAINT, "c") + + self.assertEqual(e.get_num_elements(_LINEAR_CONSTRAINT), 1) + self.assertEqual(e.element_exists(_LINEAR_CONSTRAINT, c), True) + self.assertEqual(e.get_element_name(_LINEAR_CONSTRAINT, c), "c") + np.testing.assert_array_equal( + e.get_elements(_VARIABLE), [xs[1]], strict=True + ) + np.testing.assert_array_equal( + e.get_elements(_LINEAR_CONSTRAINT), + np.array([c], dtype=np.int64), + strict=True, + ) + + def test_ensure_next_element_id_at_least(self): + e = cpp_elemental.CppElemental() + e.ensure_next_element_id_at_least(_VARIABLE, 4) + self.assertEqual(e.add_element(_VARIABLE, "x"), 4) + + def test_name_handling(self): + e = cpp_elemental.CppElemental() + ids = e.add_named_elements( + _LINEAR_CONSTRAINT, + np.array(["c", "name", "a somewhat long name", "a 💩 name"]), + ) + np.testing.assert_array_equal( + e.get_element_names(_LINEAR_CONSTRAINT, ids), + np.array(["c", "name", "a somewhat long name", "a 💩 name"]), + strict=True, + ) + with self.assertRaisesRegex(ValueError, "got 1d array of dtype l"): + e.add_named_elements(_LINEAR_CONSTRAINT, np.array([1, 2, 3])) + with self.assertRaisesRegex(ValueError, "got 2d array of dtype U"): + e.add_named_elements( + _LINEAR_CONSTRAINT, np.array([["a", "b"], ["c", "d"]]) + ) + + def test_delete_with_duplicates_raises(self): + e = cpp_elemental.CppElemental() + xs = e.add_elements(_VARIABLE, 1) + with self.assertRaisesRegex(ValueError, "duplicates"): + e.delete_elements(_VARIABLE, np.array([xs[0], xs[0]])) + + def test_element_operations_bad_shape(self): + e = cpp_elemental.CppElemental() + ids = e.add_elements(_VARIABLE, 2) + with self.assertRaisesRegex( + ValueError, "array has incorrect number of dimensions: 2; expected 1" + ): + e.delete_elements(_VARIABLE, np.full((1, 1), ids[0])) + + def test_bad_element_type_raises(self): + e = cpp_elemental.CppElemental() + with self.assertRaisesRegex(TypeError, "incompatible function arguments"): + e.add_elements(-42, 1) # pytype: disable=wrong-arg-types + + def test_attr0(self): + e = cpp_elemental.CppElemental() + keys = np.empty((1, 0), np.int64) + default_value = e.get_attrs(_MAXIMIZE, keys) + self.assertFalse(e.is_attr_non_default(_MAXIMIZE, keys[0])) + self.assertEqual(e.get_attr_num_non_defaults(_MAXIMIZE), 0) + np.testing.assert_array_equal( + e.get_attr_non_defaults(_MAXIMIZE), + np.empty((0, 0), np.int64), + strict=True, + ) + + new_value = np.invert(default_value) + e.set_attrs(_MAXIMIZE, keys, new_value) + + self.assertEqual(e.get_attrs(_MAXIMIZE, keys), new_value) + np.testing.assert_array_equal( + e.bulk_is_attr_non_default(_MAXIMIZE, keys), + np.array([True]), + strict=True, + ) + self.assertEqual(e.get_attr_num_non_defaults(_MAXIMIZE), 1) + np.testing.assert_array_equal( + e.get_attr_non_defaults(_MAXIMIZE), keys, strict=True + ) + + def test_attr1(self): + e = cpp_elemental.CppElemental() + x = e.add_elements(_VARIABLE, 3) + keys = np.column_stack([x]) + np.testing.assert_array_equal( + e.get_attrs(_VARIABLE_LOWER_BOUND, keys), + np.array([-np.inf, -np.inf, -np.inf]), + ) + self.assertEqual(e.get_attr(_VARIABLE_LOWER_BOUND, keys[0]), -math.inf) + np.testing.assert_array_equal( + e.bulk_is_attr_non_default(_VARIABLE_LOWER_BOUND, keys), + np.array([False, False, False]), + strict=True, + ) + self.assertEqual( + e.is_attr_non_default(_VARIABLE_LOWER_BOUND, keys[0]), False + ) + np.testing.assert_array_equal( + e.get_attr_non_defaults(_VARIABLE_LOWER_BOUND), + np.empty((0, 1), np.int64), + strict=True, + ) + + e.set_attrs( + _VARIABLE_LOWER_BOUND, + keys[[0, 2]], + np.array([42.0, 44.0]), + ) + np.testing.assert_array_equal( + e.get_attrs(_VARIABLE_LOWER_BOUND, keys), + np.array([42.0, -np.inf, 44.0]), + strict=True, + ) + e.set_attr(_VARIABLE_LOWER_BOUND, keys[0], 45.0) + np.testing.assert_array_equal( + e.get_attrs(_VARIABLE_LOWER_BOUND, keys), + np.array([45.0, -np.inf, 44.0]), + strict=True, + ) + np.testing.assert_array_equal( + e.bulk_is_attr_non_default(_VARIABLE_LOWER_BOUND, keys), + np.array([True, False, True]), + strict=True, + ) + self.assertEqual(e.get_attr_num_non_defaults(_VARIABLE_LOWER_BOUND), 2) + # Note: sorting the result because ordering is not guaranteed. + np.testing.assert_array_equal( + np.sort(e.get_attr_non_defaults(_VARIABLE_LOWER_BOUND), axis=0), + np.array([[x[0]], [x[2]]]), + strict=True, + ) + + def test_attr2(self): + e = cpp_elemental.CppElemental() + x = e.add_elements(_VARIABLE, 1) + c = e.add_elements(_LINEAR_CONSTRAINT, 1) + keys = np.column_stack([x, c]) + np.testing.assert_array_equal( + e.get_attrs(_LINEAR_CONSTRAINT_COEFFICIENT, keys), + np.array([0.0]), + strict=True, + ) + np.testing.assert_array_equal( + e.bulk_is_attr_non_default(_LINEAR_CONSTRAINT_COEFFICIENT, keys), + np.array([False]), + strict=True, + ) + self.assertEqual( + e.get_attr_num_non_defaults(_LINEAR_CONSTRAINT_COEFFICIENT), 0 + ) + np.testing.assert_array_equal( + e.get_attr_non_defaults(_LINEAR_CONSTRAINT_COEFFICIENT), + np.empty((0, 2), np.int64), + strict=True, + ) + + e.set_attrs(_LINEAR_CONSTRAINT_COEFFICIENT, keys, np.array([42.0])) + np.testing.assert_array_equal( + e.get_attrs(_LINEAR_CONSTRAINT_COEFFICIENT, keys), + np.array([42.0]), + strict=True, + ) + np.testing.assert_array_equal( + e.bulk_is_attr_non_default(_LINEAR_CONSTRAINT_COEFFICIENT, keys), + np.array([True]), + strict=True, + ) + self.assertEqual( + e.get_attr_num_non_defaults(_LINEAR_CONSTRAINT_COEFFICIENT), 1 + ) + np.testing.assert_array_equal( + e.get_attr_non_defaults(_LINEAR_CONSTRAINT_COEFFICIENT), + keys, + strict=True, + ) + + def test_attr2_symmetric(self): + e = cpp_elemental.CppElemental() + xs = e.add_elements(_VARIABLE, 3) + q01 = [xs[0], xs[1]] + q21 = [xs[2], xs[1]] + q12 = [xs[1], xs[2]] + + e.set_attr(_OBJECTIVE_QUADRATIC_COEFFICIENT, q01, 42.0) + e.set_attr(_OBJECTIVE_QUADRATIC_COEFFICIENT, q21, 43.0) + e.set_attr(_OBJECTIVE_QUADRATIC_COEFFICIENT, q12, 44.0) + self.assertEqual( + e.get_attr_num_non_defaults(_OBJECTIVE_QUADRATIC_COEFFICIENT), 2 + ) + + # Note: sorting the result because ordering is not guaranteed. + np.testing.assert_array_equal( + np.sort( + e.get_attr_non_defaults(_OBJECTIVE_QUADRATIC_COEFFICIENT), axis=0 + ), + np.array([q01, q12]), + strict=True, + ) + + def test_attr1_element_valued(self): + e = cpp_elemental.CppElemental() + x = e.add_element(_VARIABLE, "x") + ic = e.add_element(_INDICATOR_CONSTRAINT, "ic") + + e.set_attr(_INDICATOR_CONSTRAINT_INDICATOR, [ic], x) + self.assertEqual( + e.get_attr_num_non_defaults(_INDICATOR_CONSTRAINT_INDICATOR), 1 + ) + + def test_clear_attr0(self): + e = cpp_elemental.CppElemental() + e.set_attr(_MAXIMIZE, (), True) + self.assertTrue(e.get_attr(_MAXIMIZE, ())) + e.clear_attr(_MAXIMIZE) + self.assertFalse(e.get_attr(_MAXIMIZE, ())) + + def test_clear_attr1(self): + e = cpp_elemental.CppElemental() + x = e.add_element(_VARIABLE, "x") + e.set_attr(_VARIABLE_LOWER_BOUND, (x,), 4.0) + self.assertEqual(e.get_attr(_VARIABLE_LOWER_BOUND, (x,)), 4.0) + e.clear_attr(_VARIABLE_LOWER_BOUND) + self.assertEqual(e.get_attr(_VARIABLE_LOWER_BOUND, (x,)), -math.inf) + + def test_attr0_bad_attr_id_raises(self): + e = cpp_elemental.CppElemental() + with self.assertRaisesRegex(TypeError, "incompatible function arguments"): + e.get_attrs(-42, np.array([1])) # pytype: disable=wrong-arg-types + # Note: `assertRaisesRegex` does not seem to work with multiline regexps. + with self.assertRaisesRegex(TypeError, "incompatible function arguments"): + e.get_attrs(_VARIABLE, ()) # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, "attr: BoolAttr0"): + e.get_attrs(_VARIABLE, ()) # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, "attr: DoubleAttr1"): + e.get_attrs(_VARIABLE, ()) # pytype: disable=wrong-arg-types + + def test_attr1_bad_element_id_raises(self): + e = cpp_elemental.CppElemental() + with self.assertRaisesRegex(ValueError, "-1.*variable"): + e.get_attrs(_VARIABLE_LOWER_BOUND, np.array([[-1]])) + + def test_set_attr_with_duplicates_raises(self): + e = cpp_elemental.CppElemental() + x = e.add_elements(_VARIABLE, 2) + with self.assertRaisesRegex(ValueError, "array has duplicates"): + e.set_attrs( + _VARIABLE_LOWER_BOUND, + np.array([[x[0]], [x[1]], [x[1]]]), + np.array([42.0, 44.0, 46.0]), + ) + # We should not have modified any attribute. + self.assertEqual(e.get_attr_num_non_defaults(_VARIABLE_LOWER_BOUND), 0) + + def test_set_attr_with_nonexistent_raises(self): + e = cpp_elemental.CppElemental() + x = e.add_elements(_VARIABLE, 1) + with self.assertRaisesRegex( + ValueError, "linear_constraint id 0 does not exist" + ): + e.set_attrs( + _LINEAR_CONSTRAINT_COEFFICIENT, + np.array([[x[0], -1]]), + np.array([42.0]), + ) + + def test_slice_attr1_success(self): + e = cpp_elemental.CppElemental() + x = e.add_element(_VARIABLE, "x") + y = e.add_element(_VARIABLE, "y") + e.set_attr(_VARIABLE_LOWER_BOUND, (x,), 2.0) + np.testing.assert_array_equal( + e.slice_attr(_VARIABLE_LOWER_BOUND, 0, x), + np.array([[x]], dtype=np.int64), + strict=True, + ) + self.assertEqual(e.get_attr_slice_size(_VARIABLE_LOWER_BOUND, 0, x), 1) + np.testing.assert_array_equal( + e.slice_attr(_VARIABLE_LOWER_BOUND, 0, y), + np.zeros((0, 1), dtype=np.int64), + strict=True, + ) + self.assertEqual(e.get_attr_slice_size(_VARIABLE_LOWER_BOUND, 0, y), 0) + + def test_slice_attr1_invalid_key_index(self): + e = cpp_elemental.CppElemental() + x = e.add_element(_VARIABLE, "x") + with self.assertRaisesRegex(ValueError, "key_index was: -1"): + e.slice_attr(_VARIABLE_LOWER_BOUND, -1, x) + with self.assertRaisesRegex(ValueError, "key_index was: 1"): + e.slice_attr(_VARIABLE_LOWER_BOUND, 1, x) + + def test_slice_attr1_invalid_element_index(self): + e = cpp_elemental.CppElemental() + e.add_element(_VARIABLE, "x") + with self.assertRaisesRegex(ValueError, "no element with id -1"): + e.slice_attr(_VARIABLE_LOWER_BOUND, 0, -1) + with self.assertRaisesRegex(ValueError, "no element with id 4"): + e.slice_attr(_VARIABLE_LOWER_BOUND, 0, 4) + + def test_slice_attr2_success(self): + e = cpp_elemental.CppElemental() + # The first two variables are unused so that all variable and constraint + # indices are all different. + e.add_element(_VARIABLE, "") + e.add_element(_VARIABLE, "") + x = e.add_element(_VARIABLE, "x") + y = e.add_element(_VARIABLE, "y") + z = e.add_element(_VARIABLE, "z") + + c = e.add_element(_LINEAR_CONSTRAINT, "c") + d = e.add_element(_LINEAR_CONSTRAINT, "d") + e.set_attr(_LINEAR_CONSTRAINT_COEFFICIENT, (c, x), 2.0) + e.set_attr(_LINEAR_CONSTRAINT_COEFFICIENT, (d, x), 3.0) + e.set_attr(_LINEAR_CONSTRAINT_COEFFICIENT, (d, y), 4.0) + np.testing.assert_array_equal( + e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 0, c), + np.array([[c, x]], dtype=np.int64), + strict=True, + ) + self.assertEqual( + e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 0, c), 1 + ) + + np.testing.assert_array_equal( + _sort_attr_keys(e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 0, d)), + np.array([[d, x], [d, y]], dtype=np.int64), + strict=True, + ) + self.assertEqual( + e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 0, d), 2 + ) + + np.testing.assert_array_equal( + _sort_attr_keys(e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 1, x)), + np.array([[c, x], [d, x]], dtype=np.int64), + strict=True, + ) + self.assertEqual( + e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 1, x), 2 + ) + + np.testing.assert_array_equal( + e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 1, y), + np.array([[d, y]], dtype=np.int64), + strict=True, + ) + self.assertEqual( + e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 1, y), 1 + ) + + np.testing.assert_array_equal( + e.slice_attr(_LINEAR_CONSTRAINT_COEFFICIENT, 1, z), + np.zeros((0, 2), dtype=np.int64), + strict=True, + ) + self.assertEqual( + e.get_attr_slice_size(_LINEAR_CONSTRAINT_COEFFICIENT, 1, z), 0 + ) + + def test_clone(self): + e = cpp_elemental.CppElemental(model_name="mmm") + x = e.add_element(_VARIABLE, "x") + e.set_attr(_VARIABLE_LOWER_BOUND, (x,), 4.0) + + e2 = e.clone() + self.assertEqual(e2.model_name, "mmm") + np.testing.assert_array_equal( + e2.get_elements(_VARIABLE), np.array([x], dtype=np.int64), strict=True + ) + np.testing.assert_array_equal( + e2.get_attr_non_defaults(_VARIABLE_LOWER_BOUND), + np.array([[x]], dtype=np.int64), + strict=True, + ) + self.assertEqual(e2.get_attr(_VARIABLE_LOWER_BOUND, (x,)), 4.0) + + def test_clone_with_rename(self): + e = cpp_elemental.CppElemental(model_name="mmm") + x = e.add_element(_VARIABLE, "x") + e2 = e.clone(new_model_name="yyy") + self.assertEqual(e2.model_name, "yyy") + np.testing.assert_array_equal( + e2.get_elements(_VARIABLE), np.array([x], dtype=np.int64), strict=True + ) + + def test_export_model(self): + e = cpp_elemental.CppElemental() + x = e.add_element(_VARIABLE, "x") + e.set_attr(_VARIABLE_LOWER_BOUND, (x,), 4.0) + + expected = model_pb2.ModelProto( + variables=model_pb2.VariablesProto( + ids=[0], + lower_bounds=[4.0], + upper_bounds=[math.inf], + integers=[False], + names=["x"], + ) + ) + self.assert_protos_equal(e.export_model(), expected) + + expected.variables.names[:] = [] + self.assert_protos_equal(e.export_model(remove_names=True), expected) + + def test_repr(self): + e = cpp_elemental.CppElemental() + e.add_element(_VARIABLE, "xyz") + self.assertEqual( + repr(e), + """Model: ElementType: variable num_elements: 1 next_id: 1 id: 0 name: "xyz\"""", - ) - - def test_add_and_delete_diffs(self): - e = cpp_elemental.CppElemental() - self.assertEqual(e.add_diff(), 0) - self.assertEqual(e.add_diff(), 1) - e.delete_diff(1) - - def test_export_model_update_has_update(self): - e = cpp_elemental.CppElemental() - d = e.add_diff() - e.add_element(_VARIABLE, "xyz") - - update = e.export_model_update(d) - - self.assertIsNotNone(update) - expected = model_update_pb2.ModelUpdateProto( - new_variables=model_pb2.VariablesProto( - ids=[0], - lower_bounds=[-math.inf], - upper_bounds=[math.inf], - integers=[False], - names=["xyz"], - ) - ) - self.assert_protos_equal(update, expected) - - # Now export again without names - update_no_names = e.export_model_update(d, remove_names=True) - self.assertIsNotNone(update_no_names) - expected.new_variables.names[:] = [] - self.assert_protos_equal(update_no_names, expected) - - def test_export_model_update_empty(self): - e = cpp_elemental.CppElemental() - d = e.add_diff() - update = e.export_model_update(d) - self.assertIsNone(update) - - def test_advance_diff(self): - e = cpp_elemental.CppElemental() - d = e.add_diff() - e.add_element(_VARIABLE, "xyz") - e.advance_diff(d) - update = e.export_model_update(d) - self.assertIsNone(update) - - def test_delete_diff_twice_error(self): - e = cpp_elemental.CppElemental() - self.assertEqual(e.add_diff(), 0) - e.delete_diff(0) - with self.assertRaisesRegex(ValueError, "no diff with id: 0"): - e.delete_diff(0) - - def test_delete_diff_never_created_error(self): - e = cpp_elemental.CppElemental() - with self.assertRaisesRegex(ValueError, "no diff with id: 0"): - e.delete_diff(0) - - def test_export_model_update_diff_never_created(self): - e = cpp_elemental.CppElemental() - with self.assertRaisesRegex(ValueError, "no diff with id: 0"): - e.export_model_update(0) - - def test_advance_diff_never_created(self): - e = cpp_elemental.CppElemental() - with self.assertRaisesRegex(ValueError, "no diff with id: 0"): - e.advance_diff(0) + ) + + def test_add_and_delete_diffs(self): + e = cpp_elemental.CppElemental() + self.assertEqual(e.add_diff(), 0) + self.assertEqual(e.add_diff(), 1) + e.delete_diff(1) + + def test_export_model_update_has_update(self): + e = cpp_elemental.CppElemental() + d = e.add_diff() + e.add_element(_VARIABLE, "xyz") + + update = e.export_model_update(d) + + self.assertIsNotNone(update) + expected = model_update_pb2.ModelUpdateProto( + new_variables=model_pb2.VariablesProto( + ids=[0], + lower_bounds=[-math.inf], + upper_bounds=[math.inf], + integers=[False], + names=["xyz"], + ) + ) + self.assert_protos_equal(update, expected) + + # Now export again without names + update_no_names = e.export_model_update(d, remove_names=True) + self.assertIsNotNone(update_no_names) + expected.new_variables.names[:] = [] + self.assert_protos_equal(update_no_names, expected) + + def test_export_model_update_empty(self): + e = cpp_elemental.CppElemental() + d = e.add_diff() + update = e.export_model_update(d) + self.assertIsNone(update) + + def test_advance_diff(self): + e = cpp_elemental.CppElemental() + d = e.add_diff() + e.add_element(_VARIABLE, "xyz") + e.advance_diff(d) + update = e.export_model_update(d) + self.assertIsNone(update) + + def test_delete_diff_twice_error(self): + e = cpp_elemental.CppElemental() + self.assertEqual(e.add_diff(), 0) + e.delete_diff(0) + with self.assertRaisesRegex(ValueError, "no diff with id: 0"): + e.delete_diff(0) + + def test_delete_diff_never_created_error(self): + e = cpp_elemental.CppElemental() + with self.assertRaisesRegex(ValueError, "no diff with id: 0"): + e.delete_diff(0) + + def test_export_model_update_diff_never_created(self): + e = cpp_elemental.CppElemental() + with self.assertRaisesRegex(ValueError, "no diff with id: 0"): + e.export_model_update(0) + + def test_advance_diff_never_created(self): + e = cpp_elemental.CppElemental() + with self.assertRaisesRegex(ValueError, "no diff with id: 0"): + e.advance_diff(0) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/elemental/python/enums_test.py b/ortools/math_opt/elemental/python/enums_test.py index d55e9357892..f4ab692484e 100644 --- a/ortools/math_opt/elemental/python/enums_test.py +++ b/ortools/math_opt/elemental/python/enums_test.py @@ -18,13 +18,13 @@ class EnumsTest(absltest.TestCase): - def test_element_type_enums(self): - self.assertEqual(enums.ElementType.VARIABLE.value, 0) + def test_element_type_enums(self): + self.assertEqual(enums.ElementType.VARIABLE.value, 0) - def test_attr_enums(self): - self.assertEqual(enums.BoolAttr0.MAXIMIZE.value, 0) - self.assertEqual(enums.DoubleAttr1.VARIABLE_UPPER_BOUND.value, 1) + def test_attr_enums(self): + self.assertEqual(enums.BoolAttr0.MAXIMIZE.value, 0) + self.assertEqual(enums.DoubleAttr1.VARIABLE_UPPER_BOUND.value, 1) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/io/proto_converter.cc b/ortools/math_opt/io/proto_converter.cc index 96ffaf24d02..8dd21764c34 100644 --- a/ortools/math_opt/io/proto_converter.cc +++ b/ortools/math_opt/io/proto_converter.cc @@ -317,7 +317,7 @@ MPModelProtoToMathOptModel(const ::operations_research::MPModelProto& model) { for (const MPGeneralConstraintProto& general_constraint : model.general_constraint()) { - const std::string& in_name = general_constraint.name(); + absl::string_view in_name = general_constraint.name(); switch (general_constraint.general_constraint_case()) { case MPGeneralConstraintProto::kQuadraticConstraint: { (*output.mutable_quadratic_constraints()) diff --git a/ortools/math_opt/io/python/mps_converter_test.py b/ortools/math_opt/io/python/mps_converter_test.py index 956a05206f7..d6a10567974 100644 --- a/ortools/math_opt/io/python/mps_converter_test.py +++ b/ortools/math_opt/io/python/mps_converter_test.py @@ -45,32 +45,32 @@ def simple_model_proto() -> model_pb2.ModelProto: - model = mathopt.Model(name="unbounded_integers") - x = model.add_variable(name="x", lb=1, ub=float("inf"), is_integer=True) - y = model.add_variable(name="y", lb=4, ub=float("inf"), is_integer=True) - model.add_linear_constraint(x + y >= 2, name="c") - model.minimize(x + y) - return model.export_model() + model = mathopt.Model(name="unbounded_integers") + x = model.add_variable(name="x", lb=1, ub=float("inf"), is_integer=True) + y = model.add_variable(name="y", lb=4, ub=float("inf"), is_integer=True) + model.add_linear_constraint(x + y >= 2, name="c") + model.minimize(x + y) + return model.export_model() class MPSConverterTest(absltest.TestCase, compare_proto.MathOptProtoAssertions): - def test_convert_empty_mps_to_model_proto(self) -> None: - simple_mps = "NAME MIN_SIZE_MAX_FEATURES" - model_proto = mps_converter.mps_to_model_proto(simple_mps) - self.assertEqual(model_proto.name, "MIN_SIZE_MAX_FEATURES") + def test_convert_empty_mps_to_model_proto(self) -> None: + simple_mps = "NAME MIN_SIZE_MAX_FEATURES" + model_proto = mps_converter.mps_to_model_proto(simple_mps) + self.assertEqual(model_proto.name, "MIN_SIZE_MAX_FEATURES") - def test_convert_simple_mps_to_model(self) -> None: - model_proto = mps_converter.mps_to_model_proto(_MODEL_MPS) - expected_model_proto = simple_model_proto() + def test_convert_simple_mps_to_model(self) -> None: + model_proto = mps_converter.mps_to_model_proto(_MODEL_MPS) + expected_model_proto = simple_model_proto() - self.assert_protos_equiv(model_proto, expected_model_proto) + self.assert_protos_equiv(model_proto, expected_model_proto) - def test_convert_model_proto_to_mps(self) -> None: - model_proto = simple_model_proto() - mps = mps_converter.model_proto_to_mps(model_proto) - self.assertEqual(mps, _MODEL_MPS) + def test_convert_model_proto_to_mps(self) -> None: + model_proto = simple_model_proto() + mps = mps_converter.model_proto_to_mps(model_proto) + self.assertEqual(mps, _MODEL_MPS) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/bounded_expressions.py b/ortools/math_opt/python/bounded_expressions.py index 768c687ee28..b75a3fc9a3d 100644 --- a/ortools/math_opt/python/bounded_expressions.py +++ b/ortools/math_opt/python/bounded_expressions.py @@ -29,154 +29,156 @@ def _raise_binary_operator_type_error( rhs: Type[Any], extra_message: Optional[str] = None, ) -> NoReturn: - """Raises TypeError on unsupported operators.""" - message = ( - f"unsupported operand type(s) for {operator}: {lhs.__name__!r} and" - f" {rhs.__name__!r}" - ) - if extra_message is not None: - message += "\n" + extra_message - raise TypeError(message) + """Raises TypeError on unsupported operators.""" + message = ( + f"unsupported operand type(s) for {operator}: {lhs.__name__!r} and" + f" {rhs.__name__!r}" + ) + if extra_message is not None: + message += "\n" + extra_message + raise TypeError(message) T = TypeVar("T") class BoundedExpression(Generic[T]): - """An inequality of the form lower_bound <= expression <= upper_bound. - - Where: - * expression is a T, typically LinearBase or QuadraticBase. - * lower_bound is a float. - * upper_bound is a float. - - Note: Because of limitations related to Python's handling of chained - comparisons, bounded expressions cannot be directly created usign - overloaded comparisons as in `lower_bound <= expression <= upper_bound`. - One solution is to wrap one of the inequalities in parenthesis as in - `(lower_bound <= expression) <= upper_bound`. - """ - - __slots__ = "_expression", "_lower_bound", "_upper_bound" - - def __init__(self, lower_bound: float, expression: T, upper_bound: float) -> None: - self._expression: T = expression - self._lower_bound: float = lower_bound - self._upper_bound: float = upper_bound - - @property - def expression(self) -> T: - return self._expression - - @property - def lower_bound(self) -> float: - return self._lower_bound - - @property - def upper_bound(self) -> float: - return self._upper_bound - - def __bool__(self) -> bool: - raise TypeError( - "__bool__ is unsupported for BoundedExpression" - + "\n" - + _CHAINED_COMPARISON_MESSAGE - ) + """An inequality of the form lower_bound <= expression <= upper_bound. + + Where: + * expression is a T, typically LinearBase or QuadraticBase. + * lower_bound is a float. + * upper_bound is a float. + + Note: Because of limitations related to Python's handling of chained + comparisons, bounded expressions cannot be directly created usign + overloaded comparisons as in `lower_bound <= expression <= upper_bound`. + One solution is to wrap one of the inequalities in parenthesis as in + `(lower_bound <= expression) <= upper_bound`. + """ + + __slots__ = "_expression", "_lower_bound", "_upper_bound" + + def __init__( + self, lower_bound: float, expression: T, upper_bound: float + ) -> None: + self._expression: T = expression + self._lower_bound: float = lower_bound + self._upper_bound: float = upper_bound + + @property + def expression(self) -> T: + return self._expression + + @property + def lower_bound(self) -> float: + return self._lower_bound + + @property + def upper_bound(self) -> float: + return self._upper_bound + + def __bool__(self) -> bool: + raise TypeError( + "__bool__ is unsupported for BoundedExpression" + + "\n" + + _CHAINED_COMPARISON_MESSAGE + ) - def __str__(self): - return f"{self._lower_bound} <= {self._expression!s} <= {self._upper_bound}" + def __str__(self): + return f"{self._lower_bound} <= {self._expression!s} <= {self._upper_bound}" - def __repr__(self): - return f"{self._lower_bound} <= {self._expression!r} <= {self._upper_bound}" + def __repr__(self): + return f"{self._lower_bound} <= {self._expression!r} <= {self._upper_bound}" class UpperBoundedExpression(Generic[T]): - """An inequality of the form expression <= upper_bound. - - Where: - * expression is a T, and - * upper_bound is a float - """ - - __slots__ = "_expression", "_upper_bound" - - def __init__(self, expression: T, upper_bound: float) -> None: - """Operator overloading can be used instead: e.g. `x + y <= 2.0`.""" - self._expression: T = expression - self._upper_bound: float = upper_bound - - @property - def expression(self) -> T: - return self._expression - - @property - def lower_bound(self) -> float: - return -math.inf - - @property - def upper_bound(self) -> float: - return self._upper_bound - - def __ge__(self, lhs: float) -> BoundedExpression[T]: - if isinstance(lhs, (int, float)): - return BoundedExpression[T](lhs, self.expression, self.upper_bound) - _raise_binary_operator_type_error(">=", type(self), type(lhs)) - - def __bool__(self) -> bool: - raise TypeError( - "__bool__ is unsupported for UpperBoundedExpression" - + "\n" - + _CHAINED_COMPARISON_MESSAGE - ) + """An inequality of the form expression <= upper_bound. + + Where: + * expression is a T, and + * upper_bound is a float + """ + + __slots__ = "_expression", "_upper_bound" + + def __init__(self, expression: T, upper_bound: float) -> None: + """Operator overloading can be used instead: e.g. `x + y <= 2.0`.""" + self._expression: T = expression + self._upper_bound: float = upper_bound + + @property + def expression(self) -> T: + return self._expression + + @property + def lower_bound(self) -> float: + return -math.inf + + @property + def upper_bound(self) -> float: + return self._upper_bound + + def __ge__(self, lhs: float) -> BoundedExpression[T]: + if isinstance(lhs, (int, float)): + return BoundedExpression[T](lhs, self.expression, self.upper_bound) + _raise_binary_operator_type_error(">=", type(self), type(lhs)) + + def __bool__(self) -> bool: + raise TypeError( + "__bool__ is unsupported for UpperBoundedExpression" + + "\n" + + _CHAINED_COMPARISON_MESSAGE + ) - def __str__(self): - return f"{self._expression!s} <= {self._upper_bound}" + def __str__(self): + return f"{self._expression!s} <= {self._upper_bound}" - def __repr__(self): - return f"{self._expression!r} <= {self._upper_bound}" + def __repr__(self): + return f"{self._expression!r} <= {self._upper_bound}" class LowerBoundedExpression(Generic[T]): - """An inequality of the form expression >= lower_bound. - - Where: - * expression is a linear expression, and - * lower_bound is a float - """ - - __slots__ = "_expression", "_lower_bound" - - def __init__(self, expression: T, lower_bound: float) -> None: - """Operator overloading can be used instead: e.g. `x + y >= 2.0`.""" - self._expression: T = expression - self._lower_bound: float = lower_bound - - @property - def expression(self) -> T: - return self._expression - - @property - def lower_bound(self) -> float: - return self._lower_bound - - @property - def upper_bound(self) -> float: - return math.inf - - def __le__(self, rhs: float) -> BoundedExpression[T]: - if isinstance(rhs, (int, float)): - return BoundedExpression[T](self.lower_bound, self.expression, rhs) - _raise_binary_operator_type_error("<=", type(self), type(rhs)) - - def __bool__(self) -> bool: - raise TypeError( - "__bool__ is unsupported for LowerBoundedExpression" - + "\n" - + _CHAINED_COMPARISON_MESSAGE - ) + """An inequality of the form expression >= lower_bound. + + Where: + * expression is a linear expression, and + * lower_bound is a float + """ + + __slots__ = "_expression", "_lower_bound" + + def __init__(self, expression: T, lower_bound: float) -> None: + """Operator overloading can be used instead: e.g. `x + y >= 2.0`.""" + self._expression: T = expression + self._lower_bound: float = lower_bound + + @property + def expression(self) -> T: + return self._expression + + @property + def lower_bound(self) -> float: + return self._lower_bound + + @property + def upper_bound(self) -> float: + return math.inf + + def __le__(self, rhs: float) -> BoundedExpression[T]: + if isinstance(rhs, (int, float)): + return BoundedExpression[T](self.lower_bound, self.expression, rhs) + _raise_binary_operator_type_error("<=", type(self), type(rhs)) + + def __bool__(self) -> bool: + raise TypeError( + "__bool__ is unsupported for LowerBoundedExpression" + + "\n" + + _CHAINED_COMPARISON_MESSAGE + ) - def __str__(self): - return f"{self._expression!s} >= {self._lower_bound}" + def __str__(self): + return f"{self._expression!s} >= {self._lower_bound}" - def __repr__(self): - return f"{self._expression!r} >= {self._lower_bound}" + def __repr__(self): + return f"{self._expression!r} >= {self._lower_bound}" diff --git a/ortools/math_opt/python/bounded_expressions_test.py b/ortools/math_opt/python/bounded_expressions_test.py index 224c6a00127..6ee80821c8d 100644 --- a/ortools/math_opt/python/bounded_expressions_test.py +++ b/ortools/math_opt/python/bounded_expressions_test.py @@ -22,62 +22,62 @@ class BoundedExpressionTest(absltest.TestCase): - def test_bounded_expression_read(self) -> None: - b = bounded_expressions.BoundedExpression( - lower_bound=-3.0, expression="e123", upper_bound=4.5 - ) - self.assertEqual(b.lower_bound, -3.0) - self.assertEqual(b.upper_bound, 4.5) - self.assertEqual(b.expression, "e123") - self.assertEqual(str(b), "-3.0 <= e123 <= 4.5") - self.assertEqual(repr(b), "-3.0 <= 'e123' <= 4.5") - with self.assertRaisesRegex(TypeError, _BAD_BOOL_ERROR): - bool(b) + def test_bounded_expression_read(self) -> None: + b = bounded_expressions.BoundedExpression( + lower_bound=-3.0, expression="e123", upper_bound=4.5 + ) + self.assertEqual(b.lower_bound, -3.0) + self.assertEqual(b.upper_bound, 4.5) + self.assertEqual(b.expression, "e123") + self.assertEqual(str(b), "-3.0 <= e123 <= 4.5") + self.assertEqual(repr(b), "-3.0 <= 'e123' <= 4.5") + with self.assertRaisesRegex(TypeError, _BAD_BOOL_ERROR): + bool(b) - def test_lower_bounded_expression_read(self) -> None: - b = bounded_expressions.LowerBoundedExpression( - lower_bound=-3.0, expression="e123" - ) - self.assertEqual(b.lower_bound, -3.0) - self.assertEqual(b.upper_bound, math.inf) - self.assertEqual(b.expression, "e123") - self.assertEqual(str(b), "e123 >= -3.0") - self.assertEqual(repr(b), "'e123' >= -3.0") - with self.assertRaisesRegex(TypeError, _BAD_BOOL_ERROR): - bool(b) + def test_lower_bounded_expression_read(self) -> None: + b = bounded_expressions.LowerBoundedExpression( + lower_bound=-3.0, expression="e123" + ) + self.assertEqual(b.lower_bound, -3.0) + self.assertEqual(b.upper_bound, math.inf) + self.assertEqual(b.expression, "e123") + self.assertEqual(str(b), "e123 >= -3.0") + self.assertEqual(repr(b), "'e123' >= -3.0") + with self.assertRaisesRegex(TypeError, _BAD_BOOL_ERROR): + bool(b) - def test_upper_bounded_expression_read(self) -> None: - b = bounded_expressions.UpperBoundedExpression( - expression="e123", upper_bound=4.5 - ) - self.assertEqual(b.lower_bound, -math.inf) - self.assertEqual(b.upper_bound, 4.5) - self.assertEqual(b.expression, "e123") - self.assertEqual(str(b), "e123 <= 4.5") - self.assertEqual(repr(b), "'e123' <= 4.5") - with self.assertRaisesRegex(TypeError, _BAD_BOOL_ERROR): - bool(b) + def test_upper_bounded_expression_read(self) -> None: + b = bounded_expressions.UpperBoundedExpression( + expression="e123", upper_bound=4.5 + ) + self.assertEqual(b.lower_bound, -math.inf) + self.assertEqual(b.upper_bound, 4.5) + self.assertEqual(b.expression, "e123") + self.assertEqual(str(b), "e123 <= 4.5") + self.assertEqual(repr(b), "'e123' <= 4.5") + with self.assertRaisesRegex(TypeError, _BAD_BOOL_ERROR): + bool(b) - def test_lower_bounded_to_bounded(self) -> None: - lb = bounded_expressions.LowerBoundedExpression( - lower_bound=-3.0, expression="e123" - ) - bounded = lb <= 4.5 - self.assertIsInstance(bounded, bounded_expressions.BoundedExpression) - self.assertEqual(bounded.lower_bound, -3.0) - self.assertEqual(bounded.upper_bound, 4.5) - self.assertEqual(bounded.expression, "e123") + def test_lower_bounded_to_bounded(self) -> None: + lb = bounded_expressions.LowerBoundedExpression( + lower_bound=-3.0, expression="e123" + ) + bounded = lb <= 4.5 + self.assertIsInstance(bounded, bounded_expressions.BoundedExpression) + self.assertEqual(bounded.lower_bound, -3.0) + self.assertEqual(bounded.upper_bound, 4.5) + self.assertEqual(bounded.expression, "e123") - def test_upper_bounded_to_bounded(self) -> None: - ub = bounded_expressions.UpperBoundedExpression( - expression="e123", upper_bound=4.5 - ) - bounded = -3.0 <= ub - self.assertIsInstance(bounded, bounded_expressions.BoundedExpression) - self.assertEqual(bounded.lower_bound, -3.0) - self.assertEqual(bounded.upper_bound, 4.5) - self.assertEqual(bounded.expression, "e123") + def test_upper_bounded_to_bounded(self) -> None: + ub = bounded_expressions.UpperBoundedExpression( + expression="e123", upper_bound=4.5 + ) + bounded = -3.0 <= ub + self.assertIsInstance(bounded, bounded_expressions.BoundedExpression) + self.assertEqual(bounded.lower_bound, -3.0) + self.assertEqual(bounded.upper_bound, 4.5) + self.assertEqual(bounded.expression, "e123") if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/callback.py b/ortools/math_opt/python/callback.py index 36f0dfdef26..86af96a9e31 100644 --- a/ortools/math_opt/python/callback.py +++ b/ortools/math_opt/python/callback.py @@ -27,35 +27,35 @@ @enum.unique class Event(enum.Enum): - """The supported events during a solve for callbacks. - - * UNSPECIFIED: The event is unknown (typically an internal error). - * PRESOLVE: The solver is currently running presolve. Gurobi only. - * SIMPLEX: The solver is currently running the simplex method. Gurobi only. - * MIP: The solver is in the MIP loop (called periodically before starting a - new node). Useful for early termination. Note that this event does not - provide information on LP relaxations nor about new incumbent solutions. - Gurobi only. - * MIP_SOLUTION: Called every time a new MIP incumbent is found. Fully - supported by Gurobi, partially supported by CP-SAT (you can observe new - solutions, but not add lazy constraints). - * MIP_NODE: Called inside a MIP node. Note that there is no guarantee that the - callback function will be called on every node. That behavior is - solver-dependent. Gurobi only. - - Disabling cuts using SolveParameters may interfere with this event being - called and/or adding cuts at this event, the behavior is solver specific. - * BARRIER: Called in each iterate of an interior point/barrier method. Gurobi - only. - """ - - UNSPECIFIED = callback_pb2.CALLBACK_EVENT_UNSPECIFIED - PRESOLVE = callback_pb2.CALLBACK_EVENT_PRESOLVE - SIMPLEX = callback_pb2.CALLBACK_EVENT_SIMPLEX - MIP = callback_pb2.CALLBACK_EVENT_MIP - MIP_SOLUTION = callback_pb2.CALLBACK_EVENT_MIP_SOLUTION - MIP_NODE = callback_pb2.CALLBACK_EVENT_MIP_NODE - BARRIER = callback_pb2.CALLBACK_EVENT_BARRIER + """The supported events during a solve for callbacks. + + * UNSPECIFIED: The event is unknown (typically an internal error). + * PRESOLVE: The solver is currently running presolve. Gurobi only. + * SIMPLEX: The solver is currently running the simplex method. Gurobi only. + * MIP: The solver is in the MIP loop (called periodically before starting a + new node). Useful for early termination. Note that this event does not + provide information on LP relaxations nor about new incumbent solutions. + Gurobi only. + * MIP_SOLUTION: Called every time a new MIP incumbent is found. Fully + supported by Gurobi, partially supported by CP-SAT (you can observe new + solutions, but not add lazy constraints). + * MIP_NODE: Called inside a MIP node. Note that there is no guarantee that the + callback function will be called on every node. That behavior is + solver-dependent. Gurobi only. + + Disabling cuts using SolveParameters may interfere with this event being + called and/or adding cuts at this event, the behavior is solver specific. + * BARRIER: Called in each iterate of an interior point/barrier method. Gurobi + only. + """ + + UNSPECIFIED = callback_pb2.CALLBACK_EVENT_UNSPECIFIED + PRESOLVE = callback_pb2.CALLBACK_EVENT_PRESOLVE + SIMPLEX = callback_pb2.CALLBACK_EVENT_SIMPLEX + MIP = callback_pb2.CALLBACK_EVENT_MIP + MIP_SOLUTION = callback_pb2.CALLBACK_EVENT_MIP_SOLUTION + MIP_NODE = callback_pb2.CALLBACK_EVENT_MIP_NODE + BARRIER = callback_pb2.CALLBACK_EVENT_BARRIER PresolveStats = callback_pb2.CallbackDataProto.PresolveStats @@ -66,293 +66,299 @@ class Event(enum.Enum): @dataclasses.dataclass class CallbackData: - """Input to the solve callback (produced by the solver). - - Attributes: - event: The current state of the solver when the callback is run. The event - (partially) determines what data is available and what the user is allowed - to return. - solution: A solution to the primal optimization problem, if available. For - Event.MIP_SOLUTION, solution is always present, integral, and feasible. - For Event.MIP_NODE, the primal_solution contains the current LP-node - relaxation. In some cases, no solution will be available (e.g. because LP - was infeasible or the solve was imprecise). Empty for other events. - messages: Logs generated by the underlying solver, as a list of strings - without new lines (each string is a line). Only filled on Event.MESSAGE. - runtime: The time since Solve() was invoked. - presolve_stats: Filled for Event.PRESOLVE only. - simplex_stats: Filled for Event.SIMPLEX only. - barrier_stats: Filled for Event.BARRIER only. - mip_stats: Filled for the events MIP, MIP_SOLUTION and MIP_NODE only. - """ - - event: Event = Event.UNSPECIFIED - solution: Optional[Dict[variables.Variable, float]] = None - messages: List[str] = dataclasses.field(default_factory=list) - runtime: datetime.timedelta = datetime.timedelta() - presolve_stats: PresolveStats = dataclasses.field(default_factory=PresolveStats) - simplex_stats: SimplexStats = dataclasses.field(default_factory=SimplexStats) - barrier_stats: BarrierStats = dataclasses.field(default_factory=BarrierStats) - mip_stats: MipStats = dataclasses.field(default_factory=MipStats) + """Input to the solve callback (produced by the solver). + + Attributes: + event: The current state of the solver when the callback is run. The event + (partially) determines what data is available and what the user is allowed + to return. + solution: A solution to the primal optimization problem, if available. For + Event.MIP_SOLUTION, solution is always present, integral, and feasible. + For Event.MIP_NODE, the primal_solution contains the current LP-node + relaxation. In some cases, no solution will be available (e.g. because LP + was infeasible or the solve was imprecise). Empty for other events. + messages: Logs generated by the underlying solver, as a list of strings + without new lines (each string is a line). Only filled on Event.MESSAGE. + runtime: The time since Solve() was invoked. + presolve_stats: Filled for Event.PRESOLVE only. + simplex_stats: Filled for Event.SIMPLEX only. + barrier_stats: Filled for Event.BARRIER only. + mip_stats: Filled for the events MIP, MIP_SOLUTION and MIP_NODE only. + """ + + event: Event = Event.UNSPECIFIED + solution: Optional[Dict[variables.Variable, float]] = None + messages: List[str] = dataclasses.field(default_factory=list) + runtime: datetime.timedelta = datetime.timedelta() + presolve_stats: PresolveStats = dataclasses.field( + default_factory=PresolveStats + ) + simplex_stats: SimplexStats = dataclasses.field(default_factory=SimplexStats) + barrier_stats: BarrierStats = dataclasses.field(default_factory=BarrierStats) + mip_stats: MipStats = dataclasses.field(default_factory=MipStats) def parse_callback_data( cb_data: callback_pb2.CallbackDataProto, mod: model.Model ) -> CallbackData: - """Creates a CallbackData from an equivalent proto. - - Args: - cb_data: A protocol buffer with the information the user needs for a - callback. - mod: The model being solved. - - Returns: - An equivalent CallbackData. - - Raises: - ValueError: if cb_data is invalid or inconsistent with mod, e.g. cb_data - refers to a variable id not in mod. - """ - result = CallbackData() - result.event = Event(cb_data.event) - if cb_data.HasField("primal_solution_vector"): - primal_solution = cb_data.primal_solution_vector - result.solution = { - mod.get_variable(id): val - for (id, val) in zip(primal_solution.ids, primal_solution.values) - } - result.runtime = cb_data.runtime.ToTimedelta() - result.presolve_stats = cb_data.presolve_stats - result.simplex_stats = cb_data.simplex_stats - result.barrier_stats = cb_data.barrier_stats - result.mip_stats = cb_data.mip_stats - return result + """Creates a CallbackData from an equivalent proto. + + Args: + cb_data: A protocol buffer with the information the user needs for a + callback. + mod: The model being solved. + + Returns: + An equivalent CallbackData. + + Raises: + ValueError: if cb_data is invalid or inconsistent with mod, e.g. cb_data + refers to a variable id not in mod. + """ + result = CallbackData() + result.event = Event(cb_data.event) + if cb_data.HasField("primal_solution_vector"): + primal_solution = cb_data.primal_solution_vector + result.solution = { + mod.get_variable(id): val + for (id, val) in zip(primal_solution.ids, primal_solution.values) + } + result.runtime = cb_data.runtime.ToTimedelta() + result.presolve_stats = cb_data.presolve_stats + result.simplex_stats = cb_data.simplex_stats + result.barrier_stats = cb_data.barrier_stats + result.mip_stats = cb_data.mip_stats + return result @dataclasses.dataclass class CallbackRegistration: - """Request the events and input data and reports output types for a callback. - - Note that it is an error to add a constraint in a callback without setting - add_cuts and/or add_lazy_constraints to true. - - Attributes: - events: When the callback should be invoked, by default, never. If an - unsupported event for a solver/model combination is selected, an - excecption is raised, see Event above for details. - mip_solution_filter: restricts the variable values returned in - CallbackData.solution (the callback argument) at each MIP_SOLUTION event. - By default, values are returned for all variables. - mip_node_filter: restricts the variable values returned in - CallbackData.solution (the callback argument) at each MIP_NODE event. By - default, values are returned for all variables. - add_cuts: The callback may add "user cuts" (linear constraints that - strengthen the LP without cutting of integer points) at MIP_NODE events. - add_lazy_constraints: The callback may add "lazy constraints" (linear - constraints that cut off integer solutions) at MIP_NODE or MIP_SOLUTION - events. - """ - - events: Set[Event] = dataclasses.field(default_factory=set) - mip_solution_filter: sparse_containers.VariableFilter = ( - sparse_containers.VariableFilter() + """Request the events and input data and reports output types for a callback. + + Note that it is an error to add a constraint in a callback without setting + add_cuts and/or add_lazy_constraints to true. + + Attributes: + events: When the callback should be invoked, by default, never. If an + unsupported event for a solver/model combination is selected, an + excecption is raised, see Event above for details. + mip_solution_filter: restricts the variable values returned in + CallbackData.solution (the callback argument) at each MIP_SOLUTION event. + By default, values are returned for all variables. + mip_node_filter: restricts the variable values returned in + CallbackData.solution (the callback argument) at each MIP_NODE event. By + default, values are returned for all variables. + add_cuts: The callback may add "user cuts" (linear constraints that + strengthen the LP without cutting of integer points) at MIP_NODE events. + add_lazy_constraints: The callback may add "lazy constraints" (linear + constraints that cut off integer solutions) at MIP_NODE or MIP_SOLUTION + events. + """ + + events: Set[Event] = dataclasses.field(default_factory=set) + mip_solution_filter: sparse_containers.VariableFilter = ( + sparse_containers.VariableFilter() + ) + mip_node_filter: sparse_containers.VariableFilter = ( + sparse_containers.VariableFilter() + ) + add_cuts: bool = False + add_lazy_constraints: bool = False + + def to_proto(self) -> callback_pb2.CallbackRegistrationProto: + """Returns an equivalent proto to this CallbackRegistration.""" + result = callback_pb2.CallbackRegistrationProto() + result.request_registration[:] = sorted( + [event.value for event in self.events] ) - mip_node_filter: sparse_containers.VariableFilter = ( - sparse_containers.VariableFilter() - ) - add_cuts: bool = False - add_lazy_constraints: bool = False - - def to_proto(self) -> callback_pb2.CallbackRegistrationProto: - """Returns an equivalent proto to this CallbackRegistration.""" - result = callback_pb2.CallbackRegistrationProto() - result.request_registration[:] = sorted([event.value for event in self.events]) - result.mip_solution_filter.CopyFrom(self.mip_solution_filter.to_proto()) - result.mip_node_filter.CopyFrom(self.mip_node_filter.to_proto()) - result.add_cuts = self.add_cuts - result.add_lazy_constraints = self.add_lazy_constraints - return result + result.mip_solution_filter.CopyFrom(self.mip_solution_filter.to_proto()) + result.mip_node_filter.CopyFrom(self.mip_node_filter.to_proto()) + result.add_cuts = self.add_cuts + result.add_lazy_constraints = self.add_lazy_constraints + return result @dataclasses.dataclass class GeneratedConstraint: - """A linear constraint to add inside a callback. - - Models a constraint of the form: - lb <= sum_{i in I} a_i * x_i <= ub - - Two types of generated linear constraints are supported based on is_lazy: - * The "lazy constraint" can remove integer points from the feasible - region and can be added at event Event.MIP_NODE or - Event.MIP_SOLUTION - * The "user cut" (on is_lazy=false) strengthens the LP without removing - integer points. It can only be added at Event.MIP_NODE. - - - Attributes: - terms: The variables and linear coefficients in the constraint, a_i and x_i - in the model above. - lower_bound: lb in the model above. - upper_bound: ub in the model above. - is_lazy: Indicates if the constraint should be interpreted as a "lazy - constraint" (cuts off integer solutions) or a "user cut" (strengthens the - LP relaxation without cutting of integer solutions). - """ - - terms: Mapping[variables.Variable, float] = dataclasses.field(default_factory=dict) - lower_bound: float = -math.inf - upper_bound: float = math.inf - is_lazy: bool = False - - def to_proto( - self, - ) -> callback_pb2.CallbackResultProto.GeneratedLinearConstraint: - """Returns an equivalent proto for the constraint.""" - result = callback_pb2.CallbackResultProto.GeneratedLinearConstraint() - result.is_lazy = self.is_lazy - result.lower_bound = self.lower_bound - result.upper_bound = self.upper_bound - result.linear_expression.CopyFrom( - sparse_containers.to_sparse_double_vector_proto(self.terms) - ) - return result + """A linear constraint to add inside a callback. + + Models a constraint of the form: + lb <= sum_{i in I} a_i * x_i <= ub + + Two types of generated linear constraints are supported based on is_lazy: + * The "lazy constraint" can remove integer points from the feasible + region and can be added at event Event.MIP_NODE or + Event.MIP_SOLUTION + * The "user cut" (on is_lazy=false) strengthens the LP without removing + integer points. It can only be added at Event.MIP_NODE. + + + Attributes: + terms: The variables and linear coefficients in the constraint, a_i and x_i + in the model above. + lower_bound: lb in the model above. + upper_bound: ub in the model above. + is_lazy: Indicates if the constraint should be interpreted as a "lazy + constraint" (cuts off integer solutions) or a "user cut" (strengthens the + LP relaxation without cutting of integer solutions). + """ + + terms: Mapping[variables.Variable, float] = dataclasses.field( + default_factory=dict + ) + lower_bound: float = -math.inf + upper_bound: float = math.inf + is_lazy: bool = False + + def to_proto( + self, + ) -> callback_pb2.CallbackResultProto.GeneratedLinearConstraint: + """Returns an equivalent proto for the constraint.""" + result = callback_pb2.CallbackResultProto.GeneratedLinearConstraint() + result.is_lazy = self.is_lazy + result.lower_bound = self.lower_bound + result.upper_bound = self.upper_bound + result.linear_expression.CopyFrom( + sparse_containers.to_sparse_double_vector_proto(self.terms) + ) + return result @dataclasses.dataclass class CallbackResult: - """The value returned by a solve callback (produced by the user). - - Attributes: - terminate: When true it tells the solver to interrupt the solve as soon as - possible. - - It can be set from any event. This is equivalent to using a - SolveInterrupter and triggering it from the callback. - - Some solvers don't support interruption, in that case this is simply - ignored and the solve terminates as usual. On top of that solvers may not - immediately stop the solve. Thus the user should expect the callback to - still be called after they set `terminate` to true in a previous - call. Returning with `terminate` false after having previously returned - true won't cancel the interruption. - generated_constraints: Constraints to add to the model. For details, see - GeneratedConstraint documentation. - suggested_solutions: A list of solutions (or partially defined solutions) to - suggest to the solver. Some solvers (e.g. gurobi) will try and convert a - partial solution into a full solution by solving a MIP. Use only for - Event.MIP_NODE. - """ + """The value returned by a solve callback (produced by the user). + + Attributes: + terminate: When true it tells the solver to interrupt the solve as soon as + possible. + + It can be set from any event. This is equivalent to using a + SolveInterrupter and triggering it from the callback. + + Some solvers don't support interruption, in that case this is simply + ignored and the solve terminates as usual. On top of that solvers may not + immediately stop the solve. Thus the user should expect the callback to + still be called after they set `terminate` to true in a previous + call. Returning with `terminate` false after having previously returned + true won't cancel the interruption. + generated_constraints: Constraints to add to the model. For details, see + GeneratedConstraint documentation. + suggested_solutions: A list of solutions (or partially defined solutions) to + suggest to the solver. Some solvers (e.g. gurobi) will try and convert a + partial solution into a full solution by solving a MIP. Use only for + Event.MIP_NODE. + """ + + terminate: bool = False + generated_constraints: List[GeneratedConstraint] = dataclasses.field( + default_factory=list + ) + suggested_solutions: List[Mapping[variables.Variable, float]] = ( + dataclasses.field(default_factory=list) + ) + + def add_generated_constraint( + self, + bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None, + *, + lb: Optional[float] = None, + ub: Optional[float] = None, + expr: Optional[variables.LinearTypes] = None, + is_lazy: bool, + ) -> None: + """Adds a linear constraint to the list of generated constraints. + + The constraint can be of two exclusive types: a "lazy constraint" or a + "user cut. A "user cut" is a constraint that excludes the current LP + solution, but does not cut off any integer-feasible points that satisfy the + already added constraints (either in callbacks or through + Model.add_linear_constraint()). A "lazy constraint" is a constraint that + excludes such integer-feasible points and hence is needed for corrctness of + the forlumation. + + The simplest way to specify the constraint is by passing a one-sided or + two-sided linear inequality as in: + * add_generated_constraint(x + y + 1.0 <= 2.0, is_lazy=True), + * add_generated_constraint(x + y >= 2.0, is_lazy=True), or + * add_generated_constraint((1.0 <= x + y) <= 2.0, is_lazy=True). + + Note the extra parenthesis for two-sided linear inequalities, which is + required due to some language limitations (see + https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). + If the parenthesis are omitted, a TypeError will be raised explaining the + issue (if this error was not raised the first inequality would have been + silently ignored because of the noted language limitations). + + The second way to specify the constraint is by setting lb, ub, and/o expr as + in: + * add_generated_constraint(expr=x + y + 1.0, ub=2.0, is_lazy=True), + * add_generated_constraint(expr=x + y, lb=2.0, is_lazy=True), + * add_generated_constraint(expr=x + y, lb=1.0, ub=2.0, is_lazy=True), or + * add_generated_constraint(lb=1.0, is_lazy=True). + Omitting lb is equivalent to setting it to -math.inf and omiting ub is + equivalent to setting it to math.inf. + + These two alternatives are exclusive and a combined call like: + * add_generated_constraint(x + y <= 2.0, lb=1.0, is_lazy=True), or + * add_generated_constraint(x + y <= 2.0, ub=math.inf, is_lazy=True) + will raise a ValueError. A ValueError is also raised if expr's offset is + infinite. - terminate: bool = False - generated_constraints: List[GeneratedConstraint] = dataclasses.field( - default_factory=list - ) - suggested_solutions: List[Mapping[variables.Variable, float]] = dataclasses.field( - default_factory=list + Args: + bounded_expr: a linear inequality describing the constraint. Cannot be + specified together with lb, ub, or expr. + lb: The constraint's lower bound if bounded_expr is omitted (if both + bounder_expr and lb are omitted, the lower bound is -math.inf). + ub: The constraint's upper bound if bounded_expr is omitted (if both + bounder_expr and ub are omitted, the upper bound is math.inf). + expr: The constraint's linear expression if bounded_expr is omitted. + is_lazy: Whether the constraint is lazy or not. + """ + norm_ineq = normalized_inequality.as_normalized_linear_inequality( + bounded_expr, lb=lb, ub=ub, expr=expr ) - - def add_generated_constraint( - self, - bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None, - *, - lb: Optional[float] = None, - ub: Optional[float] = None, - expr: Optional[variables.LinearTypes] = None, - is_lazy: bool, - ) -> None: - """Adds a linear constraint to the list of generated constraints. - - The constraint can be of two exclusive types: a "lazy constraint" or a - "user cut. A "user cut" is a constraint that excludes the current LP - solution, but does not cut off any integer-feasible points that satisfy the - already added constraints (either in callbacks or through - Model.add_linear_constraint()). A "lazy constraint" is a constraint that - excludes such integer-feasible points and hence is needed for corrctness of - the forlumation. - - The simplest way to specify the constraint is by passing a one-sided or - two-sided linear inequality as in: - * add_generated_constraint(x + y + 1.0 <= 2.0, is_lazy=True), - * add_generated_constraint(x + y >= 2.0, is_lazy=True), or - * add_generated_constraint((1.0 <= x + y) <= 2.0, is_lazy=True). - - Note the extra parenthesis for two-sided linear inequalities, which is - required due to some language limitations (see - https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). - If the parenthesis are omitted, a TypeError will be raised explaining the - issue (if this error was not raised the first inequality would have been - silently ignored because of the noted language limitations). - - The second way to specify the constraint is by setting lb, ub, and/o expr as - in: - * add_generated_constraint(expr=x + y + 1.0, ub=2.0, is_lazy=True), - * add_generated_constraint(expr=x + y, lb=2.0, is_lazy=True), - * add_generated_constraint(expr=x + y, lb=1.0, ub=2.0, is_lazy=True), or - * add_generated_constraint(lb=1.0, is_lazy=True). - Omitting lb is equivalent to setting it to -math.inf and omiting ub is - equivalent to setting it to math.inf. - - These two alternatives are exclusive and a combined call like: - * add_generated_constraint(x + y <= 2.0, lb=1.0, is_lazy=True), or - * add_generated_constraint(x + y <= 2.0, ub=math.inf, is_lazy=True) - will raise a ValueError. A ValueError is also raised if expr's offset is - infinite. - - Args: - bounded_expr: a linear inequality describing the constraint. Cannot be - specified together with lb, ub, or expr. - lb: The constraint's lower bound if bounded_expr is omitted (if both - bounder_expr and lb are omitted, the lower bound is -math.inf). - ub: The constraint's upper bound if bounded_expr is omitted (if both - bounder_expr and ub are omitted, the upper bound is math.inf). - expr: The constraint's linear expression if bounded_expr is omitted. - is_lazy: Whether the constraint is lazy or not. - """ - norm_ineq = normalized_inequality.as_normalized_linear_inequality( - bounded_expr, lb=lb, ub=ub, expr=expr - ) - self.generated_constraints.append( - GeneratedConstraint( - lower_bound=norm_ineq.lb, - terms=norm_ineq.coefficients, - upper_bound=norm_ineq.ub, - is_lazy=is_lazy, - ) + self.generated_constraints.append( + GeneratedConstraint( + lower_bound=norm_ineq.lb, + terms=norm_ineq.coefficients, + upper_bound=norm_ineq.ub, + is_lazy=is_lazy, ) + ) - def add_lazy_constraint( - self, - bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None, - *, - lb: Optional[float] = None, - ub: Optional[float] = None, - expr: Optional[variables.LinearTypes] = None, - ) -> None: - """Shortcut for add_generated_constraint(..., is_lazy=True)..""" - self.add_generated_constraint( - bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=True - ) + def add_lazy_constraint( + self, + bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None, + *, + lb: Optional[float] = None, + ub: Optional[float] = None, + expr: Optional[variables.LinearTypes] = None, + ) -> None: + """Shortcut for add_generated_constraint(..., is_lazy=True)..""" + self.add_generated_constraint( + bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=True + ) - def add_user_cut( - self, - bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None, - *, - lb: Optional[float] = None, - ub: Optional[float] = None, - expr: Optional[variables.LinearTypes] = None, - ) -> None: - """Shortcut for add_generated_constraint(..., is_lazy=False).""" - self.add_generated_constraint( - bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=False - ) + def add_user_cut( + self, + bounded_expr: Optional[Union[bool, variables.BoundedLinearTypes]] = None, + *, + lb: Optional[float] = None, + ub: Optional[float] = None, + expr: Optional[variables.LinearTypes] = None, + ) -> None: + """Shortcut for add_generated_constraint(..., is_lazy=False).""" + self.add_generated_constraint( + bounded_expr, lb=lb, ub=ub, expr=expr, is_lazy=False + ) - def to_proto(self) -> callback_pb2.CallbackResultProto: - """Returns a proto equivalent to this CallbackResult.""" - result = callback_pb2.CallbackResultProto(terminate=self.terminate) - for generated_constraint in self.generated_constraints: - result.cuts.add().CopyFrom(generated_constraint.to_proto()) - for suggested_solution in self.suggested_solutions: - result.suggested_solutions.add().CopyFrom( - sparse_containers.to_sparse_double_vector_proto(suggested_solution) - ) - return result + def to_proto(self) -> callback_pb2.CallbackResultProto: + """Returns a proto equivalent to this CallbackResult.""" + result = callback_pb2.CallbackResultProto(terminate=self.terminate) + for generated_constraint in self.generated_constraints: + result.cuts.add().CopyFrom(generated_constraint.to_proto()) + for suggested_solution in self.suggested_solutions: + result.suggested_solutions.add().CopyFrom( + sparse_containers.to_sparse_double_vector_proto(suggested_solution) + ) + return result diff --git a/ortools/math_opt/python/callback_test.py b/ortools/math_opt/python/callback_test.py index 30ce057ecf1..3c4fcd23e80 100644 --- a/ortools/math_opt/python/callback_test.py +++ b/ortools/math_opt/python/callback_test.py @@ -26,233 +26,241 @@ class CallbackDataTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_parse_callback_data_no_solution(self) -> None: - mod = model.Model(name="test_model") - cb_data_proto = callback_pb2.CallbackDataProto( - event=callback_pb2.CALLBACK_EVENT_PRESOLVE - ) - cb_data_proto.runtime.FromTimedelta(datetime.timedelta(seconds=16.0)) - cb_data_proto.presolve_stats.removed_variables = 10 - cb_data_proto.simplex_stats.iteration_count = 3 - cb_data_proto.barrier_stats.primal_objective = 2.0 - cb_data_proto.mip_stats.open_nodes = 5 - cb_data = callback.parse_callback_data(cb_data_proto, mod) - self.assertEqual(cb_data.event, callback.Event.PRESOLVE) - self.assertIsNone(cb_data.solution) - self.assertEqual(16.0, cb_data.runtime.seconds) - self.assert_protos_equiv( - cb_data.presolve_stats, - callback_pb2.CallbackDataProto.PresolveStats(removed_variables=10), - ) - self.assert_protos_equiv( - cb_data.simplex_stats, - callback_pb2.CallbackDataProto.SimplexStats(iteration_count=3), - ) - self.assert_protos_equiv( - cb_data.barrier_stats, - callback_pb2.CallbackDataProto.BarrierStats(primal_objective=2.0), - ) - self.assert_protos_equiv( - cb_data.mip_stats, callback_pb2.CallbackDataProto.MipStats(open_nodes=5) - ) + def test_parse_callback_data_no_solution(self) -> None: + mod = model.Model(name="test_model") + cb_data_proto = callback_pb2.CallbackDataProto( + event=callback_pb2.CALLBACK_EVENT_PRESOLVE + ) + cb_data_proto.runtime.FromTimedelta(datetime.timedelta(seconds=16.0)) + cb_data_proto.presolve_stats.removed_variables = 10 + cb_data_proto.simplex_stats.iteration_count = 3 + cb_data_proto.barrier_stats.primal_objective = 2.0 + cb_data_proto.mip_stats.open_nodes = 5 + cb_data = callback.parse_callback_data(cb_data_proto, mod) + self.assertEqual(cb_data.event, callback.Event.PRESOLVE) + self.assertIsNone(cb_data.solution) + self.assertEqual(16.0, cb_data.runtime.seconds) + self.assert_protos_equiv( + cb_data.presolve_stats, + callback_pb2.CallbackDataProto.PresolveStats(removed_variables=10), + ) + self.assert_protos_equiv( + cb_data.simplex_stats, + callback_pb2.CallbackDataProto.SimplexStats(iteration_count=3), + ) + self.assert_protos_equiv( + cb_data.barrier_stats, + callback_pb2.CallbackDataProto.BarrierStats(primal_objective=2.0), + ) + self.assert_protos_equiv( + cb_data.mip_stats, callback_pb2.CallbackDataProto.MipStats(open_nodes=5) + ) - def test_parse_callback_data_with_solution(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - cb_data_proto = callback_pb2.CallbackDataProto( - event=callback_pb2.CALLBACK_EVENT_MIP_SOLUTION - ) - solution = cb_data_proto.primal_solution_vector - solution.ids[:] = [0, 1] - solution.values[:] = [0.0, 1.0] - cb_data_proto.runtime.FromTimedelta(datetime.timedelta(seconds=12.0)) - cb_data = callback.parse_callback_data(cb_data_proto, mod) - self.assertEqual(cb_data.event, callback.Event.MIP_SOLUTION) - self.assertDictEqual(cb_data.solution, {x: 0.0, y: 1.0}) - self.assertListEqual(cb_data.messages, []) - self.assertEqual(12.0, cb_data.runtime.seconds) - self.assert_protos_equiv( - cb_data.presolve_stats, callback_pb2.CallbackDataProto.PresolveStats() - ) - self.assert_protos_equiv( - cb_data.simplex_stats, callback_pb2.CallbackDataProto.SimplexStats() - ) - self.assert_protos_equiv( - cb_data.barrier_stats, callback_pb2.CallbackDataProto.BarrierStats() - ) - self.assert_protos_equiv( - cb_data.mip_stats, callback_pb2.CallbackDataProto.MipStats() - ) + def test_parse_callback_data_with_solution(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + cb_data_proto = callback_pb2.CallbackDataProto( + event=callback_pb2.CALLBACK_EVENT_MIP_SOLUTION + ) + solution = cb_data_proto.primal_solution_vector + solution.ids[:] = [0, 1] + solution.values[:] = [0.0, 1.0] + cb_data_proto.runtime.FromTimedelta(datetime.timedelta(seconds=12.0)) + cb_data = callback.parse_callback_data(cb_data_proto, mod) + self.assertEqual(cb_data.event, callback.Event.MIP_SOLUTION) + self.assertDictEqual(cb_data.solution, {x: 0.0, y: 1.0}) + self.assertListEqual(cb_data.messages, []) + self.assertEqual(12.0, cb_data.runtime.seconds) + self.assert_protos_equiv( + cb_data.presolve_stats, callback_pb2.CallbackDataProto.PresolveStats() + ) + self.assert_protos_equiv( + cb_data.simplex_stats, callback_pb2.CallbackDataProto.SimplexStats() + ) + self.assert_protos_equiv( + cb_data.barrier_stats, callback_pb2.CallbackDataProto.BarrierStats() + ) + self.assert_protos_equiv( + cb_data.mip_stats, callback_pb2.CallbackDataProto.MipStats() + ) -class CallbackRegistrationTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): +class CallbackRegistrationTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): - def testToProto(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") + def testToProto(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") - reg = callback.CallbackRegistration() - reg.events = {callback.Event.MIP_SOLUTION, callback.Event.MIP_NODE} - reg.mip_node_filter = sparse_containers.VariableFilter(filtered_items=(z, x)) - reg.mip_solution_filter = sparse_containers.VariableFilter( - skip_zero_values=True - ) - reg.add_lazy_constraints = True - reg.add_cuts = False + reg = callback.CallbackRegistration() + reg.events = {callback.Event.MIP_SOLUTION, callback.Event.MIP_NODE} + reg.mip_node_filter = sparse_containers.VariableFilter( + filtered_items=(z, x) + ) + reg.mip_solution_filter = sparse_containers.VariableFilter( + skip_zero_values=True + ) + reg.add_lazy_constraints = True + reg.add_cuts = False - self.assert_protos_equiv( - reg.to_proto(), - callback_pb2.CallbackRegistrationProto( - request_registration=[ - callback_pb2.CALLBACK_EVENT_MIP_SOLUTION, - callback_pb2.CALLBACK_EVENT_MIP_NODE, - ], - mip_node_filter=sparse_containers_pb2.SparseVectorFilterProto( - filter_by_ids=True, filtered_ids=[0, 2] - ), - mip_solution_filter=sparse_containers_pb2.SparseVectorFilterProto( - skip_zero_values=True - ), - add_lazy_constraints=True, - add_cuts=False, + self.assert_protos_equiv( + reg.to_proto(), + callback_pb2.CallbackRegistrationProto( + request_registration=[ + callback_pb2.CALLBACK_EVENT_MIP_SOLUTION, + callback_pb2.CALLBACK_EVENT_MIP_NODE, + ], + mip_node_filter=sparse_containers_pb2.SparseVectorFilterProto( + filter_by_ids=True, filtered_ids=[0, 2] + ), + mip_solution_filter=sparse_containers_pb2.SparseVectorFilterProto( + skip_zero_values=True ), - ) + add_lazy_constraints=True, + add_cuts=False, + ), + ) class GeneratedLinearConstraintTest( compare_proto.MathOptProtoAssertions, absltest.TestCase ): - def testToProto(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") + def testToProto(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") - gen_con = callback.GeneratedConstraint() - gen_con.terms = {x: 2.0, z: 4.0} - gen_con.lower_bound = -math.inf - gen_con.upper_bound = 5.0 - gen_con.is_lazy = True + gen_con = callback.GeneratedConstraint() + gen_con.terms = {x: 2.0, z: 4.0} + gen_con.lower_bound = -math.inf + gen_con.upper_bound = 5.0 + gen_con.is_lazy = True - self.assert_protos_equiv( - gen_con.to_proto(), - callback_pb2.CallbackResultProto.GeneratedLinearConstraint( - lower_bound=-math.inf, - upper_bound=5.0, - is_lazy=True, - linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 2], values=[2.0, 4.0] - ), + self.assert_protos_equiv( + gen_con.to_proto(), + callback_pb2.CallbackResultProto.GeneratedLinearConstraint( + lower_bound=-math.inf, + upper_bound=5.0, + is_lazy=True, + linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 2], values=[2.0, 4.0] ), - ) + ), + ) -class CallbackResultTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): +class CallbackResultTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): - def testToProto(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") + def testToProto(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") - result = callback.CallbackResult() - result.terminate = True - # Test le/ge combinations to avoid mutants. - result.add_lazy_constraint(2 * x <= 0) - result.add_lazy_constraint(2 * x >= 0) - result.add_user_cut(2 * z >= 2) - result.add_user_cut(2 * z <= 2) - result.add_generated_constraint(expr=2 * z, lb=2, is_lazy=False) - result.add_generated_constraint(expr=2 * z, ub=2, is_lazy=False) - result.suggested_solutions.append({x: 1.0, y: 0.0, z: 1.0}) - result.suggested_solutions.append({x: 0.0, y: 0.0, z: 0.0}) + result = callback.CallbackResult() + result.terminate = True + # Test le/ge combinations to avoid mutants. + result.add_lazy_constraint(2 * x <= 0) + result.add_lazy_constraint(2 * x >= 0) + result.add_user_cut(2 * z >= 2) + result.add_user_cut(2 * z <= 2) + result.add_generated_constraint(expr=2 * z, lb=2, is_lazy=False) + result.add_generated_constraint(expr=2 * z, ub=2, is_lazy=False) + result.suggested_solutions.append({x: 1.0, y: 0.0, z: 1.0}) + result.suggested_solutions.append({x: 0.0, y: 0.0, z: 0.0}) - expected = callback_pb2.CallbackResultProto( - terminate=True, - cuts=[ - callback_pb2.CallbackResultProto.GeneratedLinearConstraint( - lower_bound=-math.inf, - upper_bound=0.0, - is_lazy=True, - linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[2.0] - ), - ), - callback_pb2.CallbackResultProto.GeneratedLinearConstraint( - lower_bound=0.0, - upper_bound=math.inf, - is_lazy=True, - linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[2.0] - ), - ), - callback_pb2.CallbackResultProto.GeneratedLinearConstraint( - lower_bound=2.0, - upper_bound=math.inf, - is_lazy=False, - linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[2], values=[2.0] - ), + expected = callback_pb2.CallbackResultProto( + terminate=True, + cuts=[ + callback_pb2.CallbackResultProto.GeneratedLinearConstraint( + lower_bound=-math.inf, + upper_bound=0.0, + is_lazy=True, + linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[2.0] ), - callback_pb2.CallbackResultProto.GeneratedLinearConstraint( - lower_bound=-math.inf, - upper_bound=2.0, - is_lazy=False, - linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[2], values=[2.0] - ), + ), + callback_pb2.CallbackResultProto.GeneratedLinearConstraint( + lower_bound=0.0, + upper_bound=math.inf, + is_lazy=True, + linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[2.0] ), - callback_pb2.CallbackResultProto.GeneratedLinearConstraint( - lower_bound=2.0, - upper_bound=math.inf, - is_lazy=False, - linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[2], values=[2.0] - ), + ), + callback_pb2.CallbackResultProto.GeneratedLinearConstraint( + lower_bound=2.0, + upper_bound=math.inf, + is_lazy=False, + linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[2], values=[2.0] ), - callback_pb2.CallbackResultProto.GeneratedLinearConstraint( - lower_bound=-math.inf, - upper_bound=2.0, - is_lazy=False, - linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[2], values=[2.0] - ), + ), + callback_pb2.CallbackResultProto.GeneratedLinearConstraint( + lower_bound=-math.inf, + upper_bound=2.0, + is_lazy=False, + linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[2], values=[2.0] ), - ], - suggested_solutions=[ - sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1, 2], values=[1.0, 0.0, 1.0] + ), + callback_pb2.CallbackResultProto.GeneratedLinearConstraint( + lower_bound=2.0, + upper_bound=math.inf, + is_lazy=False, + linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[2], values=[2.0] ), - sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1, 2], values=[0.0, 0.0, 0.0] + ), + callback_pb2.CallbackResultProto.GeneratedLinearConstraint( + lower_bound=-math.inf, + upper_bound=2.0, + is_lazy=False, + linear_expression=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[2], values=[2.0] ), - ], - ) - self.assert_protos_equiv(result.to_proto(), expected) + ), + ], + suggested_solutions=[ + sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1, 2], values=[1.0, 0.0, 1.0] + ), + sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1, 2], values=[0.0, 0.0, 0.0] + ), + ], + ) + self.assert_protos_equiv(result.to_proto(), expected) - def testConstraintErrors(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") + def testConstraintErrors(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") - result = callback.CallbackResult() - with self.assertRaisesRegex( - TypeError, - "unsupported operand.*\n.*two or more non-constant linear expressions", - ): - result.add_lazy_constraint(x <= (y <= z)) - with self.assertRaisesRegex(AssertionError, "lb cannot be specified.*"): - result.add_user_cut(x + y == 1, lb=1) + result = callback.CallbackResult() + with self.assertRaisesRegex( + TypeError, + "unsupported operand.*\n.*two or more non-constant linear expressions", + ): + result.add_lazy_constraint(x <= (y <= z)) + with self.assertRaisesRegex(AssertionError, "lb cannot be specified.*"): + result.add_user_cut(x + y == 1, lb=1) - def testToProtoEmpty(self) -> None: - result = callback.CallbackResult() - self.assert_protos_equiv(result.to_proto(), callback_pb2.CallbackResultProto()) + def testToProtoEmpty(self) -> None: + result = callback.CallbackResult() + self.assert_protos_equiv( + result.to_proto(), callback_pb2.CallbackResultProto() + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/compute_infeasible_subsystem_result.py b/ortools/math_opt/python/compute_infeasible_subsystem_result.py index 0777630c24e..1c4e864bfce 100644 --- a/ortools/math_opt/python/compute_infeasible_subsystem_result.py +++ b/ortools/math_opt/python/compute_infeasible_subsystem_result.py @@ -27,173 +27,180 @@ @dataclasses.dataclass(frozen=True) class ModelSubsetBounds: - """Presence of the upper and lower bounds in a two-sided constraint. + """Presence of the upper and lower bounds in a two-sided constraint. - E.g. for 1 <= x <= 2, `lower` is the constraint 1 <= x and `upper` is the - constraint x <= 2. + E.g. for 1 <= x <= 2, `lower` is the constraint 1 <= x and `upper` is the + constraint x <= 2. - Attributes: - lower: If the lower bound half of the two-sided constraint is selected. - upper: If the upper bound half of the two-sided constraint is selected. - """ + Attributes: + lower: If the lower bound half of the two-sided constraint is selected. + upper: If the upper bound half of the two-sided constraint is selected. + """ - lower: bool = False - upper: bool = False + lower: bool = False + upper: bool = False - def empty(self) -> bool: - """Is empty if both `lower` and `upper` are False.""" - return not (self.lower or self.upper) + def empty(self) -> bool: + """Is empty if both `lower` and `upper` are False.""" + return not (self.lower or self.upper) - def to_proto(self) -> infeasible_subsystem_pb2.ModelSubsetProto.Bounds: - """Returns an equivalent proto message for these bounds.""" - return infeasible_subsystem_pb2.ModelSubsetProto.Bounds( - lower=self.lower, upper=self.upper - ) + def to_proto(self) -> infeasible_subsystem_pb2.ModelSubsetProto.Bounds: + """Returns an equivalent proto message for these bounds.""" + return infeasible_subsystem_pb2.ModelSubsetProto.Bounds( + lower=self.lower, upper=self.upper + ) def parse_model_subset_bounds( bounds: infeasible_subsystem_pb2.ModelSubsetProto.Bounds, ) -> ModelSubsetBounds: - """Returns an equivalent `ModelSubsetBounds` to the input proto.""" - return ModelSubsetBounds(lower=bounds.lower, upper=bounds.upper) + """Returns an equivalent `ModelSubsetBounds` to the input proto.""" + return ModelSubsetBounds(lower=bounds.lower, upper=bounds.upper) @dataclasses.dataclass(frozen=True) class ModelSubset: - """A subset of a Model's constraints (including variable bounds/integrality). - - When returned from `solve.compute_infeasible_subsystem`, the contained - `ModelSubsetBounds` will all be nonempty. - - Attributes: - variable_bounds: The upper and/or lower bound constraints on these variables - are included in the subset. - variable_integrality: The constraint that a variable is integer is included - in the subset. - linear_constraints: The upper and/or lower bounds from these linear - constraints are included in the subset. + """A subset of a Model's constraints (including variable bounds/integrality). + + When returned from `solve.compute_infeasible_subsystem`, the contained + `ModelSubsetBounds` will all be nonempty. + + Attributes: + variable_bounds: The upper and/or lower bound constraints on these variables + are included in the subset. + variable_integrality: The constraint that a variable is integer is included + in the subset. + linear_constraints: The upper and/or lower bounds from these linear + constraints are included in the subset. + """ + + variable_bounds: Mapping[variables_mod.Variable, ModelSubsetBounds] = ( + immutabledict.immutabledict() + ) + variable_integrality: FrozenSet[variables_mod.Variable] = frozenset() + linear_constraints: Mapping[ + linear_constraints_mod.LinearConstraint, ModelSubsetBounds + ] = immutabledict.immutabledict() + + def empty(self) -> bool: + """Returns true if all the nested constraint collections are empty. + + Warning: When `self.variable_bounds` or `self.linear_constraints` contain + only ModelSubsetBounds which are themselves empty, this function will return + False. + + Returns: + True if this is empty. """ - - variable_bounds: Mapping[variables_mod.Variable, ModelSubsetBounds] = ( - immutabledict.immutabledict() + return not ( + self.variable_bounds + or self.variable_integrality + or self.linear_constraints ) - variable_integrality: FrozenSet[variables_mod.Variable] = frozenset() - linear_constraints: Mapping[ - linear_constraints_mod.LinearConstraint, ModelSubsetBounds - ] = immutabledict.immutabledict() - - def empty(self) -> bool: - """Returns true if all the nested constraint collections are empty. - - Warning: When `self.variable_bounds` or `self.linear_constraints` contain - only ModelSubsetBounds which are themselves empty, this function will return - False. - - Returns: - True if this is empty. - """ - return not ( - self.variable_bounds or self.variable_integrality or self.linear_constraints - ) - - def to_proto(self) -> infeasible_subsystem_pb2.ModelSubsetProto: - """Returns an equivalent proto message for this `ModelSubset`.""" - return infeasible_subsystem_pb2.ModelSubsetProto( - variable_bounds={ - var.id: bounds.to_proto() - for (var, bounds) in self.variable_bounds.items() - }, - variable_integrality=sorted(var.id for var in self.variable_integrality), - linear_constraints={ - con.id: bounds.to_proto() - for (con, bounds) in self.linear_constraints.items() - }, - ) - -def parse_model_subset( - model_subset: infeasible_subsystem_pb2.ModelSubsetProto, mod: model.Model -) -> ModelSubset: - """Returns an equivalent `ModelSubset` to the input proto.""" - if model_subset.quadratic_constraints: - raise NotImplementedError( - "quadratic_constraints not yet implemented for ModelSubset in Python" - ) - if model_subset.second_order_cone_constraints: - raise NotImplementedError( - "second_order_cone_constraints not yet implemented for ModelSubset in" - " Python" - ) - if model_subset.sos1_constraints: - raise NotImplementedError( - "sos1_constraints not yet implemented for ModelSubset in Python" - ) - if model_subset.sos2_constraints: - raise NotImplementedError( - "sos2_constraints not yet implemented for ModelSubset in Python" - ) - if model_subset.indicator_constraints: - raise NotImplementedError( - "indicator_constraints not yet implemented for ModelSubset in Python" - ) - return ModelSubset( + def to_proto(self) -> infeasible_subsystem_pb2.ModelSubsetProto: + """Returns an equivalent proto message for this `ModelSubset`.""" + return infeasible_subsystem_pb2.ModelSubsetProto( variable_bounds={ - mod.get_variable(var_id): parse_model_subset_bounds(bounds) - for var_id, bounds in model_subset.variable_bounds.items() + var.id: bounds.to_proto() + for (var, bounds) in self.variable_bounds.items() }, - variable_integrality=frozenset( - mod.get_variable(var_id) for var_id in model_subset.variable_integrality + variable_integrality=sorted( + var.id for var in self.variable_integrality ), linear_constraints={ - mod.get_linear_constraint(con_id): parse_model_subset_bounds(bounds) - for con_id, bounds in model_subset.linear_constraints.items() + con.id: bounds.to_proto() + for (con, bounds) in self.linear_constraints.items() }, ) -@dataclasses.dataclass(frozen=True) -class ComputeInfeasibleSubsystemResult: - """The result of searching for an infeasible subsystem. - - This is the result of calling `mathopt.compute_infeasible_subsystem()`. - - Attributes: - feasibility: If the problem was proven feasible, infeasible, or no - conclusion was reached. The fields below are ignored unless the problem - was proven infeasible. - infeasible_subsystem: Ignored unless `feasibility` is `INFEASIBLE`, a subset - of the model that is still infeasible. - is_minimal: Ignored unless `feasibility` is `INFEASIBLE`. If True, then the - removal of any constraint from `infeasible_subsystem` makes the sub-model - feasible. Note that, due to problem transformations MathOpt applies or - idiosyncrasies of the solvers contract, the returned infeasible subsystem - may not actually be minimal. - """ +def parse_model_subset( + model_subset: infeasible_subsystem_pb2.ModelSubsetProto, mod: model.Model +) -> ModelSubset: + """Returns an equivalent `ModelSubset` to the input proto.""" + if model_subset.quadratic_constraints: + raise NotImplementedError( + "quadratic_constraints not yet implemented for ModelSubset in Python" + ) + if model_subset.second_order_cone_constraints: + raise NotImplementedError( + "second_order_cone_constraints not yet implemented for ModelSubset in" + " Python" + ) + if model_subset.sos1_constraints: + raise NotImplementedError( + "sos1_constraints not yet implemented for ModelSubset in Python" + ) + if model_subset.sos2_constraints: + raise NotImplementedError( + "sos2_constraints not yet implemented for ModelSubset in Python" + ) + if model_subset.indicator_constraints: + raise NotImplementedError( + "indicator_constraints not yet implemented for ModelSubset in Python" + ) + return ModelSubset( + variable_bounds={ + mod.get_variable(var_id): parse_model_subset_bounds(bounds) + for var_id, bounds in model_subset.variable_bounds.items() + }, + variable_integrality=frozenset( + mod.get_variable(var_id) + for var_id in model_subset.variable_integrality + ), + linear_constraints={ + mod.get_linear_constraint(con_id): parse_model_subset_bounds(bounds) + for con_id, bounds in model_subset.linear_constraints.items() + }, + ) - feasibility: result.FeasibilityStatus = result.FeasibilityStatus.UNDETERMINED - infeasible_subsystem: ModelSubset = ModelSubset() - is_minimal: bool = False - def to_proto( - self, - ) -> infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto: - """Returns an equivalent proto for this `ComputeInfeasibleSubsystemResult`.""" - return infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto( - feasibility=self.feasibility.value, - infeasible_subsystem=self.infeasible_subsystem.to_proto(), - is_minimal=self.is_minimal, - ) +@dataclasses.dataclass(frozen=True) +class ComputeInfeasibleSubsystemResult: + """The result of searching for an infeasible subsystem. + + This is the result of calling `mathopt.compute_infeasible_subsystem()`. + + Attributes: + feasibility: If the problem was proven feasible, infeasible, or no + conclusion was reached. The fields below are ignored unless the problem + was proven infeasible. + infeasible_subsystem: Ignored unless `feasibility` is `INFEASIBLE`, a subset + of the model that is still infeasible. + is_minimal: Ignored unless `feasibility` is `INFEASIBLE`. If True, then the + removal of any constraint from `infeasible_subsystem` makes the sub-model + feasible. Note that, due to problem transformations MathOpt applies or + idiosyncrasies of the solvers contract, the returned infeasible subsystem + may not actually be minimal. + """ + + feasibility: result.FeasibilityStatus = result.FeasibilityStatus.UNDETERMINED + infeasible_subsystem: ModelSubset = ModelSubset() + is_minimal: bool = False + + def to_proto( + self, + ) -> infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto: + """Returns an equivalent proto for this `ComputeInfeasibleSubsystemResult`.""" + return infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto( + feasibility=self.feasibility.value, + infeasible_subsystem=self.infeasible_subsystem.to_proto(), + is_minimal=self.is_minimal, + ) def parse_compute_infeasible_subsystem_result( infeasible_system_result: infeasible_subsystem_pb2.ComputeInfeasibleSubsystemResultProto, mod: model.Model, ) -> ComputeInfeasibleSubsystemResult: - """Returns an equivalent `ComputeInfeasibleSubsystemResult` to the input proto.""" - return ComputeInfeasibleSubsystemResult( - feasibility=result.FeasibilityStatus(infeasible_system_result.feasibility), - infeasible_subsystem=parse_model_subset( - infeasible_system_result.infeasible_subsystem, mod - ), - is_minimal=infeasible_system_result.is_minimal, - ) + """Returns an equivalent `ComputeInfeasibleSubsystemResult` to the input proto.""" + return ComputeInfeasibleSubsystemResult( + feasibility=result.FeasibilityStatus( + infeasible_system_result.feasibility + ), + infeasible_subsystem=parse_model_subset( + infeasible_system_result.infeasible_subsystem, mod + ), + is_minimal=infeasible_system_result.is_minimal, + ) diff --git a/ortools/math_opt/python/compute_infeasible_subsystem_result_test.py b/ortools/math_opt/python/compute_infeasible_subsystem_result_test.py index 7d169ac2684..6496926d9d6 100644 --- a/ortools/math_opt/python/compute_infeasible_subsystem_result_test.py +++ b/ortools/math_opt/python/compute_infeasible_subsystem_result_test.py @@ -26,176 +26,186 @@ ) -class ModelSubsetBoundsTest(absltest.TestCase, compare_proto.MathOptProtoAssertions): - - def test_empty(self) -> None: - self.assertTrue(_ModelSubsetBounds().empty()) - self.assertFalse(_ModelSubsetBounds(lower=True).empty()) - self.assertFalse(_ModelSubsetBounds(upper=True).empty()) - - def test_proto(self) -> None: - start_bounds = _ModelSubsetBounds(lower=True) - self.assert_protos_equiv( - start_bounds.to_proto(), - infeasible_subsystem_pb2.ModelSubsetProto.Bounds(lower=True), - ) - - def test_proto_round_trip_lower(self) -> None: - start_bounds = _ModelSubsetBounds(lower=True) - self.assertEqual( - compute_infeasible_subsystem_result.parse_model_subset_bounds( - start_bounds.to_proto() - ), - start_bounds, - ) - - def test_proto_round_trip_upper(self) -> None: - start_bounds = _ModelSubsetBounds(upper=True) - self.assertEqual( - compute_infeasible_subsystem_result.parse_model_subset_bounds( - start_bounds.to_proto() - ), - start_bounds, - ) +class ModelSubsetBoundsTest( + absltest.TestCase, compare_proto.MathOptProtoAssertions +): + + def test_empty(self) -> None: + self.assertTrue(_ModelSubsetBounds().empty()) + self.assertFalse(_ModelSubsetBounds(lower=True).empty()) + self.assertFalse(_ModelSubsetBounds(upper=True).empty()) + + def test_proto(self) -> None: + start_bounds = _ModelSubsetBounds(lower=True) + self.assert_protos_equiv( + start_bounds.to_proto(), + infeasible_subsystem_pb2.ModelSubsetProto.Bounds(lower=True), + ) + + def test_proto_round_trip_lower(self) -> None: + start_bounds = _ModelSubsetBounds(lower=True) + self.assertEqual( + compute_infeasible_subsystem_result.parse_model_subset_bounds( + start_bounds.to_proto() + ), + start_bounds, + ) + + def test_proto_round_trip_upper(self) -> None: + start_bounds = _ModelSubsetBounds(upper=True) + self.assertEqual( + compute_infeasible_subsystem_result.parse_model_subset_bounds( + start_bounds.to_proto() + ), + start_bounds, + ) class ModelSubsetTest(absltest.TestCase, compare_proto.MathOptProtoAssertions): - def test_empty(self) -> None: - m = model.Model() - x = m.add_binary_variable() - c = m.add_linear_constraint() - self.assertTrue(_ModelSubset().empty()) - self.assertFalse(_ModelSubset(variable_integrality=frozenset((x,))).empty()) - self.assertFalse( - _ModelSubset(variable_bounds={x: _ModelSubsetBounds(lower=True)}).empty() - ) - self.assertFalse( - _ModelSubset(linear_constraints={c: _ModelSubsetBounds(upper=True)}).empty() - ) - - def test_to_proto(self) -> None: - m = model.Model() - x = m.add_binary_variable() - y = m.add_binary_variable() - c = m.add_linear_constraint() - d = m.add_linear_constraint() - model_subset = _ModelSubset( - variable_integrality=frozenset((x, y)), - variable_bounds={y: _ModelSubsetBounds(upper=True)}, - linear_constraints={ - c: _ModelSubsetBounds(upper=True), - d: _ModelSubsetBounds(lower=True), - }, - ) - expected = infeasible_subsystem_pb2.ModelSubsetProto() - expected.variable_bounds[1].upper = True - expected.variable_integrality[:] = [0, 1] - expected.linear_constraints[0].upper = True - expected.linear_constraints[1].lower = True - self.assert_protos_equiv(model_subset.to_proto(), expected) - - def test_proto_round_trip_empty(self) -> None: - m = model.Model() - subset = _ModelSubset() - self.assertEqual( - compute_infeasible_subsystem_result.parse_model_subset( - subset.to_proto(), m - ), - subset, - ) - - def test_proto_round_trip_full(self) -> None: - m = model.Model() - x = m.add_binary_variable() - y = m.add_binary_variable() - c = m.add_linear_constraint() - d = m.add_linear_constraint() - start_subset = _ModelSubset( - variable_integrality=frozenset((x,)), - variable_bounds={ - x: _ModelSubsetBounds(lower=True), - y: _ModelSubsetBounds(upper=True), - }, - linear_constraints={ - c: _ModelSubsetBounds(upper=True), - d: _ModelSubsetBounds(lower=True), - }, - ) - self.assertEqual( - compute_infeasible_subsystem_result.parse_model_subset( - start_subset.to_proto(), m - ), - start_subset, - ) - - def test_parse_proto_quadratic_constraint_unsupported(self) -> None: - m = model.Model() - model_subset = infeasible_subsystem_pb2.ModelSubsetProto() - model_subset.quadratic_constraints[3].lower = True - with self.assertRaisesRegex(NotImplementedError, "quadratic_constraints"): - compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) - - def test_parse_proto_second_order_cone_unsupported(self) -> None: - m = model.Model() - model_subset = infeasible_subsystem_pb2.ModelSubsetProto( - second_order_cone_constraints=[2] - ) - with self.assertRaisesRegex( - NotImplementedError, "second_order_cone_constraints" - ): - compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) - - def test_parse_proto_sos1_unsupported(self) -> None: - m = model.Model() - model_subset = infeasible_subsystem_pb2.ModelSubsetProto(sos1_constraints=[2]) - with self.assertRaisesRegex(NotImplementedError, "sos1_constraints"): - compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) - - def test_parse_proto_sos2_unsupported(self) -> None: - m = model.Model() - model_subset = infeasible_subsystem_pb2.ModelSubsetProto(sos2_constraints=[2]) - with self.assertRaisesRegex(NotImplementedError, "sos2_constraints"): - compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) - - def test_parse_proto_indicator_unsupported(self) -> None: - m = model.Model() - model_subset = infeasible_subsystem_pb2.ModelSubsetProto( - indicator_constraints=[2] - ) - with self.assertRaisesRegex(NotImplementedError, "indicator_constraints"): - compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) + def test_empty(self) -> None: + m = model.Model() + x = m.add_binary_variable() + c = m.add_linear_constraint() + self.assertTrue(_ModelSubset().empty()) + self.assertFalse(_ModelSubset(variable_integrality=frozenset((x,))).empty()) + self.assertFalse( + _ModelSubset( + variable_bounds={x: _ModelSubsetBounds(lower=True)} + ).empty() + ) + self.assertFalse( + _ModelSubset( + linear_constraints={c: _ModelSubsetBounds(upper=True)} + ).empty() + ) + + def test_to_proto(self) -> None: + m = model.Model() + x = m.add_binary_variable() + y = m.add_binary_variable() + c = m.add_linear_constraint() + d = m.add_linear_constraint() + model_subset = _ModelSubset( + variable_integrality=frozenset((x, y)), + variable_bounds={y: _ModelSubsetBounds(upper=True)}, + linear_constraints={ + c: _ModelSubsetBounds(upper=True), + d: _ModelSubsetBounds(lower=True), + }, + ) + expected = infeasible_subsystem_pb2.ModelSubsetProto() + expected.variable_bounds[1].upper = True + expected.variable_integrality[:] = [0, 1] + expected.linear_constraints[0].upper = True + expected.linear_constraints[1].lower = True + self.assert_protos_equiv(model_subset.to_proto(), expected) + + def test_proto_round_trip_empty(self) -> None: + m = model.Model() + subset = _ModelSubset() + self.assertEqual( + compute_infeasible_subsystem_result.parse_model_subset( + subset.to_proto(), m + ), + subset, + ) + + def test_proto_round_trip_full(self) -> None: + m = model.Model() + x = m.add_binary_variable() + y = m.add_binary_variable() + c = m.add_linear_constraint() + d = m.add_linear_constraint() + start_subset = _ModelSubset( + variable_integrality=frozenset((x,)), + variable_bounds={ + x: _ModelSubsetBounds(lower=True), + y: _ModelSubsetBounds(upper=True), + }, + linear_constraints={ + c: _ModelSubsetBounds(upper=True), + d: _ModelSubsetBounds(lower=True), + }, + ) + self.assertEqual( + compute_infeasible_subsystem_result.parse_model_subset( + start_subset.to_proto(), m + ), + start_subset, + ) + + def test_parse_proto_quadratic_constraint_unsupported(self) -> None: + m = model.Model() + model_subset = infeasible_subsystem_pb2.ModelSubsetProto() + model_subset.quadratic_constraints[3].lower = True + with self.assertRaisesRegex(NotImplementedError, "quadratic_constraints"): + compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) + + def test_parse_proto_second_order_cone_unsupported(self) -> None: + m = model.Model() + model_subset = infeasible_subsystem_pb2.ModelSubsetProto( + second_order_cone_constraints=[2] + ) + with self.assertRaisesRegex( + NotImplementedError, "second_order_cone_constraints" + ): + compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) + + def test_parse_proto_sos1_unsupported(self) -> None: + m = model.Model() + model_subset = infeasible_subsystem_pb2.ModelSubsetProto( + sos1_constraints=[2] + ) + with self.assertRaisesRegex(NotImplementedError, "sos1_constraints"): + compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) + + def test_parse_proto_sos2_unsupported(self) -> None: + m = model.Model() + model_subset = infeasible_subsystem_pb2.ModelSubsetProto( + sos2_constraints=[2] + ) + with self.assertRaisesRegex(NotImplementedError, "sos2_constraints"): + compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) + + def test_parse_proto_indicator_unsupported(self) -> None: + m = model.Model() + model_subset = infeasible_subsystem_pb2.ModelSubsetProto( + indicator_constraints=[2] + ) + with self.assertRaisesRegex(NotImplementedError, "indicator_constraints"): + compute_infeasible_subsystem_result.parse_model_subset(model_subset, m) class ComputeInfeasibleSubsystemResultTest(absltest.TestCase): - def test_to_proto_round_trip(self) -> None: - m = model.Model() - x = m.add_binary_variable() - iis_result = _ComputeInfeasibleSubsystemResult( - feasibility=result.FeasibilityStatus.INFEASIBLE, - is_minimal=True, - infeasible_subsystem=_ModelSubset(variable_integrality=frozenset((x,))), - ) - self.assertEqual( - compute_infeasible_subsystem_result.parse_compute_infeasible_subsystem_result( - iis_result.to_proto(), m - ), - iis_result, - ) - - def test_to_proto_round_trip_empty(self) -> None: - m = model.Model() - iis_result = _ComputeInfeasibleSubsystemResult( - feasibility=result.FeasibilityStatus.UNDETERMINED - ) - self.assertEqual( - compute_infeasible_subsystem_result.parse_compute_infeasible_subsystem_result( - iis_result.to_proto(), m - ), - iis_result, - ) + def test_to_proto_round_trip(self) -> None: + m = model.Model() + x = m.add_binary_variable() + iis_result = _ComputeInfeasibleSubsystemResult( + feasibility=result.FeasibilityStatus.INFEASIBLE, + is_minimal=True, + infeasible_subsystem=_ModelSubset(variable_integrality=frozenset((x,))), + ) + self.assertEqual( + compute_infeasible_subsystem_result.parse_compute_infeasible_subsystem_result( + iis_result.to_proto(), m + ), + iis_result, + ) + + def test_to_proto_round_trip_empty(self) -> None: + m = model.Model() + iis_result = _ComputeInfeasibleSubsystemResult( + feasibility=result.FeasibilityStatus.UNDETERMINED + ) + self.assertEqual( + compute_infeasible_subsystem_result.parse_compute_infeasible_subsystem_result( + iis_result.to_proto(), m + ), + iis_result, + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/elemental/elemental.py b/ortools/math_opt/python/elemental/elemental.py index 48927f39185..b1ec9341fab 100644 --- a/ortools/math_opt/python/elemental/elemental.py +++ b/ortools/math_opt/python/elemental/elemental.py @@ -26,373 +26,385 @@ class Elemental(Protocol): - """An API for building, modifying, and tracking changes to a MathOpt model. - - On functions that return protocol buffers: These functions can fail for two - reasons: - (1) The data is too large for proto's in memory representation. Specifically, - any repeated field can have at most 2^31 entries (~2 billion). So if your - model has this many nonzeros in the constraint matrix, we cannot build a - proto for it (we can potentially export to a text format still). - (2) The particular combination of Elemental and Proto you are using must - serialize your message (typically to cross a Python/C++ language - boundary). Proto has a limit of 2GB for serialized messages, which is - generally hit much earlier than the repeated field limit. - - Note that for users solving locally, they can avoid needing to serialize - their proto by: - - using the C++ implementation of Elemental - - using the upb or cpp implementations of proto for python and compile - correctly, see go/fastpythonproto and - https://github.com/protocolbuffers/protobuf/blob/main/python/README.md. + """An API for building, modifying, and tracking changes to a MathOpt model. + + On functions that return protocol buffers: These functions can fail for two + reasons: + (1) The data is too large for proto's in memory representation. Specifically, + any repeated field can have at most 2^31 entries (~2 billion). So if your + model has this many nonzeros in the constraint matrix, we cannot build a + proto for it (we can potentially export to a text format still). + (2) The particular combination of Elemental and Proto you are using must + serialize your message (typically to cross a Python/C++ language + boundary). Proto has a limit of 2GB for serialized messages, which is + generally hit much earlier than the repeated field limit. + + Note that for users solving locally, they can avoid needing to serialize + their proto by: + - using the C++ implementation of Elemental + - using the upb or cpp implementations of proto for python and compile + correctly, see go/fastpythonproto and + https://github.com/protocolbuffers/protobuf/blob/main/python/README.md. + """ + + def __init__( + self, *, model_name: str = "", primary_objective_name: str = "" + ) -> None: + """Creates an empty optimization model. + + Args: + model_name: The name of the model, used for logging and export only. + primary_objective_name: The name of the main objective of the problem. + Typically used only for multi-objective problems. """ - def __init__( - self, *, model_name: str = "", primary_objective_name: str = "" - ) -> None: - """Creates an empty optimization model. - - Args: - model_name: The name of the model, used for logging and export only. - primary_objective_name: The name of the main objective of the problem. - Typically used only for multi-objective problems. - """ - - @classmethod - def from_model_proto(cls, proto: model_pb2.ModelProto) -> Self: - """Returns an Elemental equivalent to the input proto.""" - - def clone(self, *, new_model_name: Optional[str] = None) -> Self: - """Returns a copy of this model with no associated diffs.""" - - @property - def model_name(self) -> str: - """The name of the model.""" - - @property - def primary_objective_name(self) -> str: - """The name of the primary objective of the model (rarely used).""" - - def add_element(self, element_type: enums.ElementType, name: str) -> int: - """Adds an element of `element_type` to the model and returns its id.""" - - def add_elements( - self, element_type: enums.ElementType, num: int - ) -> np.typing.NDArray[np.int64]: - """Adds `num` `element_type`s to the model and returns their ids. - - All elements added will have the name ''. - - Args: - element_type: The ElementType of elements to add to the model. - num: How many elements are added. - - Returns: - A numpy array with shape (num,) with the ids of the newly added elements. - """ - - def add_named_elements( - self, - element_type: enums.ElementType, - names: np.typing.NDArray, - ) -> np.typing.NDArray[np.int64]: - """Adds an element of `element_type` for each name in names and returns ids. - - Args: - element_type: The ElementType of elements to add to the model. - names: The names the elements, must have shape (n,) and string values. - - Returns: - A numpy array with shape (n,) with the ids of the newly added elements. - """ - - def delete_element(self, element_type: enums.ElementType, element_id: int) -> bool: - """Deletes element `id` of `element_type` from model, returns success.""" - - def delete_elements( - self, - element_type: enums.ElementType, - elements: np.typing.NDArray[np.int64], - ) -> np.typing.NDArray[np.bool_]: - """Removes `elements` from the model, returning true elementwise on success. - - A value of false is returned when an element was deleted in a previous call - to delete elements or the element was never in the model. Note that - repeating an id in `elements` for a single call to this function results in - an exception. - - Args: - element_type: The ElementType of elements to delete from to the model. - elements: The ids of the elements to delete, must have shape (n,). - - Returns: - A numpy array with shape (n,) indicating if each element was successfully - deleted. (Entries are false when the element id was previously deleted or - was never in the model.) - - Raises: - ValueError: if elements contains any duplicates. No modifications to the - model will be applied when this exception is raised. - """ - - def get_element_name(self, element_type: enums.ElementType, element_id: int) -> str: - """Returns the name of the element `id` of ElementType `element_type`.""" - - def get_element_names( - self, - element_type: enums.ElementType, - elements: np.typing.NDArray[np.int64], - ) -> np.typing.NDArray: - """Returns the name of each element in `elements`. - - Note that elements have a default name of '' if no name is provided. - - Args: - element_type: The ElementType of elements to get the names for. - elements: The ids of the elements, must have shape (n,). - - Returns: - A numpy array with shape (n,) containing the names. - - Raises: - ValueError: if any id from `elements` is not in the model. - """ - - def element_exists(self, element_type: enums.ElementType, element_id: int) -> bool: - """Returns if element `id` of ElementType `element_type` is in the model.""" - - def elements_exist( - self, - element_type: enums.ElementType, - elements: np.typing.NDArray[np.int64], - ) -> np.typing.NDArray[np.bool_]: - """Returns if each id in `elements` is an element in the model. - - Args: - element_type: The ElementType to check. - elements: The ids to look for, must have shape (n,). - - Returns: - A numpy array with shape (n,) containing true if each element is in the - model (the id has been created and not deleted). - """ - - def get_next_element_id(self, element_type: enums.ElementType) -> int: - """Returns the next available element id of type `element_type`.""" - - def get_num_elements(self, element_type: enums.ElementType) -> int: - """Returns the number of elements of type `element_type` in the model.""" - - def get_elements( - self, element_type: enums.ElementType - ) -> np.typing.NDArray[np.int64]: - """Returns all element ids for type `element_type` in unspecified order.""" - - def ensure_next_element_id_at_least( - self, element_type: enums.ElementType, element_id: int - ) -> None: - """Increases next_element_id() to `element_id` if it is currently less.""" - - def set_attr( - self, - attr: enums.PyAttr[enums.AttrPyValueType], - key: Sequence[int], - values: enums.AttrPyValueType, - ) -> None: - """Sets an attribute to a value for a key.""" - - def set_attrs( - self, - attr: enums.Attr[enums.AttrValueType], - keys: np.typing.NDArray[np.int64], - values: np.typing.NDArray[enums.AttrValueType], - ) -> None: - """Sets the value of an attribute for a list of keys. - - Args: - attr: The attribute to modify, with k elements in each key. - keys: An (n, k) array of n keys to set this attribute for. - values: An array with shape (n,), the values to set for each key. - - Raises: - ValueError: if (1) any key is repeated (2) any key references an element - not in the model, (3) the shape of keys are values is invalid, or (4) - the shape of keys and values is inconsistent. No changes are applied for - any key if the operation fails. - """ - - def get_attr( - self, attr: enums.PyAttr[enums.AttrPyValueType], key: Sequence[int] - ) -> enums.AttrPyValueType: - """Returns the attribute value for a key. - - The type of the attribute determines the number of elements in the key and - return type. E.g. when attr=DoubleAttr1.VARIABLE_LOWER_BOUND, key should - have size one (the element id of the variable) and the return type is float. - - Args: - attr: The attribute to query, which implies the key size and return type. - key: A sequence of k ints, the element ids of the key. - - Returns: - The value for the key, or the default value for the attribute if the key - is not set. - - Raises: - ValueError: if key is of the wrong size or key refers to an element id - is not in the model. - """ - - def get_attrs( - self, - attr: enums.Attr[enums.AttrValueType], - keys: np.typing.NDArray[np.int64], - ) -> np.typing.NDArray[enums.AttrValueType]: - """Returns the values of an attribute for a list of keys. - - Repeated keys are okay. - - Args: - attr: The attribute to query, with k elements in each key. - keys: An (n, k) array of n keys to read. - - Returns: - An array with shape (n,) with the values for each key. The default value - of the attribute is returned if it was never set for the key. - - Raises: - ValueError: if (1) any key references an element not in the model or (2) - the shape of keys is invalid. - """ - - def clear_attr(self, attr: enums.AnyAttr) -> None: - """Restores an attribute to its default value for every key.""" - - def is_attr_non_default(self, attr: enums.AnyAttr, key: Sequence[int]) -> bool: - """Returns true if the attribute has a non-default value for key.""" - - def bulk_is_attr_non_default( - self, attr: enums.AnyAttr, keys: np.typing.NDArray[np.int64] - ) -> np.typing.NDArray[np.bool_]: - """Returns which keys take a value different from the attribute's default. - - Repeated keys are okay. - - Args: - attr: The attribute to query, with k elements in each key. - keys: An (n, k) array to of n keys to query. - - Returns: - An array with shape (n,), for each key, if it had a non-default value. - - Raises: - ValueError: if (1) any key references an element not in the model or (2) - the shape of keys is invalid. - """ - - def get_attr_num_non_defaults(self, attr: enums.AnyAttr) -> int: - """Returns the number of keys with a non-default value for an attribute.""" - - def get_attr_non_defaults(self, attr: enums.AnyAttr) -> np.typing.NDArray[np.int64]: - """Returns the keys with a non-default value for an attribute. - - Args: - attr: The attribute to query, with k elements in each key. - - Returns: - An array with shape (n, k) if there are n keys with a non-default value - for this attribute. - """ - - def slice_attr( - self, attr: enums.AnyAttr, key_index: int, element_id: int - ) -> np.typing.NDArray[np.int64]: - """Returns the keys with a non-default value for an attribute along a slice. - - Args: - attr: The attribute to query, with k elements in each key. - key_index: The index of the key to slice on, in [0..k). - element_id: The value of the key to slice on, must be the id of an element - with type given by the `key_index` key for `attr`. - - Returns: - An array with shape (n, k) if there are n keys along the slice with a - non-default value for this attribute. - - Raises: - ValueError: if (1) `key_index` is not in [0..k) or (2) if no element with - `element_id` exists. - """ - - def get_attr_slice_size( - self, attr: enums.AnyAttr, key_index: int, element_id: int - ) -> int: - """Returns the number of keys in slice_attr(attr, key_index, element_id).""" - - def export_model(self, *, remove_names: bool = False) -> model_pb2.ModelProto: - """Returns a ModelProto equivalent to this model. - - Args: - remove_names: If True, exclude names (e.g. variable names, the model name) - from the returned proto. - - Returns: - The equivalent ModelProto. - - Raises: - ValueError: if the model is too big to fit into the proto, see class - description for details. - """ - - def add_diff(self) -> int: - """Creates a new Diff to track changes to the model and returns its id.""" + @classmethod + def from_model_proto(cls, proto: model_pb2.ModelProto) -> Self: + """Returns an Elemental equivalent to the input proto.""" - def delete_diff(self, diff_id: int) -> None: - """Stop tracking changes to the model for the Diff with id `diff_id`.""" + def clone(self, *, new_model_name: Optional[str] = None) -> Self: + """Returns a copy of this model with no associated diffs.""" - def advance_diff(self, diff_id: int) -> None: - """Discards any previously tracked changes for this Diff. - - The diff will now track changes from the point onward. + @property + def model_name(self) -> str: + """The name of the model.""" - Args: - diff_id: The id of to the Diff to advance. + @property + def primary_objective_name(self) -> str: + """The name of the primary objective of the model (rarely used).""" - Raises: - ValueError: if diff_id does not reference a Diff for this model (e.g., - the Diff was already deleted). - """ + def add_element(self, element_type: enums.ElementType, name: str) -> int: + """Adds an element of `element_type` to the model and returns its id.""" - def export_model_update( - self, diff_id: int, *, remove_names: bool = False - ) -> Optional[model_update_pb2.ModelUpdateProto]: - """Returns a ModelUpdateProto with the changes for the Diff `diff_id`. + def add_elements( + self, element_type: enums.ElementType, num: int + ) -> np.typing.NDArray[np.int64]: + """Adds `num` `element_type`s to the model and returns their ids. - Args: - diff_id: The id of the Diff to get changes for. - remove_names: If True, exclude names (e.g. variable names, the model name) - from the returned proto. - - Returns: - All changes to the model since the most recent call to - `advance(diff_id)`, or since the Diff was created if it was never - advanced. Returns `None` instead of an empty proto when there are no - changes. - - Raises: - ValueError: if the update is too big to fit into the proto (see class - description for details) or if diff_id does not reference a Diff for - this model (e.g., the id was already deleted). - """ - - def apply_update(self, update_proto: model_update_pb2.ModelUpdateProto) -> None: - """Modifies the model to apply the changes from `update_proto`. - - Args: - update_proto: the changes to apply to the model. - - Raises: - ValueError: if the update proto is invalid for the current model, or if - the implementation must serialize the proto and it is too large (see - class description). - """ + All elements added will have the name ''. + + Args: + element_type: The ElementType of elements to add to the model. + num: How many elements are added. + + Returns: + A numpy array with shape (num,) with the ids of the newly added elements. + """ + + def add_named_elements( + self, + element_type: enums.ElementType, + names: np.typing.NDArray, + ) -> np.typing.NDArray[np.int64]: + """Adds an element of `element_type` for each name in names and returns ids. + + Args: + element_type: The ElementType of elements to add to the model. + names: The names the elements, must have shape (n,) and string values. + + Returns: + A numpy array with shape (n,) with the ids of the newly added elements. + """ + + def delete_element( + self, element_type: enums.ElementType, element_id: int + ) -> bool: + """Deletes element `id` of `element_type` from model, returns success.""" + + def delete_elements( + self, + element_type: enums.ElementType, + elements: np.typing.NDArray[np.int64], + ) -> np.typing.NDArray[np.bool_]: + """Removes `elements` from the model, returning true elementwise on success. + + A value of false is returned when an element was deleted in a previous call + to delete elements or the element was never in the model. Note that + repeating an id in `elements` for a single call to this function results in + an exception. + + Args: + element_type: The ElementType of elements to delete from to the model. + elements: The ids of the elements to delete, must have shape (n,). + + Returns: + A numpy array with shape (n,) indicating if each element was successfully + deleted. (Entries are false when the element id was previously deleted or + was never in the model.) + + Raises: + ValueError: if elements contains any duplicates. No modifications to the + model will be applied when this exception is raised. + """ + + def get_element_name( + self, element_type: enums.ElementType, element_id: int + ) -> str: + """Returns the name of the element `id` of ElementType `element_type`.""" + + def get_element_names( + self, + element_type: enums.ElementType, + elements: np.typing.NDArray[np.int64], + ) -> np.typing.NDArray: + """Returns the name of each element in `elements`. + + Note that elements have a default name of '' if no name is provided. + + Args: + element_type: The ElementType of elements to get the names for. + elements: The ids of the elements, must have shape (n,). + + Returns: + A numpy array with shape (n,) containing the names. + + Raises: + ValueError: if any id from `elements` is not in the model. + """ + + def element_exists( + self, element_type: enums.ElementType, element_id: int + ) -> bool: + """Returns if element `id` of ElementType `element_type` is in the model.""" + + def elements_exist( + self, + element_type: enums.ElementType, + elements: np.typing.NDArray[np.int64], + ) -> np.typing.NDArray[np.bool_]: + """Returns if each id in `elements` is an element in the model. + + Args: + element_type: The ElementType to check. + elements: The ids to look for, must have shape (n,). + + Returns: + A numpy array with shape (n,) containing true if each element is in the + model (the id has been created and not deleted). + """ + + def get_next_element_id(self, element_type: enums.ElementType) -> int: + """Returns the next available element id of type `element_type`.""" + + def get_num_elements(self, element_type: enums.ElementType) -> int: + """Returns the number of elements of type `element_type` in the model.""" + + def get_elements( + self, element_type: enums.ElementType + ) -> np.typing.NDArray[np.int64]: + """Returns all element ids for type `element_type` in unspecified order.""" + + def ensure_next_element_id_at_least( + self, element_type: enums.ElementType, element_id: int + ) -> None: + """Increases next_element_id() to `element_id` if it is currently less.""" + + def set_attr( + self, + attr: enums.PyAttr[enums.AttrPyValueType], + key: Sequence[int], + values: enums.AttrPyValueType, + ) -> None: + """Sets an attribute to a value for a key.""" + + def set_attrs( + self, + attr: enums.Attr[enums.AttrValueType], + keys: np.typing.NDArray[np.int64], + values: np.typing.NDArray[enums.AttrValueType], + ) -> None: + """Sets the value of an attribute for a list of keys. + + Args: + attr: The attribute to modify, with k elements in each key. + keys: An (n, k) array of n keys to set this attribute for. + values: An array with shape (n,), the values to set for each key. + + Raises: + ValueError: if (1) any key is repeated (2) any key references an element + not in the model, (3) the shape of keys are values is invalid, or (4) + the shape of keys and values is inconsistent. No changes are applied for + any key if the operation fails. + """ + + def get_attr( + self, attr: enums.PyAttr[enums.AttrPyValueType], key: Sequence[int] + ) -> enums.AttrPyValueType: + """Returns the attribute value for a key. + + The type of the attribute determines the number of elements in the key and + return type. E.g. when attr=DoubleAttr1.VARIABLE_LOWER_BOUND, key should + have size one (the element id of the variable) and the return type is float. + + Args: + attr: The attribute to query, which implies the key size and return type. + key: A sequence of k ints, the element ids of the key. + + Returns: + The value for the key, or the default value for the attribute if the key + is not set. + + Raises: + ValueError: if key is of the wrong size or key refers to an element id + is not in the model. + """ + + def get_attrs( + self, + attr: enums.Attr[enums.AttrValueType], + keys: np.typing.NDArray[np.int64], + ) -> np.typing.NDArray[enums.AttrValueType]: + """Returns the values of an attribute for a list of keys. + + Repeated keys are okay. + + Args: + attr: The attribute to query, with k elements in each key. + keys: An (n, k) array of n keys to read. + + Returns: + An array with shape (n,) with the values for each key. The default value + of the attribute is returned if it was never set for the key. + + Raises: + ValueError: if (1) any key references an element not in the model or (2) + the shape of keys is invalid. + """ + + def clear_attr(self, attr: enums.AnyAttr) -> None: + """Restores an attribute to its default value for every key.""" + + def is_attr_non_default( + self, attr: enums.AnyAttr, key: Sequence[int] + ) -> bool: + """Returns true if the attribute has a non-default value for key.""" + + def bulk_is_attr_non_default( + self, attr: enums.AnyAttr, keys: np.typing.NDArray[np.int64] + ) -> np.typing.NDArray[np.bool_]: + """Returns which keys take a value different from the attribute's default. + + Repeated keys are okay. + + Args: + attr: The attribute to query, with k elements in each key. + keys: An (n, k) array to of n keys to query. + + Returns: + An array with shape (n,), for each key, if it had a non-default value. + + Raises: + ValueError: if (1) any key references an element not in the model or (2) + the shape of keys is invalid. + """ + + def get_attr_num_non_defaults(self, attr: enums.AnyAttr) -> int: + """Returns the number of keys with a non-default value for an attribute.""" + + def get_attr_non_defaults( + self, attr: enums.AnyAttr + ) -> np.typing.NDArray[np.int64]: + """Returns the keys with a non-default value for an attribute. + + Args: + attr: The attribute to query, with k elements in each key. + + Returns: + An array with shape (n, k) if there are n keys with a non-default value + for this attribute. + """ + + def slice_attr( + self, attr: enums.AnyAttr, key_index: int, element_id: int + ) -> np.typing.NDArray[np.int64]: + """Returns the keys with a non-default value for an attribute along a slice. + + Args: + attr: The attribute to query, with k elements in each key. + key_index: The index of the key to slice on, in [0..k). + element_id: The value of the key to slice on, must be the id of an element + with type given by the `key_index` key for `attr`. + + Returns: + An array with shape (n, k) if there are n keys along the slice with a + non-default value for this attribute. + + Raises: + ValueError: if (1) `key_index` is not in [0..k) or (2) if no element with + `element_id` exists. + """ + + def get_attr_slice_size( + self, attr: enums.AnyAttr, key_index: int, element_id: int + ) -> int: + """Returns the number of keys in slice_attr(attr, key_index, element_id).""" + + def export_model(self, *, remove_names: bool = False) -> model_pb2.ModelProto: + """Returns a ModelProto equivalent to this model. + + Args: + remove_names: If True, exclude names (e.g. variable names, the model name) + from the returned proto. + + Returns: + The equivalent ModelProto. + + Raises: + ValueError: if the model is too big to fit into the proto, see class + description for details. + """ + + def add_diff(self) -> int: + """Creates a new Diff to track changes to the model and returns its id.""" + + def delete_diff(self, diff_id: int) -> None: + """Stop tracking changes to the model for the Diff with id `diff_id`.""" + + def advance_diff(self, diff_id: int) -> None: + """Discards any previously tracked changes for this Diff. + + The diff will now track changes from the point onward. + + Args: + diff_id: The id of to the Diff to advance. + + Raises: + ValueError: if diff_id does not reference a Diff for this model (e.g., + the Diff was already deleted). + """ + + def export_model_update( + self, diff_id: int, *, remove_names: bool = False + ) -> Optional[model_update_pb2.ModelUpdateProto]: + """Returns a ModelUpdateProto with the changes for the Diff `diff_id`. + + Args: + diff_id: The id of the Diff to get changes for. + remove_names: If True, exclude names (e.g. variable names, the model name) + from the returned proto. + + Returns: + All changes to the model since the most recent call to + `advance(diff_id)`, or since the Diff was created if it was never + advanced. Returns `None` instead of an empty proto when there are no + changes. + + Raises: + ValueError: if the update is too big to fit into the proto (see class + description for details) or if diff_id does not reference a Diff for + this model (e.g., the id was already deleted). + """ + + def apply_update( + self, update_proto: model_update_pb2.ModelUpdateProto + ) -> None: + """Modifies the model to apply the changes from `update_proto`. + + Args: + update_proto: the changes to apply to the model. + + Raises: + ValueError: if the update proto is invalid for the current model, or if + the implementation must serialize the proto and it is too large (see + class description). + """ diff --git a/ortools/math_opt/python/errors.py b/ortools/math_opt/python/errors.py index 687dabe2053..670268cd17a 100644 --- a/ortools/math_opt/python/errors.py +++ b/ortools/math_opt/python/errors.py @@ -23,82 +23,82 @@ class _StatusCode(enum.Enum): - """The C++ absl::Status::code() values.""" - - OK = 0 - CANCELLED = 1 - UNKNOWN = 2 - INVALID_ARGUMENT = 3 - DEADLINE_EXCEEDED = 4 - NOT_FOUND = 5 - ALREADY_EXISTS = 6 - PERMISSION_DENIED = 7 - UNAUTHENTICATED = 16 - RESOURCE_EXHAUSTED = 8 - FAILED_PRECONDITION = 9 - ABORTED = 10 - OUT_OF_RANGE = 11 - UNIMPLEMENTED = 12 - INTERNAL = 13 - UNAVAILABLE = 14 - DATA_LOSS = 15 + """The C++ absl::Status::code() values.""" + + OK = 0 + CANCELLED = 1 + UNKNOWN = 2 + INVALID_ARGUMENT = 3 + DEADLINE_EXCEEDED = 4 + NOT_FOUND = 5 + ALREADY_EXISTS = 6 + PERMISSION_DENIED = 7 + UNAUTHENTICATED = 16 + RESOURCE_EXHAUSTED = 8 + FAILED_PRECONDITION = 9 + ABORTED = 10 + OUT_OF_RANGE = 11 + UNIMPLEMENTED = 12 + INTERNAL = 13 + UNAVAILABLE = 14 + DATA_LOSS = 15 class InternalMathOptError(RuntimeError): - """Some MathOpt internal error. + """Some MathOpt internal error. - This error is usually raised because of a bug in MathOpt or one of the solver - library it wraps. - """ + This error is usually raised because of a bug in MathOpt or one of the solver + library it wraps. + """ def status_proto_to_exception( status_proto: rpc_pb2.StatusProto, ) -> Optional[Exception]: - """Returns the Python exception that best match the input absl::Status. - - There are some Status that we expect the MathOpt code to return, for those the - matching exceptions are: - - InvalidArgument: ValueError - - FailedPrecondition: AssertionError - - Unimplemented: NotImplementedError - - Internal: InternalMathOptError - - Other Status's are not used by MathOpt, if they are seen a - InternalMathOptError is raised (as if the Status was Internal) and the error - message contains the unexpected code. - - Args: - status_proto: The input proto to convert to an exception. - - Returns: - The corresponding exception. None if the input status is OK. - """ - try: - code = _StatusCode(status_proto.code) - except ValueError: - return InternalMathOptError( - f"unknown C++ error (code = {status_proto.code}):" - f" {status_proto.message}" - ) - - if code == _StatusCode.OK: - return None - - # For expected errors we compute the corresponding class. - error_type: Optional[Type[Exception]] = None - if code == _StatusCode.INVALID_ARGUMENT: - error_type = ValueError - if code == _StatusCode.FAILED_PRECONDITION: - error_type = AssertionError - if code == _StatusCode.UNIMPLEMENTED: - error_type = NotImplementedError - if code == _StatusCode.INTERNAL: - error_type = InternalMathOptError - - if error_type is not None: - return error_type(f"{status_proto.message} (was C++ {code.name})") - + """Returns the Python exception that best match the input absl::Status. + + There are some Status that we expect the MathOpt code to return, for those the + matching exceptions are: + - InvalidArgument: ValueError + - FailedPrecondition: AssertionError + - Unimplemented: NotImplementedError + - Internal: InternalMathOptError + + Other Status's are not used by MathOpt, if they are seen a + InternalMathOptError is raised (as if the Status was Internal) and the error + message contains the unexpected code. + + Args: + status_proto: The input proto to convert to an exception. + + Returns: + The corresponding exception. None if the input status is OK. + """ + try: + code = _StatusCode(status_proto.code) + except ValueError: return InternalMathOptError( - f"unexpected C++ error {code.name}: {status_proto.message}" + f"unknown C++ error (code = {status_proto.code}):" + f" {status_proto.message}" ) + + if code == _StatusCode.OK: + return None + + # For expected errors we compute the corresponding class. + error_type: Optional[Type[Exception]] = None + if code == _StatusCode.INVALID_ARGUMENT: + error_type = ValueError + if code == _StatusCode.FAILED_PRECONDITION: + error_type = AssertionError + if code == _StatusCode.UNIMPLEMENTED: + error_type = NotImplementedError + if code == _StatusCode.INTERNAL: + error_type = InternalMathOptError + + if error_type is not None: + return error_type(f"{status_proto.message} (was C++ {code.name})") + + return InternalMathOptError( + f"unexpected C++ error {code.name}: {status_proto.message}" + ) diff --git a/ortools/math_opt/python/errors_test.py b/ortools/math_opt/python/errors_test.py index 6835365ae0a..2da4ea26fa1 100644 --- a/ortools/math_opt/python/errors_test.py +++ b/ortools/math_opt/python/errors_test.py @@ -21,68 +21,68 @@ class StatusProtoToExceptionTest(absltest.TestCase): - def test_ok(self) -> None: - self.assertIsNone( - errors.status_proto_to_exception( - rpc_pb2.StatusProto(code=errors._StatusCode.OK.value) - ) + def test_ok(self) -> None: + self.assertIsNone( + errors.status_proto_to_exception( + rpc_pb2.StatusProto(code=errors._StatusCode.OK.value) ) + ) - def test_invalid_argument(self) -> None: - error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.INVALID_ARGUMENT.value, message="something" - ) + def test_invalid_argument(self) -> None: + error = errors.status_proto_to_exception( + rpc_pb2.StatusProto( + code=errors._StatusCode.INVALID_ARGUMENT.value, message="something" ) - self.assertIsInstance(error, ValueError) - self.assertEqual(str(error), "something (was C++ INVALID_ARGUMENT)") + ) + self.assertIsInstance(error, ValueError) + self.assertEqual(str(error), "something (was C++ INVALID_ARGUMENT)") - def test_failed_precondition(self) -> None: - error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.FAILED_PRECONDITION.value, - message="something", - ) + def test_failed_precondition(self) -> None: + error = errors.status_proto_to_exception( + rpc_pb2.StatusProto( + code=errors._StatusCode.FAILED_PRECONDITION.value, + message="something", ) - self.assertIsInstance(error, AssertionError) - self.assertEqual(str(error), "something (was C++ FAILED_PRECONDITION)") + ) + self.assertIsInstance(error, AssertionError) + self.assertEqual(str(error), "something (was C++ FAILED_PRECONDITION)") - def test_unimplemented(self) -> None: - error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.UNIMPLEMENTED.value, message="something" - ) + def test_unimplemented(self) -> None: + error = errors.status_proto_to_exception( + rpc_pb2.StatusProto( + code=errors._StatusCode.UNIMPLEMENTED.value, message="something" ) - self.assertIsInstance(error, NotImplementedError) - self.assertEqual(str(error), "something (was C++ UNIMPLEMENTED)") + ) + self.assertIsInstance(error, NotImplementedError) + self.assertEqual(str(error), "something (was C++ UNIMPLEMENTED)") - def test_internal(self) -> None: - error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.INTERNAL.value, message="something" - ) + def test_internal(self) -> None: + error = errors.status_proto_to_exception( + rpc_pb2.StatusProto( + code=errors._StatusCode.INTERNAL.value, message="something" ) - self.assertIsInstance(error, errors.InternalMathOptError) - self.assertEqual(str(error), "something (was C++ INTERNAL)") + ) + self.assertIsInstance(error, errors.InternalMathOptError) + self.assertEqual(str(error), "something (was C++ INTERNAL)") - def test_unexpected_code(self) -> None: - error = errors.status_proto_to_exception( - rpc_pb2.StatusProto( - code=errors._StatusCode.DEADLINE_EXCEEDED.value, message="something" - ) - ) - self.assertIsInstance(error, errors.InternalMathOptError) - self.assertEqual( - str(error), "unexpected C++ error DEADLINE_EXCEEDED: something" + def test_unexpected_code(self) -> None: + error = errors.status_proto_to_exception( + rpc_pb2.StatusProto( + code=errors._StatusCode.DEADLINE_EXCEEDED.value, message="something" ) + ) + self.assertIsInstance(error, errors.InternalMathOptError) + self.assertEqual( + str(error), "unexpected C++ error DEADLINE_EXCEEDED: something" + ) - def test_unknown_code(self) -> None: - error = errors.status_proto_to_exception( - rpc_pb2.StatusProto(code=-5, message="something") - ) - self.assertIsInstance(error, errors.InternalMathOptError) - self.assertEqual(str(error), "unknown C++ error (code = -5): something") + def test_unknown_code(self) -> None: + error = errors.status_proto_to_exception( + rpc_pb2.StatusProto(code=-5, message="something") + ) + self.assertIsInstance(error, errors.InternalMathOptError) + self.assertEqual(str(error), "unknown C++ error (code = -5): something") if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/expressions.py b/ortools/math_opt/python/expressions.py index 1309636a53a..80a844aae18 100644 --- a/ortools/math_opt/python/expressions.py +++ b/ortools/math_opt/python/expressions.py @@ -20,13 +20,15 @@ @typing.overload -def fast_sum(summands: Iterable[variables.LinearTypes]) -> variables.LinearSum: ... +def fast_sum(summands: Iterable[variables.LinearTypes]) -> variables.LinearSum: + ... @typing.overload def fast_sum( summands: Iterable[variables.QuadraticTypes], -) -> Union[variables.LinearSum, variables.QuadraticSum]: ... +) -> Union[variables.LinearSum, variables.QuadraticSum]: + ... # TODO(b/312200030): There is a pytype bug so that for the code: @@ -37,49 +39,51 @@ def fast_sum( # rather than Union[variables.LinearSum, variables.QuadraticSum]. Once the bug # is fixed, confirm that the overloads actually work. def fast_sum(summands): - """Sums the elements of summand into a linear or quadratic expression. + """Sums the elements of summand into a linear or quadratic expression. - Similar to Python's sum function, but faster for input that not just integers - and floats. + Similar to Python's sum function, but faster for input that not just integers + and floats. - Unlike sum(), the function returns a linear expression when all inputs are - floats and/or integers. Importantly, the code: - model.add_linear_constraint(fast_sum(maybe_empty_list) <= 1.0) - is safe to call, while: - model.add_linear_constraint(sum(maybe_empty_list) <= 1.0) - fails at runtime when the list is empty. + Unlike sum(), the function returns a linear expression when all inputs are + floats and/or integers. Importantly, the code: + model.add_linear_constraint(fast_sum(maybe_empty_list) <= 1.0) + is safe to call, while: + model.add_linear_constraint(sum(maybe_empty_list) <= 1.0) + fails at runtime when the list is empty. - Args: - summands: The elements to add up. + Args: + summands: The elements to add up. - Returns: - A linear or quadratic expression with the sum of the elements of summand. - """ - summands_tuple = tuple(summands) - for s in summands_tuple: - if isinstance(s, variables.QuadraticBase): - return variables.QuadraticSum(summands_tuple) - return variables.LinearSum(summands_tuple) + Returns: + A linear or quadratic expression with the sum of the elements of summand. + """ + summands_tuple = tuple(summands) + for s in summands_tuple: + if isinstance(s, variables.QuadraticBase): + return variables.QuadraticSum(summands_tuple) + return variables.LinearSum(summands_tuple) def evaluate_expression( expression: variables.QuadraticTypes, variable_values: Mapping[variables.Variable, float], ) -> float: - """Evaluates a linear or quadratic expression for given variable values. - - E.g. if expression = 3 * x + 4 and variable_values = {x: 2.0}, then - evaluate_expression(expression, variable_values) equals 10.0. - - Args: - expression: The expression to evaluate. - variable_values: Must contain a value for every variable in expression. - - Returns: - The value of the expression when replacing variables by their value. - """ - if isinstance(expression, variables.QuadraticBase): - return variables.as_flat_quadratic_expression(expression).evaluate( - variable_values - ) - return variables.as_flat_linear_expression(expression).evaluate(variable_values) + """Evaluates a linear or quadratic expression for given variable values. + + E.g. if expression = 3 * x + 4 and variable_values = {x: 2.0}, then + evaluate_expression(expression, variable_values) equals 10.0. + + Args: + expression: The expression to evaluate. + variable_values: Must contain a value for every variable in expression. + + Returns: + The value of the expression when replacing variables by their value. + """ + if isinstance(expression, variables.QuadraticBase): + return variables.as_flat_quadratic_expression(expression).evaluate( + variable_values + ) + return variables.as_flat_linear_expression(expression).evaluate( + variable_values + ) diff --git a/ortools/math_opt/python/expressions_test.py b/ortools/math_opt/python/expressions_test.py index 7908e64777f..6d1a30a0482 100644 --- a/ortools/math_opt/python/expressions_test.py +++ b/ortools/math_opt/python/expressions_test.py @@ -19,98 +19,98 @@ def _type_check_linear_sum(x: variables.LinearSum) -> None: - """Does nothing at runtime, forces the type checker to run on x.""" - del x # Unused. + """Does nothing at runtime, forces the type checker to run on x.""" + del x # Unused. class FastSumTest(absltest.TestCase): - def test_variables(self) -> None: - mod = model.Model() - x = mod.add_binary_variable() - y = mod.add_binary_variable() - z = 4 - result = expressions.fast_sum([x, y, z]) - _type_check_linear_sum(result) - self.assertIsInstance(result, variables.LinearSum) - result_expr = variables.as_flat_linear_expression(result) - self.assertEqual(result_expr.offset, 4.0) - self.assertDictEqual(dict(result_expr.terms), {x: 1.0, y: 1.0}) - - def test_numbers(self) -> None: - result = expressions.fast_sum([2.0, 4.0]) - _type_check_linear_sum(result) - self.assertIsInstance(result, variables.LinearSum) - result_expr = variables.as_flat_linear_expression(result) - self.assertEqual(result_expr.offset, 6.0) - self.assertEmpty(result_expr.terms) - - def test_heterogeneous_linear(self) -> None: - mod = model.Model() - x = mod.add_binary_variable() - result = expressions.fast_sum([2.0, 3.0 * x]) - _type_check_linear_sum(result) - self.assertIsInstance(result, variables.LinearSum) - result_expr = variables.as_flat_linear_expression(result) - self.assertEqual(result_expr.offset, 2.0) - self.assertDictEqual(dict(result_expr.terms), {x: 3.0}) - - def test_heterogeneous_quad(self) -> None: - mod = model.Model() - x = mod.add_binary_variable() - result = expressions.fast_sum([2.0, 3.0 * x * x, x]) - self.assertIsInstance(result, variables.QuadraticSum) - result_expr = variables.as_flat_quadratic_expression(result) - self.assertEqual(result_expr.offset, 2.0) - self.assertDictEqual(dict(result_expr.linear_terms), {x: 1.0}) - self.assertDictEqual( - dict(result_expr.quadratic_terms), - {variables.QuadraticTermKey(x, x): 3.0}, - ) - - def test_all_quad(self) -> None: - mod = model.Model() - x = mod.add_binary_variable() - result = expressions.fast_sum([3.0 * x * x, x * x]) - self.assertIsInstance(result, variables.QuadraticSum) - result_expr = variables.as_flat_quadratic_expression(result) - self.assertEqual(result_expr.offset, 0.0) - self.assertEmpty(result_expr.linear_terms) - self.assertDictEqual( - dict(result_expr.quadratic_terms), - {variables.QuadraticTermKey(x, x): 4.0}, - ) + def test_variables(self) -> None: + mod = model.Model() + x = mod.add_binary_variable() + y = mod.add_binary_variable() + z = 4 + result = expressions.fast_sum([x, y, z]) + _type_check_linear_sum(result) + self.assertIsInstance(result, variables.LinearSum) + result_expr = variables.as_flat_linear_expression(result) + self.assertEqual(result_expr.offset, 4.0) + self.assertDictEqual(dict(result_expr.terms), {x: 1.0, y: 1.0}) + + def test_numbers(self) -> None: + result = expressions.fast_sum([2.0, 4.0]) + _type_check_linear_sum(result) + self.assertIsInstance(result, variables.LinearSum) + result_expr = variables.as_flat_linear_expression(result) + self.assertEqual(result_expr.offset, 6.0) + self.assertEmpty(result_expr.terms) + + def test_heterogeneous_linear(self) -> None: + mod = model.Model() + x = mod.add_binary_variable() + result = expressions.fast_sum([2.0, 3.0 * x]) + _type_check_linear_sum(result) + self.assertIsInstance(result, variables.LinearSum) + result_expr = variables.as_flat_linear_expression(result) + self.assertEqual(result_expr.offset, 2.0) + self.assertDictEqual(dict(result_expr.terms), {x: 3.0}) + + def test_heterogeneous_quad(self) -> None: + mod = model.Model() + x = mod.add_binary_variable() + result = expressions.fast_sum([2.0, 3.0 * x * x, x]) + self.assertIsInstance(result, variables.QuadraticSum) + result_expr = variables.as_flat_quadratic_expression(result) + self.assertEqual(result_expr.offset, 2.0) + self.assertDictEqual(dict(result_expr.linear_terms), {x: 1.0}) + self.assertDictEqual( + dict(result_expr.quadratic_terms), + {variables.QuadraticTermKey(x, x): 3.0}, + ) + + def test_all_quad(self) -> None: + mod = model.Model() + x = mod.add_binary_variable() + result = expressions.fast_sum([3.0 * x * x, x * x]) + self.assertIsInstance(result, variables.QuadraticSum) + result_expr = variables.as_flat_quadratic_expression(result) + self.assertEqual(result_expr.offset, 0.0) + self.assertEmpty(result_expr.linear_terms) + self.assertDictEqual( + dict(result_expr.quadratic_terms), + {variables.QuadraticTermKey(x, x): 4.0}, + ) class EvaluateExpressionTest(absltest.TestCase): - def test_scalar_expression(self) -> None: - mod = model.Model() - x = mod.add_binary_variable() - sol = {x: 1.0} - expr = 4.0 - self.assertEqual(expressions.evaluate_expression(expr, sol), 4.0) - - def test_linear(self) -> None: - mod = model.Model() - x = mod.add_binary_variable() - y = mod.add_variable() - sol = {x: 1.0, y: 4.0} - expr = 3 * x + y + 2.0 - self.assertAlmostEqual( - expressions.evaluate_expression(expr, sol), 9.0, delta=1.0e-10 - ) - - def test_quadratic(self) -> None: - mod = model.Model() - x = mod.add_binary_variable() - y = mod.add_variable() - sol = {x: 1.0, y: 4.0} - expr = 3 * x * y + y * y + 2.0 * x + 2.0 - self.assertAlmostEqual( - expressions.evaluate_expression(expr, sol), 32.0, delta=1.0e-10 - ) + def test_scalar_expression(self) -> None: + mod = model.Model() + x = mod.add_binary_variable() + sol = {x: 1.0} + expr = 4.0 + self.assertEqual(expressions.evaluate_expression(expr, sol), 4.0) + + def test_linear(self) -> None: + mod = model.Model() + x = mod.add_binary_variable() + y = mod.add_variable() + sol = {x: 1.0, y: 4.0} + expr = 3 * x + y + 2.0 + self.assertAlmostEqual( + expressions.evaluate_expression(expr, sol), 9.0, delta=1.0e-10 + ) + + def test_quadratic(self) -> None: + mod = model.Model() + x = mod.add_binary_variable() + y = mod.add_variable() + sol = {x: 1.0, y: 4.0} + expr = 3 * x * y + y * y + 2.0 * x + 2.0 + self.assertAlmostEqual( + expressions.evaluate_expression(expr, sol), 32.0, delta=1.0e-10 + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/from_model.py b/ortools/math_opt/python/from_model.py index 0da81eb5f60..5a38f2e6d56 100644 --- a/ortools/math_opt/python/from_model.py +++ b/ortools/math_opt/python/from_model.py @@ -22,16 +22,17 @@ class FromModel(Protocol): - __slots__ = () + __slots__ = () - @property - def elemental(self) -> elemental.Elemental: ... + @property + def elemental(self) -> elemental.Elemental: + ... def model_is_same(e1: FromModel, e2: FromModel) -> None: - if e1.elemental is not e2.elemental: - raise ValueError( - f"Expected two elements from the same model, but observed {e1} from" - f" model named: '{e1.elemental.model_name!r}', and {e2} from model" - f" named: '{e2.elemental.model_name!r}'." - ) + if e1.elemental is not e2.elemental: + raise ValueError( + f"Expected two elements from the same model, but observed {e1} from" + f" model named: '{e1.elemental.model_name!r}', and {e2} from model" + f" named: '{e2.elemental.model_name!r}'." + ) diff --git a/ortools/math_opt/python/hash_model_storage.py b/ortools/math_opt/python/hash_model_storage.py index 96cad3aa77f..bf1643a37a2 100644 --- a/ortools/math_opt/python/hash_model_storage.py +++ b/ortools/math_opt/python/hash_model_storage.py @@ -25,819 +25,833 @@ class _UpdateTracker(model_storage.StorageUpdateTracker): - """Tracks model updates for HashModelStorage.""" - - def __init__(self, mod: "HashModelStorage"): - self.retired: bool = False - self.model: "HashModelStorage" = mod - # Changes for variables with id < variables_checkpoint are explicitly - # tracked. - self.variables_checkpoint: int = self.model._next_var_id - # Changes for linear constraints with id < linear_constraints_checkpoint - # are explicitly tracked. - self.linear_constraints_checkpoint: int = self.model._next_lin_con_id - - self.objective_direction: bool = False - self.objective_offset: bool = False - - self.variable_deletes: Set[int] = set() - self.variable_lbs: Set[int] = set() - self.variable_ubs: Set[int] = set() - self.variable_integers: Set[int] = set() - - self.linear_objective_coefficients: Set[int] = set() - self.quadratic_objective_coefficients: Set[_QuadraticKey] = set() - - self.linear_constraint_deletes: Set[int] = set() - self.linear_constraint_lbs: Set[int] = set() - self.linear_constraint_ubs: Set[int] = set() - - self.linear_constraint_matrix: Set[Tuple[int, int]] = set() - - def export_update(self) -> Optional[model_update_pb2.ModelUpdateProto]: - if self.retired: - raise model_storage.UsedUpdateTrackerAfterRemovalError() - if ( - self.variables_checkpoint == self.model.next_variable_id() - and ( - self.linear_constraints_checkpoint - == self.model.next_linear_constraint_id() - ) - and not self.objective_direction - and not self.objective_offset - and not self.variable_deletes - and not self.variable_lbs - and not self.variable_ubs - and not self.variable_integers - and not self.linear_objective_coefficients - and not self.quadratic_objective_coefficients - and not self.linear_constraint_deletes - and not self.linear_constraint_lbs - and not self.linear_constraint_ubs - and not self.linear_constraint_matrix - ): - return None - result = model_update_pb2.ModelUpdateProto() - result.deleted_variable_ids[:] = sorted(self.variable_deletes) - result.deleted_linear_constraint_ids[:] = sorted(self.linear_constraint_deletes) - # Variable updates - _set_sparse_double_vector( - sorted((vid, self.model.get_variable_lb(vid)) for vid in self.variable_lbs), - result.variable_updates.lower_bounds, - ) - _set_sparse_double_vector( - sorted((vid, self.model.get_variable_ub(vid)) for vid in self.variable_ubs), - result.variable_updates.upper_bounds, - ) - _set_sparse_bool_vector( - sorted( - (vid, self.model.get_variable_is_integer(vid)) - for vid in self.variable_integers - ), - result.variable_updates.integers, - ) - # Linear constraint updates - _set_sparse_double_vector( - sorted( - (cid, self.model.get_linear_constraint_lb(cid)) - for cid in self.linear_constraint_lbs - ), - result.linear_constraint_updates.lower_bounds, + """Tracks model updates for HashModelStorage.""" + + def __init__(self, mod: "HashModelStorage"): + self.retired: bool = False + self.model: "HashModelStorage" = mod + # Changes for variables with id < variables_checkpoint are explicitly + # tracked. + self.variables_checkpoint: int = self.model._next_var_id + # Changes for linear constraints with id < linear_constraints_checkpoint + # are explicitly tracked. + self.linear_constraints_checkpoint: int = self.model._next_lin_con_id + + self.objective_direction: bool = False + self.objective_offset: bool = False + + self.variable_deletes: Set[int] = set() + self.variable_lbs: Set[int] = set() + self.variable_ubs: Set[int] = set() + self.variable_integers: Set[int] = set() + + self.linear_objective_coefficients: Set[int] = set() + self.quadratic_objective_coefficients: Set[_QuadraticKey] = set() + + self.linear_constraint_deletes: Set[int] = set() + self.linear_constraint_lbs: Set[int] = set() + self.linear_constraint_ubs: Set[int] = set() + + self.linear_constraint_matrix: Set[Tuple[int, int]] = set() + + def export_update(self) -> Optional[model_update_pb2.ModelUpdateProto]: + if self.retired: + raise model_storage.UsedUpdateTrackerAfterRemovalError() + if ( + self.variables_checkpoint == self.model.next_variable_id() + and ( + self.linear_constraints_checkpoint + == self.model.next_linear_constraint_id() ) - _set_sparse_double_vector( - sorted( - (cid, self.model.get_linear_constraint_ub(cid)) - for cid in self.linear_constraint_ubs - ), - result.linear_constraint_updates.upper_bounds, + and not self.objective_direction + and not self.objective_offset + and not self.variable_deletes + and not self.variable_lbs + and not self.variable_ubs + and not self.variable_integers + and not self.linear_objective_coefficients + and not self.quadratic_objective_coefficients + and not self.linear_constraint_deletes + and not self.linear_constraint_lbs + and not self.linear_constraint_ubs + and not self.linear_constraint_matrix + ): + return None + result = model_update_pb2.ModelUpdateProto() + result.deleted_variable_ids[:] = sorted(self.variable_deletes) + result.deleted_linear_constraint_ids[:] = sorted( + self.linear_constraint_deletes + ) + # Variable updates + _set_sparse_double_vector( + sorted( + (vid, self.model.get_variable_lb(vid)) for vid in self.variable_lbs + ), + result.variable_updates.lower_bounds, + ) + _set_sparse_double_vector( + sorted( + (vid, self.model.get_variable_ub(vid)) for vid in self.variable_ubs + ), + result.variable_updates.upper_bounds, + ) + _set_sparse_bool_vector( + sorted( + (vid, self.model.get_variable_is_integer(vid)) + for vid in self.variable_integers + ), + result.variable_updates.integers, + ) + # Linear constraint updates + _set_sparse_double_vector( + sorted( + (cid, self.model.get_linear_constraint_lb(cid)) + for cid in self.linear_constraint_lbs + ), + result.linear_constraint_updates.lower_bounds, + ) + _set_sparse_double_vector( + sorted( + (cid, self.model.get_linear_constraint_ub(cid)) + for cid in self.linear_constraint_ubs + ), + result.linear_constraint_updates.upper_bounds, + ) + # New variables and constraints + new_vars = [] + for vid in range(self.variables_checkpoint, self.model.next_variable_id()): + var = self.model.variables.get(vid) + if var is not None: + new_vars.append((vid, var)) + _variables_to_proto(new_vars, result.new_variables) + new_lin_cons = [] + for lin_con_id in range( + self.linear_constraints_checkpoint, + self.model.next_linear_constraint_id(), + ): + lin_con = self.model.linear_constraints.get(lin_con_id) + if lin_con is not None: + new_lin_cons.append((lin_con_id, lin_con)) + _linear_constraints_to_proto(new_lin_cons, result.new_linear_constraints) + # Objective update + if self.objective_direction: + result.objective_updates.direction_update = self.model.get_is_maximize() + if self.objective_offset: + result.objective_updates.offset_update = self.model.get_objective_offset() + _set_sparse_double_vector( + sorted( + (var, self.model.get_linear_objective_coefficient(var)) + for var in self.linear_objective_coefficients + ), + result.objective_updates.linear_coefficients, + ) + for new_var in range( + self.variables_checkpoint, self.model.next_variable_id() + ): + # NOTE: the value will be 0.0 if either the coefficient is not set or the + # variable has been deleted. Calling + # model.get_linear_objective_coefficient() throws an exception if the + # variable has been deleted. + obj_coef = self.model.linear_objective_coefficient.get(new_var, 0.0) + if obj_coef: + result.objective_updates.linear_coefficients.ids.append(new_var) + result.objective_updates.linear_coefficients.values.append(obj_coef) + + quadratic_objective_updates = [ + ( + key.id1, + key.id2, + self.model.get_quadratic_objective_coefficient(key.id1, key.id2), ) - # New variables and constraints - new_vars = [] - for vid in range(self.variables_checkpoint, self.model.next_variable_id()): - var = self.model.variables.get(vid) - if var is not None: - new_vars.append((vid, var)) - _variables_to_proto(new_vars, result.new_variables) - new_lin_cons = [] - for lin_con_id in range( - self.linear_constraints_checkpoint, - self.model.next_linear_constraint_id(), + for key in self.quadratic_objective_coefficients + ] + for new_var in range( + self.variables_checkpoint, self.model.next_variable_id() + ): + if self.model.variable_exists(new_var): + for other_var in self.model.get_quadratic_objective_adjacent_variables( + new_var ): - lin_con = self.model.linear_constraints.get(lin_con_id) - if lin_con is not None: - new_lin_cons.append((lin_con_id, lin_con)) - _linear_constraints_to_proto(new_lin_cons, result.new_linear_constraints) - # Objective update - if self.objective_direction: - result.objective_updates.direction_update = self.model.get_is_maximize() - if self.objective_offset: - result.objective_updates.offset_update = self.model.get_objective_offset() - _set_sparse_double_vector( - sorted( - (var, self.model.get_linear_objective_coefficient(var)) - for var in self.linear_objective_coefficients - ), - result.objective_updates.linear_coefficients, - ) - for new_var in range(self.variables_checkpoint, self.model.next_variable_id()): - # NOTE: the value will be 0.0 if either the coefficient is not set or the - # variable has been deleted. Calling - # model.get_linear_objective_coefficient() throws an exception if the - # variable has been deleted. - obj_coef = self.model.linear_objective_coefficient.get(new_var, 0.0) - if obj_coef: - result.objective_updates.linear_coefficients.ids.append(new_var) - result.objective_updates.linear_coefficients.values.append(obj_coef) - - quadratic_objective_updates = [ - ( + key = _QuadraticKey(new_var, other_var) + if new_var >= other_var: + key = _QuadraticKey(new_var, other_var) + quadratic_objective_updates.append(( key.id1, key.id2, - self.model.get_quadratic_objective_coefficient(key.id1, key.id2), - ) - for key in self.quadratic_objective_coefficients - ] - for new_var in range(self.variables_checkpoint, self.model.next_variable_id()): - if self.model.variable_exists(new_var): - for other_var in self.model.get_quadratic_objective_adjacent_variables( - new_var - ): - key = _QuadraticKey(new_var, other_var) - if new_var >= other_var: - key = _QuadraticKey(new_var, other_var) - quadratic_objective_updates.append( - ( - key.id1, - key.id2, - self.model.get_quadratic_objective_coefficient( - key.id1, key.id2 - ), - ) - ) - quadratic_objective_updates.sort() - if quadratic_objective_updates: - first_var_ids, second_var_ids, coefficients = zip( - *quadratic_objective_updates - ) - result.objective_updates.quadratic_coefficients.row_ids[:] = first_var_ids - result.objective_updates.quadratic_coefficients.column_ids[:] = ( - second_var_ids - ) - result.objective_updates.quadratic_coefficients.coefficients[:] = ( - coefficients - ) - # Linear constraint matrix updates - matrix_updates = [ - (l, v, self.model.get_linear_constraint_coefficient(l, v)) - for (l, v) in self.linear_constraint_matrix - ] - for new_var in range(self.variables_checkpoint, self.model.next_variable_id()): - if self.model.variable_exists(new_var): - for lin_con in self.model.get_linear_constraints_with_variable(new_var): - matrix_updates.append( - ( - lin_con, - new_var, - self.model.get_linear_constraint_coefficient( - lin_con, new_var - ), - ) - ) - for new_lin_con in range( - self.linear_constraints_checkpoint, - self.model.next_linear_constraint_id(), - ): - if self.model.linear_constraint_exists(new_lin_con): - for var in self.model.get_variables_for_linear_constraint(new_lin_con): - # We have already gotten the new variables above. Note that we do at - # most twice as much work as we should from this. - if var < self.variables_checkpoint: - matrix_updates.append( - ( - new_lin_con, - var, - self.model.get_linear_constraint_coefficient( - new_lin_con, var - ), - ) - ) - matrix_updates.sort() - if matrix_updates: - lin_cons, variables, coefs = zip(*matrix_updates) - result.linear_constraint_matrix_updates.row_ids[:] = lin_cons - result.linear_constraint_matrix_updates.column_ids[:] = variables - result.linear_constraint_matrix_updates.coefficients[:] = coefs - return result - - def advance_checkpoint(self) -> None: - if self.retired: - raise model_storage.UsedUpdateTrackerAfterRemovalError() - self.objective_direction = False - self.objective_offset = False - self.variable_deletes = set() - self.variable_lbs = set() - self.variable_ubs = set() - self.variable_integers = set() - self.linear_objective_coefficients = set() - self.linear_constraint_deletes = set() - self.linear_constraint_lbs = set() - self.linear_constraint_ubs = set() - self.linear_constraint_matrix = set() - - self.variables_checkpoint = self.model.next_variable_id() - self.linear_constraints_checkpoint = self.model.next_linear_constraint_id() + self.model.get_quadratic_objective_coefficient( + key.id1, key.id2 + ), + )) + quadratic_objective_updates.sort() + if quadratic_objective_updates: + first_var_ids, second_var_ids, coefficients = zip( + *quadratic_objective_updates + ) + result.objective_updates.quadratic_coefficients.row_ids[:] = first_var_ids + result.objective_updates.quadratic_coefficients.column_ids[:] = ( + second_var_ids + ) + result.objective_updates.quadratic_coefficients.coefficients[:] = ( + coefficients + ) + # Linear constraint matrix updates + matrix_updates = [ + (l, v, self.model.get_linear_constraint_coefficient(l, v)) + for (l, v) in self.linear_constraint_matrix + ] + for new_var in range( + self.variables_checkpoint, self.model.next_variable_id() + ): + if self.model.variable_exists(new_var): + for lin_con in self.model.get_linear_constraints_with_variable(new_var): + matrix_updates.append(( + lin_con, + new_var, + self.model.get_linear_constraint_coefficient(lin_con, new_var), + )) + for new_lin_con in range( + self.linear_constraints_checkpoint, + self.model.next_linear_constraint_id(), + ): + if self.model.linear_constraint_exists(new_lin_con): + for var in self.model.get_variables_for_linear_constraint(new_lin_con): + # We have already gotten the new variables above. Note that we do at + # most twice as much work as we should from this. + if var < self.variables_checkpoint: + matrix_updates.append(( + new_lin_con, + var, + self.model.get_linear_constraint_coefficient(new_lin_con, var), + )) + matrix_updates.sort() + if matrix_updates: + lin_cons, variables, coefs = zip(*matrix_updates) + result.linear_constraint_matrix_updates.row_ids[:] = lin_cons + result.linear_constraint_matrix_updates.column_ids[:] = variables + result.linear_constraint_matrix_updates.coefficients[:] = coefs + return result + + def advance_checkpoint(self) -> None: + if self.retired: + raise model_storage.UsedUpdateTrackerAfterRemovalError() + self.objective_direction = False + self.objective_offset = False + self.variable_deletes = set() + self.variable_lbs = set() + self.variable_ubs = set() + self.variable_integers = set() + self.linear_objective_coefficients = set() + self.linear_constraint_deletes = set() + self.linear_constraint_lbs = set() + self.linear_constraint_ubs = set() + self.linear_constraint_matrix = set() + + self.variables_checkpoint = self.model.next_variable_id() + self.linear_constraints_checkpoint = self.model.next_linear_constraint_id() class _VariableStorage: - """Data specific to each decision variable in the optimization problem.""" + """Data specific to each decision variable in the optimization problem.""" - def __init__(self, lb: float, ub: float, is_integer: bool, name: str) -> None: - self.lower_bound: float = lb - self.upper_bound: float = ub - self.is_integer: bool = is_integer - self.name: str = name - self.linear_constraint_nonzeros: Set[int] = set() + def __init__(self, lb: float, ub: float, is_integer: bool, name: str) -> None: + self.lower_bound: float = lb + self.upper_bound: float = ub + self.is_integer: bool = is_integer + self.name: str = name + self.linear_constraint_nonzeros: Set[int] = set() class _LinearConstraintStorage: - """Data specific to each linear constraint in the optimization problem.""" + """Data specific to each linear constraint in the optimization problem.""" - def __init__(self, lb: float, ub: float, name: str) -> None: - self.lower_bound: float = lb - self.upper_bound: float = ub - self.name: str = name - self.variable_nonzeros: Set[int] = set() + def __init__(self, lb: float, ub: float, name: str) -> None: + self.lower_bound: float = lb + self.upper_bound: float = ub + self.name: str = name + self.variable_nonzeros: Set[int] = set() class _QuadraticTermStorage: - """Data describing quadratic terms with non-zero coefficients.""" - - def __init__(self) -> None: - self._coefficients: Dict[_QuadraticKey, float] = {} - # For a variable i that does not appear in a quadratic objective term with - # a non-zero coefficient, we may have self._adjacent_variable[i] being an - # empty set or i not appearing in self._adjacent_variable.keys() (e.g. - # depeding on whether the variable previously appeared in a quadratic term). - self._adjacent_variables: Dict[int, Set[int]] = {} - - def __bool__(self) -> bool: - """Returns true if and only if there are any quadratic terms with non-zero coefficients.""" - return bool(self._coefficients) - - def get_adjacent_variables(self, variable_id: int) -> Iterator[int]: - """Yields the variables multiplying a variable in the stored quadratic terms. - - If variable_id is not in the model the function yields the empty set. - - Args: - variable_id: Function yields the variables multiplying variable_id in the - stored quadratic terms. - - Yields: - The variables multiplying variable_id in the stored quadratic terms. - """ - yield from self._adjacent_variables.get(variable_id, ()) - - def keys(self) -> Iterator[_QuadraticKey]: - """Yields the variable-pair keys associated to the stored quadratic terms.""" - yield from self._coefficients.keys() - - def coefficients(self) -> Iterator[model_storage.QuadraticEntry]: - """Yields the stored quadratic terms as QuadraticEntry.""" - for key, coef in self._coefficients.items(): - yield model_storage.QuadraticEntry(id_key=key, coefficient=coef) - - def delete_variable(self, variable_id: int) -> None: - """Updates the data structure to consider variable_id as deleted.""" - if variable_id not in self._adjacent_variables.keys(): - return - for adjacent_variable_id in self._adjacent_variables[variable_id]: - if variable_id != adjacent_variable_id: - self._adjacent_variables[adjacent_variable_id].remove(variable_id) - del self._coefficients[_QuadraticKey(variable_id, adjacent_variable_id)] - self._adjacent_variables[variable_id].clear() - - def clear(self) -> None: - """Clears the data structure.""" - self._coefficients.clear() - self._adjacent_variables.clear() - - def set_coefficient( - self, first_variable_id: int, second_variable_id: int, value: float - ) -> bool: - """Sets the coefficient for the quadratic term associated to the product between two variables. - - The ordering of the input variables does not matter. - - Args: - first_variable_id: The first variable in the product. - second_variable_id: The second variable in the product. - value: The value of the coefficient. - - Returns: - True if the coefficient is updated, False otherwise. - """ - key = _QuadraticKey(first_variable_id, second_variable_id) - if value == self._coefficients.get(key, 0.0): - return False - if value == 0.0: - # Assuming self._coefficients/_adjacent_variables are filled according - # to get_coefficient(key) != 0.0. - del self._coefficients[key] - self._adjacent_variables[first_variable_id].remove(second_variable_id) - if first_variable_id != second_variable_id: - self._adjacent_variables[second_variable_id].remove(first_variable_id) - else: - if first_variable_id not in self._adjacent_variables.keys(): - self._adjacent_variables[first_variable_id] = set() - if second_variable_id not in self._adjacent_variables.keys(): - self._adjacent_variables[second_variable_id] = set() - self._coefficients[key] = value - self._adjacent_variables[first_variable_id].add(second_variable_id) - self._adjacent_variables[second_variable_id].add(first_variable_id) - return True - - def get_coefficient(self, first_variable_id: int, second_variable_id: int) -> float: - """Gets the objective coefficient for the quadratic term associated to the product between two variables. - - The ordering of the input variables does not matter. - - Args: - first_variable_id: The first variable in the product. - second_variable_id: The second variable in the product. - - Returns: - The value of the coefficient. - """ - return self._coefficients.get( - _QuadraticKey(first_variable_id, second_variable_id), 0.0 - ) + """Data describing quadratic terms with non-zero coefficients.""" + def __init__(self) -> None: + self._coefficients: Dict[_QuadraticKey, float] = {} + # For a variable i that does not appear in a quadratic objective term with + # a non-zero coefficient, we may have self._adjacent_variable[i] being an + # empty set or i not appearing in self._adjacent_variable.keys() (e.g. + # depeding on whether the variable previously appeared in a quadratic term). + self._adjacent_variables: Dict[int, Set[int]] = {} -class HashModelStorage(model_storage.ModelStorage): - """A simple, pure python implementation of ModelStorage. - - Attributes: - _linear_constraint_matrix: A dictionary with (linear_constraint_id, - variable_id) keys and numeric values, representing the matrix A for the - constraints lb_c <= A*x <= ub_c. Invariant: the values have no zeros. - linear_objective_coefficient: A dictionary with variable_id keys and - numeric values, representing the linear terms in the objective. - Invariant: the values have no zeros. - _quadratic_objective_coefficients: A data structure containing quadratic - terms in the objective. - """ + def __bool__(self) -> bool: + """Returns true if and only if there are any quadratic terms with non-zero coefficients.""" + return bool(self._coefficients) - def __init__(self, name: str = "") -> None: - super().__init__() - self._name: str = name - self.variables: Dict[int, _VariableStorage] = {} - self.linear_constraints: Dict[int, _LinearConstraintStorage] = {} - self._linear_constraint_matrix: Dict[Tuple[int, int], float] = {} # - self._is_maximize: bool = False - self._objective_offset: float = 0.0 - self.linear_objective_coefficient: Dict[int, float] = {} - self._quadratic_objective_coefficients: _QuadraticTermStorage = ( - _QuadraticTermStorage() - ) - self._next_var_id: int = 0 - self._next_lin_con_id: int = 0 - self._update_trackers: weakref.WeakSet[_UpdateTracker] = weakref.WeakSet() - - @property - def name(self) -> str: - return self._name - - def add_variable(self, lb: float, ub: float, is_integer: bool, name: str) -> int: - var_id = self._next_var_id - self._next_var_id += 1 - self.variables[var_id] = _VariableStorage(lb, ub, is_integer, name) - return var_id - - def delete_variable(self, variable_id: int) -> None: - self._check_variable_id(variable_id) - variable = self.variables[variable_id] - # First update the watchers - for watcher in self._update_trackers: - if variable_id < watcher.variables_checkpoint: - watcher.variable_deletes.add(variable_id) - watcher.variable_lbs.discard(variable_id) - watcher.variable_ubs.discard(variable_id) - watcher.variable_integers.discard(variable_id) - watcher.linear_objective_coefficients.discard(variable_id) - for ( - other_variable_id - ) in self._quadratic_objective_coefficients.get_adjacent_variables( - variable_id - ): - key = _QuadraticKey(variable_id, other_variable_id) - watcher.quadratic_objective_coefficients.discard(key) - for lin_con_id in variable.linear_constraint_nonzeros: - if lin_con_id < watcher.linear_constraints_checkpoint: - watcher.linear_constraint_matrix.discard( - (lin_con_id, variable_id) - ) - # Then update self. - for lin_con_id in variable.linear_constraint_nonzeros: - self.linear_constraints[lin_con_id].variable_nonzeros.remove(variable_id) - del self._linear_constraint_matrix[(lin_con_id, variable_id)] - del self.variables[variable_id] - self.linear_objective_coefficient.pop(variable_id, None) - self._quadratic_objective_coefficients.delete_variable(variable_id) - - def variable_exists(self, variable_id: int) -> bool: - return variable_id in self.variables - - def next_variable_id(self) -> int: - return self._next_var_id - - def set_variable_lb(self, variable_id: int, lb: float) -> None: - self._check_variable_id(variable_id) - if lb == self.variables[variable_id].lower_bound: - return - self.variables[variable_id].lower_bound = lb - for watcher in self._update_trackers: - if variable_id < watcher.variables_checkpoint: - watcher.variable_lbs.add(variable_id) - - def set_variable_ub(self, variable_id: int, ub: float) -> None: - self._check_variable_id(variable_id) - if ub == self.variables[variable_id].upper_bound: - return - self.variables[variable_id].upper_bound = ub - for watcher in self._update_trackers: - if variable_id < watcher.variables_checkpoint: - watcher.variable_ubs.add(variable_id) - - def set_variable_is_integer(self, variable_id: int, is_integer: bool) -> None: - self._check_variable_id(variable_id) - if is_integer == self.variables[variable_id].is_integer: - return - self.variables[variable_id].is_integer = is_integer - for watcher in self._update_trackers: - if variable_id < watcher.variables_checkpoint: - watcher.variable_integers.add(variable_id) - - def get_variable_lb(self, variable_id: int) -> float: - self._check_variable_id(variable_id) - return self.variables[variable_id].lower_bound - - def get_variable_ub(self, variable_id: int) -> float: - self._check_variable_id(variable_id) - return self.variables[variable_id].upper_bound - - def get_variable_is_integer(self, variable_id: int) -> bool: - self._check_variable_id(variable_id) - return self.variables[variable_id].is_integer - - def get_variable_name(self, variable_id: int) -> str: - self._check_variable_id(variable_id) - return self.variables[variable_id].name - - def get_variables(self) -> Iterator[int]: - yield from self.variables.keys() - - def add_linear_constraint(self, lb: float, ub: float, name: str) -> int: - lin_con_id = self._next_lin_con_id - self._next_lin_con_id += 1 - self.linear_constraints[lin_con_id] = _LinearConstraintStorage(lb, ub, name) - return lin_con_id - - def delete_linear_constraint(self, linear_constraint_id: int) -> None: - self._check_linear_constraint_id(linear_constraint_id) - con = self.linear_constraints[linear_constraint_id] - # First update the watchers - for watcher in self._update_trackers: - if linear_constraint_id < watcher.linear_constraints_checkpoint: - watcher.linear_constraint_deletes.add(linear_constraint_id) - watcher.linear_constraint_lbs.discard(linear_constraint_id) - watcher.linear_constraint_ubs.discard(linear_constraint_id) - for var_id in con.variable_nonzeros: - if var_id < watcher.variables_checkpoint: - watcher.linear_constraint_matrix.discard( - (linear_constraint_id, var_id) - ) - # Then update self. - for var_id in con.variable_nonzeros: - self.variables[var_id].linear_constraint_nonzeros.remove( - linear_constraint_id - ) - del self._linear_constraint_matrix[(linear_constraint_id, var_id)] - del self.linear_constraints[linear_constraint_id] - - def linear_constraint_exists(self, linear_constraint_id: int) -> bool: - return linear_constraint_id in self.linear_constraints - - def next_linear_constraint_id(self) -> int: - return self._next_lin_con_id - - def set_linear_constraint_lb(self, linear_constraint_id: int, lb: float) -> None: - self._check_linear_constraint_id(linear_constraint_id) - if lb == self.linear_constraints[linear_constraint_id].lower_bound: - return - self.linear_constraints[linear_constraint_id].lower_bound = lb - for watcher in self._update_trackers: - if linear_constraint_id < watcher.linear_constraints_checkpoint: - watcher.linear_constraint_lbs.add(linear_constraint_id) - - def set_linear_constraint_ub(self, linear_constraint_id: int, ub: float) -> None: - self._check_linear_constraint_id(linear_constraint_id) - if ub == self.linear_constraints[linear_constraint_id].upper_bound: - return - self.linear_constraints[linear_constraint_id].upper_bound = ub - for watcher in self._update_trackers: - if linear_constraint_id < watcher.linear_constraints_checkpoint: - watcher.linear_constraint_ubs.add(linear_constraint_id) - - def get_linear_constraint_lb(self, linear_constraint_id: int) -> float: - self._check_linear_constraint_id(linear_constraint_id) - return self.linear_constraints[linear_constraint_id].lower_bound - - def get_linear_constraint_ub(self, linear_constraint_id: int) -> float: - self._check_linear_constraint_id(linear_constraint_id) - return self.linear_constraints[linear_constraint_id].upper_bound - - def get_linear_constraint_name(self, linear_constraint_id: int) -> str: - self._check_linear_constraint_id(linear_constraint_id) - return self.linear_constraints[linear_constraint_id].name - - def get_linear_constraints(self) -> Iterator[int]: - yield from self.linear_constraints.keys() - - def set_linear_constraint_coefficient( - self, linear_constraint_id: int, variable_id: int, value: float - ) -> None: - self._check_linear_constraint_id(linear_constraint_id) - self._check_variable_id(variable_id) - if value == self._linear_constraint_matrix.get( - (linear_constraint_id, variable_id), 0.0 - ): - return - if value == 0.0: - self._linear_constraint_matrix.pop( - (linear_constraint_id, variable_id), None - ) - self.variables[variable_id].linear_constraint_nonzeros.discard( - linear_constraint_id - ) - self.linear_constraints[linear_constraint_id].variable_nonzeros.discard( - variable_id - ) - else: - self._linear_constraint_matrix[(linear_constraint_id, variable_id)] = value - self.variables[variable_id].linear_constraint_nonzeros.add( - linear_constraint_id - ) - self.linear_constraints[linear_constraint_id].variable_nonzeros.add( - variable_id - ) - for watcher in self._update_trackers: - if ( - variable_id < watcher.variables_checkpoint - and linear_constraint_id < watcher.linear_constraints_checkpoint - ): - watcher.linear_constraint_matrix.add( - (linear_constraint_id, variable_id) - ) - - def get_linear_constraint_coefficient( - self, linear_constraint_id: int, variable_id: int - ) -> float: - self._check_linear_constraint_id(linear_constraint_id) - self._check_variable_id(variable_id) - return self._linear_constraint_matrix.get( - (linear_constraint_id, variable_id), 0.0 - ) + def get_adjacent_variables(self, variable_id: int) -> Iterator[int]: + """Yields the variables multiplying a variable in the stored quadratic terms. - def get_linear_constraints_with_variable(self, variable_id: int) -> Iterator[int]: - self._check_variable_id(variable_id) - yield from self.variables[variable_id].linear_constraint_nonzeros - - def get_variables_for_linear_constraint( - self, linear_constraint_id: int - ) -> Iterator[int]: - self._check_linear_constraint_id(linear_constraint_id) - yield from self.linear_constraints[linear_constraint_id].variable_nonzeros - - def get_linear_constraint_matrix_entries( - self, - ) -> Iterator[model_storage.LinearConstraintMatrixIdEntry]: - for (constraint, variable), coef in self._linear_constraint_matrix.items(): - yield model_storage.LinearConstraintMatrixIdEntry( - linear_constraint_id=constraint, - variable_id=variable, - coefficient=coef, - ) + If variable_id is not in the model the function yields the empty set. - def clear_objective(self) -> None: - for variable_id in self.linear_objective_coefficient: - for watcher in self._update_trackers: - if variable_id < watcher.variables_checkpoint: - watcher.linear_objective_coefficients.add(variable_id) - self.linear_objective_coefficient.clear() - for key in self._quadratic_objective_coefficients.keys(): - for watcher in self._update_trackers: - if key.id2 < watcher.variables_checkpoint: - watcher.quadratic_objective_coefficients.add(key) - self._quadratic_objective_coefficients.clear() - self.set_objective_offset(0.0) - - def set_linear_objective_coefficient(self, variable_id: int, value: float) -> None: - self._check_variable_id(variable_id) - if value == self.linear_objective_coefficient.get(variable_id, 0.0): - return - if value == 0.0: - self.linear_objective_coefficient.pop(variable_id, None) - else: - self.linear_objective_coefficient[variable_id] = value - for watcher in self._update_trackers: - if variable_id < watcher.variables_checkpoint: - watcher.linear_objective_coefficients.add(variable_id) - - def get_linear_objective_coefficient(self, variable_id: int) -> float: - self._check_variable_id(variable_id) - return self.linear_objective_coefficient.get(variable_id, 0.0) - - def get_linear_objective_coefficients( - self, - ) -> Iterator[model_storage.LinearObjectiveEntry]: - for var_id, coef in self.linear_objective_coefficient.items(): - yield model_storage.LinearObjectiveEntry( - variable_id=var_id, coefficient=coef - ) + Args: + variable_id: Function yields the variables multiplying variable_id in the + stored quadratic terms. - def set_quadratic_objective_coefficient( - self, first_variable_id: int, second_variable_id: int, value: float - ) -> None: - self._check_variable_id(first_variable_id) - self._check_variable_id(second_variable_id) - updated = self._quadratic_objective_coefficients.set_coefficient( - first_variable_id, second_variable_id, value - ) - if updated: - for watcher in self._update_trackers: - if ( - max(first_variable_id, second_variable_id) - < watcher.variables_checkpoint - ): - watcher.quadratic_objective_coefficients.add( - _QuadraticKey(first_variable_id, second_variable_id) - ) - - def get_quadratic_objective_coefficient( - self, first_variable_id: int, second_variable_id: int - ) -> float: - self._check_variable_id(first_variable_id) - self._check_variable_id(second_variable_id) - return self._quadratic_objective_coefficients.get_coefficient( - first_variable_id, second_variable_id - ) + Yields: + The variables multiplying variable_id in the stored quadratic terms. + """ + yield from self._adjacent_variables.get(variable_id, ()) + + def keys(self) -> Iterator[_QuadraticKey]: + """Yields the variable-pair keys associated to the stored quadratic terms.""" + yield from self._coefficients.keys() + + def coefficients(self) -> Iterator[model_storage.QuadraticEntry]: + """Yields the stored quadratic terms as QuadraticEntry.""" + for key, coef in self._coefficients.items(): + yield model_storage.QuadraticEntry(id_key=key, coefficient=coef) + + def delete_variable(self, variable_id: int) -> None: + """Updates the data structure to consider variable_id as deleted.""" + if variable_id not in self._adjacent_variables.keys(): + return + for adjacent_variable_id in self._adjacent_variables[variable_id]: + if variable_id != adjacent_variable_id: + self._adjacent_variables[adjacent_variable_id].remove(variable_id) + del self._coefficients[_QuadraticKey(variable_id, adjacent_variable_id)] + self._adjacent_variables[variable_id].clear() + + def clear(self) -> None: + """Clears the data structure.""" + self._coefficients.clear() + self._adjacent_variables.clear() + + def set_coefficient( + self, first_variable_id: int, second_variable_id: int, value: float + ) -> bool: + """Sets the coefficient for the quadratic term associated to the product between two variables. + + The ordering of the input variables does not matter. + + Args: + first_variable_id: The first variable in the product. + second_variable_id: The second variable in the product. + value: The value of the coefficient. + + Returns: + True if the coefficient is updated, False otherwise. + """ + key = _QuadraticKey(first_variable_id, second_variable_id) + if value == self._coefficients.get(key, 0.0): + return False + if value == 0.0: + # Assuming self._coefficients/_adjacent_variables are filled according + # to get_coefficient(key) != 0.0. + del self._coefficients[key] + self._adjacent_variables[first_variable_id].remove(second_variable_id) + if first_variable_id != second_variable_id: + self._adjacent_variables[second_variable_id].remove(first_variable_id) + else: + if first_variable_id not in self._adjacent_variables.keys(): + self._adjacent_variables[first_variable_id] = set() + if second_variable_id not in self._adjacent_variables.keys(): + self._adjacent_variables[second_variable_id] = set() + self._coefficients[key] = value + self._adjacent_variables[first_variable_id].add(second_variable_id) + self._adjacent_variables[second_variable_id].add(first_variable_id) + return True + + def get_coefficient( + self, first_variable_id: int, second_variable_id: int + ) -> float: + """Gets the objective coefficient for the quadratic term associated to the product between two variables. + + The ordering of the input variables does not matter. + + Args: + first_variable_id: The first variable in the product. + second_variable_id: The second variable in the product. + + Returns: + The value of the coefficient. + """ + return self._coefficients.get( + _QuadraticKey(first_variable_id, second_variable_id), 0.0 + ) - def get_quadratic_objective_coefficients( - self, - ) -> Iterator[model_storage.QuadraticEntry]: - yield from self._quadratic_objective_coefficients.coefficients() - def get_quadratic_objective_adjacent_variables( - self, variable_id: int - ) -> Iterator[int]: - self._check_variable_id(variable_id) - yield from self._quadratic_objective_coefficients.get_adjacent_variables( +class HashModelStorage(model_storage.ModelStorage): + """A simple, pure python implementation of ModelStorage. + + Attributes: + _linear_constraint_matrix: A dictionary with (linear_constraint_id, + variable_id) keys and numeric values, representing the matrix A for the + constraints lb_c <= A*x <= ub_c. Invariant: the values have no zeros. + linear_objective_coefficient: A dictionary with variable_id keys and + numeric values, representing the linear terms in the objective. + Invariant: the values have no zeros. + _quadratic_objective_coefficients: A data structure containing quadratic + terms in the objective. + """ + + def __init__(self, name: str = "") -> None: + super().__init__() + self._name: str = name + self.variables: Dict[int, _VariableStorage] = {} + self.linear_constraints: Dict[int, _LinearConstraintStorage] = {} + self._linear_constraint_matrix: Dict[Tuple[int, int], float] = {} # + self._is_maximize: bool = False + self._objective_offset: float = 0.0 + self.linear_objective_coefficient: Dict[int, float] = {} + self._quadratic_objective_coefficients: _QuadraticTermStorage = ( + _QuadraticTermStorage() + ) + self._next_var_id: int = 0 + self._next_lin_con_id: int = 0 + self._update_trackers: weakref.WeakSet[_UpdateTracker] = weakref.WeakSet() + + @property + def name(self) -> str: + return self._name + + def add_variable( + self, lb: float, ub: float, is_integer: bool, name: str + ) -> int: + var_id = self._next_var_id + self._next_var_id += 1 + self.variables[var_id] = _VariableStorage(lb, ub, is_integer, name) + return var_id + + def delete_variable(self, variable_id: int) -> None: + self._check_variable_id(variable_id) + variable = self.variables[variable_id] + # First update the watchers + for watcher in self._update_trackers: + if variable_id < watcher.variables_checkpoint: + watcher.variable_deletes.add(variable_id) + watcher.variable_lbs.discard(variable_id) + watcher.variable_ubs.discard(variable_id) + watcher.variable_integers.discard(variable_id) + watcher.linear_objective_coefficients.discard(variable_id) + for ( + other_variable_id + ) in self._quadratic_objective_coefficients.get_adjacent_variables( variable_id + ): + key = _QuadraticKey(variable_id, other_variable_id) + watcher.quadratic_objective_coefficients.discard(key) + for lin_con_id in variable.linear_constraint_nonzeros: + if lin_con_id < watcher.linear_constraints_checkpoint: + watcher.linear_constraint_matrix.discard((lin_con_id, variable_id)) + # Then update self. + for lin_con_id in variable.linear_constraint_nonzeros: + self.linear_constraints[lin_con_id].variable_nonzeros.remove(variable_id) + del self._linear_constraint_matrix[(lin_con_id, variable_id)] + del self.variables[variable_id] + self.linear_objective_coefficient.pop(variable_id, None) + self._quadratic_objective_coefficients.delete_variable(variable_id) + + def variable_exists(self, variable_id: int) -> bool: + return variable_id in self.variables + + def next_variable_id(self) -> int: + return self._next_var_id + + def set_variable_lb(self, variable_id: int, lb: float) -> None: + self._check_variable_id(variable_id) + if lb == self.variables[variable_id].lower_bound: + return + self.variables[variable_id].lower_bound = lb + for watcher in self._update_trackers: + if variable_id < watcher.variables_checkpoint: + watcher.variable_lbs.add(variable_id) + + def set_variable_ub(self, variable_id: int, ub: float) -> None: + self._check_variable_id(variable_id) + if ub == self.variables[variable_id].upper_bound: + return + self.variables[variable_id].upper_bound = ub + for watcher in self._update_trackers: + if variable_id < watcher.variables_checkpoint: + watcher.variable_ubs.add(variable_id) + + def set_variable_is_integer(self, variable_id: int, is_integer: bool) -> None: + self._check_variable_id(variable_id) + if is_integer == self.variables[variable_id].is_integer: + return + self.variables[variable_id].is_integer = is_integer + for watcher in self._update_trackers: + if variable_id < watcher.variables_checkpoint: + watcher.variable_integers.add(variable_id) + + def get_variable_lb(self, variable_id: int) -> float: + self._check_variable_id(variable_id) + return self.variables[variable_id].lower_bound + + def get_variable_ub(self, variable_id: int) -> float: + self._check_variable_id(variable_id) + return self.variables[variable_id].upper_bound + + def get_variable_is_integer(self, variable_id: int) -> bool: + self._check_variable_id(variable_id) + return self.variables[variable_id].is_integer + + def get_variable_name(self, variable_id: int) -> str: + self._check_variable_id(variable_id) + return self.variables[variable_id].name + + def get_variables(self) -> Iterator[int]: + yield from self.variables.keys() + + def add_linear_constraint(self, lb: float, ub: float, name: str) -> int: + lin_con_id = self._next_lin_con_id + self._next_lin_con_id += 1 + self.linear_constraints[lin_con_id] = _LinearConstraintStorage(lb, ub, name) + return lin_con_id + + def delete_linear_constraint(self, linear_constraint_id: int) -> None: + self._check_linear_constraint_id(linear_constraint_id) + con = self.linear_constraints[linear_constraint_id] + # First update the watchers + for watcher in self._update_trackers: + if linear_constraint_id < watcher.linear_constraints_checkpoint: + watcher.linear_constraint_deletes.add(linear_constraint_id) + watcher.linear_constraint_lbs.discard(linear_constraint_id) + watcher.linear_constraint_ubs.discard(linear_constraint_id) + for var_id in con.variable_nonzeros: + if var_id < watcher.variables_checkpoint: + watcher.linear_constraint_matrix.discard( + (linear_constraint_id, var_id) + ) + # Then update self. + for var_id in con.variable_nonzeros: + self.variables[var_id].linear_constraint_nonzeros.remove( + linear_constraint_id + ) + del self._linear_constraint_matrix[(linear_constraint_id, var_id)] + del self.linear_constraints[linear_constraint_id] + + def linear_constraint_exists(self, linear_constraint_id: int) -> bool: + return linear_constraint_id in self.linear_constraints + + def next_linear_constraint_id(self) -> int: + return self._next_lin_con_id + + def set_linear_constraint_lb( + self, linear_constraint_id: int, lb: float + ) -> None: + self._check_linear_constraint_id(linear_constraint_id) + if lb == self.linear_constraints[linear_constraint_id].lower_bound: + return + self.linear_constraints[linear_constraint_id].lower_bound = lb + for watcher in self._update_trackers: + if linear_constraint_id < watcher.linear_constraints_checkpoint: + watcher.linear_constraint_lbs.add(linear_constraint_id) + + def set_linear_constraint_ub( + self, linear_constraint_id: int, ub: float + ) -> None: + self._check_linear_constraint_id(linear_constraint_id) + if ub == self.linear_constraints[linear_constraint_id].upper_bound: + return + self.linear_constraints[linear_constraint_id].upper_bound = ub + for watcher in self._update_trackers: + if linear_constraint_id < watcher.linear_constraints_checkpoint: + watcher.linear_constraint_ubs.add(linear_constraint_id) + + def get_linear_constraint_lb(self, linear_constraint_id: int) -> float: + self._check_linear_constraint_id(linear_constraint_id) + return self.linear_constraints[linear_constraint_id].lower_bound + + def get_linear_constraint_ub(self, linear_constraint_id: int) -> float: + self._check_linear_constraint_id(linear_constraint_id) + return self.linear_constraints[linear_constraint_id].upper_bound + + def get_linear_constraint_name(self, linear_constraint_id: int) -> str: + self._check_linear_constraint_id(linear_constraint_id) + return self.linear_constraints[linear_constraint_id].name + + def get_linear_constraints(self) -> Iterator[int]: + yield from self.linear_constraints.keys() + + def set_linear_constraint_coefficient( + self, linear_constraint_id: int, variable_id: int, value: float + ) -> None: + self._check_linear_constraint_id(linear_constraint_id) + self._check_variable_id(variable_id) + if value == self._linear_constraint_matrix.get( + (linear_constraint_id, variable_id), 0.0 + ): + return + if value == 0.0: + self._linear_constraint_matrix.pop( + (linear_constraint_id, variable_id), None + ) + self.variables[variable_id].linear_constraint_nonzeros.discard( + linear_constraint_id + ) + self.linear_constraints[linear_constraint_id].variable_nonzeros.discard( + variable_id + ) + else: + self._linear_constraint_matrix[(linear_constraint_id, variable_id)] = ( + value + ) + self.variables[variable_id].linear_constraint_nonzeros.add( + linear_constraint_id + ) + self.linear_constraints[linear_constraint_id].variable_nonzeros.add( + variable_id + ) + for watcher in self._update_trackers: + if ( + variable_id < watcher.variables_checkpoint + and linear_constraint_id < watcher.linear_constraints_checkpoint + ): + watcher.linear_constraint_matrix.add( + (linear_constraint_id, variable_id) ) - def set_is_maximize(self, is_maximize: bool) -> None: - if self._is_maximize == is_maximize: - return - self._is_maximize = is_maximize - for watcher in self._update_trackers: - watcher.objective_direction = True - - def get_is_maximize(self) -> bool: - return self._is_maximize - - def set_objective_offset(self, offset: float) -> None: - if self._objective_offset == offset: - return - self._objective_offset = offset - for watcher in self._update_trackers: - watcher.objective_offset = True - - def get_objective_offset(self) -> float: - return self._objective_offset - - def export_model(self) -> model_pb2.ModelProto: - m: model_pb2.ModelProto = model_pb2.ModelProto() - m.name = self._name - _variables_to_proto(self.variables.items(), m.variables) - _linear_constraints_to_proto( - self.linear_constraints.items(), m.linear_constraints - ) - m.objective.maximize = self._is_maximize - m.objective.offset = self._objective_offset - if self.linear_objective_coefficient: - obj_ids, obj_coefs = zip(*sorted(self.linear_objective_coefficient.items())) - m.objective.linear_coefficients.ids.extend(obj_ids) - m.objective.linear_coefficients.values.extend(obj_coefs) - if self._quadratic_objective_coefficients: - first_var_ids, second_var_ids, coefficients = zip( - *sorted( - [ - (entry.id_key.id1, entry.id_key.id2, entry.coefficient) - for entry in self._quadratic_objective_coefficients.coefficients() - ] - ) - ) - m.objective.quadratic_coefficients.row_ids.extend(first_var_ids) - m.objective.quadratic_coefficients.column_ids.extend(second_var_ids) - m.objective.quadratic_coefficients.coefficients.extend(coefficients) - if self._linear_constraint_matrix: - flat_matrix_items = [ - (con_id, var_id, coef) - for ((con_id, var_id), coef) in self._linear_constraint_matrix.items() - ] - lin_con_ids, var_ids, lin_con_coefs = zip(*sorted(flat_matrix_items)) - m.linear_constraint_matrix.row_ids.extend(lin_con_ids) - m.linear_constraint_matrix.column_ids.extend(var_ids) - m.linear_constraint_matrix.coefficients.extend(lin_con_coefs) - return m - - def add_update_tracker(self) -> model_storage.StorageUpdateTracker: - tracker = _UpdateTracker(self) - self._update_trackers.add(tracker) - return tracker - - def remove_update_tracker( - self, tracker: model_storage.StorageUpdateTracker - ) -> None: - self._update_trackers.remove(tracker) - tracker.retired = True - - def _check_variable_id(self, variable_id: int) -> None: - if variable_id not in self.variables: - raise model_storage.BadVariableIdError(variable_id) - - def _check_linear_constraint_id(self, linear_constraint_id: int) -> None: - if linear_constraint_id not in self.linear_constraints: - raise model_storage.BadLinearConstraintIdError(linear_constraint_id) + def get_linear_constraint_coefficient( + self, linear_constraint_id: int, variable_id: int + ) -> float: + self._check_linear_constraint_id(linear_constraint_id) + self._check_variable_id(variable_id) + return self._linear_constraint_matrix.get( + (linear_constraint_id, variable_id), 0.0 + ) + + def get_linear_constraints_with_variable( + self, variable_id: int + ) -> Iterator[int]: + self._check_variable_id(variable_id) + yield from self.variables[variable_id].linear_constraint_nonzeros + + def get_variables_for_linear_constraint( + self, linear_constraint_id: int + ) -> Iterator[int]: + self._check_linear_constraint_id(linear_constraint_id) + yield from self.linear_constraints[linear_constraint_id].variable_nonzeros + + def get_linear_constraint_matrix_entries( + self, + ) -> Iterator[model_storage.LinearConstraintMatrixIdEntry]: + for (constraint, variable), coef in self._linear_constraint_matrix.items(): + yield model_storage.LinearConstraintMatrixIdEntry( + linear_constraint_id=constraint, + variable_id=variable, + coefficient=coef, + ) + + def clear_objective(self) -> None: + for variable_id in self.linear_objective_coefficient: + for watcher in self._update_trackers: + if variable_id < watcher.variables_checkpoint: + watcher.linear_objective_coefficients.add(variable_id) + self.linear_objective_coefficient.clear() + for key in self._quadratic_objective_coefficients.keys(): + for watcher in self._update_trackers: + if key.id2 < watcher.variables_checkpoint: + watcher.quadratic_objective_coefficients.add(key) + self._quadratic_objective_coefficients.clear() + self.set_objective_offset(0.0) + + def set_linear_objective_coefficient( + self, variable_id: int, value: float + ) -> None: + self._check_variable_id(variable_id) + if value == self.linear_objective_coefficient.get(variable_id, 0.0): + return + if value == 0.0: + self.linear_objective_coefficient.pop(variable_id, None) + else: + self.linear_objective_coefficient[variable_id] = value + for watcher in self._update_trackers: + if variable_id < watcher.variables_checkpoint: + watcher.linear_objective_coefficients.add(variable_id) + + def get_linear_objective_coefficient(self, variable_id: int) -> float: + self._check_variable_id(variable_id) + return self.linear_objective_coefficient.get(variable_id, 0.0) + + def get_linear_objective_coefficients( + self, + ) -> Iterator[model_storage.LinearObjectiveEntry]: + for var_id, coef in self.linear_objective_coefficient.items(): + yield model_storage.LinearObjectiveEntry( + variable_id=var_id, coefficient=coef + ) + + def set_quadratic_objective_coefficient( + self, first_variable_id: int, second_variable_id: int, value: float + ) -> None: + self._check_variable_id(first_variable_id) + self._check_variable_id(second_variable_id) + updated = self._quadratic_objective_coefficients.set_coefficient( + first_variable_id, second_variable_id, value + ) + if updated: + for watcher in self._update_trackers: + if ( + max(first_variable_id, second_variable_id) + < watcher.variables_checkpoint + ): + watcher.quadratic_objective_coefficients.add( + _QuadraticKey(first_variable_id, second_variable_id) + ) + + def get_quadratic_objective_coefficient( + self, first_variable_id: int, second_variable_id: int + ) -> float: + self._check_variable_id(first_variable_id) + self._check_variable_id(second_variable_id) + return self._quadratic_objective_coefficients.get_coefficient( + first_variable_id, second_variable_id + ) + + def get_quadratic_objective_coefficients( + self, + ) -> Iterator[model_storage.QuadraticEntry]: + yield from self._quadratic_objective_coefficients.coefficients() + + def get_quadratic_objective_adjacent_variables( + self, variable_id: int + ) -> Iterator[int]: + self._check_variable_id(variable_id) + yield from self._quadratic_objective_coefficients.get_adjacent_variables( + variable_id + ) + + def set_is_maximize(self, is_maximize: bool) -> None: + if self._is_maximize == is_maximize: + return + self._is_maximize = is_maximize + for watcher in self._update_trackers: + watcher.objective_direction = True + + def get_is_maximize(self) -> bool: + return self._is_maximize + + def set_objective_offset(self, offset: float) -> None: + if self._objective_offset == offset: + return + self._objective_offset = offset + for watcher in self._update_trackers: + watcher.objective_offset = True + + def get_objective_offset(self) -> float: + return self._objective_offset + + def export_model(self) -> model_pb2.ModelProto: + m: model_pb2.ModelProto = model_pb2.ModelProto() + m.name = self._name + _variables_to_proto(self.variables.items(), m.variables) + _linear_constraints_to_proto( + self.linear_constraints.items(), m.linear_constraints + ) + m.objective.maximize = self._is_maximize + m.objective.offset = self._objective_offset + if self.linear_objective_coefficient: + obj_ids, obj_coefs = zip( + *sorted(self.linear_objective_coefficient.items()) + ) + m.objective.linear_coefficients.ids.extend(obj_ids) + m.objective.linear_coefficients.values.extend(obj_coefs) + if self._quadratic_objective_coefficients: + first_var_ids, second_var_ids, coefficients = zip( + *sorted([ + (entry.id_key.id1, entry.id_key.id2, entry.coefficient) + for entry in self._quadratic_objective_coefficients.coefficients() + ]) + ) + m.objective.quadratic_coefficients.row_ids.extend(first_var_ids) + m.objective.quadratic_coefficients.column_ids.extend(second_var_ids) + m.objective.quadratic_coefficients.coefficients.extend(coefficients) + if self._linear_constraint_matrix: + flat_matrix_items = [ + (con_id, var_id, coef) + for ((con_id, var_id), coef) in self._linear_constraint_matrix.items() + ] + lin_con_ids, var_ids, lin_con_coefs = zip(*sorted(flat_matrix_items)) + m.linear_constraint_matrix.row_ids.extend(lin_con_ids) + m.linear_constraint_matrix.column_ids.extend(var_ids) + m.linear_constraint_matrix.coefficients.extend(lin_con_coefs) + return m + + def add_update_tracker(self) -> model_storage.StorageUpdateTracker: + tracker = _UpdateTracker(self) + self._update_trackers.add(tracker) + return tracker + + def remove_update_tracker( + self, tracker: model_storage.StorageUpdateTracker + ) -> None: + self._update_trackers.remove(tracker) + tracker.retired = True + + def _check_variable_id(self, variable_id: int) -> None: + if variable_id not in self.variables: + raise model_storage.BadVariableIdError(variable_id) + + def _check_linear_constraint_id(self, linear_constraint_id: int) -> None: + if linear_constraint_id not in self.linear_constraints: + raise model_storage.BadLinearConstraintIdError(linear_constraint_id) def _set_sparse_double_vector( id_value_pairs: Iterable[Tuple[int, float]], proto: sparse_containers_pb2.SparseDoubleVectorProto, ) -> None: - """id_value_pairs must be sorted, proto is filled.""" - if not id_value_pairs: - return - ids, values = zip(*id_value_pairs) - proto.ids[:] = ids - proto.values[:] = values + """id_value_pairs must be sorted, proto is filled.""" + if not id_value_pairs: + return + ids, values = zip(*id_value_pairs) + proto.ids[:] = ids + proto.values[:] = values def _set_sparse_bool_vector( id_value_pairs: Iterable[Tuple[int, bool]], proto: sparse_containers_pb2.SparseBoolVectorProto, ) -> None: - """id_value_pairs must be sorted, proto is filled.""" - if not id_value_pairs: - return - ids, values = zip(*id_value_pairs) - proto.ids[:] = ids - proto.values[:] = values + """id_value_pairs must be sorted, proto is filled.""" + if not id_value_pairs: + return + ids, values = zip(*id_value_pairs) + proto.ids[:] = ids + proto.values[:] = values def _variables_to_proto( variables: Iterable[Tuple[int, _VariableStorage]], proto: model_pb2.VariablesProto, ) -> None: - """Exports variables to proto.""" - has_named_var = False - for _, var_storage in variables: - if var_storage.name: - has_named_var = True - break - for var_id, var_storage in variables: - proto.ids.append(var_id) - proto.lower_bounds.append(var_storage.lower_bound) - proto.upper_bounds.append(var_storage.upper_bound) - proto.integers.append(var_storage.is_integer) - if has_named_var: - proto.names.append(var_storage.name) + """Exports variables to proto.""" + has_named_var = False + for _, var_storage in variables: + if var_storage.name: + has_named_var = True + break + for var_id, var_storage in variables: + proto.ids.append(var_id) + proto.lower_bounds.append(var_storage.lower_bound) + proto.upper_bounds.append(var_storage.upper_bound) + proto.integers.append(var_storage.is_integer) + if has_named_var: + proto.names.append(var_storage.name) def _linear_constraints_to_proto( linear_constraints: Iterable[Tuple[int, _LinearConstraintStorage]], proto: model_pb2.LinearConstraintsProto, ) -> None: - """Exports variables to proto.""" - has_named_lin_con = False - for _, lin_con_storage in linear_constraints: - if lin_con_storage.name: - has_named_lin_con = True - break - for lin_con_id, lin_con_storage in linear_constraints: - proto.ids.append(lin_con_id) - proto.lower_bounds.append(lin_con_storage.lower_bound) - proto.upper_bounds.append(lin_con_storage.upper_bound) - if has_named_lin_con: - proto.names.append(lin_con_storage.name) + """Exports variables to proto.""" + has_named_lin_con = False + for _, lin_con_storage in linear_constraints: + if lin_con_storage.name: + has_named_lin_con = True + break + for lin_con_id, lin_con_storage in linear_constraints: + proto.ids.append(lin_con_id) + proto.lower_bounds.append(lin_con_storage.lower_bound) + proto.upper_bounds.append(lin_con_storage.upper_bound) + if has_named_lin_con: + proto.names.append(lin_con_storage.name) diff --git a/ortools/math_opt/python/hash_model_storage_test.py b/ortools/math_opt/python/hash_model_storage_test.py index 7c5feb37b3f..dfb416f463f 100644 --- a/ortools/math_opt/python/hash_model_storage_test.py +++ b/ortools/math_opt/python/hash_model_storage_test.py @@ -20,12 +20,12 @@ class HashModelStorageTest(absltest.TestCase): - def test_quadratic_term_storage(self): - storage = hash_model_storage._QuadraticTermStorage() - storage.set_coefficient(0, 1, 1.0) - storage.delete_variable(0) - self.assertEmpty(list(storage.get_adjacent_variables(0))) + def test_quadratic_term_storage(self): + storage = hash_model_storage._QuadraticTermStorage() + storage.set_coefficient(0, 1, 1.0) + storage.delete_variable(0) + self.assertEmpty(list(storage.get_adjacent_variables(0))) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/indicator_constraints.py b/ortools/math_opt/python/indicator_constraints.py index 3793bf545d2..67814cae3a9 100644 --- a/ortools/math_opt/python/indicator_constraints.py +++ b/ortools/math_opt/python/indicator_constraints.py @@ -22,125 +22,125 @@ class IndicatorConstraint(from_model.FromModel): - """An indicator constraint for an optimization model. - - An IndicatorConstraint adds the following restriction on feasible solutions to - an optimization model: - if z == 1 then lb <= sum_{i in I} a_i * x_i <= ub - where z is a binary decision variable (or its negation) and x_i are the - decision variables of the problem. Equality constraints lb == ub is allowed, - which models the constraint: - if z == 1 then sum_{i in I} a_i * x_i == b - Setting lb > ub will result in an InvalidArgument error at solve time. - - Indicator constraints have limited mutability. You can delete a variable - that the constraint uses, or you can delete the entire constraint. You - currently cannot update bounds or coefficients. This may change in future - versions. - - If the indicator variable is deleted or was None at creation time, the - constraint will lead to an invalid model at solve time, unless the constraint - is deleted before solving. - - The name is optional, read only, and used only for debugging. Non-empty names - should be distinct. - - Do not create an IndicatorConstraint directly, use - Model.add_indicator_constraint() instead. Two IndicatorConstraint objects can - represent the same constraint (for the same model). They will have the same - underlying IndicatorConstraint.elemental for storing the data. The - IndicatorConstraint class is simply a reference to an Elemental. - """ - - __slots__ = "_elemental", "_id" - - def __init__(self, elem: elemental.Elemental, cid: int) -> None: - """Internal only, prefer Model functions (add_indicator_constraint() and get_indicator_constraint()).""" - if not isinstance(cid, int): - raise TypeError(f"cid type should be int, was:{type(cid).__name__!r}") - self._elemental: elemental.Elemental = elem - self._id: int = cid - - @property - def lower_bound(self) -> float: - return self._elemental.get_attr( - enums.DoubleAttr1.INDICATOR_CONSTRAINT_LOWER_BOUND, (self._id,) - ) - - @property - def upper_bound(self) -> float: - return self._elemental.get_attr( - enums.DoubleAttr1.INDICATOR_CONSTRAINT_UPPER_BOUND, (self._id,) - ) - - @property - def activate_on_zero(self) -> bool: - return self._elemental.get_attr( - enums.BoolAttr1.INDICATOR_CONSTRAINT_ACTIVATE_ON_ZERO, (self._id,) - ) - - @property - def indicator_variable(self) -> Optional[variables.Variable]: - var_id = self._elemental.get_attr( - enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR, (self._id,) - ) - if var_id < 0: - return None - return variables.Variable(self._elemental, var_id) - - @property - def name(self) -> str: - return self._elemental.get_element_name( - enums.ElementType.INDICATOR_CONSTRAINT, self._id - ) - - @property - def id(self) -> int: - return self._id - - @property - def elemental(self) -> elemental.Elemental: - """Internal use only.""" - return self._elemental - - def get_coefficient(self, var: variables.Variable) -> float: - from_model.model_is_same(var, self) - return self._elemental.get_attr( - enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, - (self._id, var.id), - ) - - def terms(self) -> Iterator[variables.LinearTerm]: - """Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint.""" - keys = self._elemental.slice_attr( - enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, 0, self._id - ) - coefs = self._elemental.get_attrs( - enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, keys - ) - for i in range(len(keys)): - yield variables.LinearTerm( - variable=variables.Variable(self._elemental, int(keys[i, 1])), - coefficient=float(coefs[i]), - ) - - def get_implied_constraint(self) -> variables.BoundedLinearExpression: - """Returns the bounded expression from lower_bound, upper_bound and terms.""" - return variables.BoundedLinearExpression( - self.lower_bound, variables.LinearSum(self.terms()), self.upper_bound - ) - - def __str__(self): - """Returns the name, or a string containing the id if the name is empty.""" - return self.name if self.name else f"linear_constraint_{self.id}" - - def __repr__(self): - return f"" - - def __eq__(self, other: Any) -> bool: - if isinstance(other, IndicatorConstraint): - return self._id == other._id and self._elemental is other._elemental - return False - - def __hash__(self) -> int: - return hash(self._id) + """An indicator constraint for an optimization model. + + An IndicatorConstraint adds the following restriction on feasible solutions to + an optimization model: + if z == 1 then lb <= sum_{i in I} a_i * x_i <= ub + where z is a binary decision variable (or its negation) and x_i are the + decision variables of the problem. Equality constraints lb == ub is allowed, + which models the constraint: + if z == 1 then sum_{i in I} a_i * x_i == b + Setting lb > ub will result in an InvalidArgument error at solve time. + + Indicator constraints have limited mutability. You can delete a variable + that the constraint uses, or you can delete the entire constraint. You + currently cannot update bounds or coefficients. This may change in future + versions. + + If the indicator variable is deleted or was None at creation time, the + constraint will lead to an invalid model at solve time, unless the constraint + is deleted before solving. + + The name is optional, read only, and used only for debugging. Non-empty names + should be distinct. + + Do not create an IndicatorConstraint directly, use + Model.add_indicator_constraint() instead. Two IndicatorConstraint objects can + represent the same constraint (for the same model). They will have the same + underlying IndicatorConstraint.elemental for storing the data. The + IndicatorConstraint class is simply a reference to an Elemental. + """ + + __slots__ = "_elemental", "_id" + + def __init__(self, elem: elemental.Elemental, cid: int) -> None: + """Internal only, prefer Model functions (add_indicator_constraint() and get_indicator_constraint()).""" + if not isinstance(cid, int): + raise TypeError(f"cid type should be int, was:{type(cid).__name__!r}") + self._elemental: elemental.Elemental = elem + self._id: int = cid + + @property + def lower_bound(self) -> float: + return self._elemental.get_attr( + enums.DoubleAttr1.INDICATOR_CONSTRAINT_LOWER_BOUND, (self._id,) + ) + + @property + def upper_bound(self) -> float: + return self._elemental.get_attr( + enums.DoubleAttr1.INDICATOR_CONSTRAINT_UPPER_BOUND, (self._id,) + ) + + @property + def activate_on_zero(self) -> bool: + return self._elemental.get_attr( + enums.BoolAttr1.INDICATOR_CONSTRAINT_ACTIVATE_ON_ZERO, (self._id,) + ) + + @property + def indicator_variable(self) -> Optional[variables.Variable]: + var_id = self._elemental.get_attr( + enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR, (self._id,) + ) + if var_id < 0: + return None + return variables.Variable(self._elemental, var_id) + + @property + def name(self) -> str: + return self._elemental.get_element_name( + enums.ElementType.INDICATOR_CONSTRAINT, self._id + ) + + @property + def id(self) -> int: + return self._id + + @property + def elemental(self) -> elemental.Elemental: + """Internal use only.""" + return self._elemental + + def get_coefficient(self, var: variables.Variable) -> float: + from_model.model_is_same(var, self) + return self._elemental.get_attr( + enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, + (self._id, var.id), + ) + + def terms(self) -> Iterator[variables.LinearTerm]: + """Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint.""" + keys = self._elemental.slice_attr( + enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, 0, self._id + ) + coefs = self._elemental.get_attrs( + enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, keys + ) + for i in range(len(keys)): + yield variables.LinearTerm( + variable=variables.Variable(self._elemental, int(keys[i, 1])), + coefficient=float(coefs[i]), + ) + + def get_implied_constraint(self) -> variables.BoundedLinearExpression: + """Returns the bounded expression from lower_bound, upper_bound and terms.""" + return variables.BoundedLinearExpression( + self.lower_bound, variables.LinearSum(self.terms()), self.upper_bound + ) + + def __str__(self): + """Returns the name, or a string containing the id if the name is empty.""" + return self.name if self.name else f"linear_constraint_{self.id}" + + def __repr__(self): + return f"" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, IndicatorConstraint): + return self._id == other._id and self._elemental is other._elemental + return False + + def __hash__(self) -> int: + return hash(self._id) diff --git a/ortools/math_opt/python/indicator_constraints_test.py b/ortools/math_opt/python/indicator_constraints_test.py index 70f1fcdb03d..98f126a1d0f 100644 --- a/ortools/math_opt/python/indicator_constraints_test.py +++ b/ortools/math_opt/python/indicator_constraints_test.py @@ -24,102 +24,102 @@ def _terms_dict( con: indicator_constraints.IndicatorConstraint, ) -> Dict[variables.Variable, float]: - return {term.variable: term.coefficient for term in con.terms()} + return {term.variable: term.coefficient for term in con.terms()} class IndicatorConstraintsTest(absltest.TestCase): - def test_getters_empty(self) -> None: - mod = model.Model() - con = mod.add_indicator_constraint() - self.assertIsNone(con.indicator_variable) - self.assertEmpty(list(con.terms())) - self.assertEqual(con.lower_bound, -math.inf) - self.assertEqual(con.upper_bound, math.inf) - self.assertFalse(con.activate_on_zero) - self.assertEqual(con.name, "") - bounded_expr = con.get_implied_constraint() - self.assertEqual(bounded_expr.lower_bound, -math.inf) - self.assertEqual(bounded_expr.upper_bound, math.inf) - expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(expr.offset, 0.0) - self.assertEmpty(expr.terms) - - def test_getters_nonempty(self) -> None: - mod = model.Model() - x = mod.add_binary_variable() - y = mod.add_variable() - z = mod.add_variable() - con = mod.add_indicator_constraint( - indicator=x, - activate_on_zero=True, - implied_constraint=2 * y + z == 3.0, - name="c123", - ) - self.assertEqual(con.indicator_variable, x) - self.assertDictEqual(_terms_dict(con), {y: 2.0, z: 1.0}) - self.assertEqual(con.lower_bound, 3.0) - self.assertEqual(con.upper_bound, 3.0) - self.assertTrue(con.activate_on_zero) - self.assertEqual(con.name, "c123") - self.assertEqual(con.get_coefficient(y), 2.0) - self.assertEqual(con.get_coefficient(x), 0.0) - - bounded_expr = con.get_implied_constraint() - self.assertEqual(bounded_expr.lower_bound, 3.0) - self.assertEqual(bounded_expr.upper_bound, 3.0) - expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(expr.offset, 0.0) - self.assertEqual(expr.terms, {y: 2.0, z: 1.0}) - - def test_create_by_attrs(self) -> None: - mod = model.Model() - x = mod.add_binary_variable() - y = mod.add_variable() - z = mod.add_variable() - con = mod.add_indicator_constraint( - indicator=x, - activate_on_zero=True, - implied_lb=4.0, - implied_ub=5.0, - implied_expr=10 * y + 9 * z + 3.0, - name="c123", - ) - self.assertEqual(con.indicator_variable, x) - self.assertDictEqual(_terms_dict(con), {y: 10.0, z: 9.0}) - self.assertEqual(con.lower_bound, 1.0) - self.assertEqual(con.upper_bound, 2.0) - self.assertTrue(con.activate_on_zero) - self.assertEqual(con.name, "c123") - - def test_get_coefficient_wrong_model(self) -> None: - mod = model.Model() - x = mod.add_variable() - mod2 = model.Model() - con = mod2.add_indicator_constraint() - with self.assertRaises(ValueError): - con.get_coefficient(x) - - def test_eq(self) -> None: - mod_a = model.Model() - con_a1 = mod_a.add_indicator_constraint() - con_a2 = mod_a.add_indicator_constraint() - con_a1_alt = mod_a.get_indicator_constraint(0) - - mod_b = model.Model() - con_b1 = mod_b.add_indicator_constraint() - - self.assertEqual(con_a1, con_a1) - self.assertEqual(con_a1, con_a1_alt) - self.assertNotEqual(con_a1, con_a2) - self.assertNotEqual(con_a1, con_b1) - self.assertNotEqual(con_a1, "cat") - - def test_hash_no_crash(self) -> None: - mod_a = model.Model() - con = mod_a.add_indicator_constraint() - self.assertIsInstance(hash(con), int) + def test_getters_empty(self) -> None: + mod = model.Model() + con = mod.add_indicator_constraint() + self.assertIsNone(con.indicator_variable) + self.assertEmpty(list(con.terms())) + self.assertEqual(con.lower_bound, -math.inf) + self.assertEqual(con.upper_bound, math.inf) + self.assertFalse(con.activate_on_zero) + self.assertEqual(con.name, "") + bounded_expr = con.get_implied_constraint() + self.assertEqual(bounded_expr.lower_bound, -math.inf) + self.assertEqual(bounded_expr.upper_bound, math.inf) + expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(expr.offset, 0.0) + self.assertEmpty(expr.terms) + + def test_getters_nonempty(self) -> None: + mod = model.Model() + x = mod.add_binary_variable() + y = mod.add_variable() + z = mod.add_variable() + con = mod.add_indicator_constraint( + indicator=x, + activate_on_zero=True, + implied_constraint=2 * y + z == 3.0, + name="c123", + ) + self.assertEqual(con.indicator_variable, x) + self.assertDictEqual(_terms_dict(con), {y: 2.0, z: 1.0}) + self.assertEqual(con.lower_bound, 3.0) + self.assertEqual(con.upper_bound, 3.0) + self.assertTrue(con.activate_on_zero) + self.assertEqual(con.name, "c123") + self.assertEqual(con.get_coefficient(y), 2.0) + self.assertEqual(con.get_coefficient(x), 0.0) + + bounded_expr = con.get_implied_constraint() + self.assertEqual(bounded_expr.lower_bound, 3.0) + self.assertEqual(bounded_expr.upper_bound, 3.0) + expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(expr.offset, 0.0) + self.assertEqual(expr.terms, {y: 2.0, z: 1.0}) + + def test_create_by_attrs(self) -> None: + mod = model.Model() + x = mod.add_binary_variable() + y = mod.add_variable() + z = mod.add_variable() + con = mod.add_indicator_constraint( + indicator=x, + activate_on_zero=True, + implied_lb=4.0, + implied_ub=5.0, + implied_expr=10 * y + 9 * z + 3.0, + name="c123", + ) + self.assertEqual(con.indicator_variable, x) + self.assertDictEqual(_terms_dict(con), {y: 10.0, z: 9.0}) + self.assertEqual(con.lower_bound, 1.0) + self.assertEqual(con.upper_bound, 2.0) + self.assertTrue(con.activate_on_zero) + self.assertEqual(con.name, "c123") + + def test_get_coefficient_wrong_model(self) -> None: + mod = model.Model() + x = mod.add_variable() + mod2 = model.Model() + con = mod2.add_indicator_constraint() + with self.assertRaises(ValueError): + con.get_coefficient(x) + + def test_eq(self) -> None: + mod_a = model.Model() + con_a1 = mod_a.add_indicator_constraint() + con_a2 = mod_a.add_indicator_constraint() + con_a1_alt = mod_a.get_indicator_constraint(0) + + mod_b = model.Model() + con_b1 = mod_b.add_indicator_constraint() + + self.assertEqual(con_a1, con_a1) + self.assertEqual(con_a1, con_a1_alt) + self.assertNotEqual(con_a1, con_a2) + self.assertNotEqual(con_a1, con_b1) + self.assertNotEqual(con_a1, "cat") + + def test_hash_no_crash(self) -> None: + mod_a = model.Model() + con = mod_a.add_indicator_constraint() + self.assertIsInstance(hash(con), int) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/init_arguments.py b/ortools/math_opt/python/init_arguments.py index 2d5903b7f53..79611ecedfe 100644 --- a/ortools/math_opt/python/init_arguments.py +++ b/ortools/math_opt/python/init_arguments.py @@ -22,159 +22,159 @@ @dataclasses.dataclass class StreamableGScipInitArguments: - """Streamable GScip specific parameters for solver instantiation.""" + """Streamable GScip specific parameters for solver instantiation.""" @dataclasses.dataclass(frozen=True) class GurobiISVKey: - """The Gurobi ISV key, an alternative to license files. - - Contact Gurobi for details. - - Attributes: - name: A string, typically a company/organization. - application_name: A string, typically a project. - expiration: An int, a value of 0 indicates no expiration. - key: A string, the secret. - """ - - name: str = "" - application_name: str = "" - expiration: int = 0 - key: str = "" - - def to_proto(self) -> gurobi_pb2.GurobiInitializerProto.ISVKey: - """Returns a protocol buffer equivalent of this.""" - return gurobi_pb2.GurobiInitializerProto.ISVKey( - name=self.name, - application_name=self.application_name, - expiration=self.expiration, - key=self.key, - ) + """The Gurobi ISV key, an alternative to license files. + + Contact Gurobi for details. + + Attributes: + name: A string, typically a company/organization. + application_name: A string, typically a project. + expiration: An int, a value of 0 indicates no expiration. + key: A string, the secret. + """ + + name: str = "" + application_name: str = "" + expiration: int = 0 + key: str = "" + + def to_proto(self) -> gurobi_pb2.GurobiInitializerProto.ISVKey: + """Returns a protocol buffer equivalent of this.""" + return gurobi_pb2.GurobiInitializerProto.ISVKey( + name=self.name, + application_name=self.application_name, + expiration=self.expiration, + key=self.key, + ) def gurobi_isv_key_from_proto( proto: gurobi_pb2.GurobiInitializerProto.ISVKey, ) -> GurobiISVKey: - """Returns an equivalent GurobiISVKey to the input proto.""" - return GurobiISVKey( - name=proto.name, - application_name=proto.application_name, - expiration=proto.expiration, - key=proto.key, - ) + """Returns an equivalent GurobiISVKey to the input proto.""" + return GurobiISVKey( + name=proto.name, + application_name=proto.application_name, + expiration=proto.expiration, + key=proto.key, + ) @dataclasses.dataclass class StreamableGurobiInitArguments: - """Streamable Gurobi specific parameters for solver instantiation.""" + """Streamable Gurobi specific parameters for solver instantiation.""" - isv_key: Optional[GurobiISVKey] = None + isv_key: Optional[GurobiISVKey] = None - def to_proto(self) -> gurobi_pb2.GurobiInitializerProto: - """Returns a protocol buffer equivalent of this.""" - return gurobi_pb2.GurobiInitializerProto( - isv_key=self.isv_key.to_proto() if self.isv_key else None - ) + def to_proto(self) -> gurobi_pb2.GurobiInitializerProto: + """Returns a protocol buffer equivalent of this.""" + return gurobi_pb2.GurobiInitializerProto( + isv_key=self.isv_key.to_proto() if self.isv_key else None + ) def streamable_gurobi_init_arguments_from_proto( proto: gurobi_pb2.GurobiInitializerProto, ) -> StreamableGurobiInitArguments: - """Returns an equivalent StreamableGurobiInitArguments to the input proto.""" - result = StreamableGurobiInitArguments() - if proto.HasField("isv_key"): - result.isv_key = gurobi_isv_key_from_proto(proto.isv_key) - return result + """Returns an equivalent StreamableGurobiInitArguments to the input proto.""" + result = StreamableGurobiInitArguments() + if proto.HasField("isv_key"): + result.isv_key = gurobi_isv_key_from_proto(proto.isv_key) + return result @dataclasses.dataclass class StreamableGlopInitArguments: - """Streamable Glop specific parameters for solver instantiation.""" + """Streamable Glop specific parameters for solver instantiation.""" @dataclasses.dataclass class StreamableCpSatInitArguments: - """Streamable CP-SAT specific parameters for solver instantiation.""" + """Streamable CP-SAT specific parameters for solver instantiation.""" @dataclasses.dataclass class StreamablePdlpInitArguments: - """Streamable Pdlp specific parameters for solver instantiation.""" + """Streamable Pdlp specific parameters for solver instantiation.""" @dataclasses.dataclass class StreamableGlpkInitArguments: - """Streamable GLPK specific parameters for solver instantiation.""" + """Streamable GLPK specific parameters for solver instantiation.""" @dataclasses.dataclass class StreamableOsqpInitArguments: - """Streamable OSQP specific parameters for solver instantiation.""" + """Streamable OSQP specific parameters for solver instantiation.""" @dataclasses.dataclass class StreamableEcosInitArguments: - """Streamable Ecos specific parameters for solver instantiation.""" + """Streamable Ecos specific parameters for solver instantiation.""" @dataclasses.dataclass class StreamableScsInitArguments: - """Streamable Scs specific parameters for solver instantiation.""" + """Streamable Scs specific parameters for solver instantiation.""" @dataclasses.dataclass class StreamableHighsInitArguments: - """Streamable Highs specific parameters for solver instantiation.""" + """Streamable Highs specific parameters for solver instantiation.""" @dataclasses.dataclass class StreamableSantoriniInitArguments: - """Streamable Santorini specific parameters for solver instantiation.""" + """Streamable Santorini specific parameters for solver instantiation.""" @dataclasses.dataclass class StreamableSolverInitArguments: - """Solver initialization parameters that can be sent to another process. - - Attributes: - gscip: Initialization parameters specific to GScip. - gurobi: Initialization parameters specific to Gurobi. - glop: Initialization parameters specific to GLOP. - cp_sat: Initialization parameters specific to CP-SAT. - pdlp: Initialization parameters specific to PDLP. - glpk: Initialization parameters specific to GLPK. - osqp: Initialization parameters specific to OSQP. - ecos: Initialization parameters specific to ECOS. - scs: Initialization parameters specific to SCS. - highs: Initialization parameters specific to HiGHS. - santorini: Initialization parameters specific to Santorini. - """ - - gscip: Optional[StreamableGScipInitArguments] = None - gurobi: Optional[StreamableGurobiInitArguments] = None - glop: Optional[StreamableGlopInitArguments] = None - cp_sat: Optional[StreamableCpSatInitArguments] = None - pdlp: Optional[StreamablePdlpInitArguments] = None - glpk: Optional[StreamableGlpkInitArguments] = None - osqp: Optional[StreamableOsqpInitArguments] = None - ecos: Optional[StreamableEcosInitArguments] = None - scs: Optional[StreamableScsInitArguments] = None - highs: Optional[StreamableHighsInitArguments] = None - santorini: Optional[StreamableSantoriniInitArguments] = None - - def to_proto(self) -> parameters_pb2.SolverInitializerProto: - """Returns a protocol buffer equivalent of this.""" - return parameters_pb2.SolverInitializerProto( - gurobi=self.gurobi.to_proto() if self.gurobi else None - ) + """Solver initialization parameters that can be sent to another process. + + Attributes: + gscip: Initialization parameters specific to GScip. + gurobi: Initialization parameters specific to Gurobi. + glop: Initialization parameters specific to GLOP. + cp_sat: Initialization parameters specific to CP-SAT. + pdlp: Initialization parameters specific to PDLP. + glpk: Initialization parameters specific to GLPK. + osqp: Initialization parameters specific to OSQP. + ecos: Initialization parameters specific to ECOS. + scs: Initialization parameters specific to SCS. + highs: Initialization parameters specific to HiGHS. + santorini: Initialization parameters specific to Santorini. + """ + + gscip: Optional[StreamableGScipInitArguments] = None + gurobi: Optional[StreamableGurobiInitArguments] = None + glop: Optional[StreamableGlopInitArguments] = None + cp_sat: Optional[StreamableCpSatInitArguments] = None + pdlp: Optional[StreamablePdlpInitArguments] = None + glpk: Optional[StreamableGlpkInitArguments] = None + osqp: Optional[StreamableOsqpInitArguments] = None + ecos: Optional[StreamableEcosInitArguments] = None + scs: Optional[StreamableScsInitArguments] = None + highs: Optional[StreamableHighsInitArguments] = None + santorini: Optional[StreamableSantoriniInitArguments] = None + + def to_proto(self) -> parameters_pb2.SolverInitializerProto: + """Returns a protocol buffer equivalent of this.""" + return parameters_pb2.SolverInitializerProto( + gurobi=self.gurobi.to_proto() if self.gurobi else None + ) def streamable_solver_init_arguments_from_proto( proto: parameters_pb2.SolverInitializerProto, ) -> StreamableSolverInitArguments: - """Returns an equivalent StreamableSolverInitArguments to the input proto.""" - result = StreamableSolverInitArguments() - if proto.HasField("gurobi"): - result.gurobi = streamable_gurobi_init_arguments_from_proto(proto.gurobi) - return result + """Returns an equivalent StreamableSolverInitArguments to the input proto.""" + result = StreamableSolverInitArguments() + if proto.HasField("gurobi"): + result.gurobi = streamable_gurobi_init_arguments_from_proto(proto.gurobi) + return result diff --git a/ortools/math_opt/python/init_arguments_test.py b/ortools/math_opt/python/init_arguments_test.py index badebd205ba..739cd92825d 100644 --- a/ortools/math_opt/python/init_arguments_test.py +++ b/ortools/math_opt/python/init_arguments_test.py @@ -21,82 +21,82 @@ class GurobiISVKeyTest(absltest.TestCase, compare_proto.MathOptProtoAssertions): - def test_proto_conversions(self) -> None: - isv = init_arguments.GurobiISVKey( - name="cat", application_name="hat", expiration=4, key="bat" - ) - proto_isv = gurobi_pb2.GurobiInitializerProto.ISVKey( - name="cat", application_name="hat", expiration=4, key="bat" - ) - self.assert_protos_equiv(isv.to_proto(), proto_isv) - self.assertEqual(init_arguments.gurobi_isv_key_from_proto(proto_isv), isv) + def test_proto_conversions(self) -> None: + isv = init_arguments.GurobiISVKey( + name="cat", application_name="hat", expiration=4, key="bat" + ) + proto_isv = gurobi_pb2.GurobiInitializerProto.ISVKey( + name="cat", application_name="hat", expiration=4, key="bat" + ) + self.assert_protos_equiv(isv.to_proto(), proto_isv) + self.assertEqual(init_arguments.gurobi_isv_key_from_proto(proto_isv), isv) class StreamableGurobiInitArgumentsTest( absltest.TestCase, compare_proto.MathOptProtoAssertions ): - def test_proto_conversions_isv_key_set(self) -> None: - init = init_arguments.StreamableGurobiInitArguments( - isv_key=init_arguments.GurobiISVKey( - name="cat", application_name="hat", expiration=4, key="bat" - ) - ) - proto_init = gurobi_pb2.GurobiInitializerProto( - isv_key=gurobi_pb2.GurobiInitializerProto.ISVKey( - name="cat", application_name="hat", expiration=4, key="bat" - ) + def test_proto_conversions_isv_key_set(self) -> None: + init = init_arguments.StreamableGurobiInitArguments( + isv_key=init_arguments.GurobiISVKey( + name="cat", application_name="hat", expiration=4, key="bat" ) - self.assert_protos_equiv(init.to_proto(), proto_init) - self.assertEqual( - init_arguments.streamable_gurobi_init_arguments_from_proto(proto_init), - init, + ) + proto_init = gurobi_pb2.GurobiInitializerProto( + isv_key=gurobi_pb2.GurobiInitializerProto.ISVKey( + name="cat", application_name="hat", expiration=4, key="bat" ) + ) + self.assert_protos_equiv(init.to_proto(), proto_init) + self.assertEqual( + init_arguments.streamable_gurobi_init_arguments_from_proto(proto_init), + init, + ) - def test_proto_conversions_isv_key_not_set(self) -> None: - init = init_arguments.StreamableGurobiInitArguments() - proto_init = gurobi_pb2.GurobiInitializerProto() - self.assert_protos_equiv(init.to_proto(), proto_init) - self.assertEqual( - init_arguments.streamable_gurobi_init_arguments_from_proto(proto_init), - init, - ) + def test_proto_conversions_isv_key_not_set(self) -> None: + init = init_arguments.StreamableGurobiInitArguments() + proto_init = gurobi_pb2.GurobiInitializerProto() + self.assert_protos_equiv(init.to_proto(), proto_init) + self.assertEqual( + init_arguments.streamable_gurobi_init_arguments_from_proto(proto_init), + init, + ) class StreamableSolverInitArgumentsTest( absltest.TestCase, compare_proto.MathOptProtoAssertions ): - def test_proto_conversions_gurobi_set(self) -> None: - init = init_arguments.StreamableSolverInitArguments( - gurobi=init_arguments.StreamableGurobiInitArguments( - isv_key=init_arguments.GurobiISVKey( - name="cat", application_name="hat", expiration=4, key="bat" - ) + def test_proto_conversions_gurobi_set(self) -> None: + init = init_arguments.StreamableSolverInitArguments( + gurobi=init_arguments.StreamableGurobiInitArguments( + isv_key=init_arguments.GurobiISVKey( + name="cat", application_name="hat", expiration=4, key="bat" ) ) - proto_init = parameters_pb2.SolverInitializerProto( - gurobi=gurobi_pb2.GurobiInitializerProto( - isv_key=gurobi_pb2.GurobiInitializerProto.ISVKey( - name="cat", application_name="hat", expiration=4, key="bat" - ) + ) + proto_init = parameters_pb2.SolverInitializerProto( + gurobi=gurobi_pb2.GurobiInitializerProto( + isv_key=gurobi_pb2.GurobiInitializerProto.ISVKey( + name="cat", application_name="hat", expiration=4, key="bat" ) ) - self.assert_protos_equiv(init.to_proto(), proto_init) - self.assertEqual( - init_arguments.streamable_solver_init_arguments_from_proto(proto_init), - init, - ) + ) + self.assert_protos_equiv(init.to_proto(), proto_init) + self.assertEqual( + init_arguments.streamable_solver_init_arguments_from_proto(proto_init), + init, + ) - def test_proto_conversions_gurobi_not_set(self) -> None: - init = init_arguments.StreamableSolverInitArguments() - proto_init = parameters_pb2.SolverInitializerProto() - self.assert_protos_equiv(init.to_proto(), proto_init) - self.assertEqual( - init_arguments.streamable_solver_init_arguments_from_proto(proto_init), - init, - ) + def test_proto_conversions_gurobi_not_set(self) -> None: + init = init_arguments.StreamableSolverInitArguments() + proto_init = parameters_pb2.SolverInitializerProto() + self.assert_protos_equiv(init.to_proto(), proto_init) + self.assertEqual( + init_arguments.streamable_solver_init_arguments_from_proto(proto_init), + init, + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/ipc/proto_converter.py b/ortools/math_opt/python/ipc/proto_converter.py index 59e223efbc3..294e3428987 100644 --- a/ortools/math_opt/python/ipc/proto_converter.py +++ b/ortools/math_opt/python/ipc/proto_converter.py @@ -37,49 +37,49 @@ def convert_request( request: rpc_pb2.SolveRequest, ) -> optimization_pb2.SolveMathOptModelRequest: - """Converts a `SolveRequest` to a `SolveMathOptModelRequest`. + """Converts a `SolveRequest` to a `SolveMathOptModelRequest`. - Args: - request: A `SolveRequest` request built from a MathOpt model. + Args: + request: A `SolveRequest` request built from a MathOpt model. - Returns: - A `SolveMathOptModelRequest` for the Operations Research API. + Returns: + A `SolveMathOptModelRequest` for the Operations Research API. - Raises: - ValueError: If a field that is not supported in the expernal proto is - present in the request or if the request can't be parsed to a - `SolveMathOptModelRequest`. - """ - normalize.math_opt_normalize_proto(request) - if request.HasField("initializer"): - raise ValueError(str("initializer is not supported")) - for param in _UNSUPPORTED_SOLVER_SPECIFIC_PARAMETERS: - if request.parameters.HasField(param): - raise ValueError(f"SolveParameters.{param} not supported") + Raises: + ValueError: If a field that is not supported in the expernal proto is + present in the request or if the request can't be parsed to a + `SolveMathOptModelRequest`. + """ + normalize.math_opt_normalize_proto(request) + if request.HasField("initializer"): + raise ValueError(str("initializer is not supported")) + for param in _UNSUPPORTED_SOLVER_SPECIFIC_PARAMETERS: + if request.parameters.HasField(param): + raise ValueError(f"SolveParameters.{param} not supported") - try: - external_request = optimization_pb2.SolveMathOptModelRequest.FromString( - request.SerializeToString() - ) - return external_request - except (message.DecodeError, message.EncodeError): - raise ValueError("request can not be parsed") from None + try: + external_request = optimization_pb2.SolveMathOptModelRequest.FromString( + request.SerializeToString() + ) + return external_request + except (message.DecodeError, message.EncodeError): + raise ValueError("request can not be parsed") from None def convert_response( api_response: optimization_pb2.SolveMathOptModelResponse, ) -> rpc_pb2.SolveResponse: - """Converts a `SolveMathOptModelResponse` to a `SolveResponse`. + """Converts a `SolveMathOptModelResponse` to a `SolveResponse`. - Args: - api_response: A `SolveMathOptModelResponse` response built from a MathOpt - model. + Args: + api_response: A `SolveMathOptModelResponse` response built from a MathOpt + model. - Returns: - A `SolveResponse` response built from a MathOpt model. - """ - api_response.DiscardUnknownFields() - normalize.math_opt_normalize_proto(api_response) - response = rpc_pb2.SolveResponse.FromString(api_response.SerializeToString()) - response.DiscardUnknownFields() - return response + Returns: + A `SolveResponse` response built from a MathOpt model. + """ + api_response.DiscardUnknownFields() + normalize.math_opt_normalize_proto(api_response) + response = rpc_pb2.SolveResponse.FromString(api_response.SerializeToString()) + response.DiscardUnknownFields() + return response diff --git a/ortools/math_opt/python/ipc/proto_converter_test.py b/ortools/math_opt/python/ipc/proto_converter_test.py index 98350c8d2e6..3dc5020d8f0 100644 --- a/ortools/math_opt/python/ipc/proto_converter_test.py +++ b/ortools/math_opt/python/ipc/proto_converter_test.py @@ -32,113 +32,117 @@ def _simple_request() -> rpc_pb2.SolveRequest: - mod = mathopt.Model(name="test_mod") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - mod.maximize(x - y) - resources = mathopt.SolverResources(cpu=2.0, ram=1024 * 1024 * 1024) - params = mathopt.SolveParameters(threads=2) - - request = rpc_pb2.SolveRequest( - solver_type=parameters_pb2.SOLVER_TYPE_GSCIP, - model=mod.export_model(), - resources=resources.to_proto(), - parameters=params.to_proto(), - ) - return request - - -class ProtoConverterTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - - def test_convert_request(self): - request = _simple_request() - expected = optimization_pb2.SolveMathOptModelRequest( - solver_type=api_parameters_pb2.SOLVER_TYPE_GSCIP, - model=api_model_pb2.ModelProto( - name="test_mod", - variables=api_model_pb2.VariablesProto( + mod = mathopt.Model(name="test_mod") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + mod.maximize(x - y) + resources = mathopt.SolverResources(cpu=2.0, ram=1024 * 1024 * 1024) + params = mathopt.SolveParameters(threads=2) + + request = rpc_pb2.SolveRequest( + solver_type=parameters_pb2.SOLVER_TYPE_GSCIP, + model=mod.export_model(), + resources=resources.to_proto(), + parameters=params.to_proto(), + ) + return request + + +class ProtoConverterTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_convert_request(self): + request = _simple_request() + expected = optimization_pb2.SolveMathOptModelRequest( + solver_type=api_parameters_pb2.SOLVER_TYPE_GSCIP, + model=api_model_pb2.ModelProto( + name="test_mod", + variables=api_model_pb2.VariablesProto( + ids=[0, 1], + lower_bounds=[0, 0], + upper_bounds=[1, 1], + integers=[True, True], + names=["x", "y"], + ), + objective=api_model_pb2.ObjectiveProto( + maximize=True, + linear_coefficients=api_sparse_containers_pb2.SparseDoubleVectorProto( ids=[0, 1], - lower_bounds=[0, 0], - upper_bounds=[1, 1], - integers=[True, True], - names=["x", "y"], - ), - objective=api_model_pb2.ObjectiveProto( - maximize=True, - linear_coefficients=api_sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], - values=[1.0, -1.0], - ), + values=[1.0, -1.0], ), ), - resources=api_solver_resources_pb2.SolverResourcesProto( - cpu=2.0, ram=1024 * 1024 * 1024 - ), - parameters=api_parameters_pb2.SolveParametersProto(threads=2), - ) + ), + resources=api_solver_resources_pb2.SolverResourcesProto( + cpu=2.0, ram=1024 * 1024 * 1024 + ), + parameters=api_parameters_pb2.SolveParametersProto(threads=2), + ) - self.assert_protos_equal(proto_converter.convert_request(request), expected) + self.assert_protos_equal(proto_converter.convert_request(request), expected) - def test_initializer_is_not_supported(self): - request = _simple_request() - request.initializer.gurobi.isv_key.name = "test_name_key" + def test_initializer_is_not_supported(self): + request = _simple_request() + request.initializer.gurobi.isv_key.name = "test_name_key" - with self.assertRaisesRegex(ValueError, "initializer is not supported"): - proto_converter.convert_request(request) + with self.assertRaisesRegex(ValueError, "initializer is not supported"): + proto_converter.convert_request(request) - def test_solver_parameters_are_not_supported(self): - request = _simple_request() - request.parameters.osqp.rho = 1.0 + def test_solver_parameters_are_not_supported(self): + request = _simple_request() + request.parameters.osqp.rho = 1.0 - with self.assertRaisesRegex(ValueError, "SolveParameters.osqp not supported"): - proto_converter.convert_request(request) + with self.assertRaisesRegex( + ValueError, "SolveParameters.osqp not supported" + ): + proto_converter.convert_request(request) - def test_convert_response(self): - api_response = optimization_pb2.SolveMathOptModelResponse() - result = api_response.result - result.termination.reason = api_result_pb2.TERMINATION_REASON_OPTIMAL - result.termination.problem_status.primal_status = ( - api_result_pb2.FEASIBILITY_STATUS_FEASIBLE - ) - result.termination.problem_status.dual_status = ( - api_result_pb2.FEASIBILITY_STATUS_FEASIBLE - ) - result.termination.objective_bounds.primal_bound = 1.0 - result.termination.objective_bounds.dual_bound = 1.0 - result.solutions.append( - api_solution_pb2.SolutionProto( - primal_solution=api_solution_pb2.PrimalSolutionProto( - variable_values=api_sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], values=[1.0, 0.0] - ) + def test_convert_response(self): + api_response = optimization_pb2.SolveMathOptModelResponse() + result = api_response.result + result.termination.reason = api_result_pb2.TERMINATION_REASON_OPTIMAL + result.termination.problem_status.primal_status = ( + api_result_pb2.FEASIBILITY_STATUS_FEASIBLE + ) + result.termination.problem_status.dual_status = ( + api_result_pb2.FEASIBILITY_STATUS_FEASIBLE + ) + result.termination.objective_bounds.primal_bound = 1.0 + result.termination.objective_bounds.dual_bound = 1.0 + result.solutions.append( + api_solution_pb2.SolutionProto( + primal_solution=api_solution_pb2.PrimalSolutionProto( + variable_values=api_sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1], values=[1.0, 0.0] ) ) ) - expected_response = rpc_pb2.SolveResponse() - expected_result = expected_response.result - expected_result.termination.reason = result_pb2.TERMINATION_REASON_OPTIMAL - expected_result.termination.problem_status.primal_status = ( - result_pb2.FEASIBILITY_STATUS_FEASIBLE - ) - expected_result.termination.problem_status.dual_status = ( - result_pb2.FEASIBILITY_STATUS_FEASIBLE - ) - expected_result.termination.objective_bounds.primal_bound = 1.0 - expected_result.termination.objective_bounds.dual_bound = 1.0 - expected_result.solutions.append( - solution_pb2.SolutionProto( - primal_solution=solution_pb2.PrimalSolutionProto( - variable_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], values=[1.0, 0.0] - ) + ) + expected_response = rpc_pb2.SolveResponse() + expected_result = expected_response.result + expected_result.termination.reason = result_pb2.TERMINATION_REASON_OPTIMAL + expected_result.termination.problem_status.primal_status = ( + result_pb2.FEASIBILITY_STATUS_FEASIBLE + ) + expected_result.termination.problem_status.dual_status = ( + result_pb2.FEASIBILITY_STATUS_FEASIBLE + ) + expected_result.termination.objective_bounds.primal_bound = 1.0 + expected_result.termination.objective_bounds.dual_bound = 1.0 + expected_result.solutions.append( + solution_pb2.SolutionProto( + primal_solution=solution_pb2.PrimalSolutionProto( + variable_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1], values=[1.0, 0.0] ) ) ) + ) - self.assert_protos_equal( - proto_converter.convert_response(api_response), expected_response - ) + self.assert_protos_equal( + proto_converter.convert_response(api_response), expected_response + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/ipc/remote_http_solve.py b/ortools/math_opt/python/ipc/remote_http_solve.py index 1c1e1e06f20..c71cb9dd7f8 100644 --- a/ortools/math_opt/python/ipc/remote_http_solve.py +++ b/ortools/math_opt/python/ipc/remote_http_solve.py @@ -23,12 +23,14 @@ from ortools.math_opt.python.ipc import proto_converter _DEFAULT_DEADLINE_SEC = 10 -_DEFAULT_ENDPOINT = "https://optimization.googleapis.com/v1/mathopt:solveMathOptModel" +_DEFAULT_ENDPOINT = ( + "https://optimization.googleapis.com/v1/mathopt:solveMathOptModel" +) _RELATIVE_TIME_BUFFER = 0.05 class OptimizationServiceError(Exception): - """Error produced when solving a MathOpt model via HTTP request.""" + """Error produced when solving a MathOpt model via HTTP request.""" def remote_http_solve( @@ -41,75 +43,77 @@ def remote_http_solve( deadline_sec: Optional[float] = _DEFAULT_DEADLINE_SEC, resources: Optional[mathopt.SolverResources] = None, ) -> Tuple[mathopt.SolveResult, List[str]]: - """Solves a MathOpt model via HTTP request to the OR API. - - Args: - model: The optimization model. - solver_type: The underlying solver to use. - params: Optional configuration of the underlying solver. - model_params: Optional configuration of the solver that is model specific. - endpoint: An URI identifying the service for remote solves. - api_key: Key to the OR API. - deadline_sec: The number of seconds before the request times out. - resources: Hints on resources requested for the solve. - - Returns: - A SolveResult containing the termination reason, solution(s) and stats. - A list of messages with the logs (if specified in the `params`). - - Raises: - OptimizationServiceError: if an HTTP error is returned while solving a - model. - """ - if api_key is None: - # TODO(b/306709279): Relax this when unauthenticated solves are allowed. - raise ValueError("api_key can't be None when solving remotely") - - payload = _build_json_payload(model, solver_type, params, model_params, resources) - - session = create_optimization_service_session(api_key, deadline_sec) - response = session.post( - url=endpoint, - json=payload, - timeout=deadline_sec, - ) - - if not response.ok: - http_error = json.loads(response.content)["error"] - raise OptimizationServiceError( - f'status code {http_error["code"]}: {http_error["message"]}' - ) from None - - return _build_solve_result(response.content, model) + """Solves a MathOpt model via HTTP request to the OR API. + + Args: + model: The optimization model. + solver_type: The underlying solver to use. + params: Optional configuration of the underlying solver. + model_params: Optional configuration of the solver that is model specific. + endpoint: An URI identifying the service for remote solves. + api_key: Key to the OR API. + deadline_sec: The number of seconds before the request times out. + resources: Hints on resources requested for the solve. + + Returns: + A SolveResult containing the termination reason, solution(s) and stats. + A list of messages with the logs (if specified in the `params`). + + Raises: + OptimizationServiceError: if an HTTP error is returned while solving a + model. + """ + if api_key is None: + # TODO(b/306709279): Relax this when unauthenticated solves are allowed. + raise ValueError("api_key can't be None when solving remotely") + + payload = _build_json_payload( + model, solver_type, params, model_params, resources + ) + + session = create_optimization_service_session(api_key, deadline_sec) + response = session.post( + url=endpoint, + json=payload, + timeout=deadline_sec, + ) + + if not response.ok: + http_error = json.loads(response.content)["error"] + raise OptimizationServiceError( + f'status code {http_error["code"]}: {http_error["message"]}' + ) from None + + return _build_solve_result(response.content, model) def create_optimization_service_session( api_key: str, deadline_sec: float, ) -> requests.Session: - """Creates a session with the appropriate headers. - - This function sets headers for authentication via an API key, and it sets - deadlines set for the server and the connection. - - Args: - api_key: Key to the OR API. - deadline_sec: The number of seconds before the request times out. - - Returns: - requests.Session a session with the necessary headers to call the - optimization service. - """ - session = requests.Session() - server_timeout = deadline_sec * (1 - _RELATIVE_TIME_BUFFER) - session.headers = { - "Content-Type": "application/json", - "Connection": "keep-alive", - "Keep-Alive": f"timeout={deadline_sec}, max=1", - "X-Server-Timeout": f"{server_timeout}", - "X-Goog-Api-Key": api_key, - } - return session + """Creates a session with the appropriate headers. + + This function sets headers for authentication via an API key, and it sets + deadlines set for the server and the connection. + + Args: + api_key: Key to the OR API. + deadline_sec: The number of seconds before the request times out. + + Returns: + requests.Session a session with the necessary headers to call the + optimization service. + """ + session = requests.Session() + server_timeout = deadline_sec * (1 - _RELATIVE_TIME_BUFFER) + session.headers = { + "Content-Type": "application/json", + "Connection": "keep-alive", + "Keep-Alive": f"timeout={deadline_sec}, max=1", + "X-Server-Timeout": f"{server_timeout}", + "X-Goog-Api-Key": api_key, + } + return session def _build_json_payload( @@ -119,66 +123,68 @@ def _build_json_payload( model_params: Optional[mathopt.ModelSolveParameters], resources: Optional[mathopt.SolverResources], ): - """Builds a JSON payload. - - Args: - model: The optimization model. - solver_type: The underlying solver to use. - params: Optional configuration of the underlying solver. - model_params: Optional configuration of the solver that is model specific. - resources: Hints on resources requested for the solve. - - Returns: - A JSON object with a MathOpt model and corresponding parameters. - - Raises: - SerializationError: If building the OR API proto is not successful or - deserializing to JSON fails. - """ - params = params or mathopt.SolveParameters() - model_params = model_params or mathopt.ModelSolveParameters() - resources = resources or mathopt.SolverResources() - try: - request = rpc_pb2.SolveRequest( - model=model.export_model(), - solver_type=solver_type.value, - resources=resources.to_proto(), - parameters=params.to_proto(), - model_parameters=model_params.to_proto(), - ) - api_request = proto_converter.convert_request(request) - except ValueError as err: - raise ValueError from err - - return json.loads(json_format.MessageToJson(api_request)) + """Builds a JSON payload. + + Args: + model: The optimization model. + solver_type: The underlying solver to use. + params: Optional configuration of the underlying solver. + model_params: Optional configuration of the solver that is model specific. + resources: Hints on resources requested for the solve. + + Returns: + A JSON object with a MathOpt model and corresponding parameters. + + Raises: + SerializationError: If building the OR API proto is not successful or + deserializing to JSON fails. + """ + params = params or mathopt.SolveParameters() + model_params = model_params or mathopt.ModelSolveParameters() + resources = resources or mathopt.SolverResources() + try: + request = rpc_pb2.SolveRequest( + model=model.export_model(), + solver_type=solver_type.value, + resources=resources.to_proto(), + parameters=params.to_proto(), + model_parameters=model_params.to_proto(), + ) + api_request = proto_converter.convert_request(request) + except ValueError as err: + raise ValueError from err + + return json.loads(json_format.MessageToJson(api_request)) def _build_solve_result( json_response: bytes, model: mathopt.Model ) -> Tuple[mathopt.SolveResult, List[str]]: - """Parses a JSON representation of a response to a SolveResult object. - - Args: - json_response: bytes representing the `SolveMathOptModelResponse` in JSON - format - model: The optimization model that was solved - - Returns: - A SolveResult of the model. - A list of messages with the logs. - - Raises: - SerializationError: If parsing the json response fails or if converting the - OR API response to the internal MathOpt response fails. - """ - try: - api_response = json_format.Parse( - json_response, optimization_pb2.SolveMathOptModelResponse() - ) - except json_format.ParseError as json_err: - raise ValueError( - "API response is not a valid SolveMathOptModelResponse JSON" - ) from json_err - - response = proto_converter.convert_response(api_response) - return mathopt.parse_solve_result(response.result, model), list(response.messages) + """Parses a JSON representation of a response to a SolveResult object. + + Args: + json_response: bytes representing the `SolveMathOptModelResponse` in JSON + format + model: The optimization model that was solved + + Returns: + A SolveResult of the model. + A list of messages with the logs. + + Raises: + SerializationError: If parsing the json response fails or if converting the + OR API response to the internal MathOpt response fails. + """ + try: + api_response = json_format.Parse( + json_response, optimization_pb2.SolveMathOptModelResponse() + ) + except json_format.ParseError as json_err: + raise ValueError( + "API response is not a valid SolveMathOptModelResponse JSON" + ) from json_err + + response = proto_converter.convert_response(api_response) + return mathopt.parse_solve_result(response.result, model), list( + response.messages + ) diff --git a/ortools/math_opt/python/ipc/remote_http_solve_test.py b/ortools/math_opt/python/ipc/remote_http_solve_test.py index 92804205f23..c04e6c39a27 100644 --- a/ortools/math_opt/python/ipc/remote_http_solve_test.py +++ b/ortools/math_opt/python/ipc/remote_http_solve_test.py @@ -69,144 +69,148 @@ def _simple_model() -> tuple[mathopt.Model, mathopt.Variable, mathopt.Variable]: - mod = mathopt.Model(name="test_mod") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - mod.maximize(x - y) - return mod, x, y + mod = mathopt.Model(name="test_mod") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + mod.maximize(x - y) + return mod, x, y class RemoteHttpSolveTest(absltest.TestCase): - def test_session_headers(self): - deadline = 1.0 - server_deadline = deadline * (1 - remote_http_solve._RELATIVE_TIME_BUFFER) - session = remote_http_solve.create_optimization_service_session( - _MOCK_API_KEY, deadline - ) - self.assertEqual(session.headers["Content-Type"], "application/json") - self.assertEqual(session.headers["Connection"], "keep-alive") - self.assertEqual(session.headers["Keep-Alive"], "timeout=1.0, max=1") - self.assertAlmostEqual( - float(session.headers["X-Server-Timeout"]), server_deadline, delta=1e-4 - ) - self.assertEqual(session.headers["X-Goog-Api-Key"], _MOCK_API_KEY) - - @requests_mock.Mocker() - def test_remote_http_solve(self, m: requests_mock.Mocker): - mod, x, y = _simple_model() - # Mock request with the JSON response and a fake API key. - m.post( - _ENDPOINT, - text=_JSON_RESPONSE, - status_code=200, - ) - - remote_solve_result, messages = remote_http_solve.remote_http_solve( - mod, - mathopt.SolverType.GSCIP, - params=mathopt.SolveParameters(enable_output=True), - api_key=_MOCK_API_KEY, - resources=mathopt.SolverResources(ram=1024 * 1024 * 1024), - ) - - self.assertGreaterEqual(len(remote_solve_result.solutions), 1) - self.assertIsNotNone(remote_solve_result.solutions[0].primal_solution) - self.assertAlmostEqual( - remote_solve_result.solutions[0].primal_solution.objective_value, - 1.0, - delta=1e-4, - ) - self.assertEqual( - remote_solve_result.solutions[0].primal_solution.variable_values[x], 1.0 - ) - self.assertEqual( - remote_solve_result.solutions[0].primal_solution.variable_values[y], 0.0 - ) - self.assertListEqual(messages, ["Logs from the solver:", "End of logs"]) - - @requests_mock.Mocker() - def test_remote_http_solve_on_different_endpoint(self, m: requests_mock.Mocker): - mod, _, _ = _simple_model() - # Mock request with the JSON response and a fake API key. - new_endpoint = "https://new.math.opt.com/v1:solve" - m.post( - new_endpoint, - text=_JSON_RESPONSE, - status_code=200, - ) - - remote_solve_result, messages = remote_http_solve.remote_http_solve( - mod, - mathopt.SolverType.GSCIP, - endpoint=new_endpoint, - api_key=_MOCK_API_KEY, - ) - - self.assertGreaterEqual(len(remote_solve_result.solutions), 1) - self.assertIsNotNone(remote_solve_result.solutions[0].primal_solution) - self.assertAlmostEqual( - remote_solve_result.solutions[0].primal_solution.objective_value, - 1.0, - delta=1e-4, - ) - self.assertGreater(len(messages), 1) - self.assertListEqual(messages, ["Logs from the solver:", "End of logs"]) - - @requests_mock.Mocker() - def test_server_down(self, m: requests_mock.Mocker): - mod, _, _ = _simple_model() - # Mock request with the JSON response and a fake API key. - server_error_json = """{ + def test_session_headers(self): + deadline = 1.0 + server_deadline = deadline * (1 - remote_http_solve._RELATIVE_TIME_BUFFER) + session = remote_http_solve.create_optimization_service_session( + _MOCK_API_KEY, deadline + ) + self.assertEqual(session.headers["Content-Type"], "application/json") + self.assertEqual(session.headers["Connection"], "keep-alive") + self.assertEqual(session.headers["Keep-Alive"], "timeout=1.0, max=1") + self.assertAlmostEqual( + float(session.headers["X-Server-Timeout"]), server_deadline, delta=1e-4 + ) + self.assertEqual(session.headers["X-Goog-Api-Key"], _MOCK_API_KEY) + + @requests_mock.Mocker() + def test_remote_http_solve(self, m: requests_mock.Mocker): + mod, x, y = _simple_model() + # Mock request with the JSON response and a fake API key. + m.post( + _ENDPOINT, + text=_JSON_RESPONSE, + status_code=200, + ) + + remote_solve_result, messages = remote_http_solve.remote_http_solve( + mod, + mathopt.SolverType.GSCIP, + params=mathopt.SolveParameters(enable_output=True), + api_key=_MOCK_API_KEY, + resources=mathopt.SolverResources(ram=1024 * 1024 * 1024), + ) + + self.assertGreaterEqual(len(remote_solve_result.solutions), 1) + self.assertIsNotNone(remote_solve_result.solutions[0].primal_solution) + self.assertAlmostEqual( + remote_solve_result.solutions[0].primal_solution.objective_value, + 1.0, + delta=1e-4, + ) + self.assertEqual( + remote_solve_result.solutions[0].primal_solution.variable_values[x], 1.0 + ) + self.assertEqual( + remote_solve_result.solutions[0].primal_solution.variable_values[y], 0.0 + ) + self.assertListEqual(messages, ["Logs from the solver:", "End of logs"]) + + @requests_mock.Mocker() + def test_remote_http_solve_on_different_endpoint( + self, m: requests_mock.Mocker + ): + mod, _, _ = _simple_model() + # Mock request with the JSON response and a fake API key. + new_endpoint = "https://new.math.opt.com/v1:solve" + m.post( + new_endpoint, + text=_JSON_RESPONSE, + status_code=200, + ) + + remote_solve_result, messages = remote_http_solve.remote_http_solve( + mod, + mathopt.SolverType.GSCIP, + endpoint=new_endpoint, + api_key=_MOCK_API_KEY, + ) + + self.assertGreaterEqual(len(remote_solve_result.solutions), 1) + self.assertIsNotNone(remote_solve_result.solutions[0].primal_solution) + self.assertAlmostEqual( + remote_solve_result.solutions[0].primal_solution.objective_value, + 1.0, + delta=1e-4, + ) + self.assertGreater(len(messages), 1) + self.assertListEqual(messages, ["Logs from the solver:", "End of logs"]) + + @requests_mock.Mocker() + def test_server_down(self, m: requests_mock.Mocker): + mod, _, _ = _simple_model() + # Mock request with the JSON response and a fake API key. + server_error_json = """{ "error": { "code": 500, "message": "server down, sorry"} } """ - m.post( - _ENDPOINT, - text=server_error_json, - status_code=500, - ) - - with self.assertRaisesRegex( - remote_http_solve.OptimizationServiceError, "server down, sorry" - ): - remote_http_solve.remote_http_solve( - mod, mathopt.SolverType.GSCIP, api_key=_MOCK_API_KEY - ) - - def test_request_serialization_error(self): - mod, _, _ = _simple_model() - params = mathopt.SolveParameters() - params.gscip.heuristics = gscip_pb2.GScipParameters.MetaParamValue.AGGRESSIVE - - with self.assertRaises(ValueError): - remote_http_solve.remote_http_solve( - mod, mathopt.SolverType.GSCIP, params, api_key=_MOCK_API_KEY - ) - - @requests_mock.Mocker() - def test_response_serialization_error(self, m: requests_mock.Mocker): - mod, _, _ = _simple_model() - # Mock request with the JSON response and a fake API key. - invalid_json_response = """{ + m.post( + _ENDPOINT, + text=server_error_json, + status_code=500, + ) + + with self.assertRaisesRegex( + remote_http_solve.OptimizationServiceError, "server down, sorry" + ): + remote_http_solve.remote_http_solve( + mod, mathopt.SolverType.GSCIP, api_key=_MOCK_API_KEY + ) + + def test_request_serialization_error(self): + mod, _, _ = _simple_model() + params = mathopt.SolveParameters() + params.gscip.heuristics = ( + gscip_pb2.GScipParameters.MetaParamValue.AGGRESSIVE + ) + + with self.assertRaises(ValueError): + remote_http_solve.remote_http_solve( + mod, mathopt.SolverType.GSCIP, params, api_key=_MOCK_API_KEY + ) + + @requests_mock.Mocker() + def test_response_serialization_error(self, m: requests_mock.Mocker): + mod, _, _ = _simple_model() + # Mock request with the JSON response and a fake API key. + invalid_json_response = """{ "notAValidResponse" = 0.0 } """ - m.post( - _ENDPOINT, - text=invalid_json_response, - ) + m.post( + _ENDPOINT, + text=invalid_json_response, + ) - with self.assertRaisesRegex( - ValueError, - "not a valid SolveMathOptModelResponse JSON", - ): - remote_http_solve.remote_http_solve( - mod, mathopt.SolverType.GSCIP, api_key=_MOCK_API_KEY - ) + with self.assertRaisesRegex( + ValueError, + "not a valid SolveMathOptModelResponse JSON", + ): + remote_http_solve.remote_http_solve( + mod, mathopt.SolverType.GSCIP, api_key=_MOCK_API_KEY + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/linear_constraints.py b/ortools/math_opt/python/linear_constraints.py index b589eb2d927..ccf757aa1bf 100644 --- a/ortools/math_opt/python/linear_constraints.py +++ b/ortools/math_opt/python/linear_constraints.py @@ -22,134 +22,136 @@ class LinearConstraint(from_model.FromModel): - """A linear constraint for an optimization model. - - A LinearConstraint adds the following restriction on feasible solutions to an - optimization model: - lb <= sum_{i in I} a_i * x_i <= ub - where x_i are the decision variables of the problem. lb == ub is allowed, this - models the equality constraint: - sum_{i in I} a_i * x_i == b - Setting lb > ub will result in an InvalidArgument error at solve time (the - values are allowed to cross temporarily between solves). - - A LinearConstraint can be configured as follows: - * lower_bound: a float property, lb above. Should not be NaN nor +inf. - * upper_bound: a float property, ub above. Should not be NaN nor -inf. - * set_coefficient() and get_coefficient(): get and set the a_i * x_i - terms. The variable must be from the same model as this constraint, and - the a_i must be finite and not NaN. The coefficient for any variable not - set is 0.0, and setting a coefficient to 0.0 removes it from I above. - - The name is optional, read only, and used only for debugging. Non-empty names - should be distinct. - - Do not create a LinearConstraint directly, use Model.add_linear_constraint() - instead. Two LinearConstraint objects can represent the same constraint (for - the same model). They will have the same underlying LinearConstraint.elemental - for storing the data. The LinearConstraint class is simply a reference to an - Elemental. - """ - - __slots__ = "_elemental", "_id" - - def __init__(self, elem: elemental.Elemental, cid: int) -> None: - """Internal only, prefer Model functions (add_linear_constraint() and get_linear_constraint()).""" - if not isinstance(cid, int): - raise TypeError(f"cid type should be int, was:{type(cid).__name__!r}") - self._elemental: elemental.Elemental = elem - self._id: int = cid - - @property - def lower_bound(self) -> float: - return self._elemental.get_attr( - enums.DoubleAttr1.LINEAR_CONSTRAINT_LOWER_BOUND, (self._id,) - ) - - @lower_bound.setter - def lower_bound(self, value: float) -> None: - self._elemental.set_attr( - enums.DoubleAttr1.LINEAR_CONSTRAINT_LOWER_BOUND, (self._id,), value - ) - - @property - def upper_bound(self) -> float: - return self._elemental.get_attr( - enums.DoubleAttr1.LINEAR_CONSTRAINT_UPPER_BOUND, (self._id,) - ) - - @upper_bound.setter - def upper_bound(self, value: float) -> None: - self._elemental.set_attr( - enums.DoubleAttr1.LINEAR_CONSTRAINT_UPPER_BOUND, (self._id,), value - ) - - @property - def name(self) -> str: - return self._elemental.get_element_name( - enums.ElementType.LINEAR_CONSTRAINT, self._id - ) - - @property - def id(self) -> int: - return self._id - - @property - def elemental(self) -> elemental.Elemental: - """Internal use only.""" - return self._elemental - - def set_coefficient(self, var: variables.Variable, coefficient: float) -> None: - from_model.model_is_same(var, self) - self._elemental.set_attr( - enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, - (self._id, var.id), - coefficient, - ) - - def get_coefficient(self, var: variables.Variable) -> float: - from_model.model_is_same(var, self) - return self._elemental.get_attr( - enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, (self._id, var.id) - ) - - def terms(self) -> Iterator[variables.LinearTerm]: - """Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint.""" - keys = self._elemental.slice_attr( - enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 0, self._id - ) - coefs = self._elemental.get_attrs( - enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, keys - ) - for i in range(len(keys)): - yield variables.LinearTerm( - variable=variables.Variable(self._elemental, int(keys[i, 1])), - coefficient=float(coefs[i]), - ) - - def as_bounded_linear_expression(self) -> variables.BoundedLinearExpression: - """Returns the bounded expression from lower_bound, upper_bound and terms.""" - return variables.BoundedLinearExpression( - self.lower_bound, variables.LinearSum(self.terms()), self.upper_bound - ) - - def __str__(self): - """Returns the name, or a string containing the id if the name is empty.""" - return self.name if self.name else f"linear_constraint_{self.id}" - - def __repr__(self): - return f"" - - def __eq__(self, other: Any) -> bool: - if isinstance(other, LinearConstraint): - return self._id == other._id and self._elemental is other._elemental - return False - - def __hash__(self) -> int: - return hash(self._id) + """A linear constraint for an optimization model. + + A LinearConstraint adds the following restriction on feasible solutions to an + optimization model: + lb <= sum_{i in I} a_i * x_i <= ub + where x_i are the decision variables of the problem. lb == ub is allowed, this + models the equality constraint: + sum_{i in I} a_i * x_i == b + Setting lb > ub will result in an InvalidArgument error at solve time (the + values are allowed to cross temporarily between solves). + + A LinearConstraint can be configured as follows: + * lower_bound: a float property, lb above. Should not be NaN nor +inf. + * upper_bound: a float property, ub above. Should not be NaN nor -inf. + * set_coefficient() and get_coefficient(): get and set the a_i * x_i + terms. The variable must be from the same model as this constraint, and + the a_i must be finite and not NaN. The coefficient for any variable not + set is 0.0, and setting a coefficient to 0.0 removes it from I above. + + The name is optional, read only, and used only for debugging. Non-empty names + should be distinct. + + Do not create a LinearConstraint directly, use Model.add_linear_constraint() + instead. Two LinearConstraint objects can represent the same constraint (for + the same model). They will have the same underlying LinearConstraint.elemental + for storing the data. The LinearConstraint class is simply a reference to an + Elemental. + """ + + __slots__ = "_elemental", "_id" + + def __init__(self, elem: elemental.Elemental, cid: int) -> None: + """Internal only, prefer Model functions (add_linear_constraint() and get_linear_constraint()).""" + if not isinstance(cid, int): + raise TypeError(f"cid type should be int, was:{type(cid).__name__!r}") + self._elemental: elemental.Elemental = elem + self._id: int = cid + + @property + def lower_bound(self) -> float: + return self._elemental.get_attr( + enums.DoubleAttr1.LINEAR_CONSTRAINT_LOWER_BOUND, (self._id,) + ) + + @lower_bound.setter + def lower_bound(self, value: float) -> None: + self._elemental.set_attr( + enums.DoubleAttr1.LINEAR_CONSTRAINT_LOWER_BOUND, (self._id,), value + ) + + @property + def upper_bound(self) -> float: + return self._elemental.get_attr( + enums.DoubleAttr1.LINEAR_CONSTRAINT_UPPER_BOUND, (self._id,) + ) + + @upper_bound.setter + def upper_bound(self, value: float) -> None: + self._elemental.set_attr( + enums.DoubleAttr1.LINEAR_CONSTRAINT_UPPER_BOUND, (self._id,), value + ) + + @property + def name(self) -> str: + return self._elemental.get_element_name( + enums.ElementType.LINEAR_CONSTRAINT, self._id + ) + + @property + def id(self) -> int: + return self._id + + @property + def elemental(self) -> elemental.Elemental: + """Internal use only.""" + return self._elemental + + def set_coefficient( + self, var: variables.Variable, coefficient: float + ) -> None: + from_model.model_is_same(var, self) + self._elemental.set_attr( + enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, + (self._id, var.id), + coefficient, + ) + + def get_coefficient(self, var: variables.Variable) -> float: + from_model.model_is_same(var, self) + return self._elemental.get_attr( + enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, (self._id, var.id) + ) + + def terms(self) -> Iterator[variables.LinearTerm]: + """Yields the variable/coefficient pairs with nonzero coefficient for this linear constraint.""" + keys = self._elemental.slice_attr( + enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 0, self._id + ) + coefs = self._elemental.get_attrs( + enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, keys + ) + for i in range(len(keys)): + yield variables.LinearTerm( + variable=variables.Variable(self._elemental, int(keys[i, 1])), + coefficient=float(coefs[i]), + ) + + def as_bounded_linear_expression(self) -> variables.BoundedLinearExpression: + """Returns the bounded expression from lower_bound, upper_bound and terms.""" + return variables.BoundedLinearExpression( + self.lower_bound, variables.LinearSum(self.terms()), self.upper_bound + ) + + def __str__(self): + """Returns the name, or a string containing the id if the name is empty.""" + return self.name if self.name else f"linear_constraint_{self.id}" + + def __repr__(self): + return f"" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, LinearConstraint): + return self._id == other._id and self._elemental is other._elemental + return False + + def __hash__(self) -> int: + return hash(self._id) class LinearConstraintMatrixEntry(NamedTuple): - linear_constraint: LinearConstraint - variable: variables.Variable - coefficient: float + linear_constraint: LinearConstraint + variable: variables.Variable + coefficient: float diff --git a/ortools/math_opt/python/linear_expression_test.py b/ortools/math_opt/python/linear_expression_test.py index 013a7dfda4d..ef72112569f 100644 --- a/ortools/math_opt/python/linear_expression_test.py +++ b/ortools/math_opt/python/linear_expression_test.py @@ -40,580 +40,588 @@ class BoundedLinearExprTest(absltest.TestCase): - def test_eq_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = x + 2 * y + 1.0 == 2.0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.lower_bound, 2.0) - self.assertEqual(bounded_expr.upper_bound, 2.0) - - # Also call __eq__ directly to confirm there are no pytype issues. - def test_eq_float_explicit(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = (x + 2 * y + 1.0).__eq__(2.0) - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.lower_bound, 2.0) - self.assertEqual(bounded_expr.upper_bound, 2.0) - - def test_eq_expr(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = x + 2 * y + 1.0 == 3 * y - 2.0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 3.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: -1.0}) - self.assertEqual(bounded_expr.lower_bound, 0.0) - self.assertEqual(bounded_expr.upper_bound, 0.0) - - # Check Variable.__eq__ calls LinearBase.__eq__ when appropriate. - bounded_expr_var_on_lhs = x == 3 * y - 2.0 - self.assertIsInstance( - bounded_expr_var_on_lhs, bounded_expressions.BoundedExpression - ) - flat_expr_var_on_lhs = variables.as_flat_linear_expression( - bounded_expr_var_on_lhs.expression - ) - self.assertEqual(flat_expr_var_on_lhs.offset, 2.0) - self.assertDictEqual(dict(flat_expr_var_on_lhs.terms), {x: 1.0, y: -3.0}) - self.assertEqual(bounded_expr_var_on_lhs.lower_bound, 0.0) - self.assertEqual(bounded_expr_var_on_lhs.upper_bound, 0.0) - - bounded_expr_var_on_rhs = 3 * y - 2.0 == x - self.assertIsInstance( - bounded_expr_var_on_rhs, bounded_expressions.BoundedExpression - ) - flat_expr_var_on_rhs = variables.as_flat_linear_expression( - bounded_expr_var_on_rhs.expression - ) - self.assertEqual(flat_expr_var_on_rhs.offset, -2.0) - self.assertDictEqual(dict(flat_expr_var_on_rhs.terms), {x: -1.0, y: 3.0}) - self.assertEqual(bounded_expr_var_on_rhs.lower_bound, 0.0) - self.assertEqual(bounded_expr_var_on_rhs.upper_bound, 0.0) - - # Also call __eq__ directly to confirm there are no pytype issues. - def test_eq_expr_explicit(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = (x + 2 * y + 1.0).__eq__(3 * y - 2.0) - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 3.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: -1.0}) - self.assertEqual(bounded_expr.lower_bound, 0.0) - self.assertEqual(bounded_expr.upper_bound, 0.0) - - # Check Variable.__eq__ calls LinearBase.__eq__ when appropriate. - bounded_expr_var_on_lhs = x.__eq__(3 * y - 2.0) - self.assertIsInstance( - bounded_expr_var_on_lhs, bounded_expressions.BoundedExpression - ) - flat_expr_var_on_lhs = variables.as_flat_linear_expression( - bounded_expr_var_on_lhs.expression - ) - self.assertEqual(flat_expr_var_on_lhs.offset, 2.0) - self.assertDictEqual(dict(flat_expr_var_on_lhs.terms), {x: 1.0, y: -3.0}) - self.assertEqual(bounded_expr_var_on_lhs.lower_bound, 0.0) - self.assertEqual(bounded_expr_var_on_lhs.upper_bound, 0.0) - - bounded_expr_var_on_rhs = (3 * y - 2.0).__eq__(x) - self.assertIsInstance( - bounded_expr_var_on_rhs, bounded_expressions.BoundedExpression - ) - flat_expr_var_on_rhs = variables.as_flat_linear_expression( - bounded_expr_var_on_rhs.expression - ) - self.assertEqual(flat_expr_var_on_rhs.offset, -2.0) - self.assertDictEqual(dict(flat_expr_var_on_rhs.terms), {x: -1.0, y: 3.0}) - self.assertEqual(bounded_expr_var_on_rhs.lower_bound, 0.0) - self.assertEqual(bounded_expr_var_on_rhs.upper_bound, 0.0) - - def test_var_eq_var(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - also_x = x - bounded_expr = x == y - self.assertIsInstance(bounded_expr, variables.VarEqVar) - self.assertEqual(bounded_expr.first_variable, x) - self.assertEqual(bounded_expr.second_variable, y) - - second_mod = model.Model() - second_x = second_mod.add_binary_variable(name="x") - # pylint: disable=g-generic-assert - self.assertTrue(x == also_x) - self.assertFalse(x == y) - self.assertEqual(x.id, second_x.id) - self.assertFalse(x == second_x) - # pylint: enable=g-generic-assert - - # Also call __eq__ directly to confirm there are no pytype issues (see - # b/227214976). - def test_var_eq_var_explicit(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - also_x = x - bounded_expr = x.__eq__(y) - self.assertIsInstance(bounded_expr, variables.VarEqVar) - self.assertEqual(bounded_expr.first_variable, x) - self.assertEqual(bounded_expr.second_variable, y) - - second_mod = model.Model() - second_x = second_mod.add_binary_variable(name="x") - # pylint: disable=g-generic-assert - self.assertTrue(x.__eq__(also_x)) - self.assertFalse(x.__eq__(y)) - self.assertEqual(x.id, second_x.id) - self.assertFalse(x.__eq__(second_x)) - # pylint: enable=g-generic-assert - - def test_var_neq_var(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - also_x = x - second_mod = model.Model() - second_x = second_mod.add_binary_variable(name="x") - # pylint: disable=g-generic-assert - self.assertFalse(x != also_x) - self.assertTrue(x != y) - self.assertEqual(x.id, second_x.id) - self.assertTrue(x != second_x) - # pylint: enable=g-generic-assert - - # Also call __ne__ directly to confirm there are no pytype issues (see - # b/227214976). - def test_var_neq_var_explicit(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - also_x = x - second_mod = model.Model() - second_x = second_mod.add_binary_variable(name="x") - # pylint: disable=g-generic-assert - self.assertFalse(x.__ne__(also_x)) - self.assertTrue(x.__ne__(y)) - self.assertEqual(x.id, second_x.id) - self.assertTrue(x.__ne__(second_x)) - # pylint: enable=g-generic-assert - - # Mock Variable.__hash__ to have a collision in the dictionary lookup so that - # a correct behavior of x == y is needed to recover the values. For instance, - # if VarEqVar.__bool__ always returned True, this test would fail. - @mock.patch.object(variables.Variable, "__hash__") - def test_var_dict(self, fixed_hash: mock.MagicMock) -> None: - fixed_hash.return_value = 111 - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - var_dict = {x: 1.0, y: 2.0} - self.assertEqual(x.__hash__(), 111) - self.assertEqual(y.__hash__(), 111) - self.assertEqual(var_dict[x], 1.0) - self.assertEqual(var_dict[y], 2.0) - - def test_leq_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = x + 2 * y + 1.0 <= 2.0 - self.assertIsInstance(bounded_expr, bounded_expressions.UpperBoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.upper_bound, 2.0) - - def test_leq_float_rev(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = 2.0 >= x + 2 * y + 1.0 - self.assertIsInstance(bounded_expr, bounded_expressions.UpperBoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.upper_bound, 2.0) - - def test_geq_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = x + 2 * y + 1.0 >= 2.0 - self.assertIsInstance(bounded_expr, bounded_expressions.LowerBoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.lower_bound, 2.0) - - def test_geq_float_rev(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = 2.0 <= x + 2 * y + 1.0 - self.assertIsInstance(bounded_expr, bounded_expressions.LowerBoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.lower_bound, 2.0) - - def test_geq_leq_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.upper_bound, 2.0) - self.assertEqual(bounded_expr.lower_bound, 0.0) - - def test_geq_leq_float_rev(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = 2.0 >= (x + 2 * y + 1.0 >= 0) - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.upper_bound, 2.0) - self.assertEqual(bounded_expr.lower_bound, 0.0) - - def test_leq_geq_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = 0.0 <= (x + 2 * y + 1.0 <= 2.0) - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.upper_bound, 2.0) - self.assertEqual(bounded_expr.lower_bound, 0.0) - - def test_leq_geq_float_rev(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = (2.0 >= x + 2 * y + 1.0) >= 0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) - self.assertEqual(bounded_expr.upper_bound, 2.0) - self.assertEqual(bounded_expr.lower_bound, 0.0) - - def test_leq_expr(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - bounded_expr = x + 3 * y + 2.0 <= y - 4.0 * z + 1.0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0, z: 4.0}) - self.assertEqual(bounded_expr.lower_bound, -math.inf) - self.assertEqual(bounded_expr.upper_bound, 0.0) - - def test_geq_expr(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - bounded_expr = x + 3 * y + 2.0 >= y - 4.0 * z + 1.0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(flat_expr.offset, 1.0) - self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0, z: 4.0}) - self.assertEqual(bounded_expr.lower_bound, 0.0) - self.assertEqual(bounded_expr.upper_bound, math.inf) + def test_eq_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = x + 2 * y + 1.0 == 2.0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.lower_bound, 2.0) + self.assertEqual(bounded_expr.upper_bound, 2.0) + + # Also call __eq__ directly to confirm there are no pytype issues. + def test_eq_float_explicit(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = (x + 2 * y + 1.0).__eq__(2.0) + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.lower_bound, 2.0) + self.assertEqual(bounded_expr.upper_bound, 2.0) + + def test_eq_expr(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = x + 2 * y + 1.0 == 3 * y - 2.0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 3.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: -1.0}) + self.assertEqual(bounded_expr.lower_bound, 0.0) + self.assertEqual(bounded_expr.upper_bound, 0.0) + + # Check Variable.__eq__ calls LinearBase.__eq__ when appropriate. + bounded_expr_var_on_lhs = x == 3 * y - 2.0 + self.assertIsInstance( + bounded_expr_var_on_lhs, bounded_expressions.BoundedExpression + ) + flat_expr_var_on_lhs = variables.as_flat_linear_expression( + bounded_expr_var_on_lhs.expression + ) + self.assertEqual(flat_expr_var_on_lhs.offset, 2.0) + self.assertDictEqual(dict(flat_expr_var_on_lhs.terms), {x: 1.0, y: -3.0}) + self.assertEqual(bounded_expr_var_on_lhs.lower_bound, 0.0) + self.assertEqual(bounded_expr_var_on_lhs.upper_bound, 0.0) + + bounded_expr_var_on_rhs = 3 * y - 2.0 == x + self.assertIsInstance( + bounded_expr_var_on_rhs, bounded_expressions.BoundedExpression + ) + flat_expr_var_on_rhs = variables.as_flat_linear_expression( + bounded_expr_var_on_rhs.expression + ) + self.assertEqual(flat_expr_var_on_rhs.offset, -2.0) + self.assertDictEqual(dict(flat_expr_var_on_rhs.terms), {x: -1.0, y: 3.0}) + self.assertEqual(bounded_expr_var_on_rhs.lower_bound, 0.0) + self.assertEqual(bounded_expr_var_on_rhs.upper_bound, 0.0) + + # Also call __eq__ directly to confirm there are no pytype issues. + def test_eq_expr_explicit(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = (x + 2 * y + 1.0).__eq__(3 * y - 2.0) + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 3.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: -1.0}) + self.assertEqual(bounded_expr.lower_bound, 0.0) + self.assertEqual(bounded_expr.upper_bound, 0.0) + + # Check Variable.__eq__ calls LinearBase.__eq__ when appropriate. + bounded_expr_var_on_lhs = x.__eq__(3 * y - 2.0) + self.assertIsInstance( + bounded_expr_var_on_lhs, bounded_expressions.BoundedExpression + ) + flat_expr_var_on_lhs = variables.as_flat_linear_expression( + bounded_expr_var_on_lhs.expression + ) + self.assertEqual(flat_expr_var_on_lhs.offset, 2.0) + self.assertDictEqual(dict(flat_expr_var_on_lhs.terms), {x: 1.0, y: -3.0}) + self.assertEqual(bounded_expr_var_on_lhs.lower_bound, 0.0) + self.assertEqual(bounded_expr_var_on_lhs.upper_bound, 0.0) + + bounded_expr_var_on_rhs = (3 * y - 2.0).__eq__(x) + self.assertIsInstance( + bounded_expr_var_on_rhs, bounded_expressions.BoundedExpression + ) + flat_expr_var_on_rhs = variables.as_flat_linear_expression( + bounded_expr_var_on_rhs.expression + ) + self.assertEqual(flat_expr_var_on_rhs.offset, -2.0) + self.assertDictEqual(dict(flat_expr_var_on_rhs.terms), {x: -1.0, y: 3.0}) + self.assertEqual(bounded_expr_var_on_rhs.lower_bound, 0.0) + self.assertEqual(bounded_expr_var_on_rhs.upper_bound, 0.0) + + def test_var_eq_var(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + also_x = x + bounded_expr = x == y + self.assertIsInstance(bounded_expr, variables.VarEqVar) + self.assertEqual(bounded_expr.first_variable, x) + self.assertEqual(bounded_expr.second_variable, y) + + second_mod = model.Model() + second_x = second_mod.add_binary_variable(name="x") + # pylint: disable=g-generic-assert + self.assertTrue(x == also_x) + self.assertFalse(x == y) + self.assertEqual(x.id, second_x.id) + self.assertFalse(x == second_x) + # pylint: enable=g-generic-assert + + # Also call __eq__ directly to confirm there are no pytype issues (see + # b/227214976). + def test_var_eq_var_explicit(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + also_x = x + bounded_expr = x.__eq__(y) + self.assertIsInstance(bounded_expr, variables.VarEqVar) + self.assertEqual(bounded_expr.first_variable, x) + self.assertEqual(bounded_expr.second_variable, y) + + second_mod = model.Model() + second_x = second_mod.add_binary_variable(name="x") + # pylint: disable=g-generic-assert + self.assertTrue(x.__eq__(also_x)) + self.assertFalse(x.__eq__(y)) + self.assertEqual(x.id, second_x.id) + self.assertFalse(x.__eq__(second_x)) + # pylint: enable=g-generic-assert + + def test_var_neq_var(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + also_x = x + second_mod = model.Model() + second_x = second_mod.add_binary_variable(name="x") + # pylint: disable=g-generic-assert + self.assertFalse(x != also_x) + self.assertTrue(x != y) + self.assertEqual(x.id, second_x.id) + self.assertTrue(x != second_x) + # pylint: enable=g-generic-assert + + # Also call __ne__ directly to confirm there are no pytype issues (see + # b/227214976). + def test_var_neq_var_explicit(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + also_x = x + second_mod = model.Model() + second_x = second_mod.add_binary_variable(name="x") + # pylint: disable=g-generic-assert + self.assertFalse(x.__ne__(also_x)) + self.assertTrue(x.__ne__(y)) + self.assertEqual(x.id, second_x.id) + self.assertTrue(x.__ne__(second_x)) + # pylint: enable=g-generic-assert + + # Mock Variable.__hash__ to have a collision in the dictionary lookup so that + # a correct behavior of x == y is needed to recover the values. For instance, + # if VarEqVar.__bool__ always returned True, this test would fail. + @mock.patch.object(variables.Variable, "__hash__") + def test_var_dict(self, fixed_hash: mock.MagicMock) -> None: + fixed_hash.return_value = 111 + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + var_dict = {x: 1.0, y: 2.0} + self.assertEqual(x.__hash__(), 111) + self.assertEqual(y.__hash__(), 111) + self.assertEqual(var_dict[x], 1.0) + self.assertEqual(var_dict[y], 2.0) + + def test_leq_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = x + 2 * y + 1.0 <= 2.0 + self.assertIsInstance( + bounded_expr, bounded_expressions.UpperBoundedExpression + ) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.upper_bound, 2.0) + + def test_leq_float_rev(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = 2.0 >= x + 2 * y + 1.0 + self.assertIsInstance( + bounded_expr, bounded_expressions.UpperBoundedExpression + ) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.upper_bound, 2.0) + + def test_geq_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = x + 2 * y + 1.0 >= 2.0 + self.assertIsInstance( + bounded_expr, bounded_expressions.LowerBoundedExpression + ) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.lower_bound, 2.0) + + def test_geq_float_rev(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = 2.0 <= x + 2 * y + 1.0 + self.assertIsInstance( + bounded_expr, bounded_expressions.LowerBoundedExpression + ) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.lower_bound, 2.0) + + def test_geq_leq_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.upper_bound, 2.0) + self.assertEqual(bounded_expr.lower_bound, 0.0) + + def test_geq_leq_float_rev(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = 2.0 >= (x + 2 * y + 1.0 >= 0) + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.upper_bound, 2.0) + self.assertEqual(bounded_expr.lower_bound, 0.0) + + def test_leq_geq_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = 0.0 <= (x + 2 * y + 1.0 <= 2.0) + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.upper_bound, 2.0) + self.assertEqual(bounded_expr.lower_bound, 0.0) + + def test_leq_geq_float_rev(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = (2.0 >= x + 2 * y + 1.0) >= 0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0}) + self.assertEqual(bounded_expr.upper_bound, 2.0) + self.assertEqual(bounded_expr.lower_bound, 0.0) + + def test_leq_expr(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + bounded_expr = x + 3 * y + 2.0 <= y - 4.0 * z + 1.0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0, z: 4.0}) + self.assertEqual(bounded_expr.lower_bound, -math.inf) + self.assertEqual(bounded_expr.upper_bound, 0.0) + + def test_geq_expr(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + bounded_expr = x + 3 * y + 2.0 >= y - 4.0 * z + 1.0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + flat_expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(flat_expr.offset, 1.0) + self.assertDictEqual(dict(flat_expr.terms), {x: 1.0, y: 2.0, z: 4.0}) + self.assertEqual(bounded_expr.lower_bound, 0.0) + self.assertEqual(bounded_expr.upper_bound, math.inf) class BoundedLinearExprErrorTest(absltest.TestCase): - def test_ne(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, "!= constraints are not supported" - ): - x != y - x - with self.assertRaisesWithLiteralMatch( - TypeError, "!= constraints are not supported" - ): - x.__ne__(y - x) - with self.assertRaisesWithLiteralMatch( - TypeError, "!= constraints are not supported" - ): - y - x != x - with self.assertRaisesWithLiteralMatch( - TypeError, "!= constraints are not supported" - ): - (y - x).__ne__(x) - with self.assertRaisesWithLiteralMatch( - TypeError, "!= constraints are not supported" - ): - y - x != x + y - with self.assertRaisesWithLiteralMatch( - TypeError, "!= constraints are not supported" - ): - (y - x).__ne__(x + y) - # pylint: enable=pointless-statement - - def test_eq(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, "unsupported operand type(s) for ==: 'Variable' and 'str'" - ): - x == "x" # pylint: disable=pointless-statement - - # pylint: disable=pointless-statement - # pytype: disable=unsupported-operands - with self.assertRaisesWithLiteralMatch( - TypeError, "unsupported operand type(s) for ==: 'Variable' and 'str'" - ): - x.__eq__("x") - # pylint: enable=pointless-statement - # pylint: enable=unsupported-operands - - def test_float_le_expr_le_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - self.assertIsInstance( - 0.0 <= x + 2 * y + 1.0, bounded_expressions.LowerBoundedExpression - ) - with self.assertRaisesRegex( - TypeError, - "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality", - ): - (0.0 <= x + 2 * y + 1.0 <= 2.0) # pylint: disable=pointless-statement - - def test_float_ge_expr_ge_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - self.assertIsInstance( - 2.0 >= x + 2 * y + 1.0, bounded_expressions.UpperBoundedExpression - ) - with self.assertRaisesRegex( - TypeError, - "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality", - ): - (2.0 >= x + 2 * y + 1.0 >= 0.0) # pylint: disable=pointless-statement - - def test_expr_le_expr_le_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - self.assertIsInstance( - x <= x + 2 * y + 1.0, bounded_expressions.BoundedExpression - ) - with self.assertRaisesRegex( - TypeError, - "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality.*", - ): - (x <= x + 2 * y + 1.0 <= 2.0) # pylint: disable=pointless-statement - - def test_expr_ge_expr_ge_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - self.assertIsInstance( - x >= x + 2 * y + 1.0, bounded_expressions.BoundedExpression - ) - with self.assertRaisesRegex( - TypeError, - "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality", - ): - (x >= x + 2 * y + 1.0 >= 0.0) # pylint: disable=pointless-statement - - def test_lower_bounded_expr_leq_expr(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - rhs_expr = 2.0 + x - lower_bounded_expr = 0.0 <= x + 2 * y + 1.0 - self.assertIsInstance( - lower_bounded_expr, bounded_expressions.LowerBoundedExpression - ) - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, - "unsupported operand type(s) for <=:" - f" {type(lower_bounded_expr).__name__!r} and" - f" {type(rhs_expr).__name__!r}", - ): - lower_bounded_expr <= rhs_expr - # pylint: enable=pointless-statement - - def test_lower_bounded_expr_geq_expr(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - rhs_expr = 2.0 + x - lower_bounded_expr = 0.0 <= x + 2 * y + 1.0 - self.assertIsInstance( - lower_bounded_expr, bounded_expressions.LowerBoundedExpression - ) - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, - f"unsupported operand type(s) for <=: {type(rhs_expr).__name__!r} and" - f" {type(lower_bounded_expr).__name__!r}", - ): - lower_bounded_expr >= rhs_expr - # pylint: enable=pointless-statement - - def test_lower_bounded_expr_geq_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - lower_bounded_expr = 0.0 <= x + 2 * y + 1.0 - self.assertIsInstance( - lower_bounded_expr, bounded_expressions.LowerBoundedExpression - ) - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, - "'>=' not supported between instances of" - f" {type(lower_bounded_expr).__name__!r} and 'float'", - ): - lower_bounded_expr >= 2.0 - # pylint: enable=pointless-statement - - def test_upper_bounded_expr_geq_expr(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - rhs_expr = 1.0 + x - upper_bounded_expr = 2.0 >= x + 2 * y + 1.0 - self.assertIsInstance( - upper_bounded_expr, bounded_expressions.UpperBoundedExpression - ) - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, - "unsupported operand type(s) for >=:" - f" {type(upper_bounded_expr).__name__!r} and" - f" {type(rhs_expr).__name__!r}", - ): - upper_bounded_expr >= rhs_expr - # pylint: enable=pointless-statement - - def test_upper_bounded_expr_leq_expr(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - rhs_expr = 1.0 + x - upper_bounded_expr = 2.0 >= x + 2 * y + 1.0 - self.assertIsInstance( - upper_bounded_expr, bounded_expressions.UpperBoundedExpression - ) - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, - f"unsupported operand type(s) for >=: {type(rhs_expr).__name__!r} and" - f" {type(upper_bounded_expr).__name__!r}", - ): - upper_bounded_expr <= rhs_expr - # pylint: enable=pointless-statement - - def test_upper_bounded_expr_leq_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - upper_bounded_expr = 2.0 >= x + 2 * y + 1.0 - self.assertIsInstance( - upper_bounded_expr, bounded_expressions.UpperBoundedExpression - ) - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, - "'<=' not supported between instances of" - f" {type(upper_bounded_expr).__name__!r} and 'float'", - ): - upper_bounded_expr <= 2.0 - # pylint: enable=pointless-statement - - def test_bounded_expr_leq_expr(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - rhs_expr = 1.0 + x - bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - # pylint: disable=pointless-statement - with self.assertRaisesRegex( - TypeError, - "unsupported operand.*\n.*two or more non-constant linear expressions", - ): - bounded_expr <= rhs_expr - # pylint: enable=pointless-statement - - def test_bounded_expr_leq_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, - "'<=' not supported between instances of" - f" {type(bounded_expr).__name__!r} and 'float'", - ): - bounded_expr <= 2.0 - # pylint: enable=pointless-statement - - def test_bounded_expr_geq_expr(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - rhs_expr = 1.0 + x - bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - # pylint: disable=pointless-statement - with self.assertRaisesRegex( - TypeError, - "unsupported operand.*\n.*two or more non-constant linear expressions", - ): - bounded_expr >= rhs_expr - # pylint: enable=pointless-statement - - def test_bounded_expr_geq_float(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 - self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) - # pylint: disable=pointless-statement - with self.assertRaisesWithLiteralMatch( - TypeError, - "'>=' not supported between instances of" - f" {type(bounded_expr).__name__!r} and 'float'", - ): - bounded_expr >= 2.0 - # pylint: enable=pointless-statement + def test_ne(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, "!= constraints are not supported" + ): + x != y - x + with self.assertRaisesWithLiteralMatch( + TypeError, "!= constraints are not supported" + ): + x.__ne__(y - x) + with self.assertRaisesWithLiteralMatch( + TypeError, "!= constraints are not supported" + ): + y - x != x + with self.assertRaisesWithLiteralMatch( + TypeError, "!= constraints are not supported" + ): + (y - x).__ne__(x) + with self.assertRaisesWithLiteralMatch( + TypeError, "!= constraints are not supported" + ): + y - x != x + y + with self.assertRaisesWithLiteralMatch( + TypeError, "!= constraints are not supported" + ): + (y - x).__ne__(x + y) + # pylint: enable=pointless-statement + + def test_eq(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, "unsupported operand type(s) for ==: 'Variable' and 'str'" + ): + x == "x" # pylint: disable=pointless-statement + + # pylint: disable=pointless-statement + # pytype: disable=unsupported-operands + with self.assertRaisesWithLiteralMatch( + TypeError, "unsupported operand type(s) for ==: 'Variable' and 'str'" + ): + x.__eq__("x") + # pylint: enable=pointless-statement + # pylint: enable=unsupported-operands + + def test_float_le_expr_le_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + self.assertIsInstance( + 0.0 <= x + 2 * y + 1.0, bounded_expressions.LowerBoundedExpression + ) + with self.assertRaisesRegex( + TypeError, + "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality", + ): + (0.0 <= x + 2 * y + 1.0 <= 2.0) # pylint: disable=pointless-statement + + def test_float_ge_expr_ge_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + self.assertIsInstance( + 2.0 >= x + 2 * y + 1.0, bounded_expressions.UpperBoundedExpression + ) + with self.assertRaisesRegex( + TypeError, + "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality", + ): + (2.0 >= x + 2 * y + 1.0 >= 0.0) # pylint: disable=pointless-statement + + def test_expr_le_expr_le_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + self.assertIsInstance( + x <= x + 2 * y + 1.0, bounded_expressions.BoundedExpression + ) + with self.assertRaisesRegex( + TypeError, + "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality.*", + ): + (x <= x + 2 * y + 1.0 <= 2.0) # pylint: disable=pointless-statement + + def test_expr_ge_expr_ge_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + self.assertIsInstance( + x >= x + 2 * y + 1.0, bounded_expressions.BoundedExpression + ) + with self.assertRaisesRegex( + TypeError, + "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality", + ): + (x >= x + 2 * y + 1.0 >= 0.0) # pylint: disable=pointless-statement + + def test_lower_bounded_expr_leq_expr(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + rhs_expr = 2.0 + x + lower_bounded_expr = 0.0 <= x + 2 * y + 1.0 + self.assertIsInstance( + lower_bounded_expr, bounded_expressions.LowerBoundedExpression + ) + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, + "unsupported operand type(s) for <=:" + f" {type(lower_bounded_expr).__name__!r} and" + f" {type(rhs_expr).__name__!r}", + ): + lower_bounded_expr <= rhs_expr + # pylint: enable=pointless-statement + + def test_lower_bounded_expr_geq_expr(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + rhs_expr = 2.0 + x + lower_bounded_expr = 0.0 <= x + 2 * y + 1.0 + self.assertIsInstance( + lower_bounded_expr, bounded_expressions.LowerBoundedExpression + ) + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, + f"unsupported operand type(s) for <=: {type(rhs_expr).__name__!r} and" + f" {type(lower_bounded_expr).__name__!r}", + ): + lower_bounded_expr >= rhs_expr + # pylint: enable=pointless-statement + + def test_lower_bounded_expr_geq_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + lower_bounded_expr = 0.0 <= x + 2 * y + 1.0 + self.assertIsInstance( + lower_bounded_expr, bounded_expressions.LowerBoundedExpression + ) + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, + "'>=' not supported between instances of" + f" {type(lower_bounded_expr).__name__!r} and 'float'", + ): + lower_bounded_expr >= 2.0 + # pylint: enable=pointless-statement + + def test_upper_bounded_expr_geq_expr(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + rhs_expr = 1.0 + x + upper_bounded_expr = 2.0 >= x + 2 * y + 1.0 + self.assertIsInstance( + upper_bounded_expr, bounded_expressions.UpperBoundedExpression + ) + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, + "unsupported operand type(s) for >=:" + f" {type(upper_bounded_expr).__name__!r} and" + f" {type(rhs_expr).__name__!r}", + ): + upper_bounded_expr >= rhs_expr + # pylint: enable=pointless-statement + + def test_upper_bounded_expr_leq_expr(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + rhs_expr = 1.0 + x + upper_bounded_expr = 2.0 >= x + 2 * y + 1.0 + self.assertIsInstance( + upper_bounded_expr, bounded_expressions.UpperBoundedExpression + ) + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, + f"unsupported operand type(s) for >=: {type(rhs_expr).__name__!r} and" + f" {type(upper_bounded_expr).__name__!r}", + ): + upper_bounded_expr <= rhs_expr + # pylint: enable=pointless-statement + + def test_upper_bounded_expr_leq_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + upper_bounded_expr = 2.0 >= x + 2 * y + 1.0 + self.assertIsInstance( + upper_bounded_expr, bounded_expressions.UpperBoundedExpression + ) + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, + "'<=' not supported between instances of" + f" {type(upper_bounded_expr).__name__!r} and 'float'", + ): + upper_bounded_expr <= 2.0 + # pylint: enable=pointless-statement + + def test_bounded_expr_leq_expr(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + rhs_expr = 1.0 + x + bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + # pylint: disable=pointless-statement + with self.assertRaisesRegex( + TypeError, + "unsupported operand.*\n.*two or more non-constant linear expressions", + ): + bounded_expr <= rhs_expr + # pylint: enable=pointless-statement + + def test_bounded_expr_leq_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, + "'<=' not supported between instances of" + f" {type(bounded_expr).__name__!r} and 'float'", + ): + bounded_expr <= 2.0 + # pylint: enable=pointless-statement + + def test_bounded_expr_geq_expr(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + rhs_expr = 1.0 + x + bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + # pylint: disable=pointless-statement + with self.assertRaisesRegex( + TypeError, + "unsupported operand.*\n.*two or more non-constant linear expressions", + ): + bounded_expr >= rhs_expr + # pylint: enable=pointless-statement + + def test_bounded_expr_geq_float(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + bounded_expr = (0.0 <= x + 2 * y + 1.0) <= 2.0 + self.assertIsInstance(bounded_expr, bounded_expressions.BoundedExpression) + # pylint: disable=pointless-statement + with self.assertRaisesWithLiteralMatch( + TypeError, + "'>=' not supported between instances of" + f" {type(bounded_expr).__name__!r} and 'float'", + ): + bounded_expr >= 2.0 + # pylint: enable=pointless-statement def _assert_any_bounded_quad_equals( @@ -624,21 +632,23 @@ def _assert_any_bounded_quad_equals( ub: float = math.inf, offset: float = 0.0, lin: Optional[Dict[variables.Variable, float]] = None, - quad: Optional[Dict[Tuple[variables.Variable, variables.Variable], float]] = None, + quad: Optional[ + Dict[Tuple[variables.Variable, variables.Variable], float] + ] = None, ) -> None: - test_case.assertIsInstance(actual.expression, variables.QuadraticBase) - lin = lin or {} - quad = quad or {} - test_case.assertEqual(actual.lower_bound, lb) - test_case.assertEqual(actual.upper_bound, ub) - actual_quad = variables.as_flat_quadratic_expression(actual.expression) - test_case.assertEqual(actual_quad.offset, offset) - test_case.assertDictEqual(dict(actual_quad.linear_terms), lin) - actual_quad_terms = { - (key.first_var, key.second_var): coef - for (key, coef) in actual_quad.quadratic_terms.items() - } - test_case.assertDictEqual(actual_quad_terms, quad) + test_case.assertIsInstance(actual.expression, variables.QuadraticBase) + lin = lin or {} + quad = quad or {} + test_case.assertEqual(actual.lower_bound, lb) + test_case.assertEqual(actual.upper_bound, ub) + actual_quad = variables.as_flat_quadratic_expression(actual.expression) + test_case.assertEqual(actual_quad.offset, offset) + test_case.assertDictEqual(dict(actual_quad.linear_terms), lin) + actual_quad_terms = { + (key.first_var, key.second_var): coef + for (key, coef) in actual_quad.quadratic_terms.items() + } + test_case.assertDictEqual(actual_quad_terms, quad) # TODO(b/73944659): We do not want to accept bool, but pytype cannot do the @@ -651,12 +661,14 @@ def _assert_bounded_quad_equals( ub: float = math.inf, offset: float = 0.0, lin: Optional[Dict[variables.Variable, float]] = None, - quad: Optional[Dict[Tuple[variables.Variable, variables.Variable], float]] = None, + quad: Optional[ + Dict[Tuple[variables.Variable, variables.Variable], float] + ] = None, ) -> None: - test_case.assertIsInstance(actual, bounded_expressions.BoundedExpression) - _assert_any_bounded_quad_equals( - test_case, actual, lb=lb, ub=ub, offset=offset, lin=lin, quad=quad - ) + test_case.assertIsInstance(actual, bounded_expressions.BoundedExpression) + _assert_any_bounded_quad_equals( + test_case, actual, lb=lb, ub=ub, offset=offset, lin=lin, quad=quad + ) # TODO(b/73944659): We do not want to accept bool, but pytype cannot do the @@ -668,12 +680,14 @@ def _assert_lower_bounded_quad_equals( lb: float = -math.inf, offset: float = 0.0, lin: Optional[Dict[variables.Variable, float]] = None, - quad: Optional[Dict[Tuple[variables.Variable, variables.Variable], float]] = None, + quad: Optional[ + Dict[Tuple[variables.Variable, variables.Variable], float] + ] = None, ) -> None: - test_case.assertIsInstance(actual, bounded_expressions.LowerBoundedExpression) - _assert_any_bounded_quad_equals( - test_case, actual, lb=lb, ub=math.inf, offset=offset, lin=lin, quad=quad - ) + test_case.assertIsInstance(actual, bounded_expressions.LowerBoundedExpression) + _assert_any_bounded_quad_equals( + test_case, actual, lb=lb, ub=math.inf, offset=offset, lin=lin, quad=quad + ) # TODO(b/73944659): We do not want to accept bool, but pytype cannot do the @@ -685,684 +699,694 @@ def _assert_upper_bounded_quad_equals( ub: float = math.inf, offset: float = 0.0, lin: Optional[Dict[variables.Variable, float]] = None, - quad: Optional[Dict[Tuple[variables.Variable, variables.Variable], float]] = None, + quad: Optional[ + Dict[Tuple[variables.Variable, variables.Variable], float] + ] = None, ) -> None: - test_case.assertIsInstance(actual, bounded_expressions.UpperBoundedExpression) - _assert_any_bounded_quad_equals( - test_case, actual, lb=-math.inf, ub=ub, offset=offset, lin=lin, quad=quad - ) + test_case.assertIsInstance(actual, bounded_expressions.UpperBoundedExpression) + _assert_any_bounded_quad_equals( + test_case, actual, lb=-math.inf, ub=ub, offset=offset, lin=lin, quad=quad + ) class BoundedQuadraticExpressionTest(absltest.TestCase): - """Tests the creation of bounded quadratic expressions by operators.""" - - def test_quad_eq_float(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 5.0 * x * x == 3.0 - _assert_bounded_quad_equals(self, bounded, lb=3.0, ub=3.0, quad={(x, x): 5.0}) - - def test_float_eq_quad(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 3.0 == 5.0 * x * x - _assert_bounded_quad_equals(self, bounded, lb=3.0, ub=3.0, quad={(x, x): 5.0}) - - def test_quad_eq_lin(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 5.0 * x * x == x - _assert_bounded_quad_equals( - self, bounded, lb=0.0, ub=0.0, lin={x: -1.0}, quad={(x, x): 5.0} - ) + """Tests the creation of bounded quadratic expressions by operators.""" + + def test_quad_eq_float(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 5.0 * x * x == 3.0 + _assert_bounded_quad_equals( + self, bounded, lb=3.0, ub=3.0, quad={(x, x): 5.0} + ) - def test_lin_eq_quad(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = x == 5.0 * x * x - _assert_bounded_quad_equals( - self, bounded, lb=0.0, ub=0.0, lin={x: -1.0}, quad={(x, x): 5.0} - ) + def test_float_eq_quad(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 3.0 == 5.0 * x * x + _assert_bounded_quad_equals( + self, bounded, lb=3.0, ub=3.0, quad={(x, x): 5.0} + ) - def test_quad_eq_str_raises_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, "==.*str"): - 5.0 * x * x == "hello" # pylint: disable=pointless-statement - - def test_quad_ne_raises_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, "!= constraints"): - x * x != 1.0 # pylint: disable=pointless-statement - - def test_quad_le_float(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 5.0 * x * x <= 3.0 - _assert_upper_bounded_quad_equals(self, bounded, ub=3.0, quad={(x, x): 5.0}) - - def test_float_ge_quad(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 3.0 >= 5.0 * x * x - _assert_upper_bounded_quad_equals(self, bounded, ub=3.0, quad={(x, x): 5.0}) - - def test_quad_le_lin(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 5.0 * x * x <= x + 2.0 - _assert_bounded_quad_equals( - self, bounded, ub=0.0, quad={(x, x): 5.0}, lin={x: -1}, offset=-2.0 - ) + def test_quad_eq_lin(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 5.0 * x * x == x + _assert_bounded_quad_equals( + self, bounded, lb=0.0, ub=0.0, lin={x: -1.0}, quad={(x, x): 5.0} + ) - def test_lin_ge_quad(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = x + 2.0 >= 5.0 * x * x - _assert_bounded_quad_equals( - self, bounded, ub=0.0, quad={(x, x): 5.0}, lin={x: -1}, offset=-2.0 - ) + def test_lin_eq_quad(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = x == 5.0 * x * x + _assert_bounded_quad_equals( + self, bounded, lb=0.0, ub=0.0, lin={x: -1.0}, quad={(x, x): 5.0} + ) - def test_quad_le_str_raises_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, "<=.*str"): - 5.0 * x * x <= "test" # pylint: disable=pointless-statement - - def test_quad_ge_float(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 5.0 * x * x >= 3.0 - _assert_lower_bounded_quad_equals(self, bounded, lb=3.0, quad={(x, x): 5.0}) - - def test_float_le_quad(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 3.0 <= 5.0 * x * x - _assert_lower_bounded_quad_equals(self, bounded, lb=3.0, quad={(x, x): 5.0}) - - def test_quad_ge_lin(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 5.0 * x * x >= x + 2.0 - _assert_bounded_quad_equals( - self, bounded, lb=0.0, quad={(x, x): 5.0}, lin={x: -1}, offset=-2.0 - ) + def test_quad_eq_str_raises_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, "==.*str"): + 5.0 * x * x == "hello" # pylint: disable=pointless-statement + + def test_quad_ne_raises_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, "!= constraints"): + x * x != 1.0 # pylint: disable=pointless-statement + + def test_quad_le_float(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 5.0 * x * x <= 3.0 + _assert_upper_bounded_quad_equals(self, bounded, ub=3.0, quad={(x, x): 5.0}) + + def test_float_ge_quad(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 3.0 >= 5.0 * x * x + _assert_upper_bounded_quad_equals(self, bounded, ub=3.0, quad={(x, x): 5.0}) + + def test_quad_le_lin(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 5.0 * x * x <= x + 2.0 + _assert_bounded_quad_equals( + self, bounded, ub=0.0, quad={(x, x): 5.0}, lin={x: -1}, offset=-2.0 + ) - def test_lin_le_quad(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = x + 2.0 <= 5.0 * x * x - _assert_bounded_quad_equals( - self, bounded, lb=0.0, quad={(x, x): 5.0}, lin={x: -1}, offset=-2.0 - ) + def test_lin_ge_quad(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = x + 2.0 >= 5.0 * x * x + _assert_bounded_quad_equals( + self, bounded, ub=0.0, quad={(x, x): 5.0}, lin={x: -1}, offset=-2.0 + ) - def test_quad_ge_str_raises_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, ">=.*str"): - 5.0 * x * x >= "test" # pylint: disable=pointless-statement - - def test_ge_twice(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = (1.0 <= 5.0 * x * x) <= 2.0 - _assert_bounded_quad_equals( - self, - bounded, - lb=1.0, - ub=2.0, - quad={(x, x): 5.0}, - ) + def test_quad_le_str_raises_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, "<=.*str"): + 5.0 * x * x <= "test" # pylint: disable=pointless-statement + + def test_quad_ge_float(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 5.0 * x * x >= 3.0 + _assert_lower_bounded_quad_equals(self, bounded, lb=3.0, quad={(x, x): 5.0}) + + def test_float_le_quad(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 3.0 <= 5.0 * x * x + _assert_lower_bounded_quad_equals(self, bounded, lb=3.0, quad={(x, x): 5.0}) + + def test_quad_ge_lin(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 5.0 * x * x >= x + 2.0 + _assert_bounded_quad_equals( + self, bounded, lb=0.0, quad={(x, x): 5.0}, lin={x: -1}, offset=-2.0 + ) - def test_ge_twice_fails_when_ambiguous(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, "'BoundedExpression' and 'float'"): - (x <= 5.0 * x * x) <= 2.0 # pylint: disable=pointless-statement - - def test_no_quad_ge_bounded_expr(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, r"\(a <= b\) <= c"): - x * x >= (x == 5.0) # pylint: disable=pointless-statement - - def test_le_twice(self) -> None: - mod = model.Model() - x = mod.add_variable() - bounded = 1.0 <= (5.0 * x * x <= 2.0) - _assert_bounded_quad_equals( - self, - bounded, - lb=1.0, - ub=2.0, - quad={(x, x): 5.0}, - ) + def test_lin_le_quad(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = x + 2.0 <= 5.0 * x * x + _assert_bounded_quad_equals( + self, bounded, lb=0.0, quad={(x, x): 5.0}, lin={x: -1}, offset=-2.0 + ) - def test_le_twice_fails_when_ambiguous(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, "'BoundedExpression' and 'float'"): - (x >= 5.0 * x * x) >= 2.0 # pylint: disable=pointless-statement + def test_quad_ge_str_raises_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, ">=.*str"): + 5.0 * x * x >= "test" # pylint: disable=pointless-statement + + def test_ge_twice(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = (1.0 <= 5.0 * x * x) <= 2.0 + _assert_bounded_quad_equals( + self, + bounded, + lb=1.0, + ub=2.0, + quad={(x, x): 5.0}, + ) + + def test_ge_twice_fails_when_ambiguous(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, "'BoundedExpression' and 'float'"): + (x <= 5.0 * x * x) <= 2.0 # pylint: disable=pointless-statement + + def test_no_quad_ge_bounded_expr(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, r"\(a <= b\) <= c"): + x * x >= (x == 5.0) # pylint: disable=pointless-statement + + def test_le_twice(self) -> None: + mod = model.Model() + x = mod.add_variable() + bounded = 1.0 <= (5.0 * x * x <= 2.0) + _assert_bounded_quad_equals( + self, + bounded, + lb=1.0, + ub=2.0, + quad={(x, x): 5.0}, + ) + + def test_le_twice_fails_when_ambiguous(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, "'BoundedExpression' and 'float'"): + (x >= 5.0 * x * x) >= 2.0 # pylint: disable=pointless-statement - def test_no_quad_le_bounded_expr(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, r"\(a <= b\) <= c"): - x * x <= (x == 5.0) # pylint: disable=pointless-statement + def test_no_quad_le_bounded_expr(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, r"\(a <= b\) <= c"): + x * x <= (x == 5.0) # pylint: disable=pointless-statement # TODO(b/216492143): change __str__ to match C++ implementation in cl/421649402. class LinearStrAndReprTest(parameterized.TestCase): - def test_sorting_ok(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - zero_plus_var_plus_pos_term = 0 + x + 2 * y - self.assertEqual( - repr(zero_plus_var_plus_pos_term), - f'LinearSum((LinearSum((0, )), ' - f'LinearTerm(, 2)))', - ) - # This fails if we don't sort by variable names in Variable.__str__(). - self.assertEqual(str(zero_plus_var_plus_pos_term), "0.0 + 1.0 * x + 2.0 * y") - - def test_simple_expressions(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - - var_plus_pos_term = x + 2 * y - self.assertEqual( - repr(var_plus_pos_term), - f'LinearSum((, ' - f'LinearTerm(, 2)))', - ) - self.assertEqual(str(var_plus_pos_term), "0.0 + 1.0 * x + 2.0 * y") + def test_sorting_ok(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + zero_plus_var_plus_pos_term = 0 + x + 2 * y + self.assertEqual( + repr(zero_plus_var_plus_pos_term), + f'LinearSum((LinearSum((0, )), ' + f'LinearTerm(, 2)))', + ) + # This fails if we don't sort by variable names in Variable.__str__(). + self.assertEqual( + str(zero_plus_var_plus_pos_term), "0.0 + 1.0 * x + 2.0 * y" + ) - var_plus_neg_term = x - 3 * y - self.assertEqual( - repr(var_plus_neg_term), - f'LinearSum((, ' - f'LinearTerm(, -3)))', - ) - self.assertEqual(str(var_plus_neg_term), "0.0 + 1.0 * x - 3.0 * y") + def test_simple_expressions(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") - var_plus_num = x + 1.0 - self.assertEqual( - repr(var_plus_num), f'LinearSum((, 1.0))' - ) - self.assertEqual(str(var_plus_num), "1.0 + 1.0 * x") + var_plus_pos_term = x + 2 * y + self.assertEqual( + repr(var_plus_pos_term), + f'LinearSum((, ' + f'LinearTerm(, 2)))', + ) + self.assertEqual(str(var_plus_pos_term), "0.0 + 1.0 * x + 2.0 * y") - num_times_var_sum = 2 * (x + y + 3) - self.assertEqual( - repr(num_times_var_sum), - "LinearProduct(2.0, LinearSum((LinearSum((, )), 3)))', - ) - self.assertEqual(str(num_times_var_sum), "6.0 + 2.0 * x + 2.0 * y") - self.assertEqual( - repr(variables.as_flat_linear_expression(num_times_var_sum)), - f'LinearExpression(6.0, {"{"!s}' - f': 2.0, ' - f': 2.0{"}"!s})', - ) - self.assertEqual( - str(variables.as_flat_linear_expression(num_times_var_sum)), - "6.0 + 2.0 * x + 2.0 * y", - ) + var_plus_neg_term = x - 3 * y + self.assertEqual( + repr(var_plus_neg_term), + f'LinearSum((, ' + f'LinearTerm(, -3)))', + ) + self.assertEqual(str(var_plus_neg_term), "0.0 + 1.0 * x - 3.0 * y") - linear_term = 2 * x - self.assertEqual( - repr(linear_term), f'LinearTerm(, 2)' - ) - self.assertEqual(str(linear_term), "2 * x") - - def test_sum_expressions(self) -> None: - mod = model.Model() - x = [mod.add_binary_variable(name=f"x{i}") for i in range(3)] - - linear_sum = variables.LinearSum(i * x[i] for i in range(3)) - self.assertEqual( - repr(linear_sum), - "LinearSum((" - f'LinearTerm(, 0), ' - f'LinearTerm(, 1), ' - f'LinearTerm(, 2)))', - ) - self.assertEqual(str(linear_sum), "0.0 + 1.0 * x1 + 2.0 * x2") - - python_sum = sum(i * x[i] for i in range(3)) - self.assertEqual( - repr(python_sum), - "LinearSum((" - "LinearSum((" - f'LinearSum((0, LinearTerm(, 0))), ' - f'LinearTerm(, 1))), ' - f'LinearTerm(, 2)))', - ) - self.assertEqual(str(python_sum), "0.0 + 1.0 * x1 + 2.0 * x2") + var_plus_num = x + 1.0 + self.assertEqual( + repr(var_plus_num), f'LinearSum((, 1.0))' + ) + self.assertEqual(str(var_plus_num), "1.0 + 1.0 * x") + + num_times_var_sum = 2 * (x + y + 3) + self.assertEqual( + repr(num_times_var_sum), + "LinearProduct(2.0, LinearSum((LinearSum((, )), 3)))", + ) + self.assertEqual(str(num_times_var_sum), "6.0 + 2.0 * x + 2.0 * y") + self.assertEqual( + repr(variables.as_flat_linear_expression(num_times_var_sum)), + f'LinearExpression(6.0, {"{"!s}' + f': 2.0, ' + f': 2.0{"}"!s})', + ) + self.assertEqual( + str(variables.as_flat_linear_expression(num_times_var_sum)), + "6.0 + 2.0 * x + 2.0 * y", + ) + + linear_term = 2 * x + self.assertEqual( + repr(linear_term), f'LinearTerm(, 2)' + ) + self.assertEqual(str(linear_term), "2 * x") + + def test_sum_expressions(self) -> None: + mod = model.Model() + x = [mod.add_binary_variable(name=f"x{i}") for i in range(3)] + + linear_sum = variables.LinearSum(i * x[i] for i in range(3)) + self.assertEqual( + repr(linear_sum), + "LinearSum((" + f"LinearTerm(, 0), " + f"LinearTerm(, 1), " + f"LinearTerm(, 2)))", + ) + self.assertEqual(str(linear_sum), "0.0 + 1.0 * x1 + 2.0 * x2") + + python_sum = sum(i * x[i] for i in range(3)) + self.assertEqual( + repr(python_sum), + "LinearSum((" + "LinearSum((" + f"LinearSum((0, LinearTerm(, 0))), " + f"LinearTerm(, 1))), " + f"LinearTerm(, 2)))", + ) + self.assertEqual(str(python_sum), "0.0 + 1.0 * x1 + 2.0 * x2") # TODO(b/216492143): change __str__ to match C++ implementation in cl/421649402. class QuadraticStrAndReprTest(parameterized.TestCase): - def test_sorting_ok(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - zero_plus_var_plus_pos_term = 0 + x + 2 * y + x * x - self.assertEqual( - repr(zero_plus_var_plus_pos_term), - "QuadraticSum((" - f'LinearSum((LinearSum((0, )), ' - f'LinearTerm(, 2))), ' - f'QuadraticTerm(QuadraticTermKey(, ' - f'), 1.0)))', - ) - # This fails if we don't sort by variable names in Variable.__str__(). - self.assertEqual( - str(zero_plus_var_plus_pos_term), - "0.0 + 1.0 * x + 2.0 * y + 1.0 * x * x", - ) + def test_sorting_ok(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + zero_plus_var_plus_pos_term = 0 + x + 2 * y + x * x + self.assertEqual( + repr(zero_plus_var_plus_pos_term), + "QuadraticSum((" + f"LinearSum((LinearSum((0, )), " + f"LinearTerm(, 2))), " + f"QuadraticTerm(QuadraticTermKey(, " + f"), 1.0)))", + ) + # This fails if we don't sort by variable names in Variable.__str__(). + self.assertEqual( + str(zero_plus_var_plus_pos_term), + "0.0 + 1.0 * x + 2.0 * y + 1.0 * x * x", + ) - def test_simple_expressions(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - - var_plus_pos_term = x + 2 * y * y - self.assertEqual( - repr(var_plus_pos_term), - f'QuadraticSum((, ' - f'QuadraticTerm(QuadraticTermKey(, ' - f'), 2)))', - ) - self.assertEqual(str(var_plus_pos_term), "0.0 + 1.0 * x + 2.0 * y * y") - - var_plus_neg_term = x - 3 * y * y - self.assertEqual( - repr(var_plus_neg_term), - f'QuadraticSum((, ' - f'QuadraticTerm(QuadraticTermKey(, ' - f'), -3)))', - ) - self.assertEqual(str(var_plus_neg_term), "0.0 + 1.0 * x - 3.0 * y * y") - - num_times_term_sum = 2 * (x * x + y + 3) - self.assertEqual( - repr(num_times_term_sum), - "QuadraticProduct(2.0," - " QuadraticSum((QuadraticSum((QuadraticTerm(QuadraticTermKey(, ), 1.0),' - f' )), 3)))', - ) - self.assertEqual(str(num_times_term_sum), "6.0 + 2.0 * y + 2.0 * x * x") - self.assertEqual( - repr(variables.as_flat_quadratic_expression(num_times_term_sum)), - f'QuadraticExpression(6.0, {"{"!s}' - f': 2.0{"}"!s}, ' - f'{"{"!s}QuadraticTermKey(, ' - f'): 2.0{"}"!s})', - ) - self.assertEqual( - str(variables.as_flat_quadratic_expression(num_times_term_sum)), - "6.0 + 2.0 * y + 2.0 * x * x", - ) + def test_simple_expressions(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + + var_plus_pos_term = x + 2 * y * y + self.assertEqual( + repr(var_plus_pos_term), + f'QuadraticSum((, ' + f'QuadraticTerm(QuadraticTermKey(, ' + f'), 2)))', + ) + self.assertEqual(str(var_plus_pos_term), "0.0 + 1.0 * x + 2.0 * y * y") + + var_plus_neg_term = x - 3 * y * y + self.assertEqual( + repr(var_plus_neg_term), + f'QuadraticSum((, ' + f'QuadraticTerm(QuadraticTermKey(, ' + f'), -3)))', + ) + self.assertEqual(str(var_plus_neg_term), "0.0 + 1.0 * x - 3.0 * y * y") + + num_times_term_sum = 2 * (x * x + y + 3) + self.assertEqual( + repr(num_times_term_sum), + "QuadraticProduct(2.0," + " QuadraticSum((QuadraticSum((QuadraticTerm(QuadraticTermKey(, ), 1.0)," + f" )), 3)))", + ) + self.assertEqual(str(num_times_term_sum), "6.0 + 2.0 * y + 2.0 * x * x") + self.assertEqual( + repr(variables.as_flat_quadratic_expression(num_times_term_sum)), + f'QuadraticExpression(6.0, {"{"!s}' + f': 2.0{"}"!s}, ' + f'{"{"!s}QuadraticTermKey(, ' + f'): 2.0{"}"!s})', + ) + self.assertEqual( + str(variables.as_flat_quadratic_expression(num_times_term_sum)), + "6.0 + 2.0 * y + 2.0 * x * x", + ) - linear_times_linear = (2 * x) * (1 + y) - self.assertEqual( - repr(linear_times_linear), - f'LinearLinearProduct(LinearTerm(, 2), ' - f'LinearSum((1, )))', - ) - self.assertEqual(str(linear_times_linear), "0.0 + 2.0 * x + 2.0 * x * y") + linear_times_linear = (2 * x) * (1 + y) + self.assertEqual( + repr(linear_times_linear), + f'LinearLinearProduct(LinearTerm(, 2), ' + f'LinearSum((1, )))', + ) + self.assertEqual(str(linear_times_linear), "0.0 + 2.0 * x + 2.0 * x * y") - quadratic_term = 2 * x * x - self.assertEqual( - repr(quadratic_term), - "QuadraticTerm(QuadraticTermKey(, ), 2)', - ) - self.assertEqual(str(quadratic_term), "2 * x * x") - - def test_sum_expressions(self) -> None: - mod = model.Model() - x = [mod.add_binary_variable(name=f"x{i}") for i in range(3)] - - quadratic_sum = variables.QuadraticSum(i * x[i] * x[i] for i in range(3)) - self.assertEqual( - repr(quadratic_sum), - "QuadraticSum((" - f'QuadraticTerm(QuadraticTermKey(, ' - f'), 0), ' - f'QuadraticTerm(QuadraticTermKey(, ' - f'), 1), ' - f'QuadraticTerm(QuadraticTermKey(, ' - f'), 2)))', - ) - self.assertEqual(str(quadratic_sum), "0.0 + 1.0 * x1 * x1 + 2.0 * x2 * x2") - - python_sum = sum(i * x[i] * x[i] for i in range(3)) - self.assertEqual( - repr(python_sum), - "QuadraticSum((" - "QuadraticSum((" - "QuadraticSum((0, QuadraticTerm(QuadraticTermKey(, ), 0))), ' - f'QuadraticTerm(QuadraticTermKey(, ' - f'), 1))), ' - f'QuadraticTerm(QuadraticTermKey(, ' - f'), 2)))', - ) - self.assertEqual(str(python_sum), "0.0 + 1.0 * x1 * x1 + 2.0 * x2 * x2") + quadratic_term = 2 * x * x + self.assertEqual( + repr(quadratic_term), + "QuadraticTerm(QuadraticTermKey(, ), 2)", + ) + self.assertEqual(str(quadratic_term), "2 * x * x") + + def test_sum_expressions(self) -> None: + mod = model.Model() + x = [mod.add_binary_variable(name=f"x{i}") for i in range(3)] + + quadratic_sum = variables.QuadraticSum(i * x[i] * x[i] for i in range(3)) + self.assertEqual( + repr(quadratic_sum), + "QuadraticSum((" + f"QuadraticTerm(QuadraticTermKey(, " + f"), 0), " + f"QuadraticTerm(QuadraticTermKey(, " + f"), 1), " + f"QuadraticTerm(QuadraticTermKey(, " + f"), 2)))", + ) + self.assertEqual(str(quadratic_sum), "0.0 + 1.0 * x1 * x1 + 2.0 * x2 * x2") + + python_sum = sum(i * x[i] * x[i] for i in range(3)) + self.assertEqual( + repr(python_sum), + "QuadraticSum((" + "QuadraticSum((" + "QuadraticSum((0, QuadraticTerm(QuadraticTermKey(, ), 0))), " + f"QuadraticTerm(QuadraticTermKey(, " + f"), 1))), " + f"QuadraticTerm(QuadraticTermKey(, " + f"), 2)))", + ) + self.assertEqual(str(python_sum), "0.0 + 1.0 * x1 * x1 + 2.0 * x2 * x2") class LinearNumberOpTestsParameters(NamedTuple): - linear_type: str - constant: Union[float, int] - linear_first: bool + linear_type: str + constant: Union[float, int] + linear_first: bool - def test_suffix(self): - if self.linear_first: - return f"_{self.linear_type}_{type(self.constant).__name__}" - else: - return f"_{type(self.constant).__name__}_{self.linear_type}" + def test_suffix(self): + if self.linear_first: + return f"_{self.linear_type}_{type(self.constant).__name__}" + else: + return f"_{type(self.constant).__name__}_{self.linear_type}" def all_linear_number_op_parameters() -> List[LinearNumberOpTestsParameters]: - result = [] - for t in _LINEAR_TYPES: - for c in (2, 0.25): - for first in (True, False): - result.append( - LinearNumberOpTestsParameters( - linear_type=t, constant=c, linear_first=first - ) - ) - return result + result = [] + for t in _LINEAR_TYPES: + for c in (2, 0.25): + for first in (True, False): + result.append( + LinearNumberOpTestsParameters( + linear_type=t, constant=c, linear_first=first + ) + ) + return result # Test all operations (including inplace) between a number and a Linear object class LinearNumberOpTests(parameterized.TestCase): - @parameterized.named_parameters( - (p.test_suffix(), p.linear_type, p.constant, p.linear_first) - for p in all_linear_number_op_parameters() - ) - def test_mult( - self, linear_type: str, constant: Union[float, int], linear_first: bool - ) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - if linear_type == "Variable": - linear = x - expected_type = variables.LinearTerm - expected_offset = 0 - expected_terms = {x: constant} - elif linear_type == "LinearTerm": - linear = variables.LinearTerm(x, 2) - expected_type = variables.LinearTerm - expected_offset = 0 - expected_terms = {x: 2 * constant} - elif linear_type == "LinearExpression": - linear = variables.LinearExpression(x - 2 * y + 3) - expected_type = variables.LinearProduct - expected_offset = 3 * constant - expected_terms = {x: constant, y: -2 * constant} - elif linear_type == "LinearSum": - linear = variables.LinearSum((x, -2 * y, 3)) - expected_type = variables.LinearProduct - expected_offset = 3 * constant - expected_terms = {x: constant, y: -2 * constant} - elif linear_type == "LinearProduct": - linear = variables.LinearProduct(2, x) - expected_type = variables.LinearProduct - expected_offset = 0 - expected_terms = {x: 2 * constant} - else: - raise AssertionError(f"unknown linear type: {linear_type!r}") - - # Check __mul__ and __rmul__ - s = linear * constant if linear_first else constant * linear - e = variables.as_flat_linear_expression(s) - self.assertIsInstance(s, expected_type) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqual(dict(e.terms), expected_terms) - - # Also check __imul__ - if linear_first: - expr = linear - expr *= constant - else: - expr = constant - expr *= linear - e_inplace = variables.as_flat_linear_expression(expr) - self.assertIsInstance(expr, expected_type) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqual(dict(e_inplace.terms), expected_terms) - - @parameterized.named_parameters( - (p.test_suffix(), p.linear_type, p.constant, p.linear_first) - for p in all_linear_number_op_parameters() - ) - def test_div( - self, linear_type: str, constant: Union[float, int], linear_first: bool - ) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - if linear_type == "Variable": - linear = x - expected_type = variables.LinearTerm - expected_offset = 0 - expected_terms = {x: 1 / constant} - elif linear_type == "LinearTerm": - linear = variables.LinearTerm(x, 2) - expected_type = variables.LinearTerm - expected_offset = 0 - expected_terms = {x: 2 / constant} - elif linear_type == "LinearExpression": - linear = variables.LinearExpression(x - 2 * y + 3) - expected_type = variables.LinearProduct - expected_offset = 3 / constant - expected_terms = {x: 1 / constant, y: -2 / constant} - elif linear_type == "LinearSum": - linear = variables.LinearSum((x, -2 * y, 3)) - expected_type = variables.LinearProduct - expected_offset = 3 / constant - expected_terms = {x: 1 / constant, y: -2 / constant} - elif linear_type == "LinearProduct": - linear = variables.LinearProduct(2, x) - expected_type = variables.LinearProduct - expected_offset = 0 - expected_terms = {x: 2 / constant} - else: - raise AssertionError(f"unknown linear type: {linear_type!r}") - - # Check __truediv__ - if linear_first: - s = linear / constant - e = variables.as_flat_linear_expression(s) - self.assertIsInstance(s, expected_type) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqual(dict(e.terms), expected_terms) - else: - with self.assertRaisesWithLiteralMatch( - TypeError, - f"unsupported operand type(s) for /: {type(constant).__name__!r} " - f"and {type(linear).__name__!r}", - ): - s = constant / linear # pytype: disable=unsupported-operands - - # Also check __itruediv__ - if linear_first: - linear /= constant - e_inplace = variables.as_flat_linear_expression(linear) - self.assertIsInstance(linear, expected_type) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqual(dict(e_inplace.terms), expected_terms) - else: - with self.assertRaisesWithLiteralMatch( - TypeError, - f"unsupported operand type(s) for /=: {type(constant).__name__!r} " - f"and {type(linear).__name__!r}", - ): - expr = constant - expr /= linear # pytype: disable=unsupported-operands - - @parameterized.named_parameters( - (p.test_suffix(), p.linear_type, p.constant, p.linear_first) - for p in all_linear_number_op_parameters() - ) - def test_add( - self, linear_type: str, constant: Union[float, int], linear_first: bool - ) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - if linear_type == "Variable": - linear = x - expected_offset = constant - expected_terms = {x: 1} - elif linear_type == "LinearTerm": - linear = variables.LinearTerm(x, 2) - expected_offset = constant - expected_terms = {x: 2} - elif linear_type == "LinearExpression": - linear = variables.LinearExpression(x - 2 * y + 1) - expected_offset = constant + 1 - expected_terms = {x: 1, y: -2} - elif linear_type == "LinearSum": - linear = variables.LinearSum((x, -2 * y, 1)) - expected_offset = constant + 1 - expected_terms = {x: 1, y: -2} - elif linear_type == "LinearProduct": - linear = variables.LinearProduct(2, x) - expected_offset = constant - expected_terms = {x: 2} - else: - raise AssertionError(f"unknown linear type: {linear_type!r}") - - # Check __add__ and __radd__ - s = linear + constant if linear_first else constant + linear - e = variables.as_flat_linear_expression(s) - self.assertIsInstance(s, variables.LinearSum) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqual(dict(e.terms), expected_terms) - - # Also check __iadd__ - if linear_first: - expr = linear - expr += constant - else: - expr = constant - expr += linear - e_inplace = variables.as_flat_linear_expression(expr) - self.assertIsInstance(expr, variables.LinearSum) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqual(dict(e_inplace.terms), expected_terms) - - @parameterized.named_parameters( - (p.test_suffix(), p.linear_type, p.constant, p.linear_first) - for p in all_linear_number_op_parameters() - ) - def test_sub( - self, linear_type: str, constant: Union[float, int], linear_first: bool - ) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - sign = 1 if linear_first else -1 - if linear_type == "Variable": - linear = x - expected_offset = -sign * constant - expected_terms = {x: sign} - elif linear_type == "LinearTerm": - linear = variables.LinearTerm(x, 2) - expected_offset = -sign * constant - expected_terms = {x: sign * 2} - elif linear_type == "LinearExpression": - linear = variables.LinearExpression(x - 2 * y + 3) - expected_offset = -sign * constant + 3 * sign - expected_terms = {x: sign, y: -sign * 2} - elif linear_type == "LinearSum": - linear = variables.LinearSum((x, -2 * y, 3)) - expected_offset = -sign * constant + 3 * sign - expected_terms = {x: sign, y: -sign * 2} - elif linear_type == "LinearProduct": - linear = variables.LinearProduct(2, x) - expected_offset = -sign * constant - expected_terms = {x: sign * 2} - else: - raise AssertionError(f"unknown linear type: {linear_type!r}") - - # Check __sub__ and __rsub__ - s = linear - constant if linear_first else constant - linear - e = variables.as_flat_linear_expression(s) - self.assertIsInstance(s, variables.LinearSum) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqual(dict(e.terms), expected_terms) - - # Also check __isub__ - if linear_first: - linear -= constant - e_inplace = variables.as_flat_linear_expression(linear) - self.assertIsInstance(linear, variables.LinearSum) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqual(dict(e_inplace.terms), expected_terms) + @parameterized.named_parameters( + (p.test_suffix(), p.linear_type, p.constant, p.linear_first) + for p in all_linear_number_op_parameters() + ) + def test_mult( + self, linear_type: str, constant: Union[float, int], linear_first: bool + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + if linear_type == "Variable": + linear = x + expected_type = variables.LinearTerm + expected_offset = 0 + expected_terms = {x: constant} + elif linear_type == "LinearTerm": + linear = variables.LinearTerm(x, 2) + expected_type = variables.LinearTerm + expected_offset = 0 + expected_terms = {x: 2 * constant} + elif linear_type == "LinearExpression": + linear = variables.LinearExpression(x - 2 * y + 3) + expected_type = variables.LinearProduct + expected_offset = 3 * constant + expected_terms = {x: constant, y: -2 * constant} + elif linear_type == "LinearSum": + linear = variables.LinearSum((x, -2 * y, 3)) + expected_type = variables.LinearProduct + expected_offset = 3 * constant + expected_terms = {x: constant, y: -2 * constant} + elif linear_type == "LinearProduct": + linear = variables.LinearProduct(2, x) + expected_type = variables.LinearProduct + expected_offset = 0 + expected_terms = {x: 2 * constant} + else: + raise AssertionError(f"unknown linear type: {linear_type!r}") + + # Check __mul__ and __rmul__ + s = linear * constant if linear_first else constant * linear + e = variables.as_flat_linear_expression(s) + self.assertIsInstance(s, expected_type) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqual(dict(e.terms), expected_terms) + + # Also check __imul__ + if linear_first: + expr = linear + expr *= constant + else: + expr = constant + expr *= linear + e_inplace = variables.as_flat_linear_expression(expr) + self.assertIsInstance(expr, expected_type) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqual(dict(e_inplace.terms), expected_terms) + + @parameterized.named_parameters( + (p.test_suffix(), p.linear_type, p.constant, p.linear_first) + for p in all_linear_number_op_parameters() + ) + def test_div( + self, linear_type: str, constant: Union[float, int], linear_first: bool + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + if linear_type == "Variable": + linear = x + expected_type = variables.LinearTerm + expected_offset = 0 + expected_terms = {x: 1 / constant} + elif linear_type == "LinearTerm": + linear = variables.LinearTerm(x, 2) + expected_type = variables.LinearTerm + expected_offset = 0 + expected_terms = {x: 2 / constant} + elif linear_type == "LinearExpression": + linear = variables.LinearExpression(x - 2 * y + 3) + expected_type = variables.LinearProduct + expected_offset = 3 / constant + expected_terms = {x: 1 / constant, y: -2 / constant} + elif linear_type == "LinearSum": + linear = variables.LinearSum((x, -2 * y, 3)) + expected_type = variables.LinearProduct + expected_offset = 3 / constant + expected_terms = {x: 1 / constant, y: -2 / constant} + elif linear_type == "LinearProduct": + linear = variables.LinearProduct(2, x) + expected_type = variables.LinearProduct + expected_offset = 0 + expected_terms = {x: 2 / constant} + else: + raise AssertionError(f"unknown linear type: {linear_type!r}") + + # Check __truediv__ + if linear_first: + s = linear / constant + e = variables.as_flat_linear_expression(s) + self.assertIsInstance(s, expected_type) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqual(dict(e.terms), expected_terms) + else: + with self.assertRaisesWithLiteralMatch( + TypeError, + f"unsupported operand type(s) for /: {type(constant).__name__!r} " + f"and {type(linear).__name__!r}", + ): + s = constant / linear # pytype: disable=unsupported-operands + + # Also check __itruediv__ + if linear_first: + linear /= constant + e_inplace = variables.as_flat_linear_expression(linear) + self.assertIsInstance(linear, expected_type) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqual(dict(e_inplace.terms), expected_terms) + else: + with self.assertRaisesWithLiteralMatch( + TypeError, + f"unsupported operand type(s) for /=: {type(constant).__name__!r} " + f"and {type(linear).__name__!r}", + ): + expr = constant + expr /= linear # pytype: disable=unsupported-operands + + @parameterized.named_parameters( + (p.test_suffix(), p.linear_type, p.constant, p.linear_first) + for p in all_linear_number_op_parameters() + ) + def test_add( + self, linear_type: str, constant: Union[float, int], linear_first: bool + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + if linear_type == "Variable": + linear = x + expected_offset = constant + expected_terms = {x: 1} + elif linear_type == "LinearTerm": + linear = variables.LinearTerm(x, 2) + expected_offset = constant + expected_terms = {x: 2} + elif linear_type == "LinearExpression": + linear = variables.LinearExpression(x - 2 * y + 1) + expected_offset = constant + 1 + expected_terms = {x: 1, y: -2} + elif linear_type == "LinearSum": + linear = variables.LinearSum((x, -2 * y, 1)) + expected_offset = constant + 1 + expected_terms = {x: 1, y: -2} + elif linear_type == "LinearProduct": + linear = variables.LinearProduct(2, x) + expected_offset = constant + expected_terms = {x: 2} + else: + raise AssertionError(f"unknown linear type: {linear_type!r}") + + # Check __add__ and __radd__ + s = linear + constant if linear_first else constant + linear + e = variables.as_flat_linear_expression(s) + self.assertIsInstance(s, variables.LinearSum) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqual(dict(e.terms), expected_terms) + + # Also check __iadd__ + if linear_first: + expr = linear + expr += constant + else: + expr = constant + expr += linear + e_inplace = variables.as_flat_linear_expression(expr) + self.assertIsInstance(expr, variables.LinearSum) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqual(dict(e_inplace.terms), expected_terms) + + @parameterized.named_parameters( + (p.test_suffix(), p.linear_type, p.constant, p.linear_first) + for p in all_linear_number_op_parameters() + ) + def test_sub( + self, linear_type: str, constant: Union[float, int], linear_first: bool + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + sign = 1 if linear_first else -1 + if linear_type == "Variable": + linear = x + expected_offset = -sign * constant + expected_terms = {x: sign} + elif linear_type == "LinearTerm": + linear = variables.LinearTerm(x, 2) + expected_offset = -sign * constant + expected_terms = {x: sign * 2} + elif linear_type == "LinearExpression": + linear = variables.LinearExpression(x - 2 * y + 3) + expected_offset = -sign * constant + 3 * sign + expected_terms = {x: sign, y: -sign * 2} + elif linear_type == "LinearSum": + linear = variables.LinearSum((x, -2 * y, 3)) + expected_offset = -sign * constant + 3 * sign + expected_terms = {x: sign, y: -sign * 2} + elif linear_type == "LinearProduct": + linear = variables.LinearProduct(2, x) + expected_offset = -sign * constant + expected_terms = {x: sign * 2} + else: + raise AssertionError(f"unknown linear type: {linear_type!r}") + + # Check __sub__ and __rsub__ + s = linear - constant if linear_first else constant - linear + e = variables.as_flat_linear_expression(s) + self.assertIsInstance(s, variables.LinearSum) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqual(dict(e.terms), expected_terms) + + # Also check __isub__ + if linear_first: + linear -= constant + e_inplace = variables.as_flat_linear_expression(linear) + self.assertIsInstance(linear, variables.LinearSum) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqual(dict(e_inplace.terms), expected_terms) class QuadraticTermKey(absltest.TestCase): - # Mock QuadraticTermKey.__hash__ to have a collision in the dictionary lookup - # so that a correct behavior of term1 == term2 is needed to recover the - # values. For instance, if QuadraticTermKey.__eq__ only compared equality of - # the first variables in the keys, this test would fail. - @mock.patch.object(variables.QuadraticTermKey, "__hash__") - def test_var_dict(self, fixed_hash: mock.MagicMock) -> None: - fixed_hash.return_value = 111 - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - var_dict = {xx: 1, xy: 2, yy: 3} - self.assertEqual(xx.__hash__(), 111) - self.assertEqual(xy.__hash__(), 111) - self.assertEqual(yy.__hash__(), 111) - self.assertEqual(var_dict[xx], 1) - self.assertEqual(var_dict[xy], 2) - self.assertEqual(var_dict[yy], 3) + # Mock QuadraticTermKey.__hash__ to have a collision in the dictionary lookup + # so that a correct behavior of term1 == term2 is needed to recover the + # values. For instance, if QuadraticTermKey.__eq__ only compared equality of + # the first variables in the keys, this test would fail. + @mock.patch.object(variables.QuadraticTermKey, "__hash__") + def test_var_dict(self, fixed_hash: mock.MagicMock) -> None: + fixed_hash.return_value = 111 + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + var_dict = {xx: 1, xy: 2, yy: 3} + self.assertEqual(xx.__hash__(), 111) + self.assertEqual(xy.__hash__(), 111) + self.assertEqual(yy.__hash__(), 111) + self.assertEqual(var_dict[xx], 1) + self.assertEqual(var_dict[xy], 2) + self.assertEqual(var_dict[yy], 3) class QuadraticNumberOpTestsParameters(NamedTuple): - quadratic_type: str - constant: Union[float, int] - quadratic_first: bool - - def test_suffix(self): - if self.quadratic_first: - return f"_{self.quadratic_type}_{type(self.constant).__name__}" - else: - return f"_{type(self.constant).__name__}_{self.quadratic_type}" - - -def all_quadratic_number_op_parameters() -> List[QuadraticNumberOpTestsParameters]: - result = [] - for t in _QUADRATIC_TYPES: - for c in (2, 0.25): - for first in (True, False): - result.append( - QuadraticNumberOpTestsParameters( - quadratic_type=t, constant=c, quadratic_first=first - ) - ) - return result + quadratic_type: str + constant: Union[float, int] + quadratic_first: bool + + def test_suffix(self): + if self.quadratic_first: + return f"_{self.quadratic_type}_{type(self.constant).__name__}" + else: + return f"_{type(self.constant).__name__}_{self.quadratic_type}" + + +def all_quadratic_number_op_parameters() -> ( + List[QuadraticNumberOpTestsParameters] +): + result = [] + for t in _QUADRATIC_TYPES: + for c in (2, 0.25): + for first in (True, False): + result.append( + QuadraticNumberOpTestsParameters( + quadratic_type=t, constant=c, quadratic_first=first + ) + ) + return result # Test all operations (including inplace) between a number and a Quadratic @@ -1373,403 +1397,415 @@ def all_quadratic_number_op_parameters() -> List[QuadraticNumberOpTestsParameter ) class QuadraticNumberOpTests(parameterized.TestCase): - def test_mult( - self, - quadratic_type: str, - constant: Union[float, int], - quadratic_first: bool, - ) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - if quadratic_type == "QuadraticTerm": - quadratic = variables.QuadraticTerm(xy, 2) - expected_type = variables.QuadraticTerm - expected_offset = 0 - expected_linear_terms = {} - expected_quadratic_terms = {xy: 2 * constant} - elif quadratic_type == "QuadraticExpression": - quadratic = variables.QuadraticExpression(x * x - 2 * x * y - x + 3) - expected_type = variables.QuadraticProduct - expected_offset = 3 * constant - expected_linear_terms = {x: -constant} - expected_quadratic_terms = {xx: constant, xy: -2 * constant} - elif quadratic_type == "QuadraticSum": - quadratic = variables.QuadraticSum((x, -2 * y, 3, y * y)) - expected_type = variables.QuadraticProduct - expected_offset = 3 * constant - expected_linear_terms = {x: constant, y: -2 * constant} - expected_quadratic_terms = {yy: constant} - elif quadratic_type == "LinearLinearProduct": - quadratic = variables.LinearLinearProduct(x + y + 1, x + 1) - expected_type = variables.QuadraticProduct - expected_offset = constant - expected_linear_terms = {x: 2 * constant, y: constant} - expected_quadratic_terms = {xx: constant, xy: constant} - elif quadratic_type == "QuadraticProduct": - quadratic = variables.QuadraticProduct(2, x * x) - expected_type = variables.QuadraticProduct - expected_offset = 0.0 - expected_linear_terms = {} - expected_quadratic_terms = {xx: 2 * constant} - else: - raise AssertionError(f"unknown quaratic type: {quadratic_type!r}") - - # Check __mul__ and __rmul__ - s = quadratic * constant if quadratic_first else constant * quadratic - e = variables.as_flat_quadratic_expression(s) - self.assertIsInstance(s, expected_type) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqual(dict(e.linear_terms), expected_linear_terms) - self.assertDictEqual(dict(e.quadratic_terms), expected_quadratic_terms) - - # Also check __imul__ - if quadratic_first: - expr = quadratic - expr *= constant - else: - expr = constant - expr *= quadratic - e_inplace = variables.as_flat_quadratic_expression(expr) - self.assertIsInstance(expr, expected_type) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqual(dict(e_inplace.linear_terms), expected_linear_terms) - self.assertDictEqual(dict(e_inplace.quadratic_terms), expected_quadratic_terms) - - def test_div( - self, - quadratic_type: str, - constant: Union[float, int], - quadratic_first: bool, - ) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - if quadratic_type == "QuadraticTerm": - quadratic = variables.QuadraticTerm(xy, 2) - expected_type = variables.QuadraticTerm - expected_offset = 0 - expected_linear_terms = {} - expected_quadratic_terms = {xy: 2 / constant} - elif quadratic_type == "QuadraticExpression": - quadratic = variables.QuadraticExpression(x * x - 2 * x * y - x + 3) - expected_type = variables.QuadraticProduct - expected_offset = 3 / constant - expected_linear_terms = {x: -1 / constant} - expected_quadratic_terms = {xx: 1 / constant, xy: -2 / constant} - elif quadratic_type == "QuadraticSum": - quadratic = variables.QuadraticSum((x, -2 * y, 3, y * y)) - expected_type = variables.QuadraticProduct - expected_offset = 3 / constant - expected_linear_terms = {x: 1 / constant, y: -2 / constant} - expected_quadratic_terms = {yy: 1 / constant} - elif quadratic_type == "LinearLinearProduct": - quadratic = variables.LinearLinearProduct(x + y + 1, x + 1) - expected_type = variables.QuadraticProduct - expected_offset = 1 / constant - expected_linear_terms = {x: 2 / constant, y: 1 / constant} - expected_quadratic_terms = {xx: 1 / constant, xy: 1 / constant} - elif quadratic_type == "QuadraticProduct": - quadratic = variables.QuadraticProduct(2, x * x) - expected_type = variables.QuadraticProduct - expected_offset = 0.0 - expected_linear_terms = {} - expected_quadratic_terms = {xx: 2 / constant} - else: - raise AssertionError(f"unknown quaratic type: {quadratic_type!r}") - - # Check __truediv__ - if quadratic_first: - s = quadratic / constant - e = variables.as_flat_quadratic_expression(s) - self.assertIsInstance(s, expected_type) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqual(dict(e.linear_terms), expected_linear_terms) - self.assertDictEqual(dict(e.quadratic_terms), expected_quadratic_terms) - else: - with self.assertRaisesWithLiteralMatch( - TypeError, - f"unsupported operand type(s) for /: {type(constant).__name__!r} " - f"and {type(quadratic).__name__!r}", - ): - s = constant / quadratic # pytype: disable=unsupported-operands - - # Also check __itruediv__ - if quadratic_first: - quadratic /= constant - e_inplace = variables.as_flat_quadratic_expression(quadratic) - self.assertIsInstance(quadratic, expected_type) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqual(dict(e_inplace.linear_terms), expected_linear_terms) - self.assertDictEqual( - dict(e_inplace.quadratic_terms), expected_quadratic_terms - ) - else: - with self.assertRaisesWithLiteralMatch( - TypeError, - f"unsupported operand type(s) for /=: {type(constant).__name__!r} " - f"and {type(quadratic).__name__!r}", - ): - expr = constant - expr /= quadratic # pytype: disable=unsupported-operands - - def test_add( - self, - quadratic_type: str, - constant: Union[float, int], - quadratic_first: bool, - ) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - if quadratic_type == "QuadraticTerm": - quadratic = variables.QuadraticTerm(xy, 2) - expected_offset = constant - expected_linear_terms = {} - expected_quadratic_terms = {xy: 2} - elif quadratic_type == "QuadraticExpression": - quadratic = variables.QuadraticExpression(x * x - 2 * x * y - x + 3) - expected_offset = 3 + constant - expected_linear_terms = {x: -1} - expected_quadratic_terms = {xx: 1, xy: -2} - elif quadratic_type == "QuadraticSum": - quadratic = variables.QuadraticSum((x, -2 * y, 3, y * y)) - expected_offset = 3 + constant - expected_linear_terms = {x: 1, y: -2} - expected_quadratic_terms = {yy: 1} - elif quadratic_type == "LinearLinearProduct": - quadratic = variables.LinearLinearProduct(x + y + 1, x + 1) - expected_offset = 1 + constant - expected_linear_terms = {x: 2, y: 1} - expected_quadratic_terms = {xx: 1, xy: 1} - elif quadratic_type == "QuadraticProduct": - quadratic = variables.QuadraticProduct(2, x * x) - expected_offset = constant - expected_linear_terms = {} - expected_quadratic_terms = {xx: 2} - else: - raise AssertionError(f"unknown quaratic type: {quadratic_type!r}") - - # Check __add__ and __radd__ - s = quadratic + constant if quadratic_first else constant + quadratic - e = variables.as_flat_quadratic_expression(s) - self.assertIsInstance(s, variables.QuadraticSum) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqual(dict(e.linear_terms), expected_linear_terms) - self.assertDictEqual(dict(e.quadratic_terms), expected_quadratic_terms) - - # Also check __iadd__ - if quadratic_first: - expr = quadratic - expr += constant - else: - expr = constant - expr += quadratic - e_inplace = variables.as_flat_quadratic_expression(expr) - self.assertIsInstance(expr, variables.QuadraticSum) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqual(dict(e_inplace.linear_terms), expected_linear_terms) - self.assertDictEqual(dict(e_inplace.quadratic_terms), expected_quadratic_terms) - - def test_sub( - self, - quadratic_type: str, - constant: Union[float, int], - quadratic_first: bool, - ) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - sign = 1 if quadratic_first else -1 - if quadratic_type == "QuadraticTerm": - quadratic = variables.QuadraticTerm(xy, 2) - expected_offset = -sign * constant - expected_linear_terms = {} - expected_quadratic_terms = {xy: sign * 2} - elif quadratic_type == "QuadraticExpression": - quadratic = variables.QuadraticExpression(x * x - 2 * x * y - x + 3) - expected_offset = sign * 3 - sign * constant - expected_linear_terms = {x: sign * (-1)} - expected_quadratic_terms = {xx: sign * 1, xy: sign * (-2)} - elif quadratic_type == "QuadraticSum": - quadratic = variables.QuadraticSum((x, -2 * y, 3, y * y)) - expected_offset = sign * 3 - sign * constant - expected_linear_terms = {x: sign * 1, y: sign * (-2)} - expected_quadratic_terms = {yy: sign} - elif quadratic_type == "LinearLinearProduct": - quadratic = variables.LinearLinearProduct(x + y + 1, x + 1) - expected_offset = sign * 1 - sign * constant - expected_linear_terms = {x: sign * 2, y: sign * 1} - expected_quadratic_terms = {xx: sign, xy: sign} - elif quadratic_type == "QuadraticProduct": - quadratic = variables.QuadraticProduct(2, x * x) - expected_offset = -sign * constant - expected_linear_terms = {} - expected_quadratic_terms = {xx: sign * 2} - else: - raise AssertionError(f"unknown quaratic type: {quadratic_type!r}") - - # Check __sub__ and __rsub__ - s = quadratic - constant if quadratic_first else constant - quadratic - e = variables.as_flat_quadratic_expression(s) - self.assertIsInstance(s, variables.QuadraticSum) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqual(dict(e.linear_terms), expected_linear_terms) - self.assertDictEqual(dict(e.quadratic_terms), expected_quadratic_terms) - - # Also check __isub__ - if quadratic_first: - quadratic -= constant - e_inplace = variables.as_flat_quadratic_expression(quadratic) - self.assertIsInstance(quadratic, variables.QuadraticSum) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqual(dict(e_inplace.linear_terms), expected_linear_terms) - self.assertDictEqual( - dict(e_inplace.quadratic_terms), expected_quadratic_terms - ) + def test_mult( + self, + quadratic_type: str, + constant: Union[float, int], + quadratic_first: bool, + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + if quadratic_type == "QuadraticTerm": + quadratic = variables.QuadraticTerm(xy, 2) + expected_type = variables.QuadraticTerm + expected_offset = 0 + expected_linear_terms = {} + expected_quadratic_terms = {xy: 2 * constant} + elif quadratic_type == "QuadraticExpression": + quadratic = variables.QuadraticExpression(x * x - 2 * x * y - x + 3) + expected_type = variables.QuadraticProduct + expected_offset = 3 * constant + expected_linear_terms = {x: -constant} + expected_quadratic_terms = {xx: constant, xy: -2 * constant} + elif quadratic_type == "QuadraticSum": + quadratic = variables.QuadraticSum((x, -2 * y, 3, y * y)) + expected_type = variables.QuadraticProduct + expected_offset = 3 * constant + expected_linear_terms = {x: constant, y: -2 * constant} + expected_quadratic_terms = {yy: constant} + elif quadratic_type == "LinearLinearProduct": + quadratic = variables.LinearLinearProduct(x + y + 1, x + 1) + expected_type = variables.QuadraticProduct + expected_offset = constant + expected_linear_terms = {x: 2 * constant, y: constant} + expected_quadratic_terms = {xx: constant, xy: constant} + elif quadratic_type == "QuadraticProduct": + quadratic = variables.QuadraticProduct(2, x * x) + expected_type = variables.QuadraticProduct + expected_offset = 0.0 + expected_linear_terms = {} + expected_quadratic_terms = {xx: 2 * constant} + else: + raise AssertionError(f"unknown quaratic type: {quadratic_type!r}") + + # Check __mul__ and __rmul__ + s = quadratic * constant if quadratic_first else constant * quadratic + e = variables.as_flat_quadratic_expression(s) + self.assertIsInstance(s, expected_type) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqual(dict(e.linear_terms), expected_linear_terms) + self.assertDictEqual(dict(e.quadratic_terms), expected_quadratic_terms) + + # Also check __imul__ + if quadratic_first: + expr = quadratic + expr *= constant + else: + expr = constant + expr *= quadratic + e_inplace = variables.as_flat_quadratic_expression(expr) + self.assertIsInstance(expr, expected_type) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqual(dict(e_inplace.linear_terms), expected_linear_terms) + self.assertDictEqual( + dict(e_inplace.quadratic_terms), expected_quadratic_terms + ) + + def test_div( + self, + quadratic_type: str, + constant: Union[float, int], + quadratic_first: bool, + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + if quadratic_type == "QuadraticTerm": + quadratic = variables.QuadraticTerm(xy, 2) + expected_type = variables.QuadraticTerm + expected_offset = 0 + expected_linear_terms = {} + expected_quadratic_terms = {xy: 2 / constant} + elif quadratic_type == "QuadraticExpression": + quadratic = variables.QuadraticExpression(x * x - 2 * x * y - x + 3) + expected_type = variables.QuadraticProduct + expected_offset = 3 / constant + expected_linear_terms = {x: -1 / constant} + expected_quadratic_terms = {xx: 1 / constant, xy: -2 / constant} + elif quadratic_type == "QuadraticSum": + quadratic = variables.QuadraticSum((x, -2 * y, 3, y * y)) + expected_type = variables.QuadraticProduct + expected_offset = 3 / constant + expected_linear_terms = {x: 1 / constant, y: -2 / constant} + expected_quadratic_terms = {yy: 1 / constant} + elif quadratic_type == "LinearLinearProduct": + quadratic = variables.LinearLinearProduct(x + y + 1, x + 1) + expected_type = variables.QuadraticProduct + expected_offset = 1 / constant + expected_linear_terms = {x: 2 / constant, y: 1 / constant} + expected_quadratic_terms = {xx: 1 / constant, xy: 1 / constant} + elif quadratic_type == "QuadraticProduct": + quadratic = variables.QuadraticProduct(2, x * x) + expected_type = variables.QuadraticProduct + expected_offset = 0.0 + expected_linear_terms = {} + expected_quadratic_terms = {xx: 2 / constant} + else: + raise AssertionError(f"unknown quaratic type: {quadratic_type!r}") + + # Check __truediv__ + if quadratic_first: + s = quadratic / constant + e = variables.as_flat_quadratic_expression(s) + self.assertIsInstance(s, expected_type) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqual(dict(e.linear_terms), expected_linear_terms) + self.assertDictEqual(dict(e.quadratic_terms), expected_quadratic_terms) + else: + with self.assertRaisesWithLiteralMatch( + TypeError, + f"unsupported operand type(s) for /: {type(constant).__name__!r} " + f"and {type(quadratic).__name__!r}", + ): + s = constant / quadratic # pytype: disable=unsupported-operands + + # Also check __itruediv__ + if quadratic_first: + quadratic /= constant + e_inplace = variables.as_flat_quadratic_expression(quadratic) + self.assertIsInstance(quadratic, expected_type) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqual(dict(e_inplace.linear_terms), expected_linear_terms) + self.assertDictEqual( + dict(e_inplace.quadratic_terms), expected_quadratic_terms + ) + else: + with self.assertRaisesWithLiteralMatch( + TypeError, + f"unsupported operand type(s) for /=: {type(constant).__name__!r} " + f"and {type(quadratic).__name__!r}", + ): + expr = constant + expr /= quadratic # pytype: disable=unsupported-operands + + def test_add( + self, + quadratic_type: str, + constant: Union[float, int], + quadratic_first: bool, + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + if quadratic_type == "QuadraticTerm": + quadratic = variables.QuadraticTerm(xy, 2) + expected_offset = constant + expected_linear_terms = {} + expected_quadratic_terms = {xy: 2} + elif quadratic_type == "QuadraticExpression": + quadratic = variables.QuadraticExpression(x * x - 2 * x * y - x + 3) + expected_offset = 3 + constant + expected_linear_terms = {x: -1} + expected_quadratic_terms = {xx: 1, xy: -2} + elif quadratic_type == "QuadraticSum": + quadratic = variables.QuadraticSum((x, -2 * y, 3, y * y)) + expected_offset = 3 + constant + expected_linear_terms = {x: 1, y: -2} + expected_quadratic_terms = {yy: 1} + elif quadratic_type == "LinearLinearProduct": + quadratic = variables.LinearLinearProduct(x + y + 1, x + 1) + expected_offset = 1 + constant + expected_linear_terms = {x: 2, y: 1} + expected_quadratic_terms = {xx: 1, xy: 1} + elif quadratic_type == "QuadraticProduct": + quadratic = variables.QuadraticProduct(2, x * x) + expected_offset = constant + expected_linear_terms = {} + expected_quadratic_terms = {xx: 2} + else: + raise AssertionError(f"unknown quaratic type: {quadratic_type!r}") + + # Check __add__ and __radd__ + s = quadratic + constant if quadratic_first else constant + quadratic + e = variables.as_flat_quadratic_expression(s) + self.assertIsInstance(s, variables.QuadraticSum) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqual(dict(e.linear_terms), expected_linear_terms) + self.assertDictEqual(dict(e.quadratic_terms), expected_quadratic_terms) + + # Also check __iadd__ + if quadratic_first: + expr = quadratic + expr += constant + else: + expr = constant + expr += quadratic + e_inplace = variables.as_flat_quadratic_expression(expr) + self.assertIsInstance(expr, variables.QuadraticSum) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqual(dict(e_inplace.linear_terms), expected_linear_terms) + self.assertDictEqual( + dict(e_inplace.quadratic_terms), expected_quadratic_terms + ) + + def test_sub( + self, + quadratic_type: str, + constant: Union[float, int], + quadratic_first: bool, + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + sign = 1 if quadratic_first else -1 + if quadratic_type == "QuadraticTerm": + quadratic = variables.QuadraticTerm(xy, 2) + expected_offset = -sign * constant + expected_linear_terms = {} + expected_quadratic_terms = {xy: sign * 2} + elif quadratic_type == "QuadraticExpression": + quadratic = variables.QuadraticExpression(x * x - 2 * x * y - x + 3) + expected_offset = sign * 3 - sign * constant + expected_linear_terms = {x: sign * (-1)} + expected_quadratic_terms = {xx: sign * 1, xy: sign * (-2)} + elif quadratic_type == "QuadraticSum": + quadratic = variables.QuadraticSum((x, -2 * y, 3, y * y)) + expected_offset = sign * 3 - sign * constant + expected_linear_terms = {x: sign * 1, y: sign * (-2)} + expected_quadratic_terms = {yy: sign} + elif quadratic_type == "LinearLinearProduct": + quadratic = variables.LinearLinearProduct(x + y + 1, x + 1) + expected_offset = sign * 1 - sign * constant + expected_linear_terms = {x: sign * 2, y: sign * 1} + expected_quadratic_terms = {xx: sign, xy: sign} + elif quadratic_type == "QuadraticProduct": + quadratic = variables.QuadraticProduct(2, x * x) + expected_offset = -sign * constant + expected_linear_terms = {} + expected_quadratic_terms = {xx: sign * 2} + else: + raise AssertionError(f"unknown quaratic type: {quadratic_type!r}") + + # Check __sub__ and __rsub__ + s = quadratic - constant if quadratic_first else constant - quadratic + e = variables.as_flat_quadratic_expression(s) + self.assertIsInstance(s, variables.QuadraticSum) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqual(dict(e.linear_terms), expected_linear_terms) + self.assertDictEqual(dict(e.quadratic_terms), expected_quadratic_terms) + + # Also check __isub__ + if quadratic_first: + quadratic -= constant + e_inplace = variables.as_flat_quadratic_expression(quadratic) + self.assertIsInstance(quadratic, variables.QuadraticSum) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqual(dict(e_inplace.linear_terms), expected_linear_terms) + self.assertDictEqual( + dict(e_inplace.quadratic_terms), expected_quadratic_terms + ) class LinearLinearAddSubTestParams(NamedTuple): - lhs_type: str - rhs_type: str - subtract: bool - - def test_suffix(self): - return ( - f"_{self.lhs_type}_{self.rhs_type}_" - f'{"subtract" if self.subtract else "add"}' - ) + lhs_type: str + rhs_type: str + subtract: bool + + def test_suffix(self): + return ( + f"_{self.lhs_type}_{self.rhs_type}_" + f"{'subtract' if self.subtract else 'add'}" + ) def all_linear_linear_add_sub_params() -> List[LinearLinearAddSubTestParams]: - result = [] - for lhs_type in _LINEAR_TYPES: - for rhs_type in _LINEAR_TYPES: - for sub in (True, False): - result.append( - LinearLinearAddSubTestParams( - lhs_type=lhs_type, rhs_type=rhs_type, subtract=sub - ) - ) - return result + result = [] + for lhs_type in _LINEAR_TYPES: + for rhs_type in _LINEAR_TYPES: + for sub in (True, False): + result.append( + LinearLinearAddSubTestParams( + lhs_type=lhs_type, rhs_type=rhs_type, subtract=sub + ) + ) + return result # Test add/sub operations (including inplace) between two Linear objects. class LinearLinearAddSubTest(parameterized.TestCase): - @parameterized.named_parameters( - (p.test_suffix(), p.lhs_type, p.rhs_type, p.subtract) - for p in all_linear_linear_add_sub_params() - ) - def test_add_and_sub(self, lhs_type: str, rhs_type: str, subtract: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - x_coefficient = 0 - y_coefficient = 0 - expected_offset = 0 - sign = -1 if subtract else 1 - # Setup first linear term. - if lhs_type == "Variable": - first_linear = x - x_coefficient += 1 - elif lhs_type == "LinearTerm": - first_linear = variables.LinearTerm(x, 2) - x_coefficient += 2 - elif lhs_type == "LinearExpression": - first_linear = variables.LinearExpression(x - 2 * y + 1) - x_coefficient += 1 - y_coefficient += -2 - expected_offset += 1 - elif lhs_type == "LinearSum": - first_linear = variables.LinearSum((x, -2 * y, 1)) - x_coefficient += 1 - y_coefficient += -2 - expected_offset += 1 - elif lhs_type == "LinearProduct": - first_linear = variables.LinearProduct(2, x) - x_coefficient += 2 - else: - raise AssertionError(f"unknown linear type: {lhs_type!r}") - - # Setup second linear term - if rhs_type == "Variable": - second_linear = y - y_coefficient += sign * 1 - elif rhs_type == "LinearTerm": - second_linear = variables.LinearTerm(y, 2) - y_coefficient += sign * 2 - elif rhs_type == "LinearExpression": - second_linear = variables.LinearExpression(y - 2 * x + 1) - x_coefficient += sign * (-2) - y_coefficient += sign * 1 - expected_offset += sign * 1 - elif rhs_type == "LinearSum": - second_linear = variables.LinearSum((y, -2 * x, 1)) - x_coefficient += sign * (-2) - y_coefficient += sign * 1 - expected_offset += sign * 1 - elif rhs_type == "LinearProduct": - second_linear = variables.LinearProduct(2, y) - y_coefficient += sign * 2 - else: - raise AssertionError(f"unknown linear type: {rhs_type!r}") - - # Check __add__ and __sub__ - s = first_linear - second_linear if subtract else first_linear + second_linear - e = variables.as_flat_linear_expression(s) - self.assertIsInstance(s, variables.LinearSum) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqual(dict(e.terms), {x: x_coefficient, y: y_coefficient}) - - # Also check __iadd__ and __isub__ - if subtract: - first_linear -= second_linear - else: - first_linear += second_linear - e_inplace = variables.as_flat_linear_expression(first_linear) - self.assertIsInstance(first_linear, variables.LinearSum) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqual( - dict(e_inplace.terms), {x: x_coefficient, y: y_coefficient} - ) + @parameterized.named_parameters( + (p.test_suffix(), p.lhs_type, p.rhs_type, p.subtract) + for p in all_linear_linear_add_sub_params() + ) + def test_add_and_sub( + self, lhs_type: str, rhs_type: str, subtract: bool + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + x_coefficient = 0 + y_coefficient = 0 + expected_offset = 0 + sign = -1 if subtract else 1 + # Setup first linear term. + if lhs_type == "Variable": + first_linear = x + x_coefficient += 1 + elif lhs_type == "LinearTerm": + first_linear = variables.LinearTerm(x, 2) + x_coefficient += 2 + elif lhs_type == "LinearExpression": + first_linear = variables.LinearExpression(x - 2 * y + 1) + x_coefficient += 1 + y_coefficient += -2 + expected_offset += 1 + elif lhs_type == "LinearSum": + first_linear = variables.LinearSum((x, -2 * y, 1)) + x_coefficient += 1 + y_coefficient += -2 + expected_offset += 1 + elif lhs_type == "LinearProduct": + first_linear = variables.LinearProduct(2, x) + x_coefficient += 2 + else: + raise AssertionError(f"unknown linear type: {lhs_type!r}") + + # Setup second linear term + if rhs_type == "Variable": + second_linear = y + y_coefficient += sign * 1 + elif rhs_type == "LinearTerm": + second_linear = variables.LinearTerm(y, 2) + y_coefficient += sign * 2 + elif rhs_type == "LinearExpression": + second_linear = variables.LinearExpression(y - 2 * x + 1) + x_coefficient += sign * (-2) + y_coefficient += sign * 1 + expected_offset += sign * 1 + elif rhs_type == "LinearSum": + second_linear = variables.LinearSum((y, -2 * x, 1)) + x_coefficient += sign * (-2) + y_coefficient += sign * 1 + expected_offset += sign * 1 + elif rhs_type == "LinearProduct": + second_linear = variables.LinearProduct(2, y) + y_coefficient += sign * 2 + else: + raise AssertionError(f"unknown linear type: {rhs_type!r}") + + # Check __add__ and __sub__ + s = ( + first_linear - second_linear + if subtract + else first_linear + second_linear + ) + e = variables.as_flat_linear_expression(s) + self.assertIsInstance(s, variables.LinearSum) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqual(dict(e.terms), {x: x_coefficient, y: y_coefficient}) + + # Also check __iadd__ and __isub__ + if subtract: + first_linear -= second_linear + else: + first_linear += second_linear + e_inplace = variables.as_flat_linear_expression(first_linear) + self.assertIsInstance(first_linear, variables.LinearSum) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqual( + dict(e_inplace.terms), {x: x_coefficient, y: y_coefficient} + ) class LinearQuadraticAddSubTestParams(NamedTuple): - lhs_type: str - rhs_type: str - subtract: bool - - def test_suffix(self): - return ( - f"_{self.lhs_type}_{self.rhs_type}_" - f'{"subtract" if self.subtract else "add"}' - ) + lhs_type: str + rhs_type: str + subtract: bool + + def test_suffix(self): + return ( + f"_{self.lhs_type}_{self.rhs_type}_" + f"{'subtract' if self.subtract else 'add'}" + ) -def all_linear_quadratic_add_sub_params() -> List[LinearQuadraticAddSubTestParams]: - result = [] - for lhs_type in _LINEAR_TYPES + _QUADRATIC_TYPES: - for rhs_type in _LINEAR_TYPES + _QUADRATIC_TYPES: - for sub in (True, False): - result.append( - LinearQuadraticAddSubTestParams( - lhs_type=lhs_type, rhs_type=rhs_type, subtract=sub - ) - ) - return result +def all_linear_quadratic_add_sub_params() -> ( + List[LinearQuadraticAddSubTestParams] +): + result = [] + for lhs_type in _LINEAR_TYPES + _QUADRATIC_TYPES: + for rhs_type in _LINEAR_TYPES + _QUADRATIC_TYPES: + for sub in (True, False): + result.append( + LinearQuadraticAddSubTestParams( + lhs_type=lhs_type, rhs_type=rhs_type, subtract=sub + ) + ) + return result # Test add/sub operations (including inplace) between Quadratic and Linear @@ -1777,767 +1813,777 @@ def all_linear_quadratic_add_sub_params() -> List[LinearQuadraticAddSubTestParam # result is intereted as a QuadraticExpression. class LinearQuadraticAddSubTest(parameterized.TestCase): - def assertDictEqualWithZeroDefault( - self, dict1: dict[Any, float], dict2: dict[Any, float] - ) -> None: - for key in dict1.keys(): - if key not in dict2: - dict2[key] = 0.0 - for key in dict2.keys(): - if key not in dict1: - dict1[key] = 0.0 - self.assertDictEqual(dict1, dict2) - - @parameterized.named_parameters( - (p.test_suffix(), p.lhs_type, p.rhs_type, p.subtract) - for p in all_linear_quadratic_add_sub_params() - ) - def test_add_and_sub(self, lhs_type: str, rhs_type: str, subtract: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - x_coefficient = 0 - y_coefficient = 0 - xx_coefficient = 0 - xy_coefficient = 0 - yy_coefficient = 0 - expected_offset = 0 - sign = -1 if subtract else 1 - # Setup first linear term. - if lhs_type == "Variable": - first_linear_or_quadratic = x - x_coefficient += 1 - elif lhs_type == "LinearTerm": - first_linear_or_quadratic = variables.LinearTerm(x, 2) - x_coefficient += 2 - elif lhs_type == "LinearExpression": - first_linear_or_quadratic = variables.LinearExpression(x - 2 * y + 1) - x_coefficient += 1 - y_coefficient += -2 - expected_offset += 1 - elif lhs_type == "LinearSum": - first_linear_or_quadratic = variables.LinearSum((x, -2 * y, 1)) - x_coefficient += 1 - y_coefficient += -2 - expected_offset += 1 - elif lhs_type == "LinearProduct": - first_linear_or_quadratic = variables.LinearProduct(2, x) - x_coefficient += 2 - elif lhs_type == "QuadraticTerm": - first_linear_or_quadratic = variables.QuadraticTerm(xx, 2) - xx_coefficient += 2 - elif lhs_type == "QuadraticExpression": - first_linear_or_quadratic = variables.QuadraticExpression( - x - 2 * y + 1 + 3 * x * x - 4 * x * y - ) - x_coefficient += 1 - y_coefficient += -2 - expected_offset += 1 - xx_coefficient += 3 - xy_coefficient += -4 - elif lhs_type == "QuadraticSum": - first_linear_or_quadratic = variables.QuadraticSum( - (x, -2 * y, 1, y * y, -2 * x * y) - ) - x_coefficient += 1 - y_coefficient += -2 - expected_offset += 1 - yy_coefficient += 1 - xy_coefficient += -2 - elif lhs_type == "LinearLinearProduct": - first_linear_or_quadratic = variables.LinearLinearProduct(y, x + y) - yy_coefficient += 1 - xy_coefficient += 1 - elif lhs_type == "QuadraticProduct": - first_linear_or_quadratic = variables.QuadraticProduct(2, y * (x + y)) - yy_coefficient += 2 - xy_coefficient += 2 - else: - raise AssertionError(f"unknown linear type: {lhs_type!r}") - - # Setup second linear term - if rhs_type == "Variable": - second_linear_or_quadratic = y - y_coefficient += 1 * sign - elif rhs_type == "LinearTerm": - second_linear_or_quadratic = variables.LinearTerm(y, 2) - y_coefficient += 2 * sign - elif rhs_type == "LinearExpression": - second_linear_or_quadratic = variables.LinearExpression(y - 2 * x + 1) - x_coefficient += -2 * sign - y_coefficient += 1 * sign - expected_offset += 1 * sign - elif rhs_type == "LinearSum": - second_linear_or_quadratic = variables.LinearSum((y, -2 * x, 1)) - x_coefficient += -2 * sign - y_coefficient += 1 * sign - expected_offset += 1 * sign - elif rhs_type == "LinearProduct": - second_linear_or_quadratic = variables.LinearProduct(2, y) - y_coefficient += 2 * sign - elif rhs_type == "QuadraticTerm": - second_linear_or_quadratic = variables.QuadraticTerm(xy, 5) - xy_coefficient += 5 * sign - elif rhs_type == "QuadraticExpression": - second_linear_or_quadratic = variables.QuadraticExpression( - x - 2 * y + 1 + 3 * x * y - 4 * y * y - ) - x_coefficient += 1 * sign - y_coefficient += -2 * sign - expected_offset += 1 * sign - xy_coefficient += 3 * sign - yy_coefficient += -4 * sign - elif rhs_type == "QuadraticSum": - second_linear_or_quadratic = variables.QuadraticSum( - (x, -2 * y, 1, y * x, -2 * x * x) - ) - x_coefficient += 1 * sign - y_coefficient += -2 * sign - expected_offset += 1 * sign - xy_coefficient += 1 * sign - xx_coefficient += -2 * sign - elif rhs_type == "LinearLinearProduct": - second_linear_or_quadratic = variables.LinearLinearProduct(x, x + y) - xx_coefficient += sign - xy_coefficient += sign - elif rhs_type == "QuadraticProduct": - second_linear_or_quadratic = variables.QuadraticProduct(2, x * (x + y)) - xx_coefficient += 2 * sign - xy_coefficient += 2 * sign - else: - raise AssertionError(f"unknown linear type: {lhs_type!r}") - - # Check __add__ and __sub__ - s = ( - first_linear_or_quadratic - second_linear_or_quadratic - if subtract - else first_linear_or_quadratic + second_linear_or_quadratic - ) - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(e.offset, expected_offset) - self.assertDictEqualWithZeroDefault( - dict(e.linear_terms), {x: x_coefficient, y: y_coefficient} - ) - self.assertDictEqualWithZeroDefault( - dict(e.quadratic_terms), - {xx: xx_coefficient, xy: xy_coefficient, yy: yy_coefficient}, - ) + def assertDictEqualWithZeroDefault( + self, dict1: dict[Any, float], dict2: dict[Any, float] + ) -> None: + for key in dict1.keys(): + if key not in dict2: + dict2[key] = 0.0 + for key in dict2.keys(): + if key not in dict1: + dict1[key] = 0.0 + self.assertDictEqual(dict1, dict2) + + @parameterized.named_parameters( + (p.test_suffix(), p.lhs_type, p.rhs_type, p.subtract) + for p in all_linear_quadratic_add_sub_params() + ) + def test_add_and_sub( + self, lhs_type: str, rhs_type: str, subtract: bool + ) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + x_coefficient = 0 + y_coefficient = 0 + xx_coefficient = 0 + xy_coefficient = 0 + yy_coefficient = 0 + expected_offset = 0 + sign = -1 if subtract else 1 + # Setup first linear term. + if lhs_type == "Variable": + first_linear_or_quadratic = x + x_coefficient += 1 + elif lhs_type == "LinearTerm": + first_linear_or_quadratic = variables.LinearTerm(x, 2) + x_coefficient += 2 + elif lhs_type == "LinearExpression": + first_linear_or_quadratic = variables.LinearExpression(x - 2 * y + 1) + x_coefficient += 1 + y_coefficient += -2 + expected_offset += 1 + elif lhs_type == "LinearSum": + first_linear_or_quadratic = variables.LinearSum((x, -2 * y, 1)) + x_coefficient += 1 + y_coefficient += -2 + expected_offset += 1 + elif lhs_type == "LinearProduct": + first_linear_or_quadratic = variables.LinearProduct(2, x) + x_coefficient += 2 + elif lhs_type == "QuadraticTerm": + first_linear_or_quadratic = variables.QuadraticTerm(xx, 2) + xx_coefficient += 2 + elif lhs_type == "QuadraticExpression": + first_linear_or_quadratic = variables.QuadraticExpression( + x - 2 * y + 1 + 3 * x * x - 4 * x * y + ) + x_coefficient += 1 + y_coefficient += -2 + expected_offset += 1 + xx_coefficient += 3 + xy_coefficient += -4 + elif lhs_type == "QuadraticSum": + first_linear_or_quadratic = variables.QuadraticSum( + (x, -2 * y, 1, y * y, -2 * x * y) + ) + x_coefficient += 1 + y_coefficient += -2 + expected_offset += 1 + yy_coefficient += 1 + xy_coefficient += -2 + elif lhs_type == "LinearLinearProduct": + first_linear_or_quadratic = variables.LinearLinearProduct(y, x + y) + yy_coefficient += 1 + xy_coefficient += 1 + elif lhs_type == "QuadraticProduct": + first_linear_or_quadratic = variables.QuadraticProduct(2, y * (x + y)) + yy_coefficient += 2 + xy_coefficient += 2 + else: + raise AssertionError(f"unknown linear type: {lhs_type!r}") + + # Setup second linear term + if rhs_type == "Variable": + second_linear_or_quadratic = y + y_coefficient += 1 * sign + elif rhs_type == "LinearTerm": + second_linear_or_quadratic = variables.LinearTerm(y, 2) + y_coefficient += 2 * sign + elif rhs_type == "LinearExpression": + second_linear_or_quadratic = variables.LinearExpression(y - 2 * x + 1) + x_coefficient += -2 * sign + y_coefficient += 1 * sign + expected_offset += 1 * sign + elif rhs_type == "LinearSum": + second_linear_or_quadratic = variables.LinearSum((y, -2 * x, 1)) + x_coefficient += -2 * sign + y_coefficient += 1 * sign + expected_offset += 1 * sign + elif rhs_type == "LinearProduct": + second_linear_or_quadratic = variables.LinearProduct(2, y) + y_coefficient += 2 * sign + elif rhs_type == "QuadraticTerm": + second_linear_or_quadratic = variables.QuadraticTerm(xy, 5) + xy_coefficient += 5 * sign + elif rhs_type == "QuadraticExpression": + second_linear_or_quadratic = variables.QuadraticExpression( + x - 2 * y + 1 + 3 * x * y - 4 * y * y + ) + x_coefficient += 1 * sign + y_coefficient += -2 * sign + expected_offset += 1 * sign + xy_coefficient += 3 * sign + yy_coefficient += -4 * sign + elif rhs_type == "QuadraticSum": + second_linear_or_quadratic = variables.QuadraticSum( + (x, -2 * y, 1, y * x, -2 * x * x) + ) + x_coefficient += 1 * sign + y_coefficient += -2 * sign + expected_offset += 1 * sign + xy_coefficient += 1 * sign + xx_coefficient += -2 * sign + elif rhs_type == "LinearLinearProduct": + second_linear_or_quadratic = variables.LinearLinearProduct(x, x + y) + xx_coefficient += sign + xy_coefficient += sign + elif rhs_type == "QuadraticProduct": + second_linear_or_quadratic = variables.QuadraticProduct(2, x * (x + y)) + xx_coefficient += 2 * sign + xy_coefficient += 2 * sign + else: + raise AssertionError(f"unknown linear type: {lhs_type!r}") - # Also check __iadd__ and __isub__ - if subtract: - first_linear_or_quadratic -= second_linear_or_quadratic - else: - first_linear_or_quadratic += second_linear_or_quadratic - e_inplace = variables.as_flat_quadratic_expression(first_linear_or_quadratic) - self.assertEqual(e_inplace.offset, expected_offset) - self.assertDictEqualWithZeroDefault( - dict(e_inplace.linear_terms), {x: x_coefficient, y: y_coefficient} - ) - self.assertDictEqualWithZeroDefault( - dict(e_inplace.quadratic_terms), - {xx: xx_coefficient, xy: xy_coefficient, yy: yy_coefficient}, - ) + # Check __add__ and __sub__ + s = ( + first_linear_or_quadratic - second_linear_or_quadratic + if subtract + else first_linear_or_quadratic + second_linear_or_quadratic + ) + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(e.offset, expected_offset) + self.assertDictEqualWithZeroDefault( + dict(e.linear_terms), {x: x_coefficient, y: y_coefficient} + ) + self.assertDictEqualWithZeroDefault( + dict(e.quadratic_terms), + {xx: xx_coefficient, xy: xy_coefficient, yy: yy_coefficient}, + ) + + # Also check __iadd__ and __isub__ + if subtract: + first_linear_or_quadratic -= second_linear_or_quadratic + else: + first_linear_or_quadratic += second_linear_or_quadratic + e_inplace = variables.as_flat_quadratic_expression( + first_linear_or_quadratic + ) + self.assertEqual(e_inplace.offset, expected_offset) + self.assertDictEqualWithZeroDefault( + dict(e_inplace.linear_terms), {x: x_coefficient, y: y_coefficient} + ) + self.assertDictEqualWithZeroDefault( + dict(e_inplace.quadratic_terms), + {xx: xx_coefficient, xy: xy_coefficient, yy: yy_coefficient}, + ) # Test multiplication of two Linear objects. class LinearLinearMulTest(parameterized.TestCase): - def assertDictEqualWithZeroDefault( - self, dict1: dict[Any, float], dict2: dict[Any, float] - ) -> None: - for key in dict1.keys(): - if key not in dict2: - dict2[key] = 0.0 - for key in dict2.keys(): - if key not in dict1: - dict1[key] = 0.0 - self.assertDictEqual(dict1, dict2) - - @parameterized.named_parameters(("_x_first", True), ("_y_first", False)) - def test_var_var(self, x_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xy = variables.QuadraticTermKey(x, y) - if x_first: - s = x * y - else: - s = y * x - self.assertIsInstance(s, variables.QuadraticTerm) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault({xy: 1.0}, dict(e.quadratic_terms)) - self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_var_first", True), ("_term_first", False)) - def test_term_term(self, var_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xy = variables.QuadraticTermKey(x, y) - if var_first: - s = variables.LinearTerm(x, 2) * variables.LinearTerm(y, 3) - else: - s = variables.LinearTerm(x, 3) * variables.LinearTerm(y, 2) - self.assertIsInstance(s, variables.QuadraticTerm) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault({xy: 6.0}, dict(e.quadratic_terms)) - self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_expr1_first", True), ("_expr2_first", False)) - def test_expr_expr(self, expr1_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - expr1 = variables.LinearExpression(x - 2 * y + 3) - expr2 = variables.LinearExpression(-x + y + 1) - if expr1_first: - s = expr1 * expr2 - else: - s = expr2 * expr1 - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - { - xx: -1.0, - xy: 3.0, - yy: -2.0, - }, - dict(e.quadratic_terms), - ) - self.assertDictEqualWithZeroDefault({x: -2.0, y: 1.0}, dict(e.linear_terms)) - self.assertEqual(3.0, e.offset) - - @parameterized.named_parameters(("_sum1_first", True), ("_sum2_first", False)) - def test_sum_sum(self, sum1_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - sum1 = variables.LinearSum((x, -2 * y, 3)) - sum2 = variables.LinearSum((-x, y, 1)) - if sum1_first: - s = sum1 * sum2 - else: - s = sum2 * sum1 - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - { - xx: -1.0, - xy: 3.0, - yy: -2.0, - }, - dict(e.quadratic_terms), - ) - self.assertDictEqualWithZeroDefault({x: -2.0, y: 1.0}, dict(e.linear_terms)) - self.assertEqual(3.0, e.offset) - - @parameterized.named_parameters(("_prod1_first", True), ("_prod2_first", False)) - def test_prod_prod(self, prod1_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - prod1 = variables.LinearProduct(2.0, x) - prod2 = variables.LinearProduct(3.0, x + 2 * y - 1) - if prod1_first: - s = prod1 * prod2 - else: - s = prod2 * prod1 - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - { - xx: 6.0, - xy: 12.0, - }, - dict(e.quadratic_terms), - ) - self.assertDictEqualWithZeroDefault({x: -6.0}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_var_first", True), ("_term_first", False)) - def test_var_term(self, var_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xy = variables.QuadraticTermKey(x, y) - term = variables.LinearTerm(y, 2) - if var_first: - s = x * term - else: - s = term * x - self.assertIsInstance(s, variables.QuadraticTerm) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault({xy: 2.0}, dict(e.quadratic_terms)) - self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_var_first", True), ("_expr_first", False)) - def test_var_expr(self, var_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - expr = variables.LinearExpression(x - 2 * y + 3) - if var_first: - s = x * expr - else: - s = expr * x - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - {xx: 1.0, xy: -2.0}, dict(e.quadratic_terms) - ) - self.assertDictEqualWithZeroDefault({x: 3.0}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_var_first", True), ("_sum_first", False)) - def test_var_sum(self, var_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - linear_sum = variables.LinearSum((x, -2 * y, 3)) - if var_first: - s = x * linear_sum - else: - s = linear_sum * x - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - {xx: 1.0, xy: -2.0}, dict(e.quadratic_terms) - ) - self.assertDictEqualWithZeroDefault({x: 3.0}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_var_first", True), ("_prod_first", False)) - def test_var_prod(self, var_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xy = variables.QuadraticTermKey(x, y) - expr = variables.LinearProduct(2.0, y) - if var_first: - s = x * expr - else: - s = expr * x - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault({xy: 2.0}, dict(e.quadratic_terms)) - self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_term_first", True), ("_expr_first", False)) - def test_term_expr(self, term_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - term = variables.LinearTerm(x, 2) - expr = variables.LinearExpression(x - 2 * y + 3) - if term_first: - s = term * expr - else: - s = expr * term - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - {xx: 2.0, xy: -4.0}, dict(e.quadratic_terms) - ) - self.assertDictEqualWithZeroDefault({x: 6.0}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_term_first", True), ("_sum_first", False)) - def test_term_sum(self, term_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - term = variables.LinearTerm(x, 2) - linear_sum = variables.LinearSum((x, -2 * y, 3)) - if term_first: - s = term * linear_sum - else: - s = linear_sum * term - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - {xx: 2.0, xy: -4.0}, dict(e.quadratic_terms) - ) - self.assertDictEqualWithZeroDefault({x: 6.0}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_term_first", True), ("_prod_first", False)) - def test_term_prod(self, term_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xy = variables.QuadraticTermKey(x, y) - term = variables.LinearTerm(x, 2) - prod = variables.LinearProduct(2.0, y) - if term_first: - s = term * prod - else: - s = prod * term - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault({xy: 4.0}, dict(e.quadratic_terms)) - self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_expr_first", True), ("_sum_first", False)) - def test_expr_sum(self, expr_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - expr = variables.LinearExpression(-x + y + 1) - linear_sum = variables.LinearSum((x, -2 * y, 3)) - if expr_first: - s = expr * linear_sum - else: - s = linear_sum * expr - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - { - xx: -1.0, - xy: 3.0, - yy: -2.0, - }, - dict(e.quadratic_terms), - ) - self.assertDictEqualWithZeroDefault({x: -2.0, y: 1.0}, dict(e.linear_terms)) - self.assertEqual(3.0, e.offset) - - @parameterized.named_parameters(("_expr_first", True), ("_prod_first", False)) - def test_expr_prod(self, expr_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - expr = variables.LinearExpression(-x + y + 1) - prod = variables.LinearProduct(2.0, y) - if expr_first: - s = expr * prod - else: - s = prod * expr - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - {xy: -2.0, yy: 2.0}, dict(e.quadratic_terms) - ) - self.assertDictEqualWithZeroDefault({y: 2.0}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) - - @parameterized.named_parameters(("_sum_first", True), ("_prod_first", False)) - def test_sum_prod(self, sum_first: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - linear_sum = variables.LinearSum((-x, y, 1)) - prod = variables.LinearProduct(2.0, y) - if sum_first: - s = linear_sum * prod - else: - s = prod * linear_sum - self.assertIsInstance(s, variables.LinearLinearProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertDictEqualWithZeroDefault( - {xy: -2.0, yy: 2.0}, dict(e.quadratic_terms) - ) - self.assertDictEqualWithZeroDefault({y: 2.0}, dict(e.linear_terms)) - self.assertEqual(0.0, e.offset) + def assertDictEqualWithZeroDefault( + self, dict1: dict[Any, float], dict2: dict[Any, float] + ) -> None: + for key in dict1.keys(): + if key not in dict2: + dict2[key] = 0.0 + for key in dict2.keys(): + if key not in dict1: + dict1[key] = 0.0 + self.assertDictEqual(dict1, dict2) + + @parameterized.named_parameters(("_x_first", True), ("_y_first", False)) + def test_var_var(self, x_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xy = variables.QuadraticTermKey(x, y) + if x_first: + s = x * y + else: + s = y * x + self.assertIsInstance(s, variables.QuadraticTerm) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault({xy: 1.0}, dict(e.quadratic_terms)) + self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_var_first", True), ("_term_first", False)) + def test_term_term(self, var_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xy = variables.QuadraticTermKey(x, y) + if var_first: + s = variables.LinearTerm(x, 2) * variables.LinearTerm(y, 3) + else: + s = variables.LinearTerm(x, 3) * variables.LinearTerm(y, 2) + self.assertIsInstance(s, variables.QuadraticTerm) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault({xy: 6.0}, dict(e.quadratic_terms)) + self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters( + ("_expr1_first", True), ("_expr2_first", False) + ) + def test_expr_expr(self, expr1_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + expr1 = variables.LinearExpression(x - 2 * y + 3) + expr2 = variables.LinearExpression(-x + y + 1) + if expr1_first: + s = expr1 * expr2 + else: + s = expr2 * expr1 + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + { + xx: -1.0, + xy: 3.0, + yy: -2.0, + }, + dict(e.quadratic_terms), + ) + self.assertDictEqualWithZeroDefault({x: -2.0, y: 1.0}, dict(e.linear_terms)) + self.assertEqual(3.0, e.offset) + + @parameterized.named_parameters(("_sum1_first", True), ("_sum2_first", False)) + def test_sum_sum(self, sum1_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + sum1 = variables.LinearSum((x, -2 * y, 3)) + sum2 = variables.LinearSum((-x, y, 1)) + if sum1_first: + s = sum1 * sum2 + else: + s = sum2 * sum1 + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + { + xx: -1.0, + xy: 3.0, + yy: -2.0, + }, + dict(e.quadratic_terms), + ) + self.assertDictEqualWithZeroDefault({x: -2.0, y: 1.0}, dict(e.linear_terms)) + self.assertEqual(3.0, e.offset) + + @parameterized.named_parameters( + ("_prod1_first", True), ("_prod2_first", False) + ) + def test_prod_prod(self, prod1_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + prod1 = variables.LinearProduct(2.0, x) + prod2 = variables.LinearProduct(3.0, x + 2 * y - 1) + if prod1_first: + s = prod1 * prod2 + else: + s = prod2 * prod1 + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + { + xx: 6.0, + xy: 12.0, + }, + dict(e.quadratic_terms), + ) + self.assertDictEqualWithZeroDefault({x: -6.0}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_var_first", True), ("_term_first", False)) + def test_var_term(self, var_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xy = variables.QuadraticTermKey(x, y) + term = variables.LinearTerm(y, 2) + if var_first: + s = x * term + else: + s = term * x + self.assertIsInstance(s, variables.QuadraticTerm) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault({xy: 2.0}, dict(e.quadratic_terms)) + self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_var_first", True), ("_expr_first", False)) + def test_var_expr(self, var_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + expr = variables.LinearExpression(x - 2 * y + 3) + if var_first: + s = x * expr + else: + s = expr * x + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + {xx: 1.0, xy: -2.0}, dict(e.quadratic_terms) + ) + self.assertDictEqualWithZeroDefault({x: 3.0}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_var_first", True), ("_sum_first", False)) + def test_var_sum(self, var_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + linear_sum = variables.LinearSum((x, -2 * y, 3)) + if var_first: + s = x * linear_sum + else: + s = linear_sum * x + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + {xx: 1.0, xy: -2.0}, dict(e.quadratic_terms) + ) + self.assertDictEqualWithZeroDefault({x: 3.0}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_var_first", True), ("_prod_first", False)) + def test_var_prod(self, var_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xy = variables.QuadraticTermKey(x, y) + expr = variables.LinearProduct(2.0, y) + if var_first: + s = x * expr + else: + s = expr * x + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault({xy: 2.0}, dict(e.quadratic_terms)) + self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_term_first", True), ("_expr_first", False)) + def test_term_expr(self, term_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + term = variables.LinearTerm(x, 2) + expr = variables.LinearExpression(x - 2 * y + 3) + if term_first: + s = term * expr + else: + s = expr * term + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + {xx: 2.0, xy: -4.0}, dict(e.quadratic_terms) + ) + self.assertDictEqualWithZeroDefault({x: 6.0}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_term_first", True), ("_sum_first", False)) + def test_term_sum(self, term_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + term = variables.LinearTerm(x, 2) + linear_sum = variables.LinearSum((x, -2 * y, 3)) + if term_first: + s = term * linear_sum + else: + s = linear_sum * term + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + {xx: 2.0, xy: -4.0}, dict(e.quadratic_terms) + ) + self.assertDictEqualWithZeroDefault({x: 6.0}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_term_first", True), ("_prod_first", False)) + def test_term_prod(self, term_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xy = variables.QuadraticTermKey(x, y) + term = variables.LinearTerm(x, 2) + prod = variables.LinearProduct(2.0, y) + if term_first: + s = term * prod + else: + s = prod * term + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault({xy: 4.0}, dict(e.quadratic_terms)) + self.assertDictEqualWithZeroDefault({}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_expr_first", True), ("_sum_first", False)) + def test_expr_sum(self, expr_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + expr = variables.LinearExpression(-x + y + 1) + linear_sum = variables.LinearSum((x, -2 * y, 3)) + if expr_first: + s = expr * linear_sum + else: + s = linear_sum * expr + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + { + xx: -1.0, + xy: 3.0, + yy: -2.0, + }, + dict(e.quadratic_terms), + ) + self.assertDictEqualWithZeroDefault({x: -2.0, y: 1.0}, dict(e.linear_terms)) + self.assertEqual(3.0, e.offset) + + @parameterized.named_parameters(("_expr_first", True), ("_prod_first", False)) + def test_expr_prod(self, expr_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + expr = variables.LinearExpression(-x + y + 1) + prod = variables.LinearProduct(2.0, y) + if expr_first: + s = expr * prod + else: + s = prod * expr + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + {xy: -2.0, yy: 2.0}, dict(e.quadratic_terms) + ) + self.assertDictEqualWithZeroDefault({y: 2.0}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) + + @parameterized.named_parameters(("_sum_first", True), ("_prod_first", False)) + def test_sum_prod(self, sum_first: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + linear_sum = variables.LinearSum((-x, y, 1)) + prod = variables.LinearProduct(2.0, y) + if sum_first: + s = linear_sum * prod + else: + s = prod * linear_sum + self.assertIsInstance(s, variables.LinearLinearProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertDictEqualWithZeroDefault( + {xy: -2.0, yy: 2.0}, dict(e.quadratic_terms) + ) + self.assertDictEqualWithZeroDefault({y: 2.0}, dict(e.linear_terms)) + self.assertEqual(0.0, e.offset) # Test negate on Linear and Quadratic objects. class NegateTest(parameterized.TestCase): - def test_negate_var(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - s = -x - self.assertIsInstance(s, variables.LinearTerm) - e = variables.as_flat_linear_expression(s) - self.assertEqual(0, e.offset) - self.assertDictEqual({x: -1}, dict(e.terms)) - - def test_negate_linear_term(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - term = variables.LinearTerm(x, 0.5) - s = -term - self.assertIsInstance(s, variables.LinearTerm) - e = variables.as_flat_linear_expression(s) - self.assertEqual(0, e.offset) - self.assertDictEqual({x: -0.5}, dict(e.terms)) - - def test_negate_linear_expression(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - expression = variables.LinearExpression(y - 2 * x + 1) - s = -expression - self.assertIsInstance(s, variables.LinearProduct) - e = variables.as_flat_linear_expression(s) - self.assertEqual(-1, e.offset) - self.assertDictEqual({x: 2, y: -1}, dict(e.terms)) - - def test_negate_linear_sum(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - expression = variables.LinearSum((y, -2 * x, 1)) - s = -expression - self.assertIsInstance(s, variables.LinearProduct) - e = variables.as_flat_linear_expression(s) - self.assertEqual(-1, e.offset) - self.assertDictEqual({x: 2, y: -1}, dict(e.terms)) - - def test_negate_ast_product(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - ast_product = variables.LinearProduct(0.5, x) - s = -ast_product - self.assertIsInstance(s, variables.LinearProduct) - e = variables.as_flat_linear_expression(s) - self.assertEqual(0, e.offset) - self.assertDictEqual({x: -0.5}, dict(e.terms)) - - def test_negate_quadratic_term(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - xx = variables.QuadraticTermKey(x, x) - term = variables.QuadraticTerm(xx, 0.5) - s = -term - self.assertIsInstance(s, variables.QuadraticTerm) - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(0, e.offset) - self.assertDictEqual({}, dict(e.linear_terms)) - self.assertDictEqual({xx: -0.5}, dict(e.quadratic_terms)) - - def test_negate_quadratic_expression(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - xy = variables.QuadraticTermKey(x, y) - expression = variables.QuadraticExpression(y - 2 * x + 1 + x * y) - s = -expression - self.assertIsInstance(s, variables.QuadraticProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(-1, e.offset) - self.assertDictEqual({x: 2, y: -1}, dict(e.linear_terms)) - self.assertDictEqual({xy: -1}, dict(e.quadratic_terms)) - - def test_negate_quadratic_sum(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - yy = variables.QuadraticTermKey(y, y) - expression = variables.QuadraticSum((y, -2 * x, 1, -y * y)) - s = -expression - self.assertIsInstance(s, variables.QuadraticProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(-1, e.offset) - self.assertDictEqual({x: 2, y: -1}, dict(e.linear_terms)) - self.assertDictEqual({yy: 1}, dict(e.quadratic_terms)) - - def test_negate_linear_linear_product(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - xx = variables.QuadraticTermKey(x, x) - ast_product = variables.LinearLinearProduct(x, x + 1) - s = -ast_product - self.assertIsInstance(s, variables.QuadraticProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(0, e.offset) - self.assertDictEqual({x: -1}, dict(e.linear_terms)) - self.assertDictEqual({xx: -1}, dict(e.quadratic_terms)) - - def test_negate_quadratic_product(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - xx = variables.QuadraticTermKey(x, x) - ast_product = variables.QuadraticProduct(0.5, x + x * x) - s = -ast_product - self.assertIsInstance(s, variables.QuadraticProduct) - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(0, e.offset) - self.assertDictEqual({x: -0.5}, dict(e.linear_terms)) - self.assertDictEqual({xx: -0.5}, dict(e.quadratic_terms)) + def test_negate_var(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + s = -x + self.assertIsInstance(s, variables.LinearTerm) + e = variables.as_flat_linear_expression(s) + self.assertEqual(0, e.offset) + self.assertDictEqual({x: -1}, dict(e.terms)) + + def test_negate_linear_term(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + term = variables.LinearTerm(x, 0.5) + s = -term + self.assertIsInstance(s, variables.LinearTerm) + e = variables.as_flat_linear_expression(s) + self.assertEqual(0, e.offset) + self.assertDictEqual({x: -0.5}, dict(e.terms)) + + def test_negate_linear_expression(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + expression = variables.LinearExpression(y - 2 * x + 1) + s = -expression + self.assertIsInstance(s, variables.LinearProduct) + e = variables.as_flat_linear_expression(s) + self.assertEqual(-1, e.offset) + self.assertDictEqual({x: 2, y: -1}, dict(e.terms)) + + def test_negate_linear_sum(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + expression = variables.LinearSum((y, -2 * x, 1)) + s = -expression + self.assertIsInstance(s, variables.LinearProduct) + e = variables.as_flat_linear_expression(s) + self.assertEqual(-1, e.offset) + self.assertDictEqual({x: 2, y: -1}, dict(e.terms)) + + def test_negate_ast_product(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + ast_product = variables.LinearProduct(0.5, x) + s = -ast_product + self.assertIsInstance(s, variables.LinearProduct) + e = variables.as_flat_linear_expression(s) + self.assertEqual(0, e.offset) + self.assertDictEqual({x: -0.5}, dict(e.terms)) + + def test_negate_quadratic_term(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + xx = variables.QuadraticTermKey(x, x) + term = variables.QuadraticTerm(xx, 0.5) + s = -term + self.assertIsInstance(s, variables.QuadraticTerm) + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(0, e.offset) + self.assertDictEqual({}, dict(e.linear_terms)) + self.assertDictEqual({xx: -0.5}, dict(e.quadratic_terms)) + + def test_negate_quadratic_expression(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + xy = variables.QuadraticTermKey(x, y) + expression = variables.QuadraticExpression(y - 2 * x + 1 + x * y) + s = -expression + self.assertIsInstance(s, variables.QuadraticProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(-1, e.offset) + self.assertDictEqual({x: 2, y: -1}, dict(e.linear_terms)) + self.assertDictEqual({xy: -1}, dict(e.quadratic_terms)) + + def test_negate_quadratic_sum(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + yy = variables.QuadraticTermKey(y, y) + expression = variables.QuadraticSum((y, -2 * x, 1, -y * y)) + s = -expression + self.assertIsInstance(s, variables.QuadraticProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(-1, e.offset) + self.assertDictEqual({x: 2, y: -1}, dict(e.linear_terms)) + self.assertDictEqual({yy: 1}, dict(e.quadratic_terms)) + + def test_negate_linear_linear_product(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + xx = variables.QuadraticTermKey(x, x) + ast_product = variables.LinearLinearProduct(x, x + 1) + s = -ast_product + self.assertIsInstance(s, variables.QuadraticProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(0, e.offset) + self.assertDictEqual({x: -1}, dict(e.linear_terms)) + self.assertDictEqual({xx: -1}, dict(e.quadratic_terms)) + + def test_negate_quadratic_product(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + xx = variables.QuadraticTermKey(x, x) + ast_product = variables.QuadraticProduct(0.5, x + x * x) + s = -ast_product + self.assertIsInstance(s, variables.QuadraticProduct) + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(0, e.offset) + self.assertDictEqual({x: -0.5}, dict(e.linear_terms)) + self.assertDictEqual({xx: -0.5}, dict(e.quadratic_terms)) class UnsupportedProductOperandTestParams(NamedTuple): - lhs_type: str - rhs_type: str + lhs_type: str + rhs_type: str - def test_suffix(self): - return f"_{self.lhs_type}_{self.rhs_type}" + def test_suffix(self): + return f"_{self.lhs_type}_{self.rhs_type}" def all_unsupported_product_operand_params() -> ( List[UnsupportedProductOperandTestParams] ): - result = [] - for lhs_type in _LINEAR_TYPES: - result.append( - UnsupportedProductOperandTestParams(lhs_type=lhs_type, rhs_type="complex") + result = [] + for lhs_type in _LINEAR_TYPES: + result.append( + UnsupportedProductOperandTestParams( + lhs_type=lhs_type, rhs_type="complex" ) - for lhs_type in _QUADRATIC_TYPES + ("complex",): - for rhs_type in _QUADRATIC_TYPES + ("complex",): - if lhs_type == "complex" and rhs_type == "complex": - continue - result.append( - UnsupportedProductOperandTestParams( - lhs_type=lhs_type, rhs_type=rhs_type - ) - ) - return result + ) + for lhs_type in _QUADRATIC_TYPES + ("complex",): + for rhs_type in _QUADRATIC_TYPES + ("complex",): + if lhs_type == "complex" and rhs_type == "complex": + continue + result.append( + UnsupportedProductOperandTestParams( + lhs_type=lhs_type, rhs_type=rhs_type + ) + ) + return result def all_unsupported_division_operand_params() -> ( List[UnsupportedProductOperandTestParams] ): - result = [] - for lhs_type in _LINEAR_TYPES + _QUADRATIC_TYPES + ("complex",): - for rhs_type in _LINEAR_TYPES + _QUADRATIC_TYPES + ("complex",): - if lhs_type == "complex" and rhs_type == "complex": - continue - result.append( - UnsupportedProductOperandTestParams( - lhs_type=lhs_type, rhs_type=rhs_type - ) - ) - return result + result = [] + for lhs_type in _LINEAR_TYPES + _QUADRATIC_TYPES + ("complex",): + for rhs_type in _LINEAR_TYPES + _QUADRATIC_TYPES + ("complex",): + if lhs_type == "complex" and rhs_type == "complex": + continue + result.append( + UnsupportedProductOperandTestParams( + lhs_type=lhs_type, rhs_type=rhs_type + ) + ) + return result def get_linear_or_quadratic_for_unsupported_operand_test( type_string: str, ) -> Union[variables.LinearBase, variables.QuadraticBase, complex]: - mod_ = model.Model() - x = mod_.add_binary_variable(name="x") - y = mod_.add_binary_variable(name="y") - xy = variables.QuadraticTermKey(x, y) - if type_string == "Variable": - return x - elif type_string == "LinearTerm": - return variables.LinearTerm(x, 2) - elif type_string == "LinearExpression": - return variables.LinearExpression(x - 2 * y + 3) - elif type_string == "LinearSum": - return variables.LinearSum((x, -2 * y, 3)) - elif type_string == "LinearProduct": - return variables.LinearProduct(2, x) - elif type_string == "QuadraticTerm": - return variables.QuadraticTerm(xy, 5) - elif type_string == "QuadraticExpression": - return variables.QuadraticExpression(x - 2 * y + 1 + 3 * x * y - 4 * y * y) - elif type_string == "QuadraticSum": - return variables.QuadraticSum((x, -2 * y, 1, y * x, -2 * x * x)) - elif type_string == "LinearLinearProduct": - return variables.LinearLinearProduct(x, x + y) - elif type_string == "QuadraticProduct": - return variables.QuadraticProduct(2, x * (x + y)) - elif type_string == "complex": - return 6j - else: - raise AssertionError(f"unknown linear or quadratic type: {type_string!r}") + mod_ = model.Model() + x = mod_.add_binary_variable(name="x") + y = mod_.add_binary_variable(name="y") + xy = variables.QuadraticTermKey(x, y) + if type_string == "Variable": + return x + elif type_string == "LinearTerm": + return variables.LinearTerm(x, 2) + elif type_string == "LinearExpression": + return variables.LinearExpression(x - 2 * y + 3) + elif type_string == "LinearSum": + return variables.LinearSum((x, -2 * y, 3)) + elif type_string == "LinearProduct": + return variables.LinearProduct(2, x) + elif type_string == "QuadraticTerm": + return variables.QuadraticTerm(xy, 5) + elif type_string == "QuadraticExpression": + return variables.QuadraticExpression(x - 2 * y + 1 + 3 * x * y - 4 * y * y) + elif type_string == "QuadraticSum": + return variables.QuadraticSum((x, -2 * y, 1, y * x, -2 * x * x)) + elif type_string == "LinearLinearProduct": + return variables.LinearLinearProduct(x, x + y) + elif type_string == "QuadraticProduct": + return variables.QuadraticProduct(2, x * (x + y)) + elif type_string == "complex": + return 6j + else: + raise AssertionError(f"unknown linear or quadratic type: {type_string!r}") class UnsupportedProductOperandTest(parameterized.TestCase): - @parameterized.named_parameters( - (p.test_suffix(), p.lhs_type, p.rhs_type) - for p in all_unsupported_product_operand_params() - ) - def test_mult(self, lhs_type: str, rhs_type: str) -> None: - lhs = get_linear_or_quadratic_for_unsupported_operand_test(lhs_type) - rhs = get_linear_or_quadratic_for_unsupported_operand_test(rhs_type) - - expected_string = f"unsupported operand.*[*].*{lhs_type}.*and.*{rhs_type}" - - # pylint: disable=pointless-statement - # pytype: disable=unsupported-operands - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, expected_string): - lhs * rhs - - with self.assertRaisesRegex(TypeError, expected_string): - lhs *= rhs - # pylint: enable=pointless-statement - # pytype: enable=unsupported-operands - # pytype: enable=wrong-arg-types - - @parameterized.named_parameters( - (p.test_suffix(), p.lhs_type, p.rhs_type) - for p in all_unsupported_division_operand_params() - ) - def test_div(self, lhs_type: str, rhs_type: str) -> None: - lhs = get_linear_or_quadratic_for_unsupported_operand_test(lhs_type) - rhs = get_linear_or_quadratic_for_unsupported_operand_test(rhs_type) - - expected_string = f"unsupported operand.*[/].*{lhs_type}.*and.*{rhs_type}" - - # pylint: disable=pointless-statement - # pytype: disable=unsupported-operands - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, expected_string): - lhs / rhs - - if lhs_type == "str": - expected_string = f"unsupported operand.*[/].*{lhs_type}.*and.*{rhs_type}" - with self.assertRaisesRegex(TypeError, expected_string): - lhs /= rhs - # pylint: enable=pointless-statement - # pytype: enable=unsupported-operands - # pytype: enable=wrong-arg-types + @parameterized.named_parameters( + (p.test_suffix(), p.lhs_type, p.rhs_type) + for p in all_unsupported_product_operand_params() + ) + def test_mult(self, lhs_type: str, rhs_type: str) -> None: + lhs = get_linear_or_quadratic_for_unsupported_operand_test(lhs_type) + rhs = get_linear_or_quadratic_for_unsupported_operand_test(rhs_type) + + expected_string = f"unsupported operand.*[*].*{lhs_type}.*and.*{rhs_type}" + + # pylint: disable=pointless-statement + # pytype: disable=unsupported-operands + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, expected_string): + lhs * rhs + + with self.assertRaisesRegex(TypeError, expected_string): + lhs *= rhs + # pylint: enable=pointless-statement + # pytype: enable=unsupported-operands + # pytype: enable=wrong-arg-types + + @parameterized.named_parameters( + (p.test_suffix(), p.lhs_type, p.rhs_type) + for p in all_unsupported_division_operand_params() + ) + def test_div(self, lhs_type: str, rhs_type: str) -> None: + lhs = get_linear_or_quadratic_for_unsupported_operand_test(lhs_type) + rhs = get_linear_or_quadratic_for_unsupported_operand_test(rhs_type) + + expected_string = f"unsupported operand.*[/].*{lhs_type}.*and.*{rhs_type}" + + # pylint: disable=pointless-statement + # pytype: disable=unsupported-operands + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, expected_string): + lhs / rhs + + if lhs_type == "str": + expected_string = f"unsupported operand.*[/].*{lhs_type}.*and.*{rhs_type}" + with self.assertRaisesRegex(TypeError, expected_string): + lhs /= rhs + # pylint: enable=pointless-statement + # pytype: enable=unsupported-operands + # pytype: enable=wrong-arg-types class UnsupportedAdditionOperandTestParams(NamedTuple): - linear_or_quadratic_type: str - linear_or_quadratic_first: bool + linear_or_quadratic_type: str + linear_or_quadratic_first: bool - def test_suffix(self): - if self.linear_or_quadratic_first: - return f"_{self.linear_or_quadratic_type}_str" - else: - return f"_str_{self.linear_or_quadratic_type}" + def test_suffix(self): + if self.linear_or_quadratic_first: + return f"_{self.linear_or_quadratic_type}_str" + else: + return f"_str_{self.linear_or_quadratic_type}" def all_unsupported_addition_operand_params() -> ( List[UnsupportedAdditionOperandTestParams] ): - result = [] - for linear_or_quadratic_type in _LINEAR_TYPES + _QUADRATIC_TYPES: - for linear_or_quadratic_first in (True, False): - result.append( - UnsupportedAdditionOperandTestParams( - linear_or_quadratic_type=linear_or_quadratic_type, - linear_or_quadratic_first=linear_or_quadratic_first, - ) - ) - return result + result = [] + for linear_or_quadratic_type in _LINEAR_TYPES + _QUADRATIC_TYPES: + for linear_or_quadratic_first in (True, False): + result.append( + UnsupportedAdditionOperandTestParams( + linear_or_quadratic_type=linear_or_quadratic_type, + linear_or_quadratic_first=linear_or_quadratic_first, + ) + ) + return result @parameterized.named_parameters( @@ -2546,511 +2592,523 @@ def all_unsupported_addition_operand_params() -> ( ) class UnsupportedAdditionOperandTest(parameterized.TestCase): - def test_add( - self, linear_or_quadratic_type: str, linear_or_quadratic_first: bool - ) -> None: - linear_or_quadratic = get_linear_or_quadratic_for_unsupported_operand_test( - linear_or_quadratic_type - ) - other = 6j - - expected_string = r"unsupported operand type\(s\) for \+.*" - if linear_or_quadratic_first: - expected_string += ( - f"{linear_or_quadratic_type}.*and.*{type(other).__name__}.*" - ) - else: - expected_string += ( - f"{type(other).__name__}.*and.*{linear_or_quadratic_type}.*" - ) - - # pylint: disable=pointless-statement - # pytype: disable=unsupported-operands - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, expected_string): - if linear_or_quadratic_first: - linear_or_quadratic + other - else: - other + linear_or_quadratic - - with self.assertRaisesRegex(TypeError, expected_string): - if linear_or_quadratic_first: - linear_or_quadratic += other - else: - other += linear_or_quadratic - # pylint: enable=pointless-statement - # pytype: enable=unsupported-operands - # pytype: enable=wrong-arg-types - - def test_sub( - self, linear_or_quadratic_type: str, linear_or_quadratic_first: bool - ) -> None: - linear_or_quadratic = get_linear_or_quadratic_for_unsupported_operand_test( - linear_or_quadratic_type - ) - other = 6j + def test_add( + self, linear_or_quadratic_type: str, linear_or_quadratic_first: bool + ) -> None: + linear_or_quadratic = get_linear_or_quadratic_for_unsupported_operand_test( + linear_or_quadratic_type + ) + other = 6j - expected_string = "unsupported operand type[(]s[)] for [-].*" - if linear_or_quadratic_first: - expected_string += ( - f"{linear_or_quadratic_type}.*and.*{type(other).__name__}.*" - ) - else: - expected_string += ( - f"{type(other).__name__}.*and.*{linear_or_quadratic_type}.*" - ) + expected_string = r"unsupported operand type\(s\) for \+.*" + if linear_or_quadratic_first: + expected_string += ( + f"{linear_or_quadratic_type}.*and.*{type(other).__name__}.*" + ) + else: + expected_string += ( + f"{type(other).__name__}.*and.*{linear_or_quadratic_type}.*" + ) + + # pylint: disable=pointless-statement + # pytype: disable=unsupported-operands + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, expected_string): + if linear_or_quadratic_first: + linear_or_quadratic + other + else: + other + linear_or_quadratic + + with self.assertRaisesRegex(TypeError, expected_string): + if linear_or_quadratic_first: + linear_or_quadratic += other + else: + other += linear_or_quadratic + # pylint: enable=pointless-statement + # pytype: enable=unsupported-operands + # pytype: enable=wrong-arg-types + + def test_sub( + self, linear_or_quadratic_type: str, linear_or_quadratic_first: bool + ) -> None: + linear_or_quadratic = get_linear_or_quadratic_for_unsupported_operand_test( + linear_or_quadratic_type + ) + other = 6j - # pylint: disable=pointless-statement - # pytype: disable=unsupported-operands - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, expected_string): - if linear_or_quadratic_first: - linear_or_quadratic - other - else: - other - linear_or_quadratic - - with self.assertRaisesRegex(TypeError, expected_string): - if linear_or_quadratic_first: - linear_or_quadratic -= other - else: - other -= linear_or_quadratic - # pylint: enable=pointless-statement - # pytype: enable=unsupported-operands - # pytype: enable=wrong-arg-types + expected_string = "unsupported operand type[(]s[)] for [-].*" + if linear_or_quadratic_first: + expected_string += ( + f"{linear_or_quadratic_type}.*and.*{type(other).__name__}.*" + ) + else: + expected_string += ( + f"{type(other).__name__}.*and.*{linear_or_quadratic_type}.*" + ) + + # pylint: disable=pointless-statement + # pytype: disable=unsupported-operands + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, expected_string): + if linear_or_quadratic_first: + linear_or_quadratic - other + else: + other - linear_or_quadratic + + with self.assertRaisesRegex(TypeError, expected_string): + if linear_or_quadratic_first: + linear_or_quadratic -= other + else: + other -= linear_or_quadratic + # pylint: enable=pointless-statement + # pytype: enable=unsupported-operands + # pytype: enable=wrong-arg-types class UnsupportedInitializationTest(parameterized.TestCase): - def test_linear_sum_not_tuple(self): - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, "object is not iterable"): - variables.LinearSum(2.0) - # pytype: enable=wrong-arg-types - - def test_linear_sum_not_linear_in_tuple(self): - mod = model.Model() - x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, "unsupported type in iterable argument"): - variables.LinearSum((2.0, x * x)) - # pytype: enable=wrong-arg-types - - def test_quadratic_sum_not_tuple(self): - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, "object is not iterable"): - variables.QuadraticSum(2.0) - # pytype: enable=wrong-arg-types - - def test_quadratic_sum_not_linear_in_tuple(self): - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex(TypeError, "unsupported type in iterable argument"): - variables.QuadraticSum((2.0, "string")) - # pytype: enable=wrong-arg-types - - def test_linear_product_not_scalar(self): - mod = model.Model() - x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex( - TypeError, "unsupported type for scalar argument in LinearProduct" - ): - variables.LinearProduct(x, x) - # pytype: enable=wrong-arg-types - - def test_linear_product_not_linear(self): - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex( - TypeError, "unsupported type for linear argument in LinearProduct" - ): - variables.LinearProduct(2.0, "string") - # pytype: enable=wrong-arg-types - - def test_quadratic_product_not_scalar(self): - mod = model.Model() - x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex( - TypeError, "unsupported type for scalar argument in QuadraticProduct" - ): - variables.QuadraticProduct(x, x) - # pytype: enable=wrong-arg-types - - def test_quadratic_product_not_quadratic(self): - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex( - TypeError, "unsupported type for linear argument in QuadraticProduct" - ): - variables.QuadraticProduct(2.0, "string") - # pytype: enable=wrong-arg-types - - def test_linear_linear_product_first_not_linear(self): - mod = model.Model() - x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex( - TypeError, - "unsupported type for first_linear argument in LinearLinearProduct", - ): - variables.LinearLinearProduct("string", x) - # pytype: enable=wrong-arg-types - - def test_linear_linear_product_second_not_linear(self): - mod = model.Model() - x = mod.add_binary_variable(name="x") - # pytype: disable=wrong-arg-types - with self.assertRaisesRegex( - TypeError, - "unsupported type for second_linear argument in LinearLinearProduct", - ): - variables.LinearLinearProduct(x, "string") - # pytype: enable=wrong-arg-types + def test_linear_sum_not_tuple(self): + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, "object is not iterable"): + variables.LinearSum(2.0) + # pytype: enable=wrong-arg-types + + def test_linear_sum_not_linear_in_tuple(self): + mod = model.Model() + x = mod.add_binary_variable(name="x") + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( + TypeError, "unsupported type in iterable argument" + ): + variables.LinearSum((2.0, x * x)) + # pytype: enable=wrong-arg-types + + def test_quadratic_sum_not_tuple(self): + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex(TypeError, "object is not iterable"): + variables.QuadraticSum(2.0) + # pytype: enable=wrong-arg-types + + def test_quadratic_sum_not_linear_in_tuple(self): + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( + TypeError, "unsupported type in iterable argument" + ): + variables.QuadraticSum((2.0, "string")) + # pytype: enable=wrong-arg-types + + def test_linear_product_not_scalar(self): + mod = model.Model() + x = mod.add_binary_variable(name="x") + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( + TypeError, "unsupported type for scalar argument in LinearProduct" + ): + variables.LinearProduct(x, x) + # pytype: enable=wrong-arg-types + + def test_linear_product_not_linear(self): + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( + TypeError, "unsupported type for linear argument in LinearProduct" + ): + variables.LinearProduct(2.0, "string") + # pytype: enable=wrong-arg-types + + def test_quadratic_product_not_scalar(self): + mod = model.Model() + x = mod.add_binary_variable(name="x") + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( + TypeError, "unsupported type for scalar argument in QuadraticProduct" + ): + variables.QuadraticProduct(x, x) + # pytype: enable=wrong-arg-types + + def test_quadratic_product_not_quadratic(self): + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( + TypeError, "unsupported type for linear argument in QuadraticProduct" + ): + variables.QuadraticProduct(2.0, "string") + # pytype: enable=wrong-arg-types + + def test_linear_linear_product_first_not_linear(self): + mod = model.Model() + x = mod.add_binary_variable(name="x") + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( + TypeError, + "unsupported type for first_linear argument in LinearLinearProduct", + ): + variables.LinearLinearProduct("string", x) + # pytype: enable=wrong-arg-types + + def test_linear_linear_product_second_not_linear(self): + mod = model.Model() + x = mod.add_binary_variable(name="x") + # pytype: disable=wrong-arg-types + with self.assertRaisesRegex( + TypeError, + "unsupported type for second_linear argument in LinearLinearProduct", + ): + variables.LinearLinearProduct(x, "string") + # pytype: enable=wrong-arg-types @parameterized.named_parameters(("_python_sum", True), ("LinearSum", False)) class SumTest(parameterized.TestCase): - def test_sum_vars(self, python_sum: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - array = [x, z, x, x, y] - if python_sum: - s = sum(array) + 8.0 - else: - s = variables.LinearSum(array) + 8.0 - e = variables.as_flat_linear_expression(s) - self.assertEqual(8.0, e.offset) - self.assertDictEqual({x: 3, y: 1, z: 1}, dict(e.terms)) - - def test_sum_linear_terms(self, python_sum: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - array = [1.25 * x, z, x, x, y, -0.5 * y, 1.0] - if python_sum: - s = sum(array) + 8.0 - else: - s = variables.LinearSum(array) + 8.0 - e = variables.as_flat_linear_expression(s) - self.assertEqual(9.0, e.offset) - self.assertDictEqual({x: 3.25, y: 0.5, z: 1}, dict(e.terms)) - - def test_sum_quadratic_terms(self, python_sum: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - array = [1.25 * x, z, x, x, y, -0.5 * y, 1.0, 2.5 * x * x, -x * y] - if python_sum: - s = sum(array) + 8.0 - else: - s = variables.QuadraticSum(array) + 8.0 - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(9.0, e.offset) - self.assertDictEqual({x: 3.25, y: 0.5, z: 1}, dict(e.linear_terms)) - self.assertDictEqual({xx: 2.5, xy: -1}, dict(e.quadratic_terms)) - - def test_sum_linear_expression(self, python_sum: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - array = [1.25 * x + z, x, x + y, -0.5 * y + 1.0] - if python_sum: - s = sum(array) + 8.0 - else: - s = variables.LinearSum(array) + 8.0 - e = variables.as_flat_linear_expression(s) - self.assertEqual(9.0, e.offset) - self.assertDictEqual({x: 3.25, y: 0.5, z: 1}, dict(e.terms)) - - def test_sum_quadratic_expression(self, python_sum: bool) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - array = [ - 1.25 * x + z, - x, - x + y, - -0.5 * y + 1.0, - 2.5 * x * x - x * y, - x * z + y * z, - ] - if python_sum: - s = sum(array) + 8.0 - else: - s = variables.QuadraticSum(array) + 8.0 - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(9.0, e.offset) - self.assertDictEqual({x: 3.25, y: 0.5, z: 1}, dict(e.linear_terms)) - self.assertDictEqual( - { - xx: 2.5, - xy: -1, - variables.QuadraticTermKey(x, z): 1.0, - variables.QuadraticTermKey(y, z): 1, - }, - dict(e.quadratic_terms), - ) + def test_sum_vars(self, python_sum: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + array = [x, z, x, x, y] + if python_sum: + s = sum(array) + 8.0 + else: + s = variables.LinearSum(array) + 8.0 + e = variables.as_flat_linear_expression(s) + self.assertEqual(8.0, e.offset) + self.assertDictEqual({x: 3, y: 1, z: 1}, dict(e.terms)) + + def test_sum_linear_terms(self, python_sum: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + array = [1.25 * x, z, x, x, y, -0.5 * y, 1.0] + if python_sum: + s = sum(array) + 8.0 + else: + s = variables.LinearSum(array) + 8.0 + e = variables.as_flat_linear_expression(s) + self.assertEqual(9.0, e.offset) + self.assertDictEqual({x: 3.25, y: 0.5, z: 1}, dict(e.terms)) + + def test_sum_quadratic_terms(self, python_sum: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + array = [1.25 * x, z, x, x, y, -0.5 * y, 1.0, 2.5 * x * x, -x * y] + if python_sum: + s = sum(array) + 8.0 + else: + s = variables.QuadraticSum(array) + 8.0 + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(9.0, e.offset) + self.assertDictEqual({x: 3.25, y: 0.5, z: 1}, dict(e.linear_terms)) + self.assertDictEqual({xx: 2.5, xy: -1}, dict(e.quadratic_terms)) + + def test_sum_linear_expression(self, python_sum: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + array = [1.25 * x + z, x, x + y, -0.5 * y + 1.0] + if python_sum: + s = sum(array) + 8.0 + else: + s = variables.LinearSum(array) + 8.0 + e = variables.as_flat_linear_expression(s) + self.assertEqual(9.0, e.offset) + self.assertDictEqual({x: 3.25, y: 0.5, z: 1}, dict(e.terms)) + + def test_sum_quadratic_expression(self, python_sum: bool) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + array = [ + 1.25 * x + z, + x, + x + y, + -0.5 * y + 1.0, + 2.5 * x * x - x * y, + x * z + y * z, + ] + if python_sum: + s = sum(array) + 8.0 + else: + s = variables.QuadraticSum(array) + 8.0 + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(9.0, e.offset) + self.assertDictEqual({x: 3.25, y: 0.5, z: 1}, dict(e.linear_terms)) + self.assertDictEqual( + { + xx: 2.5, + xy: -1, + variables.QuadraticTermKey(x, z): 1.0, + variables.QuadraticTermKey(y, z): 1, + }, + dict(e.quadratic_terms), + ) - def test_generator_sum_vars(self, python_sum: bool) -> None: - mod = model.Model() - x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] - if python_sum: - s = sum(x[i] for i in range(3)) + 8.0 - else: - s = variables.LinearSum(x[i] for i in range(3)) + 8.0 - e = variables.as_flat_linear_expression(s) - self.assertEqual(8.0, e.offset) - self.assertDictEqual({x[0]: 1, x[1]: 1, x[2]: 1}, dict(e.terms)) - - def test_generator_sum_terms(self, python_sum: bool) -> None: - mod = model.Model() - x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] - if python_sum: - s = sum(i * x[i] for i in range(3)) + 8.0 - else: - s = variables.LinearSum(i * x[i] for i in range(3)) + 8.0 - e = variables.as_flat_linear_expression(s) - self.assertEqual(8.0, e.offset) - self.assertDictEqual({x[0]: 0, x[1]: 1, x[2]: 2}, dict(e.terms)) - - def test_generator_sum_quadratic_terms(self, python_sum: bool) -> None: - mod = model.Model() - x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(4)] - if python_sum: - s = sum(i * x[i] * x[i + 1] for i in range(3)) + 8.0 - else: - s = variables.QuadraticSum(i * x[i] * x[i + 1] for i in range(3)) + 8.0 - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(8.0, e.offset) - self.assertDictEqual({}, dict(e.linear_terms)) - self.assertDictEqual( - { - variables.QuadraticTermKey(x[0], x[1]): 0, - variables.QuadraticTermKey(x[1], x[2]): 1, - variables.QuadraticTermKey(x[2], x[3]): 2, - }, - dict(e.quadratic_terms), - ) + def test_generator_sum_vars(self, python_sum: bool) -> None: + mod = model.Model() + x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] + if python_sum: + s = sum(x[i] for i in range(3)) + 8.0 + else: + s = variables.LinearSum(x[i] for i in range(3)) + 8.0 + e = variables.as_flat_linear_expression(s) + self.assertEqual(8.0, e.offset) + self.assertDictEqual({x[0]: 1, x[1]: 1, x[2]: 1}, dict(e.terms)) + + def test_generator_sum_terms(self, python_sum: bool) -> None: + mod = model.Model() + x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] + if python_sum: + s = sum(i * x[i] for i in range(3)) + 8.0 + else: + s = variables.LinearSum(i * x[i] for i in range(3)) + 8.0 + e = variables.as_flat_linear_expression(s) + self.assertEqual(8.0, e.offset) + self.assertDictEqual({x[0]: 0, x[1]: 1, x[2]: 2}, dict(e.terms)) + + def test_generator_sum_quadratic_terms(self, python_sum: bool) -> None: + mod = model.Model() + x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(4)] + if python_sum: + s = sum(i * x[i] * x[i + 1] for i in range(3)) + 8.0 + else: + s = variables.QuadraticSum(i * x[i] * x[i + 1] for i in range(3)) + 8.0 + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(8.0, e.offset) + self.assertDictEqual({}, dict(e.linear_terms)) + self.assertDictEqual( + { + variables.QuadraticTermKey(x[0], x[1]): 0, + variables.QuadraticTermKey(x[1], x[2]): 1, + variables.QuadraticTermKey(x[2], x[3]): 2, + }, + dict(e.quadratic_terms), + ) - def test_generator_sum_expression(self, python_sum: bool) -> None: - mod = model.Model() - x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] - if python_sum: - s = sum(2 * x[i] - x[i + 1] + 1.5 for i in range(2)) + 8.0 - else: - s = variables.LinearSum(2 * x[i] - x[i + 1] + 1.5 for i in range(2)) + 8.0 - e = variables.as_flat_linear_expression(s) - self.assertEqual(11.0, e.offset) - self.assertDictEqual({x[0]: 2, x[1]: 1, x[2]: -1}, dict(e.terms)) - - def test_generator_quadratic_sum_expression(self, python_sum: bool) -> None: - mod = model.Model() - x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] - if python_sum: - s = sum(2 * x[i] - x[i + 1] + 1.5 + x[i] * x[i + 1] for i in range(2)) + 8.0 - else: - s = ( - variables.QuadraticSum( - 2 * x[i] - x[i + 1] + 1.5 + x[i] * x[i + 1] for i in range(2) - ) - + 8.0 - ) - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(11.0, e.offset) - self.assertDictEqual({x[0]: 2, x[1]: 1, x[2]: -1}, dict(e.linear_terms)) - self.assertDictEqual( - { - variables.QuadraticTermKey(x[0], x[1]): 1, - variables.QuadraticTermKey(x[1], x[2]): 1, - }, - dict(e.quadratic_terms), - ) + def test_generator_sum_expression(self, python_sum: bool) -> None: + mod = model.Model() + x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] + if python_sum: + s = sum(2 * x[i] - x[i + 1] + 1.5 for i in range(2)) + 8.0 + else: + s = variables.LinearSum(2 * x[i] - x[i + 1] + 1.5 for i in range(2)) + 8.0 + e = variables.as_flat_linear_expression(s) + self.assertEqual(11.0, e.offset) + self.assertDictEqual({x[0]: 2, x[1]: 1, x[2]: -1}, dict(e.terms)) + + def test_generator_quadratic_sum_expression(self, python_sum: bool) -> None: + mod = model.Model() + x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] + if python_sum: + s = ( + sum(2 * x[i] - x[i + 1] + 1.5 + x[i] * x[i + 1] for i in range(2)) + + 8.0 + ) + else: + s = ( + variables.QuadraticSum( + 2 * x[i] - x[i + 1] + 1.5 + x[i] * x[i + 1] for i in range(2) + ) + + 8.0 + ) + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(11.0, e.offset) + self.assertDictEqual({x[0]: 2, x[1]: 1, x[2]: -1}, dict(e.linear_terms)) + self.assertDictEqual( + { + variables.QuadraticTermKey(x[0], x[1]): 1, + variables.QuadraticTermKey(x[1], x[2]): 1, + }, + dict(e.quadratic_terms), + ) class AstTest(parameterized.TestCase): - def assertDictEqualWithZeroDefault( - self, dict1: dict[Any, float], dict2: dict[Any, float] - ) -> None: - for key in dict1.keys(): - if key not in dict2: - dict2[key] = 0.0 - for key in dict2.keys(): - if key not in dict1: - dict1[key] = 0.0 - self.assertDictEqual(dict1, dict2) - - def test_simple_linear_ast(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - s = (6 * (3 * x + y + 3) + 6 * z + 12) / 3 + y + 7 + (x + y) * 2.0 - z * 3.0 - e = variables.as_flat_linear_expression(s) - self.assertEqual(6 * 3 / 3 + 12 / 3 + 7, e.offset) - self.assertDictEqualWithZeroDefault( - {x: 8, y: 3 + 6 / 3, z: 6 / 3 - 3}, dict(e.terms) - ) + def assertDictEqualWithZeroDefault( + self, dict1: dict[Any, float], dict2: dict[Any, float] + ) -> None: + for key in dict1.keys(): + if key not in dict2: + dict2[key] = 0.0 + for key in dict2.keys(): + if key not in dict1: + dict1[key] = 0.0 + self.assertDictEqual(dict1, dict2) + + def test_simple_linear_ast(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + s = (6 * (3 * x + y + 3) + 6 * z + 12) / 3 + y + 7 + (x + y) * 2.0 - z * 3.0 + e = variables.as_flat_linear_expression(s) + self.assertEqual(6 * 3 / 3 + 12 / 3 + 7, e.offset) + self.assertDictEqualWithZeroDefault( + {x: 8, y: 3 + 6 / 3, z: 6 / 3 - 3}, dict(e.terms) + ) - def test_simple_quadratic_ast(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - xx = variables.QuadraticTermKey(x, x) - xy = variables.QuadraticTermKey(x, y) - yy = variables.QuadraticTermKey(y, y) - s = ( - (6 * (3 * x + y + 3) * x + 6 * z + 12) / 3 - + y * y - + 7 - + (x - y) * (x + y) * 2.0 - - z * 3.0 - ) - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(12 / 3 + 7, e.offset) - self.assertDictEqualWithZeroDefault({x: 6, z: 6 / 3 - 3}, dict(e.linear_terms)) - self.assertDictEqualWithZeroDefault( - {xx: 6 * 3 / 3 + 2, xy: 6 / 3, yy: 1 - 2}, dict(e.quadratic_terms) - ) + def test_simple_quadratic_ast(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + xx = variables.QuadraticTermKey(x, x) + xy = variables.QuadraticTermKey(x, y) + yy = variables.QuadraticTermKey(y, y) + s = ( + (6 * (3 * x + y + 3) * x + 6 * z + 12) / 3 + + y * y + + 7 + + (x - y) * (x + y) * 2.0 + - z * 3.0 + ) + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(12 / 3 + 7, e.offset) + self.assertDictEqualWithZeroDefault( + {x: 6, z: 6 / 3 - 3}, dict(e.linear_terms) + ) + self.assertDictEqualWithZeroDefault( + {xx: 6 * 3 / 3 + 2, xy: 6 / 3, yy: 1 - 2}, dict(e.quadratic_terms) + ) - def test_linear_sum_ast(self) -> None: - mod = model.Model() - x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(5)] - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - s = ( - (2 * (z + y) + 2 * y) / 2 - + sum( - x[i] - + 0.5 * variables.LinearSum([4 * x[1] - 2 * x[0], 2 * x[2], 2.5, -0.5]) - for i in range(3) - ) - - variables.LinearSum([x[3], x[4]]) - ) - e = variables.as_flat_linear_expression(s) - self.assertEqual(3.0, e.offset) - self.assertDictEqualWithZeroDefault( - {x[0]: -2, x[1]: 7, x[2]: 4, x[3]: -1, x[4]: -1, y: 2, z: 1}, - dict(e.terms), - ) + def test_linear_sum_ast(self) -> None: + mod = model.Model() + x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(5)] + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + s = ( + (2 * (z + y) + 2 * y) / 2 + + sum( + x[i] + + 0.5 + * variables.LinearSum([4 * x[1] - 2 * x[0], 2 * x[2], 2.5, -0.5]) + for i in range(3) + ) + - variables.LinearSum([x[3], x[4]]) + ) + e = variables.as_flat_linear_expression(s) + self.assertEqual(3.0, e.offset) + self.assertDictEqualWithZeroDefault( + {x[0]: -2, x[1]: 7, x[2]: 4, x[3]: -1, x[4]: -1, y: 2, z: 1}, + dict(e.terms), + ) - def test_quadratic_sum_ast(self) -> None: - mod = model.Model() - x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - yy = variables.QuadraticTermKey(y, y) - zz = variables.QuadraticTermKey(z, z) - s = ( - 1 - + y * y - + z - + sum(x[i] + x[i] * variables.LinearSum([y, z]) for i in range(3)) - - variables.QuadraticSum([y, z * z]) - ) - e = variables.as_flat_quadratic_expression(s) - self.assertEqual(1.0, e.offset) - self.assertDictEqualWithZeroDefault( - {x[0]: 1, x[1]: 1, x[2]: 1, y: -1, z: 1}, dict(e.linear_terms) - ) - self.assertDictEqualWithZeroDefault( - { - yy: 1, - zz: -1, - variables.QuadraticTermKey(x[0], y): 1, - variables.QuadraticTermKey(x[1], y): 1, - variables.QuadraticTermKey(x[2], y): 1, - variables.QuadraticTermKey(x[0], z): 1, - variables.QuadraticTermKey(x[1], z): 1, - variables.QuadraticTermKey(x[2], z): 1, - }, - dict(e.quadratic_terms), - ) + def test_quadratic_sum_ast(self) -> None: + mod = model.Model() + x = [mod.add_binary_variable(name=f"x[{i}]") for i in range(3)] + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + yy = variables.QuadraticTermKey(y, y) + zz = variables.QuadraticTermKey(z, z) + s = ( + 1 + + y * y + + z + + sum(x[i] + x[i] * variables.LinearSum([y, z]) for i in range(3)) + - variables.QuadraticSum([y, z * z]) + ) + e = variables.as_flat_quadratic_expression(s) + self.assertEqual(1.0, e.offset) + self.assertDictEqualWithZeroDefault( + {x[0]: 1, x[1]: 1, x[2]: 1, y: -1, z: 1}, dict(e.linear_terms) + ) + self.assertDictEqualWithZeroDefault( + { + yy: 1, + zz: -1, + variables.QuadraticTermKey(x[0], y): 1, + variables.QuadraticTermKey(x[1], y): 1, + variables.QuadraticTermKey(x[2], y): 1, + variables.QuadraticTermKey(x[0], z): 1, + variables.QuadraticTermKey(x[1], z): 1, + variables.QuadraticTermKey(x[2], z): 1, + }, + dict(e.quadratic_terms), + ) # Test behavior of LinearExpression and as_flat_linear_expression that is # not covered by other tests. class LinearExpressionTest(absltest.TestCase): - def test_init_to_zero(self) -> None: - expression = variables.LinearExpression() - self.assertEqual(expression.offset, 0.0) - self.assertEmpty(expression.terms) - - def test_terms_read_only(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - expression = variables.LinearExpression(y - 2 * x + 1) - with self.assertRaisesRegex(TypeError, "does not support item assignment"): - expression.terms[x] += 1 # pytype: disable=unsupported-operands - - def test_no_copy_of_linear_expression(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - expression = variables.LinearExpression(y - 2 * x + 1) - self.assertIs(expression, variables.as_flat_linear_expression(expression)) - - def test_number_as_flat_linear_expression(self) -> None: - expression = variables.LinearExpression(2.0) - self.assertDictEqual(dict(expression.terms), {}) - self.assertEqual(expression.offset, 2.0) - - def test_evaluate(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - expression = variables.LinearExpression(3 * x + y + 2.0) - self.assertEqual(expression.evaluate({x: 4.0, y: 3.0}), 17.0) + def test_init_to_zero(self) -> None: + expression = variables.LinearExpression() + self.assertEqual(expression.offset, 0.0) + self.assertEmpty(expression.terms) + + def test_terms_read_only(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + expression = variables.LinearExpression(y - 2 * x + 1) + with self.assertRaisesRegex(TypeError, "does not support item assignment"): + expression.terms[x] += 1 # pytype: disable=unsupported-operands + + def test_no_copy_of_linear_expression(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + expression = variables.LinearExpression(y - 2 * x + 1) + self.assertIs(expression, variables.as_flat_linear_expression(expression)) + + def test_number_as_flat_linear_expression(self) -> None: + expression = variables.LinearExpression(2.0) + self.assertDictEqual(dict(expression.terms), {}) + self.assertEqual(expression.offset, 2.0) + + def test_evaluate(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + expression = variables.LinearExpression(3 * x + y + 2.0) + self.assertEqual(expression.evaluate({x: 4.0, y: 3.0}), 17.0) # Test behavior of QuadraticExpression and as_flat_quadratic_expression that is # not covered by other tests. class QuadraticExpressionTest(absltest.TestCase): - def test_terms_read_only(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - yy = variables.QuadraticTermKey(y, y) - expression = variables.QuadraticExpression(y * y - 2 * x + 1) - with self.assertRaisesRegex(TypeError, "does not support item assignment"): - expression.linear_terms[x] += 1 # pytype: disable=unsupported-operands - with self.assertRaisesRegex(TypeError, "does not support item assignment"): - expression.quadratic_terms[yy] += 1 # pytype: disable=unsupported-operands - - def test_no_copy_of_quadratic_expression(self) -> None: - mod = model.Model() - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - expression = variables.QuadraticExpression(y * y - 2 * x + 1) - self.assertIs(expression, variables.as_flat_quadratic_expression(expression)) - - def test_number_as_flat_quadratic_expression(self) -> None: - expression = variables.QuadraticExpression(2.0) - self.assertDictEqual(dict(expression.linear_terms), {}) - self.assertDictEqual(dict(expression.quadratic_terms), {}) - self.assertEqual(expression.offset, 2.0) - - def test_evaluate(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - expression = variables.QuadraticExpression(x * x + 2 * x * y + 4 * y + 2.0) - # 16 + 24 + 12 + 2 = 54 - self.assertEqual(expression.evaluate({x: 4.0, y: 3.0}), 54.0) + def test_terms_read_only(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + yy = variables.QuadraticTermKey(y, y) + expression = variables.QuadraticExpression(y * y - 2 * x + 1) + with self.assertRaisesRegex(TypeError, "does not support item assignment"): + expression.linear_terms[x] += 1 # pytype: disable=unsupported-operands + with self.assertRaisesRegex(TypeError, "does not support item assignment"): + expression.quadratic_terms[yy] += 1 # pytype: disable=unsupported-operands + + def test_no_copy_of_quadratic_expression(self) -> None: + mod = model.Model() + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + expression = variables.QuadraticExpression(y * y - 2 * x + 1) + self.assertIs( + expression, variables.as_flat_quadratic_expression(expression) + ) + + def test_number_as_flat_quadratic_expression(self) -> None: + expression = variables.QuadraticExpression(2.0) + self.assertDictEqual(dict(expression.linear_terms), {}) + self.assertDictEqual(dict(expression.quadratic_terms), {}) + self.assertEqual(expression.offset, 2.0) + + def test_evaluate(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + expression = variables.QuadraticExpression(x * x + 2 * x * y + 4 * y + 2.0) + # 16 + 24 + 12 + 2 = 54 + self.assertEqual(expression.evaluate({x: 4.0, y: 3.0}), 54.0) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/mathopt_test.py b/ortools/math_opt/python/mathopt_test.py index 315efade21d..2ba30d43899 100644 --- a/ortools/math_opt/python/mathopt_test.py +++ b/ortools/math_opt/python/mathopt_test.py @@ -81,43 +81,43 @@ def _is_actual_export(v: Any) -> bool: - if inspect.ismodule(v): - return False - if getattr(v, "__module__", None) != typing.__name__: - return True - return v not in _TYPING_PUBLIC_CONTENT + if inspect.ismodule(v): + return False + if getattr(v, "__module__", None) != typing.__name__: + return True + return v not in _TYPING_PUBLIC_CONTENT def _get_public_api(module: types.ModuleType) -> List[Tuple[str, Any]]: - tuple_list = inspect.getmembers(module, _is_actual_export) - return [(name, obj) for name, obj in tuple_list if not name.startswith("_")] + tuple_list = inspect.getmembers(module, _is_actual_export) + return [(name, obj) for name, obj in tuple_list if not name.startswith("_")] class MathoptTest(absltest.TestCase): - def test_imports(self) -> None: - missing_imports: List[str] = [] - for module in _MODULES_TO_CHECK: - for name, obj in _get_public_api(module): - if (module, name) in _EXCLUDED_SYMBOLS: - continue - if hasattr(mathopt, name): - self.assertIs( - getattr(mathopt, name), - obj, - msg=f"module: {module.__name__} name: {name}", - ) - else: - # We don't immediately asserts on a missing import so that we can get - # the list of all missing ones. - missing_imports.append(f"from {module.__name__} import {name}") - # We can't have \ in an expression inside an f-string. - nl = "\n" - self.assertFalse( - bool(missing_imports), - msg=f"missing imports:\n{nl.join(missing_imports)}", - ) + def test_imports(self) -> None: + missing_imports: List[str] = [] + for module in _MODULES_TO_CHECK: + for name, obj in _get_public_api(module): + if (module, name) in _EXCLUDED_SYMBOLS: + continue + if hasattr(mathopt, name): + self.assertIs( + getattr(mathopt, name), + obj, + msg=f"module: {module.__name__} name: {name}", + ) + else: + # We don't immediately asserts on a missing import so that we can get + # the list of all missing ones. + missing_imports.append(f"from {module.__name__} import {name}") + # We can't have \ in an expression inside an f-string. + nl = "\n" + self.assertFalse( + bool(missing_imports), + msg=f"missing imports:\n{nl.join(missing_imports)}", + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/message_callback.py b/ortools/math_opt/python/message_callback.py index 5752ab09edc..cf0ff17d9b7 100644 --- a/ortools/math_opt/python/message_callback.py +++ b/ortools/math_opt/python/message_callback.py @@ -42,101 +42,103 @@ def printer_message_callback( *, file: TextIO = sys.stdout, prefix: str = "" ) -> SolveMessageCallback: - """Returns a message callback that prints to a file. + """Returns a message callback that prints to a file. - It prints its output to the given text file, prefixing each line with the - given prefix. + It prints its output to the given text file, prefixing each line with the + given prefix. - For each call to the returned message callback, the output_stream is flushed. + For each call to the returned message callback, the output_stream is flushed. - Args: - file: The file to print to. It prints to stdout by default. - prefix: The prefix to print in front of each line. + Args: + file: The file to print to. It prints to stdout by default. + prefix: The prefix to print in front of each line. - Returns: - A function matching the expected signature for message callbacks. - """ - mutex = threading.Lock() + Returns: + A function matching the expected signature for message callbacks. + """ + mutex = threading.Lock() - def callback(messages: Sequence[str]) -> None: - with mutex: - for message in messages: - file.write(prefix) - file.write(message) - file.write("\n") - file.flush() + def callback(messages: Sequence[str]) -> None: + with mutex: + for message in messages: + file.write(prefix) + file.write(message) + file.write("\n") + file.flush() - return callback + return callback def log_messages( messages: Sequence[str], *, level: int = logging.INFO, prefix: str = "" ) -> None: - """Logs the input messages from a message callback using absl.logging.log(). + """Logs the input messages from a message callback using absl.logging.log(). - It logs each line with the given prefix. It setups absl.logging so that the - logs use the file name and line of the caller of this function. + It logs each line with the given prefix. It setups absl.logging so that the + logs use the file name and line of the caller of this function. - Typical usage example: + Typical usage example: - result = solve.solve( - model, parameters.SolverType.GSCIP, - msg_cb=lambda msgs: message_callback.log_messages( - msgs, prefix='[solver] ')) + result = solve.solve( + model, parameters.SolverType.GSCIP, + msg_cb=lambda msgs: message_callback.log_messages( + msgs, prefix='[solver] ')) - Args: - messages: The messages received in the message callback (typically a lambda - function in the caller code). - level: One of absl.logging.(DEBUG|INFO|WARNING|ERROR|FATAL). - prefix: The prefix to print in front of each line. - """ - for message in messages: - logging.log(level, "%s%s", prefix, message) + Args: + messages: The messages received in the message callback (typically a lambda + function in the caller code). + level: One of absl.logging.(DEBUG|INFO|WARNING|ERROR|FATAL). + prefix: The prefix to print in front of each line. + """ + for message in messages: + logging.log(level, "%s%s", prefix, message) logging.ABSLLogger.register_frame_to_skip(__file__, log_messages.__name__) -def vlog_messages(messages: Sequence[str], level: int, *, prefix: str = "") -> None: - """Logs the input messages from a message callback using absl.logging.vlog(). +def vlog_messages( + messages: Sequence[str], level: int, *, prefix: str = "" +) -> None: + """Logs the input messages from a message callback using absl.logging.vlog(). - It logs each line with the given prefix. It setups absl.logging so that the - logs use the file name and line of the caller of this function. + It logs each line with the given prefix. It setups absl.logging so that the + logs use the file name and line of the caller of this function. - Typical usage example: + Typical usage example: - result = solve.solve( - model, parameters.SolverType.GSCIP, - msg_cb=lambda msgs: message_callback.vlog_messages( - msgs, 1, prefix='[solver] ')) + result = solve.solve( + model, parameters.SolverType.GSCIP, + msg_cb=lambda msgs: message_callback.vlog_messages( + msgs, 1, prefix='[solver] ')) - Args: - messages: The messages received in the message callback (typically a lambda - function in the caller code). - level: The verbose log level, e.g. 1, 2... - prefix: The prefix to print in front of each line. - """ - for message in messages: - logging.vlog(level, "%s%s", prefix, message) + Args: + messages: The messages received in the message callback (typically a lambda + function in the caller code). + level: The verbose log level, e.g. 1, 2... + prefix: The prefix to print in front of each line. + """ + for message in messages: + logging.vlog(level, "%s%s", prefix, message) logging.ABSLLogger.register_frame_to_skip(__file__, vlog_messages.__name__) def list_message_callback(sink: List[str]) -> SolveMessageCallback: - """Returns a message callback that logs messages to a list. + """Returns a message callback that logs messages to a list. - Args: - sink: The list to append messages to. + Args: + sink: The list to append messages to. - Returns: - A function matching the expected signature for message callbacks. - """ - mutex = threading.Lock() + Returns: + A function matching the expected signature for message callbacks. + """ + mutex = threading.Lock() - def callback(messages: Sequence[str]) -> None: - with mutex: - for message in messages: - sink.append(message) + def callback(messages: Sequence[str]) -> None: + with mutex: + for message in messages: + sink.append(message) - return callback + return callback diff --git a/ortools/math_opt/python/message_callback_test.py b/ortools/math_opt/python/message_callback_test.py index 21fa8b1b3e5..6553021c6a6 100644 --- a/ortools/math_opt/python/message_callback_test.py +++ b/ortools/math_opt/python/message_callback_test.py @@ -23,114 +23,114 @@ class PrinterMessageCallbackTest(absltest.TestCase): - def test_no_prefix(self): - class FlushCountingStringIO(io.StringIO): + def test_no_prefix(self): + class FlushCountingStringIO(io.StringIO): - def __init__(self): - super().__init__() - self.num_flushes: int = 0 + def __init__(self): + super().__init__() + self.num_flushes: int = 0 - def flush(self): - super().flush() - self.num_flushes += 1 + def flush(self): + super().flush() + self.num_flushes += 1 - buf = FlushCountingStringIO() - cb = message_callback.printer_message_callback(file=buf) - cb(["line 1", "line 2"]) - cb(["line 3"]) + buf = FlushCountingStringIO() + cb = message_callback.printer_message_callback(file=buf) + cb(["line 1", "line 2"]) + cb(["line 3"]) - self.assertMultiLineEqual(buf.getvalue(), "line 1\nline 2\nline 3\n") - self.assertEqual(buf.num_flushes, 2) + self.assertMultiLineEqual(buf.getvalue(), "line 1\nline 2\nline 3\n") + self.assertEqual(buf.num_flushes, 2) - def test_with_prefix(self): - buf = io.StringIO() - cb = message_callback.printer_message_callback(file=buf, prefix="test> ") - cb(["line 1", "line 2"]) - cb(["line 3"]) + def test_with_prefix(self): + buf = io.StringIO() + cb = message_callback.printer_message_callback(file=buf, prefix="test> ") + cb(["line 1", "line 2"]) + cb(["line 3"]) - self.assertMultiLineEqual( - buf.getvalue(), "test> line 1\ntest> line 2\ntest> line 3\n" - ) + self.assertMultiLineEqual( + buf.getvalue(), "test> line 1\ntest> line 2\ntest> line 3\n" + ) class LogMessagesTest(absltest.TestCase): - def test_defaults(self): - with self.assertLogs(logger="absl", level="INFO") as logs: - message_callback.log_messages(["line 1", "line 2"]) - self.assertListEqual(logs.output, ["INFO:absl:line 1", "INFO:absl:line 2"]) - - def test_prefix(self): - with self.assertLogs(logger="absl") as logs: - message_callback.log_messages(["line 1", "line 2"], prefix="solver: ") - self.assertListEqual( - logs.output, ["INFO:absl:solver: line 1", "INFO:absl:solver: line 2"] - ) - - def test_warning(self): - with self.assertLogs(logger="absl") as logs: - message_callback.log_messages(["line 1", "line 2"], level=logging.WARNING) - self.assertListEqual( - logs.output, ["WARNING:absl:line 1", "WARNING:absl:line 2"] - ) - - def test_records_path(self): - with self.assertLogs(logger="absl") as logs: - message_callback.log_messages(["line 1", "line 2"]) - self.assertSetEqual( - set(os.path.basename(r.pathname) for r in logs.records), - set(("message_callback_test.py",)), - ) + def test_defaults(self): + with self.assertLogs(logger="absl", level="INFO") as logs: + message_callback.log_messages(["line 1", "line 2"]) + self.assertListEqual(logs.output, ["INFO:absl:line 1", "INFO:absl:line 2"]) + + def test_prefix(self): + with self.assertLogs(logger="absl") as logs: + message_callback.log_messages(["line 1", "line 2"], prefix="solver: ") + self.assertListEqual( + logs.output, ["INFO:absl:solver: line 1", "INFO:absl:solver: line 2"] + ) + + def test_warning(self): + with self.assertLogs(logger="absl") as logs: + message_callback.log_messages(["line 1", "line 2"], level=logging.WARNING) + self.assertListEqual( + logs.output, ["WARNING:absl:line 1", "WARNING:absl:line 2"] + ) + + def test_records_path(self): + with self.assertLogs(logger="absl") as logs: + message_callback.log_messages(["line 1", "line 2"]) + self.assertSetEqual( + set(os.path.basename(r.pathname) for r in logs.records), + set(("message_callback_test.py",)), + ) class VLogMessagesTest(absltest.TestCase): - """Tests of vlog_messages(). + """Tests of vlog_messages(). - In the tests we abuse the logging level 0 since there is not API in the - `logging` module to change the verbosity. - """ + In the tests we abuse the logging level 0 since there is not API in the + `logging` module to change the verbosity. + """ - def test_defaults(self): - with self.assertLogs(logger="absl") as logs: - message_callback.vlog_messages(["line 1", "line 2"], 0) - self.assertListEqual(logs.output, ["INFO:absl:line 1", "INFO:absl:line 2"]) + def test_defaults(self): + with self.assertLogs(logger="absl") as logs: + message_callback.vlog_messages(["line 1", "line 2"], 0) + self.assertListEqual(logs.output, ["INFO:absl:line 1", "INFO:absl:line 2"]) - def test_prefix(self): - with self.assertLogs(logger="absl") as logs: - message_callback.vlog_messages(["line 1", "line 2"], 0, prefix="solver: ") - self.assertListEqual( - logs.output, ["INFO:absl:solver: line 1", "INFO:absl:solver: line 2"] - ) + def test_prefix(self): + with self.assertLogs(logger="absl") as logs: + message_callback.vlog_messages(["line 1", "line 2"], 0, prefix="solver: ") + self.assertListEqual( + logs.output, ["INFO:absl:solver: line 1", "INFO:absl:solver: line 2"] + ) - def test_records_path(self): - with self.assertLogs(logger="absl") as logs: - message_callback.vlog_messages(["line 1", "line 2"], 0) - self.assertSetEqual( - set(os.path.basename(r.pathname) for r in logs.records), - set(("message_callback_test.py",)), - ) + def test_records_path(self): + with self.assertLogs(logger="absl") as logs: + message_callback.vlog_messages(["line 1", "line 2"], 0) + self.assertSetEqual( + set(os.path.basename(r.pathname) for r in logs.records), + set(("message_callback_test.py",)), + ) class ListMessageCallbackTest(absltest.TestCase): - def test_empty(self): - msgs = [] - cb = message_callback.list_message_callback(msgs) - cb(["line 1", "line 2"]) - cb(["line 3"]) + def test_empty(self): + msgs = [] + cb = message_callback.list_message_callback(msgs) + cb(["line 1", "line 2"]) + cb(["line 3"]) - self.assertSequenceEqual(msgs, ("line 1", "line 2", "line 3")) + self.assertSequenceEqual(msgs, ("line 1", "line 2", "line 3")) - def test_not_empty(self): - msgs = ["initial", "content"] - cb = message_callback.list_message_callback(msgs) - cb(["line 1", "line 2"]) - cb(["line 3"]) + def test_not_empty(self): + msgs = ["initial", "content"] + cb = message_callback.list_message_callback(msgs) + cb(["line 1", "line 2"]) + cb(["line 3"]) - self.assertSequenceEqual( - msgs, ("initial", "content", "line 1", "line 2", "line 3") - ) + self.assertSequenceEqual( + msgs, ("initial", "content", "line 1", "line 2", "line 3") + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/model.py b/ortools/math_opt/python/model.py index cae5e888cdd..f411d585ed9 100644 --- a/ortools/math_opt/python/model.py +++ b/ortools/math_opt/python/model.py @@ -59,887 +59,911 @@ class UpdateTracker: - """Tracks updates to an optimization model from a ModelStorage. - - Do not instantiate directly, instead create through - ModelStorage.add_update_tracker(). - - Querying an UpdateTracker after calling Model.remove_update_tracker will - result in a model_storage.UsedUpdateTrackerAfterRemovalError. - - Example: - mod = Model() - x = mod.add_variable(0.0, 1.0, True, 'x') - y = mod.add_variable(0.0, 1.0, True, 'y') - tracker = mod.add_update_tracker() - mod.set_variable_ub(x, 3.0) - tracker.export_update() - => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" - mod.set_variable_ub(y, 2.0) - tracker.export_update() - => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" - tracker.advance_checkpoint() - tracker.export_update() - => None - mod.set_variable_ub(y, 4.0) - tracker.export_update() - => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" - tracker.advance_checkpoint() - mod.remove_update_tracker(tracker) - """ - - def __init__( - self, - diff_id: int, - elem: elemental.Elemental, - ): - """Do not invoke directly, use Model.add_update_tracker() instead.""" - self._diff_id = diff_id - self._elemental = elem - - def export_update( - self, *, remove_names: bool = False - ) -> Optional[model_update_pb2.ModelUpdateProto]: - """Returns changes to the model since last call to checkpoint/creation.""" - return self._elemental.export_model_update( - self._diff_id, remove_names=remove_names - ) - - def advance_checkpoint(self) -> None: - """Track changes to the model only after this function call.""" - return self._elemental.advance_diff(self._diff_id) - - @property - def diff_id(self) -> int: - return self._diff_id + """Tracks updates to an optimization model from a ModelStorage. + + Do not instantiate directly, instead create through + ModelStorage.add_update_tracker(). + + Querying an UpdateTracker after calling Model.remove_update_tracker will + result in a model_storage.UsedUpdateTrackerAfterRemovalError. + + Example: + mod = Model() + x = mod.add_variable(0.0, 1.0, True, 'x') + y = mod.add_variable(0.0, 1.0, True, 'y') + tracker = mod.add_update_tracker() + mod.set_variable_ub(x, 3.0) + tracker.export_update() + => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" + mod.set_variable_ub(y, 2.0) + tracker.export_update() + => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" + tracker.advance_checkpoint() + tracker.export_update() + => None + mod.set_variable_ub(y, 4.0) + tracker.export_update() + => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" + tracker.advance_checkpoint() + mod.remove_update_tracker(tracker) + """ + + def __init__( + self, + diff_id: int, + elem: elemental.Elemental, + ): + """Do not invoke directly, use Model.add_update_tracker() instead.""" + self._diff_id = diff_id + self._elemental = elem + + def export_update( + self, *, remove_names: bool = False + ) -> Optional[model_update_pb2.ModelUpdateProto]: + """Returns changes to the model since last call to checkpoint/creation.""" + return self._elemental.export_model_update( + self._diff_id, remove_names=remove_names + ) + + def advance_checkpoint(self) -> None: + """Track changes to the model only after this function call.""" + return self._elemental.advance_diff(self._diff_id) + + @property + def diff_id(self) -> int: + return self._diff_id class Model: - """An optimization model. - - The objective function of the model can be linear or quadratic, and some - solvers can only handle linear objective functions. For this reason Model has - three versions of all objective setting functions: - * A generic one (e.g. maximize()), which accepts linear or quadratic - expressions, - * a quadratic version (e.g. maximize_quadratic_objective()), which also - accepts linear or quadratic expressions and can be used to signal a - quadratic objective is possible, and - * a linear version (e.g. maximize_linear_objective()), which only accepts - linear expressions and can be used to avoid solve time errors for solvers - that do not accept quadratic objectives. - - Attributes: - name: A description of the problem, can be empty. - objective: A function to maximize or minimize. - storage: Implementation detail, do not access directly. - _variable_ids: Maps variable ids to Variable objects. - _linear_constraint_ids: Maps linear constraint ids to LinearConstraint - objects. + """An optimization model. + + The objective function of the model can be linear or quadratic, and some + solvers can only handle linear objective functions. For this reason Model has + three versions of all objective setting functions: + * A generic one (e.g. maximize()), which accepts linear or quadratic + expressions, + * a quadratic version (e.g. maximize_quadratic_objective()), which also + accepts linear or quadratic expressions and can be used to signal a + quadratic objective is possible, and + * a linear version (e.g. maximize_linear_objective()), which only accepts + linear expressions and can be used to avoid solve time errors for solvers + that do not accept quadratic objectives. + + Attributes: + name: A description of the problem, can be empty. + objective: A function to maximize or minimize. + storage: Implementation detail, do not access directly. + _variable_ids: Maps variable ids to Variable objects. + _linear_constraint_ids: Maps linear constraint ids to LinearConstraint + objects. + """ + + __slots__ = ("_elemental",) + + def __init__( + self, + *, + name: str = "", # TODO(b/371236599): rename to model_name + primary_objective_name: str = "", + ) -> None: + self._elemental: elemental.Elemental = cpp_elemental.CppElemental( + model_name=name, primary_objective_name=primary_objective_name + ) + + @property + def name(self) -> str: + return self._elemental.model_name + + ############################################################################## + # Variables + ############################################################################## + + def add_variable( + self, + *, + lb: float = -math.inf, + ub: float = math.inf, + is_integer: bool = False, + name: str = "", + ) -> variables_mod.Variable: + """Adds a decision variable to the optimization model. + + Args: + lb: The new variable must take at least this value (a lower bound). + ub: The new variable must be at most this value (an upper bound). + is_integer: Indicates if the variable can only take integer values + (otherwise, the variable can take any continuous value). + name: For debugging purposes only, but nonempty names must be distinct. + + Returns: + A reference to the new decision variable. """ - __slots__ = ("_elemental",) - - def __init__( - self, - *, - name: str = "", # TODO(b/371236599): rename to model_name - primary_objective_name: str = "", - ) -> None: - self._elemental: elemental.Elemental = cpp_elemental.CppElemental( - model_name=name, primary_objective_name=primary_objective_name - ) - - @property - def name(self) -> str: - return self._elemental.model_name - - ############################################################################## - # Variables - ############################################################################## - - def add_variable( - self, - *, - lb: float = -math.inf, - ub: float = math.inf, - is_integer: bool = False, - name: str = "", - ) -> variables_mod.Variable: - """Adds a decision variable to the optimization model. - - Args: - lb: The new variable must take at least this value (a lower bound). - ub: The new variable must be at most this value (an upper bound). - is_integer: Indicates if the variable can only take integer values - (otherwise, the variable can take any continuous value). - name: For debugging purposes only, but nonempty names must be distinct. - - Returns: - A reference to the new decision variable. - """ - - variable_id = self._elemental.add_element(enums.ElementType.VARIABLE, name) - result = variables_mod.Variable(self._elemental, variable_id) - result.lower_bound = lb - result.upper_bound = ub - result.integer = is_integer - return result - - def add_integer_variable( - self, *, lb: float = -math.inf, ub: float = math.inf, name: str = "" - ) -> variables_mod.Variable: - return self.add_variable(lb=lb, ub=ub, is_integer=True, name=name) - - def add_binary_variable(self, *, name: str = "") -> variables_mod.Variable: - return self.add_variable(lb=0.0, ub=1.0, is_integer=True, name=name) - - def get_variable( - self, var_id: int, *, validate: bool = True - ) -> variables_mod.Variable: - """Returns the Variable for the id var_id, or raises KeyError.""" - if validate and not self._elemental.element_exists( - enums.ElementType.VARIABLE, var_id - ): - raise KeyError(f"Variable does not exist with id {var_id}.") - return variables_mod.Variable(self._elemental, var_id) - - def has_variable(self, var_id: int) -> bool: - """Returns true if a Variable with this id is in the model.""" - return self._elemental.element_exists(enums.ElementType.VARIABLE, var_id) - - def get_num_variables(self) -> int: - """Returns the number of variables in the model.""" - return self._elemental.get_num_elements(enums.ElementType.VARIABLE) - - def get_next_variable_id(self) -> int: - """Returns the id of the next variable created in the model.""" - return self._elemental.get_next_element_id(enums.ElementType.VARIABLE) - - def ensure_next_variable_id_at_least(self, var_id: int) -> None: - """If the next variable id would be less than `var_id`, sets it to `var_id`.""" - self._elemental.ensure_next_element_id_at_least( - enums.ElementType.VARIABLE, var_id - ) - - def delete_variable(self, var: variables_mod.Variable) -> None: - """Removes this variable from the model.""" - self.check_compatible(var) - if not self._elemental.delete_element(enums.ElementType.VARIABLE, var.id): - raise ValueError(f"Variable with id {var.id} was not in the model.") - - def variables(self) -> Iterator[variables_mod.Variable]: - """Yields the variables in the order of creation.""" - var_ids = self._elemental.get_elements(enums.ElementType.VARIABLE) - var_ids.sort() - for var_id in var_ids: - yield variables_mod.Variable(self._elemental, int(var_id)) - - ############################################################################## - # Objective - ############################################################################## - - @property - def objective(self) -> objectives.Objective: - return objectives.PrimaryObjective(self._elemental) - - def maximize(self, obj: variables_mod.QuadraticTypes) -> None: - """Sets the objective to maximize the provided expression `obj`.""" - self.set_objective(obj, is_maximize=True) - - def maximize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: - """Sets the objective to maximize the provided linear expression `obj`.""" - self.set_linear_objective(obj, is_maximize=True) - - def maximize_quadratic_objective(self, obj: variables_mod.QuadraticTypes) -> None: - """Sets the objective to maximize the provided quadratic expression `obj`.""" - self.set_quadratic_objective(obj, is_maximize=True) - - def minimize(self, obj: variables_mod.QuadraticTypes) -> None: - """Sets the objective to minimize the provided expression `obj`.""" - self.set_objective(obj, is_maximize=False) - - def minimize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: - """Sets the objective to minimize the provided linear expression `obj`.""" - self.set_linear_objective(obj, is_maximize=False) - - def minimize_quadratic_objective(self, obj: variables_mod.QuadraticTypes) -> None: - """Sets the objective to minimize the provided quadratic expression `obj`.""" - self.set_quadratic_objective(obj, is_maximize=False) - - def set_objective( - self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool - ) -> None: - """Sets the objective to optimize the provided expression `obj`.""" - self.objective.set_to_expression(obj) - self.objective.is_maximize = is_maximize - - def set_linear_objective( - self, obj: variables_mod.LinearTypes, *, is_maximize: bool - ) -> None: - """Sets the objective to optimize the provided linear expression `obj`.""" - self.objective.set_to_linear_expression(obj) - self.objective.is_maximize = is_maximize - - def set_quadratic_objective( - self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool - ) -> None: - """Sets the objective to optimize the provided quadratic expression `obj`.""" - self.objective.set_to_quadratic_expression(obj) - self.objective.is_maximize = is_maximize - - def linear_objective_terms(self) -> Iterator[variables_mod.LinearTerm]: - """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" - yield from self.objective.linear_terms() - - def quadratic_objective_terms(self) -> Iterator[variables_mod.QuadraticTerm]: - """Yields the quadratic terms with nonzero objective coefficient in undefined order.""" - yield from self.objective.quadratic_terms() - - ############################################################################## - # Auxiliary Objectives - ############################################################################## - - def add_auxiliary_objective( - self, - *, - priority: int, - name: str = "", - expr: Optional[variables_mod.LinearTypes] = None, - is_maximize: bool = False, - ) -> objectives.AuxiliaryObjective: - """Adds an additional objective to the model.""" - obj_id = self._elemental.add_element( - enums.ElementType.AUXILIARY_OBJECTIVE, name - ) - self._elemental.set_attr( - enums.IntAttr1.AUXILIARY_OBJECTIVE_PRIORITY, (obj_id,), priority - ) - result = objectives.AuxiliaryObjective(self._elemental, obj_id) - if expr is not None: - result.set_to_linear_expression(expr) - result.is_maximize = is_maximize - return result - - def add_maximization_objective( - self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" - ) -> objectives.AuxiliaryObjective: - """Adds an additional objective to the model that is maximizaition.""" - result = self.add_auxiliary_objective( - priority=priority, name=name, expr=expr, is_maximize=True - ) - return result - - def add_minimization_objective( - self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" - ) -> objectives.AuxiliaryObjective: - """Adds an additional objective to the model that is minimizaition.""" - result = self.add_auxiliary_objective( - priority=priority, name=name, expr=expr, is_maximize=False - ) - return result - - def delete_auxiliary_objective(self, obj: objectives.AuxiliaryObjective) -> None: - """Removes an auxiliary objective from the model.""" - self.check_compatible(obj) - if not self._elemental.delete_element( - enums.ElementType.AUXILIARY_OBJECTIVE, obj.id - ): - raise ValueError( - f"Auxiliary objective with id {obj.id} is not in the model." - ) - - def has_auxiliary_objective(self, obj_id: int) -> bool: - """Returns true if the model has an auxiliary objective with id `obj_id`.""" - return self._elemental.element_exists( - enums.ElementType.AUXILIARY_OBJECTIVE, obj_id - ) - - def next_auxiliary_objective_id(self) -> int: - """Returns the id of the next auxiliary objective added to the model.""" - return self._elemental.get_next_element_id( - enums.ElementType.AUXILIARY_OBJECTIVE - ) - - def num_auxiliary_objectives(self) -> int: - """Returns the number of auxiliary objectives in this model.""" - return self._elemental.get_num_elements(enums.ElementType.AUXILIARY_OBJECTIVE) - - def ensure_next_auxiliary_objective_id_at_least(self, obj_id: int) -> None: - """If the next auxiliary objective id would be less than `obj_id`, sets it to `obj_id`.""" - self._elemental.ensure_next_element_id_at_least( - enums.ElementType.AUXILIARY_OBJECTIVE, obj_id - ) - - def get_auxiliary_objective( - self, obj_id: int, *, validate: bool = True - ) -> objectives.AuxiliaryObjective: - """Returns the auxiliary objective with this id. - - If there is no objective with this id, an exception is thrown if validate is - true, and an invalid AuxiliaryObjective is returned if validate is false - (later interactions with this object will cause unpredictable errors). Only - set validate=False if there is a known performance problem. - - Args: - obj_id: The id of the auxiliary objective to look for. - validate: Set to false for more speed, but fails to raise an exception if - the objective is missing. - - Raises: - KeyError: If `validate` is True and there is no objective with this id. - """ - if validate and not self.has_auxiliary_objective(obj_id): - raise KeyError(f"Model has no auxiliary objective with id {obj_id}") - return objectives.AuxiliaryObjective(self._elemental, obj_id) - - def auxiliary_objectives(self) -> Iterator[objectives.AuxiliaryObjective]: - """Returns the auxiliary objectives in the model in the order of creation.""" - ids = self._elemental.get_elements(enums.ElementType.AUXILIARY_OBJECTIVE) - ids.sort() - for aux_obj_id in ids: - yield objectives.AuxiliaryObjective(self._elemental, int(aux_obj_id)) - - ############################################################################## - # Linear Constraints - ############################################################################## - - # TODO(b/227214976): Update the note below and link to pytype bug number. - # Note: bounded_expr's type includes bool only as a workaround to a pytype - # issue. Passing a bool for bounded_expr will raise an error in runtime. - def add_linear_constraint( - self, - bounded_expr: Optional[Union[bool, variables_mod.BoundedLinearTypes]] = None, - *, - lb: Optional[float] = None, - ub: Optional[float] = None, - expr: Optional[variables_mod.LinearTypes] = None, - name: str = "", - ) -> linear_constraints_mod.LinearConstraint: - """Adds a linear constraint to the optimization model. - - The simplest way to specify the constraint is by passing a one-sided or - two-sided linear inequality as in: - * add_linear_constraint(x + y + 1.0 <= 2.0), - * add_linear_constraint(x + y >= 2.0), or - * add_linear_constraint((1.0 <= x + y) <= 2.0). - - Note the extra parenthesis for two-sided linear inequalities, which is - required due to some language limitations (see - https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). - If the parenthesis are omitted, a TypeError will be raised explaining the - issue (if this error was not raised the first inequality would have been - silently ignored because of the noted language limitations). - - The second way to specify the constraint is by setting lb, ub, and/or expr - as in: - * add_linear_constraint(expr=x + y + 1.0, ub=2.0), - * add_linear_constraint(expr=x + y, lb=2.0), - * add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or - * add_linear_constraint(lb=1.0). - Omitting lb is equivalent to setting it to -math.inf and omiting ub is - equivalent to setting it to math.inf. - - These two alternatives are exclusive and a combined call like: - * add_linear_constraint(x + y <= 2.0, lb=1.0), or - * add_linear_constraint(x + y <= 2.0, ub=math.inf) - will raise a ValueError. A ValueError is also raised if expr's offset is - infinite. - - Args: - bounded_expr: a linear inequality describing the constraint. Cannot be - specified together with lb, ub, or expr. - lb: The constraint's lower bound if bounded_expr is omitted (if both - bounder_expr and lb are omitted, the lower bound is -math.inf). - ub: The constraint's upper bound if bounded_expr is omitted (if both - bounder_expr and ub are omitted, the upper bound is math.inf). - expr: The constraint's linear expression if bounded_expr is omitted. - name: For debugging purposes only, but nonempty names must be distinct. - - Returns: - A reference to the new linear constraint. - """ - norm_ineq = normalized_inequality.as_normalized_linear_inequality( - bounded_expr, lb=lb, ub=ub, expr=expr - ) - lin_con_id = self._elemental.add_element( - enums.ElementType.LINEAR_CONSTRAINT, name - ) - - result = linear_constraints_mod.LinearConstraint(self._elemental, lin_con_id) - result.lower_bound = norm_ineq.lb - result.upper_bound = norm_ineq.ub - for var, coefficient in norm_ineq.coefficients.items(): - result.set_coefficient(var, coefficient) - return result - - def has_linear_constraint(self, con_id: int) -> bool: - """Returns true if a linear constraint with this id is in the model.""" - return self._elemental.element_exists( - enums.ElementType.LINEAR_CONSTRAINT, con_id - ) - - def get_num_linear_constraints(self) -> int: - """Returns the number of linear constraints in the model.""" - return self._elemental.get_num_elements(enums.ElementType.LINEAR_CONSTRAINT) - - def get_next_linear_constraint_id(self) -> int: - """Returns the id of the next linear constraint created in the model.""" - return self._elemental.get_next_element_id(enums.ElementType.LINEAR_CONSTRAINT) - - def ensure_next_linear_constraint_id_at_least(self, con_id: int) -> None: - """If the next linear constraint id would be less than `con_id`, sets it to `con_id`.""" - self._elemental.ensure_next_element_id_at_least( - enums.ElementType.LINEAR_CONSTRAINT, con_id - ) - - def get_linear_constraint( - self, con_id: int, *, validate: bool = True - ) -> linear_constraints_mod.LinearConstraint: - """Returns the LinearConstraint for the id con_id.""" - if validate and not self._elemental.element_exists( - enums.ElementType.LINEAR_CONSTRAINT, con_id - ): - raise KeyError(f"Linear constraint does not exist with id {con_id}.") - return linear_constraints_mod.LinearConstraint(self._elemental, con_id) - - def delete_linear_constraint( - self, lin_con: linear_constraints_mod.LinearConstraint - ) -> None: - self.check_compatible(lin_con) - if not self._elemental.delete_element( - enums.ElementType.LINEAR_CONSTRAINT, lin_con.id - ): - raise ValueError( - f"Linear constraint with id {lin_con.id} was not in the model." - ) - - def linear_constraints( - self, - ) -> Iterator[linear_constraints_mod.LinearConstraint]: - """Yields the linear constraints in the order of creation.""" - lin_con_ids = self._elemental.get_elements(enums.ElementType.LINEAR_CONSTRAINT) - lin_con_ids.sort() - for lin_con_id in lin_con_ids: - yield linear_constraints_mod.LinearConstraint( - self._elemental, int(lin_con_id) - ) - - def row_nonzeros( - self, lin_con: linear_constraints_mod.LinearConstraint - ) -> Iterator[variables_mod.Variable]: - """Yields the variables with nonzero coefficient for this linear constraint.""" - keys = self._elemental.slice_attr( - enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 0, lin_con.id - ) - for var_id in keys[:, 1]: - yield variables_mod.Variable(self._elemental, int(var_id)) - - def column_nonzeros( - self, var: variables_mod.Variable - ) -> Iterator[linear_constraints_mod.LinearConstraint]: - """Yields the linear constraints with nonzero coefficient for this variable.""" - keys = self._elemental.slice_attr( - enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 1, var.id - ) - for lin_con_id in keys[:, 0]: - yield linear_constraints_mod.LinearConstraint( - self._elemental, int(lin_con_id) - ) - - def linear_constraint_matrix_entries( - self, - ) -> Iterator[linear_constraints_mod.LinearConstraintMatrixEntry]: - """Yields the nonzero elements of the linear constraint matrix in undefined order.""" - keys = self._elemental.get_attr_non_defaults( - enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT - ) - coefs = self._elemental.get_attrs( - enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, keys - ) - for i in range(len(keys)): - yield linear_constraints_mod.LinearConstraintMatrixEntry( - linear_constraint=linear_constraints_mod.LinearConstraint( - self._elemental, int(keys[i, 0]) - ), - variable=variables_mod.Variable(self._elemental, int(keys[i, 1])), - coefficient=float(coefs[i]), - ) - - ############################################################################## - # Quadratic Constraints - ############################################################################## - - def add_quadratic_constraint( - self, - bounded_expr: Optional[ - Union[ - bool, - variables_mod.BoundedLinearTypes, - variables_mod.BoundedQuadraticTypes, - ] - ] = None, - *, - lb: Optional[float] = None, - ub: Optional[float] = None, - expr: Optional[variables_mod.QuadraticTypes] = None, - name: str = "", - ) -> quadratic_constraints.QuadraticConstraint: - """Adds a quadratic constraint to the optimization model. - - The simplest way to specify the constraint is by passing a one-sided or - two-sided quadratic inequality as in: - * add_quadratic_constraint(x * x + y + 1.0 <= 2.0), - * add_quadratic_constraint(x * x + y >= 2.0), or - * add_quadratic_constraint((1.0 <= x * x + y) <= 2.0). - - Note the extra parenthesis for two-sided linear inequalities, which is - required due to some language limitations (see add_linear_constraint for - details). - - The second way to specify the constraint is by setting lb, ub, and/or expr - as in: - * add_quadratic_constraint(expr=x * x + y + 1.0, ub=2.0), - * add_quadratic_constraint(expr=x * x + y, lb=2.0), - * add_quadratic_constraint(expr=x * x + y, lb=1.0, ub=2.0), or - * add_quadratic_constraint(lb=1.0). - Omitting lb is equivalent to setting it to -math.inf and omiting ub is - equivalent to setting it to math.inf. - - These two alternatives are exclusive and a combined call like: - * add_quadratic_constraint(x * x + y <= 2.0, lb=1.0), or - * add_quadratic_constraint(x * x+ y <= 2.0, ub=math.inf) - will raise a ValueError. A ValueError is also raised if expr's offset is - infinite. - - Args: - bounded_expr: a quadratic inequality describing the constraint. Cannot be - specified together with lb, ub, or expr. - lb: The constraint's lower bound if bounded_expr is omitted (if both - bounder_expr and lb are omitted, the lower bound is -math.inf). - ub: The constraint's upper bound if bounded_expr is omitted (if both - bounder_expr and ub are omitted, the upper bound is math.inf). - expr: The constraint's quadratic expression if bounded_expr is omitted. - name: For debugging purposes only, but nonempty names must be distinct. - - Returns: - A reference to the new quadratic constraint. - """ - norm_quad = normalized_inequality.as_normalized_quadratic_inequality( - bounded_expr, lb=lb, ub=ub, expr=expr - ) - quad_con_id = self._elemental.add_element( - enums.ElementType.QUADRATIC_CONSTRAINT, name - ) - for var, coef in norm_quad.linear_coefficients.items(): - self._elemental.set_attr( - enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, - (quad_con_id, var.id), - coef, - ) - for key, coef in norm_quad.quadratic_coefficients.items(): - self._elemental.set_attr( - enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, - (quad_con_id, key.first_var.id, key.second_var.id), - coef, - ) - if norm_quad.lb > -math.inf: - self._elemental.set_attr( - enums.DoubleAttr1.QUADRATIC_CONSTRAINT_LOWER_BOUND, - (quad_con_id,), - norm_quad.lb, - ) - if norm_quad.ub < math.inf: - self._elemental.set_attr( - enums.DoubleAttr1.QUADRATIC_CONSTRAINT_UPPER_BOUND, - (quad_con_id,), - norm_quad.ub, - ) - return quadratic_constraints.QuadraticConstraint(self._elemental, quad_con_id) - - def has_quadratic_constraint(self, con_id: int) -> bool: - """Returns true if a quadratic constraint with this id is in the model.""" - return self._elemental.element_exists( - enums.ElementType.QUADRATIC_CONSTRAINT, con_id - ) - - def get_num_quadratic_constraints(self) -> int: - """Returns the number of quadratic constraints in the model.""" - return self._elemental.get_num_elements(enums.ElementType.QUADRATIC_CONSTRAINT) - - def get_next_quadratic_constraint_id(self) -> int: - """Returns the id of the next quadratic constraint created in the model.""" - return self._elemental.get_next_element_id( - enums.ElementType.QUADRATIC_CONSTRAINT - ) - - def ensure_next_quadratic_constraint_id_at_least(self, con_id: int) -> None: - """If the next quadratic constraint id would be less than `con_id`, sets it to `con_id`.""" - self._elemental.ensure_next_element_id_at_least( - enums.ElementType.QUADRATIC_CONSTRAINT, con_id - ) - - def get_quadratic_constraint( - self, con_id: int, *, validate: bool = True - ) -> quadratic_constraints.QuadraticConstraint: - """Returns the constraint for the id, or raises KeyError if not in model.""" - if validate and not self._elemental.element_exists( - enums.ElementType.QUADRATIC_CONSTRAINT, con_id - ): - raise KeyError(f"Quadratic constraint does not exist with id {con_id}.") - return quadratic_constraints.QuadraticConstraint(self._elemental, con_id) - - def delete_quadratic_constraint( - self, quad_con: quadratic_constraints.QuadraticConstraint - ) -> None: - """Deletes the constraint with id, or raises ValueError if not in model.""" - self.check_compatible(quad_con) - if not self._elemental.delete_element( - enums.ElementType.QUADRATIC_CONSTRAINT, quad_con.id - ): - raise ValueError( - f"Quadratic constraint with id {quad_con.id} was not in the model." - ) - - def get_quadratic_constraints( - self, - ) -> Iterator[quadratic_constraints.QuadraticConstraint]: - """Yields the quadratic constraints in the order of creation.""" - quad_con_ids = self._elemental.get_elements( - enums.ElementType.QUADRATIC_CONSTRAINT - ) - quad_con_ids.sort() - for quad_con_id in quad_con_ids: - yield quadratic_constraints.QuadraticConstraint( - self._elemental, int(quad_con_id) - ) - - def quadratic_constraint_linear_nonzeros( - self, - ) -> Iterator[ - Tuple[ - quadratic_constraints.QuadraticConstraint, - variables_mod.Variable, - float, - ] - ]: - """Yields the linear coefficients for all quadratic constraints in the model.""" - keys = self._elemental.get_attr_non_defaults( - enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT - ) - coefs = self._elemental.get_attrs( - enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, keys - ) - for i in range(len(keys)): - yield ( - quadratic_constraints.QuadraticConstraint( - self._elemental, int(keys[i, 0]) - ), - variables_mod.Variable(self._elemental, int(keys[i, 1])), - float(coefs[i]), - ) - - def quadratic_constraint_quadratic_nonzeros( - self, - ) -> Iterator[ - Tuple[ - quadratic_constraints.QuadraticConstraint, - variables_mod.Variable, - variables_mod.Variable, - float, - ] - ]: - """Yields the quadratic coefficients for all quadratic constraints in the model.""" - keys = self._elemental.get_attr_non_defaults( - enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT - ) - coefs = self._elemental.get_attrs( - enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, - keys, - ) - for i in range(len(keys)): - yield ( - quadratic_constraints.QuadraticConstraint( - self._elemental, int(keys[i, 0]) - ), - variables_mod.Variable(self._elemental, int(keys[i, 1])), - variables_mod.Variable(self._elemental, int(keys[i, 2])), - float(coefs[i]), - ) - - ############################################################################## - # Indicator Constraints - ############################################################################## - - def add_indicator_constraint( - self, - *, - indicator: Optional[variables_mod.Variable] = None, - activate_on_zero: bool = False, - implied_constraint: Optional[ - Union[bool, variables_mod.BoundedLinearTypes] - ] = None, - implied_lb: Optional[float] = None, - implied_ub: Optional[float] = None, - implied_expr: Optional[variables_mod.LinearTypes] = None, - name: str = "", - ) -> indicator_constraints.IndicatorConstraint: - """Adds an indicator constraint to the model. - - If indicator is None or the variable equal to indicator is deleted from - the model, the model will be considered invalid at solve time (unless this - constraint is also deleted before solving). Likewise, the variable indicator - must be binary at solve time for the model to be valid. - - If implied_constraint is set, you may not set implied_lb, implied_ub, or - implied_expr. - - Args: - indicator: The variable whose value determines if implied_constraint must - be enforced. - activate_on_zero: If true, implied_constraint must hold when indicator is - zero, otherwise, the implied_constraint must hold when indicator is one. - implied_constraint: A linear constraint to conditionally enforce, if set. - If None, that information is instead passed via implied_lb, implied_ub, - and implied_expr. - implied_lb: The lower bound of the condtionally enforced linear constraint - (or -inf if None), used only when implied_constraint is None. - implied_ub: The upper bound of the condtionally enforced linear constraint - (or +inf if None), used only when implied_constraint is None. - implied_expr: The linear part of the condtionally enforced linear - constraint (or 0 if None), used only when implied_constraint is None. If - expr has a nonzero offset, it is subtracted from lb and ub. - name: For debugging purposes only, but nonempty names must be distinct. - - Returns: - A reference to the new indicator constraint. - """ - ind_con_id = self._elemental.add_element( - enums.ElementType.INDICATOR_CONSTRAINT, name - ) - if indicator is not None: - self._elemental.set_attr( - enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR, - (ind_con_id,), - indicator.id, - ) - self._elemental.set_attr( - enums.BoolAttr1.INDICATOR_CONSTRAINT_ACTIVATE_ON_ZERO, - (ind_con_id,), - activate_on_zero, - ) - implied_inequality = normalized_inequality.as_normalized_linear_inequality( - implied_constraint, lb=implied_lb, ub=implied_ub, expr=implied_expr - ) - self._elemental.set_attr( - enums.DoubleAttr1.INDICATOR_CONSTRAINT_LOWER_BOUND, - (ind_con_id,), - implied_inequality.lb, - ) - self._elemental.set_attr( - enums.DoubleAttr1.INDICATOR_CONSTRAINT_UPPER_BOUND, - (ind_con_id,), - implied_inequality.ub, - ) - for var, coef in implied_inequality.coefficients.items(): - self._elemental.set_attr( - enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, - (ind_con_id, var.id), - coef, - ) - - return indicator_constraints.IndicatorConstraint(self._elemental, ind_con_id) - - def has_indicator_constraint(self, con_id: int) -> bool: - """Returns true if an indicator constraint with this id is in the model.""" - return self._elemental.element_exists( - enums.ElementType.INDICATOR_CONSTRAINT, con_id - ) - - def get_num_indicator_constraints(self) -> int: - """Returns the number of indicator constraints in the model.""" - return self._elemental.get_num_elements(enums.ElementType.INDICATOR_CONSTRAINT) - - def get_next_indicator_constraint_id(self) -> int: - """Returns the id of the next indicator constraint created in the model.""" - return self._elemental.get_next_element_id( - enums.ElementType.INDICATOR_CONSTRAINT - ) - - def ensure_next_indicator_constraint_id_at_least(self, con_id: int) -> None: - """If the next indicator constraint id would be less than `con_id`, sets it to `con_id`.""" - self._elemental.ensure_next_element_id_at_least( - enums.ElementType.INDICATOR_CONSTRAINT, con_id - ) - - def get_indicator_constraint( - self, con_id: int, *, validate: bool = True - ) -> indicator_constraints.IndicatorConstraint: - """Returns the IndicatorConstraint for the id con_id.""" - if validate and not self._elemental.element_exists( - enums.ElementType.INDICATOR_CONSTRAINT, con_id - ): - raise KeyError(f"Indicator constraint does not exist with id {con_id}.") - return indicator_constraints.IndicatorConstraint(self._elemental, con_id) - - def delete_indicator_constraint( - self, ind_con: indicator_constraints.IndicatorConstraint - ) -> None: - self.check_compatible(ind_con) - if not self._elemental.delete_element( - enums.ElementType.INDICATOR_CONSTRAINT, ind_con.id - ): - raise ValueError( - f"Indicator constraint with id {ind_con.id} was not in the model." - ) - - def get_indicator_constraints( - self, - ) -> Iterator[indicator_constraints.IndicatorConstraint]: - """Yields the indicator constraints in the order of creation.""" - ind_con_ids = self._elemental.get_elements( - enums.ElementType.INDICATOR_CONSTRAINT - ) - ind_con_ids.sort() - for ind_con_id in ind_con_ids: - yield indicator_constraints.IndicatorConstraint( - self._elemental, int(ind_con_id) - ) - - ############################################################################## - # Proto import/export - ############################################################################## - - def export_model(self) -> model_pb2.ModelProto: - """Returns a protocol buffer equivalent to this model.""" - return self._elemental.export_model(remove_names=False) - - def add_update_tracker(self) -> UpdateTracker: - """Creates an UpdateTracker registered on this model to view changes.""" - return UpdateTracker(self._elemental.add_diff(), self._elemental) - - def remove_update_tracker(self, tracker: UpdateTracker): - """Stops tracker from getting updates on changes to this model. - - An error will be raised if tracker was not created by this Model or if - tracker has been previously removed. - - Using (via checkpoint or update) an UpdateTracker after it has been removed - will result in an error. - - Args: - tracker: The UpdateTracker to unregister. - - Raises: - KeyError: The tracker was created by another model or was already removed. - """ - self._elemental.delete_diff(tracker.diff_id) - - def check_compatible(self, e: from_model.FromModel) -> None: - """Raises a ValueError if the model of var_or_constraint is not self.""" - if e.elemental is not self._elemental: - raise ValueError( - f"Expected element from model named: '{self._elemental.model_name}'," - f" but observed element {e} from model named:" - f" '{e.elemental.model_name}'." - ) + variable_id = self._elemental.add_element(enums.ElementType.VARIABLE, name) + result = variables_mod.Variable(self._elemental, variable_id) + result.lower_bound = lb + result.upper_bound = ub + result.integer = is_integer + return result + + def add_integer_variable( + self, *, lb: float = -math.inf, ub: float = math.inf, name: str = "" + ) -> variables_mod.Variable: + return self.add_variable(lb=lb, ub=ub, is_integer=True, name=name) + + def add_binary_variable(self, *, name: str = "") -> variables_mod.Variable: + return self.add_variable(lb=0.0, ub=1.0, is_integer=True, name=name) + + def get_variable( + self, var_id: int, *, validate: bool = True + ) -> variables_mod.Variable: + """Returns the Variable for the id var_id, or raises KeyError.""" + if validate and not self._elemental.element_exists( + enums.ElementType.VARIABLE, var_id + ): + raise KeyError(f"Variable does not exist with id {var_id}.") + return variables_mod.Variable(self._elemental, var_id) + + def has_variable(self, var_id: int) -> bool: + """Returns true if a Variable with this id is in the model.""" + return self._elemental.element_exists(enums.ElementType.VARIABLE, var_id) + + def get_num_variables(self) -> int: + """Returns the number of variables in the model.""" + return self._elemental.get_num_elements(enums.ElementType.VARIABLE) + + def get_next_variable_id(self) -> int: + """Returns the id of the next variable created in the model.""" + return self._elemental.get_next_element_id(enums.ElementType.VARIABLE) + + def ensure_next_variable_id_at_least(self, var_id: int) -> None: + """If the next variable id would be less than `var_id`, sets it to `var_id`.""" + self._elemental.ensure_next_element_id_at_least( + enums.ElementType.VARIABLE, var_id + ) + + def delete_variable(self, var: variables_mod.Variable) -> None: + """Removes this variable from the model.""" + self.check_compatible(var) + if not self._elemental.delete_element(enums.ElementType.VARIABLE, var.id): + raise ValueError(f"Variable with id {var.id} was not in the model.") + + def variables(self) -> Iterator[variables_mod.Variable]: + """Yields the variables in the order of creation.""" + var_ids = self._elemental.get_elements(enums.ElementType.VARIABLE) + var_ids.sort() + for var_id in var_ids: + yield variables_mod.Variable(self._elemental, int(var_id)) + + ############################################################################## + # Objective + ############################################################################## + + @property + def objective(self) -> objectives.Objective: + return objectives.PrimaryObjective(self._elemental) + + def maximize(self, obj: variables_mod.QuadraticTypes) -> None: + """Sets the objective to maximize the provided expression `obj`.""" + self.set_objective(obj, is_maximize=True) + + def maximize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: + """Sets the objective to maximize the provided linear expression `obj`.""" + self.set_linear_objective(obj, is_maximize=True) + + def maximize_quadratic_objective( + self, obj: variables_mod.QuadraticTypes + ) -> None: + """Sets the objective to maximize the provided quadratic expression `obj`.""" + self.set_quadratic_objective(obj, is_maximize=True) + + def minimize(self, obj: variables_mod.QuadraticTypes) -> None: + """Sets the objective to minimize the provided expression `obj`.""" + self.set_objective(obj, is_maximize=False) + + def minimize_linear_objective(self, obj: variables_mod.LinearTypes) -> None: + """Sets the objective to minimize the provided linear expression `obj`.""" + self.set_linear_objective(obj, is_maximize=False) + + def minimize_quadratic_objective( + self, obj: variables_mod.QuadraticTypes + ) -> None: + """Sets the objective to minimize the provided quadratic expression `obj`.""" + self.set_quadratic_objective(obj, is_maximize=False) + + def set_objective( + self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool + ) -> None: + """Sets the objective to optimize the provided expression `obj`.""" + self.objective.set_to_expression(obj) + self.objective.is_maximize = is_maximize + + def set_linear_objective( + self, obj: variables_mod.LinearTypes, *, is_maximize: bool + ) -> None: + """Sets the objective to optimize the provided linear expression `obj`.""" + self.objective.set_to_linear_expression(obj) + self.objective.is_maximize = is_maximize + + def set_quadratic_objective( + self, obj: variables_mod.QuadraticTypes, *, is_maximize: bool + ) -> None: + """Sets the objective to optimize the provided quadratic expression `obj`.""" + self.objective.set_to_quadratic_expression(obj) + self.objective.is_maximize = is_maximize + + def linear_objective_terms(self) -> Iterator[variables_mod.LinearTerm]: + """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" + yield from self.objective.linear_terms() + + def quadratic_objective_terms(self) -> Iterator[variables_mod.QuadraticTerm]: + """Yields the quadratic terms with nonzero objective coefficient in undefined order.""" + yield from self.objective.quadratic_terms() + + ############################################################################## + # Auxiliary Objectives + ############################################################################## + + def add_auxiliary_objective( + self, + *, + priority: int, + name: str = "", + expr: Optional[variables_mod.LinearTypes] = None, + is_maximize: bool = False, + ) -> objectives.AuxiliaryObjective: + """Adds an additional objective to the model.""" + obj_id = self._elemental.add_element( + enums.ElementType.AUXILIARY_OBJECTIVE, name + ) + self._elemental.set_attr( + enums.IntAttr1.AUXILIARY_OBJECTIVE_PRIORITY, (obj_id,), priority + ) + result = objectives.AuxiliaryObjective(self._elemental, obj_id) + if expr is not None: + result.set_to_linear_expression(expr) + result.is_maximize = is_maximize + return result + + def add_maximization_objective( + self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" + ) -> objectives.AuxiliaryObjective: + """Adds an additional objective to the model that is maximizaition.""" + result = self.add_auxiliary_objective( + priority=priority, name=name, expr=expr, is_maximize=True + ) + return result + + def add_minimization_objective( + self, expr: variables_mod.LinearTypes, *, priority: int, name: str = "" + ) -> objectives.AuxiliaryObjective: + """Adds an additional objective to the model that is minimizaition.""" + result = self.add_auxiliary_objective( + priority=priority, name=name, expr=expr, is_maximize=False + ) + return result + + def delete_auxiliary_objective( + self, obj: objectives.AuxiliaryObjective + ) -> None: + """Removes an auxiliary objective from the model.""" + self.check_compatible(obj) + if not self._elemental.delete_element( + enums.ElementType.AUXILIARY_OBJECTIVE, obj.id + ): + raise ValueError( + f"Auxiliary objective with id {obj.id} is not in the model." + ) + + def has_auxiliary_objective(self, obj_id: int) -> bool: + """Returns true if the model has an auxiliary objective with id `obj_id`.""" + return self._elemental.element_exists( + enums.ElementType.AUXILIARY_OBJECTIVE, obj_id + ) + + def next_auxiliary_objective_id(self) -> int: + """Returns the id of the next auxiliary objective added to the model.""" + return self._elemental.get_next_element_id( + enums.ElementType.AUXILIARY_OBJECTIVE + ) + + def num_auxiliary_objectives(self) -> int: + """Returns the number of auxiliary objectives in this model.""" + return self._elemental.get_num_elements( + enums.ElementType.AUXILIARY_OBJECTIVE + ) + + def ensure_next_auxiliary_objective_id_at_least(self, obj_id: int) -> None: + """If the next auxiliary objective id would be less than `obj_id`, sets it to `obj_id`.""" + self._elemental.ensure_next_element_id_at_least( + enums.ElementType.AUXILIARY_OBJECTIVE, obj_id + ) + + def get_auxiliary_objective( + self, obj_id: int, *, validate: bool = True + ) -> objectives.AuxiliaryObjective: + """Returns the auxiliary objective with this id. + + If there is no objective with this id, an exception is thrown if validate is + true, and an invalid AuxiliaryObjective is returned if validate is false + (later interactions with this object will cause unpredictable errors). Only + set validate=False if there is a known performance problem. + + Args: + obj_id: The id of the auxiliary objective to look for. + validate: Set to false for more speed, but fails to raise an exception if + the objective is missing. + + Raises: + KeyError: If `validate` is True and there is no objective with this id. + """ + if validate and not self.has_auxiliary_objective(obj_id): + raise KeyError(f"Model has no auxiliary objective with id {obj_id}") + return objectives.AuxiliaryObjective(self._elemental, obj_id) + + def auxiliary_objectives(self) -> Iterator[objectives.AuxiliaryObjective]: + """Returns the auxiliary objectives in the model in the order of creation.""" + ids = self._elemental.get_elements(enums.ElementType.AUXILIARY_OBJECTIVE) + ids.sort() + for aux_obj_id in ids: + yield objectives.AuxiliaryObjective(self._elemental, int(aux_obj_id)) + + ############################################################################## + # Linear Constraints + ############################################################################## + + # TODO(b/227214976): Update the note below and link to pytype bug number. + # Note: bounded_expr's type includes bool only as a workaround to a pytype + # issue. Passing a bool for bounded_expr will raise an error in runtime. + def add_linear_constraint( + self, + bounded_expr: Optional[ + Union[bool, variables_mod.BoundedLinearTypes] + ] = None, + *, + lb: Optional[float] = None, + ub: Optional[float] = None, + expr: Optional[variables_mod.LinearTypes] = None, + name: str = "", + ) -> linear_constraints_mod.LinearConstraint: + """Adds a linear constraint to the optimization model. + + The simplest way to specify the constraint is by passing a one-sided or + two-sided linear inequality as in: + * add_linear_constraint(x + y + 1.0 <= 2.0), + * add_linear_constraint(x + y >= 2.0), or + * add_linear_constraint((1.0 <= x + y) <= 2.0). + + Note the extra parenthesis for two-sided linear inequalities, which is + required due to some language limitations (see + https://peps.python.org/pep-0335/ and https://peps.python.org/pep-0535/). + If the parenthesis are omitted, a TypeError will be raised explaining the + issue (if this error was not raised the first inequality would have been + silently ignored because of the noted language limitations). + + The second way to specify the constraint is by setting lb, ub, and/or expr + as in: + * add_linear_constraint(expr=x + y + 1.0, ub=2.0), + * add_linear_constraint(expr=x + y, lb=2.0), + * add_linear_constraint(expr=x + y, lb=1.0, ub=2.0), or + * add_linear_constraint(lb=1.0). + Omitting lb is equivalent to setting it to -math.inf and omiting ub is + equivalent to setting it to math.inf. + + These two alternatives are exclusive and a combined call like: + * add_linear_constraint(x + y <= 2.0, lb=1.0), or + * add_linear_constraint(x + y <= 2.0, ub=math.inf) + will raise a ValueError. A ValueError is also raised if expr's offset is + infinite. + + Args: + bounded_expr: a linear inequality describing the constraint. Cannot be + specified together with lb, ub, or expr. + lb: The constraint's lower bound if bounded_expr is omitted (if both + bounder_expr and lb are omitted, the lower bound is -math.inf). + ub: The constraint's upper bound if bounded_expr is omitted (if both + bounder_expr and ub are omitted, the upper bound is math.inf). + expr: The constraint's linear expression if bounded_expr is omitted. + name: For debugging purposes only, but nonempty names must be distinct. + + Returns: + A reference to the new linear constraint. + """ + norm_ineq = normalized_inequality.as_normalized_linear_inequality( + bounded_expr, lb=lb, ub=ub, expr=expr + ) + lin_con_id = self._elemental.add_element( + enums.ElementType.LINEAR_CONSTRAINT, name + ) + + result = linear_constraints_mod.LinearConstraint( + self._elemental, lin_con_id + ) + result.lower_bound = norm_ineq.lb + result.upper_bound = norm_ineq.ub + for var, coefficient in norm_ineq.coefficients.items(): + result.set_coefficient(var, coefficient) + return result + + def has_linear_constraint(self, con_id: int) -> bool: + """Returns true if a linear constraint with this id is in the model.""" + return self._elemental.element_exists( + enums.ElementType.LINEAR_CONSTRAINT, con_id + ) + + def get_num_linear_constraints(self) -> int: + """Returns the number of linear constraints in the model.""" + return self._elemental.get_num_elements(enums.ElementType.LINEAR_CONSTRAINT) + + def get_next_linear_constraint_id(self) -> int: + """Returns the id of the next linear constraint created in the model.""" + return self._elemental.get_next_element_id( + enums.ElementType.LINEAR_CONSTRAINT + ) + + def ensure_next_linear_constraint_id_at_least(self, con_id: int) -> None: + """If the next linear constraint id would be less than `con_id`, sets it to `con_id`.""" + self._elemental.ensure_next_element_id_at_least( + enums.ElementType.LINEAR_CONSTRAINT, con_id + ) + + def get_linear_constraint( + self, con_id: int, *, validate: bool = True + ) -> linear_constraints_mod.LinearConstraint: + """Returns the LinearConstraint for the id con_id.""" + if validate and not self._elemental.element_exists( + enums.ElementType.LINEAR_CONSTRAINT, con_id + ): + raise KeyError(f"Linear constraint does not exist with id {con_id}.") + return linear_constraints_mod.LinearConstraint(self._elemental, con_id) + + def delete_linear_constraint( + self, lin_con: linear_constraints_mod.LinearConstraint + ) -> None: + self.check_compatible(lin_con) + if not self._elemental.delete_element( + enums.ElementType.LINEAR_CONSTRAINT, lin_con.id + ): + raise ValueError( + f"Linear constraint with id {lin_con.id} was not in the model." + ) + + def linear_constraints( + self, + ) -> Iterator[linear_constraints_mod.LinearConstraint]: + """Yields the linear constraints in the order of creation.""" + lin_con_ids = self._elemental.get_elements( + enums.ElementType.LINEAR_CONSTRAINT + ) + lin_con_ids.sort() + for lin_con_id in lin_con_ids: + yield linear_constraints_mod.LinearConstraint( + self._elemental, int(lin_con_id) + ) + + def row_nonzeros( + self, lin_con: linear_constraints_mod.LinearConstraint + ) -> Iterator[variables_mod.Variable]: + """Yields the variables with nonzero coefficient for this linear constraint.""" + keys = self._elemental.slice_attr( + enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 0, lin_con.id + ) + for var_id in keys[:, 1]: + yield variables_mod.Variable(self._elemental, int(var_id)) + + def column_nonzeros( + self, var: variables_mod.Variable + ) -> Iterator[linear_constraints_mod.LinearConstraint]: + """Yields the linear constraints with nonzero coefficient for this variable.""" + keys = self._elemental.slice_attr( + enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, 1, var.id + ) + for lin_con_id in keys[:, 0]: + yield linear_constraints_mod.LinearConstraint( + self._elemental, int(lin_con_id) + ) + + def linear_constraint_matrix_entries( + self, + ) -> Iterator[linear_constraints_mod.LinearConstraintMatrixEntry]: + """Yields the nonzero elements of the linear constraint matrix in undefined order.""" + keys = self._elemental.get_attr_non_defaults( + enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT + ) + coefs = self._elemental.get_attrs( + enums.DoubleAttr2.LINEAR_CONSTRAINT_COEFFICIENT, keys + ) + for i in range(len(keys)): + yield linear_constraints_mod.LinearConstraintMatrixEntry( + linear_constraint=linear_constraints_mod.LinearConstraint( + self._elemental, int(keys[i, 0]) + ), + variable=variables_mod.Variable(self._elemental, int(keys[i, 1])), + coefficient=float(coefs[i]), + ) + + ############################################################################## + # Quadratic Constraints + ############################################################################## + + def add_quadratic_constraint( + self, + bounded_expr: Optional[ + Union[ + bool, + variables_mod.BoundedLinearTypes, + variables_mod.BoundedQuadraticTypes, + ] + ] = None, + *, + lb: Optional[float] = None, + ub: Optional[float] = None, + expr: Optional[variables_mod.QuadraticTypes] = None, + name: str = "", + ) -> quadratic_constraints.QuadraticConstraint: + """Adds a quadratic constraint to the optimization model. + + The simplest way to specify the constraint is by passing a one-sided or + two-sided quadratic inequality as in: + * add_quadratic_constraint(x * x + y + 1.0 <= 2.0), + * add_quadratic_constraint(x * x + y >= 2.0), or + * add_quadratic_constraint((1.0 <= x * x + y) <= 2.0). + + Note the extra parenthesis for two-sided linear inequalities, which is + required due to some language limitations (see add_linear_constraint for + details). + + The second way to specify the constraint is by setting lb, ub, and/or expr + as in: + * add_quadratic_constraint(expr=x * x + y + 1.0, ub=2.0), + * add_quadratic_constraint(expr=x * x + y, lb=2.0), + * add_quadratic_constraint(expr=x * x + y, lb=1.0, ub=2.0), or + * add_quadratic_constraint(lb=1.0). + Omitting lb is equivalent to setting it to -math.inf and omiting ub is + equivalent to setting it to math.inf. + + These two alternatives are exclusive and a combined call like: + * add_quadratic_constraint(x * x + y <= 2.0, lb=1.0), or + * add_quadratic_constraint(x * x+ y <= 2.0, ub=math.inf) + will raise a ValueError. A ValueError is also raised if expr's offset is + infinite. + + Args: + bounded_expr: a quadratic inequality describing the constraint. Cannot be + specified together with lb, ub, or expr. + lb: The constraint's lower bound if bounded_expr is omitted (if both + bounder_expr and lb are omitted, the lower bound is -math.inf). + ub: The constraint's upper bound if bounded_expr is omitted (if both + bounder_expr and ub are omitted, the upper bound is math.inf). + expr: The constraint's quadratic expression if bounded_expr is omitted. + name: For debugging purposes only, but nonempty names must be distinct. + + Returns: + A reference to the new quadratic constraint. + """ + norm_quad = normalized_inequality.as_normalized_quadratic_inequality( + bounded_expr, lb=lb, ub=ub, expr=expr + ) + quad_con_id = self._elemental.add_element( + enums.ElementType.QUADRATIC_CONSTRAINT, name + ) + for var, coef in norm_quad.linear_coefficients.items(): + self._elemental.set_attr( + enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, + (quad_con_id, var.id), + coef, + ) + for key, coef in norm_quad.quadratic_coefficients.items(): + self._elemental.set_attr( + enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, + (quad_con_id, key.first_var.id, key.second_var.id), + coef, + ) + if norm_quad.lb > -math.inf: + self._elemental.set_attr( + enums.DoubleAttr1.QUADRATIC_CONSTRAINT_LOWER_BOUND, + (quad_con_id,), + norm_quad.lb, + ) + if norm_quad.ub < math.inf: + self._elemental.set_attr( + enums.DoubleAttr1.QUADRATIC_CONSTRAINT_UPPER_BOUND, + (quad_con_id,), + norm_quad.ub, + ) + return quadratic_constraints.QuadraticConstraint( + self._elemental, quad_con_id + ) + + def has_quadratic_constraint(self, con_id: int) -> bool: + """Returns true if a quadratic constraint with this id is in the model.""" + return self._elemental.element_exists( + enums.ElementType.QUADRATIC_CONSTRAINT, con_id + ) + + def get_num_quadratic_constraints(self) -> int: + """Returns the number of quadratic constraints in the model.""" + return self._elemental.get_num_elements( + enums.ElementType.QUADRATIC_CONSTRAINT + ) + + def get_next_quadratic_constraint_id(self) -> int: + """Returns the id of the next quadratic constraint created in the model.""" + return self._elemental.get_next_element_id( + enums.ElementType.QUADRATIC_CONSTRAINT + ) + + def ensure_next_quadratic_constraint_id_at_least(self, con_id: int) -> None: + """If the next quadratic constraint id would be less than `con_id`, sets it to `con_id`.""" + self._elemental.ensure_next_element_id_at_least( + enums.ElementType.QUADRATIC_CONSTRAINT, con_id + ) + + def get_quadratic_constraint( + self, con_id: int, *, validate: bool = True + ) -> quadratic_constraints.QuadraticConstraint: + """Returns the constraint for the id, or raises KeyError if not in model.""" + if validate and not self._elemental.element_exists( + enums.ElementType.QUADRATIC_CONSTRAINT, con_id + ): + raise KeyError(f"Quadratic constraint does not exist with id {con_id}.") + return quadratic_constraints.QuadraticConstraint(self._elemental, con_id) + + def delete_quadratic_constraint( + self, quad_con: quadratic_constraints.QuadraticConstraint + ) -> None: + """Deletes the constraint with id, or raises ValueError if not in model.""" + self.check_compatible(quad_con) + if not self._elemental.delete_element( + enums.ElementType.QUADRATIC_CONSTRAINT, quad_con.id + ): + raise ValueError( + f"Quadratic constraint with id {quad_con.id} was not in the model." + ) + + def get_quadratic_constraints( + self, + ) -> Iterator[quadratic_constraints.QuadraticConstraint]: + """Yields the quadratic constraints in the order of creation.""" + quad_con_ids = self._elemental.get_elements( + enums.ElementType.QUADRATIC_CONSTRAINT + ) + quad_con_ids.sort() + for quad_con_id in quad_con_ids: + yield quadratic_constraints.QuadraticConstraint( + self._elemental, int(quad_con_id) + ) + + def quadratic_constraint_linear_nonzeros( + self, + ) -> Iterator[ + Tuple[ + quadratic_constraints.QuadraticConstraint, + variables_mod.Variable, + float, + ] + ]: + """Yields the linear coefficients for all quadratic constraints in the model.""" + keys = self._elemental.get_attr_non_defaults( + enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT + ) + coefs = self._elemental.get_attrs( + enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, keys + ) + for i in range(len(keys)): + yield ( + quadratic_constraints.QuadraticConstraint( + self._elemental, int(keys[i, 0]) + ), + variables_mod.Variable(self._elemental, int(keys[i, 1])), + float(coefs[i]), + ) + + def quadratic_constraint_quadratic_nonzeros( + self, + ) -> Iterator[ + Tuple[ + quadratic_constraints.QuadraticConstraint, + variables_mod.Variable, + variables_mod.Variable, + float, + ] + ]: + """Yields the quadratic coefficients for all quadratic constraints in the model.""" + keys = self._elemental.get_attr_non_defaults( + enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT + ) + coefs = self._elemental.get_attrs( + enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, + keys, + ) + for i in range(len(keys)): + yield ( + quadratic_constraints.QuadraticConstraint( + self._elemental, int(keys[i, 0]) + ), + variables_mod.Variable(self._elemental, int(keys[i, 1])), + variables_mod.Variable(self._elemental, int(keys[i, 2])), + float(coefs[i]), + ) + + ############################################################################## + # Indicator Constraints + ############################################################################## + + def add_indicator_constraint( + self, + *, + indicator: Optional[variables_mod.Variable] = None, + activate_on_zero: bool = False, + implied_constraint: Optional[ + Union[bool, variables_mod.BoundedLinearTypes] + ] = None, + implied_lb: Optional[float] = None, + implied_ub: Optional[float] = None, + implied_expr: Optional[variables_mod.LinearTypes] = None, + name: str = "", + ) -> indicator_constraints.IndicatorConstraint: + """Adds an indicator constraint to the model. + + If indicator is None or the variable equal to indicator is deleted from + the model, the model will be considered invalid at solve time (unless this + constraint is also deleted before solving). Likewise, the variable indicator + must be binary at solve time for the model to be valid. + + If implied_constraint is set, you may not set implied_lb, implied_ub, or + implied_expr. + + Args: + indicator: The variable whose value determines if implied_constraint must + be enforced. + activate_on_zero: If true, implied_constraint must hold when indicator is + zero, otherwise, the implied_constraint must hold when indicator is one. + implied_constraint: A linear constraint to conditionally enforce, if set. + If None, that information is instead passed via implied_lb, implied_ub, + and implied_expr. + implied_lb: The lower bound of the condtionally enforced linear constraint + (or -inf if None), used only when implied_constraint is None. + implied_ub: The upper bound of the condtionally enforced linear constraint + (or +inf if None), used only when implied_constraint is None. + implied_expr: The linear part of the condtionally enforced linear + constraint (or 0 if None), used only when implied_constraint is None. If + expr has a nonzero offset, it is subtracted from lb and ub. + name: For debugging purposes only, but nonempty names must be distinct. + + Returns: + A reference to the new indicator constraint. + """ + ind_con_id = self._elemental.add_element( + enums.ElementType.INDICATOR_CONSTRAINT, name + ) + if indicator is not None: + self._elemental.set_attr( + enums.VariableAttr1.INDICATOR_CONSTRAINT_INDICATOR, + (ind_con_id,), + indicator.id, + ) + self._elemental.set_attr( + enums.BoolAttr1.INDICATOR_CONSTRAINT_ACTIVATE_ON_ZERO, + (ind_con_id,), + activate_on_zero, + ) + implied_inequality = normalized_inequality.as_normalized_linear_inequality( + implied_constraint, lb=implied_lb, ub=implied_ub, expr=implied_expr + ) + self._elemental.set_attr( + enums.DoubleAttr1.INDICATOR_CONSTRAINT_LOWER_BOUND, + (ind_con_id,), + implied_inequality.lb, + ) + self._elemental.set_attr( + enums.DoubleAttr1.INDICATOR_CONSTRAINT_UPPER_BOUND, + (ind_con_id,), + implied_inequality.ub, + ) + for var, coef in implied_inequality.coefficients.items(): + self._elemental.set_attr( + enums.DoubleAttr2.INDICATOR_CONSTRAINT_LINEAR_COEFFICIENT, + (ind_con_id, var.id), + coef, + ) + + return indicator_constraints.IndicatorConstraint( + self._elemental, ind_con_id + ) + + def has_indicator_constraint(self, con_id: int) -> bool: + """Returns true if an indicator constraint with this id is in the model.""" + return self._elemental.element_exists( + enums.ElementType.INDICATOR_CONSTRAINT, con_id + ) + + def get_num_indicator_constraints(self) -> int: + """Returns the number of indicator constraints in the model.""" + return self._elemental.get_num_elements( + enums.ElementType.INDICATOR_CONSTRAINT + ) + + def get_next_indicator_constraint_id(self) -> int: + """Returns the id of the next indicator constraint created in the model.""" + return self._elemental.get_next_element_id( + enums.ElementType.INDICATOR_CONSTRAINT + ) + + def ensure_next_indicator_constraint_id_at_least(self, con_id: int) -> None: + """If the next indicator constraint id would be less than `con_id`, sets it to `con_id`.""" + self._elemental.ensure_next_element_id_at_least( + enums.ElementType.INDICATOR_CONSTRAINT, con_id + ) + + def get_indicator_constraint( + self, con_id: int, *, validate: bool = True + ) -> indicator_constraints.IndicatorConstraint: + """Returns the IndicatorConstraint for the id con_id.""" + if validate and not self._elemental.element_exists( + enums.ElementType.INDICATOR_CONSTRAINT, con_id + ): + raise KeyError(f"Indicator constraint does not exist with id {con_id}.") + return indicator_constraints.IndicatorConstraint(self._elemental, con_id) + + def delete_indicator_constraint( + self, ind_con: indicator_constraints.IndicatorConstraint + ) -> None: + self.check_compatible(ind_con) + if not self._elemental.delete_element( + enums.ElementType.INDICATOR_CONSTRAINT, ind_con.id + ): + raise ValueError( + f"Indicator constraint with id {ind_con.id} was not in the model." + ) + + def get_indicator_constraints( + self, + ) -> Iterator[indicator_constraints.IndicatorConstraint]: + """Yields the indicator constraints in the order of creation.""" + ind_con_ids = self._elemental.get_elements( + enums.ElementType.INDICATOR_CONSTRAINT + ) + ind_con_ids.sort() + for ind_con_id in ind_con_ids: + yield indicator_constraints.IndicatorConstraint( + self._elemental, int(ind_con_id) + ) + + ############################################################################## + # Proto import/export + ############################################################################## + + def export_model(self) -> model_pb2.ModelProto: + """Returns a protocol buffer equivalent to this model.""" + return self._elemental.export_model(remove_names=False) + + def add_update_tracker(self) -> UpdateTracker: + """Creates an UpdateTracker registered on this model to view changes.""" + return UpdateTracker(self._elemental.add_diff(), self._elemental) + + def remove_update_tracker(self, tracker: UpdateTracker): + """Stops tracker from getting updates on changes to this model. + + An error will be raised if tracker was not created by this Model or if + tracker has been previously removed. + + Using (via checkpoint or update) an UpdateTracker after it has been removed + will result in an error. + + Args: + tracker: The UpdateTracker to unregister. + + Raises: + KeyError: The tracker was created by another model or was already removed. + """ + self._elemental.delete_diff(tracker.diff_id) + + def check_compatible(self, e: from_model.FromModel) -> None: + """Raises a ValueError if the model of var_or_constraint is not self.""" + if e.elemental is not self._elemental: + raise ValueError( + f"Expected element from model named: '{self._elemental.model_name}'," + f" but observed element {e} from model named:" + f" '{e.elemental.model_name}'." + ) diff --git a/ortools/math_opt/python/model_element_test.py b/ortools/math_opt/python/model_element_test.py index 1bf1cc15c67..7aabb8ea903 100644 --- a/ortools/math_opt/python/model_element_test.py +++ b/ortools/math_opt/python/model_element_test.py @@ -34,22 +34,22 @@ # We cannot use Callable here because we need to support a named argument. class GetElement(Protocol, Generic[T]): - def __call__( - self, mod: model.Model, element_id: int, *, validate: bool = True - ) -> T: - pass + def __call__( + self, mod: model.Model, element_id: int, *, validate: bool = True + ) -> T: + pass @dataclasses.dataclass(frozen=True) class ElementAdapter(Generic[T]): - add: Callable[[model.Model], T] - delete: Callable[[model.Model, T], None] - has: Callable[[model.Model, int], bool] - get: GetElement[T] - get_all: Callable[[model.Model], Iterator[T]] - num: Callable[[model.Model], int] - next_id: Callable[[model.Model], int] - ensure_next_id: Callable[[model.Model, int], None] + add: Callable[[model.Model], T] + delete: Callable[[model.Model, T], None] + has: Callable[[model.Model, int], bool] + get: GetElement[T] + get_all: Callable[[model.Model], Iterator[T]] + num: Callable[[model.Model], int] + next_id: Callable[[model.Model], int] + ensure_next_id: Callable[[model.Model, int], None] _VARIABLE_ADAPTER = ElementAdapter[variables.Variable]( @@ -63,7 +63,9 @@ class ElementAdapter(Generic[T]): ensure_next_id=model.Model.ensure_next_variable_id_at_least, ) -_LINEAR_CONSTRAINT_ADAPTER = ElementAdapter[linear_constraints.LinearConstraint]( +_LINEAR_CONSTRAINT_ADAPTER = ElementAdapter[ + linear_constraints.LinearConstraint +]( add=model.Model.add_linear_constraint, delete=model.Model.delete_linear_constraint, has=model.Model.has_linear_constraint, @@ -76,7 +78,7 @@ class ElementAdapter(Generic[T]): def _aux_add(mod: model.Model) -> objectives.AuxiliaryObjective: - return mod.add_auxiliary_objective(priority=1) + return mod.add_auxiliary_objective(priority=1) _AUX_OBJECTIVE_ADAPTER = ElementAdapter[objectives.AuxiliaryObjective]( @@ -134,137 +136,141 @@ def _aux_add(mod: model.Model) -> objectives.AuxiliaryObjective: ) class ModelElementTest(parameterized.TestCase): - def test_no_elements(self, element_adapter: _ADAPTER) -> None: - mod = model.Model() - self.assertFalse(element_adapter.has(mod, 0)) - self.assertEqual(element_adapter.next_id(mod), 0) - self.assertEqual(element_adapter.num(mod), 0) - self.assertEmpty(list(element_adapter.get_all(mod))) - - def test_add_element(self, element_adapter: _ADAPTER) -> None: - mod = model.Model() - e0 = element_adapter.add(mod) - e1 = element_adapter.add(mod) - e2 = element_adapter.add(mod) - - self.assertTrue(element_adapter.has(mod, 0)) - self.assertTrue(element_adapter.has(mod, 1)) - self.assertTrue(element_adapter.has(mod, 2)) - self.assertFalse(element_adapter.has(mod, 3)) - - self.assertEqual(element_adapter.next_id(mod), 3) - self.assertEqual(element_adapter.num(mod), 3) - self.assertEqual(list(element_adapter.get_all(mod)), [e0, e1, e2]) - - self.assertEqual(element_adapter.get(mod, 1), e1) - - def test_get_invalid_element(self, element_adapter: _ADAPTER) -> None: - mod = model.Model() - with self.assertRaises(KeyError): - element_adapter.get(mod, 0, validate=True) - # Check that default for validate is True as well - with self.assertRaises(KeyError): - element_adapter.get(mod, 0) - - # No crash - bad_el = element_adapter.get(mod, 0, validate=False) - del bad_el - - def test_delete_element(self, element_adapter: _ADAPTER) -> None: - mod = model.Model() - e0 = element_adapter.add(mod) - e1 = element_adapter.add(mod) - e2 = element_adapter.add(mod) - - element_adapter.delete(mod, e1) - - self.assertTrue(element_adapter.has(mod, 0)) - self.assertFalse(element_adapter.has(mod, 1)) - self.assertTrue(element_adapter.has(mod, 2)) - self.assertFalse(element_adapter.has(mod, 3)) - - self.assertEqual(element_adapter.next_id(mod), 3) - self.assertEqual(element_adapter.num(mod), 2) - self.assertEqual(list(element_adapter.get_all(mod)), [e0, e2]) - - self.assertEqual(element_adapter.get(mod, 2), e2) - - def test_delete_invalid_element_error(self, element_adapter: _ADAPTER) -> None: - mod = model.Model() - bad_el = element_adapter.get(mod, 0, validate=False) - with self.assertRaises(ValueError): - element_adapter.delete(mod, bad_el) - - def test_delete_element_twice_error(self, element_adapter: _ADAPTER) -> None: - mod = model.Model() - el = element_adapter.add(mod) - element_adapter.delete(mod, el) - with self.assertRaises(ValueError): - element_adapter.delete(mod, el) - - def test_delete_element_wrong_model_error(self, element_adapter: _ADAPTER) -> None: - mod1 = model.Model() - element_adapter.add(mod1) - - mod2 = model.Model() - e2 = element_adapter.add(mod2) - - with self.assertRaises(ValueError): - element_adapter.delete(mod1, e2) - - def test_get_deleted_element_error(self, element_adapter: _ADAPTER) -> None: - mod = model.Model() - el = element_adapter.add(mod) - element_adapter.delete(mod, el) - with self.assertRaises(KeyError): - element_adapter.get(mod, 0, validate=True) - - # No crash - bad_el = element_adapter.get(mod, 0, validate=False) - del bad_el - - def test_ensure_next_id_with_effect(self, element_adapter: _ADAPTER) -> None: - mod = model.Model() - element_adapter.ensure_next_id(mod, 6) - - self.assertEqual(element_adapter.next_id(mod), 6) - self.assertFalse(element_adapter.has(mod, 0)) - self.assertFalse(element_adapter.has(mod, 6)) - self.assertEqual(element_adapter.num(mod), 0) - self.assertEmpty(list(element_adapter.get_all(mod))) - - e6 = element_adapter.add(mod) - e7 = element_adapter.add(mod) - - self.assertFalse(element_adapter.has(mod, 0)) - self.assertTrue(element_adapter.has(mod, 6)) - self.assertTrue(element_adapter.has(mod, 7)) - self.assertFalse(element_adapter.has(mod, 8)) - - self.assertEqual(element_adapter.next_id(mod), 8) - self.assertEqual(element_adapter.num(mod), 2) - self.assertEqual(list(element_adapter.get_all(mod)), [e6, e7]) - self.assertEqual(element_adapter.get(mod, 6), e6) - self.assertEqual(element_adapter.get(mod, 7), e7) - - def test_ensure_next_id_no_effect(self, element_adapter: _ADAPTER) -> None: - mod = model.Model() - e0 = element_adapter.add(mod) - e1 = element_adapter.add(mod) - e2 = element_adapter.add(mod) - - element_adapter.ensure_next_id(mod, 1) - - self.assertEqual(element_adapter.next_id(mod), 3) - self.assertEqual(element_adapter.num(mod), 3) - self.assertEqual(list(element_adapter.get_all(mod)), [e0, e1, e2]) - - e3 = element_adapter.add(mod) - self.assertEqual(element_adapter.next_id(mod), 4) - self.assertEqual(element_adapter.num(mod), 4) - self.assertEqual(list(element_adapter.get_all(mod)), [e0, e1, e2, e3]) - self.assertEqual(element_adapter.get(mod, 3), e3) + def test_no_elements(self, element_adapter: _ADAPTER) -> None: + mod = model.Model() + self.assertFalse(element_adapter.has(mod, 0)) + self.assertEqual(element_adapter.next_id(mod), 0) + self.assertEqual(element_adapter.num(mod), 0) + self.assertEmpty(list(element_adapter.get_all(mod))) + + def test_add_element(self, element_adapter: _ADAPTER) -> None: + mod = model.Model() + e0 = element_adapter.add(mod) + e1 = element_adapter.add(mod) + e2 = element_adapter.add(mod) + + self.assertTrue(element_adapter.has(mod, 0)) + self.assertTrue(element_adapter.has(mod, 1)) + self.assertTrue(element_adapter.has(mod, 2)) + self.assertFalse(element_adapter.has(mod, 3)) + + self.assertEqual(element_adapter.next_id(mod), 3) + self.assertEqual(element_adapter.num(mod), 3) + self.assertEqual(list(element_adapter.get_all(mod)), [e0, e1, e2]) + + self.assertEqual(element_adapter.get(mod, 1), e1) + + def test_get_invalid_element(self, element_adapter: _ADAPTER) -> None: + mod = model.Model() + with self.assertRaises(KeyError): + element_adapter.get(mod, 0, validate=True) + # Check that default for validate is True as well + with self.assertRaises(KeyError): + element_adapter.get(mod, 0) + + # No crash + bad_el = element_adapter.get(mod, 0, validate=False) + del bad_el + + def test_delete_element(self, element_adapter: _ADAPTER) -> None: + mod = model.Model() + e0 = element_adapter.add(mod) + e1 = element_adapter.add(mod) + e2 = element_adapter.add(mod) + + element_adapter.delete(mod, e1) + + self.assertTrue(element_adapter.has(mod, 0)) + self.assertFalse(element_adapter.has(mod, 1)) + self.assertTrue(element_adapter.has(mod, 2)) + self.assertFalse(element_adapter.has(mod, 3)) + + self.assertEqual(element_adapter.next_id(mod), 3) + self.assertEqual(element_adapter.num(mod), 2) + self.assertEqual(list(element_adapter.get_all(mod)), [e0, e2]) + + self.assertEqual(element_adapter.get(mod, 2), e2) + + def test_delete_invalid_element_error( + self, element_adapter: _ADAPTER + ) -> None: + mod = model.Model() + bad_el = element_adapter.get(mod, 0, validate=False) + with self.assertRaises(ValueError): + element_adapter.delete(mod, bad_el) + + def test_delete_element_twice_error(self, element_adapter: _ADAPTER) -> None: + mod = model.Model() + el = element_adapter.add(mod) + element_adapter.delete(mod, el) + with self.assertRaises(ValueError): + element_adapter.delete(mod, el) + + def test_delete_element_wrong_model_error( + self, element_adapter: _ADAPTER + ) -> None: + mod1 = model.Model() + element_adapter.add(mod1) + + mod2 = model.Model() + e2 = element_adapter.add(mod2) + + with self.assertRaises(ValueError): + element_adapter.delete(mod1, e2) + + def test_get_deleted_element_error(self, element_adapter: _ADAPTER) -> None: + mod = model.Model() + el = element_adapter.add(mod) + element_adapter.delete(mod, el) + with self.assertRaises(KeyError): + element_adapter.get(mod, 0, validate=True) + + # No crash + bad_el = element_adapter.get(mod, 0, validate=False) + del bad_el + + def test_ensure_next_id_with_effect(self, element_adapter: _ADAPTER) -> None: + mod = model.Model() + element_adapter.ensure_next_id(mod, 6) + + self.assertEqual(element_adapter.next_id(mod), 6) + self.assertFalse(element_adapter.has(mod, 0)) + self.assertFalse(element_adapter.has(mod, 6)) + self.assertEqual(element_adapter.num(mod), 0) + self.assertEmpty(list(element_adapter.get_all(mod))) + + e6 = element_adapter.add(mod) + e7 = element_adapter.add(mod) + + self.assertFalse(element_adapter.has(mod, 0)) + self.assertTrue(element_adapter.has(mod, 6)) + self.assertTrue(element_adapter.has(mod, 7)) + self.assertFalse(element_adapter.has(mod, 8)) + + self.assertEqual(element_adapter.next_id(mod), 8) + self.assertEqual(element_adapter.num(mod), 2) + self.assertEqual(list(element_adapter.get_all(mod)), [e6, e7]) + self.assertEqual(element_adapter.get(mod, 6), e6) + self.assertEqual(element_adapter.get(mod, 7), e7) + + def test_ensure_next_id_no_effect(self, element_adapter: _ADAPTER) -> None: + mod = model.Model() + e0 = element_adapter.add(mod) + e1 = element_adapter.add(mod) + e2 = element_adapter.add(mod) + + element_adapter.ensure_next_id(mod, 1) + + self.assertEqual(element_adapter.next_id(mod), 3) + self.assertEqual(element_adapter.num(mod), 3) + self.assertEqual(list(element_adapter.get_all(mod)), [e0, e1, e2]) + + e3 = element_adapter.add(mod) + self.assertEqual(element_adapter.next_id(mod), 4) + self.assertEqual(element_adapter.num(mod), 4) + self.assertEqual(list(element_adapter.get_all(mod)), [e0, e1, e2, e3]) + self.assertEqual(element_adapter.get(mod, 3), e3) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/model_objective_test.py b/ortools/math_opt/python/model_objective_test.py index 946258e57b9..6392d43c516 100644 --- a/ortools/math_opt/python/model_objective_test.py +++ b/ortools/math_opt/python/model_objective_test.py @@ -27,208 +27,210 @@ def _lin_terms(obj: objectives.Objective) -> Dict[variables.Variable, float]: - return {term.variable: term.coefficient for term in obj.linear_terms()} + return {term.variable: term.coefficient for term in obj.linear_terms()} def _quad_terms( obj: objectives.Objective, ) -> Dict[Tuple[variables.Variable, variables.Variable], float]: - return { - (term.key.first_var, term.key.second_var): term.coefficient - for term in obj.quadratic_terms() - } + return { + (term.key.first_var, term.key.second_var): term.coefficient + for term in obj.quadratic_terms() + } class ModelSetObjectiveTest(absltest.TestCase): - def test_maximize(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - mod.objective.set_linear_coefficient(x, 10.0) - mod.objective.set_linear_coefficient(y, 11.0) - - mod.maximize(3 * x * x + 2 * x + 1) - - self.assertTrue(mod.objective.is_maximize) - self.assertEqual(mod.objective.offset, 1.0) - self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) - self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) - - def test_maximize_linear_obj(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - mod.objective.set_linear_coefficient(x, 10.0) - mod.objective.set_linear_coefficient(y, 11.0) - - mod.maximize_linear_objective(2 * x + 1) - - self.assertTrue(mod.objective.is_maximize) - self.assertEqual(mod.objective.offset, 1.0) - self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) - self.assertEmpty(_quad_terms(mod.objective)) - - def test_maximize_linear_obj_type_error_quadratic(self) -> None: - mod = model.Model() - x = mod.add_variable() - - with self.assertRaisesRegex(TypeError, "Quadratic"): - mod.maximize_linear_objective(x * x) # pytype: disable=wrong-arg-types - - def test_maximize_quadratic_objective(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - mod.objective.set_linear_coefficient(x, 10.0) - mod.objective.set_linear_coefficient(y, 11.0) - - mod.maximize_quadratic_objective(3 * x * x + 2 * x + 1) - - self.assertTrue(mod.objective.is_maximize) - self.assertEqual(mod.objective.offset, 1.0) - self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) - self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) - - def test_minimize(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - mod.objective.set_linear_coefficient(x, 10.0) - mod.objective.set_linear_coefficient(y, 11.0) - mod.objective.is_maximize = True - - mod.minimize(3 * x * x + 2 * x + 1) - - self.assertFalse(mod.objective.is_maximize) - self.assertEqual(mod.objective.offset, 1.0) - self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) - self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) - - def test_minimize_linear_obj(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - mod.objective.set_linear_coefficient(x, 10.0) - mod.objective.set_linear_coefficient(y, 11.0) - mod.objective.is_maximize = True - - mod.minimize_linear_objective(2 * x + 1) - - self.assertFalse(mod.objective.is_maximize) - self.assertEqual(mod.objective.offset, 1.0) - self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) - self.assertEmpty(_quad_terms(mod.objective)) - - def test_minimize_linear_obj_type_error_quadratic(self) -> None: - mod = model.Model() - x = mod.add_variable() - - with self.assertRaisesRegex(TypeError, "Quadratic"): - mod.minimize_linear_objective(x * x) # pytype: disable=wrong-arg-types - - def test_minimize_quadratic_objective(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - mod.objective.set_linear_coefficient(x, 10.0) - mod.objective.set_linear_coefficient(y, 11.0) - mod.objective.is_maximize = True - - mod.minimize_quadratic_objective(3 * x * x + 2 * x + 1) - - self.assertFalse(mod.objective.is_maximize) - self.assertEqual(mod.objective.offset, 1.0) - self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) - self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) - - def test_set_objective(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - mod.objective.set_linear_coefficient(x, 10.0) - mod.objective.set_linear_coefficient(y, 11.0) - - mod.set_objective(3 * x * x + 2 * x + 1, is_maximize=True) - - self.assertTrue(mod.objective.is_maximize) - self.assertEqual(mod.objective.offset, 1.0) - self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) - self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) - - def test_set_objective_linear_obj(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - mod.objective.set_linear_coefficient(x, 10.0) - mod.objective.set_linear_coefficient(y, 11.0) - mod.objective.is_maximize = True - - mod.set_linear_objective(2 * x + 1, is_maximize=False) - - self.assertFalse(mod.objective.is_maximize) - self.assertEqual(mod.objective.offset, 1.0) - self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) - self.assertEmpty(_quad_terms(mod.objective)) - - def test_set_objective_linear_obj_type_error_quadratic(self) -> None: - mod = model.Model() - x = mod.add_variable() - - with self.assertRaisesRegex(TypeError, "Quadratic"): - mod.set_linear_objective( - x * x, is_maximize=True - ) # pytype: disable=wrong-arg-types - - def test_set_objective_quadratic_objective(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - mod.objective.set_linear_coefficient(x, 10.0) - mod.objective.set_linear_coefficient(y, 11.0) - - mod.set_quadratic_objective(3 * x * x + 2 * x + 1, is_maximize=True) - - self.assertTrue(mod.objective.is_maximize) - self.assertEqual(mod.objective.offset, 1.0) - self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) - self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) + def test_maximize(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + mod.objective.set_linear_coefficient(x, 10.0) + mod.objective.set_linear_coefficient(y, 11.0) + + mod.maximize(3 * x * x + 2 * x + 1) + + self.assertTrue(mod.objective.is_maximize) + self.assertEqual(mod.objective.offset, 1.0) + self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) + self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) + + def test_maximize_linear_obj(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + mod.objective.set_linear_coefficient(x, 10.0) + mod.objective.set_linear_coefficient(y, 11.0) + + mod.maximize_linear_objective(2 * x + 1) + + self.assertTrue(mod.objective.is_maximize) + self.assertEqual(mod.objective.offset, 1.0) + self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) + self.assertEmpty(_quad_terms(mod.objective)) + + def test_maximize_linear_obj_type_error_quadratic(self) -> None: + mod = model.Model() + x = mod.add_variable() + + with self.assertRaisesRegex(TypeError, "Quadratic"): + mod.maximize_linear_objective(x * x) # pytype: disable=wrong-arg-types + + def test_maximize_quadratic_objective(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + mod.objective.set_linear_coefficient(x, 10.0) + mod.objective.set_linear_coefficient(y, 11.0) + + mod.maximize_quadratic_objective(3 * x * x + 2 * x + 1) + + self.assertTrue(mod.objective.is_maximize) + self.assertEqual(mod.objective.offset, 1.0) + self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) + self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) + + def test_minimize(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + mod.objective.set_linear_coefficient(x, 10.0) + mod.objective.set_linear_coefficient(y, 11.0) + mod.objective.is_maximize = True + + mod.minimize(3 * x * x + 2 * x + 1) + + self.assertFalse(mod.objective.is_maximize) + self.assertEqual(mod.objective.offset, 1.0) + self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) + self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) + + def test_minimize_linear_obj(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + mod.objective.set_linear_coefficient(x, 10.0) + mod.objective.set_linear_coefficient(y, 11.0) + mod.objective.is_maximize = True + + mod.minimize_linear_objective(2 * x + 1) + + self.assertFalse(mod.objective.is_maximize) + self.assertEqual(mod.objective.offset, 1.0) + self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) + self.assertEmpty(_quad_terms(mod.objective)) + + def test_minimize_linear_obj_type_error_quadratic(self) -> None: + mod = model.Model() + x = mod.add_variable() + + with self.assertRaisesRegex(TypeError, "Quadratic"): + mod.minimize_linear_objective(x * x) # pytype: disable=wrong-arg-types + + def test_minimize_quadratic_objective(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + mod.objective.set_linear_coefficient(x, 10.0) + mod.objective.set_linear_coefficient(y, 11.0) + mod.objective.is_maximize = True + + mod.minimize_quadratic_objective(3 * x * x + 2 * x + 1) + + self.assertFalse(mod.objective.is_maximize) + self.assertEqual(mod.objective.offset, 1.0) + self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) + self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) + + def test_set_objective(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + mod.objective.set_linear_coefficient(x, 10.0) + mod.objective.set_linear_coefficient(y, 11.0) + + mod.set_objective(3 * x * x + 2 * x + 1, is_maximize=True) + + self.assertTrue(mod.objective.is_maximize) + self.assertEqual(mod.objective.offset, 1.0) + self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) + self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) + + def test_set_objective_linear_obj(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + mod.objective.set_linear_coefficient(x, 10.0) + mod.objective.set_linear_coefficient(y, 11.0) + mod.objective.is_maximize = True + + mod.set_linear_objective(2 * x + 1, is_maximize=False) + + self.assertFalse(mod.objective.is_maximize) + self.assertEqual(mod.objective.offset, 1.0) + self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) + self.assertEmpty(_quad_terms(mod.objective)) + + def test_set_objective_linear_obj_type_error_quadratic(self) -> None: + mod = model.Model() + x = mod.add_variable() + + with self.assertRaisesRegex(TypeError, "Quadratic"): + mod.set_linear_objective( + x * x, is_maximize=True + ) # pytype: disable=wrong-arg-types + + def test_set_objective_quadratic_objective(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + mod.objective.set_linear_coefficient(x, 10.0) + mod.objective.set_linear_coefficient(y, 11.0) + + mod.set_quadratic_objective(3 * x * x + 2 * x + 1, is_maximize=True) + + self.assertTrue(mod.objective.is_maximize) + self.assertEqual(mod.objective.offset, 1.0) + self.assertDictEqual(_lin_terms(mod.objective), {x: 2.0}) + self.assertDictEqual(_quad_terms(mod.objective), {(x, x): 3.0}) class ModelAuxObjTest(absltest.TestCase): - def test_add_aux_obj_with_expr(self) -> None: - mod = model.Model() - x = mod.add_variable() - aux = mod.add_auxiliary_objective(priority=10, expr=3.0 * x + 4.0, name="aux") - - self.assertEqual(aux.offset, 4.0) - self.assertDictEqual(_lin_terms(aux), {x: 3.0}) - self.assertFalse(aux.is_maximize) - self.assertEqual(aux.priority, 10) - self.assertEqual(aux.name, "aux") - - def test_add_aux_obj_with_maximize(self) -> None: - mod = model.Model() - x = mod.add_variable() - aux = mod.add_maximization_objective(3.0 * x + 4.0, priority=10, name="aux") - - self.assertEqual(aux.offset, 4.0) - self.assertDictEqual(_lin_terms(aux), {x: 3.0}) - self.assertTrue(aux.is_maximize) - self.assertEqual(aux.priority, 10) - self.assertEqual(aux.name, "aux") - - def test_add_aux_obj_with_minimize(self) -> None: - mod = model.Model() - x = mod.add_variable() - aux = mod.add_minimization_objective(3.0 * x + 4.0, priority=10, name="aux") - - self.assertEqual(aux.offset, 4.0) - self.assertDictEqual(_lin_terms(aux), {x: 3.0}) - self.assertFalse(aux.is_maximize) - self.assertEqual(aux.priority, 10) - self.assertEqual(aux.name, "aux") + def test_add_aux_obj_with_expr(self) -> None: + mod = model.Model() + x = mod.add_variable() + aux = mod.add_auxiliary_objective( + priority=10, expr=3.0 * x + 4.0, name="aux" + ) + + self.assertEqual(aux.offset, 4.0) + self.assertDictEqual(_lin_terms(aux), {x: 3.0}) + self.assertFalse(aux.is_maximize) + self.assertEqual(aux.priority, 10) + self.assertEqual(aux.name, "aux") + + def test_add_aux_obj_with_maximize(self) -> None: + mod = model.Model() + x = mod.add_variable() + aux = mod.add_maximization_objective(3.0 * x + 4.0, priority=10, name="aux") + + self.assertEqual(aux.offset, 4.0) + self.assertDictEqual(_lin_terms(aux), {x: 3.0}) + self.assertTrue(aux.is_maximize) + self.assertEqual(aux.priority, 10) + self.assertEqual(aux.name, "aux") + + def test_add_aux_obj_with_minimize(self) -> None: + mod = model.Model() + x = mod.add_variable() + aux = mod.add_minimization_objective(3.0 * x + 4.0, priority=10, name="aux") + + self.assertEqual(aux.offset, 4.0) + self.assertDictEqual(_lin_terms(aux), {x: 3.0}) + self.assertFalse(aux.is_maximize) + self.assertEqual(aux.priority, 10) + self.assertEqual(aux.name, "aux") _PROTO_VARS = model_pb2.VariablesProto( @@ -243,94 +245,94 @@ def test_add_aux_obj_with_minimize(self) -> None: class ModelObjectiveExportProtoIntegrationTest( compare_proto.MathOptProtoAssertions, absltest.TestCase ): - """Test Model.export_model() and UpdateTracker.export_update() for objectives. - - These tests are not comprehensive, the proto generation code is completely - tested in the tests for Elemental. We just want to make sure everything is - connected. - """ - - def test_export_model_with_objective(self) -> None: - mod = model.Model(primary_objective_name="obj-A") - x = mod.add_variable(lb=-2.0, ub=2.0) - mod.objective.priority = 3 - mod.maximize(3 * x * x + 4 * x + 5) - proto_obj = model_pb2.ObjectiveProto( - maximize=True, - name="obj-A", - priority=3, - offset=5.0, + """Test Model.export_model() and UpdateTracker.export_update() for objectives. + + These tests are not comprehensive, the proto generation code is completely + tested in the tests for Elemental. We just want to make sure everything is + connected. + """ + + def test_export_model_with_objective(self) -> None: + mod = model.Model(primary_objective_name="obj-A") + x = mod.add_variable(lb=-2.0, ub=2.0) + mod.objective.priority = 3 + mod.maximize(3 * x * x + 4 * x + 5) + proto_obj = model_pb2.ObjectiveProto( + maximize=True, + name="obj-A", + priority=3, + offset=5.0, + linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[4.0] + ), + quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[0], column_ids=[0], coefficients=[3.0] + ), + ) + expected = model_pb2.ModelProto(variables=_PROTO_VARS, objective=proto_obj) + + self.assert_protos_equal(mod.export_model(), expected) + + def test_export_model_update_with_objective(self) -> None: + mod = model.Model(primary_objective_name="obj-A") + x = mod.add_variable(lb=-2.0, ub=2.0) + tracker = mod.add_update_tracker() + mod.objective.set_linear_coefficient(x, 2.5) + + expected = model_update_pb2.ModelUpdateProto( + objective_updates=model_update_pb2.ObjectiveUpdatesProto( linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[4.0] - ), - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0], column_ids=[0], coefficients=[3.0] - ), - ) - expected = model_pb2.ModelProto(variables=_PROTO_VARS, objective=proto_obj) - - self.assert_protos_equal(mod.export_model(), expected) - - def test_export_model_update_with_objective(self) -> None: - mod = model.Model(primary_objective_name="obj-A") - x = mod.add_variable(lb=-2.0, ub=2.0) - tracker = mod.add_update_tracker() - mod.objective.set_linear_coefficient(x, 2.5) - - expected = model_update_pb2.ModelUpdateProto( - objective_updates=model_update_pb2.ObjectiveUpdatesProto( - linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[2.5] - ) + ids=[0], values=[2.5] ) ) - - self.assert_protos_equal(tracker.export_update(), expected) - - def test_export_model_with_auxiliary_objective(self) -> None: - mod = model.Model() - x = mod.add_variable(lb=-2.0, ub=2.0) - aux = mod.add_auxiliary_objective(priority=3, name="obj-A") - aux.set_to_expression(4 * x + 5) - aux.is_maximize = True - - proto_obj = model_pb2.ObjectiveProto( - maximize=True, - name="obj-A", - priority=3, - offset=5.0, - linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[4.0] - ), - ) - - expected = model_pb2.ModelProto( - variables=_PROTO_VARS, auxiliary_objectives={0: proto_obj} - ) - - self.assert_protos_equal(mod.export_model(), expected) - - def test_export_model_update_with_aux_obj_update(self) -> None: - mod = model.Model(primary_objective_name="obj-A") - x = mod.add_variable(lb=-2.0, ub=2.0) - aux = mod.add_auxiliary_objective(priority=20) - tracker = mod.add_update_tracker() - aux.set_linear_coefficient(x, 2.5) - - expected = model_update_pb2.ModelUpdateProto( - auxiliary_objectives_updates=model_update_pb2.AuxiliaryObjectivesUpdatesProto( - objective_updates={ - 0: model_update_pb2.ObjectiveUpdatesProto( - linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[2.5] - ) + ) + + self.assert_protos_equal(tracker.export_update(), expected) + + def test_export_model_with_auxiliary_objective(self) -> None: + mod = model.Model() + x = mod.add_variable(lb=-2.0, ub=2.0) + aux = mod.add_auxiliary_objective(priority=3, name="obj-A") + aux.set_to_expression(4 * x + 5) + aux.is_maximize = True + + proto_obj = model_pb2.ObjectiveProto( + maximize=True, + name="obj-A", + priority=3, + offset=5.0, + linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[4.0] + ), + ) + + expected = model_pb2.ModelProto( + variables=_PROTO_VARS, auxiliary_objectives={0: proto_obj} + ) + + self.assert_protos_equal(mod.export_model(), expected) + + def test_export_model_update_with_aux_obj_update(self) -> None: + mod = model.Model(primary_objective_name="obj-A") + x = mod.add_variable(lb=-2.0, ub=2.0) + aux = mod.add_auxiliary_objective(priority=20) + tracker = mod.add_update_tracker() + aux.set_linear_coefficient(x, 2.5) + + expected = model_update_pb2.ModelUpdateProto( + auxiliary_objectives_updates=model_update_pb2.AuxiliaryObjectivesUpdatesProto( + objective_updates={ + 0: model_update_pb2.ObjectiveUpdatesProto( + linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[2.5] ) - } - ) + ) + } ) + ) - self.assert_protos_equal(tracker.export_update(), expected) + self.assert_protos_equal(tracker.export_update(), expected) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/model_parameters.py b/ortools/math_opt/python/model_parameters.py index 38a53895f20..06565e77ac3 100644 --- a/ortools/math_opt/python/model_parameters.py +++ b/ortools/math_opt/python/model_parameters.py @@ -27,238 +27,238 @@ @dataclasses.dataclass class SolutionHint: - """A suggested starting solution for the solver. + """A suggested starting solution for the solver. - MIP solvers generally only want primal information (`variable_values`), - while LP solvers want both primal and dual information (`dual_values`). + MIP solvers generally only want primal information (`variable_values`), + while LP solvers want both primal and dual information (`dual_values`). - Many MIP solvers can work with: (1) partial solutions that do not specify all - variables or (2) infeasible solutions. In these cases, solvers typically solve - a sub-MIP to complete/correct the hint. + Many MIP solvers can work with: (1) partial solutions that do not specify all + variables or (2) infeasible solutions. In these cases, solvers typically solve + a sub-MIP to complete/correct the hint. - How the hint is used by the solver, if at all, is highly dependent on the - solver, the problem type, and the algorithm used. The most reliable way to - ensure your hint has an effect is to read the underlying solvers logs with - and without the hint. + How the hint is used by the solver, if at all, is highly dependent on the + solver, the problem type, and the algorithm used. The most reliable way to + ensure your hint has an effect is to read the underlying solvers logs with + and without the hint. - Simplex-based LP solvers typically prefer an initial basis to a solution - hint (they need to crossover to convert the hint to a basic feasible - solution otherwise). + Simplex-based LP solvers typically prefer an initial basis to a solution + hint (they need to crossover to convert the hint to a basic feasible + solution otherwise). - Floating point values should be finite and not NaN, they are validated by - MathOpt at Solve() time (resulting in an exception). + Floating point values should be finite and not NaN, they are validated by + MathOpt at Solve() time (resulting in an exception). - Attributes: - variable_values: a potentially partial assignment from the model's primal - variables to finite (and not NaN) double values. - dual_values: a potentially partial assignment from the model's linear - constraints to finite (and not NaN) double values. - """ + Attributes: + variable_values: a potentially partial assignment from the model's primal + variables to finite (and not NaN) double values. + dual_values: a potentially partial assignment from the model's linear + constraints to finite (and not NaN) double values. + """ - variable_values: Dict[variables.Variable, float] = dataclasses.field( - default_factory=dict - ) - dual_values: Dict[linear_constraints.LinearConstraint, float] = dataclasses.field( - default_factory=dict - ) + variable_values: Dict[variables.Variable, float] = dataclasses.field( + default_factory=dict + ) + dual_values: Dict[linear_constraints.LinearConstraint, float] = ( + dataclasses.field(default_factory=dict) + ) - def to_proto(self) -> model_parameters_pb2.SolutionHintProto: - """Returns an equivalent protocol buffer to this.""" - return model_parameters_pb2.SolutionHintProto( - variable_values=sparse_containers.to_sparse_double_vector_proto( - self.variable_values - ), - dual_values=sparse_containers.to_sparse_double_vector_proto( - self.dual_values - ), - ) + def to_proto(self) -> model_parameters_pb2.SolutionHintProto: + """Returns an equivalent protocol buffer to this.""" + return model_parameters_pb2.SolutionHintProto( + variable_values=sparse_containers.to_sparse_double_vector_proto( + self.variable_values + ), + dual_values=sparse_containers.to_sparse_double_vector_proto( + self.dual_values + ), + ) def parse_solution_hint( hint_proto: model_parameters_pb2.SolutionHintProto, mod: model.Model ) -> SolutionHint: - """Returns an equivalent SolutionHint to `hint_proto`. + """Returns an equivalent SolutionHint to `hint_proto`. - Args: - hint_proto: The solution, as encoded by the ids of the variables and - constraints. - mod: A MathOpt Model that must contain variables and linear constraints with - the ids from hint_proto. + Args: + hint_proto: The solution, as encoded by the ids of the variables and + constraints. + mod: A MathOpt Model that must contain variables and linear constraints with + the ids from hint_proto. - Returns: - A SolutionHint equivalent. + Returns: + A SolutionHint equivalent. - Raises: - ValueError if hint_proto is invalid or refers to variables or constraints - not in mod. - """ - return SolutionHint( - variable_values=sparse_containers.parse_variable_map( - hint_proto.variable_values, mod - ), - dual_values=sparse_containers.parse_linear_constraint_map( - hint_proto.dual_values, mod - ), - ) + Raises: + ValueError if hint_proto is invalid or refers to variables or constraints + not in mod. + """ + return SolutionHint( + variable_values=sparse_containers.parse_variable_map( + hint_proto.variable_values, mod + ), + dual_values=sparse_containers.parse_linear_constraint_map( + hint_proto.dual_values, mod + ), + ) @dataclasses.dataclass class ObjectiveParameters: - """Parameters for an individual objective in a multi-objective model. + """Parameters for an individual objective in a multi-objective model. - This class mirrors (and can generate) the related proto - model_parameters_pb2.ObjectiveParametersProto. + This class mirrors (and can generate) the related proto + model_parameters_pb2.ObjectiveParametersProto. - Attributes: - objective_degradation_absolute_tolerance: Optional objective degradation - absolute tolerance. For a hierarchical multi-objective solver, each - objective fⁱ is processed in priority order: the solver determines the - optimal objective value Γⁱ, if it exists, subject to all constraints in - the model and the additional constraints that fᵏ(x) = Γᵏ (within - tolerances) for each k < i. If set, a solution is considered to be "within - tolerances" for this objective fᵏ if |fᵏ(x) - Γᵏ| ≤ - `objective_degradation_absolute_tolerance`. See also - `objective_degradation_relative_tolerance`; if both parameters are set for - a given objective, the solver need only satisfy one to be considered - "within tolerances". If not None, must be nonnegative. - objective_degradation_relative_tolerance: Optional objective degradation - relative tolerance. For a hierarchical multi-objective solver, each - objective fⁱ is processed in priority order: the solver determines the - optimal objective value Γⁱ, if it exists, subject to all constraints in - the model and the additional constraints that fᵏ(x) = Γᵏ (within - tolerances) for each k < i. If set, a solution is considered to be "within - tolerances" for this objective fᵏ if |fᵏ(x) - Γᵏ| ≤ - `objective_degradation_relative_tolerance` * |Γᵏ|. See also - `objective_degradation_absolute_tolerance`; if both parameters are set for - a given objective, the solver need only satisfy one to be considered - "within tolerances". If not None, must be nonnegative. - time_limit: The maximum time a solver should spend on optimizing this - particular objective (or infinite if not set). Note that this does not - supersede the global time limit in SolveParameters.time_limit; both will - be enforced when set. This value is not a hard limit, solve time may - slightly exceed this value. - """ + Attributes: + objective_degradation_absolute_tolerance: Optional objective degradation + absolute tolerance. For a hierarchical multi-objective solver, each + objective fⁱ is processed in priority order: the solver determines the + optimal objective value Γⁱ, if it exists, subject to all constraints in + the model and the additional constraints that fᵏ(x) = Γᵏ (within + tolerances) for each k < i. If set, a solution is considered to be "within + tolerances" for this objective fᵏ if |fᵏ(x) - Γᵏ| ≤ + `objective_degradation_absolute_tolerance`. See also + `objective_degradation_relative_tolerance`; if both parameters are set for + a given objective, the solver need only satisfy one to be considered + "within tolerances". If not None, must be nonnegative. + objective_degradation_relative_tolerance: Optional objective degradation + relative tolerance. For a hierarchical multi-objective solver, each + objective fⁱ is processed in priority order: the solver determines the + optimal objective value Γⁱ, if it exists, subject to all constraints in + the model and the additional constraints that fᵏ(x) = Γᵏ (within + tolerances) for each k < i. If set, a solution is considered to be "within + tolerances" for this objective fᵏ if |fᵏ(x) - Γᵏ| ≤ + `objective_degradation_relative_tolerance` * |Γᵏ|. See also + `objective_degradation_absolute_tolerance`; if both parameters are set for + a given objective, the solver need only satisfy one to be considered + "within tolerances". If not None, must be nonnegative. + time_limit: The maximum time a solver should spend on optimizing this + particular objective (or infinite if not set). Note that this does not + supersede the global time limit in SolveParameters.time_limit; both will + be enforced when set. This value is not a hard limit, solve time may + slightly exceed this value. + """ - objective_degradation_absolute_tolerance: Optional[float] = None - objective_degradation_relative_tolerance: Optional[float] = None - time_limit: Optional[datetime.timedelta] = None + objective_degradation_absolute_tolerance: Optional[float] = None + objective_degradation_relative_tolerance: Optional[float] = None + time_limit: Optional[datetime.timedelta] = None - def to_proto(self) -> model_parameters_pb2.ObjectiveParametersProto: - """Returns an equivalent protocol buffer.""" - result = model_parameters_pb2.ObjectiveParametersProto() - if self.objective_degradation_absolute_tolerance is not None: - result.objective_degradation_absolute_tolerance = ( - self.objective_degradation_absolute_tolerance - ) - if self.objective_degradation_relative_tolerance is not None: - result.objective_degradation_relative_tolerance = ( - self.objective_degradation_relative_tolerance - ) - if self.time_limit is not None: - result.time_limit.FromTimedelta(self.time_limit) - return result + def to_proto(self) -> model_parameters_pb2.ObjectiveParametersProto: + """Returns an equivalent protocol buffer.""" + result = model_parameters_pb2.ObjectiveParametersProto() + if self.objective_degradation_absolute_tolerance is not None: + result.objective_degradation_absolute_tolerance = ( + self.objective_degradation_absolute_tolerance + ) + if self.objective_degradation_relative_tolerance is not None: + result.objective_degradation_relative_tolerance = ( + self.objective_degradation_relative_tolerance + ) + if self.time_limit is not None: + result.time_limit.FromTimedelta(self.time_limit) + return result def parse_objective_parameters( proto: model_parameters_pb2.ObjectiveParametersProto, ) -> ObjectiveParameters: - """Returns an equivalent ObjectiveParameters to the input proto.""" - result = ObjectiveParameters() - if proto.HasField("objective_degradation_absolute_tolerance"): - result.objective_degradation_absolute_tolerance = ( - proto.objective_degradation_absolute_tolerance - ) - if proto.HasField("objective_degradation_relative_tolerance"): - result.objective_degradation_relative_tolerance = ( - proto.objective_degradation_relative_tolerance - ) - if proto.HasField("time_limit"): - result.time_limit = proto.time_limit.ToTimedelta() - return result + """Returns an equivalent ObjectiveParameters to the input proto.""" + result = ObjectiveParameters() + if proto.HasField("objective_degradation_absolute_tolerance"): + result.objective_degradation_absolute_tolerance = ( + proto.objective_degradation_absolute_tolerance + ) + if proto.HasField("objective_degradation_relative_tolerance"): + result.objective_degradation_relative_tolerance = ( + proto.objective_degradation_relative_tolerance + ) + if proto.HasField("time_limit"): + result.time_limit = proto.time_limit.ToTimedelta() + return result @dataclasses.dataclass class ModelSolveParameters: - """Model specific solver configuration, for example, an initial basis. + """Model specific solver configuration, for example, an initial basis. - This class mirrors (and can generate) the related proto - model_parameters_pb2.ModelSolveParametersProto. + This class mirrors (and can generate) the related proto + model_parameters_pb2.ModelSolveParametersProto. - Attributes: - variable_values_filter: Only return solution and primal ray values for - variables accepted by this filter (default accepts all variables). - dual_values_filter: Only return dual variable values and dual ray values for - linear constraints accepted by this filter (default accepts all linear - constraints). - quadratic_dual_values_filter: Only return quadratic constraint dual values - accepted by this filter (default accepts all quadratic constraints). - reduced_costs_filter: Only return reduced cost and dual ray values for - variables accepted by this filter (default accepts all variables). - initial_basis: If set, provides a warm start for simplex based solvers. - solution_hints: Optional solution hints. If the underlying solver only - accepts a single hint, the first hint is used. - branching_priorities: Optional branching priorities. Variables with higher - values will be branched on first. Variables for which priorities are not - set get the solver's default priority (usually zero). - objective_parameters: Optional per objective parameters used only only for - multi-objective models. - lazy_linear_constraints: Optional lazy constraint annotations. Included - linear constraints will be marked as "lazy" with supporting solvers, - meaning that they will only be added to the working model as-needed as the - solver runs. Note that this an algorithmic hint that does not affect the - model's feasible region; solvers not supporting these annotations will - simply ignore it. - """ + Attributes: + variable_values_filter: Only return solution and primal ray values for + variables accepted by this filter (default accepts all variables). + dual_values_filter: Only return dual variable values and dual ray values for + linear constraints accepted by this filter (default accepts all linear + constraints). + quadratic_dual_values_filter: Only return quadratic constraint dual values + accepted by this filter (default accepts all quadratic constraints). + reduced_costs_filter: Only return reduced cost and dual ray values for + variables accepted by this filter (default accepts all variables). + initial_basis: If set, provides a warm start for simplex based solvers. + solution_hints: Optional solution hints. If the underlying solver only + accepts a single hint, the first hint is used. + branching_priorities: Optional branching priorities. Variables with higher + values will be branched on first. Variables for which priorities are not + set get the solver's default priority (usually zero). + objective_parameters: Optional per objective parameters used only only for + multi-objective models. + lazy_linear_constraints: Optional lazy constraint annotations. Included + linear constraints will be marked as "lazy" with supporting solvers, + meaning that they will only be added to the working model as-needed as the + solver runs. Note that this an algorithmic hint that does not affect the + model's feasible region; solvers not supporting these annotations will + simply ignore it. + """ - variable_values_filter: sparse_containers.VariableFilter = ( - sparse_containers.VariableFilter() - ) - dual_values_filter: sparse_containers.LinearConstraintFilter = ( - sparse_containers.LinearConstraintFilter() - ) - quadratic_dual_values_filter: sparse_containers.QuadraticConstraintFilter = ( - sparse_containers.QuadraticConstraintFilter() - ) - reduced_costs_filter: sparse_containers.VariableFilter = ( - sparse_containers.VariableFilter() - ) - initial_basis: Optional[solution.Basis] = None - solution_hints: List[SolutionHint] = dataclasses.field(default_factory=list) - branching_priorities: Dict[variables.Variable, int] = dataclasses.field( - default_factory=dict - ) - objective_parameters: Dict[objectives.Objective, ObjectiveParameters] = ( - dataclasses.field(default_factory=dict) - ) - lazy_linear_constraints: Set[linear_constraints.LinearConstraint] = ( - dataclasses.field(default_factory=set) - ) + variable_values_filter: sparse_containers.VariableFilter = ( + sparse_containers.VariableFilter() + ) + dual_values_filter: sparse_containers.LinearConstraintFilter = ( + sparse_containers.LinearConstraintFilter() + ) + quadratic_dual_values_filter: sparse_containers.QuadraticConstraintFilter = ( + sparse_containers.QuadraticConstraintFilter() + ) + reduced_costs_filter: sparse_containers.VariableFilter = ( + sparse_containers.VariableFilter() + ) + initial_basis: Optional[solution.Basis] = None + solution_hints: List[SolutionHint] = dataclasses.field(default_factory=list) + branching_priorities: Dict[variables.Variable, int] = dataclasses.field( + default_factory=dict + ) + objective_parameters: Dict[objectives.Objective, ObjectiveParameters] = ( + dataclasses.field(default_factory=dict) + ) + lazy_linear_constraints: Set[linear_constraints.LinearConstraint] = ( + dataclasses.field(default_factory=set) + ) - def to_proto(self) -> model_parameters_pb2.ModelSolveParametersProto: - """Returns an equivalent protocol buffer.""" - # TODO(b/236289022): these methods should check that the variables are from - # the correct model. - result = model_parameters_pb2.ModelSolveParametersProto( - variable_values_filter=self.variable_values_filter.to_proto(), - dual_values_filter=self.dual_values_filter.to_proto(), - quadratic_dual_values_filter=self.quadratic_dual_values_filter.to_proto(), - reduced_costs_filter=self.reduced_costs_filter.to_proto(), - branching_priorities=sparse_containers.to_sparse_int32_vector_proto( - self.branching_priorities - ), - ) - if self.initial_basis: - result.initial_basis.CopyFrom(self.initial_basis.to_proto()) - for hint in self.solution_hints: - result.solution_hints.append(hint.to_proto()) - for obj, params in self.objective_parameters.items(): - if isinstance(obj, objectives.AuxiliaryObjective): - result.auxiliary_objective_parameters[obj.id].CopyFrom( - params.to_proto() - ) - else: - result.primary_objective_parameters.CopyFrom(params.to_proto()) - result.lazy_linear_constraint_ids[:] = sorted( - con.id for con in self.lazy_linear_constraints + def to_proto(self) -> model_parameters_pb2.ModelSolveParametersProto: + """Returns an equivalent protocol buffer.""" + # TODO(b/236289022): these methods should check that the variables are from + # the correct model. + result = model_parameters_pb2.ModelSolveParametersProto( + variable_values_filter=self.variable_values_filter.to_proto(), + dual_values_filter=self.dual_values_filter.to_proto(), + quadratic_dual_values_filter=self.quadratic_dual_values_filter.to_proto(), + reduced_costs_filter=self.reduced_costs_filter.to_proto(), + branching_priorities=sparse_containers.to_sparse_int32_vector_proto( + self.branching_priorities + ), + ) + if self.initial_basis: + result.initial_basis.CopyFrom(self.initial_basis.to_proto()) + for hint in self.solution_hints: + result.solution_hints.append(hint.to_proto()) + for obj, params in self.objective_parameters.items(): + if isinstance(obj, objectives.AuxiliaryObjective): + result.auxiliary_objective_parameters[obj.id].CopyFrom( + params.to_proto() ) - return result + else: + result.primary_objective_parameters.CopyFrom(params.to_proto()) + result.lazy_linear_constraint_ids[:] = sorted( + con.id for con in self.lazy_linear_constraints + ) + return result diff --git a/ortools/math_opt/python/model_parameters_test.py b/ortools/math_opt/python/model_parameters_test.py index 7a87834e270..df3737622f8 100644 --- a/ortools/math_opt/python/model_parameters_test.py +++ b/ortools/math_opt/python/model_parameters_test.py @@ -26,161 +26,165 @@ from ortools.math_opt.python.testing import compare_proto -class ModelParametersTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - - def test_solution_hint_round_trip(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - - hint = model_parameters.SolutionHint( - variable_values={x: 2.0, y: 3.0}, dual_values={c: 4.0, d: 5.0} - ) - hint_round_trip = model_parameters.parse_solution_hint(hint.to_proto(), mod) - self.assertDictEqual(hint_round_trip.variable_values, hint.variable_values) - self.assertDictEqual(hint_round_trip.dual_values, hint.dual_values) - - def test_objective_parameters_empty_round_trip(self) -> None: - params = model_parameters.ObjectiveParameters() - proto = model_parameters_pb2.ObjectiveParametersProto() - self.assert_protos_equiv(params.to_proto(), proto) - self.assertEqual(model_parameters.parse_objective_parameters(proto), params) - - def test_objective_parameters_full_round_trip(self) -> None: - params = model_parameters.ObjectiveParameters( - objective_degradation_absolute_tolerance=4.1, - objective_degradation_relative_tolerance=4.2, - time_limit=datetime.timedelta(minutes=1), - ) - proto = model_parameters_pb2.ObjectiveParametersProto( - objective_degradation_absolute_tolerance=4.1, - objective_degradation_relative_tolerance=4.2, - time_limit=duration_pb2.Duration(seconds=60), +class ModelParametersTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_solution_hint_round_trip(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + + hint = model_parameters.SolutionHint( + variable_values={x: 2.0, y: 3.0}, dual_values={c: 4.0, d: 5.0} + ) + hint_round_trip = model_parameters.parse_solution_hint(hint.to_proto(), mod) + self.assertDictEqual(hint_round_trip.variable_values, hint.variable_values) + self.assertDictEqual(hint_round_trip.dual_values, hint.dual_values) + + def test_objective_parameters_empty_round_trip(self) -> None: + params = model_parameters.ObjectiveParameters() + proto = model_parameters_pb2.ObjectiveParametersProto() + self.assert_protos_equiv(params.to_proto(), proto) + self.assertEqual(model_parameters.parse_objective_parameters(proto), params) + + def test_objective_parameters_full_round_trip(self) -> None: + params = model_parameters.ObjectiveParameters( + objective_degradation_absolute_tolerance=4.1, + objective_degradation_relative_tolerance=4.2, + time_limit=datetime.timedelta(minutes=1), + ) + proto = model_parameters_pb2.ObjectiveParametersProto( + objective_degradation_absolute_tolerance=4.1, + objective_degradation_relative_tolerance=4.2, + time_limit=duration_pb2.Duration(seconds=60), + ) + self.assert_protos_equiv(params.to_proto(), proto) + self.assertEqual(model_parameters.parse_objective_parameters(proto), params) + + def test_model_parameters_to_proto_no_basis(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + # Ensure q and c have different ids. + mod.add_quadratic_constraint() + q = mod.add_quadratic_constraint(name="q") + params = model_parameters.ModelSolveParameters() + params.variable_values_filter = sparse_containers.SparseVectorFilter( + filtered_items=(y,) + ) + params.reduced_costs_filter = sparse_containers.SparseVectorFilter( + skip_zero_values=True + ) + params.dual_values_filter = sparse_containers.SparseVectorFilter( + filtered_items=(c,) + ) + params.quadratic_dual_values_filter = sparse_containers.SparseVectorFilter( + filtered_items=(q,) + ) + params.solution_hints.append( + model_parameters.SolutionHint( + variable_values={x: 1.0, y: 1.0}, dual_values={c: 3.0} ) - self.assert_protos_equiv(params.to_proto(), proto) - self.assertEqual(model_parameters.parse_objective_parameters(proto), params) - - def test_model_parameters_to_proto_no_basis(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - # Ensure q and c have different ids. - mod.add_quadratic_constraint() - q = mod.add_quadratic_constraint(name="q") - params = model_parameters.ModelSolveParameters() - params.variable_values_filter = sparse_containers.SparseVectorFilter( - filtered_items=(y,) - ) - params.reduced_costs_filter = sparse_containers.SparseVectorFilter( + ) + params.solution_hints.append( + model_parameters.SolutionHint(variable_values={y: 0.0}) + ) + params.branching_priorities[y] = 2 + actual = params.to_proto() + expected = model_parameters_pb2.ModelSolveParametersProto( + variable_values_filter=sparse_containers_pb2.SparseVectorFilterProto( + filter_by_ids=True, filtered_ids=(1,) + ), + reduced_costs_filter=sparse_containers_pb2.SparseVectorFilterProto( skip_zero_values=True - ) - params.dual_values_filter = sparse_containers.SparseVectorFilter( - filtered_items=(c,) - ) - params.quadratic_dual_values_filter = sparse_containers.SparseVectorFilter( - filtered_items=(q,) - ) - params.solution_hints.append( - model_parameters.SolutionHint( - variable_values={x: 1.0, y: 1.0}, dual_values={c: 3.0} - ) - ) - params.solution_hints.append( - model_parameters.SolutionHint(variable_values={y: 0.0}) - ) - params.branching_priorities[y] = 2 - actual = params.to_proto() - expected = model_parameters_pb2.ModelSolveParametersProto( - variable_values_filter=sparse_containers_pb2.SparseVectorFilterProto( - filter_by_ids=True, filtered_ids=(1,) - ), - reduced_costs_filter=sparse_containers_pb2.SparseVectorFilterProto( - skip_zero_values=True - ), - dual_values_filter=sparse_containers_pb2.SparseVectorFilterProto( - filter_by_ids=True, filtered_ids=(0,) - ), - quadratic_dual_values_filter=sparse_containers_pb2.SparseVectorFilterProto( - filter_by_ids=True, filtered_ids=(1,) - ), - branching_priorities=sparse_containers_pb2.SparseInt32VectorProto( - ids=[1], values=[2] - ), - ) - h1 = expected.solution_hints.add() - h1.variable_values.ids[:] = [0, 1] - h1.variable_values.values[:] = [1.0, 1.0] - h1.dual_values.ids[:] = [0] - h1.dual_values.values[:] = [3] - h2 = expected.solution_hints.add() - h2.variable_values.ids.append(1) - h2.variable_values.values.append(0.0) - self.assert_protos_equiv(actual, expected) - - def test_model_parameters_to_proto_with_basis(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - params = model_parameters.ModelSolveParameters() - params.initial_basis = solution.Basis() - params.initial_basis.variable_status[x] = solution.BasisStatus.AT_UPPER_BOUND - actual = params.to_proto() - expected = model_parameters_pb2.ModelSolveParametersProto() - expected.initial_basis.variable_status.ids.append(0) - expected.initial_basis.variable_status.values.append( - solution_pb2.BASIS_STATUS_AT_UPPER_BOUND - ) - self.assert_protos_equiv(expected, actual) - - def test_model_parameters_to_proto_with_objective_params(self) -> None: - mod = model.Model() - aux1 = mod.add_auxiliary_objective(priority=1) - mod.add_auxiliary_objective(priority=2) - aux3 = mod.add_auxiliary_objective(priority=3) - - def make_param(abs_tol: float) -> model_parameters.ObjectiveParameters: - return model_parameters.ObjectiveParameters( - objective_degradation_absolute_tolerance=abs_tol - ) - - def make_proto_param( - abs_tol: float, - ) -> model_parameters_pb2.ObjectiveParametersProto: - return model_parameters_pb2.ObjectiveParametersProto( - objective_degradation_absolute_tolerance=abs_tol - ) - - model_params = model_parameters.ModelSolveParameters( - objective_parameters={ - mod.objective: make_param(0.1), - aux1: make_param(0.2), - aux3: make_param(0.3), - } - ) - expected = model_parameters_pb2.ModelSolveParametersProto( - primary_objective_parameters=make_proto_param(0.1), - auxiliary_objective_parameters={ - 0: make_proto_param(0.2), - 2: make_proto_param(0.3), - }, - ) - self.assert_protos_equiv(model_params.to_proto(), expected) - - def test_model_parameters_to_proto_with_lazy_constraints(self) -> None: - mod = model.Model() - c0 = mod.add_linear_constraint() - mod.add_linear_constraint() - c2 = mod.add_linear_constraint() - model_params = model_parameters.ModelSolveParameters( - lazy_linear_constraints={c0, c2} - ) - expected = model_parameters_pb2.ModelSolveParametersProto( - lazy_linear_constraint_ids=[0, 2] - ) - self.assert_protos_equiv(model_params.to_proto(), expected) + ), + dual_values_filter=sparse_containers_pb2.SparseVectorFilterProto( + filter_by_ids=True, filtered_ids=(0,) + ), + quadratic_dual_values_filter=sparse_containers_pb2.SparseVectorFilterProto( + filter_by_ids=True, filtered_ids=(1,) + ), + branching_priorities=sparse_containers_pb2.SparseInt32VectorProto( + ids=[1], values=[2] + ), + ) + h1 = expected.solution_hints.add() + h1.variable_values.ids[:] = [0, 1] + h1.variable_values.values[:] = [1.0, 1.0] + h1.dual_values.ids[:] = [0] + h1.dual_values.values[:] = [3] + h2 = expected.solution_hints.add() + h2.variable_values.ids.append(1) + h2.variable_values.values.append(0.0) + self.assert_protos_equiv(actual, expected) + + def test_model_parameters_to_proto_with_basis(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + params = model_parameters.ModelSolveParameters() + params.initial_basis = solution.Basis() + params.initial_basis.variable_status[x] = ( + solution.BasisStatus.AT_UPPER_BOUND + ) + actual = params.to_proto() + expected = model_parameters_pb2.ModelSolveParametersProto() + expected.initial_basis.variable_status.ids.append(0) + expected.initial_basis.variable_status.values.append( + solution_pb2.BASIS_STATUS_AT_UPPER_BOUND + ) + self.assert_protos_equiv(expected, actual) + + def test_model_parameters_to_proto_with_objective_params(self) -> None: + mod = model.Model() + aux1 = mod.add_auxiliary_objective(priority=1) + mod.add_auxiliary_objective(priority=2) + aux3 = mod.add_auxiliary_objective(priority=3) + + def make_param(abs_tol: float) -> model_parameters.ObjectiveParameters: + return model_parameters.ObjectiveParameters( + objective_degradation_absolute_tolerance=abs_tol + ) + + def make_proto_param( + abs_tol: float, + ) -> model_parameters_pb2.ObjectiveParametersProto: + return model_parameters_pb2.ObjectiveParametersProto( + objective_degradation_absolute_tolerance=abs_tol + ) + + model_params = model_parameters.ModelSolveParameters( + objective_parameters={ + mod.objective: make_param(0.1), + aux1: make_param(0.2), + aux3: make_param(0.3), + } + ) + expected = model_parameters_pb2.ModelSolveParametersProto( + primary_objective_parameters=make_proto_param(0.1), + auxiliary_objective_parameters={ + 0: make_proto_param(0.2), + 2: make_proto_param(0.3), + }, + ) + self.assert_protos_equiv(model_params.to_proto(), expected) + + def test_model_parameters_to_proto_with_lazy_constraints(self) -> None: + mod = model.Model() + c0 = mod.add_linear_constraint() + mod.add_linear_constraint() + c2 = mod.add_linear_constraint() + model_params = model_parameters.ModelSolveParameters( + lazy_linear_constraints={c0, c2} + ) + expected = model_parameters_pb2.ModelSolveParametersProto( + lazy_linear_constraint_ids=[0, 2] + ) + self.assert_protos_equiv(model_params.to_proto(), expected) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/model_quadratic_constraint_test.py b/ortools/math_opt/python/model_quadratic_constraint_test.py index a15bc8c5873..faf2e8fc71a 100644 --- a/ortools/math_opt/python/model_quadratic_constraint_test.py +++ b/ortools/math_opt/python/model_quadratic_constraint_test.py @@ -30,115 +30,117 @@ def _lin_terms_dict( quad_con: quadratic_constraints.QuadraticConstraint, ) -> Dict[variables.Variable, float]: - return {term.variable: term.coefficient for term in quad_con.linear_terms()} + return {term.variable: term.coefficient for term in quad_con.linear_terms()} def _quad_terms_dict( quad_con: quadratic_constraints.QuadraticConstraint, ) -> Dict[Tuple[variables.Variable, variables.Variable], float]: - return { - (term.key.first_var, term.key.second_var): term.coefficient - for term in quad_con.quadratic_terms() - } + return { + (term.key.first_var, term.key.second_var): term.coefficient + for term in quad_con.quadratic_terms() + } class ModelQuadraticConstraintTest(absltest.TestCase): - def test_add_quadratic_constraint_expr_with_offset(self): - mod = model.Model() - x = mod.add_variable() - c = mod.add_quadratic_constraint( - lb=-3.0, ub=3.0, expr=2.0 * x * x + 1.0, name="cde" - ) - self.assertEqual(c.name, "cde") - self.assertAlmostEqual(c.lower_bound, -4.0, delta=1e-10) - self.assertAlmostEqual(c.upper_bound, 2.0, delta=1e-10) - self.assertEmpty(list(c.linear_terms())) - self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) - - def test_add_quadratic_constraint_expr_with_offset_unbounded(self): - mod = model.Model() - x = mod.add_variable() - c = mod.add_quadratic_constraint(expr=2.0 * x * x + 1.0) - self.assertEqual(c.lower_bound, -math.inf) - self.assertEqual(c.upper_bound, math.inf) - self.assertEmpty(list(c.linear_terms())) - self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) - - def test_add_quadratic_constraint_upper_bounded_expr(self): - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - c = mod.add_quadratic_constraint( - 2.0 * x * x + 3.0 * y + 4.0 <= 10.0, name="cde" - ) - self.assertEqual(c.name, "cde") - self.assertEqual(c.lower_bound, -math.inf) - self.assertAlmostEqual(c.upper_bound, 6.0, delta=1e-10) - self.assertDictEqual(_lin_terms_dict(c), {y: 3.0}) - self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) - - def test_add_quadratic_constraint_lower_bounded_expr(self): - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - c = mod.add_quadratic_constraint(-10.0 <= 2.0 * x * x + 3.0 * y + 4.0) - self.assertAlmostEqual(c.lower_bound, -14.0, delta=1e-10) - self.assertEqual(c.upper_bound, math.inf) - self.assertDictEqual(_lin_terms_dict(c), {y: 3.0}) - self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) - - def test_add_quadratic_constraint_bounded_expr(self): - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - c = mod.add_quadratic_constraint((-10.0 <= 2.0 * x * x + 3.0 * y + 4.0) <= 10.0) - self.assertAlmostEqual(c.lower_bound, -14.0, delta=1e-10) - self.assertAlmostEqual(c.upper_bound, 6.0, delta=1e-10) - self.assertDictEqual(_lin_terms_dict(c), {y: 3.0}) - self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) - - def test_add_quadratic_no_variables_error(self): - mod = model.Model() - with self.assertRaisesRegex(TypeError, "constant left-hand-side"): - mod.add_quadratic_constraint(-10.0 <= 12.0) - - def test_add_quadratic_bad_double_inequality(self): - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, "two-sided or ranged"): - mod.add_quadratic_constraint(1.0 <= x * x <= 2.0) - - def test_all_linear_terms(self): - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - z = mod.add_variable() - c = mod.add_quadratic_constraint(expr=2.0 * x + 3.0 * y) - mod.add_quadratic_constraint(expr=x * x) - e = mod.add_quadratic_constraint(expr=4.0 * x + 5.0 * z) - self.assertCountEqual( - list(mod.quadratic_constraint_linear_nonzeros()), - [(c, x, 2.0), (c, y, 3.0), (e, x, 4.0), (e, z, 5.0)], - ) - - def test_all_quadratic_terms(self): - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - z = mod.add_variable() - c = mod.add_quadratic_constraint(expr=2.0 * x * x + 3.0 * y * x) - mod.add_quadratic_constraint(expr=x) - e = mod.add_quadratic_constraint(expr=4.0 * x * x + 5.0 * y * z) - self.assertCountEqual( - list(mod.quadratic_constraint_quadratic_nonzeros()), - [(c, x, x, 2.0), (c, x, y, 3.0), (e, x, x, 4.0), (e, y, z, 5.0)], - ) - - def test_quadratic_terms_empty(self): - mod = model.Model() - self.assertEmpty(list(mod.quadratic_constraint_linear_nonzeros())) - self.assertEmpty(list(mod.quadratic_constraint_quadratic_nonzeros())) + def test_add_quadratic_constraint_expr_with_offset(self): + mod = model.Model() + x = mod.add_variable() + c = mod.add_quadratic_constraint( + lb=-3.0, ub=3.0, expr=2.0 * x * x + 1.0, name="cde" + ) + self.assertEqual(c.name, "cde") + self.assertAlmostEqual(c.lower_bound, -4.0, delta=1e-10) + self.assertAlmostEqual(c.upper_bound, 2.0, delta=1e-10) + self.assertEmpty(list(c.linear_terms())) + self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) + + def test_add_quadratic_constraint_expr_with_offset_unbounded(self): + mod = model.Model() + x = mod.add_variable() + c = mod.add_quadratic_constraint(expr=2.0 * x * x + 1.0) + self.assertEqual(c.lower_bound, -math.inf) + self.assertEqual(c.upper_bound, math.inf) + self.assertEmpty(list(c.linear_terms())) + self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) + + def test_add_quadratic_constraint_upper_bounded_expr(self): + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + c = mod.add_quadratic_constraint( + 2.0 * x * x + 3.0 * y + 4.0 <= 10.0, name="cde" + ) + self.assertEqual(c.name, "cde") + self.assertEqual(c.lower_bound, -math.inf) + self.assertAlmostEqual(c.upper_bound, 6.0, delta=1e-10) + self.assertDictEqual(_lin_terms_dict(c), {y: 3.0}) + self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) + + def test_add_quadratic_constraint_lower_bounded_expr(self): + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + c = mod.add_quadratic_constraint(-10.0 <= 2.0 * x * x + 3.0 * y + 4.0) + self.assertAlmostEqual(c.lower_bound, -14.0, delta=1e-10) + self.assertEqual(c.upper_bound, math.inf) + self.assertDictEqual(_lin_terms_dict(c), {y: 3.0}) + self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) + + def test_add_quadratic_constraint_bounded_expr(self): + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + c = mod.add_quadratic_constraint( + (-10.0 <= 2.0 * x * x + 3.0 * y + 4.0) <= 10.0 + ) + self.assertAlmostEqual(c.lower_bound, -14.0, delta=1e-10) + self.assertAlmostEqual(c.upper_bound, 6.0, delta=1e-10) + self.assertDictEqual(_lin_terms_dict(c), {y: 3.0}) + self.assertDictEqual(_quad_terms_dict(c), {(x, x): 2.0}) + + def test_add_quadratic_no_variables_error(self): + mod = model.Model() + with self.assertRaisesRegex(TypeError, "constant left-hand-side"): + mod.add_quadratic_constraint(-10.0 <= 12.0) + + def test_add_quadratic_bad_double_inequality(self): + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, "two-sided or ranged"): + mod.add_quadratic_constraint(1.0 <= x * x <= 2.0) + + def test_all_linear_terms(self): + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + z = mod.add_variable() + c = mod.add_quadratic_constraint(expr=2.0 * x + 3.0 * y) + mod.add_quadratic_constraint(expr=x * x) + e = mod.add_quadratic_constraint(expr=4.0 * x + 5.0 * z) + self.assertCountEqual( + list(mod.quadratic_constraint_linear_nonzeros()), + [(c, x, 2.0), (c, y, 3.0), (e, x, 4.0), (e, z, 5.0)], + ) + + def test_all_quadratic_terms(self): + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + z = mod.add_variable() + c = mod.add_quadratic_constraint(expr=2.0 * x * x + 3.0 * y * x) + mod.add_quadratic_constraint(expr=x) + e = mod.add_quadratic_constraint(expr=4.0 * x * x + 5.0 * y * z) + self.assertCountEqual( + list(mod.quadratic_constraint_quadratic_nonzeros()), + [(c, x, x, 2.0), (c, x, y, 3.0), (e, x, x, 4.0), (e, y, z, 5.0)], + ) + + def test_quadratic_terms_empty(self): + mod = model.Model() + self.assertEmpty(list(mod.quadratic_constraint_linear_nonzeros())) + self.assertEmpty(list(mod.quadratic_constraint_quadratic_nonzeros())) _PROTO_VARS = model_pb2.VariablesProto( @@ -154,7 +156,9 @@ def test_quadratic_terms_empty(self): name="q1", lower_bound=-3.0, upper_bound=4.0, - linear_terms=sparse_containers_pb2.SparseDoubleVectorProto(ids=[0], values=[6.0]), + linear_terms=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[6.0] + ), quadratic_terms=sparse_containers_pb2.SparseDoubleMatrixProto( row_ids=[0], column_ids=[0], coefficients=[5.0] ), @@ -164,54 +168,54 @@ def test_quadratic_terms_empty(self): class ModelQuadraticConstraintExportProtoIntegrationTest( compare_proto.MathOptProtoAssertions, absltest.TestCase ): - """Test ModelProto and ModelUpdateProto export for quadratic constraints. - - These tests are not comprehensive, the proto generation code is completely - tested in the tests for Elemental. We just want to make sure everything is - connected. - """ - - def test_export_model(self) -> None: - mod = model.Model() - x = mod.add_variable(lb=-2.0, ub=2.0) - mod.add_quadratic_constraint( - lb=-3.0, ub=4.0, expr=5.0 * x * x + 6.0 * x, name="q1" - ) - expected = model_pb2.ModelProto( - variables=_PROTO_VARS, quadratic_constraints={0: _QUAD_CON} - ) - self.assert_protos_equal(mod.export_model(), expected) - - def test_export_model_update_add_constraint(self) -> None: - mod = model.Model() - x = mod.add_variable(lb=-2.0, ub=2.0) - tracker = mod.add_update_tracker() - mod.add_quadratic_constraint( - lb=-3.0, ub=4.0, expr=5.0 * x * x + 6.0 * x, name="q1" - ) - - expected = model_update_pb2.ModelUpdateProto( - quadratic_constraint_updates=model_update_pb2.QuadraticConstraintUpdatesProto( - new_constraints={0: _QUAD_CON} - ) + """Test ModelProto and ModelUpdateProto export for quadratic constraints. + + These tests are not comprehensive, the proto generation code is completely + tested in the tests for Elemental. We just want to make sure everything is + connected. + """ + + def test_export_model(self) -> None: + mod = model.Model() + x = mod.add_variable(lb=-2.0, ub=2.0) + mod.add_quadratic_constraint( + lb=-3.0, ub=4.0, expr=5.0 * x * x + 6.0 * x, name="q1" + ) + expected = model_pb2.ModelProto( + variables=_PROTO_VARS, quadratic_constraints={0: _QUAD_CON} + ) + self.assert_protos_equal(mod.export_model(), expected) + + def test_export_model_update_add_constraint(self) -> None: + mod = model.Model() + x = mod.add_variable(lb=-2.0, ub=2.0) + tracker = mod.add_update_tracker() + mod.add_quadratic_constraint( + lb=-3.0, ub=4.0, expr=5.0 * x * x + 6.0 * x, name="q1" + ) + + expected = model_update_pb2.ModelUpdateProto( + quadratic_constraint_updates=model_update_pb2.QuadraticConstraintUpdatesProto( + new_constraints={0: _QUAD_CON} ) + ) - self.assert_protos_equal(tracker.export_update(), expected) + self.assert_protos_equal(tracker.export_update(), expected) - def test_export_model_update_delete_constraint(self) -> None: - mod = model.Model() - q = mod.add_quadratic_constraint() - tracker = mod.add_update_tracker() - mod.delete_quadratic_constraint(q) + def test_export_model_update_delete_constraint(self) -> None: + mod = model.Model() + q = mod.add_quadratic_constraint() + tracker = mod.add_update_tracker() + mod.delete_quadratic_constraint(q) - expected = model_update_pb2.ModelUpdateProto( - quadratic_constraint_updates=model_update_pb2.QuadraticConstraintUpdatesProto( - deleted_constraint_ids=[0] - ) + expected = model_update_pb2.ModelUpdateProto( + quadratic_constraint_updates=model_update_pb2.QuadraticConstraintUpdatesProto( + deleted_constraint_ids=[0] ) + ) - self.assert_protos_equal(tracker.export_update(), expected) + self.assert_protos_equal(tracker.export_update(), expected) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/model_storage.py b/ortools/math_opt/python/model_storage.py index cf67f6a87d6..3abf5734af5 100644 --- a/ortools/math_opt/python/model_storage.py +++ b/ortools/math_opt/python/model_storage.py @@ -25,416 +25,426 @@ # available. @dataclasses.dataclass(frozen=True) class LinearConstraintMatrixIdEntry: - __slots__ = "linear_constraint_id", "variable_id", "coefficient" - linear_constraint_id: int - variable_id: int - coefficient: float + __slots__ = "linear_constraint_id", "variable_id", "coefficient" + linear_constraint_id: int + variable_id: int + coefficient: float # TODO(b/231426528): remove __slots__ and set slots=True when Python 3.10 is # available. @dataclasses.dataclass(frozen=True) class LinearObjectiveEntry: - __slots__ = "variable_id", "coefficient" - variable_id: int - coefficient: float + __slots__ = "variable_id", "coefficient" + variable_id: int + coefficient: float # TODO(b/231426528): remove __slots__ and set slots=True when Python 3.10 is # available. @dataclasses.dataclass(frozen=True) class QuadraticTermIdKey: - """An ordered pair of ints used as a key for quadratic terms. + """An ordered pair of ints used as a key for quadratic terms. - QuadraticTermIdKey.id1 <= QuadraticTermIdKey.id2. - """ + QuadraticTermIdKey.id1 <= QuadraticTermIdKey.id2. + """ - __slots__ = "id1", "id2" - id1: int - id2: int + __slots__ = "id1", "id2" + id1: int + id2: int - def __init__(self, a: int, b: int): - """Ints a and b will be ordered internally.""" - id1 = a - id2 = b - if id1 > id2: - id1, id2 = id2, id1 - object.__setattr__(self, "id1", id1) - object.__setattr__(self, "id2", id2) + def __init__(self, a: int, b: int): + """Ints a and b will be ordered internally.""" + id1 = a + id2 = b + if id1 > id2: + id1, id2 = id2, id1 + object.__setattr__(self, "id1", id1) + object.__setattr__(self, "id2", id2) # TODO(b/231426528): remove __slots__ and set slots=True when Python 3.10 is # available. @dataclasses.dataclass(frozen=True) class QuadraticEntry: - """Represents an id-indexed quadratic term.""" + """Represents an id-indexed quadratic term.""" - __slots__ = "id_key", "coefficient" - id_key: QuadraticTermIdKey - coefficient: float + __slots__ = "id_key", "coefficient" + id_key: QuadraticTermIdKey + coefficient: float class StorageUpdateTracker(abc.ABC): - """Tracks updates to an optimization model from a ModelStorage. - - Do not instantiate directly, instead create through - ModelStorage.add_update_tracker(). - - Interacting with an update tracker after it has been removed from the model - will result in an UsedUpdateTrackerAfterRemovalError error. - - Example: - mod = model_storage.ModelStorage() - x = mod.add_variable(0.0, 1.0, True, 'x') - y = mod.add_variable(0.0, 1.0, True, 'y') - tracker = mod.add_update_tracker() - mod.set_variable_ub(x, 3.0) - tracker.export_update() - => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" - mod.set_variable_ub(y, 2.0) - tracker.export_update() - => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" - tracker.advance_checkpoint() - tracker.export_update() - => "" - mod.set_variable_ub(y, 4.0) - tracker.export_update() - => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" - tracker.advance_checkpoint() - mod.remove_update_tracker(tracker) - => "" - """ - - @abc.abstractmethod - def export_update(self) -> Optional[model_update_pb2.ModelUpdateProto]: - """Returns changes to the model since last call to checkpoint/creation, or None if no changes occurred.""" - pass - - @abc.abstractmethod - def advance_checkpoint(self) -> None: - """Track changes to the model only after this function call.""" - pass + """Tracks updates to an optimization model from a ModelStorage. + + Do not instantiate directly, instead create through + ModelStorage.add_update_tracker(). + + Interacting with an update tracker after it has been removed from the model + will result in an UsedUpdateTrackerAfterRemovalError error. + + Example: + mod = model_storage.ModelStorage() + x = mod.add_variable(0.0, 1.0, True, 'x') + y = mod.add_variable(0.0, 1.0, True, 'y') + tracker = mod.add_update_tracker() + mod.set_variable_ub(x, 3.0) + tracker.export_update() + => "variable_updates: {upper_bounds: {ids: [0], values[3.0] }" + mod.set_variable_ub(y, 2.0) + tracker.export_update() + => "variable_updates: {upper_bounds: {ids: [0, 1], values[3.0, 2.0] }" + tracker.advance_checkpoint() + tracker.export_update() + => "" + mod.set_variable_ub(y, 4.0) + tracker.export_update() + => "variable_updates: {upper_bounds: {ids: [1], values[4.0] }" + tracker.advance_checkpoint() + mod.remove_update_tracker(tracker) + => "" + """ + + @abc.abstractmethod + def export_update(self) -> Optional[model_update_pb2.ModelUpdateProto]: + """Returns changes to the model since last call to checkpoint/creation, or None if no changes occurred.""" + pass + + @abc.abstractmethod + def advance_checkpoint(self) -> None: + """Track changes to the model only after this function call.""" + pass class UsedUpdateTrackerAfterRemovalError(RuntimeError): - def __init__(self): - super().__init__( - "Attempted to use update tracker after removing it from model storage." - ) + def __init__(self): + super().__init__( + "Attempted to use update tracker after removing it from model storage." + ) class BadVariableIdError(LookupError): - """Raised by ModelStorage when a bad variable id is given.""" + """Raised by ModelStorage when a bad variable id is given.""" - def __init__(self, variable_id): - super().__init__(f"Unexpected variable id: {variable_id}") - self.id = variable_id + def __init__(self, variable_id): + super().__init__(f"Unexpected variable id: {variable_id}") + self.id = variable_id class BadLinearConstraintIdError(LookupError): - """Raised by ModelStorage when a bad linear constraint id is given.""" + """Raised by ModelStorage when a bad linear constraint id is given.""" - def __init__(self, linear_constraint_id): - super().__init__(f"Unexpected linear constraint id: {linear_constraint_id}") - self.id = linear_constraint_id + def __init__(self, linear_constraint_id): + super().__init__(f"Unexpected linear constraint id: {linear_constraint_id}") + self.id = linear_constraint_id class ModelStorage(abc.ABC): - """An interface for in memory storage of an optimization model. - - Most users should not use this class directly and use Model defined in - model.py. - - Stores an mixed integer programming problem of the form: - - {max/min} c*x + d - s.t. lb_c <= A * x <= ub_c - lb_v <= x <= ub_v - x_i integer for i in I - - where x is a vector of n decision variables, d is a number, lb_v, ub_v, and c - are vectors of n numbers, lb_c and ub_c are vectors of m numbers, A is a - m by n matrix, and I is a subset of {1,..., n}. - - Each of the n variables and m constraints have an integer id that you use to - get/set the problem data (c, A, lb_c etc.). Ids begin at zero and increase - sequentially. They are not reused after deletion. Note that if a variable is - deleted, your model has nonconsecutive variable ids. - - For all methods taking an id (e.g. set_variable_lb), providing a bad id - (including the id of a deleted variable) will raise a BadVariableIdError or - BadLinearConstraintIdError. Further, the ModelStorage instance is assumed to - be in a bad state after any such error and there are no guarantees on further - interactions. - - All implementations must have a constructor taking a str argument for the - model name with a default value of the empty string. - - Any ModelStorage can be exported to model_pb2.ModelProto, the format consumed - by MathOpt solvers. Changes to a model can be exported to a - model_update_pb2.ModelUpdateProto with an UpdateTracker, see the UpdateTracker - documentation for details. - - When solving this optimization problem we will additionally require that: - * No numbers are NaN, - * c, d, and A are all finite, - * lb_c and lb_v are not +inf, - * ub_c and ub_v are not -inf, - but those assumptions are not checked or enforced here (NaNs and infinite - values can be used anywhere). + """An interface for in memory storage of an optimization model. + + Most users should not use this class directly and use Model defined in + model.py. + + Stores an mixed integer programming problem of the form: + + {max/min} c*x + d + s.t. lb_c <= A * x <= ub_c + lb_v <= x <= ub_v + x_i integer for i in I + + where x is a vector of n decision variables, d is a number, lb_v, ub_v, and c + are vectors of n numbers, lb_c and ub_c are vectors of m numbers, A is a + m by n matrix, and I is a subset of {1,..., n}. + + Each of the n variables and m constraints have an integer id that you use to + get/set the problem data (c, A, lb_c etc.). Ids begin at zero and increase + sequentially. They are not reused after deletion. Note that if a variable is + deleted, your model has nonconsecutive variable ids. + + For all methods taking an id (e.g. set_variable_lb), providing a bad id + (including the id of a deleted variable) will raise a BadVariableIdError or + BadLinearConstraintIdError. Further, the ModelStorage instance is assumed to + be in a bad state after any such error and there are no guarantees on further + interactions. + + All implementations must have a constructor taking a str argument for the + model name with a default value of the empty string. + + Any ModelStorage can be exported to model_pb2.ModelProto, the format consumed + by MathOpt solvers. Changes to a model can be exported to a + model_update_pb2.ModelUpdateProto with an UpdateTracker, see the UpdateTracker + documentation for details. + + When solving this optimization problem we will additionally require that: + * No numbers are NaN, + * c, d, and A are all finite, + * lb_c and lb_v are not +inf, + * ub_c and ub_v are not -inf, + but those assumptions are not checked or enforced here (NaNs and infinite + values can be used anywhere). + """ + + @property + @abc.abstractmethod + def name(self) -> str: + pass + + @abc.abstractmethod + def add_variable( + self, lb: float, ub: float, is_integer: bool, name: str + ) -> int: + pass + + @abc.abstractmethod + def delete_variable(self, variable_id: int) -> None: + pass + + @abc.abstractmethod + def variable_exists(self, variable_id: int) -> bool: + pass + + @abc.abstractmethod + def next_variable_id(self) -> int: + pass + + @abc.abstractmethod + def set_variable_lb(self, variable_id: int, lb: float) -> None: + pass + + @abc.abstractmethod + def set_variable_ub(self, variable_id: int, ub: float) -> None: + pass + + @abc.abstractmethod + def set_variable_is_integer(self, variable_id: int, is_integer: bool) -> None: + pass + + @abc.abstractmethod + def get_variable_lb(self, variable_id: int) -> float: + pass + + @abc.abstractmethod + def get_variable_ub(self, variable_id: int) -> float: + pass + + @abc.abstractmethod + def get_variable_is_integer(self, variable_id: int) -> bool: + pass + + @abc.abstractmethod + def get_variable_name(self, variable_id: int) -> str: + pass + + @abc.abstractmethod + def get_variables(self) -> Iterator[int]: + """Yields the variable ids in order of creation.""" + pass + + @abc.abstractmethod + def add_linear_constraint(self, lb: float, ub: float, name: str) -> int: + pass + + @abc.abstractmethod + def delete_linear_constraint(self, linear_constraint_id: int) -> None: + pass + + @abc.abstractmethod + def linear_constraint_exists(self, linear_constraint_id: int) -> bool: + pass + + @abc.abstractmethod + def next_linear_constraint_id(self) -> int: + pass + + @abc.abstractmethod + def set_linear_constraint_lb( + self, linear_constraint_id: int, lb: float + ) -> None: + pass + + @abc.abstractmethod + def set_linear_constraint_ub( + self, linear_constraint_id: int, ub: float + ) -> None: + pass + + @abc.abstractmethod + def get_linear_constraint_lb(self, linear_constraint_id: int) -> float: + pass + + @abc.abstractmethod + def get_linear_constraint_ub(self, linear_constraint_id: int) -> float: + pass + + @abc.abstractmethod + def get_linear_constraint_name(self, linear_constraint_id: int) -> str: + pass + + @abc.abstractmethod + def get_linear_constraints(self) -> Iterator[int]: + """Yields the linear constraint ids in order of creation.""" + pass + + @abc.abstractmethod + def set_linear_constraint_coefficient( + self, linear_constraint_id: int, variable_id: int, lb: float + ) -> None: + pass + + @abc.abstractmethod + def get_linear_constraint_coefficient( + self, linear_constraint_id: int, variable_id: int + ) -> float: + pass + + @abc.abstractmethod + def get_linear_constraints_with_variable( + self, variable_id: int + ) -> Iterator[int]: + """Yields the linear constraints with nonzero coefficient for a variable in undefined order.""" + pass + + @abc.abstractmethod + def get_variables_for_linear_constraint( + self, linear_constraint_id: int + ) -> Iterator[int]: + """Yields the variables with nonzero coefficient in a linear constraint in undefined order.""" + pass + + @abc.abstractmethod + def get_linear_constraint_matrix_entries( + self, + ) -> Iterator[LinearConstraintMatrixIdEntry]: + """Yields the nonzero elements of the linear constraint matrix in undefined order.""" + pass + + @abc.abstractmethod + def clear_objective(self) -> None: + """Clears objective coefficients and offset. Does not change direction.""" + + @abc.abstractmethod + def set_linear_objective_coefficient( + self, variable_id: int, value: float + ) -> None: + pass + + @abc.abstractmethod + def get_linear_objective_coefficient(self, variable_id: int) -> float: + pass + + @abc.abstractmethod + def get_linear_objective_coefficients(self) -> Iterator[LinearObjectiveEntry]: + """Yields the nonzero linear objective terms in undefined order.""" + pass + + @abc.abstractmethod + def set_quadratic_objective_coefficient( + self, first_variable_id: int, second_variable_id: int, value: float + ) -> None: + """Sets the objective coefficient for the product of two variables. + + The ordering of the input variables does not matter. + + Args: + first_variable_id: The first variable in the product. + second_variable_id: The second variable in the product. + value: The value of the coefficient. + + Raises: + BadVariableIdError if first_variable_id or second_variable_id are not in + the model. + """ + + @abc.abstractmethod + def get_quadratic_objective_coefficient( + self, first_variable_id: int, second_variable_id: int + ) -> float: + """Gets the objective coefficient for the product of two variables. + + The ordering of the input variables does not matter. + + Args: + first_variable_id: The first variable in the product. + second_variable_id: The second variable in the product. + + Raises: + BadVariableIdError if first_variable_id or second_variable_id are not in + the model. + + Returns: + The value of the coefficient. + """ + + @abc.abstractmethod + def get_quadratic_objective_coefficients(self) -> Iterator[QuadraticEntry]: + """Yields the nonzero quadratic objective terms in undefined order.""" + + @abc.abstractmethod + def get_quadratic_objective_adjacent_variables( + self, variable_id: int + ) -> Iterator[int]: + """Yields the variables multiplying a variable in the objective function. + + Variables are returned in an unspecified order. + + For example, if variables x and y have ids 0 and 1 respectively, and the + quadratic portion of the objective is x^2 + 2 x*y, then + get_quadratic_objective_adjacent_variables(0) = (0, 1). + + Args: + variable_id: Function yields the variables multiplying variable_id in the + objective function. + + Yields: + The variables multiplying variable_id in the objective function. + + Raises: + BadVariableIdError if variable_id is not in the model. """ - @property - @abc.abstractmethod - def name(self) -> str: - pass - - @abc.abstractmethod - def add_variable(self, lb: float, ub: float, is_integer: bool, name: str) -> int: - pass - - @abc.abstractmethod - def delete_variable(self, variable_id: int) -> None: - pass - - @abc.abstractmethod - def variable_exists(self, variable_id: int) -> bool: - pass - - @abc.abstractmethod - def next_variable_id(self) -> int: - pass - - @abc.abstractmethod - def set_variable_lb(self, variable_id: int, lb: float) -> None: - pass - - @abc.abstractmethod - def set_variable_ub(self, variable_id: int, ub: float) -> None: - pass - - @abc.abstractmethod - def set_variable_is_integer(self, variable_id: int, is_integer: bool) -> None: - pass - - @abc.abstractmethod - def get_variable_lb(self, variable_id: int) -> float: - pass - - @abc.abstractmethod - def get_variable_ub(self, variable_id: int) -> float: - pass - - @abc.abstractmethod - def get_variable_is_integer(self, variable_id: int) -> bool: - pass - - @abc.abstractmethod - def get_variable_name(self, variable_id: int) -> str: - pass - - @abc.abstractmethod - def get_variables(self) -> Iterator[int]: - """Yields the variable ids in order of creation.""" - pass - - @abc.abstractmethod - def add_linear_constraint(self, lb: float, ub: float, name: str) -> int: - pass - - @abc.abstractmethod - def delete_linear_constraint(self, linear_constraint_id: int) -> None: - pass - - @abc.abstractmethod - def linear_constraint_exists(self, linear_constraint_id: int) -> bool: - pass - - @abc.abstractmethod - def next_linear_constraint_id(self) -> int: - pass - - @abc.abstractmethod - def set_linear_constraint_lb(self, linear_constraint_id: int, lb: float) -> None: - pass - - @abc.abstractmethod - def set_linear_constraint_ub(self, linear_constraint_id: int, ub: float) -> None: - pass - - @abc.abstractmethod - def get_linear_constraint_lb(self, linear_constraint_id: int) -> float: - pass - - @abc.abstractmethod - def get_linear_constraint_ub(self, linear_constraint_id: int) -> float: - pass - - @abc.abstractmethod - def get_linear_constraint_name(self, linear_constraint_id: int) -> str: - pass - - @abc.abstractmethod - def get_linear_constraints(self) -> Iterator[int]: - """Yields the linear constraint ids in order of creation.""" - pass - - @abc.abstractmethod - def set_linear_constraint_coefficient( - self, linear_constraint_id: int, variable_id: int, lb: float - ) -> None: - pass - - @abc.abstractmethod - def get_linear_constraint_coefficient( - self, linear_constraint_id: int, variable_id: int - ) -> float: - pass - - @abc.abstractmethod - def get_linear_constraints_with_variable(self, variable_id: int) -> Iterator[int]: - """Yields the linear constraints with nonzero coefficient for a variable in undefined order.""" - pass - - @abc.abstractmethod - def get_variables_for_linear_constraint( - self, linear_constraint_id: int - ) -> Iterator[int]: - """Yields the variables with nonzero coefficient in a linear constraint in undefined order.""" - pass - - @abc.abstractmethod - def get_linear_constraint_matrix_entries( - self, - ) -> Iterator[LinearConstraintMatrixIdEntry]: - """Yields the nonzero elements of the linear constraint matrix in undefined order.""" - pass - - @abc.abstractmethod - def clear_objective(self) -> None: - """Clears objective coefficients and offset. Does not change direction.""" - - @abc.abstractmethod - def set_linear_objective_coefficient(self, variable_id: int, value: float) -> None: - pass - - @abc.abstractmethod - def get_linear_objective_coefficient(self, variable_id: int) -> float: - pass - - @abc.abstractmethod - def get_linear_objective_coefficients(self) -> Iterator[LinearObjectiveEntry]: - """Yields the nonzero linear objective terms in undefined order.""" - pass - - @abc.abstractmethod - def set_quadratic_objective_coefficient( - self, first_variable_id: int, second_variable_id: int, value: float - ) -> None: - """Sets the objective coefficient for the product of two variables. - - The ordering of the input variables does not matter. - - Args: - first_variable_id: The first variable in the product. - second_variable_id: The second variable in the product. - value: The value of the coefficient. - - Raises: - BadVariableIdError if first_variable_id or second_variable_id are not in - the model. - """ - - @abc.abstractmethod - def get_quadratic_objective_coefficient( - self, first_variable_id: int, second_variable_id: int - ) -> float: - """Gets the objective coefficient for the product of two variables. - - The ordering of the input variables does not matter. - - Args: - first_variable_id: The first variable in the product. - second_variable_id: The second variable in the product. - - Raises: - BadVariableIdError if first_variable_id or second_variable_id are not in - the model. - - Returns: - The value of the coefficient. - """ - - @abc.abstractmethod - def get_quadratic_objective_coefficients(self) -> Iterator[QuadraticEntry]: - """Yields the nonzero quadratic objective terms in undefined order.""" - - @abc.abstractmethod - def get_quadratic_objective_adjacent_variables( - self, variable_id: int - ) -> Iterator[int]: - """Yields the variables multiplying a variable in the objective function. - - Variables are returned in an unspecified order. - - For example, if variables x and y have ids 0 and 1 respectively, and the - quadratic portion of the objective is x^2 + 2 x*y, then - get_quadratic_objective_adjacent_variables(0) = (0, 1). - - Args: - variable_id: Function yields the variables multiplying variable_id in the - objective function. - - Yields: - The variables multiplying variable_id in the objective function. - - Raises: - BadVariableIdError if variable_id is not in the model. - """ - - @abc.abstractmethod - def set_is_maximize(self, is_maximize: bool) -> None: - pass - - @abc.abstractmethod - def get_is_maximize(self) -> bool: - pass - - @abc.abstractmethod - def set_objective_offset(self, offset: float) -> None: - pass - - @abc.abstractmethod - def get_objective_offset(self) -> float: - pass + @abc.abstractmethod + def set_is_maximize(self, is_maximize: bool) -> None: + pass + + @abc.abstractmethod + def get_is_maximize(self) -> bool: + pass + + @abc.abstractmethod + def set_objective_offset(self, offset: float) -> None: + pass - @abc.abstractmethod - def export_model(self) -> model_pb2.ModelProto: - pass + @abc.abstractmethod + def get_objective_offset(self) -> float: + pass - @abc.abstractmethod - def add_update_tracker(self) -> StorageUpdateTracker: - """Creates a StorageUpdateTracker registered with self to view model changes.""" - pass + @abc.abstractmethod + def export_model(self) -> model_pb2.ModelProto: + pass - @abc.abstractmethod - def remove_update_tracker(self, tracker: StorageUpdateTracker): - """Stops tracker from getting updates on model changes in self. + @abc.abstractmethod + def add_update_tracker(self) -> StorageUpdateTracker: + """Creates a StorageUpdateTracker registered with self to view model changes.""" + pass - An error will be raised if tracker is not a StorageUpdateTracker created by - this Model that has not previously been removed. + @abc.abstractmethod + def remove_update_tracker(self, tracker: StorageUpdateTracker): + """Stops tracker from getting updates on model changes in self. - Using an UpdateTracker (via checkpoint or export_update) after it has been - removed will result in an error. + An error will be raised if tracker is not a StorageUpdateTracker created by + this Model that has not previously been removed. - Args: - tracker: The StorageUpdateTracker to unregister. + Using an UpdateTracker (via checkpoint or export_update) after it has been + removed will result in an error. - Raises: - KeyError: The tracker was created by another model or was already removed. - """ - pass + Args: + tracker: The StorageUpdateTracker to unregister. + + Raises: + KeyError: The tracker was created by another model or was already removed. + """ + pass ModelStorageImpl = TypeVar("ModelStorageImpl", bound=ModelStorage) diff --git a/ortools/math_opt/python/model_storage_test.py b/ortools/math_opt/python/model_storage_test.py index 64590af629d..19aa993a94c 100644 --- a/ortools/math_opt/python/model_storage_test.py +++ b/ortools/math_opt/python/model_storage_test.py @@ -29,913 +29,941 @@ @parameterized.parameters((hash_model_storage.HashModelStorage,)) -class ModelStorageTest(compare_proto.MathOptProtoAssertions, parameterized.TestCase): - - def test_add_and_read_variables(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - self.assertEqual(0, storage.next_variable_id()) - v1 = storage.add_variable(-1.0, 2.5, True, "x") - v2 = storage.add_variable(-math.inf, math.inf, False, "") - self.assertEqual("test_model", storage.name) - - self.assertEqual(-1.0, storage.get_variable_lb(v1)) - self.assertEqual(2.5, storage.get_variable_ub(v1)) - self.assertTrue(storage.get_variable_is_integer(v1)) - self.assertEqual("x", storage.get_variable_name(v1)) - self.assertEqual(0, v1) - self.assertTrue(storage.variable_exists(v1)) - - self.assertEqual(-math.inf, storage.get_variable_lb(v2)) - self.assertEqual(math.inf, storage.get_variable_ub(v2)) - self.assertFalse(storage.get_variable_is_integer(v2)) - self.assertEqual("", storage.get_variable_name(v2)) - self.assertEqual(1, v2) - self.assertTrue(storage.variable_exists(v2)) - - self.assertFalse(storage.variable_exists(max(v1, v2) + 1)) - self.assertListEqual([v1, v2], list(storage.get_variables())) - self.assertEqual(2, storage.next_variable_id()) - - def test_set_variable_lb(self, storage_class: _StorageClass) -> None: - storage = storage_class() - v1 = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_variable_lb(v1, -5.5) - self.assertEqual(-5.5, storage.get_variable_lb(v1)) - - def test_set_variable_ub(self, storage_class: _StorageClass) -> None: - storage = storage_class() - v1 = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_variable_ub(v1, 1.2) - self.assertEqual(1.2, storage.get_variable_ub(v1)) - - def test_set_variable_is_integer(self, storage_class: _StorageClass) -> None: - storage = storage_class() - v1 = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_variable_is_integer(v1, False) - self.assertFalse(storage.get_variable_is_integer(v1)) - - def test_add_and_read_linear_constraints( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - self.assertEqual(0, storage.next_linear_constraint_id()) - c1 = storage.add_linear_constraint(-1.0, 2.5, "c") - c2 = storage.add_linear_constraint(-math.inf, math.inf, "") - - self.assertEqual(-1.0, storage.get_linear_constraint_lb(c1)) - self.assertEqual(2.5, storage.get_linear_constraint_ub(c1)) - self.assertEqual("c", storage.get_linear_constraint_name(c1)) - self.assertEqual(0, c1) - self.assertTrue(storage.linear_constraint_exists(c1)) - - self.assertEqual(-math.inf, storage.get_linear_constraint_lb(c2)) - self.assertEqual(math.inf, storage.get_linear_constraint_ub(c2)) - self.assertEqual("", storage.get_linear_constraint_name(c2)) - self.assertEqual(1, c2) - self.assertTrue(storage.linear_constraint_exists(c2)) - - self.assertListEqual([c1, c2], list(storage.get_linear_constraints())) - self.assertFalse(storage.linear_constraint_exists(1 + max(c1, c2))) - self.assertEqual(2, storage.next_linear_constraint_id()) - - def test_set_linear_constraint_lb(self, storage_class: _StorageClass) -> None: - storage = storage_class() - c1 = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_lb(c1, -5.5) - self.assertEqual(-5.5, storage.get_linear_constraint_lb(c1)) - - def test_set_linear_constraint_ub(self, storage_class: _StorageClass) -> None: - storage = storage_class() - c1 = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_ub(c1, 1.2) - self.assertEqual(1.2, storage.get_linear_constraint_ub(c1)) - - def test_delete_variable_get_other(self, storage_class: _StorageClass) -> None: - storage = storage_class() - v1 = storage.add_variable(-1.0, 2.5, True, "x") - v2 = storage.add_variable(-3.0, 4.5, False, "y") - storage.delete_variable(v1) - self.assertEqual(-3.0, storage.get_variable_lb(v2)) - self.assertEqual(4.5, storage.get_variable_ub(v2)) - self.assertFalse(storage.get_variable_is_integer(v2)) - self.assertEqual("y", storage.get_variable_name(v2)) - self.assertEqual(1, v2) - self.assertFalse(storage.variable_exists(v1)) - self.assertTrue(storage.variable_exists(v2)) - - self.assertListEqual([v2], list(storage.get_variables())) - - def test_double_variable_delete(self, storage_class: _StorageClass) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.delete_variable(x) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.delete_variable(x) - self.assertEqual(x, cm.exception.id) - - def _deleted_variable_invoke_lookup( - self, - storage_class: _StorageClass, - getter: Callable[[model_storage.ModelStorage, int], Any], - ) -> None: - storage = storage_class() - v1 = storage.add_variable(-1.0, 2.5, True, "x") - storage.delete_variable(v1) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - getter(storage, v1) - self.assertEqual(v1, cm.exception.id) - - def test_delete_variable_lb_error(self, storage_class: _StorageClass) -> None: - self._deleted_variable_invoke_lookup( - storage_class, storage_class.get_variable_lb - ) - - def test_delete_variable_ub_error(self, storage_class: _StorageClass) -> None: - self._deleted_variable_invoke_lookup( - storage_class, storage_class.get_variable_ub - ) - - def test_delete_variable_is_integer_error( - self, storage_class: _StorageClass - ) -> None: - self._deleted_variable_invoke_lookup( - storage_class, storage_class.get_variable_is_integer - ) - - def test_delete_variable_name_error(self, storage_class: _StorageClass) -> None: - self._deleted_variable_invoke_lookup( - storage_class, storage_class.get_variable_name - ) - - def test_delete_variable_set_lb_error(self, storage_class: _StorageClass) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.delete_variable(x) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.set_variable_lb(x, -2.0) - self.assertEqual(x, cm.exception.id) - - def test_delete_variable_set_ub_error(self, storage_class: _StorageClass) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.delete_variable(x) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.set_variable_ub(x, 12.0) - self.assertEqual(x, cm.exception.id) - - def test_delete_variable_set_integer_error( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.delete_variable(x) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.set_variable_is_integer(x, False) - self.assertEqual(x, cm.exception.id) - - def test_delete_linear_constraint_get_other( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - c1 = storage.add_linear_constraint(-1.0, 2.5, "c1") - c2 = storage.add_linear_constraint(-math.inf, 5.0, "c2") - storage.delete_linear_constraint(c1) - self.assertEqual(-math.inf, storage.get_linear_constraint_lb(c2)) - self.assertEqual(5.0, storage.get_linear_constraint_ub(c2)) - self.assertEqual("c2", storage.get_linear_constraint_name(c2)) - self.assertEqual(1, c2) - - self.assertListEqual([c2], list(storage.get_linear_constraints())) - - def test_double_linear_constraint_delete( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.delete_linear_constraint(c) - with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: - storage.delete_linear_constraint(c) - self.assertEqual(c, cm.exception.id) - - def _deleted_linear_constraint_invoke_lookup( - self, - storage_class: _StorageClass, - getter: Callable[[model_storage.ModelStorage, int], Any], - ) -> None: - storage = storage_class() - c1 = storage.add_linear_constraint(-1.0, 2.5, "c1") - storage.delete_linear_constraint(c1) - with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: - getter(storage, c1) - self.assertEqual(c1, cm.exception.id) - - def test_delete_linear_constraint_lb_error( - self, storage_class: _StorageClass - ) -> None: - self._deleted_linear_constraint_invoke_lookup( - storage_class, storage_class.get_linear_constraint_lb - ) - - def test_delete_linear_constraint_ub_error( - self, storage_class: _StorageClass - ) -> None: - self._deleted_linear_constraint_invoke_lookup( - storage_class, storage_class.get_linear_constraint_ub - ) - - def test_delete_linear_constraint_name_error( - self, storage_class: _StorageClass - ) -> None: - self._deleted_linear_constraint_invoke_lookup( - storage_class, storage_class.get_linear_constraint_name - ) - - def test_delete_linear_constraint_set_lb_error( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.delete_linear_constraint(c) - with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: - storage.set_linear_constraint_lb(c, -2.0) - self.assertEqual(c, cm.exception.id) - - def test_delete_linear_constraint_set_ub_error( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.delete_linear_constraint(c) - with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: - storage.set_linear_constraint_ub(c, 12.0) - self.assertEqual(c, cm.exception.id) - - def test_objective_offset(self, storage_class: _StorageClass) -> None: - storage = storage_class() - self.assertEqual(0.0, storage.get_objective_offset()) - storage.set_objective_offset(1.5) - self.assertEqual(1.5, storage.get_objective_offset()) - - def test_objective_direction(self, storage_class: _StorageClass) -> None: - storage = storage_class() - self.assertFalse(storage.get_is_maximize()) - storage.set_is_maximize(True) - self.assertTrue(storage.get_is_maximize()) - storage.set_is_maximize(False) - self.assertFalse(storage.get_is_maximize()) - - def test_set_linear_objective_coefficient( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(0.0, 1.0, False, "y") - z = storage.add_variable(0.0, 1.0, True, "z") - storage.set_linear_objective_coefficient(x, 2.0) - storage.set_linear_objective_coefficient(z, -5.5) - self.assertEqual(2.0, storage.get_linear_objective_coefficient(x)) - self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) - self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z)) - - self.assertCountEqual( - [ - _ObjEntry(variable_id=x, coefficient=2.0), - _ObjEntry(variable_id=z, coefficient=-5.5), - ], - storage.get_linear_objective_coefficients(), - ) - - def test_clear_linear_objective_coefficient( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(0.0, 1.0, False, "y") - z = storage.add_variable(0.0, 1.0, True, "z") - storage.set_linear_objective_coefficient(x, 2.0) - storage.set_linear_objective_coefficient(z, -5.5) - storage.set_objective_offset(1.0) - self.assertEqual(2.0, storage.get_linear_objective_coefficient(x)) - self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) - self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z)) - self.assertEqual(1.0, storage.get_objective_offset()) - storage.clear_objective() - self.assertEqual(0.0, storage.get_linear_objective_coefficient(x)) - self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) - self.assertEqual(0.0, storage.get_linear_objective_coefficient(z)) - self.assertEqual(0.0, storage.get_objective_offset()) - - def test_set_linear_objective_coefficient_bad_id( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.set_linear_objective_coefficient(x + 1, 2.0) - self.assertEqual(x + 1, cm.exception.id) - - def test_set_linear_objective_coefficient_deleted_id( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_linear_objective_coefficient(y, 3.0) - storage.delete_variable(x) - self.assertEqual(3.0, storage.get_linear_objective_coefficient(y)) - self.assertCountEqual( - [model_storage.LinearObjectiveEntry(variable_id=y, coefficient=3.0)], - storage.get_linear_objective_coefficients(), - ) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.set_linear_objective_coefficient(x, 2.0) - self.assertEqual(x, cm.exception.id) - - def test_get_linear_objective_coefficient_deleted_nonzero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_linear_objective_coefficient(x, 1.0) - storage.set_linear_objective_coefficient(y, 3.0) - storage.delete_variable(x) - self.assertEqual(3.0, storage.get_linear_objective_coefficient(y)) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.get_linear_objective_coefficient(x) - self.assertEqual(x, cm.exception.id) - - def test_set_quadratic_objective_coefficient( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(0.0, 1.0, False, "y") - z = storage.add_variable(0.0, 1.0, True, "z") - storage.set_quadratic_objective_coefficient(x, y, 2.0) - storage.set_quadratic_objective_coefficient(z, z, -5.5) - storage.set_quadratic_objective_coefficient(z, y, 1.5) - self.assertEqual(2.0, storage.get_quadratic_objective_coefficient(x, y)) - self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, y)) - self.assertEqual(-5.5, storage.get_quadratic_objective_coefficient(z, z)) - self.assertEqual(1.5, storage.get_quadratic_objective_coefficient(y, z)) - - self.assertCountEqual( - [ - model_storage.QuadraticEntry( - id_key=model_storage.QuadraticTermIdKey(x, y), coefficient=2.0 - ), - model_storage.QuadraticEntry( - id_key=model_storage.QuadraticTermIdKey(z, z), coefficient=-5.5 - ), - model_storage.QuadraticEntry( - id_key=model_storage.QuadraticTermIdKey(y, z), coefficient=1.5 - ), - ], - storage.get_quadratic_objective_coefficients(), - ) - - self.assertCountEqual( - [y], storage.get_quadratic_objective_adjacent_variables(x) - ) - self.assertCountEqual( - [x, z], storage.get_quadratic_objective_adjacent_variables(y) - ) - self.assertCountEqual( - [y, z], storage.get_quadratic_objective_adjacent_variables(z) - ) - - def test_clear_quadratic_objective_coefficient( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(0.0, 1.0, False, "y") - z = storage.add_variable(0.0, 1.0, True, "z") - storage.set_linear_objective_coefficient(x, 2.0) - storage.set_linear_objective_coefficient(z, -5.5) - storage.set_quadratic_objective_coefficient(x, y, 2.0) - storage.set_quadratic_objective_coefficient(z, z, -5.5) - storage.set_quadratic_objective_coefficient(z, y, 1.5) - storage.set_objective_offset(1.0) - storage.clear_objective() - self.assertEqual(0.0, storage.get_linear_objective_coefficient(x)) - self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) - self.assertEqual(0.0, storage.get_linear_objective_coefficient(z)) - self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, y)) - self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, y)) - self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(z, z)) - self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, z)) - self.assertEqual(0.0, storage.get_objective_offset()) - self.assertEmpty(list(storage.get_quadratic_objective_adjacent_variables(x))) - self.assertEmpty(list(storage.get_quadratic_objective_adjacent_variables(y))) - self.assertEmpty(list(storage.get_quadratic_objective_adjacent_variables(z))) - - def test_set_quadratic_objective_coefficient_bad_id( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.set_quadratic_objective_coefficient(x, x + 1, 2.0) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.set_quadratic_objective_coefficient(x + 1, x, 2.0) - self.assertEqual(x + 1, cm.exception.id) - - def test_get_quadratic_objective_coefficient_bad_id( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.get_quadratic_objective_coefficient(x, x + 1) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.get_quadratic_objective_coefficient(x + 1, x) - self.assertEqual(x + 1, cm.exception.id) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - list(storage.get_quadratic_objective_adjacent_variables(x + 1)) - self.assertEqual(x + 1, cm.exception.id) - - def test_set_quadratic_objective_coefficient_existing_to_zero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, x, -1.0) - storage.set_quadratic_objective_coefficient(x, y, 1.0) - storage.set_quadratic_objective_coefficient(y, y, 3.0) - - storage.set_quadratic_objective_coefficient(x, x, 0.0) - storage.set_quadratic_objective_coefficient(x, y, 0.0) - self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, x)) - self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, y)) - self.assertEqual(3.0, storage.get_quadratic_objective_coefficient(y, y)) - self.assertCountEqual( - [y], storage.get_quadratic_objective_adjacent_variables(y) - ) - self.assertEmpty(list(storage.get_quadratic_objective_adjacent_variables(x))) - - def test_set_quadratic_objective_coefficient_deleted_id( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 1.0) - storage.set_quadratic_objective_coefficient(y, y, 3.0) - storage.delete_variable(x) - self.assertEqual(3.0, storage.get_quadratic_objective_coefficient(y, y)) - self.assertCountEqual( - [y], storage.get_quadratic_objective_adjacent_variables(y) - ) - - def test_set_quadratic_objective_coefficient_deleted_id_get_coeff_error( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 1.0) - storage.set_quadratic_objective_coefficient(y, y, 3.0) - storage.delete_variable(x) - - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.get_quadratic_objective_coefficient(x, y) - self.assertEqual(x, cm.exception.id) - - def test_set_quadratic_objective_coefficient_deleted_id_set_coeff_error( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 1.0) - storage.set_quadratic_objective_coefficient(y, y, 3.0) - storage.delete_variable(x) - - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.set_quadratic_objective_coefficient(x, y, 1.0) - self.assertEqual(x, cm.exception.id) - - def test_set_quadratic_objective_coefficient_deleted_id_adjacent_error( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 1.0) - storage.set_quadratic_objective_coefficient(y, y, 3.0) - storage.delete_variable(x) - - with self.assertRaises(model_storage.BadVariableIdError) as cm: - list(storage.get_quadratic_objective_adjacent_variables(x)) - self.assertEqual(x, cm.exception.id) - - def test_constraint_matrix(self, storage_class: _StorageClass) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, False, "y") - z = storage.add_variable(0.0, 1.0, True, "z") - c = storage.add_linear_constraint(-math.inf, 3.0, "c") - d = storage.add_linear_constraint(-math.inf, 1.0, "d") - storage.set_linear_constraint_coefficient(c, y, 1.0) - storage.set_linear_constraint_coefficient(d, x, 2.0) - storage.set_linear_constraint_coefficient(d, y, -1.0) - storage.set_linear_constraint_coefficient(d, z, 1.0) - storage.set_linear_constraint_coefficient(d, z, 0.0) - - self.assertEqual(0.0, storage.get_linear_constraint_coefficient(c, x)) - self.assertEqual(1.0, storage.get_linear_constraint_coefficient(c, y)) - self.assertEqual(0.0, storage.get_linear_constraint_coefficient(c, z)) - - self.assertEqual(2.0, storage.get_linear_constraint_coefficient(d, x)) - self.assertEqual(-1.0, storage.get_linear_constraint_coefficient(d, y)) - self.assertEqual(0.0, storage.get_linear_constraint_coefficient(d, z)) - - self.assertCountEqual([y], storage.get_variables_for_linear_constraint(c)) - self.assertCountEqual([x, y], storage.get_variables_for_linear_constraint(d)) - - self.assertCountEqual([d], storage.get_linear_constraints_with_variable(x)) - self.assertCountEqual([c, d], storage.get_linear_constraints_with_variable(y)) - self.assertCountEqual([], storage.get_linear_constraints_with_variable(z)) - - self.assertCountEqual( - [ - _MatEntry(linear_constraint_id=c, variable_id=y, coefficient=1.0), - _MatEntry(linear_constraint_id=d, variable_id=x, coefficient=2.0), - _MatEntry(linear_constraint_id=d, variable_id=y, coefficient=-1.0), - ], - storage.get_linear_constraint_matrix_entries(), - ) - - def test_constraint_matrix_zero_unset_entry( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-math.inf, 3.0, "c") - storage.set_linear_constraint_coefficient(c, x, 0.0) - self.assertEmpty(list(storage.get_linear_objective_coefficients())) - self.assertEmpty(list(storage.get_variables_for_linear_constraint(c))) - self.assertEmpty(list(storage.get_linear_constraints_with_variable(x))) - self.assertEqual(0.0, storage.get_linear_constraint_coefficient(c, x)) - - def test_constraint_matrix_with_deletion( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, False, "y") - z = storage.add_variable(0.0, 1.0, True, "z") - c = storage.add_linear_constraint(-math.inf, 3.0, "c") - d = storage.add_linear_constraint(-math.inf, 1.0, "d") - storage.set_linear_constraint_coefficient(c, y, 1.0) - storage.set_linear_constraint_coefficient(d, x, 2.0) - storage.set_linear_constraint_coefficient(d, y, -1.0) - storage.set_linear_constraint_coefficient(c, z, 1.0) - - storage.delete_variable(y) - storage.delete_linear_constraint(c) - - self.assertEqual(2.0, storage.get_linear_constraint_coefficient(d, x)) - self.assertEqual(0.0, storage.get_linear_constraint_coefficient(d, z)) - - self.assertCountEqual([x], storage.get_variables_for_linear_constraint(d)) - - self.assertCountEqual([d], storage.get_linear_constraints_with_variable(x)) - self.assertCountEqual([], storage.get_linear_constraints_with_variable(z)) - - self.assertCountEqual( - [_MatEntry(linear_constraint_id=d, variable_id=x, coefficient=2.0)], - storage.get_linear_constraint_matrix_entries(), - ) - - def test_variables_for_linear_constraint_deleted_error( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-math.inf, 3.0, "c") - storage.set_linear_constraint_coefficient(c, x, 1.0) - storage.delete_linear_constraint(c) - with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: - list(storage.get_variables_for_linear_constraint(c)) - self.assertEqual(c, cm.exception.id) - - def test_linear_constraints_with_variable_deleted_error( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-math.inf, 3.0, "c") - storage.set_linear_constraint_coefficient(c, x, 1.0) - storage.delete_variable(x) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - list(storage.get_linear_constraints_with_variable(x)) - self.assertEqual(x, cm.exception.id) - - def test_constraint_matrix_set_deleted_var( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-math.inf, 3.0, "c") - storage.delete_variable(x) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.set_linear_constraint_coefficient(c, x, 2.0) - self.assertEqual(x, cm.exception.id) - - def test_constraint_matrix_get_deleted_var( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-math.inf, 3.0, "c") - storage.delete_variable(x) - with self.assertRaises(model_storage.BadVariableIdError) as cm: - storage.get_linear_constraint_coefficient(c, x) - self.assertEqual(x, cm.exception.id) - - def test_constraint_matrix_set_deleted_constraint( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-math.inf, 3.0, "c") - storage.delete_linear_constraint(c) - with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: - storage.set_linear_constraint_coefficient(c, x, 2.0) - self.assertEqual(c, cm.exception.id) - - def test_constraint_matrix_get_deleted_constraint( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-math.inf, 3.0, "c") - storage.delete_linear_constraint(c) - with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: - storage.get_linear_constraint_coefficient(c, x) - self.assertEqual(c, cm.exception.id) - - def test_proto_export(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, False, "") - z = storage.add_variable(0.0, 1.0, True, "z") - c = storage.add_linear_constraint(-math.inf, 3.0, "") - d = storage.add_linear_constraint(0.0, 1.0, "d") - storage.set_linear_constraint_coefficient(c, y, 1.0) - storage.set_linear_constraint_coefficient(d, x, 2.0) - storage.set_linear_constraint_coefficient(d, y, -1.0) - storage.set_linear_constraint_coefficient(d, z, 1.0) - storage.set_linear_constraint_coefficient(d, z, 0.0) - storage.set_linear_objective_coefficient(x, 2.5) - storage.set_linear_objective_coefficient(z, -1.0) - storage.set_quadratic_objective_coefficient(x, x, 3.0) - storage.set_quadratic_objective_coefficient(x, y, 4.0) - storage.set_quadratic_objective_coefficient(x, z, 5.0) - storage.set_is_maximize(True) - storage.set_objective_offset(7.0) - - expected = model_pb2.ModelProto( - name="test_model", - variables=model_pb2.VariablesProto( - ids=[0, 1, 2], - lower_bounds=[-1.0, -1.0, 0.0], - upper_bounds=[2.5, 2.5, 1.0], - integers=[True, False, True], - names=["x", "", "z"], +class ModelStorageTest( + compare_proto.MathOptProtoAssertions, parameterized.TestCase +): + + def test_add_and_read_variables(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + self.assertEqual(0, storage.next_variable_id()) + v1 = storage.add_variable(-1.0, 2.5, True, "x") + v2 = storage.add_variable(-math.inf, math.inf, False, "") + self.assertEqual("test_model", storage.name) + + self.assertEqual(-1.0, storage.get_variable_lb(v1)) + self.assertEqual(2.5, storage.get_variable_ub(v1)) + self.assertTrue(storage.get_variable_is_integer(v1)) + self.assertEqual("x", storage.get_variable_name(v1)) + self.assertEqual(0, v1) + self.assertTrue(storage.variable_exists(v1)) + + self.assertEqual(-math.inf, storage.get_variable_lb(v2)) + self.assertEqual(math.inf, storage.get_variable_ub(v2)) + self.assertFalse(storage.get_variable_is_integer(v2)) + self.assertEqual("", storage.get_variable_name(v2)) + self.assertEqual(1, v2) + self.assertTrue(storage.variable_exists(v2)) + + self.assertFalse(storage.variable_exists(max(v1, v2) + 1)) + self.assertListEqual([v1, v2], list(storage.get_variables())) + self.assertEqual(2, storage.next_variable_id()) + + def test_set_variable_lb(self, storage_class: _StorageClass) -> None: + storage = storage_class() + v1 = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_variable_lb(v1, -5.5) + self.assertEqual(-5.5, storage.get_variable_lb(v1)) + + def test_set_variable_ub(self, storage_class: _StorageClass) -> None: + storage = storage_class() + v1 = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_variable_ub(v1, 1.2) + self.assertEqual(1.2, storage.get_variable_ub(v1)) + + def test_set_variable_is_integer(self, storage_class: _StorageClass) -> None: + storage = storage_class() + v1 = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_variable_is_integer(v1, False) + self.assertFalse(storage.get_variable_is_integer(v1)) + + def test_add_and_read_linear_constraints( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + self.assertEqual(0, storage.next_linear_constraint_id()) + c1 = storage.add_linear_constraint(-1.0, 2.5, "c") + c2 = storage.add_linear_constraint(-math.inf, math.inf, "") + + self.assertEqual(-1.0, storage.get_linear_constraint_lb(c1)) + self.assertEqual(2.5, storage.get_linear_constraint_ub(c1)) + self.assertEqual("c", storage.get_linear_constraint_name(c1)) + self.assertEqual(0, c1) + self.assertTrue(storage.linear_constraint_exists(c1)) + + self.assertEqual(-math.inf, storage.get_linear_constraint_lb(c2)) + self.assertEqual(math.inf, storage.get_linear_constraint_ub(c2)) + self.assertEqual("", storage.get_linear_constraint_name(c2)) + self.assertEqual(1, c2) + self.assertTrue(storage.linear_constraint_exists(c2)) + + self.assertListEqual([c1, c2], list(storage.get_linear_constraints())) + self.assertFalse(storage.linear_constraint_exists(1 + max(c1, c2))) + self.assertEqual(2, storage.next_linear_constraint_id()) + + def test_set_linear_constraint_lb(self, storage_class: _StorageClass) -> None: + storage = storage_class() + c1 = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_lb(c1, -5.5) + self.assertEqual(-5.5, storage.get_linear_constraint_lb(c1)) + + def test_set_linear_constraint_ub(self, storage_class: _StorageClass) -> None: + storage = storage_class() + c1 = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_ub(c1, 1.2) + self.assertEqual(1.2, storage.get_linear_constraint_ub(c1)) + + def test_delete_variable_get_other( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + v1 = storage.add_variable(-1.0, 2.5, True, "x") + v2 = storage.add_variable(-3.0, 4.5, False, "y") + storage.delete_variable(v1) + self.assertEqual(-3.0, storage.get_variable_lb(v2)) + self.assertEqual(4.5, storage.get_variable_ub(v2)) + self.assertFalse(storage.get_variable_is_integer(v2)) + self.assertEqual("y", storage.get_variable_name(v2)) + self.assertEqual(1, v2) + self.assertFalse(storage.variable_exists(v1)) + self.assertTrue(storage.variable_exists(v2)) + + self.assertListEqual([v2], list(storage.get_variables())) + + def test_double_variable_delete(self, storage_class: _StorageClass) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.delete_variable(x) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.delete_variable(x) + self.assertEqual(x, cm.exception.id) + + def _deleted_variable_invoke_lookup( + self, + storage_class: _StorageClass, + getter: Callable[[model_storage.ModelStorage, int], Any], + ) -> None: + storage = storage_class() + v1 = storage.add_variable(-1.0, 2.5, True, "x") + storage.delete_variable(v1) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + getter(storage, v1) + self.assertEqual(v1, cm.exception.id) + + def test_delete_variable_lb_error(self, storage_class: _StorageClass) -> None: + self._deleted_variable_invoke_lookup( + storage_class, storage_class.get_variable_lb + ) + + def test_delete_variable_ub_error(self, storage_class: _StorageClass) -> None: + self._deleted_variable_invoke_lookup( + storage_class, storage_class.get_variable_ub + ) + + def test_delete_variable_is_integer_error( + self, storage_class: _StorageClass + ) -> None: + self._deleted_variable_invoke_lookup( + storage_class, storage_class.get_variable_is_integer + ) + + def test_delete_variable_name_error( + self, storage_class: _StorageClass + ) -> None: + self._deleted_variable_invoke_lookup( + storage_class, storage_class.get_variable_name + ) + + def test_delete_variable_set_lb_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.delete_variable(x) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.set_variable_lb(x, -2.0) + self.assertEqual(x, cm.exception.id) + + def test_delete_variable_set_ub_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.delete_variable(x) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.set_variable_ub(x, 12.0) + self.assertEqual(x, cm.exception.id) + + def test_delete_variable_set_integer_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.delete_variable(x) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.set_variable_is_integer(x, False) + self.assertEqual(x, cm.exception.id) + + def test_delete_linear_constraint_get_other( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + c1 = storage.add_linear_constraint(-1.0, 2.5, "c1") + c2 = storage.add_linear_constraint(-math.inf, 5.0, "c2") + storage.delete_linear_constraint(c1) + self.assertEqual(-math.inf, storage.get_linear_constraint_lb(c2)) + self.assertEqual(5.0, storage.get_linear_constraint_ub(c2)) + self.assertEqual("c2", storage.get_linear_constraint_name(c2)) + self.assertEqual(1, c2) + + self.assertListEqual([c2], list(storage.get_linear_constraints())) + + def test_double_linear_constraint_delete( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.delete_linear_constraint(c) + with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: + storage.delete_linear_constraint(c) + self.assertEqual(c, cm.exception.id) + + def _deleted_linear_constraint_invoke_lookup( + self, + storage_class: _StorageClass, + getter: Callable[[model_storage.ModelStorage, int], Any], + ) -> None: + storage = storage_class() + c1 = storage.add_linear_constraint(-1.0, 2.5, "c1") + storage.delete_linear_constraint(c1) + with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: + getter(storage, c1) + self.assertEqual(c1, cm.exception.id) + + def test_delete_linear_constraint_lb_error( + self, storage_class: _StorageClass + ) -> None: + self._deleted_linear_constraint_invoke_lookup( + storage_class, storage_class.get_linear_constraint_lb + ) + + def test_delete_linear_constraint_ub_error( + self, storage_class: _StorageClass + ) -> None: + self._deleted_linear_constraint_invoke_lookup( + storage_class, storage_class.get_linear_constraint_ub + ) + + def test_delete_linear_constraint_name_error( + self, storage_class: _StorageClass + ) -> None: + self._deleted_linear_constraint_invoke_lookup( + storage_class, storage_class.get_linear_constraint_name + ) + + def test_delete_linear_constraint_set_lb_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.delete_linear_constraint(c) + with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: + storage.set_linear_constraint_lb(c, -2.0) + self.assertEqual(c, cm.exception.id) + + def test_delete_linear_constraint_set_ub_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.delete_linear_constraint(c) + with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: + storage.set_linear_constraint_ub(c, 12.0) + self.assertEqual(c, cm.exception.id) + + def test_objective_offset(self, storage_class: _StorageClass) -> None: + storage = storage_class() + self.assertEqual(0.0, storage.get_objective_offset()) + storage.set_objective_offset(1.5) + self.assertEqual(1.5, storage.get_objective_offset()) + + def test_objective_direction(self, storage_class: _StorageClass) -> None: + storage = storage_class() + self.assertFalse(storage.get_is_maximize()) + storage.set_is_maximize(True) + self.assertTrue(storage.get_is_maximize()) + storage.set_is_maximize(False) + self.assertFalse(storage.get_is_maximize()) + + def test_set_linear_objective_coefficient( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(0.0, 1.0, False, "y") + z = storage.add_variable(0.0, 1.0, True, "z") + storage.set_linear_objective_coefficient(x, 2.0) + storage.set_linear_objective_coefficient(z, -5.5) + self.assertEqual(2.0, storage.get_linear_objective_coefficient(x)) + self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) + self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z)) + + self.assertCountEqual( + [ + _ObjEntry(variable_id=x, coefficient=2.0), + _ObjEntry(variable_id=z, coefficient=-5.5), + ], + storage.get_linear_objective_coefficients(), + ) + + def test_clear_linear_objective_coefficient( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(0.0, 1.0, False, "y") + z = storage.add_variable(0.0, 1.0, True, "z") + storage.set_linear_objective_coefficient(x, 2.0) + storage.set_linear_objective_coefficient(z, -5.5) + storage.set_objective_offset(1.0) + self.assertEqual(2.0, storage.get_linear_objective_coefficient(x)) + self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) + self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z)) + self.assertEqual(1.0, storage.get_objective_offset()) + storage.clear_objective() + self.assertEqual(0.0, storage.get_linear_objective_coefficient(x)) + self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) + self.assertEqual(0.0, storage.get_linear_objective_coefficient(z)) + self.assertEqual(0.0, storage.get_objective_offset()) + + def test_set_linear_objective_coefficient_bad_id( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.set_linear_objective_coefficient(x + 1, 2.0) + self.assertEqual(x + 1, cm.exception.id) + + def test_set_linear_objective_coefficient_deleted_id( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_linear_objective_coefficient(y, 3.0) + storage.delete_variable(x) + self.assertEqual(3.0, storage.get_linear_objective_coefficient(y)) + self.assertCountEqual( + [model_storage.LinearObjectiveEntry(variable_id=y, coefficient=3.0)], + storage.get_linear_objective_coefficients(), + ) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.set_linear_objective_coefficient(x, 2.0) + self.assertEqual(x, cm.exception.id) + + def test_get_linear_objective_coefficient_deleted_nonzero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_linear_objective_coefficient(x, 1.0) + storage.set_linear_objective_coefficient(y, 3.0) + storage.delete_variable(x) + self.assertEqual(3.0, storage.get_linear_objective_coefficient(y)) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.get_linear_objective_coefficient(x) + self.assertEqual(x, cm.exception.id) + + def test_set_quadratic_objective_coefficient( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(0.0, 1.0, False, "y") + z = storage.add_variable(0.0, 1.0, True, "z") + storage.set_quadratic_objective_coefficient(x, y, 2.0) + storage.set_quadratic_objective_coefficient(z, z, -5.5) + storage.set_quadratic_objective_coefficient(z, y, 1.5) + self.assertEqual(2.0, storage.get_quadratic_objective_coefficient(x, y)) + self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, y)) + self.assertEqual(-5.5, storage.get_quadratic_objective_coefficient(z, z)) + self.assertEqual(1.5, storage.get_quadratic_objective_coefficient(y, z)) + + self.assertCountEqual( + [ + model_storage.QuadraticEntry( + id_key=model_storage.QuadraticTermIdKey(x, y), coefficient=2.0 ), - linear_constraints=model_pb2.LinearConstraintsProto( - ids=[0, 1], - lower_bounds=[-math.inf, 0.0], - upper_bounds=[3.0, 1.0], - names=["", "d"], + model_storage.QuadraticEntry( + id_key=model_storage.QuadraticTermIdKey(z, z), coefficient=-5.5 ), - objective=model_pb2.ObjectiveProto( - maximize=True, - offset=7.0, - linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 2], values=[2.5, -1.0] - ), - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0, 0, 0], - column_ids=[0, 1, 2], - coefficients=[3.0, 4.0, 5.0], - ), + model_storage.QuadraticEntry( + id_key=model_storage.QuadraticTermIdKey(y, z), coefficient=1.5 ), - linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0, 1, 1], - column_ids=[1, 0, 1], - coefficients=[1.0, 2.0, -1.0], + ], + storage.get_quadratic_objective_coefficients(), + ) + + self.assertCountEqual( + [y], storage.get_quadratic_objective_adjacent_variables(x) + ) + self.assertCountEqual( + [x, z], storage.get_quadratic_objective_adjacent_variables(y) + ) + self.assertCountEqual( + [y, z], storage.get_quadratic_objective_adjacent_variables(z) + ) + + def test_clear_quadratic_objective_coefficient( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(0.0, 1.0, False, "y") + z = storage.add_variable(0.0, 1.0, True, "z") + storage.set_linear_objective_coefficient(x, 2.0) + storage.set_linear_objective_coefficient(z, -5.5) + storage.set_quadratic_objective_coefficient(x, y, 2.0) + storage.set_quadratic_objective_coefficient(z, z, -5.5) + storage.set_quadratic_objective_coefficient(z, y, 1.5) + storage.set_objective_offset(1.0) + storage.clear_objective() + self.assertEqual(0.0, storage.get_linear_objective_coefficient(x)) + self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) + self.assertEqual(0.0, storage.get_linear_objective_coefficient(z)) + self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, y)) + self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, y)) + self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(z, z)) + self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, z)) + self.assertEqual(0.0, storage.get_objective_offset()) + self.assertEmpty( + list(storage.get_quadratic_objective_adjacent_variables(x)) + ) + self.assertEmpty( + list(storage.get_quadratic_objective_adjacent_variables(y)) + ) + self.assertEmpty( + list(storage.get_quadratic_objective_adjacent_variables(z)) + ) + + def test_set_quadratic_objective_coefficient_bad_id( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.set_quadratic_objective_coefficient(x, x + 1, 2.0) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.set_quadratic_objective_coefficient(x + 1, x, 2.0) + self.assertEqual(x + 1, cm.exception.id) + + def test_get_quadratic_objective_coefficient_bad_id( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.get_quadratic_objective_coefficient(x, x + 1) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.get_quadratic_objective_coefficient(x + 1, x) + self.assertEqual(x + 1, cm.exception.id) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + list(storage.get_quadratic_objective_adjacent_variables(x + 1)) + self.assertEqual(x + 1, cm.exception.id) + + def test_set_quadratic_objective_coefficient_existing_to_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, x, -1.0) + storage.set_quadratic_objective_coefficient(x, y, 1.0) + storage.set_quadratic_objective_coefficient(y, y, 3.0) + + storage.set_quadratic_objective_coefficient(x, x, 0.0) + storage.set_quadratic_objective_coefficient(x, y, 0.0) + self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, x)) + self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, y)) + self.assertEqual(3.0, storage.get_quadratic_objective_coefficient(y, y)) + self.assertCountEqual( + [y], storage.get_quadratic_objective_adjacent_variables(y) + ) + self.assertEmpty( + list(storage.get_quadratic_objective_adjacent_variables(x)) + ) + + def test_set_quadratic_objective_coefficient_deleted_id( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 1.0) + storage.set_quadratic_objective_coefficient(y, y, 3.0) + storage.delete_variable(x) + self.assertEqual(3.0, storage.get_quadratic_objective_coefficient(y, y)) + self.assertCountEqual( + [y], storage.get_quadratic_objective_adjacent_variables(y) + ) + + def test_set_quadratic_objective_coefficient_deleted_id_get_coeff_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 1.0) + storage.set_quadratic_objective_coefficient(y, y, 3.0) + storage.delete_variable(x) + + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.get_quadratic_objective_coefficient(x, y) + self.assertEqual(x, cm.exception.id) + + def test_set_quadratic_objective_coefficient_deleted_id_set_coeff_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 1.0) + storage.set_quadratic_objective_coefficient(y, y, 3.0) + storage.delete_variable(x) + + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.set_quadratic_objective_coefficient(x, y, 1.0) + self.assertEqual(x, cm.exception.id) + + def test_set_quadratic_objective_coefficient_deleted_id_adjacent_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 1.0) + storage.set_quadratic_objective_coefficient(y, y, 3.0) + storage.delete_variable(x) + + with self.assertRaises(model_storage.BadVariableIdError) as cm: + list(storage.get_quadratic_objective_adjacent_variables(x)) + self.assertEqual(x, cm.exception.id) + + def test_constraint_matrix(self, storage_class: _StorageClass) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, False, "y") + z = storage.add_variable(0.0, 1.0, True, "z") + c = storage.add_linear_constraint(-math.inf, 3.0, "c") + d = storage.add_linear_constraint(-math.inf, 1.0, "d") + storage.set_linear_constraint_coefficient(c, y, 1.0) + storage.set_linear_constraint_coefficient(d, x, 2.0) + storage.set_linear_constraint_coefficient(d, y, -1.0) + storage.set_linear_constraint_coefficient(d, z, 1.0) + storage.set_linear_constraint_coefficient(d, z, 0.0) + + self.assertEqual(0.0, storage.get_linear_constraint_coefficient(c, x)) + self.assertEqual(1.0, storage.get_linear_constraint_coefficient(c, y)) + self.assertEqual(0.0, storage.get_linear_constraint_coefficient(c, z)) + + self.assertEqual(2.0, storage.get_linear_constraint_coefficient(d, x)) + self.assertEqual(-1.0, storage.get_linear_constraint_coefficient(d, y)) + self.assertEqual(0.0, storage.get_linear_constraint_coefficient(d, z)) + + self.assertCountEqual([y], storage.get_variables_for_linear_constraint(c)) + self.assertCountEqual( + [x, y], storage.get_variables_for_linear_constraint(d) + ) + + self.assertCountEqual([d], storage.get_linear_constraints_with_variable(x)) + self.assertCountEqual( + [c, d], storage.get_linear_constraints_with_variable(y) + ) + self.assertCountEqual([], storage.get_linear_constraints_with_variable(z)) + + self.assertCountEqual( + [ + _MatEntry(linear_constraint_id=c, variable_id=y, coefficient=1.0), + _MatEntry(linear_constraint_id=d, variable_id=x, coefficient=2.0), + _MatEntry(linear_constraint_id=d, variable_id=y, coefficient=-1.0), + ], + storage.get_linear_constraint_matrix_entries(), + ) + + def test_constraint_matrix_zero_unset_entry( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-math.inf, 3.0, "c") + storage.set_linear_constraint_coefficient(c, x, 0.0) + self.assertEmpty(list(storage.get_linear_objective_coefficients())) + self.assertEmpty(list(storage.get_variables_for_linear_constraint(c))) + self.assertEmpty(list(storage.get_linear_constraints_with_variable(x))) + self.assertEqual(0.0, storage.get_linear_constraint_coefficient(c, x)) + + def test_constraint_matrix_with_deletion( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, False, "y") + z = storage.add_variable(0.0, 1.0, True, "z") + c = storage.add_linear_constraint(-math.inf, 3.0, "c") + d = storage.add_linear_constraint(-math.inf, 1.0, "d") + storage.set_linear_constraint_coefficient(c, y, 1.0) + storage.set_linear_constraint_coefficient(d, x, 2.0) + storage.set_linear_constraint_coefficient(d, y, -1.0) + storage.set_linear_constraint_coefficient(c, z, 1.0) + + storage.delete_variable(y) + storage.delete_linear_constraint(c) + + self.assertEqual(2.0, storage.get_linear_constraint_coefficient(d, x)) + self.assertEqual(0.0, storage.get_linear_constraint_coefficient(d, z)) + + self.assertCountEqual([x], storage.get_variables_for_linear_constraint(d)) + + self.assertCountEqual([d], storage.get_linear_constraints_with_variable(x)) + self.assertCountEqual([], storage.get_linear_constraints_with_variable(z)) + + self.assertCountEqual( + [_MatEntry(linear_constraint_id=d, variable_id=x, coefficient=2.0)], + storage.get_linear_constraint_matrix_entries(), + ) + + def test_variables_for_linear_constraint_deleted_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-math.inf, 3.0, "c") + storage.set_linear_constraint_coefficient(c, x, 1.0) + storage.delete_linear_constraint(c) + with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: + list(storage.get_variables_for_linear_constraint(c)) + self.assertEqual(c, cm.exception.id) + + def test_linear_constraints_with_variable_deleted_error( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-math.inf, 3.0, "c") + storage.set_linear_constraint_coefficient(c, x, 1.0) + storage.delete_variable(x) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + list(storage.get_linear_constraints_with_variable(x)) + self.assertEqual(x, cm.exception.id) + + def test_constraint_matrix_set_deleted_var( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-math.inf, 3.0, "c") + storage.delete_variable(x) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.set_linear_constraint_coefficient(c, x, 2.0) + self.assertEqual(x, cm.exception.id) + + def test_constraint_matrix_get_deleted_var( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-math.inf, 3.0, "c") + storage.delete_variable(x) + with self.assertRaises(model_storage.BadVariableIdError) as cm: + storage.get_linear_constraint_coefficient(c, x) + self.assertEqual(x, cm.exception.id) + + def test_constraint_matrix_set_deleted_constraint( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-math.inf, 3.0, "c") + storage.delete_linear_constraint(c) + with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: + storage.set_linear_constraint_coefficient(c, x, 2.0) + self.assertEqual(c, cm.exception.id) + + def test_constraint_matrix_get_deleted_constraint( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-math.inf, 3.0, "c") + storage.delete_linear_constraint(c) + with self.assertRaises(model_storage.BadLinearConstraintIdError) as cm: + storage.get_linear_constraint_coefficient(c, x) + self.assertEqual(c, cm.exception.id) + + def test_proto_export(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, False, "") + z = storage.add_variable(0.0, 1.0, True, "z") + c = storage.add_linear_constraint(-math.inf, 3.0, "") + d = storage.add_linear_constraint(0.0, 1.0, "d") + storage.set_linear_constraint_coefficient(c, y, 1.0) + storage.set_linear_constraint_coefficient(d, x, 2.0) + storage.set_linear_constraint_coefficient(d, y, -1.0) + storage.set_linear_constraint_coefficient(d, z, 1.0) + storage.set_linear_constraint_coefficient(d, z, 0.0) + storage.set_linear_objective_coefficient(x, 2.5) + storage.set_linear_objective_coefficient(z, -1.0) + storage.set_quadratic_objective_coefficient(x, x, 3.0) + storage.set_quadratic_objective_coefficient(x, y, 4.0) + storage.set_quadratic_objective_coefficient(x, z, 5.0) + storage.set_is_maximize(True) + storage.set_objective_offset(7.0) + + expected = model_pb2.ModelProto( + name="test_model", + variables=model_pb2.VariablesProto( + ids=[0, 1, 2], + lower_bounds=[-1.0, -1.0, 0.0], + upper_bounds=[2.5, 2.5, 1.0], + integers=[True, False, True], + names=["x", "", "z"], + ), + linear_constraints=model_pb2.LinearConstraintsProto( + ids=[0, 1], + lower_bounds=[-math.inf, 0.0], + upper_bounds=[3.0, 1.0], + names=["", "d"], + ), + objective=model_pb2.ObjectiveProto( + maximize=True, + offset=7.0, + linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 2], values=[2.5, -1.0] ), - ) - self.assert_protos_equiv(expected, storage.export_model()) - - def test_proto_export_with_deletes(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, False, "") - z = storage.add_variable(0.0, 1.0, True, "z") - c = storage.add_linear_constraint(-math.inf, 3.0, "") - d = storage.add_linear_constraint(0.0, 1.0, "d") - storage.set_linear_constraint_coefficient(c, y, 1.0) - storage.set_linear_constraint_coefficient(d, x, 2.0) - storage.set_linear_constraint_coefficient(d, y, -1.0) - storage.set_linear_constraint_coefficient(d, z, 1.0) - storage.set_linear_constraint_coefficient(d, z, 0.0) - storage.set_linear_objective_coefficient(x, 2.5) - storage.set_quadratic_objective_coefficient(x, x, 3.0) - storage.set_quadratic_objective_coefficient(x, y, 4.0) - storage.set_quadratic_objective_coefficient(x, z, 5.0) - storage.set_is_maximize(False) - storage.delete_variable(y) - storage.delete_linear_constraint(c) - - expected = model_pb2.ModelProto( - name="test_model", - variables=model_pb2.VariablesProto( - ids=[0, 2], - lower_bounds=[-1.0, 0.0], - upper_bounds=[2.5, 1.0], - integers=[True, True], - names=["x", "z"], + quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[0, 0, 0], + column_ids=[0, 1, 2], + coefficients=[3.0, 4.0, 5.0], ), - linear_constraints=model_pb2.LinearConstraintsProto( - ids=[1], lower_bounds=[0.0], upper_bounds=[1.0], names=["d"] + ), + linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[0, 1, 1], + column_ids=[1, 0, 1], + coefficients=[1.0, 2.0, -1.0], + ), + ) + self.assert_protos_equiv(expected, storage.export_model()) + + def test_proto_export_with_deletes( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, False, "") + z = storage.add_variable(0.0, 1.0, True, "z") + c = storage.add_linear_constraint(-math.inf, 3.0, "") + d = storage.add_linear_constraint(0.0, 1.0, "d") + storage.set_linear_constraint_coefficient(c, y, 1.0) + storage.set_linear_constraint_coefficient(d, x, 2.0) + storage.set_linear_constraint_coefficient(d, y, -1.0) + storage.set_linear_constraint_coefficient(d, z, 1.0) + storage.set_linear_constraint_coefficient(d, z, 0.0) + storage.set_linear_objective_coefficient(x, 2.5) + storage.set_quadratic_objective_coefficient(x, x, 3.0) + storage.set_quadratic_objective_coefficient(x, y, 4.0) + storage.set_quadratic_objective_coefficient(x, z, 5.0) + storage.set_is_maximize(False) + storage.delete_variable(y) + storage.delete_linear_constraint(c) + + expected = model_pb2.ModelProto( + name="test_model", + variables=model_pb2.VariablesProto( + ids=[0, 2], + lower_bounds=[-1.0, 0.0], + upper_bounds=[2.5, 1.0], + integers=[True, True], + names=["x", "z"], + ), + linear_constraints=model_pb2.LinearConstraintsProto( + ids=[1], lower_bounds=[0.0], upper_bounds=[1.0], names=["d"] + ), + objective=model_pb2.ObjectiveProto( + maximize=False, + offset=0.0, + linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[2.5] ), - objective=model_pb2.ObjectiveProto( - maximize=False, - offset=0.0, - linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[2.5] - ), - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0, 0], column_ids=[0, 2], coefficients=[3.0, 5.0] - ), + quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[0, 0], column_ids=[0, 2], coefficients=[3.0, 5.0] ), - linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[1], column_ids=[0], coefficients=[2.0] + ), + linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[1], column_ids=[0], coefficients=[2.0] + ), + ) + self.assert_protos_equiv(expected, storage.export_model()) + + def test_proto_export_empty(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + expected = model_pb2.ModelProto(name="test_model") + self.assert_protos_equiv(expected, storage.export_model()) + + def test_proto_export_feasibility(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + storage.add_variable(-1.0, 2.5, True, "x") + expected = model_pb2.ModelProto( + name="test_model", + variables=model_pb2.VariablesProto( + ids=[0], + lower_bounds=[-1.0], + upper_bounds=[2.5], + integers=[True], + names=["x"], + ), + ) + self.assert_protos_equiv(expected, storage.export_model()) + + def test_proto_export_empty_names(self, storage_class: _StorageClass) -> None: + storage = storage_class("") + storage.add_variable(-1.0, 2.5, True, "") + storage.add_linear_constraint(0.0, 1.0, "") + expected = model_pb2.ModelProto( + variables=model_pb2.VariablesProto( + ids=[0], + lower_bounds=[-1.0], + upper_bounds=[2.5], + integers=[True], + # NOTE: names is the empty list not a list with an empty string. + names=[], + ), + linear_constraints=model_pb2.LinearConstraintsProto( + ids=[0], + lower_bounds=[0.0], + upper_bounds=[1.0], + # NOTE: names is the empty list not a list with an empty string. + names=[], + ), + ) + self.assert_protos_equiv(expected, storage.export_model()) + + def _assert_nan(self, x): + self.assertTrue(math.isnan(x), f"Expected nan, found {x}") + + # Ensure that we don't silently drop NaNs. + def test_nans_pass_through(self, storage_class: _StorageClass) -> None: + storage = storage_class("nan_model") + nan = math.nan + x = storage.add_variable(nan, 2.5, True, "x") + y = storage.add_variable(-1.0, nan, True, "y") + c = storage.add_linear_constraint(nan, math.inf, "c") + d = storage.add_linear_constraint(0.0, nan, "d") + storage.set_objective_offset(nan) + storage.set_linear_objective_coefficient(x, 1.0) + storage.set_linear_objective_coefficient(y, nan) + storage.set_quadratic_objective_coefficient(x, x, 3.0) + storage.set_quadratic_objective_coefficient(x, y, nan) + storage.set_linear_constraint_coefficient(c, x, nan) + storage.set_linear_constraint_coefficient(c, y, 1.0) + storage.set_linear_constraint_coefficient(d, y, nan) + + # Test the getters. + self.assertEqual("nan_model", storage.name) + self._assert_nan(storage.get_objective_offset()) + self._assert_nan(storage.get_variable_lb(x)) + self.assertEqual(2.5, storage.get_variable_ub(x)) + self.assertEqual(-1.0, storage.get_variable_lb(y)) + self._assert_nan(storage.get_variable_ub(y)) + self.assertEqual(1.0, storage.get_linear_objective_coefficient(x)) + self._assert_nan(storage.get_linear_objective_coefficient(y)) + self._assert_nan(storage.get_linear_constraint_lb(c)) + self.assertEqual(math.inf, storage.get_linear_constraint_ub(c)) + self.assertEqual(0.0, storage.get_linear_constraint_lb(d)) + self._assert_nan(storage.get_linear_constraint_ub(d)) + self._assert_nan(storage.get_linear_constraint_coefficient(c, x)) + self.assertEqual(1.0, storage.get_linear_constraint_coefficient(c, y)) + self.assertEqual(0.0, storage.get_linear_constraint_coefficient(d, x)) + self.assertEqual(3.0, storage.get_quadratic_objective_coefficient(x, x)) + self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, y)) + self._assert_nan(storage.get_quadratic_objective_coefficient(x, y)) + self._assert_nan(storage.get_linear_constraint_coefficient(d, y)) + + # Test the iterators that interact with the NaN values. + self.assertCountEqual( + [x, y], storage.get_variables_for_linear_constraint(c) + ) + self.assertCountEqual([y], storage.get_variables_for_linear_constraint(d)) + + self.assertCountEqual([c], storage.get_linear_constraints_with_variable(x)) + self.assertCountEqual( + [c, d], storage.get_linear_constraints_with_variable(y) + ) + + mat_entries = {} + for e in storage.get_linear_constraint_matrix_entries(): + key = (e.linear_constraint_id, e.variable_id) + self.assertNotIn( + key, + mat_entries, + msg=f"found key:{key} twice, e:{e} mat_entries:{mat_entries}", + ) + mat_entries[key] = e.coefficient + self.assertSetEqual(set(mat_entries.keys()), set(((c, x), (c, y), (d, y)))) + self._assert_nan(mat_entries[(c, x)]) + self.assertEqual(mat_entries[(c, y)], 1.0) + self._assert_nan(mat_entries[(d, y)]) + + obj_entries = {} + for e in storage.get_linear_objective_coefficients(): + self.assertNotIn( + e.variable_id, + obj_entries, + msg=( + f"found variable:{e.variable_id} twice," + f" e:{e} obj_entries:{obj_entries}" + ), + ) + obj_entries[e.variable_id] = e.coefficient + self.assertSetEqual(set(obj_entries.keys()), set((x, y))) + self.assertEqual(obj_entries[x], 1.0) + self._assert_nan(obj_entries[y]) + + # Export to proto + expected = model_pb2.ModelProto( + name="nan_model", + variables=model_pb2.VariablesProto( + ids=[0, 1], + lower_bounds=[nan, -1.0], + upper_bounds=[2.5, nan], + integers=[True, True], + names=["x", "y"], + ), + linear_constraints=model_pb2.LinearConstraintsProto( + ids=[0, 1], + lower_bounds=[nan, 0.0], + upper_bounds=[math.inf, nan], + names=["c", "d"], + ), + objective=model_pb2.ObjectiveProto( + maximize=False, + offset=nan, + linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1], values=[1.0, nan] ), - ) - self.assert_protos_equiv(expected, storage.export_model()) - - def test_proto_export_empty(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - expected = model_pb2.ModelProto(name="test_model") - self.assert_protos_equiv(expected, storage.export_model()) - - def test_proto_export_feasibility(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - storage.add_variable(-1.0, 2.5, True, "x") - expected = model_pb2.ModelProto( - name="test_model", - variables=model_pb2.VariablesProto( - ids=[0], - lower_bounds=[-1.0], - upper_bounds=[2.5], - integers=[True], - names=["x"], + quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[0, 0], column_ids=[0, 1], coefficients=[3.0, nan] ), - ) - self.assert_protos_equiv(expected, storage.export_model()) - - def test_proto_export_empty_names(self, storage_class: _StorageClass) -> None: - storage = storage_class("") - storage.add_variable(-1.0, 2.5, True, "") - storage.add_linear_constraint(0.0, 1.0, "") - expected = model_pb2.ModelProto( - variables=model_pb2.VariablesProto( - ids=[0], - lower_bounds=[-1.0], - upper_bounds=[2.5], - integers=[True], - # NOTE: names is the empty list not a list with an empty string. - names=[], - ), - linear_constraints=model_pb2.LinearConstraintsProto( - ids=[0], - lower_bounds=[0.0], - upper_bounds=[1.0], - # NOTE: names is the empty list not a list with an empty string. - names=[], - ), - ) - self.assert_protos_equiv(expected, storage.export_model()) - - def _assert_nan(self, x): - self.assertTrue(math.isnan(x), f"Expected nan, found {x}") - - # Ensure that we don't silently drop NaNs. - def test_nans_pass_through(self, storage_class: _StorageClass) -> None: - storage = storage_class("nan_model") - nan = math.nan - x = storage.add_variable(nan, 2.5, True, "x") - y = storage.add_variable(-1.0, nan, True, "y") - c = storage.add_linear_constraint(nan, math.inf, "c") - d = storage.add_linear_constraint(0.0, nan, "d") - storage.set_objective_offset(nan) - storage.set_linear_objective_coefficient(x, 1.0) - storage.set_linear_objective_coefficient(y, nan) - storage.set_quadratic_objective_coefficient(x, x, 3.0) - storage.set_quadratic_objective_coefficient(x, y, nan) - storage.set_linear_constraint_coefficient(c, x, nan) - storage.set_linear_constraint_coefficient(c, y, 1.0) - storage.set_linear_constraint_coefficient(d, y, nan) - - # Test the getters. - self.assertEqual("nan_model", storage.name) - self._assert_nan(storage.get_objective_offset()) - self._assert_nan(storage.get_variable_lb(x)) - self.assertEqual(2.5, storage.get_variable_ub(x)) - self.assertEqual(-1.0, storage.get_variable_lb(y)) - self._assert_nan(storage.get_variable_ub(y)) - self.assertEqual(1.0, storage.get_linear_objective_coefficient(x)) - self._assert_nan(storage.get_linear_objective_coefficient(y)) - self._assert_nan(storage.get_linear_constraint_lb(c)) - self.assertEqual(math.inf, storage.get_linear_constraint_ub(c)) - self.assertEqual(0.0, storage.get_linear_constraint_lb(d)) - self._assert_nan(storage.get_linear_constraint_ub(d)) - self._assert_nan(storage.get_linear_constraint_coefficient(c, x)) - self.assertEqual(1.0, storage.get_linear_constraint_coefficient(c, y)) - self.assertEqual(0.0, storage.get_linear_constraint_coefficient(d, x)) - self.assertEqual(3.0, storage.get_quadratic_objective_coefficient(x, x)) - self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(y, y)) - self._assert_nan(storage.get_quadratic_objective_coefficient(x, y)) - self._assert_nan(storage.get_linear_constraint_coefficient(d, y)) - - # Test the iterators that interact with the NaN values. - self.assertCountEqual([x, y], storage.get_variables_for_linear_constraint(c)) - self.assertCountEqual([y], storage.get_variables_for_linear_constraint(d)) - - self.assertCountEqual([c], storage.get_linear_constraints_with_variable(x)) - self.assertCountEqual([c, d], storage.get_linear_constraints_with_variable(y)) - - mat_entries = {} - for e in storage.get_linear_constraint_matrix_entries(): - key = (e.linear_constraint_id, e.variable_id) - self.assertNotIn( - key, - mat_entries, - msg=f"found key:{key} twice, e:{e} mat_entries:{mat_entries}", - ) - mat_entries[key] = e.coefficient - self.assertSetEqual(set(mat_entries.keys()), set(((c, x), (c, y), (d, y)))) - self._assert_nan(mat_entries[(c, x)]) - self.assertEqual(mat_entries[(c, y)], 1.0) - self._assert_nan(mat_entries[(d, y)]) - - obj_entries = {} - for e in storage.get_linear_objective_coefficients(): - self.assertNotIn( - e.variable_id, - obj_entries, - msg=( - f"found variable:{e.variable_id} twice," - f" e:{e} obj_entries:{obj_entries}" - ), - ) - obj_entries[e.variable_id] = e.coefficient - self.assertSetEqual(set(obj_entries.keys()), set((x, y))) - self.assertEqual(obj_entries[x], 1.0) - self._assert_nan(obj_entries[y]) - - # Export to proto - expected = model_pb2.ModelProto( - name="nan_model", - variables=model_pb2.VariablesProto( - ids=[0, 1], - lower_bounds=[nan, -1.0], - upper_bounds=[2.5, nan], - integers=[True, True], - names=["x", "y"], - ), - linear_constraints=model_pb2.LinearConstraintsProto( - ids=[0, 1], - lower_bounds=[nan, 0.0], - upper_bounds=[math.inf, nan], - names=["c", "d"], - ), - objective=model_pb2.ObjectiveProto( - maximize=False, - offset=nan, - linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], values=[1.0, nan] - ), - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0, 0], column_ids=[0, 1], coefficients=[3.0, nan] - ), - ), - linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0, 0, 1], - column_ids=[0, 1, 1], - coefficients=[nan, 1.0, nan], - ), - ) - self.assert_protos_equiv(expected, storage.export_model()) + ), + linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[0, 0, 1], + column_ids=[0, 1, 1], + coefficients=[nan, 1.0, nan], + ), + ) + self.assert_protos_equiv(expected, storage.export_model()) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/model_storage_update_test.py b/ortools/math_opt/python/model_storage_update_test.py index 9419fc1e28d..02dec11f7e9 100644 --- a/ortools/math_opt/python/model_storage_update_test.py +++ b/ortools/math_opt/python/model_storage_update_test.py @@ -35,1141 +35,1175 @@ @parameterized.parameters((hash_model_storage.HashModelStorage,)) -class ModelStorageTest(compare_proto.MathOptProtoAssertions, parameterized.TestCase): - - def test_simple_delete_var(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.delete_variable(x) - self.assert_protos_equiv( - _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() +class ModelStorageTest( + compare_proto.MathOptProtoAssertions, parameterized.TestCase +): + + def test_simple_delete_var(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.delete_variable(x) + self.assert_protos_equiv( + _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() + ) + + def test_simple_delete_lin_con(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + tracker.advance_checkpoint() + storage.delete_linear_constraint(c) + self.assert_protos_equiv( + _ModelUpdateProto(deleted_linear_constraint_ids=[0]), + tracker.export_update(), + ) + + def test_update_var_lb(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.set_variable_lb(x, -7.0) + self.assert_protos_equiv( + _ModelUpdateProto( + variable_updates=_VariableUpdatesProto( + lower_bounds=_SparseDoubleVectorProto(ids=[0], values=[-7.0]) + ) + ), + tracker.export_update(), + ) + + def test_update_var_lb_same_value(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.set_variable_lb(x, -1.0) + self.assertIsNone(tracker.export_update()) + + def test_update_var_ub(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.set_variable_ub(x, 12.5) + self.assert_protos_equiv( + _ModelUpdateProto( + variable_updates=_VariableUpdatesProto( + upper_bounds=_SparseDoubleVectorProto(ids=[0], values=[12.5]) + ) + ), + tracker.export_update(), + ) + + def test_update_var_ub_same_value(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.set_variable_ub(x, 2.5) + self.assertIsNone(tracker.export_update()) + + def test_update_var_integer(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.set_variable_is_integer(x, False) + self.assert_protos_equiv( + _ModelUpdateProto( + variable_updates=_VariableUpdatesProto( + integers=_SparseBoolVectorProto(ids=[0], values=[False]) + ) + ), + tracker.export_update(), + ) + + def test_update_var_integer_same_value( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.set_variable_is_integer(x, True) + self.assertIsNone(tracker.export_update()) + + def test_update_var_then_delete(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.set_variable_lb(x, -3.0) + storage.set_variable_ub(x, 5.0) + storage.set_variable_is_integer(x, False) + storage.delete_variable(x) + self.assert_protos_equiv( + _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() + ) + + def test_update_lin_con_lb(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + tracker.advance_checkpoint() + storage.set_linear_constraint_lb(c, -7.0) + self.assert_protos_equiv( + _ModelUpdateProto( + linear_constraint_updates=_LinearConstraintUpdatesProto( + lower_bounds=_SparseDoubleVectorProto(ids=[0], values=[-7.0]) + ) + ), + tracker.export_update(), + ) + + def test_update_lin_con_lb_same_value( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + tracker.advance_checkpoint() + storage.set_linear_constraint_lb(c, -1.0) + self.assertIsNone(tracker.export_update()) + + def test_update_lin_con_ub(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + tracker.advance_checkpoint() + storage.set_linear_constraint_ub(c, 12.5) + self.assert_protos_equiv( + _ModelUpdateProto( + linear_constraint_updates=_LinearConstraintUpdatesProto( + upper_bounds=_SparseDoubleVectorProto(ids=[0], values=[12.5]) + ) + ), + tracker.export_update(), + ) + + def test_update_lin_con_ub_same_value( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + tracker.advance_checkpoint() + storage.set_linear_constraint_ub(c, 2.5) + self.assertIsNone(tracker.export_update()) + + def test_update_lin_con_then_delete( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + tracker.advance_checkpoint() + storage.set_linear_constraint_lb(c, -3.0) + storage.set_linear_constraint_ub(c, 5.0) + storage.delete_linear_constraint(c) + self.assert_protos_equiv( + _ModelUpdateProto(deleted_linear_constraint_ids=[0]), + tracker.export_update(), + ) + + def test_new_var(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + storage.add_variable(-1.0, 2.5, True, "x") + expected = _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[0], + lower_bounds=[-1.0], + upper_bounds=[2.5], + integers=[True], + names=["x"], ) - - def test_simple_delete_lin_con(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - tracker.advance_checkpoint() - storage.delete_linear_constraint(c) - self.assert_protos_equiv( - _ModelUpdateProto(deleted_linear_constraint_ids=[0]), - tracker.export_update(), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_modify_new_var(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_variable_lb(x, -4.0) + storage.set_variable_ub(x, 5.0) + storage.set_variable_is_integer(x, False) + expected = _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[0], + lower_bounds=[-4.0], + upper_bounds=[5.0], + integers=[False], + names=["x"], ) - - def test_update_var_lb(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.set_variable_lb(x, -7.0) - self.assert_protos_equiv( - _ModelUpdateProto( - variable_updates=_VariableUpdatesProto( - lower_bounds=_SparseDoubleVectorProto(ids=[0], values=[-7.0]) - ) - ), - tracker.export_update(), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_new_var_with_deletes(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(0.0, 1.0, False, "x") + storage.add_variable(-1.0, 2.5, True, "y") + storage.delete_variable(x) + expected = _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[1], + lower_bounds=[-1.0], + upper_bounds=[2.5], + integers=[True], + names=["y"], ) - - def test_update_var_lb_same_value(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.set_variable_lb(x, -1.0) - self.assertIsNone(tracker.export_update()) - - def test_update_var_ub(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.set_variable_ub(x, 12.5) - self.assert_protos_equiv( - _ModelUpdateProto( - variable_updates=_VariableUpdatesProto( - upper_bounds=_SparseDoubleVectorProto(ids=[0], values=[12.5]) - ) - ), - tracker.export_update(), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_delete_var_before_first_update( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + tracker.advance_checkpoint() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.add_variable(-2.0, 3.5, True, "y") + storage.delete_variable(x) + self.assert_protos_equiv( + _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[1], + lower_bounds=[-2.0], + upper_bounds=[3.5], + integers=[True], + names=["y"], + ) + ), + tracker.export_update(), + ) + + def test_new_lin_con(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + storage.add_linear_constraint(-1.0, 2.5, "c") + expected = _ModelUpdateProto( + new_linear_constraints=_LinearConstraintsProto( + ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] ) - - def test_update_var_ub_same_value(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.set_variable_ub(x, 2.5) - self.assertIsNone(tracker.export_update()) - - def test_update_var_integer(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.set_variable_is_integer(x, False) - self.assert_protos_equiv( - _ModelUpdateProto( - variable_updates=_VariableUpdatesProto( - integers=_SparseBoolVectorProto(ids=[0], values=[False]) - ) - ), - tracker.export_update(), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_modify_new_lin_con(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_lb(c, -4.0) + storage.set_linear_constraint_ub(c, 5.0) + expected = _ModelUpdateProto( + new_linear_constraints=_LinearConstraintsProto( + ids=[0], lower_bounds=[-4.0], upper_bounds=[5.0], names=["c"] ) - - def test_update_var_integer_same_value(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.set_variable_is_integer(x, True) - self.assertIsNone(tracker.export_update()) - - def test_update_var_then_delete(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.set_variable_lb(x, -3.0) - storage.set_variable_ub(x, 5.0) - storage.set_variable_is_integer(x, False) - storage.delete_variable(x) - self.assert_protos_equiv( - _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_new_lin_con_with_deletes(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + c = storage.add_linear_constraint(0.0, 1.0, "c") + storage.add_linear_constraint(-1.0, 2.5, "d") + storage.delete_linear_constraint(c) + expected = _ModelUpdateProto( + new_linear_constraints=_LinearConstraintsProto( + ids=[1], lower_bounds=[-1.0], upper_bounds=[2.5], names=["d"] ) - - def test_update_lin_con_lb(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - tracker.advance_checkpoint() - storage.set_linear_constraint_lb(c, -7.0) - self.assert_protos_equiv( - _ModelUpdateProto( - linear_constraint_updates=_LinearConstraintUpdatesProto( - lower_bounds=_SparseDoubleVectorProto(ids=[0], values=[-7.0]) - ) - ), - tracker.export_update(), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_delete_lin_con_before_first_update( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + tracker.advance_checkpoint() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.add_linear_constraint(-2.0, 3.5, "d") + storage.delete_linear_constraint(c) + self.assert_protos_equiv( + _ModelUpdateProto( + new_linear_constraints=_LinearConstraintsProto( + ids=[1], lower_bounds=[-2.0], upper_bounds=[3.5], names=["d"] + ) + ), + tracker.export_update(), + ) + + def test_update_objective_direction( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + storage.set_is_maximize(True) + expected = _ModelUpdateProto( + objective_updates=_ObjectiveUpdatesProto(direction_update=True) + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_update_objective_direction_same( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + storage.set_is_maximize(False) + self.assertIsNone(tracker.export_update()) + + def test_update_objective_offset(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + storage.set_objective_offset(5.0) + expected = _ModelUpdateProto( + objective_updates=_ObjectiveUpdatesProto(offset_update=5.0) + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_update_objective_offset_same( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + storage.set_objective_offset(0.0) + self.assertIsNone(tracker.export_update()) + + def test_objective_update_existing_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.set_linear_objective_coefficient(x, 3.0) + expected = _ModelUpdateProto( + objective_updates=_ObjectiveUpdatesProto( + linear_coefficients=_SparseDoubleVectorProto(ids=[0], values=[3.0]) ) - - def test_update_lin_con_lb_same_value(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - tracker.advance_checkpoint() - storage.set_linear_constraint_lb(c, -1.0) - self.assertIsNone(tracker.export_update()) - - def test_update_lin_con_ub(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - tracker.advance_checkpoint() - storage.set_linear_constraint_ub(c, 12.5) - self.assert_protos_equiv( - _ModelUpdateProto( - linear_constraint_updates=_LinearConstraintUpdatesProto( - upper_bounds=_SparseDoubleVectorProto(ids=[0], values=[12.5]) - ) - ), - tracker.export_update(), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_objective_update_existing_zero_same( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + storage.set_linear_objective_coefficient(x, 0.0) + self.assertIsNone(tracker.export_update()) + + def test_objective_update_existing_nonzero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_linear_objective_coefficient(x, 4.0) + tracker.advance_checkpoint() + storage.set_linear_objective_coefficient(x, 3.0) + expected = _ModelUpdateProto( + objective_updates=_ObjectiveUpdatesProto( + linear_coefficients=_SparseDoubleVectorProto(ids=[0], values=[3.0]) ) - - def test_update_lin_con_ub_same_value(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - tracker.advance_checkpoint() - storage.set_linear_constraint_ub(c, 2.5) - self.assertIsNone(tracker.export_update()) - - def test_update_lin_con_then_delete(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - tracker.advance_checkpoint() - storage.set_linear_constraint_lb(c, -3.0) - storage.set_linear_constraint_ub(c, 5.0) - storage.delete_linear_constraint(c) - self.assert_protos_equiv( - _ModelUpdateProto(deleted_linear_constraint_ids=[0]), - tracker.export_update(), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_objective_update_existing_nonzero_same( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_linear_objective_coefficient(x, 4.0) + tracker.advance_checkpoint() + storage.set_linear_objective_coefficient(x, 4.0) + self.assertIsNone(tracker.export_update()) + + def test_objective_update_clear(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(0.0, 1.0, False, "y") + z = storage.add_variable(0.0, 1.0, True, "z") + + storage.set_linear_objective_coefficient(x, 2.0) + storage.set_linear_objective_coefficient(z, -5.5) + storage.set_objective_offset(1.0) + self.assertEqual(2.0, storage.get_linear_objective_coefficient(x)) + self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) + self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z)) + self.assertEqual(1.0, storage.get_objective_offset()) + tracker.advance_checkpoint() + w = storage.add_variable(0.0, 1.0, True, "w") + storage.set_linear_objective_coefficient(w, 1.0) + storage.clear_objective() + expected = _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[3], + lower_bounds=[0.0], + upper_bounds=[1.0], + integers=[True], + names=["w"], + ), + objective_updates=_ObjectiveUpdatesProto( + offset_update=0.0, + linear_coefficients=_SparseDoubleVectorProto( + ids=[x, z], values=[0.0, 0.0] + ), + ), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_objective_update_existing_to_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_linear_objective_coefficient(x, 4.0) + tracker.advance_checkpoint() + storage.set_linear_objective_coefficient(x, 0.0) + expected = _ModelUpdateProto( + objective_updates=_ObjectiveUpdatesProto( + linear_coefficients=_SparseDoubleVectorProto(ids=[0], values=[0.0]) ) - - def test_new_var(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - storage.add_variable(-1.0, 2.5, True, "x") - expected = _ModelUpdateProto( + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_objective_update_existing_then_delete( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_linear_objective_coefficient(x, 4.0) + tracker.advance_checkpoint() + storage.set_linear_objective_coefficient(x, 2.0) + storage.delete_variable(x) + self.assert_protos_equiv( + _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() + ) + + def test_objective_update_new(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_linear_objective_coefficient(x, 4.0) + self.assert_protos_equiv( + _ModelUpdateProto( new_variables=_VariablesProto( ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], integers=[True], names=["x"], - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_modify_new_var(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_variable_lb(x, -4.0) - storage.set_variable_ub(x, 5.0) - storage.set_variable_is_integer(x, False) - expected = _ModelUpdateProto( + ), + objective_updates=_ObjectiveUpdatesProto( + linear_coefficients=_SparseDoubleVectorProto( + ids=[0], values=[4.0] + ) + ), + ), + tracker.export_update(), + ) + + def test_objective_update_new_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_linear_objective_coefficient(x, 4.0) + storage.set_linear_objective_coefficient(x, 0.0) + self.assert_protos_equiv( + _ModelUpdateProto( new_variables=_VariablesProto( ids=[0], - lower_bounds=[-4.0], - upper_bounds=[5.0], - integers=[False], - names=["x"], - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_new_var_with_deletes(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(0.0, 1.0, False, "x") - storage.add_variable(-1.0, 2.5, True, "y") - storage.delete_variable(x) - expected = _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[1], lower_bounds=[-1.0], upper_bounds=[2.5], integers=[True], - names=["y"], + names=["x"], ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_delete_var_before_first_update(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - tracker.advance_checkpoint() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.add_variable(-2.0, 3.5, True, "y") - storage.delete_variable(x) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[1], - lower_bounds=[-2.0], - upper_bounds=[3.5], - integers=[True], - names=["y"], + ), + tracker.export_update(), + ) + + def test_objective_update_new_then_delete( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_linear_objective_coefficient(x, 4.0) + storage.delete_variable(x) + self.assert_protos_equiv(_ModelUpdateProto(), tracker.export_update()) + + def test_objective_update_old_new_ordering( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + old_handles = [] + for i in range(4): + x = storage.add_variable(-1.0, 2.5, True, f"x_{i}") + storage.set_linear_objective_coefficient(x, i + 1.0) + old_handles.append(x) + tracker.advance_checkpoint() + for i in range(4): + x = storage.add_variable(-1.0, 2.5, True, f"x_{i+4}") + storage.set_linear_objective_coefficient(x, i + 10.0) + for i, h in enumerate(old_handles): + storage.set_linear_objective_coefficient(h, -2.0 * i) + self.assert_protos_equiv( + _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[4, 5, 6, 7], + lower_bounds=[-1.0, -1.0, -1.0, -1.0], + upper_bounds=[2.5, 2.5, 2.5, 2.5], + integers=[True, True, True, True], + names=["x_4", "x_5", "x_6", "x_7"], + ), + objective_updates=_ObjectiveUpdatesProto( + linear_coefficients=_SparseDoubleVectorProto( + ids=[0, 1, 2, 3, 4, 5, 6, 7], + values=[0.0, -2.0, -4.0, -6.0, 10.0, 11.0, 12.0, 13.0], ) ), - tracker.export_update(), - ) - - def test_new_lin_con(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - storage.add_linear_constraint(-1.0, 2.5, "c") - expected = _ModelUpdateProto( - new_linear_constraints=_LinearConstraintsProto( - ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_modify_new_lin_con(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_lb(c, -4.0) - storage.set_linear_constraint_ub(c, 5.0) - expected = _ModelUpdateProto( - new_linear_constraints=_LinearConstraintsProto( - ids=[0], lower_bounds=[-4.0], upper_bounds=[5.0], names=["c"] + ), + tracker.export_update(), + ) + + def test_quadratic_objective_update_existing_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + tracker.advance_checkpoint() + storage.set_quadratic_objective_coefficient(x, y, 3.0) + expected = _ModelUpdateProto( + objective_updates=_ObjectiveUpdatesProto( + quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[0], column_ids=[1], coefficients=[3.0] ) ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_new_lin_con_with_deletes(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - c = storage.add_linear_constraint(0.0, 1.0, "c") - storage.add_linear_constraint(-1.0, 2.5, "d") - storage.delete_linear_constraint(c) - expected = _ModelUpdateProto( - new_linear_constraints=_LinearConstraintsProto( - ids=[1], lower_bounds=[-1.0], upper_bounds=[2.5], names=["d"] + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_quadratic_objective_update_existing_zero_same( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + tracker.advance_checkpoint() + storage.set_quadratic_objective_coefficient(x, y, 0.0) + self.assertIsNone(tracker.export_update()) + + def test_quadratic_objective_update_existing_nonzero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 4.0) + tracker.advance_checkpoint() + storage.set_quadratic_objective_coefficient(x, y, 3.0) + expected = _ModelUpdateProto( + objective_updates=_ObjectiveUpdatesProto( + quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[0], column_ids=[1], coefficients=[3.0] ) ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_delete_lin_con_before_first_update( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - tracker.advance_checkpoint() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.add_linear_constraint(-2.0, 3.5, "d") - storage.delete_linear_constraint(c) - self.assert_protos_equiv( - _ModelUpdateProto( - new_linear_constraints=_LinearConstraintsProto( - ids=[1], lower_bounds=[-2.0], upper_bounds=[3.5], names=["d"] - ) + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_quadratic_objective_update_existing_nonzero_same( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 4.0) + tracker.advance_checkpoint() + storage.set_quadratic_objective_coefficient(x, y, 4.0) + self.assertIsNone(tracker.export_update()) + + def test_quadratic_objective_update_clear( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(0.0, 1.0, False, "y") + z = storage.add_variable(0.0, 1.0, True, "z") + + storage.set_linear_objective_coefficient(x, 2.0) + storage.set_linear_objective_coefficient(z, -5.5) + storage.set_quadratic_objective_coefficient(x, y, 4.0) + storage.set_objective_offset(1.0) + self.assertEqual(2.0, storage.get_linear_objective_coefficient(x)) + self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) + self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z)) + self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, x)) + self.assertEqual(4.0, storage.get_quadratic_objective_coefficient(x, y)) + self.assertEqual(1.0, storage.get_objective_offset()) + tracker.advance_checkpoint() + w = storage.add_variable(0.0, 1.0, True, "w") + storage.set_linear_objective_coefficient(w, 1.0) + storage.set_quadratic_objective_coefficient(w, w, 2.0) + storage.clear_objective() + expected = _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[3], + lower_bounds=[0.0], + upper_bounds=[1.0], + integers=[True], + names=["w"], + ), + objective_updates=_ObjectiveUpdatesProto( + offset_update=0.0, + linear_coefficients=_SparseDoubleVectorProto( + ids=[x, z], values=[0.0, 0.0] ), - tracker.export_update(), - ) - - def test_update_objective_direction(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - storage.set_is_maximize(True) - expected = _ModelUpdateProto( - objective_updates=_ObjectiveUpdatesProto(direction_update=True) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_update_objective_direction_same( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - storage.set_is_maximize(False) - self.assertIsNone(tracker.export_update()) - - def test_update_objective_offset(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - storage.set_objective_offset(5.0) - expected = _ModelUpdateProto( - objective_updates=_ObjectiveUpdatesProto(offset_update=5.0) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_update_objective_offset_same(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - storage.set_objective_offset(0.0) - self.assertIsNone(tracker.export_update()) - - def test_objective_update_existing_zero(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.set_linear_objective_coefficient(x, 3.0) - expected = _ModelUpdateProto( - objective_updates=_ObjectiveUpdatesProto( - linear_coefficients=_SparseDoubleVectorProto(ids=[0], values=[3.0]) - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_objective_update_existing_zero_same( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - storage.set_linear_objective_coefficient(x, 0.0) - self.assertIsNone(tracker.export_update()) - - def test_objective_update_existing_nonzero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_linear_objective_coefficient(x, 4.0) - tracker.advance_checkpoint() - storage.set_linear_objective_coefficient(x, 3.0) - expected = _ModelUpdateProto( - objective_updates=_ObjectiveUpdatesProto( - linear_coefficients=_SparseDoubleVectorProto(ids=[0], values=[3.0]) + quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[x], column_ids=[y], coefficients=[0.0] + ), + ), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_quadratic_objective_update_existing_to_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 4.0) + tracker.advance_checkpoint() + storage.set_quadratic_objective_coefficient(x, y, 0.0) + expected = _ModelUpdateProto( + objective_updates=_ObjectiveUpdatesProto( + quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[x], column_ids=[y], coefficients=[0.0] ) ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_objective_update_existing_nonzero_same( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_linear_objective_coefficient(x, 4.0) - tracker.advance_checkpoint() - storage.set_linear_objective_coefficient(x, 4.0) - self.assertIsNone(tracker.export_update()) - - def test_objective_update_clear(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(0.0, 1.0, False, "y") - z = storage.add_variable(0.0, 1.0, True, "z") - - storage.set_linear_objective_coefficient(x, 2.0) - storage.set_linear_objective_coefficient(z, -5.5) - storage.set_objective_offset(1.0) - self.assertEqual(2.0, storage.get_linear_objective_coefficient(x)) - self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) - self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z)) - self.assertEqual(1.0, storage.get_objective_offset()) - tracker.advance_checkpoint() - w = storage.add_variable(0.0, 1.0, True, "w") - storage.set_linear_objective_coefficient(w, 1.0) - storage.clear_objective() - expected = _ModelUpdateProto( + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_quadratic_objective_update_existing_then_delete( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 4.0) + tracker.advance_checkpoint() + storage.set_quadratic_objective_coefficient(x, y, 2.0) + storage.delete_variable(x) + self.assert_protos_equiv( + _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() + ) + + def test_quadratic_objective_update_new( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_quadratic_objective_coefficient(x, x, 4.0) + self.assert_protos_equiv( + _ModelUpdateProto( new_variables=_VariablesProto( - ids=[3], - lower_bounds=[0.0], - upper_bounds=[1.0], + ids=[0], + lower_bounds=[-1.0], + upper_bounds=[2.5], integers=[True], - names=["w"], - ), - objective_updates=_ObjectiveUpdatesProto( - offset_update=0.0, - linear_coefficients=_SparseDoubleVectorProto( - ids=[x, z], values=[0.0, 0.0] - ), + names=["x"], ), - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_objective_update_existing_to_zero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_linear_objective_coefficient(x, 4.0) - tracker.advance_checkpoint() - storage.set_linear_objective_coefficient(x, 0.0) - expected = _ModelUpdateProto( objective_updates=_ObjectiveUpdatesProto( - linear_coefficients=_SparseDoubleVectorProto(ids=[0], values=[0.0]) - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_objective_update_existing_then_delete( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_linear_objective_coefficient(x, 4.0) - tracker.advance_checkpoint() - storage.set_linear_objective_coefficient(x, 2.0) - storage.delete_variable(x) - self.assert_protos_equiv( - _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() - ) - - def test_objective_update_new(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_linear_objective_coefficient(x, 4.0) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[0], - lower_bounds=[-1.0], - upper_bounds=[2.5], - integers=[True], - names=["x"], - ), - objective_updates=_ObjectiveUpdatesProto( - linear_coefficients=_SparseDoubleVectorProto(ids=[0], values=[4.0]) - ), - ), - tracker.export_update(), - ) - - def test_objective_update_new_zero(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_linear_objective_coefficient(x, 4.0) - storage.set_linear_objective_coefficient(x, 0.0) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[0], - lower_bounds=[-1.0], - upper_bounds=[2.5], - integers=[True], - names=["x"], + quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[x], column_ids=[x], coefficients=[4.0] ) ), - tracker.export_update(), - ) - - def test_objective_update_new_then_delete( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_linear_objective_coefficient(x, 4.0) - storage.delete_variable(x) - self.assert_protos_equiv(_ModelUpdateProto(), tracker.export_update()) - - def test_objective_update_old_new_ordering( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - old_handles = [] - for i in range(4): - x = storage.add_variable(-1.0, 2.5, True, f"x_{i}") - storage.set_linear_objective_coefficient(x, i + 1.0) - old_handles.append(x) - tracker.advance_checkpoint() - for i in range(4): - x = storage.add_variable(-1.0, 2.5, True, f"x_{i+4}") - storage.set_linear_objective_coefficient(x, i + 10.0) - for i, h in enumerate(old_handles): - storage.set_linear_objective_coefficient(h, -2.0 * i) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[4, 5, 6, 7], - lower_bounds=[-1.0, -1.0, -1.0, -1.0], - upper_bounds=[2.5, 2.5, 2.5, 2.5], - integers=[True, True, True, True], - names=["x_4", "x_5", "x_6", "x_7"], - ), - objective_updates=_ObjectiveUpdatesProto( - linear_coefficients=_SparseDoubleVectorProto( - ids=[0, 1, 2, 3, 4, 5, 6, 7], - values=[0.0, -2.0, -4.0, -6.0, 10.0, 11.0, 12.0, 13.0], - ) - ), + ), + tracker.export_update(), + ) + + def test_quadratic_objective_update_new_old_deleted( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + old_var1 = storage.add_variable(-1.0, 2.5, True, "old1") + old_var2 = storage.add_variable(-1.0, 2.5, True, "old2") + deleted_var1 = storage.add_variable(-1.0, 2.5, True, "deleted1") + deleted_var2 = storage.add_variable(-1.0, 2.5, True, "deleted2") + tracker.advance_checkpoint() + new_var1 = storage.add_variable(0.0, 1.0, True, "new1") + new_var2 = storage.add_variable(0.0, 1.0, True, "new2") + storage.set_quadratic_objective_coefficient(old_var1, old_var1, 1.0) + storage.set_quadratic_objective_coefficient(old_var1, old_var2, 2.0) + storage.set_quadratic_objective_coefficient(old_var1, new_var1, 3.0) + storage.set_quadratic_objective_coefficient(new_var1, new_var1, 4.0) + storage.set_quadratic_objective_coefficient(new_var1, new_var2, 5.0) + storage.set_quadratic_objective_coefficient(deleted_var1, deleted_var1, 6.0) + storage.set_quadratic_objective_coefficient(deleted_var1, deleted_var2, 7.0) + storage.set_quadratic_objective_coefficient(deleted_var1, old_var1, 8.0) + storage.set_quadratic_objective_coefficient(deleted_var1, new_var1, 9.0) + storage.delete_variable(deleted_var1) + storage.delete_variable(deleted_var2) + self.assert_protos_equiv( + _ModelUpdateProto( + deleted_variable_ids=[deleted_var1, deleted_var2], + new_variables=_VariablesProto( + ids=[new_var1, new_var2], + lower_bounds=[0.0, 0.0], + upper_bounds=[1.0, 1.0], + integers=[True, True], + names=["new1", "new2"], ), - tracker.export_update(), - ) - - def test_quadratic_objective_update_existing_zero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - tracker.advance_checkpoint() - storage.set_quadratic_objective_coefficient(x, y, 3.0) - expected = _ModelUpdateProto( - objective_updates=_ObjectiveUpdatesProto( - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0], column_ids=[1], coefficients=[3.0] - ) - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_quadratic_objective_update_existing_zero_same( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - tracker.advance_checkpoint() - storage.set_quadratic_objective_coefficient(x, y, 0.0) - self.assertIsNone(tracker.export_update()) - - def test_quadratic_objective_update_existing_nonzero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 4.0) - tracker.advance_checkpoint() - storage.set_quadratic_objective_coefficient(x, y, 3.0) - expected = _ModelUpdateProto( objective_updates=_ObjectiveUpdatesProto( quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0], column_ids=[1], coefficients=[3.0] + row_ids=[old_var1, old_var1, old_var1, new_var1, new_var1], + column_ids=[ + old_var1, + old_var2, + new_var1, + new_var1, + new_var2, + ], + coefficients=[1.0, 2.0, 3.0, 4.0, 5.0], ) + ), + ), + tracker.export_update(), + ) + + def test_quadratic_objective_update_new_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 4.0) + storage.set_quadratic_objective_coefficient(x, y, 0.0) + storage.set_linear_objective_coefficient(x, 0.0) + self.assert_protos_equiv( + _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[0, 1], + lower_bounds=[-1.0, -1.0], + upper_bounds=[2.5, 2.5], + integers=[True, True], + names=["x", "y"], ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_quadratic_objective_update_existing_nonzero_same( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 4.0) - tracker.advance_checkpoint() - storage.set_quadratic_objective_coefficient(x, y, 4.0) - self.assertIsNone(tracker.export_update()) - - def test_quadratic_objective_update_clear( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(0.0, 1.0, False, "y") - z = storage.add_variable(0.0, 1.0, True, "z") - - storage.set_linear_objective_coefficient(x, 2.0) - storage.set_linear_objective_coefficient(z, -5.5) - storage.set_quadratic_objective_coefficient(x, y, 4.0) - storage.set_objective_offset(1.0) - self.assertEqual(2.0, storage.get_linear_objective_coefficient(x)) - self.assertEqual(0.0, storage.get_linear_objective_coefficient(y)) - self.assertEqual(-5.5, storage.get_linear_objective_coefficient(z)) - self.assertEqual(0.0, storage.get_quadratic_objective_coefficient(x, x)) - self.assertEqual(4.0, storage.get_quadratic_objective_coefficient(x, y)) - self.assertEqual(1.0, storage.get_objective_offset()) - tracker.advance_checkpoint() - w = storage.add_variable(0.0, 1.0, True, "w") - storage.set_linear_objective_coefficient(w, 1.0) - storage.set_quadratic_objective_coefficient(w, w, 2.0) - storage.clear_objective() - expected = _ModelUpdateProto( + ), + tracker.export_update(), + ) + + def test_quadratic_objective_update_new_then_delete( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + y = storage.add_variable(-1.0, 2.5, True, "y") + storage.set_quadratic_objective_coefficient(x, y, 4.0) + storage.delete_variable(x) + storage.delete_variable(y) + self.assert_protos_equiv(_ModelUpdateProto(), tracker.export_update()) + + def test_quadratic_objective_update_old_new_ordering( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + old_handles = [] + for i in range(4): + x = storage.add_variable(-1.0, 2.5, True, f"x_{i}") + old_handles.append(x) + for i in range(3): + storage.set_quadratic_objective_coefficient( + old_handles[i], old_handles[i + 1], i + 1 + ) + tracker.advance_checkpoint() + new_handles = [] + for i in range(4): + x = storage.add_variable(-1.0, 2.5, True, f"x_{i+4}") + new_handles.append(x) + for i in range(3): + storage.set_quadratic_objective_coefficient( + new_handles[i], new_handles[i + 1], i + 10 + ) + for i in range(3): + storage.set_quadratic_objective_coefficient( + old_handles[i], old_handles[i + 1], -2.0 * i + ) + self.assert_protos_equiv( + _ModelUpdateProto( new_variables=_VariablesProto( - ids=[3], - lower_bounds=[0.0], - upper_bounds=[1.0], - integers=[True], - names=["w"], + ids=[4, 5, 6, 7], + lower_bounds=[-1.0, -1.0, -1.0, -1.0], + upper_bounds=[2.5, 2.5, 2.5, 2.5], + integers=[True, True, True, True], + names=["x_4", "x_5", "x_6", "x_7"], ), objective_updates=_ObjectiveUpdatesProto( - offset_update=0.0, - linear_coefficients=_SparseDoubleVectorProto( - ids=[x, z], values=[0.0, 0.0] - ), quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[x], column_ids=[y], coefficients=[0.0] - ), - ), - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_quadratic_objective_update_existing_to_zero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 4.0) - tracker.advance_checkpoint() - storage.set_quadratic_objective_coefficient(x, y, 0.0) - expected = _ModelUpdateProto( - objective_updates=_ObjectiveUpdatesProto( - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[x], column_ids=[y], coefficients=[0.0] + row_ids=[0, 1, 2, 4, 5, 6], + column_ids=[1, 2, 3, 5, 6, 7], + coefficients=[0, -2.0, -4.0, 10, 11, 12], ) - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_quadratic_objective_update_existing_then_delete( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 4.0) - tracker.advance_checkpoint() - storage.set_quadratic_objective_coefficient(x, y, 2.0) - storage.delete_variable(x) - self.assert_protos_equiv( - _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() - ) - - def test_quadratic_objective_update_new(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_quadratic_objective_coefficient(x, x, 4.0) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[0], - lower_bounds=[-1.0], - upper_bounds=[2.5], - integers=[True], - names=["x"], - ), - objective_updates=_ObjectiveUpdatesProto( - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[x], column_ids=[x], coefficients=[4.0] - ) - ), ), - tracker.export_update(), + ), + tracker.export_update(), + ) + + def test_update_lin_con_mat_existing_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-1.0, 2.5, "c") + tracker.advance_checkpoint() + storage.set_linear_constraint_coefficient(c, x, 3.0) + expected = _ModelUpdateProto( + linear_constraint_matrix_updates=_SparseDoubleMatrixProto( + row_ids=[0], column_ids=[0], coefficients=[3.0] ) - - def test_quadratic_objective_update_new_old_deleted( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - old_var1 = storage.add_variable(-1.0, 2.5, True, "old1") - old_var2 = storage.add_variable(-1.0, 2.5, True, "old2") - deleted_var1 = storage.add_variable(-1.0, 2.5, True, "deleted1") - deleted_var2 = storage.add_variable(-1.0, 2.5, True, "deleted2") - tracker.advance_checkpoint() - new_var1 = storage.add_variable(0.0, 1.0, True, "new1") - new_var2 = storage.add_variable(0.0, 1.0, True, "new2") - storage.set_quadratic_objective_coefficient(old_var1, old_var1, 1.0) - storage.set_quadratic_objective_coefficient(old_var1, old_var2, 2.0) - storage.set_quadratic_objective_coefficient(old_var1, new_var1, 3.0) - storage.set_quadratic_objective_coefficient(new_var1, new_var1, 4.0) - storage.set_quadratic_objective_coefficient(new_var1, new_var2, 5.0) - storage.set_quadratic_objective_coefficient(deleted_var1, deleted_var1, 6.0) - storage.set_quadratic_objective_coefficient(deleted_var1, deleted_var2, 7.0) - storage.set_quadratic_objective_coefficient(deleted_var1, old_var1, 8.0) - storage.set_quadratic_objective_coefficient(deleted_var1, new_var1, 9.0) - storage.delete_variable(deleted_var1) - storage.delete_variable(deleted_var2) - self.assert_protos_equiv( - _ModelUpdateProto( - deleted_variable_ids=[deleted_var1, deleted_var2], - new_variables=_VariablesProto( - ids=[new_var1, new_var2], - lower_bounds=[0.0, 0.0], - upper_bounds=[1.0, 1.0], - integers=[True, True], - names=["new1", "new2"], - ), - objective_updates=_ObjectiveUpdatesProto( - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[old_var1, old_var1, old_var1, new_var1, new_var1], - column_ids=[ - old_var1, - old_var2, - new_var1, - new_var1, - new_var2, - ], - coefficients=[1.0, 2.0, 3.0, 4.0, 5.0], - ) - ), - ), - tracker.export_update(), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_update_lin_con_mat_existing_zero_same( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-1.0, 2.5, "c") + tracker.advance_checkpoint() + storage.set_linear_constraint_coefficient(c, x, 0.0) + self.assertIsNone(tracker.export_update()) + + def test_lin_con_mat_update_existing_nonzero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 1.0) + tracker.advance_checkpoint() + storage.set_linear_constraint_coefficient(c, x, 3.0) + expected = _ModelUpdateProto( + linear_constraint_matrix_updates=_SparseDoubleMatrixProto( + row_ids=[0], column_ids=[0], coefficients=[3.0] ) - - def test_quadratic_objective_update_new_zero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 4.0) - storage.set_quadratic_objective_coefficient(x, y, 0.0) - storage.set_linear_objective_coefficient(x, 0.0) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[0, 1], - lower_bounds=[-1.0, -1.0], - upper_bounds=[2.5, 2.5], - integers=[True, True], - names=["x", "y"], - ) - ), - tracker.export_update(), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_lin_con_mat_update_existing_nonzero_same( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 1.0) + tracker.advance_checkpoint() + storage.set_linear_constraint_coefficient(c, x, 1.0) + self.assertIsNone(tracker.export_update()) + + def test_lin_con_mat_update_existing_to_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 1.0) + tracker.advance_checkpoint() + storage.set_linear_constraint_coefficient(c, x, 0.0) + expected = _ModelUpdateProto( + linear_constraint_matrix_updates=_SparseDoubleMatrixProto( + row_ids=[0], column_ids=[0], coefficients=[0.0] ) - - def test_quadratic_objective_update_new_then_delete( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - y = storage.add_variable(-1.0, 2.5, True, "y") - storage.set_quadratic_objective_coefficient(x, y, 4.0) - storage.delete_variable(x) - storage.delete_variable(y) - self.assert_protos_equiv(_ModelUpdateProto(), tracker.export_update()) - - def test_quadratic_objective_update_old_new_ordering( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - old_handles = [] - for i in range(4): - x = storage.add_variable(-1.0, 2.5, True, f"x_{i}") - old_handles.append(x) - for i in range(3): - storage.set_quadratic_objective_coefficient( - old_handles[i], old_handles[i + 1], i + 1 - ) - tracker.advance_checkpoint() - new_handles = [] - for i in range(4): - x = storage.add_variable(-1.0, 2.5, True, f"x_{i+4}") - new_handles.append(x) - for i in range(3): - storage.set_quadratic_objective_coefficient( - new_handles[i], new_handles[i + 1], i + 10 - ) - for i in range(3): - storage.set_quadratic_objective_coefficient( - old_handles[i], old_handles[i + 1], -2.0 * i - ) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[4, 5, 6, 7], - lower_bounds=[-1.0, -1.0, -1.0, -1.0], - upper_bounds=[2.5, 2.5, 2.5, 2.5], - integers=[True, True, True, True], - names=["x_4", "x_5", "x_6", "x_7"], - ), - objective_updates=_ObjectiveUpdatesProto( - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0, 1, 2, 4, 5, 6], - column_ids=[1, 2, 3, 5, 6, 7], - coefficients=[0, -2.0, -4.0, 10, 11, 12], - ) - ), + ) + self.assert_protos_equiv(expected, tracker.export_update()) + + def test_lin_con_mat_update_existing_then_delete_var( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 1.0) + tracker.advance_checkpoint() + storage.set_linear_constraint_coefficient(c, x, 6.0) + storage.delete_variable(x) + self.assert_protos_equiv( + _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() + ) + + def test_lin_con_mat_update_existing_then_delete_lin_con( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 1.0) + tracker.advance_checkpoint() + storage.set_linear_constraint_coefficient(c, x, 6.0) + storage.delete_linear_constraint(c) + self.assert_protos_equiv( + _ModelUpdateProto(deleted_linear_constraint_ids=[0]), + tracker.export_update(), + ) + + def test_lin_con_mat_update_existing_then_delete_both( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 1.0) + tracker.advance_checkpoint() + storage.set_linear_constraint_coefficient(c, x, 6.0) + storage.delete_linear_constraint(c) + storage.delete_variable(x) + self.assert_protos_equiv( + _ModelUpdateProto( + deleted_variable_ids=[0], deleted_linear_constraint_ids=[0] + ), + tracker.export_update(), + ) + + def test_lin_con_mat_update_new_var( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + tracker.advance_checkpoint() + x = storage.add_variable(-1.0, 2.5, True, "x") + storage.set_linear_constraint_coefficient(c, x, 4.0) + self.assert_protos_equiv( + _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[0], + lower_bounds=[-1.0], + upper_bounds=[2.5], + integers=[True], + names=["x"], ), - tracker.export_update(), - ) - - def test_update_lin_con_mat_existing_zero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-1.0, 2.5, "c") - tracker.advance_checkpoint() - storage.set_linear_constraint_coefficient(c, x, 3.0) - expected = _ModelUpdateProto( - linear_constraint_matrix_updates=_SparseDoubleMatrixProto( - row_ids=[0], column_ids=[0], coefficients=[3.0] - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_update_lin_con_mat_existing_zero_same( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-1.0, 2.5, "c") - tracker.advance_checkpoint() - storage.set_linear_constraint_coefficient(c, x, 0.0) - self.assertIsNone(tracker.export_update()) - - def test_lin_con_mat_update_existing_nonzero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 1.0) - tracker.advance_checkpoint() - storage.set_linear_constraint_coefficient(c, x, 3.0) - expected = _ModelUpdateProto( linear_constraint_matrix_updates=_SparseDoubleMatrixProto( - row_ids=[0], column_ids=[0], coefficients=[3.0] - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_lin_con_mat_update_existing_nonzero_same( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 1.0) - tracker.advance_checkpoint() - storage.set_linear_constraint_coefficient(c, x, 1.0) - self.assertIsNone(tracker.export_update()) - - def test_lin_con_mat_update_existing_to_zero( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 1.0) - tracker.advance_checkpoint() - storage.set_linear_constraint_coefficient(c, x, 0.0) - expected = _ModelUpdateProto( + row_ids=[0], column_ids=[0], coefficients=[4.0] + ), + ), + tracker.export_update(), + ) + + def test_lin_con_mat_update_new_lin_con( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 4.0) + self.assert_protos_equiv( + _ModelUpdateProto( + new_linear_constraints=_LinearConstraintsProto( + ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] + ), linear_constraint_matrix_updates=_SparseDoubleMatrixProto( - row_ids=[0], column_ids=[0], coefficients=[0.0] - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - - def test_lin_con_mat_update_existing_then_delete_var( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 1.0) - tracker.advance_checkpoint() - storage.set_linear_constraint_coefficient(c, x, 6.0) - storage.delete_variable(x) - self.assert_protos_equiv( - _ModelUpdateProto(deleted_variable_ids=[0]), tracker.export_update() - ) - - def test_lin_con_mat_update_existing_then_delete_lin_con( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 1.0) - tracker.advance_checkpoint() - storage.set_linear_constraint_coefficient(c, x, 6.0) - storage.delete_linear_constraint(c) - self.assert_protos_equiv( - _ModelUpdateProto(deleted_linear_constraint_ids=[0]), - tracker.export_update(), - ) - - def test_lin_con_mat_update_existing_then_delete_both( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 1.0) - tracker.advance_checkpoint() - storage.set_linear_constraint_coefficient(c, x, 6.0) - storage.delete_linear_constraint(c) - storage.delete_variable(x) - self.assert_protos_equiv( - _ModelUpdateProto( - deleted_variable_ids=[0], deleted_linear_constraint_ids=[0] + row_ids=[0], column_ids=[0], coefficients=[4.0] ), - tracker.export_update(), - ) - - def test_lin_con_mat_update_new_var(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - tracker.advance_checkpoint() - x = storage.add_variable(-1.0, 2.5, True, "x") - storage.set_linear_constraint_coefficient(c, x, 4.0) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[0], - lower_bounds=[-1.0], - upper_bounds=[2.5], - integers=[True], - names=["x"], - ), - linear_constraint_matrix_updates=_SparseDoubleMatrixProto( - row_ids=[0], column_ids=[0], coefficients=[4.0] - ), + ), + tracker.export_update(), + ) + + def test_lin_con_mat_update_new_both( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 4.0) + self.assert_protos_equiv( + _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[0], + lower_bounds=[-1.0], + upper_bounds=[2.5], + integers=[True], + names=["x"], ), - tracker.export_update(), - ) - - def test_lin_con_mat_update_new_lin_con(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 4.0) - self.assert_protos_equiv( - _ModelUpdateProto( - new_linear_constraints=_LinearConstraintsProto( - ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] - ), - linear_constraint_matrix_updates=_SparseDoubleMatrixProto( - row_ids=[0], column_ids=[0], coefficients=[4.0] - ), + new_linear_constraints=_LinearConstraintsProto( + ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] ), - tracker.export_update(), - ) - - def test_lin_con_mat_update_new_both(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 4.0) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[0], - lower_bounds=[-1.0], - upper_bounds=[2.5], - integers=[True], - names=["x"], - ), - new_linear_constraints=_LinearConstraintsProto( - ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] - ), - linear_constraint_matrix_updates=_SparseDoubleMatrixProto( - row_ids=[0], column_ids=[0], coefficients=[4.0] - ), + linear_constraint_matrix_updates=_SparseDoubleMatrixProto( + row_ids=[0], column_ids=[0], coefficients=[4.0] ), - tracker.export_update(), - ) - - def test_lin_con_mat_update_new_zero(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 4.0) - storage.set_linear_constraint_coefficient(c, x, 0.0) - self.assert_protos_equiv( - _ModelUpdateProto( - new_linear_constraints=_LinearConstraintsProto( - ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] - ) + ), + tracker.export_update(), + ) + + def test_lin_con_mat_update_new_zero( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 4.0) + storage.set_linear_constraint_coefficient(c, x, 0.0) + self.assert_protos_equiv( + _ModelUpdateProto( + new_linear_constraints=_LinearConstraintsProto( + ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] + ) + ), + tracker.export_update(), + ) + + def test_lin_con_mat_update_new_then_delete( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + x = storage.add_variable(-1.0, 2.5, True, "x") + tracker.advance_checkpoint() + c = storage.add_linear_constraint(-1.0, 2.5, "c") + storage.set_linear_constraint_coefficient(c, x, 4.0) + storage.delete_variable(x) + self.assert_protos_equiv( + _ModelUpdateProto( + deleted_variable_ids=[0], + new_linear_constraints=_LinearConstraintsProto( + ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] ), - tracker.export_update(), - ) - - def test_lin_con_mat_update_new_then_delete( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - x = storage.add_variable(-1.0, 2.5, True, "x") - tracker.advance_checkpoint() - c = storage.add_linear_constraint(-1.0, 2.5, "c") - storage.set_linear_constraint_coefficient(c, x, 4.0) - storage.delete_variable(x) - self.assert_protos_equiv( - _ModelUpdateProto( - deleted_variable_ids=[0], - new_linear_constraints=_LinearConstraintsProto( - ids=[0], lower_bounds=[-1.0], upper_bounds=[2.5], names=["c"] - ), + ), + tracker.export_update(), + ) + + def test_lin_con_mat_update_old_new_ordering( + self, storage_class: _StorageClass + ) -> None: + storage = storage_class("test_model") + tracker = storage.add_update_tracker() + var_handles = [storage.add_variable(0.0, 1.0, True, "") for _ in range(2)] + lin_con_handles = [ + storage.add_linear_constraint(0.0, 1.0, "") for _ in range(2) + ] + for v in var_handles: + for l in lin_con_handles: + storage.set_linear_constraint_coefficient(l, v, 1.0) + tracker.advance_checkpoint() + x = storage.add_variable(0.0, 1.0, True, "x") + c = storage.add_linear_constraint(0.0, 1.0, "c") + storage.set_linear_constraint_coefficient( + lin_con_handles[0], var_handles[0], 5.0 + ) + storage.set_linear_constraint_coefficient(lin_con_handles[0], x, 4.0) + storage.set_linear_constraint_coefficient(c, var_handles[1], 3.0) + storage.set_linear_constraint_coefficient(c, x, 2.0) + self.assert_protos_equiv( + _ModelUpdateProto( + new_variables=_VariablesProto( + ids=[2], + lower_bounds=[0.0], + upper_bounds=[1.0], + integers=[True], + names=["x"], ), - tracker.export_update(), - ) - - def test_lin_con_mat_update_old_new_ordering( - self, storage_class: _StorageClass - ) -> None: - storage = storage_class("test_model") - tracker = storage.add_update_tracker() - var_handles = [storage.add_variable(0.0, 1.0, True, "") for _ in range(2)] - lin_con_handles = [ - storage.add_linear_constraint(0.0, 1.0, "") for _ in range(2) - ] - for v in var_handles: - for l in lin_con_handles: - storage.set_linear_constraint_coefficient(l, v, 1.0) - tracker.advance_checkpoint() - x = storage.add_variable(0.0, 1.0, True, "x") - c = storage.add_linear_constraint(0.0, 1.0, "c") - storage.set_linear_constraint_coefficient( - lin_con_handles[0], var_handles[0], 5.0 - ) - storage.set_linear_constraint_coefficient(lin_con_handles[0], x, 4.0) - storage.set_linear_constraint_coefficient(c, var_handles[1], 3.0) - storage.set_linear_constraint_coefficient(c, x, 2.0) - self.assert_protos_equiv( - _ModelUpdateProto( - new_variables=_VariablesProto( - ids=[2], - lower_bounds=[0.0], - upper_bounds=[1.0], - integers=[True], - names=["x"], - ), - new_linear_constraints=_LinearConstraintsProto( - ids=[2], lower_bounds=[0.0], upper_bounds=[1.0], names=["c"] - ), - linear_constraint_matrix_updates=_SparseDoubleMatrixProto( - row_ids=[0, 0, 2, 2], - column_ids=[0, 2, 1, 2], - coefficients=[5.0, 4.0, 3.0, 2.0], - ), + new_linear_constraints=_LinearConstraintsProto( + ids=[2], lower_bounds=[0.0], upper_bounds=[1.0], names=["c"] ), - tracker.export_update(), - ) - - def test_remove_update_tracker(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - x = storage.add_variable(0.0, 1.0, True, "x") - tracker = storage.add_update_tracker() - storage.set_variable_ub(x, 7.0) - expected = _ModelUpdateProto( - variable_updates=_VariableUpdatesProto( - upper_bounds=_SparseDoubleVectorProto(ids=[0], values=[7.0]) - ) - ) - self.assert_protos_equiv(expected, tracker.export_update()) - storage.remove_update_tracker(tracker) - with self.assertRaises(model_storage.UsedUpdateTrackerAfterRemovalError): - tracker.export_update() - with self.assertRaises(model_storage.UsedUpdateTrackerAfterRemovalError): - tracker.advance_checkpoint() - with self.assertRaises(KeyError): - storage.remove_update_tracker(tracker) - - def test_remove_update_tracker_wrong_model( - self, storage_class: _StorageClass - ) -> None: - storage1 = storage_class("test_model1") - storage2 = storage_class("test_model2") - tracker1 = storage1.add_update_tracker() - with self.assertRaises(KeyError): - storage2.remove_update_tracker(tracker1) - - def test_multiple_update_tracker(self, storage_class: _StorageClass) -> None: - storage = storage_class("test_model") - x = storage.add_variable(0.0, 1.0, True, "x") - y = storage.add_variable(0.0, 1.0, True, "y") - tracker1 = storage.add_update_tracker() - storage.set_variable_ub(x, 7.0) - tracker2 = storage.add_update_tracker() - storage.set_variable_ub(y, 3.0) - self.assert_protos_equiv( - _ModelUpdateProto( - variable_updates=_VariableUpdatesProto( - upper_bounds=_SparseDoubleVectorProto(ids=[0, 1], values=[7.0, 3.0]) - ) + linear_constraint_matrix_updates=_SparseDoubleMatrixProto( + row_ids=[0, 0, 2, 2], + column_ids=[0, 2, 1, 2], + coefficients=[5.0, 4.0, 3.0, 2.0], ), - tracker1.export_update(), + ), + tracker.export_update(), + ) + + def test_remove_update_tracker(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + x = storage.add_variable(0.0, 1.0, True, "x") + tracker = storage.add_update_tracker() + storage.set_variable_ub(x, 7.0) + expected = _ModelUpdateProto( + variable_updates=_VariableUpdatesProto( + upper_bounds=_SparseDoubleVectorProto(ids=[0], values=[7.0]) ) - self.assert_protos_equiv( - _ModelUpdateProto( - variable_updates=_VariableUpdatesProto( - upper_bounds=_SparseDoubleVectorProto(ids=[1], values=[3.0]) + ) + self.assert_protos_equiv(expected, tracker.export_update()) + storage.remove_update_tracker(tracker) + with self.assertRaises(model_storage.UsedUpdateTrackerAfterRemovalError): + tracker.export_update() + with self.assertRaises(model_storage.UsedUpdateTrackerAfterRemovalError): + tracker.advance_checkpoint() + with self.assertRaises(KeyError): + storage.remove_update_tracker(tracker) + + def test_remove_update_tracker_wrong_model( + self, storage_class: _StorageClass + ) -> None: + storage1 = storage_class("test_model1") + storage2 = storage_class("test_model2") + tracker1 = storage1.add_update_tracker() + with self.assertRaises(KeyError): + storage2.remove_update_tracker(tracker1) + + def test_multiple_update_tracker(self, storage_class: _StorageClass) -> None: + storage = storage_class("test_model") + x = storage.add_variable(0.0, 1.0, True, "x") + y = storage.add_variable(0.0, 1.0, True, "y") + tracker1 = storage.add_update_tracker() + storage.set_variable_ub(x, 7.0) + tracker2 = storage.add_update_tracker() + storage.set_variable_ub(y, 3.0) + self.assert_protos_equiv( + _ModelUpdateProto( + variable_updates=_VariableUpdatesProto( + upper_bounds=_SparseDoubleVectorProto( + ids=[0, 1], values=[7.0, 3.0] ) - ), - tracker2.export_update(), - ) + ) + ), + tracker1.export_update(), + ) + self.assert_protos_equiv( + _ModelUpdateProto( + variable_updates=_VariableUpdatesProto( + upper_bounds=_SparseDoubleVectorProto(ids=[1], values=[3.0]) + ) + ), + tracker2.export_update(), + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/model_test.py b/ortools/math_opt/python/model_test.py index 0187a37292d..e00a775b89a 100644 --- a/ortools/math_opt/python/model_test.py +++ b/ortools/math_opt/python/model_test.py @@ -26,580 +26,584 @@ class ModelTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_name(self) -> None: - mod = model.Model(name="test_model") - self.assertEqual("test_model", mod.name) - - def test_name_empty(self) -> None: - mod = model.Model() - self.assertEqual("", mod.name) - - def test_add_and_read_variables(self) -> None: - mod = model.Model(name="test_model") - v1 = mod.add_variable(lb=-1.0, ub=2.5, is_integer=True, name="x") - v2 = mod.add_variable() - self.assertEqual(-1.0, v1.lower_bound) - self.assertEqual(2.5, v1.upper_bound) - self.assertTrue(v1.integer) - self.assertEqual("x", v1.name) - self.assertEqual(0, v1.id) - self.assertEqual("x", str(v1)) - self.assertEqual("", repr(v1)) - - self.assertEqual(-math.inf, v2.lower_bound) - self.assertEqual(math.inf, v2.upper_bound) - self.assertFalse(v2.integer) - self.assertEqual("", v2.name) - self.assertEqual(1, v2.id) - self.assertEqual("variable_1", str(v2)) - self.assertEqual("", repr(v2)) - - self.assertListEqual([v1, v2], list(mod.variables())) - self.assertEqual(v1, mod.get_variable(0)) - self.assertEqual(v2, mod.get_variable(1)) - - def test_add_integer_variable(self) -> None: - mod = model.Model( - name="test_model", - ) - v1 = mod.add_integer_variable(lb=-1.0, ub=2.5, name="x") - self.assertEqual(-1.0, v1.lower_bound) - self.assertEqual(2.5, v1.upper_bound) - self.assertTrue(v1.integer) - self.assertEqual("x", v1.name) - self.assertEqual(0, v1.id) - - def test_add_binary_variable(self) -> None: - mod = model.Model(name="test_model") - v1 = mod.add_binary_variable(name="x") - self.assertEqual(0.0, v1.lower_bound) - self.assertEqual(1.0, v1.upper_bound) - self.assertTrue(v1.integer) - self.assertEqual("x", v1.name) - self.assertEqual(0, v1.id) - - def test_update_variable(self) -> None: - mod = model.Model(name="test_model") - v1 = mod.add_variable(lb=-1.0, ub=2.5, is_integer=True, name="x") - v1.lower_bound = -math.inf - v1.upper_bound = -3.0 - v1.integer = False - self.assertEqual(-math.inf, v1.lower_bound) - self.assertEqual(-3.0, v1.upper_bound) - self.assertFalse(v1.integer) - - def test_read_deleted_variable(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - mod.delete_variable(x) - with self.assertRaises(ValueError): - x.lower_bound # pylint: disable=pointless-statement - - def test_update_deleted_variable(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - mod.delete_variable(x) - with self.assertRaises(ValueError): - x.upper_bound = 2.0 - - def test_add_and_read_linear_constraints(self) -> None: - mod = model.Model(name="test_model") - c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c") - d = mod.add_linear_constraint() - self.assertEqual(-1.0, c.lower_bound) - self.assertEqual(2.5, c.upper_bound) - self.assertEqual("c", c.name) - self.assertEqual(0, c.id) - self.assertEqual("c", str(c)) - self.assertEqual("", repr(c)) - - self.assertEqual(-math.inf, d.lower_bound) - self.assertEqual(math.inf, d.upper_bound) - self.assertEqual("", d.name) - self.assertEqual(1, d.id) - self.assertEqual("linear_constraint_1", str(d)) - self.assertEqual("", repr(d)) - - self.assertListEqual([c, d], list(mod.linear_constraints())) - self.assertEqual(c, mod.get_linear_constraint(0)) - self.assertEqual(d, mod.get_linear_constraint(1)) - - def test_linear_constraint_as_bounded_expression(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c", expr=3 * x - 2 * y) - bounded_expr = c.as_bounded_linear_expression() - self.assertEqual(bounded_expr.lower_bound, -1.0) - self.assertEqual(bounded_expr.upper_bound, 2.5) - expr = variables.as_flat_linear_expression(bounded_expr.expression) - self.assertEqual(expr.offset, 0.0) - self.assertDictEqual(dict(expr.terms), {x: 3.0, y: -2.0}) - - def test_update_linear_constraint(self) -> None: - mod = model.Model(name="test_model") - c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c") - c.lower_bound = -math.inf - c.upper_bound = -3.0 - self.assertEqual(-math.inf, c.lower_bound) - self.assertEqual(-3.0, c.upper_bound) - - def test_read_deleted_linear_constraint(self) -> None: - mod = model.Model(name="test_model") - c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c") - mod.delete_linear_constraint(c) - with self.assertRaises(ValueError): - c.name # pylint: disable=pointless-statement - - def test_update_deleted_linear_constraint(self) -> None: - mod = model.Model(name="test_model") - c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c") - mod.delete_linear_constraint(c) - with self.assertRaises(ValueError): - c.lower_bound = -12.0 - - def test_linear_constraint_matrix(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(ub=1.0, name="d") - c.set_coefficient(x, 1.0) - c.set_coefficient(y, 0.0) - d.set_coefficient(x, 2.0) - d.set_coefficient(z, -1.0) - self.assertEqual(1.0, c.get_coefficient(x)) - self.assertEqual(0.0, c.get_coefficient(y)) - self.assertEqual(0.0, c.get_coefficient(z)) - self.assertEqual(2.0, d.get_coefficient(x)) - self.assertEqual(0.0, d.get_coefficient(y)) - self.assertEqual(-1.0, d.get_coefficient(z)) - - self.assertEqual(c.name, "c") - self.assertEqual(d.name, "d") - - self.assertCountEqual([c, d], mod.column_nonzeros(x)) - self.assertCountEqual([], mod.column_nonzeros(y)) - self.assertCountEqual([d], mod.column_nonzeros(z)) - - self.assertCountEqual([x], mod.row_nonzeros(c)) - self.assertCountEqual([x, z], mod.row_nonzeros(d)) - - self.assertCountEqual( - [repr(variables.LinearTerm(variable=x, coefficient=1.0))], - [repr(term) for term in c.terms()], - ) - self.assertCountEqual( - [ - repr(variables.LinearTerm(variable=x, coefficient=2.0)), - repr(variables.LinearTerm(variable=z, coefficient=-1.0)), - ], - [repr(term) for term in d.terms()], - ) - - self.assertCountEqual( - [ - linear_constraints.LinearConstraintMatrixEntry( - linear_constraint=c, variable=x, coefficient=1.0 - ), - linear_constraints.LinearConstraintMatrixEntry( - linear_constraint=d, variable=x, coefficient=2.0 - ), - linear_constraints.LinearConstraintMatrixEntry( - linear_constraint=d, variable=z, coefficient=-1.0 - ), - ], - list(mod.linear_constraint_matrix_entries()), - ) - - def test_linear_constraint_expression(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - c = mod.add_linear_constraint(lb=0.0, expr=x + 1.0, ub=1.0, name="c") - self.assertEqual(1.0, c.get_coefficient(x)) - self.assertEqual(0.0, c.get_coefficient(y)) - self.assertEqual(0.0, c.get_coefficient(z)) - self.assertEqual(-1.0, c.lower_bound) - self.assertEqual(0.0, c.upper_bound) - - d = mod.add_linear_constraint(ub=1.0, expr=2 * x - z, name="d") - self.assertEqual(2.0, d.get_coefficient(x)) - self.assertEqual(0.0, d.get_coefficient(y)) - self.assertEqual(-1.0, d.get_coefficient(z)) - self.assertEqual(-math.inf, d.lower_bound) - self.assertEqual(1.0, d.upper_bound) - - e = mod.add_linear_constraint(lb=0.0) - self.assertEqual(0.0, e.get_coefficient(x)) - self.assertEqual(0.0, e.get_coefficient(y)) - self.assertEqual(0.0, e.get_coefficient(z)) - self.assertEqual(0.0, e.lower_bound) - self.assertEqual(math.inf, e.upper_bound) - - f = mod.add_linear_constraint(expr=1, ub=2) - self.assertEqual(0.0, f.get_coefficient(x)) - self.assertEqual(0.0, f.get_coefficient(y)) - self.assertEqual(0.0, f.get_coefficient(z)) - self.assertEqual(-math.inf, f.lower_bound) - self.assertEqual(1, f.upper_bound) - - def test_linear_constraint_bounded_expression(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - - c = mod.add_linear_constraint((0.0 <= x + 1.0) <= 1.0, name="c") - self.assertEqual(1.0, c.get_coefficient(x)) - self.assertEqual(0.0, c.get_coefficient(y)) - self.assertEqual(0.0, c.get_coefficient(z)) - self.assertEqual(-1.0, c.lower_bound) - self.assertEqual(0.0, c.upper_bound) - - def test_linear_constraint_upper_bounded_expression(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - - d = mod.add_linear_constraint(2 * x - z + 2.0 <= 1.0, name="d") - self.assertEqual(2.0, d.get_coefficient(x)) - self.assertEqual(0.0, d.get_coefficient(y)) - self.assertEqual(-1.0, d.get_coefficient(z)) - self.assertEqual(-math.inf, d.lower_bound) - self.assertEqual(-1.0, d.upper_bound) - - def test_linear_constraint_lower_bounded_expression(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - - e = mod.add_linear_constraint(1.0 <= x + y + 2.0, name="e") - self.assertEqual(1.0, e.get_coefficient(x)) - self.assertEqual(1.0, e.get_coefficient(y)) - self.assertEqual(0.0, e.get_coefficient(z)) - self.assertEqual(-1.0, e.lower_bound) - self.assertEqual(math.inf, e.upper_bound) - - def test_linear_constraint_number_eq_expression(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - - f = mod.add_linear_constraint(1.0 == x + y + 2.0, name="e") - self.assertEqual(1.0, f.get_coefficient(x)) - self.assertEqual(1.0, f.get_coefficient(y)) - self.assertEqual(0.0, f.get_coefficient(z)) - self.assertEqual(-1.0, f.lower_bound) - self.assertEqual(-1.0, f.upper_bound) - - def test_linear_constraint_expression_eq_expression(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - - f = mod.add_linear_constraint(1.0 - x == y + 2.0, name="e") - self.assertEqual(-1.0, f.get_coefficient(x)) - self.assertEqual(-1.0, f.get_coefficient(y)) - self.assertEqual(0.0, f.get_coefficient(z)) - self.assertEqual(1.0, f.lower_bound) - self.assertEqual(1.0, f.upper_bound) - - def test_linear_constraint_variable_eq_variable(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - - f = mod.add_linear_constraint(x == y, name="e") - self.assertEqual(1.0, f.get_coefficient(x)) - self.assertEqual(-1.0, f.get_coefficient(y)) - self.assertEqual(0.0, f.get_coefficient(z)) - self.assertEqual(0.0, f.lower_bound) - self.assertEqual(0.0, f.upper_bound) - - def test_linear_constraint_errors(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - - with self.assertRaisesRegex( - TypeError, - "Unsupported type for bounded_expr.*bool.*!= constraints.*constant" " left", - ): - mod.add_linear_constraint(x != y) - - with self.assertRaisesRegex(TypeError, "!= constraints.*"): - mod.add_linear_constraint(x + y != y) - - with self.assertRaisesRegex(TypeError, "!= constraints.*"): - mod.add_linear_constraint(x != x + y) - - with self.assertRaisesRegex( - TypeError, - "Unsupported type for bounded_expr.*bool.*!= constraints.*constant" " left", - ): - mod.add_linear_constraint(1 <= 2) # pylint: disable=comparison-of-constants - - with self.assertRaisesRegex( - TypeError, - "Unsupported type for bounded_expr.*bool.*!= constraints.*constant" " left", - ): - mod.add_linear_constraint(1 <= 0) # pylint: disable=comparison-of-constants - - with self.assertRaisesRegex( - TypeError, - "Unsupported type for bounded_expr.*bool.*!= constraints.*constant" " left", - ): - mod.add_linear_constraint(True) - - with self.assertRaisesRegex( - TypeError, - "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality", - ): - mod.add_linear_constraint(x <= y <= z) - - with self.assertRaisesRegex( - TypeError, - "unsupported operand.*\n.*two or more non-constant linear expressions", - ): - mod.add_linear_constraint((x <= y) <= z) - - with self.assertRaisesRegex( - TypeError, - "unsupported operand.*\n.*two or more non-constant linear expressions", - ): - mod.add_linear_constraint(x <= (y <= z)) - - with self.assertRaisesRegex(TypeError, "unsupported operand.*"): - mod.add_linear_constraint((0 <= x) >= z) - - with self.assertRaisesRegex(AssertionError, "lb cannot be specified.*"): - mod.add_linear_constraint(x + y == 1, lb=1) - - with self.assertRaisesRegex(AssertionError, "ub cannot be specified.*"): - mod.add_linear_constraint(x + y == 1, ub=1) - - with self.assertRaisesRegex(AssertionError, "expr cannot be specified.*"): - mod.add_linear_constraint(x + y == 1, expr=2 * x) - - with self.assertRaisesRegex( - TypeError, "Unsupported type for expr argument.*str" - ): - mod.add_linear_constraint(expr="string") # pytype: disable=wrong-arg-types - - with self.assertRaisesRegex(ValueError, ".*infinite offset."): - mod.add_linear_constraint(expr=math.inf, lb=0.0) - - def test_linear_constraint_matrix_with_variable_deletion(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - c.set_coefficient(x, 1.0) - c.set_coefficient(y, 2.0) - d.set_coefficient(x, 1.0) - mod.delete_variable(x) - self.assertCountEqual( - [ - linear_constraints.LinearConstraintMatrixEntry( - linear_constraint=c, variable=y, coefficient=2.0 - ) - ], - mod.linear_constraint_matrix_entries(), - ) - self.assertCountEqual([c], mod.column_nonzeros(y)) - self.assertCountEqual( - [repr(variables.LinearTerm(variable=y, coefficient=2.0))], - [repr(term) for term in c.terms()], - ) - self.assertCountEqual([], d.terms()) - with self.assertRaises(ValueError): - c.get_coefficient(x) - - def test_linear_constraint_matrix_with_linear_constraint_deletion( - self, - ) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - c.set_coefficient(x, 1.0) - c.set_coefficient(y, 2.0) - d.set_coefficient(x, 1.0) - mod.delete_linear_constraint(c) - self.assertCountEqual( - [ - linear_constraints.LinearConstraintMatrixEntry( - linear_constraint=d, variable=x, coefficient=1.0 - ) - ], - list(mod.linear_constraint_matrix_entries()), - ) - self.assertCountEqual([d], mod.column_nonzeros(x)) - self.assertCountEqual([], mod.column_nonzeros(y)) - self.assertCountEqual( - [repr(variables.LinearTerm(variable=x, coefficient=1.0))], - [repr(term) for term in d.terms()], - ) - - def test_linear_constraint_matrix_wrong_model(self) -> None: - mod1 = model.Model(name="test_model1") - x1 = mod1.add_binary_variable(name="x") - mod2 = model.Model(name="test_model2") - mod2.add_binary_variable(name="x") - c2 = mod2.add_linear_constraint(lb=0.0, ub=1.0, name="c") - with self.assertRaises(ValueError): - c2.set_coefficient(x1, 1.0) - - def test_export(self) -> None: - mod = model.Model(name="test_model") - mod.objective.offset = 2.0 - mod.objective.is_maximize = True - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=2.0, name="c") - c.set_coefficient(x, 1.0) - c.set_coefficient(y, 2.0) - mod.objective.set_linear_coefficient(y, 3.0) - expected = model_pb2.ModelProto( - name="test_model", - variables=model_pb2.VariablesProto( - ids=[0, 1], - lower_bounds=[0.0, 0.0], - upper_bounds=[1.0, 1.0], - integers=[True, True], - names=["x", "y"], + def test_name(self) -> None: + mod = model.Model(name="test_model") + self.assertEqual("test_model", mod.name) + + def test_name_empty(self) -> None: + mod = model.Model() + self.assertEqual("", mod.name) + + def test_add_and_read_variables(self) -> None: + mod = model.Model(name="test_model") + v1 = mod.add_variable(lb=-1.0, ub=2.5, is_integer=True, name="x") + v2 = mod.add_variable() + self.assertEqual(-1.0, v1.lower_bound) + self.assertEqual(2.5, v1.upper_bound) + self.assertTrue(v1.integer) + self.assertEqual("x", v1.name) + self.assertEqual(0, v1.id) + self.assertEqual("x", str(v1)) + self.assertEqual("", repr(v1)) + + self.assertEqual(-math.inf, v2.lower_bound) + self.assertEqual(math.inf, v2.upper_bound) + self.assertFalse(v2.integer) + self.assertEqual("", v2.name) + self.assertEqual(1, v2.id) + self.assertEqual("variable_1", str(v2)) + self.assertEqual("", repr(v2)) + + self.assertListEqual([v1, v2], list(mod.variables())) + self.assertEqual(v1, mod.get_variable(0)) + self.assertEqual(v2, mod.get_variable(1)) + + def test_add_integer_variable(self) -> None: + mod = model.Model( + name="test_model", + ) + v1 = mod.add_integer_variable(lb=-1.0, ub=2.5, name="x") + self.assertEqual(-1.0, v1.lower_bound) + self.assertEqual(2.5, v1.upper_bound) + self.assertTrue(v1.integer) + self.assertEqual("x", v1.name) + self.assertEqual(0, v1.id) + + def test_add_binary_variable(self) -> None: + mod = model.Model(name="test_model") + v1 = mod.add_binary_variable(name="x") + self.assertEqual(0.0, v1.lower_bound) + self.assertEqual(1.0, v1.upper_bound) + self.assertTrue(v1.integer) + self.assertEqual("x", v1.name) + self.assertEqual(0, v1.id) + + def test_update_variable(self) -> None: + mod = model.Model(name="test_model") + v1 = mod.add_variable(lb=-1.0, ub=2.5, is_integer=True, name="x") + v1.lower_bound = -math.inf + v1.upper_bound = -3.0 + v1.integer = False + self.assertEqual(-math.inf, v1.lower_bound) + self.assertEqual(-3.0, v1.upper_bound) + self.assertFalse(v1.integer) + + def test_read_deleted_variable(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + mod.delete_variable(x) + with self.assertRaises(ValueError): + x.lower_bound # pylint: disable=pointless-statement + + def test_update_deleted_variable(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + mod.delete_variable(x) + with self.assertRaises(ValueError): + x.upper_bound = 2.0 + + def test_add_and_read_linear_constraints(self) -> None: + mod = model.Model(name="test_model") + c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c") + d = mod.add_linear_constraint() + self.assertEqual(-1.0, c.lower_bound) + self.assertEqual(2.5, c.upper_bound) + self.assertEqual("c", c.name) + self.assertEqual(0, c.id) + self.assertEqual("c", str(c)) + self.assertEqual("", repr(c)) + + self.assertEqual(-math.inf, d.lower_bound) + self.assertEqual(math.inf, d.upper_bound) + self.assertEqual("", d.name) + self.assertEqual(1, d.id) + self.assertEqual("linear_constraint_1", str(d)) + self.assertEqual("", repr(d)) + + self.assertListEqual([c, d], list(mod.linear_constraints())) + self.assertEqual(c, mod.get_linear_constraint(0)) + self.assertEqual(d, mod.get_linear_constraint(1)) + + def test_linear_constraint_as_bounded_expression(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c", expr=3 * x - 2 * y) + bounded_expr = c.as_bounded_linear_expression() + self.assertEqual(bounded_expr.lower_bound, -1.0) + self.assertEqual(bounded_expr.upper_bound, 2.5) + expr = variables.as_flat_linear_expression(bounded_expr.expression) + self.assertEqual(expr.offset, 0.0) + self.assertDictEqual(dict(expr.terms), {x: 3.0, y: -2.0}) + + def test_update_linear_constraint(self) -> None: + mod = model.Model(name="test_model") + c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c") + c.lower_bound = -math.inf + c.upper_bound = -3.0 + self.assertEqual(-math.inf, c.lower_bound) + self.assertEqual(-3.0, c.upper_bound) + + def test_read_deleted_linear_constraint(self) -> None: + mod = model.Model(name="test_model") + c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c") + mod.delete_linear_constraint(c) + with self.assertRaises(ValueError): + c.name # pylint: disable=pointless-statement + + def test_update_deleted_linear_constraint(self) -> None: + mod = model.Model(name="test_model") + c = mod.add_linear_constraint(lb=-1.0, ub=2.5, name="c") + mod.delete_linear_constraint(c) + with self.assertRaises(ValueError): + c.lower_bound = -12.0 + + def test_linear_constraint_matrix(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(ub=1.0, name="d") + c.set_coefficient(x, 1.0) + c.set_coefficient(y, 0.0) + d.set_coefficient(x, 2.0) + d.set_coefficient(z, -1.0) + self.assertEqual(1.0, c.get_coefficient(x)) + self.assertEqual(0.0, c.get_coefficient(y)) + self.assertEqual(0.0, c.get_coefficient(z)) + self.assertEqual(2.0, d.get_coefficient(x)) + self.assertEqual(0.0, d.get_coefficient(y)) + self.assertEqual(-1.0, d.get_coefficient(z)) + + self.assertEqual(c.name, "c") + self.assertEqual(d.name, "d") + + self.assertCountEqual([c, d], mod.column_nonzeros(x)) + self.assertCountEqual([], mod.column_nonzeros(y)) + self.assertCountEqual([d], mod.column_nonzeros(z)) + + self.assertCountEqual([x], mod.row_nonzeros(c)) + self.assertCountEqual([x, z], mod.row_nonzeros(d)) + + self.assertCountEqual( + [repr(variables.LinearTerm(variable=x, coefficient=1.0))], + [repr(term) for term in c.terms()], + ) + self.assertCountEqual( + [ + repr(variables.LinearTerm(variable=x, coefficient=2.0)), + repr(variables.LinearTerm(variable=z, coefficient=-1.0)), + ], + [repr(term) for term in d.terms()], + ) + + self.assertCountEqual( + [ + linear_constraints.LinearConstraintMatrixEntry( + linear_constraint=c, variable=x, coefficient=1.0 ), - linear_constraints=model_pb2.LinearConstraintsProto( - ids=[0], lower_bounds=[0.0], upper_bounds=[2.0], names=["c"] + linear_constraints.LinearConstraintMatrixEntry( + linear_constraint=d, variable=x, coefficient=2.0 ), - objective=model_pb2.ObjectiveProto( - maximize=True, - offset=2.0, - linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[1], values=[3.0] - ), + linear_constraints.LinearConstraintMatrixEntry( + linear_constraint=d, variable=z, coefficient=-1.0 ), - linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0, 0], column_ids=[0, 1], coefficients=[1.0, 2.0] + ], + list(mod.linear_constraint_matrix_entries()), + ) + + def test_linear_constraint_expression(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + c = mod.add_linear_constraint(lb=0.0, expr=x + 1.0, ub=1.0, name="c") + self.assertEqual(1.0, c.get_coefficient(x)) + self.assertEqual(0.0, c.get_coefficient(y)) + self.assertEqual(0.0, c.get_coefficient(z)) + self.assertEqual(-1.0, c.lower_bound) + self.assertEqual(0.0, c.upper_bound) + + d = mod.add_linear_constraint(ub=1.0, expr=2 * x - z, name="d") + self.assertEqual(2.0, d.get_coefficient(x)) + self.assertEqual(0.0, d.get_coefficient(y)) + self.assertEqual(-1.0, d.get_coefficient(z)) + self.assertEqual(-math.inf, d.lower_bound) + self.assertEqual(1.0, d.upper_bound) + + e = mod.add_linear_constraint(lb=0.0) + self.assertEqual(0.0, e.get_coefficient(x)) + self.assertEqual(0.0, e.get_coefficient(y)) + self.assertEqual(0.0, e.get_coefficient(z)) + self.assertEqual(0.0, e.lower_bound) + self.assertEqual(math.inf, e.upper_bound) + + f = mod.add_linear_constraint(expr=1, ub=2) + self.assertEqual(0.0, f.get_coefficient(x)) + self.assertEqual(0.0, f.get_coefficient(y)) + self.assertEqual(0.0, f.get_coefficient(z)) + self.assertEqual(-math.inf, f.lower_bound) + self.assertEqual(1, f.upper_bound) + + def test_linear_constraint_bounded_expression(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + + c = mod.add_linear_constraint((0.0 <= x + 1.0) <= 1.0, name="c") + self.assertEqual(1.0, c.get_coefficient(x)) + self.assertEqual(0.0, c.get_coefficient(y)) + self.assertEqual(0.0, c.get_coefficient(z)) + self.assertEqual(-1.0, c.lower_bound) + self.assertEqual(0.0, c.upper_bound) + + def test_linear_constraint_upper_bounded_expression(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + + d = mod.add_linear_constraint(2 * x - z + 2.0 <= 1.0, name="d") + self.assertEqual(2.0, d.get_coefficient(x)) + self.assertEqual(0.0, d.get_coefficient(y)) + self.assertEqual(-1.0, d.get_coefficient(z)) + self.assertEqual(-math.inf, d.lower_bound) + self.assertEqual(-1.0, d.upper_bound) + + def test_linear_constraint_lower_bounded_expression(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + + e = mod.add_linear_constraint(1.0 <= x + y + 2.0, name="e") + self.assertEqual(1.0, e.get_coefficient(x)) + self.assertEqual(1.0, e.get_coefficient(y)) + self.assertEqual(0.0, e.get_coefficient(z)) + self.assertEqual(-1.0, e.lower_bound) + self.assertEqual(math.inf, e.upper_bound) + + def test_linear_constraint_number_eq_expression(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + + f = mod.add_linear_constraint(1.0 == x + y + 2.0, name="e") + self.assertEqual(1.0, f.get_coefficient(x)) + self.assertEqual(1.0, f.get_coefficient(y)) + self.assertEqual(0.0, f.get_coefficient(z)) + self.assertEqual(-1.0, f.lower_bound) + self.assertEqual(-1.0, f.upper_bound) + + def test_linear_constraint_expression_eq_expression(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + + f = mod.add_linear_constraint(1.0 - x == y + 2.0, name="e") + self.assertEqual(-1.0, f.get_coefficient(x)) + self.assertEqual(-1.0, f.get_coefficient(y)) + self.assertEqual(0.0, f.get_coefficient(z)) + self.assertEqual(1.0, f.lower_bound) + self.assertEqual(1.0, f.upper_bound) + + def test_linear_constraint_variable_eq_variable(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + + f = mod.add_linear_constraint(x == y, name="e") + self.assertEqual(1.0, f.get_coefficient(x)) + self.assertEqual(-1.0, f.get_coefficient(y)) + self.assertEqual(0.0, f.get_coefficient(z)) + self.assertEqual(0.0, f.lower_bound) + self.assertEqual(0.0, f.upper_bound) + + def test_linear_constraint_errors(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + + with self.assertRaisesRegex( + TypeError, + "Unsupported type for bounded_expr.*bool.*!= constraints.*constant" + " left", + ): + mod.add_linear_constraint(x != y) + + with self.assertRaisesRegex(TypeError, "!= constraints.*"): + mod.add_linear_constraint(x + y != y) + + with self.assertRaisesRegex(TypeError, "!= constraints.*"): + mod.add_linear_constraint(x != x + y) + + with self.assertRaisesRegex( + TypeError, + "Unsupported type for bounded_expr.*bool.*!= constraints.*constant" + " left", + ): + mod.add_linear_constraint(1 <= 2) # pylint: disable=comparison-of-constants + + with self.assertRaisesRegex( + TypeError, + "Unsupported type for bounded_expr.*bool.*!= constraints.*constant" + " left", + ): + mod.add_linear_constraint(1 <= 0) # pylint: disable=comparison-of-constants + + with self.assertRaisesRegex( + TypeError, + "Unsupported type for bounded_expr.*bool.*!= constraints.*constant" + " left", + ): + mod.add_linear_constraint(True) + + with self.assertRaisesRegex( + TypeError, + "__bool__ is unsupported.*\n.*two-sided or ranged linear inequality", + ): + mod.add_linear_constraint(x <= y <= z) + + with self.assertRaisesRegex( + TypeError, + "unsupported operand.*\n.*two or more non-constant linear expressions", + ): + mod.add_linear_constraint((x <= y) <= z) + + with self.assertRaisesRegex( + TypeError, + "unsupported operand.*\n.*two or more non-constant linear expressions", + ): + mod.add_linear_constraint(x <= (y <= z)) + + with self.assertRaisesRegex(TypeError, "unsupported operand.*"): + mod.add_linear_constraint((0 <= x) >= z) + + with self.assertRaisesRegex(AssertionError, "lb cannot be specified.*"): + mod.add_linear_constraint(x + y == 1, lb=1) + + with self.assertRaisesRegex(AssertionError, "ub cannot be specified.*"): + mod.add_linear_constraint(x + y == 1, ub=1) + + with self.assertRaisesRegex(AssertionError, "expr cannot be specified.*"): + mod.add_linear_constraint(x + y == 1, expr=2 * x) + + with self.assertRaisesRegex( + TypeError, "Unsupported type for expr argument.*str" + ): + mod.add_linear_constraint(expr="string") # pytype: disable=wrong-arg-types + + with self.assertRaisesRegex(ValueError, ".*infinite offset."): + mod.add_linear_constraint(expr=math.inf, lb=0.0) + + def test_linear_constraint_matrix_with_variable_deletion(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + c.set_coefficient(x, 1.0) + c.set_coefficient(y, 2.0) + d.set_coefficient(x, 1.0) + mod.delete_variable(x) + self.assertCountEqual( + [ + linear_constraints.LinearConstraintMatrixEntry( + linear_constraint=c, variable=y, coefficient=2.0 + ) + ], + mod.linear_constraint_matrix_entries(), + ) + self.assertCountEqual([c], mod.column_nonzeros(y)) + self.assertCountEqual( + [repr(variables.LinearTerm(variable=y, coefficient=2.0))], + [repr(term) for term in c.terms()], + ) + self.assertCountEqual([], d.terms()) + with self.assertRaises(ValueError): + c.get_coefficient(x) + + def test_linear_constraint_matrix_with_linear_constraint_deletion( + self, + ) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + c.set_coefficient(x, 1.0) + c.set_coefficient(y, 2.0) + d.set_coefficient(x, 1.0) + mod.delete_linear_constraint(c) + self.assertCountEqual( + [ + linear_constraints.LinearConstraintMatrixEntry( + linear_constraint=d, variable=x, coefficient=1.0 + ) + ], + list(mod.linear_constraint_matrix_entries()), + ) + self.assertCountEqual([d], mod.column_nonzeros(x)) + self.assertCountEqual([], mod.column_nonzeros(y)) + self.assertCountEqual( + [repr(variables.LinearTerm(variable=x, coefficient=1.0))], + [repr(term) for term in d.terms()], + ) + + def test_linear_constraint_matrix_wrong_model(self) -> None: + mod1 = model.Model(name="test_model1") + x1 = mod1.add_binary_variable(name="x") + mod2 = model.Model(name="test_model2") + mod2.add_binary_variable(name="x") + c2 = mod2.add_linear_constraint(lb=0.0, ub=1.0, name="c") + with self.assertRaises(ValueError): + c2.set_coefficient(x1, 1.0) + + def test_export(self) -> None: + mod = model.Model(name="test_model") + mod.objective.offset = 2.0 + mod.objective.is_maximize = True + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=2.0, name="c") + c.set_coefficient(x, 1.0) + c.set_coefficient(y, 2.0) + mod.objective.set_linear_coefficient(y, 3.0) + expected = model_pb2.ModelProto( + name="test_model", + variables=model_pb2.VariablesProto( + ids=[0, 1], + lower_bounds=[0.0, 0.0], + upper_bounds=[1.0, 1.0], + integers=[True, True], + names=["x", "y"], + ), + linear_constraints=model_pb2.LinearConstraintsProto( + ids=[0], lower_bounds=[0.0], upper_bounds=[2.0], names=["c"] + ), + objective=model_pb2.ObjectiveProto( + maximize=True, + offset=2.0, + linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[1], values=[3.0] ), - ) - self.assert_protos_equiv(expected, mod.export_model()) - - def test_update_tracker_simple(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - t = mod.add_update_tracker() - x.upper_bound = 2.0 - expected = model_update_pb2.ModelUpdateProto( - variable_updates=model_update_pb2.VariableUpdatesProto( - upper_bounds=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[2.0] - ) + ), + linear_constraint_matrix=sparse_containers_pb2.SparseDoubleMatrixProto( + row_ids=[0, 0], column_ids=[0, 1], coefficients=[1.0, 2.0] + ), + ) + self.assert_protos_equiv(expected, mod.export_model()) + + def test_update_tracker_simple(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + t = mod.add_update_tracker() + x.upper_bound = 2.0 + expected = model_update_pb2.ModelUpdateProto( + variable_updates=model_update_pb2.VariableUpdatesProto( + upper_bounds=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[2.0] ) ) - self.assert_protos_equiv(expected, t.export_update()) - self.assert_protos_equiv(expected, t.export_update()) - t.advance_checkpoint() - self.assertIsNone(t.export_update()) - - def test_two_update_trackers(self) -> None: - mod = model.Model(name="test_model") - t1 = mod.add_update_tracker() - x = mod.add_binary_variable(name="x") - t2 = mod.add_update_tracker() - x.upper_bound = 2.0 - expected1 = model_update_pb2.ModelUpdateProto( - new_variables=model_pb2.VariablesProto( - ids=[0], - lower_bounds=[0.0], - upper_bounds=[2.0], - integers=[True], - names=["x"], - ) + ) + self.assert_protos_equiv(expected, t.export_update()) + self.assert_protos_equiv(expected, t.export_update()) + t.advance_checkpoint() + self.assertIsNone(t.export_update()) + + def test_two_update_trackers(self) -> None: + mod = model.Model(name="test_model") + t1 = mod.add_update_tracker() + x = mod.add_binary_variable(name="x") + t2 = mod.add_update_tracker() + x.upper_bound = 2.0 + expected1 = model_update_pb2.ModelUpdateProto( + new_variables=model_pb2.VariablesProto( + ids=[0], + lower_bounds=[0.0], + upper_bounds=[2.0], + integers=[True], + names=["x"], ) - expected2 = model_update_pb2.ModelUpdateProto( - variable_updates=model_update_pb2.VariableUpdatesProto( - upper_bounds=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[2.0] - ) + ) + expected2 = model_update_pb2.ModelUpdateProto( + variable_updates=model_update_pb2.VariableUpdatesProto( + upper_bounds=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[2.0] ) ) - self.assert_protos_equiv(expected1, t1.export_update()) - self.assert_protos_equiv(expected2, t2.export_update()) - - def test_remove_tracker(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - t1 = mod.add_update_tracker() - t2 = mod.add_update_tracker() - x.upper_bound = 2.0 - mod.remove_update_tracker(t1) - x.lower_bound = -1.0 - expected = model_update_pb2.ModelUpdateProto( - variable_updates=model_update_pb2.VariableUpdatesProto( - upper_bounds=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[2.0] - ), - lower_bounds=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[-1.0] - ), - ) + ) + self.assert_protos_equiv(expected1, t1.export_update()) + self.assert_protos_equiv(expected2, t2.export_update()) + + def test_remove_tracker(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + t1 = mod.add_update_tracker() + t2 = mod.add_update_tracker() + x.upper_bound = 2.0 + mod.remove_update_tracker(t1) + x.lower_bound = -1.0 + expected = model_update_pb2.ModelUpdateProto( + variable_updates=model_update_pb2.VariableUpdatesProto( + upper_bounds=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[2.0] + ), + lower_bounds=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[-1.0] + ), ) - self.assert_protos_equiv(expected, t2.export_update()) - with self.assertRaises(ValueError): - t1.export_update() - with self.assertRaises(ValueError): - t1.advance_checkpoint() - with self.assertRaises(ValueError): - mod.remove_update_tracker(t1) + ) + self.assert_protos_equiv(expected, t2.export_update()) + with self.assertRaises(ValueError): + t1.export_update() + with self.assertRaises(ValueError): + t1.advance_checkpoint() + with self.assertRaises(ValueError): + mod.remove_update_tracker(t1) class WrongAttributeTest(absltest.TestCase): - """Test case that verifies that wrong attributes are detected. + """Test case that verifies that wrong attributes are detected. - In some the tests below we have to disable pytype checks since it also detects - the issue now that the code uses __slots__. - """ + In some the tests below we have to disable pytype checks since it also detects + the issue now that the code uses __slots__. + """ - def test_variable(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_variable() - with self.assertRaises(AttributeError): - x.loer_bnd = 4 # pytype: disable=not-writable + def test_variable(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_variable() + with self.assertRaises(AttributeError): + x.loer_bnd = 4 # pytype: disable=not-writable - def test_linear_constraint(self) -> None: - mod = model.Model(name="test_model") - c = mod.add_linear_constraint() - with self.assertRaises(AttributeError): - c.uper_bound = 8 # pytype: disable=not-writable + def test_linear_constraint(self) -> None: + mod = model.Model(name="test_model") + c = mod.add_linear_constraint() + with self.assertRaises(AttributeError): + c.uper_bound = 8 # pytype: disable=not-writable - def test_objective(self) -> None: - mod = model.Model(name="test_model") - with self.assertRaises(AttributeError): - mod.objective.matimuze = True # pytype: disable=not-writable + def test_objective(self) -> None: + mod = model.Model(name="test_model") + with self.assertRaises(AttributeError): + mod.objective.matimuze = True # pytype: disable=not-writable - def test_aux_objective(self) -> None: - mod = model.Model(name="test_model") - aux = mod.add_auxiliary_objective(priority=1) - with self.assertRaises(AttributeError): - aux.matimuze = True # pytype: disable=not-writable + def test_aux_objective(self) -> None: + mod = model.Model(name="test_model") + aux = mod.add_auxiliary_objective(priority=1) + with self.assertRaises(AttributeError): + aux.matimuze = True # pytype: disable=not-writable - def test_model(self) -> None: - mod = model.Model(name="test_model") - with self.assertRaises(AttributeError): - mod.objectize = None # pytype: disable=not-writable + def test_model(self) -> None: + mod = model.Model(name="test_model") + with self.assertRaises(AttributeError): + mod.objectize = None # pytype: disable=not-writable if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/normalize.py b/ortools/math_opt/python/normalize.py index 789f9d74da1..08fb1a2c904 100644 --- a/ortools/math_opt/python/normalize.py +++ b/ortools/math_opt/python/normalize.py @@ -26,52 +26,52 @@ def math_opt_normalize_proto(protobuf_message: message.Message) -> None: - """Clears all non-duration submessages that are not in one_ofs. + """Clears all non-duration submessages that are not in one_ofs. - A message is considered `empty` if: - * every non-optional scalar fields has its default value, - * every optional scalar field is unset, - * every repeated/map fields is empty - * every oneof is unset, - * every duration field is unset - * all other message fields (singular, not oneof, not duration) are `empty`. - This function clears all `empty` fields from `message`. + A message is considered `empty` if: + * every non-optional scalar fields has its default value, + * every optional scalar field is unset, + * every repeated/map fields is empty + * every oneof is unset, + * every duration field is unset + * all other message fields (singular, not oneof, not duration) are `empty`. + This function clears all `empty` fields from `message`. - This is useful for testing. + This is useful for testing. - Args: - protobuf_message: The Message object to clear. - """ - for field, value in protobuf_message.ListFields(): - if field.type != field.TYPE_MESSAGE: - continue - if field.label == field.LABEL_REPEATED: - # Now the repeated case, recursively normalize each member. Note that - # there is no field presence for repeated fields, so we don't need to call - # ClearField(). - # - # Maps need to be handled specially. - if ( - field.message_type.has_options - and field.message_type.GetOptions().map_entry - ): - if ( - field.message_type.fields_by_number[2].type - == descriptor.FieldDescriptor.TYPE_MESSAGE - ): - for item in value.values(): - math_opt_normalize_proto(item) - # The remaining case is a regular repeated field (a list). - else: - for item in value: - math_opt_normalize_proto(item) - continue - # Last case, the non-repeated sub-message - math_opt_normalize_proto(value) - # If field value is empty, not a Duration, and not in a oneof, clear it. + Args: + protobuf_message: The Message object to clear. + """ + for field, value in protobuf_message.ListFields(): + if field.type != field.TYPE_MESSAGE: + continue + if field.label == field.LABEL_REPEATED: + # Now the repeated case, recursively normalize each member. Note that + # there is no field presence for repeated fields, so we don't need to call + # ClearField(). + # + # Maps need to be handled specially. + if ( + field.message_type.has_options + and field.message_type.GetOptions().map_entry + ): if ( - not value.ListFields() - and field.message_type != duration_pb2.Duration.DESCRIPTOR - and field.containing_oneof is None + field.message_type.fields_by_number[2].type + == descriptor.FieldDescriptor.TYPE_MESSAGE ): - protobuf_message.ClearField(field.name) + for item in value.values(): + math_opt_normalize_proto(item) + # The remaining case is a regular repeated field (a list). + else: + for item in value: + math_opt_normalize_proto(item) + continue + # Last case, the non-repeated sub-message + math_opt_normalize_proto(value) + # If field value is empty, not a Duration, and not in a oneof, clear it. + if ( + not value.ListFields() + and field.message_type != duration_pb2.Duration.DESCRIPTOR + and field.containing_oneof is None + ): + protobuf_message.ClearField(field.name) diff --git a/ortools/math_opt/python/normalize_test.py b/ortools/math_opt/python/normalize_test.py index e182f8a2069..a72348b9fbb 100644 --- a/ortools/math_opt/python/normalize_test.py +++ b/ortools/math_opt/python/normalize_test.py @@ -29,102 +29,104 @@ class MathOptProtoAssertionsTest( compare_proto.MathOptProtoAssertions, absltest.TestCase ): - def test_removes_empty_message(self) -> None: - model_with_empty_vars = model_pb2.ModelProto() - model_with_empty_vars.variables.SetInParent() - with self.assertRaisesRegex(AssertionError, ".*variables.*"): - self.assert_protos_equal(model_with_empty_vars, model_pb2.ModelProto()) + def test_removes_empty_message(self) -> None: + model_with_empty_vars = model_pb2.ModelProto() + model_with_empty_vars.variables.SetInParent() + with self.assertRaisesRegex(AssertionError, ".*variables.*"): + self.assert_protos_equal(model_with_empty_vars, model_pb2.ModelProto()) - normalize.math_opt_normalize_proto(model_with_empty_vars) - self.assert_protos_equal(model_with_empty_vars, model_pb2.ModelProto()) + normalize.math_opt_normalize_proto(model_with_empty_vars) + self.assert_protos_equal(model_with_empty_vars, model_pb2.ModelProto()) - def test_keeps_nonempty_message(self) -> None: - model_with_vars = model_pb2.ModelProto() - model_with_vars.variables.ids[:] = [1, 3] + def test_keeps_nonempty_message(self) -> None: + model_with_vars = model_pb2.ModelProto() + model_with_vars.variables.ids[:] = [1, 3] - expected = model_pb2.ModelProto() - expected.variables.ids[:] = [1, 3] + expected = model_pb2.ModelProto() + expected.variables.ids[:] = [1, 3] - normalize.math_opt_normalize_proto(model_with_vars) - self.assert_protos_equal(model_with_vars, expected) + normalize.math_opt_normalize_proto(model_with_vars) + self.assert_protos_equal(model_with_vars, expected) - def test_keeps_optional_scalar_at_default_message(self) -> None: - objective = model_update_pb2.ObjectiveUpdatesProto(offset_update=0.0) + def test_keeps_optional_scalar_at_default_message(self) -> None: + objective = model_update_pb2.ObjectiveUpdatesProto(offset_update=0.0) - normalize.math_opt_normalize_proto(objective) + normalize.math_opt_normalize_proto(objective) - wrong = model_update_pb2.ObjectiveUpdatesProto() - with self.assertRaisesRegex(AssertionError, ".*offset_update.*"): - self.assert_protos_equal(objective, wrong) + wrong = model_update_pb2.ObjectiveUpdatesProto() + with self.assertRaisesRegex(AssertionError, ".*offset_update.*"): + self.assert_protos_equal(objective, wrong) - expected = model_update_pb2.ObjectiveUpdatesProto(offset_update=0.0) - self.assert_protos_equal(objective, expected) + expected = model_update_pb2.ObjectiveUpdatesProto(offset_update=0.0) + self.assert_protos_equal(objective, expected) - def test_recursive_cleanup(self) -> None: - update_rec_empty = model_update_pb2.ModelUpdateProto() - update_rec_empty.variable_updates.lower_bounds.SetInParent() + def test_recursive_cleanup(self) -> None: + update_rec_empty = model_update_pb2.ModelUpdateProto() + update_rec_empty.variable_updates.lower_bounds.SetInParent() - normalize.math_opt_normalize_proto(update_rec_empty) - self.assert_protos_equal(update_rec_empty, model_update_pb2.ModelUpdateProto()) + normalize.math_opt_normalize_proto(update_rec_empty) + self.assert_protos_equal( + update_rec_empty, model_update_pb2.ModelUpdateProto() + ) - def test_duration_no_cleanup(self) -> None: - params = parameters_pb2.SolveParametersProto() - params.time_limit.SetInParent() + def test_duration_no_cleanup(self) -> None: + params = parameters_pb2.SolveParametersProto() + params.time_limit.SetInParent() - with self.assertRaisesRegex(AssertionError, ".*time_limit.*"): - normalize.math_opt_normalize_proto(params) - self.assert_protos_equal(params, parameters_pb2.SolveParametersProto()) + with self.assertRaisesRegex(AssertionError, ".*time_limit.*"): + normalize.math_opt_normalize_proto(params) + self.assert_protos_equal(params, parameters_pb2.SolveParametersProto()) - def test_repeated_scalar_no_cleanup(self) -> None: - vec = sparse_containers_pb2.SparseDoubleVectorProto() - vec.ids[:] = [0, 0] - normalize.math_opt_normalize_proto(vec) + def test_repeated_scalar_no_cleanup(self) -> None: + vec = sparse_containers_pb2.SparseDoubleVectorProto() + vec.ids[:] = [0, 0] + normalize.math_opt_normalize_proto(vec) - expected = sparse_containers_pb2.SparseDoubleVectorProto() - expected.ids[:] = [0, 0] + expected = sparse_containers_pb2.SparseDoubleVectorProto() + expected.ids[:] = [0, 0] - self.assert_protos_equal(vec, expected) + self.assert_protos_equal(vec, expected) - def test_reaches_into_map(self) -> None: - model = model_pb2.ModelProto() - model.quadratic_constraints[2].linear_terms.SetInParent() - normalize.math_opt_normalize_proto(model) + def test_reaches_into_map(self) -> None: + model = model_pb2.ModelProto() + model.quadratic_constraints[2].linear_terms.SetInParent() + normalize.math_opt_normalize_proto(model) - expected = model_pb2.ModelProto() - expected.quadratic_constraints[2] # pylint: disable=pointless-statement + expected = model_pb2.ModelProto() + expected.quadratic_constraints[2] # pylint: disable=pointless-statement - self.assert_protos_equal(model, expected) + self.assert_protos_equal(model, expected) - def test_reaches_into_vector(self) -> None: - params = model_parameters_pb2.ModelSolveParametersProto() - params.solution_hints.add().variable_values.SetInParent() - normalize.math_opt_normalize_proto(params) + def test_reaches_into_vector(self) -> None: + params = model_parameters_pb2.ModelSolveParametersProto() + params.solution_hints.add().variable_values.SetInParent() + normalize.math_opt_normalize_proto(params) - expected = model_parameters_pb2.ModelSolveParametersProto() - expected.solution_hints.add() + expected = model_parameters_pb2.ModelSolveParametersProto() + expected.solution_hints.add() - self.assert_protos_equal(params, expected) + self.assert_protos_equal(params, expected) - def test_oneof_is_not_cleared(self) -> None: - result = result_pb2.SolveResultProto() - result.gscip_output.SetInParent() - normalize.math_opt_normalize_proto(result) + def test_oneof_is_not_cleared(self) -> None: + result = result_pb2.SolveResultProto() + result.gscip_output.SetInParent() + normalize.math_opt_normalize_proto(result) - expected = result_pb2.SolveResultProto() - expected.gscip_output.SetInParent() + expected = result_pb2.SolveResultProto() + expected.gscip_output.SetInParent() - self.assert_protos_equal(result, expected) + self.assert_protos_equal(result, expected) - def test_reaches_into_oneof(self) -> None: - result = result_pb2.SolveResultProto() - result.gscip_output.stats.SetInParent() - normalize.math_opt_normalize_proto(result) + def test_reaches_into_oneof(self) -> None: + result = result_pb2.SolveResultProto() + result.gscip_output.stats.SetInParent() + normalize.math_opt_normalize_proto(result) - expected = result_pb2.SolveResultProto() - expected.gscip_output.SetInParent() + expected = result_pb2.SolveResultProto() + expected.gscip_output.SetInParent() - self.assert_protos_equal(result, expected) + self.assert_protos_equal(result, expected) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/normalized_inequality.py b/ortools/math_opt/python/normalized_inequality.py index 20cc3e3dcfb..22cd143e74a 100644 --- a/ortools/math_opt/python/normalized_inequality.py +++ b/ortools/math_opt/python/normalized_inequality.py @@ -49,74 +49,76 @@ def _bool_error() -> TypeError: - return TypeError( - "Unsupported type for bounded_expr argument:" - " bool. This error can occur when trying to add != constraints " - "(which are not supported) or inequalities/equalities with constant " - "left-hand-side and right-hand-side (which are redundant or make a " - "model infeasible)." - ) + return TypeError( + "Unsupported type for bounded_expr argument:" + " bool. This error can occur when trying to add != constraints " + "(which are not supported) or inequalities/equalities with constant " + "left-hand-side and right-hand-side (which are redundant or make a " + "model infeasible)." + ) @dataclasses.dataclass class NormalizedLinearInequality: - """Represents an inequality lb <= expr <= ub where expr's offset is zero.""" - - lb: float - ub: float - coefficients: Mapping[variables.Variable, float] - - def __init__( - self, - *, - lb: Optional[float], - ub: Optional[float], - expr: Optional[variables.LinearTypes], - ) -> None: - """Raises a ValueError if expr's offset is infinite.""" - lb = -math.inf if lb is None else lb - ub = math.inf if ub is None else ub - expr = 0.0 if expr is None else expr - if not isinstance(expr, (int, float, variables.LinearBase)): - raise TypeError( - f"Unsupported type for expr argument: {type(expr).__name__!r}." - ) - - flat_expr = variables.as_flat_linear_expression(expr) - if math.isinf(flat_expr.offset): - raise ValueError( - "Trying to create a linear constraint whose expression has an" - " infinite offset." - ) - self.lb = lb - flat_expr.offset - self.ub = ub - flat_expr.offset - self.coefficients = flat_expr.terms + """Represents an inequality lb <= expr <= ub where expr's offset is zero.""" + + lb: float + ub: float + coefficients: Mapping[variables.Variable, float] + + def __init__( + self, + *, + lb: Optional[float], + ub: Optional[float], + expr: Optional[variables.LinearTypes], + ) -> None: + """Raises a ValueError if expr's offset is infinite.""" + lb = -math.inf if lb is None else lb + ub = math.inf if ub is None else ub + expr = 0.0 if expr is None else expr + if not isinstance(expr, (int, float, variables.LinearBase)): + raise TypeError( + f"Unsupported type for expr argument: {type(expr).__name__!r}." + ) + + flat_expr = variables.as_flat_linear_expression(expr) + if math.isinf(flat_expr.offset): + raise ValueError( + "Trying to create a linear constraint whose expression has an" + " infinite offset." + ) + self.lb = lb - flat_expr.offset + self.ub = ub - flat_expr.offset + self.coefficients = flat_expr.terms def _normalize_bounded_linear_expression( bounded_expr: variables.BoundedLinearTypes, ) -> NormalizedLinearInequality: - """Converts a bounded linear expression into a NormalizedLinearInequality.""" - if isinstance(bounded_expr, variables.VarEqVar): - return NormalizedLinearInequality( - lb=0.0, - ub=0.0, - expr=bounded_expr.first_variable - bounded_expr.second_variable, - ) - elif isinstance(bounded_expr, _BoundedExpressions): - if isinstance(bounded_expr.expression, (int, float, variables.LinearBase)): - return NormalizedLinearInequality( - lb=bounded_expr.lower_bound, - ub=bounded_expr.upper_bound, - expr=bounded_expr.expression, - ) - else: - raise TypeError( - "Bad type of expression in bounded_expr:" - f" {type(bounded_expr.expression).__name__!r}." - ) + """Converts a bounded linear expression into a NormalizedLinearInequality.""" + if isinstance(bounded_expr, variables.VarEqVar): + return NormalizedLinearInequality( + lb=0.0, + ub=0.0, + expr=bounded_expr.first_variable - bounded_expr.second_variable, + ) + elif isinstance(bounded_expr, _BoundedExpressions): + if isinstance(bounded_expr.expression, (int, float, variables.LinearBase)): + return NormalizedLinearInequality( + lb=bounded_expr.lower_bound, + ub=bounded_expr.upper_bound, + expr=bounded_expr.expression, + ) else: - raise TypeError(f"bounded_expr has bad type: {type(bounded_expr).__name__!r}.") + raise TypeError( + "Bad type of expression in bounded_expr:" + f" {type(bounded_expr.expression).__name__!r}." + ) + else: + raise TypeError( + f"bounded_expr has bad type: {type(bounded_expr).__name__!r}." + ) # TODO(b/227214976): Update the note below and link to pytype bug number. @@ -129,111 +131,115 @@ def as_normalized_linear_inequality( ub: Optional[float] = None, expr: Optional[variables.LinearTypes] = None, ) -> NormalizedLinearInequality: - """Builds a NormalizedLinearInequality. - - If bounded_expr is not None, then all other arguments must be None. - - If expr has a nonzero offset, it will be subtracted from both lb and ub. - - When bounded_expr is unset and a named argument is unset, we use the defaults: - * lb: -math.inf - * ub: math.inf - * expr: 0 - - Args: - bounded_expr: a linear inequality describing the constraint. - lb: The lower bound when bounded_expr is None. - ub: The upper bound if bounded_expr is None. - expr: The expression when bounded_expr is None. - - Returns: - A NormalizedLinearInequality representing the linear constraint. - """ - if isinstance(bounded_expr, bool): - raise _bool_error() - if bounded_expr is not None: - if lb is not None: - raise AssertionError( - "lb cannot be specified when bounded_expr is not None." - ) - if ub is not None: - raise AssertionError( - "ub cannot be specified when bounded_expr is not None." - ) - if expr is not None: - raise AssertionError( - "expr cannot be specified when bounded_expr is not None" - ) - return _normalize_bounded_linear_expression(bounded_expr) - # Note: NormalizedLinearInequality() will runtime check the type of expr. - return NormalizedLinearInequality(lb=lb, ub=ub, expr=expr) + """Builds a NormalizedLinearInequality. + + If bounded_expr is not None, then all other arguments must be None. + + If expr has a nonzero offset, it will be subtracted from both lb and ub. + + When bounded_expr is unset and a named argument is unset, we use the defaults: + * lb: -math.inf + * ub: math.inf + * expr: 0 + + Args: + bounded_expr: a linear inequality describing the constraint. + lb: The lower bound when bounded_expr is None. + ub: The upper bound if bounded_expr is None. + expr: The expression when bounded_expr is None. + + Returns: + A NormalizedLinearInequality representing the linear constraint. + """ + if isinstance(bounded_expr, bool): + raise _bool_error() + if bounded_expr is not None: + if lb is not None: + raise AssertionError( + "lb cannot be specified when bounded_expr is not None." + ) + if ub is not None: + raise AssertionError( + "ub cannot be specified when bounded_expr is not None." + ) + if expr is not None: + raise AssertionError( + "expr cannot be specified when bounded_expr is not None" + ) + return _normalize_bounded_linear_expression(bounded_expr) + # Note: NormalizedLinearInequality() will runtime check the type of expr. + return NormalizedLinearInequality(lb=lb, ub=ub, expr=expr) @dataclasses.dataclass class NormalizedQuadraticInequality: - """Represents an inequality lb <= expr <= ub where expr's offset is zero.""" - - lb: float - ub: float - linear_coefficients: Mapping[variables.Variable, float] - quadratic_coefficients: Mapping[variables.QuadraticTermKey, float] - - def __init__( - self, - *, - lb: Optional[float] = None, - ub: Optional[float] = None, - expr: Optional[variables.QuadraticTypes] = None, - ) -> None: - """Raises a ValueError if expr's offset is infinite.""" - lb = -math.inf if lb is None else lb - ub = math.inf if ub is None else ub - expr = 0.0 if expr is None else expr - if not isinstance( - expr, (int, float, variables.LinearBase, variables.QuadraticBase) - ): - raise TypeError( - f"Unsupported type for expr argument: {type(expr).__name__!r}." - ) - flat_expr = variables.as_flat_quadratic_expression(expr) - if math.isinf(flat_expr.offset): - raise ValueError( - "Trying to create a quadratic constraint whose expression has an" - " infinite offset." - ) - self.lb = lb - flat_expr.offset - self.ub = ub - flat_expr.offset - self.linear_coefficients = flat_expr.linear_terms - self.quadratic_coefficients = flat_expr.quadratic_terms + """Represents an inequality lb <= expr <= ub where expr's offset is zero.""" + + lb: float + ub: float + linear_coefficients: Mapping[variables.Variable, float] + quadratic_coefficients: Mapping[variables.QuadraticTermKey, float] + + def __init__( + self, + *, + lb: Optional[float] = None, + ub: Optional[float] = None, + expr: Optional[variables.QuadraticTypes] = None, + ) -> None: + """Raises a ValueError if expr's offset is infinite.""" + lb = -math.inf if lb is None else lb + ub = math.inf if ub is None else ub + expr = 0.0 if expr is None else expr + if not isinstance( + expr, (int, float, variables.LinearBase, variables.QuadraticBase) + ): + raise TypeError( + f"Unsupported type for expr argument: {type(expr).__name__!r}." + ) + flat_expr = variables.as_flat_quadratic_expression(expr) + if math.isinf(flat_expr.offset): + raise ValueError( + "Trying to create a quadratic constraint whose expression has an" + " infinite offset." + ) + self.lb = lb - flat_expr.offset + self.ub = ub - flat_expr.offset + self.linear_coefficients = flat_expr.linear_terms + self.quadratic_coefficients = flat_expr.quadratic_terms def _normalize_bounded_quadratic_expression( - bounded_expr: Union[variables.BoundedQuadraticTypes, variables.BoundedLinearTypes], + bounded_expr: Union[ + variables.BoundedQuadraticTypes, variables.BoundedLinearTypes + ], ) -> NormalizedQuadraticInequality: - """Converts a bounded quadratic expression into a NormalizedQuadraticInequality.""" - if isinstance(bounded_expr, variables.VarEqVar): - return NormalizedQuadraticInequality( - lb=0.0, - ub=0.0, - expr=bounded_expr.first_variable - bounded_expr.second_variable, - ) - elif isinstance(bounded_expr, _BoundedExpressions): - if isinstance( - bounded_expr.expression, - (int, float, variables.LinearBase, variables.QuadraticBase), - ): - return NormalizedQuadraticInequality( - lb=bounded_expr.lower_bound, - ub=bounded_expr.upper_bound, - expr=bounded_expr.expression, - ) - else: - raise TypeError( - "bounded_expr.expression has bad type:" - f" {type(bounded_expr.expression).__name__!r}." - ) + """Converts a bounded quadratic expression into a NormalizedQuadraticInequality.""" + if isinstance(bounded_expr, variables.VarEqVar): + return NormalizedQuadraticInequality( + lb=0.0, + ub=0.0, + expr=bounded_expr.first_variable - bounded_expr.second_variable, + ) + elif isinstance(bounded_expr, _BoundedExpressions): + if isinstance( + bounded_expr.expression, + (int, float, variables.LinearBase, variables.QuadraticBase), + ): + return NormalizedQuadraticInequality( + lb=bounded_expr.lower_bound, + ub=bounded_expr.upper_bound, + expr=bounded_expr.expression, + ) else: - raise TypeError(f"bounded_expr has bad type: {type(bounded_expr).__name__!r}.") + raise TypeError( + "bounded_expr.expression has bad type:" + f" {type(bounded_expr.expression).__name__!r}." + ) + else: + raise TypeError( + f"bounded_expr has bad type: {type(bounded_expr).__name__!r}." + ) # TODO(b/227214976): Update the note below and link to pytype bug number. @@ -241,47 +247,49 @@ def _normalize_bounded_quadratic_expression( # issue. Passing a bool for bounded_expr will raise an error in runtime. def as_normalized_quadratic_inequality( bounded_expr: Optional[ - Union[bool, variables.BoundedLinearTypes, variables.BoundedQuadraticTypes] + Union[ + bool, variables.BoundedLinearTypes, variables.BoundedQuadraticTypes + ] ] = None, *, lb: Optional[float] = None, ub: Optional[float] = None, expr: Optional[variables.QuadraticTypes] = None, ) -> NormalizedQuadraticInequality: - """Builds a NormalizedLinearInequality. - - If bounded_expr is not None, then all other arguments must be None. - - If expr has a nonzero offset, it will be subtracted from both lb and ub. - - When bounded_expr is unset and a named argument is unset, we use the defaults: - * lb: -math.inf - * ub: math.inf - * expr: 0 - - Args: - bounded_expr: a quadratic inequality describing the constraint. - lb: The lower bound when bounded_expr is None. - ub: The upper bound if bounded_expr is None. - expr: The expression when bounded_expr is None. - - Returns: - A NormalizedLinearInequality representing the linear constraint. - """ - if isinstance(bounded_expr, bool): - raise _bool_error() - if bounded_expr is not None: - if lb is not None: - raise AssertionError( - "lb cannot be specified when bounded_expr is not None." - ) - if ub is not None: - raise AssertionError( - "ub cannot be specified when bounded_expr is not None." - ) - if expr is not None: - raise AssertionError( - "expr cannot be specified when bounded_expr is not None" - ) - return _normalize_bounded_quadratic_expression(bounded_expr) - return NormalizedQuadraticInequality(lb=lb, ub=ub, expr=expr) + """Builds a NormalizedLinearInequality. + + If bounded_expr is not None, then all other arguments must be None. + + If expr has a nonzero offset, it will be subtracted from both lb and ub. + + When bounded_expr is unset and a named argument is unset, we use the defaults: + * lb: -math.inf + * ub: math.inf + * expr: 0 + + Args: + bounded_expr: a quadratic inequality describing the constraint. + lb: The lower bound when bounded_expr is None. + ub: The upper bound if bounded_expr is None. + expr: The expression when bounded_expr is None. + + Returns: + A NormalizedLinearInequality representing the linear constraint. + """ + if isinstance(bounded_expr, bool): + raise _bool_error() + if bounded_expr is not None: + if lb is not None: + raise AssertionError( + "lb cannot be specified when bounded_expr is not None." + ) + if ub is not None: + raise AssertionError( + "ub cannot be specified when bounded_expr is not None." + ) + if expr is not None: + raise AssertionError( + "expr cannot be specified when bounded_expr is not None" + ) + return _normalize_bounded_quadratic_expression(bounded_expr) + return NormalizedQuadraticInequality(lb=lb, ub=ub, expr=expr) diff --git a/ortools/math_opt/python/normalized_inequality_test.py b/ortools/math_opt/python/normalized_inequality_test.py index 11b818115a8..29591aceda1 100644 --- a/ortools/math_opt/python/normalized_inequality_test.py +++ b/ortools/math_opt/python/normalized_inequality_test.py @@ -24,270 +24,280 @@ class NormalizedLinearInequalityTest(absltest.TestCase): - def test_init_all_present(self) -> None: - mod = model.Model() - x = mod.add_variable() - inequality = normalized_inequality.NormalizedLinearInequality( - lb=-4.0, expr=2.0 * x + 3.0, ub=8.0 - ) - self.assertAlmostEqual(inequality.lb, -7.0, delta=1e-10) - self.assertAlmostEqual(inequality.ub, 5.0, delta=1e-10) - self.assertEqual(inequality.coefficients, {x: 2.0}) - - def test_init_all_missing(self) -> None: - inequality = normalized_inequality.NormalizedLinearInequality( - lb=None, expr=None, ub=None - ) - self.assertEqual(inequality.lb, -math.inf) - self.assertEqual(inequality.ub, math.inf) - self.assertEmpty(inequality.coefficients) - - def test_init_offset_only(self) -> None: - inequality = normalized_inequality.NormalizedLinearInequality( - lb=None, expr=2.0, ub=None - ) - self.assertEqual(inequality.lb, -math.inf) - self.assertEqual(inequality.ub, math.inf) - self.assertEmpty(inequality.coefficients) - - def test_init_infinite_offset_error(self) -> None: - with self.assertRaisesRegex(ValueError, "infinite"): - normalized_inequality.NormalizedLinearInequality( - lb=1.0, expr=math.inf, ub=2.0 - ) - - def test_init_expr_wrong_type_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaises(TypeError): - normalized_inequality.NormalizedLinearInequality( - lb=1.0, expr=x * x, ub=2.0 - ) # pytype: disable=wrong-arg-types - - def test_as_normalized_inequality_from_parts(self) -> None: - mod = model.Model() - x = mod.add_variable() - inequality = normalized_inequality.as_normalized_linear_inequality( - lb=-4.0, expr=2.0 * x + 3.0, ub=8.0 - ) - self.assertAlmostEqual(inequality.lb, -7.0, delta=1e-10) - self.assertAlmostEqual(inequality.ub, 5.0, delta=1e-10) - self.assertEqual(inequality.coefficients, {x: 2.0}) - - def test_as_normalized_inequality_from_none(self) -> None: - inequality = normalized_inequality.as_normalized_linear_inequality() - self.assertEqual(inequality.lb, -math.inf) - self.assertEqual(inequality.ub, math.inf) - self.assertEmpty(inequality.coefficients) - - def test_as_normalized_inequality_from_var_eq_var(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - inequality = normalized_inequality.as_normalized_linear_inequality(x == y) - self.assertEqual(inequality.lb, 0.0) - self.assertEqual(inequality.ub, 0.0) - self.assertEqual(inequality.coefficients.keys(), {x, y}) - self.assertEqual(set(inequality.coefficients.values()), {-1.0, 1.0}) - - def test_as_normalized_inequality_from_upper_bounded_expr(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - inequality = normalized_inequality.as_normalized_linear_inequality( - 3.0 * x + y <= 2.0 - ) - self.assertEqual(inequality.lb, -math.inf) - self.assertEqual(inequality.ub, 2.0) - self.assertEqual(inequality.coefficients, {x: 3.0, y: 1.0}) - - def test_as_normalized_inequality_from_lower_bounded_expr(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - inequality = normalized_inequality.as_normalized_linear_inequality( - 2.0 <= 3.0 * x + y - ) - self.assertEqual(inequality.lb, 2.0) - self.assertEqual(inequality.ub, math.inf) - self.assertEqual(inequality.coefficients, {x: 3.0, y: 1.0}) - - def test_lb_and_bounded_expr_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(AssertionError, "lb cannot be specified"): - normalized_inequality.as_normalized_linear_inequality(x <= 1.0, lb=-3.0) - - def test_ub_and_bounded_expr_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(AssertionError, "ub cannot be specified"): - normalized_inequality.as_normalized_linear_inequality(x <= 1.0, ub=-3.0) - - def test_expr_and_bounded_expr_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(AssertionError, "expr cannot be specified"): - normalized_inequality.as_normalized_linear_inequality(x <= 1.0, expr=3 * x) - - def test_bounded_expr_bad_type_raise_error(self) -> None: - with self.assertRaisesRegex(TypeError, "bounded_expr has bad type"): - normalized_inequality.as_normalized_linear_inequality( - "dogdog" - ) # pytype: disable=wrong-arg-types - - def test_bounded_expr_inner_expr_bad_type_raise_error(self) -> None: - with self.assertRaisesRegex( - TypeError, "Bad type of expression in bounded_expr" - ): - bounded = bounded_expressions.BoundedExpression( - lower_bound=1.0, expression="dogdog", upper_bound=1.0 - ) - normalized_inequality.as_normalized_linear_inequality( - bounded - ) # pytype: disable=wrong-arg-types + def test_init_all_present(self) -> None: + mod = model.Model() + x = mod.add_variable() + inequality = normalized_inequality.NormalizedLinearInequality( + lb=-4.0, expr=2.0 * x + 3.0, ub=8.0 + ) + self.assertAlmostEqual(inequality.lb, -7.0, delta=1e-10) + self.assertAlmostEqual(inequality.ub, 5.0, delta=1e-10) + self.assertEqual(inequality.coefficients, {x: 2.0}) + + def test_init_all_missing(self) -> None: + inequality = normalized_inequality.NormalizedLinearInequality( + lb=None, expr=None, ub=None + ) + self.assertEqual(inequality.lb, -math.inf) + self.assertEqual(inequality.ub, math.inf) + self.assertEmpty(inequality.coefficients) + + def test_init_offset_only(self) -> None: + inequality = normalized_inequality.NormalizedLinearInequality( + lb=None, expr=2.0, ub=None + ) + self.assertEqual(inequality.lb, -math.inf) + self.assertEqual(inequality.ub, math.inf) + self.assertEmpty(inequality.coefficients) + + def test_init_infinite_offset_error(self) -> None: + with self.assertRaisesRegex(ValueError, "infinite"): + normalized_inequality.NormalizedLinearInequality( + lb=1.0, expr=math.inf, ub=2.0 + ) + + def test_init_expr_wrong_type_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaises(TypeError): + normalized_inequality.NormalizedLinearInequality( + lb=1.0, expr=x * x, ub=2.0 + ) # pytype: disable=wrong-arg-types + + def test_as_normalized_inequality_from_parts(self) -> None: + mod = model.Model() + x = mod.add_variable() + inequality = normalized_inequality.as_normalized_linear_inequality( + lb=-4.0, expr=2.0 * x + 3.0, ub=8.0 + ) + self.assertAlmostEqual(inequality.lb, -7.0, delta=1e-10) + self.assertAlmostEqual(inequality.ub, 5.0, delta=1e-10) + self.assertEqual(inequality.coefficients, {x: 2.0}) + + def test_as_normalized_inequality_from_none(self) -> None: + inequality = normalized_inequality.as_normalized_linear_inequality() + self.assertEqual(inequality.lb, -math.inf) + self.assertEqual(inequality.ub, math.inf) + self.assertEmpty(inequality.coefficients) + + def test_as_normalized_inequality_from_var_eq_var(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + inequality = normalized_inequality.as_normalized_linear_inequality(x == y) + self.assertEqual(inequality.lb, 0.0) + self.assertEqual(inequality.ub, 0.0) + self.assertEqual(inequality.coefficients.keys(), {x, y}) + self.assertEqual(set(inequality.coefficients.values()), {-1.0, 1.0}) + + def test_as_normalized_inequality_from_upper_bounded_expr(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + inequality = normalized_inequality.as_normalized_linear_inequality( + 3.0 * x + y <= 2.0 + ) + self.assertEqual(inequality.lb, -math.inf) + self.assertEqual(inequality.ub, 2.0) + self.assertEqual(inequality.coefficients, {x: 3.0, y: 1.0}) + + def test_as_normalized_inequality_from_lower_bounded_expr(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + inequality = normalized_inequality.as_normalized_linear_inequality( + 2.0 <= 3.0 * x + y + ) + self.assertEqual(inequality.lb, 2.0) + self.assertEqual(inequality.ub, math.inf) + self.assertEqual(inequality.coefficients, {x: 3.0, y: 1.0}) + + def test_lb_and_bounded_expr_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(AssertionError, "lb cannot be specified"): + normalized_inequality.as_normalized_linear_inequality(x <= 1.0, lb=-3.0) + + def test_ub_and_bounded_expr_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(AssertionError, "ub cannot be specified"): + normalized_inequality.as_normalized_linear_inequality(x <= 1.0, ub=-3.0) + + def test_expr_and_bounded_expr_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(AssertionError, "expr cannot be specified"): + normalized_inequality.as_normalized_linear_inequality( + x <= 1.0, expr=3 * x + ) + + def test_bounded_expr_bad_type_raise_error(self) -> None: + with self.assertRaisesRegex(TypeError, "bounded_expr has bad type"): + normalized_inequality.as_normalized_linear_inequality( + "dogdog" + ) # pytype: disable=wrong-arg-types + + def test_bounded_expr_inner_expr_bad_type_raise_error(self) -> None: + with self.assertRaisesRegex( + TypeError, "Bad type of expression in bounded_expr" + ): + bounded = bounded_expressions.BoundedExpression( + lower_bound=1.0, expression="dogdog", upper_bound=1.0 + ) + normalized_inequality.as_normalized_linear_inequality( + bounded + ) # pytype: disable=wrong-arg-types def _quad_coef_dict( quad_inequality: normalized_inequality.NormalizedQuadraticInequality, ) -> Dict[Tuple[variables.Variable, variables.Variable], float]: - return { - (key.first_var, key.second_var): coef - for (key, coef) in quad_inequality.quadratic_coefficients.items() - } + return { + (key.first_var, key.second_var): coef + for (key, coef) in quad_inequality.quadratic_coefficients.items() + } class NormalizedQuadraticInequalityTest(absltest.TestCase): - def test_init_all_present(self) -> None: - mod = model.Model() - x = mod.add_variable() - inequality = normalized_inequality.NormalizedQuadraticInequality( - lb=-4.0, expr=4.0 * x * x + 2.0 * x + 3.0, ub=8.0 - ) - self.assertAlmostEqual(inequality.lb, -7.0, delta=1e-10) - self.assertAlmostEqual(inequality.ub, 5.0, delta=1e-10) - self.assertEqual(inequality.linear_coefficients, {x: 2.0}) - self.assertEqual(_quad_coef_dict(inequality), {(x, x): 4.0}) - - def test_init_all_missing(self) -> None: - inequality = normalized_inequality.NormalizedQuadraticInequality( - lb=None, expr=None, ub=None - ) - self.assertEqual(inequality.lb, -math.inf) - self.assertEqual(inequality.ub, math.inf) - self.assertEmpty(inequality.linear_coefficients) - self.assertEmpty(inequality.quadratic_coefficients) - - def test_init_offset_only(self) -> None: - inequality = normalized_inequality.NormalizedQuadraticInequality( - lb=None, expr=2.0, ub=None - ) - self.assertEqual(inequality.lb, -math.inf) - self.assertEqual(inequality.ub, math.inf) - self.assertEmpty(inequality.linear_coefficients) - self.assertEmpty(inequality.quadratic_coefficients) - - def test_init_infinite_offset_error(self) -> None: - with self.assertRaisesRegex(ValueError, "infinite"): - normalized_inequality.NormalizedQuadraticInequality( - lb=1.0, expr=math.inf, ub=2.0 - ) - - def test_init_expr_wrong_type_error(self) -> None: - with self.assertRaises(TypeError): - normalized_inequality.NormalizedQuadraticInequality( - lb=1.0, expr="dog", ub=2.0 - ) # pytype: disable=wrong-arg-types - - def test_as_normalized_inequality_from_parts(self) -> None: - mod = model.Model() - x = mod.add_variable() - inequality = normalized_inequality.as_normalized_quadratic_inequality( - lb=-4.0, expr=4.0 * x * x + 2.0 * x + 3.0, ub=8.0 - ) - self.assertAlmostEqual(inequality.lb, -7.0, delta=1e-10) - self.assertAlmostEqual(inequality.ub, 5.0, delta=1e-10) - self.assertEqual(inequality.linear_coefficients, {x: 2.0}) - self.assertEqual(_quad_coef_dict(inequality), {(x, x): 4.0}) - - def test_as_normalized_inequality_from_none(self) -> None: - inequality = normalized_inequality.as_normalized_quadratic_inequality() - self.assertEqual(inequality.lb, -math.inf) - self.assertEqual(inequality.ub, math.inf) - self.assertEmpty(inequality.linear_coefficients) - self.assertEmpty(inequality.quadratic_coefficients) - - def test_as_normalized_inequality_from_var_eq_var(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - inequality = normalized_inequality.as_normalized_quadratic_inequality(x == y) - self.assertEqual(inequality.lb, 0.0) - self.assertEqual(inequality.ub, 0.0) - self.assertEqual(inequality.linear_coefficients.keys(), {x, y}) - self.assertEqual(set(inequality.linear_coefficients.values()), {-1.0, 1.0}) - self.assertEmpty(inequality.quadratic_coefficients) - - def test_as_normalized_inequality_from_upper_bounded_expr(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - inequality = normalized_inequality.as_normalized_quadratic_inequality( - 4.0 * x * x + 3.0 * x + y <= 2.0 - ) - self.assertEqual(inequality.lb, -math.inf) - self.assertEqual(inequality.ub, 2.0) - self.assertEqual(inequality.linear_coefficients, {x: 3.0, y: 1.0}) - self.assertEqual(_quad_coef_dict(inequality), {(x, x): 4.0}) - - def test_as_normalized_inequality_from_lower_bounded_expr(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - inequality = normalized_inequality.as_normalized_quadratic_inequality( - 2.0 <= 4.0 * x * x + 3.0 * x + y - ) - self.assertEqual(inequality.lb, 2.0) - self.assertEqual(inequality.ub, math.inf) - self.assertEqual(inequality.linear_coefficients, {x: 3.0, y: 1.0}) - self.assertEqual(_quad_coef_dict(inequality), {(x, x): 4.0}) - - def test_lb_and_boundex_expr_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(AssertionError, "lb cannot be specified"): - normalized_inequality.as_normalized_quadratic_inequality(x <= 1.0, lb=-3.0) - - def test_ub_and_boundex_expr_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(AssertionError, "ub cannot be specified"): - normalized_inequality.as_normalized_quadratic_inequality(x <= 1.0, ub=-3.0) - - def test_expr_and_boundex_expr_error(self) -> None: - mod = model.Model() - x = mod.add_variable() - with self.assertRaisesRegex(AssertionError, "expr cannot be specified"): - normalized_inequality.as_normalized_quadratic_inequality( - x <= 1.0, expr=3 * x - ) - - def test_bounded_expr_bad_type_raise_error(self) -> None: - with self.assertRaisesRegex(TypeError, "bounded_expr has bad type"): - normalized_inequality.as_normalized_quadratic_inequality( - "dogdog" - ) # pytype: disable=wrong-arg-types - - def test_bounded_expr_inner_expr_bad_type_raise_error(self) -> None: - with self.assertRaisesRegex(TypeError, "bounded_expr.expression has bad type"): - bounded = bounded_expressions.BoundedExpression( - lower_bound=1.0, expression="dogdog", upper_bound=1.0 - ) - normalized_inequality.as_normalized_quadratic_inequality( - bounded - ) # pytype: disable=wrong-arg-types + def test_init_all_present(self) -> None: + mod = model.Model() + x = mod.add_variable() + inequality = normalized_inequality.NormalizedQuadraticInequality( + lb=-4.0, expr=4.0 * x * x + 2.0 * x + 3.0, ub=8.0 + ) + self.assertAlmostEqual(inequality.lb, -7.0, delta=1e-10) + self.assertAlmostEqual(inequality.ub, 5.0, delta=1e-10) + self.assertEqual(inequality.linear_coefficients, {x: 2.0}) + self.assertEqual(_quad_coef_dict(inequality), {(x, x): 4.0}) + + def test_init_all_missing(self) -> None: + inequality = normalized_inequality.NormalizedQuadraticInequality( + lb=None, expr=None, ub=None + ) + self.assertEqual(inequality.lb, -math.inf) + self.assertEqual(inequality.ub, math.inf) + self.assertEmpty(inequality.linear_coefficients) + self.assertEmpty(inequality.quadratic_coefficients) + + def test_init_offset_only(self) -> None: + inequality = normalized_inequality.NormalizedQuadraticInequality( + lb=None, expr=2.0, ub=None + ) + self.assertEqual(inequality.lb, -math.inf) + self.assertEqual(inequality.ub, math.inf) + self.assertEmpty(inequality.linear_coefficients) + self.assertEmpty(inequality.quadratic_coefficients) + + def test_init_infinite_offset_error(self) -> None: + with self.assertRaisesRegex(ValueError, "infinite"): + normalized_inequality.NormalizedQuadraticInequality( + lb=1.0, expr=math.inf, ub=2.0 + ) + + def test_init_expr_wrong_type_error(self) -> None: + with self.assertRaises(TypeError): + normalized_inequality.NormalizedQuadraticInequality( + lb=1.0, expr="dog", ub=2.0 + ) # pytype: disable=wrong-arg-types + + def test_as_normalized_inequality_from_parts(self) -> None: + mod = model.Model() + x = mod.add_variable() + inequality = normalized_inequality.as_normalized_quadratic_inequality( + lb=-4.0, expr=4.0 * x * x + 2.0 * x + 3.0, ub=8.0 + ) + self.assertAlmostEqual(inequality.lb, -7.0, delta=1e-10) + self.assertAlmostEqual(inequality.ub, 5.0, delta=1e-10) + self.assertEqual(inequality.linear_coefficients, {x: 2.0}) + self.assertEqual(_quad_coef_dict(inequality), {(x, x): 4.0}) + + def test_as_normalized_inequality_from_none(self) -> None: + inequality = normalized_inequality.as_normalized_quadratic_inequality() + self.assertEqual(inequality.lb, -math.inf) + self.assertEqual(inequality.ub, math.inf) + self.assertEmpty(inequality.linear_coefficients) + self.assertEmpty(inequality.quadratic_coefficients) + + def test_as_normalized_inequality_from_var_eq_var(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + inequality = normalized_inequality.as_normalized_quadratic_inequality( + x == y + ) + self.assertEqual(inequality.lb, 0.0) + self.assertEqual(inequality.ub, 0.0) + self.assertEqual(inequality.linear_coefficients.keys(), {x, y}) + self.assertEqual(set(inequality.linear_coefficients.values()), {-1.0, 1.0}) + self.assertEmpty(inequality.quadratic_coefficients) + + def test_as_normalized_inequality_from_upper_bounded_expr(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + inequality = normalized_inequality.as_normalized_quadratic_inequality( + 4.0 * x * x + 3.0 * x + y <= 2.0 + ) + self.assertEqual(inequality.lb, -math.inf) + self.assertEqual(inequality.ub, 2.0) + self.assertEqual(inequality.linear_coefficients, {x: 3.0, y: 1.0}) + self.assertEqual(_quad_coef_dict(inequality), {(x, x): 4.0}) + + def test_as_normalized_inequality_from_lower_bounded_expr(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + inequality = normalized_inequality.as_normalized_quadratic_inequality( + 2.0 <= 4.0 * x * x + 3.0 * x + y + ) + self.assertEqual(inequality.lb, 2.0) + self.assertEqual(inequality.ub, math.inf) + self.assertEqual(inequality.linear_coefficients, {x: 3.0, y: 1.0}) + self.assertEqual(_quad_coef_dict(inequality), {(x, x): 4.0}) + + def test_lb_and_boundex_expr_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(AssertionError, "lb cannot be specified"): + normalized_inequality.as_normalized_quadratic_inequality( + x <= 1.0, lb=-3.0 + ) + + def test_ub_and_boundex_expr_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(AssertionError, "ub cannot be specified"): + normalized_inequality.as_normalized_quadratic_inequality( + x <= 1.0, ub=-3.0 + ) + + def test_expr_and_boundex_expr_error(self) -> None: + mod = model.Model() + x = mod.add_variable() + with self.assertRaisesRegex(AssertionError, "expr cannot be specified"): + normalized_inequality.as_normalized_quadratic_inequality( + x <= 1.0, expr=3 * x + ) + + def test_bounded_expr_bad_type_raise_error(self) -> None: + with self.assertRaisesRegex(TypeError, "bounded_expr has bad type"): + normalized_inequality.as_normalized_quadratic_inequality( + "dogdog" + ) # pytype: disable=wrong-arg-types + + def test_bounded_expr_inner_expr_bad_type_raise_error(self) -> None: + with self.assertRaisesRegex( + TypeError, "bounded_expr.expression has bad type" + ): + bounded = bounded_expressions.BoundedExpression( + lower_bound=1.0, expression="dogdog", upper_bound=1.0 + ) + normalized_inequality.as_normalized_quadratic_inequality( + bounded + ) # pytype: disable=wrong-arg-types if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/objectives.py b/ortools/math_opt/python/objectives.py index 2560fe5a1e1..ecc8ca165f7 100644 --- a/ortools/math_opt/python/objectives.py +++ b/ortools/math_opt/python/objectives.py @@ -23,523 +23,534 @@ class Objective(from_model.FromModel, metaclass=abc.ABCMeta): - """The objective for an optimization model. - - An objective is either of the form: - min o + sum_{i in I} c_i * x_i + sum_{i, j in I, i <= j} q_i,j * x_i * x_j - or - max o + sum_{i in I} c_i * x_i + sum_{(i, j) in Q} q_i,j * x_i * x_j - where x_i are the decision variables of the problem and where all pairs (i, j) - in Q satisfy i <= j. The values of o, c_i and q_i,j should be finite and not - NaN. - - The objective can be configured as follows: - * offset: a float property, o above. Should be finite and not NaN. - * is_maximize: a bool property, if the objective is to maximize or minimize. - * set_linear_coefficient and get_linear_coefficient control the c_i * x_i - terms. The variables must be from the same model as this objective, and - the c_i must be finite and not NaN. The coefficient for any variable not - set is 0.0, and setting a coefficient to 0.0 removes it from I above. - * set_quadratic_coefficient and get_quadratic_coefficient control the - q_i,j * x_i * x_j terms. The variables must be from the same model as this - objective, and the q_i,j must be finite and not NaN. The coefficient for - any pair of variables not set is 0.0, and setting a coefficient to 0.0 - removes the associated (i,j) from Q above. - - Do not create an Objective directly, use Model.objective to access the - objective instead (or Model.add_auxiliary_objective()). Two Objective objects - can represent the same objective (for the same model). They will have the same - underlying Objective.elemental for storing the data. The Objective class is - simply a reference to an Elemental. - - The objective is linear if only linear coefficients are set. This can be - useful to avoid solve-time errors with solvers that do not accept quadratic - objectives. To facilitate this linear objective guarantee we provide three - functions to add to the objective: - * add(), which accepts linear or quadratic expressions, - * add_quadratic(), which also accepts linear or quadratic expressions and - can be used to signal a quadratic objective is possible, and - * add_linear(), which only accepts linear expressions and can be used to - guarantee the objective remains linear. - - For quadratic terms, the order that variables are provided does not matter, - we always canonicalize to first_var <= second_var. So if you set (x1, x2) to 7 - then: - * getting (x2, x1) returns 7 - * setting (x2, x1) to 10 overwrites the value of 7. - Likewise, when we return nonzero quadratic coefficients, we always use the - form first_var <= second_var. - - Most problems have only a single objective, but hierarchical objectives are - supported (see Model.add_auxiliary_objective()). Note that quadratic Auxiliary - objectives are not supported. + """The objective for an optimization model. + + An objective is either of the form: + min o + sum_{i in I} c_i * x_i + sum_{i, j in I, i <= j} q_i,j * x_i * x_j + or + max o + sum_{i in I} c_i * x_i + sum_{(i, j) in Q} q_i,j * x_i * x_j + where x_i are the decision variables of the problem and where all pairs (i, j) + in Q satisfy i <= j. The values of o, c_i and q_i,j should be finite and not + NaN. + + The objective can be configured as follows: + * offset: a float property, o above. Should be finite and not NaN. + * is_maximize: a bool property, if the objective is to maximize or minimize. + * set_linear_coefficient and get_linear_coefficient control the c_i * x_i + terms. The variables must be from the same model as this objective, and + the c_i must be finite and not NaN. The coefficient for any variable not + set is 0.0, and setting a coefficient to 0.0 removes it from I above. + * set_quadratic_coefficient and get_quadratic_coefficient control the + q_i,j * x_i * x_j terms. The variables must be from the same model as this + objective, and the q_i,j must be finite and not NaN. The coefficient for + any pair of variables not set is 0.0, and setting a coefficient to 0.0 + removes the associated (i,j) from Q above. + + Do not create an Objective directly, use Model.objective to access the + objective instead (or Model.add_auxiliary_objective()). Two Objective objects + can represent the same objective (for the same model). They will have the same + underlying Objective.elemental for storing the data. The Objective class is + simply a reference to an Elemental. + + The objective is linear if only linear coefficients are set. This can be + useful to avoid solve-time errors with solvers that do not accept quadratic + objectives. To facilitate this linear objective guarantee we provide three + functions to add to the objective: + * add(), which accepts linear or quadratic expressions, + * add_quadratic(), which also accepts linear or quadratic expressions and + can be used to signal a quadratic objective is possible, and + * add_linear(), which only accepts linear expressions and can be used to + guarantee the objective remains linear. + + For quadratic terms, the order that variables are provided does not matter, + we always canonicalize to first_var <= second_var. So if you set (x1, x2) to 7 + then: + * getting (x2, x1) returns 7 + * setting (x2, x1) to 10 overwrites the value of 7. + Likewise, when we return nonzero quadratic coefficients, we always use the + form first_var <= second_var. + + Most problems have only a single objective, but hierarchical objectives are + supported (see Model.add_auxiliary_objective()). Note that quadratic Auxiliary + objectives are not supported. + """ + + __slots__ = ("_elemental",) + + def __init__(self, elem: elemental.Elemental) -> None: + """Do not invoke directly, prefer Model.objective.""" + self._elemental: elemental.Elemental = elem + + @property + def elemental(self) -> elemental.Elemental: + """The underlying data structure for the model, for internal use only.""" + return self._elemental + + @property + @abc.abstractmethod + def name(self) -> str: + """The immutable name of this objective, for display only.""" + + @property + @abc.abstractmethod + def is_maximize(self) -> bool: + """If true, the direction is maximization, otherwise minimization.""" + + @is_maximize.setter + @abc.abstractmethod + def is_maximize(self, is_maximize: bool) -> None: + ... + + @property + @abc.abstractmethod + def offset(self) -> float: + """A constant added to the objective.""" + + @offset.setter + @abc.abstractmethod + def offset(self, value: float) -> None: + ... + + @property + @abc.abstractmethod + def priority(self) -> int: + """For hierarchical problems, determines the order to apply objectives. + + The objectives are applied from lowest priority to highest. + + The default priority for the primary objective is zero, and auxiliary + objectives must specific a priority at creation time. + + Priority has no effect for problems with only one objective. """ - __slots__ = ("_elemental",) - - def __init__(self, elem: elemental.Elemental) -> None: - """Do not invoke directly, prefer Model.objective.""" - self._elemental: elemental.Elemental = elem - - @property - def elemental(self) -> elemental.Elemental: - """The underlying data structure for the model, for internal use only.""" - return self._elemental - - @property - @abc.abstractmethod - def name(self) -> str: - """The immutable name of this objective, for display only.""" - - @property - @abc.abstractmethod - def is_maximize(self) -> bool: - """If true, the direction is maximization, otherwise minimization.""" - - @is_maximize.setter - @abc.abstractmethod - def is_maximize(self, is_maximize: bool) -> None: ... - - @property - @abc.abstractmethod - def offset(self) -> float: - """A constant added to the objective.""" - - @offset.setter - @abc.abstractmethod - def offset(self, value: float) -> None: ... - - @property - @abc.abstractmethod - def priority(self) -> int: - """For hierarchical problems, determines the order to apply objectives. - - The objectives are applied from lowest priority to highest. - - The default priority for the primary objective is zero, and auxiliary - objectives must specific a priority at creation time. - - Priority has no effect for problems with only one objective. - """ - - @priority.setter - @abc.abstractmethod - def priority(self, value: int) -> None: ... - - @abc.abstractmethod - def set_linear_coefficient(self, var: variables.Variable, coef: float) -> None: - """Sets the coefficient of `var` to `coef` in the objective.""" - - @abc.abstractmethod - def get_linear_coefficient(self, var: variables.Variable) -> float: - """Returns the coefficinet of `var` (or zero if unset).""" - - @abc.abstractmethod - def linear_terms(self) -> Iterator[variables.LinearTerm]: - """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" - - @abc.abstractmethod - def set_quadratic_coefficient( - self, - first_variable: variables.Variable, - second_variable: variables.Variable, - coef: float, - ) -> None: - """Sets the coefficient for product of variables (see class description).""" - - @abc.abstractmethod - def get_quadratic_coefficient( - self, - first_variable: variables.Variable, - second_variable: variables.Variable, - ) -> float: - """Gets the coefficient for product of variables (see class description).""" - - @abc.abstractmethod - def quadratic_terms(self) -> Iterator[variables.QuadraticTerm]: - """Yields quadratic terms with nonzero objective coefficient in undefined order.""" - - @abc.abstractmethod - def clear(self) -> None: - """Clears objective coefficients and offset. Does not change direction.""" - - def as_linear_expression(self) -> variables.LinearExpression: - """Returns an equivalent LinearExpression, or errors if quadratic.""" - if any(self.quadratic_terms()): - raise TypeError("Cannot get a quadratic objective as a linear expression") - return variables.as_flat_linear_expression( - self.offset + variables.LinearSum(self.linear_terms()) - ) - - def as_quadratic_expression(self) -> variables.QuadraticExpression: - """Returns an equivalent QuadraticExpression to this objetive.""" - return variables.as_flat_quadratic_expression( - self.offset - + variables.LinearSum(self.linear_terms()) - + variables.QuadraticSum(self.quadratic_terms()) - ) - - def add(self, objective: variables.QuadraticTypes) -> None: - """Adds the provided expression `objective` to the objective function. - - For a compile time guarantee that the objective remains linear, use - add_linear() instead. - - Args: - objective: the expression to add to the objective function. - """ - if isinstance(objective, (variables.LinearBase, int, float)): - self.add_linear(objective) - elif isinstance(objective, variables.QuadraticBase): - self.add_quadratic(objective) - else: - raise TypeError( - "unsupported type in objective argument for " - f"Objective.add(): {type(objective).__name__!r}" - ) - - def add_linear(self, objective: variables.LinearTypes) -> None: - """Adds the provided linear expression `objective` to the objective function.""" - if not isinstance(objective, (variables.LinearBase, int, float)): - raise TypeError( - "unsupported type in objective argument for " - f"Objective.add_linear(): {type(objective).__name__!r}" - ) - objective_expr = variables.as_flat_linear_expression(objective) - self.offset += objective_expr.offset - for var, coefficient in objective_expr.terms.items(): - self.set_linear_coefficient( - var, self.get_linear_coefficient(var) + coefficient - ) - - def add_quadratic(self, objective: variables.QuadraticTypes) -> None: - """Adds the provided quadratic expression `objective` to the objective function.""" - if not isinstance( - objective, (variables.QuadraticBase, variables.LinearBase, int, float) - ): - raise TypeError( - "unsupported type in objective argument for " - f"Objective.add(): {type(objective).__name__!r}" - ) - objective_expr = variables.as_flat_quadratic_expression(objective) - self.offset += objective_expr.offset - for var, coefficient in objective_expr.linear_terms.items(): - self.set_linear_coefficient( - var, self.get_linear_coefficient(var) + coefficient - ) - for key, coefficient in objective_expr.quadratic_terms.items(): - self.set_quadratic_coefficient( - key.first_var, - key.second_var, - self.get_quadratic_coefficient(key.first_var, key.second_var) - + coefficient, - ) - - def set_to_linear_expression(self, linear_expr: variables.LinearTypes) -> None: - """Sets the objective to optimize to `linear_expr`.""" - if not isinstance(linear_expr, (variables.LinearBase, int, float)): - raise TypeError( - "unsupported type in objective argument for " - f"set_to_linear_expression: {type(linear_expr).__name__!r}" - ) - self.clear() - objective_expr = variables.as_flat_linear_expression(linear_expr) - self.offset = objective_expr.offset - for var, coefficient in objective_expr.terms.items(): - self.set_linear_coefficient(var, coefficient) - - def set_to_quadratic_expression( - self, quadratic_expr: variables.QuadraticTypes - ) -> None: - """Sets the objective to optimize the `quadratic_expr`.""" - if not isinstance( - quadratic_expr, - (variables.QuadraticBase, variables.LinearBase, int, float), - ): - raise TypeError( - "unsupported type in objective argument for " - f"set_to_quadratic_expression: {type(quadratic_expr).__name__!r}" - ) - self.clear() - objective_expr = variables.as_flat_quadratic_expression(quadratic_expr) - self.offset = objective_expr.offset - for var, coefficient in objective_expr.linear_terms.items(): - self.set_linear_coefficient(var, coefficient) - for quad_key, coefficient in objective_expr.quadratic_terms.items(): - self.set_quadratic_coefficient( - quad_key.first_var, quad_key.second_var, coefficient - ) - - def set_to_expression(self, expr: variables.QuadraticTypes) -> None: - """Sets the objective to optimize the `expr`.""" - if isinstance(expr, (variables.LinearBase, int, float)): - self.set_to_linear_expression(expr) - elif isinstance(expr, variables.QuadraticBase): - self.set_to_quadratic_expression(expr) - else: - raise TypeError( - "unsupported type in objective argument for " - f"set_to_expression: {type(expr).__name__!r}" - ) + @priority.setter + @abc.abstractmethod + def priority(self, value: int) -> None: + ... + + @abc.abstractmethod + def set_linear_coefficient( + self, var: variables.Variable, coef: float + ) -> None: + """Sets the coefficient of `var` to `coef` in the objective.""" + + @abc.abstractmethod + def get_linear_coefficient(self, var: variables.Variable) -> float: + """Returns the coefficinet of `var` (or zero if unset).""" + + @abc.abstractmethod + def linear_terms(self) -> Iterator[variables.LinearTerm]: + """Yields variable coefficient pairs for variables with nonzero objective coefficient in undefined order.""" + + @abc.abstractmethod + def set_quadratic_coefficient( + self, + first_variable: variables.Variable, + second_variable: variables.Variable, + coef: float, + ) -> None: + """Sets the coefficient for product of variables (see class description).""" + + @abc.abstractmethod + def get_quadratic_coefficient( + self, + first_variable: variables.Variable, + second_variable: variables.Variable, + ) -> float: + """Gets the coefficient for product of variables (see class description).""" + + @abc.abstractmethod + def quadratic_terms(self) -> Iterator[variables.QuadraticTerm]: + """Yields quadratic terms with nonzero objective coefficient in undefined order.""" + + @abc.abstractmethod + def clear(self) -> None: + """Clears objective coefficients and offset. Does not change direction.""" + + def as_linear_expression(self) -> variables.LinearExpression: + """Returns an equivalent LinearExpression, or errors if quadratic.""" + if any(self.quadratic_terms()): + raise TypeError("Cannot get a quadratic objective as a linear expression") + return variables.as_flat_linear_expression( + self.offset + variables.LinearSum(self.linear_terms()) + ) + + def as_quadratic_expression(self) -> variables.QuadraticExpression: + """Returns an equivalent QuadraticExpression to this objetive.""" + return variables.as_flat_quadratic_expression( + self.offset + + variables.LinearSum(self.linear_terms()) + + variables.QuadraticSum(self.quadratic_terms()) + ) + + def add(self, objective: variables.QuadraticTypes) -> None: + """Adds the provided expression `objective` to the objective function. + + For a compile time guarantee that the objective remains linear, use + add_linear() instead. + + Args: + objective: the expression to add to the objective function. + """ + if isinstance(objective, (variables.LinearBase, int, float)): + self.add_linear(objective) + elif isinstance(objective, variables.QuadraticBase): + self.add_quadratic(objective) + else: + raise TypeError( + "unsupported type in objective argument for " + f"Objective.add(): {type(objective).__name__!r}" + ) + + def add_linear(self, objective: variables.LinearTypes) -> None: + """Adds the provided linear expression `objective` to the objective function.""" + if not isinstance(objective, (variables.LinearBase, int, float)): + raise TypeError( + "unsupported type in objective argument for " + f"Objective.add_linear(): {type(objective).__name__!r}" + ) + objective_expr = variables.as_flat_linear_expression(objective) + self.offset += objective_expr.offset + for var, coefficient in objective_expr.terms.items(): + self.set_linear_coefficient( + var, self.get_linear_coefficient(var) + coefficient + ) + + def add_quadratic(self, objective: variables.QuadraticTypes) -> None: + """Adds the provided quadratic expression `objective` to the objective function.""" + if not isinstance( + objective, (variables.QuadraticBase, variables.LinearBase, int, float) + ): + raise TypeError( + "unsupported type in objective argument for " + f"Objective.add(): {type(objective).__name__!r}" + ) + objective_expr = variables.as_flat_quadratic_expression(objective) + self.offset += objective_expr.offset + for var, coefficient in objective_expr.linear_terms.items(): + self.set_linear_coefficient( + var, self.get_linear_coefficient(var) + coefficient + ) + for key, coefficient in objective_expr.quadratic_terms.items(): + self.set_quadratic_coefficient( + key.first_var, + key.second_var, + self.get_quadratic_coefficient(key.first_var, key.second_var) + + coefficient, + ) + + def set_to_linear_expression( + self, linear_expr: variables.LinearTypes + ) -> None: + """Sets the objective to optimize to `linear_expr`.""" + if not isinstance(linear_expr, (variables.LinearBase, int, float)): + raise TypeError( + "unsupported type in objective argument for " + f"set_to_linear_expression: {type(linear_expr).__name__!r}" + ) + self.clear() + objective_expr = variables.as_flat_linear_expression(linear_expr) + self.offset = objective_expr.offset + for var, coefficient in objective_expr.terms.items(): + self.set_linear_coefficient(var, coefficient) + + def set_to_quadratic_expression( + self, quadratic_expr: variables.QuadraticTypes + ) -> None: + """Sets the objective to optimize the `quadratic_expr`.""" + if not isinstance( + quadratic_expr, + (variables.QuadraticBase, variables.LinearBase, int, float), + ): + raise TypeError( + "unsupported type in objective argument for " + f"set_to_quadratic_expression: {type(quadratic_expr).__name__!r}" + ) + self.clear() + objective_expr = variables.as_flat_quadratic_expression(quadratic_expr) + self.offset = objective_expr.offset + for var, coefficient in objective_expr.linear_terms.items(): + self.set_linear_coefficient(var, coefficient) + for quad_key, coefficient in objective_expr.quadratic_terms.items(): + self.set_quadratic_coefficient( + quad_key.first_var, quad_key.second_var, coefficient + ) + + def set_to_expression(self, expr: variables.QuadraticTypes) -> None: + """Sets the objective to optimize the `expr`.""" + if isinstance(expr, (variables.LinearBase, int, float)): + self.set_to_linear_expression(expr) + elif isinstance(expr, variables.QuadraticBase): + self.set_to_quadratic_expression(expr) + else: + raise TypeError( + "unsupported type in objective argument for " + f"set_to_expression: {type(expr).__name__!r}" + ) class PrimaryObjective(Objective): - """The main objective, but users should program against Objective directly.""" - - __slots__ = () - - @property - def name(self) -> str: - return self._elemental.primary_objective_name - - @property - def is_maximize(self) -> bool: - return self._elemental.get_attr(enums.BoolAttr0.MAXIMIZE, ()) - - @is_maximize.setter - def is_maximize(self, is_maximize: bool) -> None: - self._elemental.set_attr(enums.BoolAttr0.MAXIMIZE, (), is_maximize) - - @property - def offset(self) -> float: - return self._elemental.get_attr(enums.DoubleAttr0.OBJECTIVE_OFFSET, ()) - - @offset.setter - def offset(self, value: float) -> None: - self._elemental.set_attr(enums.DoubleAttr0.OBJECTIVE_OFFSET, (), value) - - @property - def priority(self) -> int: - return self._elemental.get_attr(enums.IntAttr0.OBJECTIVE_PRIORITY, ()) - - @priority.setter - def priority(self, value: int) -> None: - self._elemental.set_attr(enums.IntAttr0.OBJECTIVE_PRIORITY, (), value) - - def set_linear_coefficient(self, var: variables.Variable, coef: float) -> None: - from_model.model_is_same(self, var) - self._elemental.set_attr( - enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT, (var.id,), coef - ) - - def get_linear_coefficient(self, var: variables.Variable) -> float: - from_model.model_is_same(self, var) - return self._elemental.get_attr( - enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT, (var.id,) - ) - - def linear_terms(self) -> Iterator[variables.LinearTerm]: - keys = self._elemental.get_attr_non_defaults( - enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT - ) - var_index = 0 - coefs = self._elemental.get_attrs( - enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT, keys - ) - for i in range(len(keys)): - yield variables.LinearTerm( - variable=variables.Variable(self._elemental, int(keys[i, var_index])), - coefficient=float(coefs[i]), - ) - - def set_quadratic_coefficient( - self, - first_variable: variables.Variable, - second_variable: variables.Variable, - coef: float, - ) -> None: - from_model.model_is_same(self, first_variable) - from_model.model_is_same(self, second_variable) - self._elemental.set_attr( - enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT, - (first_variable.id, second_variable.id), - coef, - ) - - def get_quadratic_coefficient( - self, - first_variable: variables.Variable, - second_variable: variables.Variable, - ) -> float: - from_model.model_is_same(self, first_variable) - from_model.model_is_same(self, second_variable) - return self._elemental.get_attr( - enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT, - (first_variable.id, second_variable.id), - ) - - def quadratic_terms(self) -> Iterator[variables.QuadraticTerm]: - keys = self._elemental.get_attr_non_defaults( - enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT - ) - coefs = self._elemental.get_attrs( - enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT, keys - ) - for i in range(len(keys)): - yield variables.QuadraticTerm( - variables.QuadraticTermKey( - variables.Variable(self._elemental, int(keys[i, 0])), - variables.Variable(self._elemental, int(keys[i, 1])), - ), - coefficient=float(coefs[i]), - ) - - def clear(self) -> None: - self._elemental.clear_attr(enums.DoubleAttr0.OBJECTIVE_OFFSET) - self._elemental.clear_attr(enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT) - self._elemental.clear_attr( - enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT - ) - - def __eq__(self, other: Any) -> bool: - if isinstance(other, PrimaryObjective): - return self._elemental is other._elemental - return False - - def __hash__(self) -> int: - return hash(self._elemental) + """The main objective, but users should program against Objective directly.""" + + __slots__ = () + + @property + def name(self) -> str: + return self._elemental.primary_objective_name + + @property + def is_maximize(self) -> bool: + return self._elemental.get_attr(enums.BoolAttr0.MAXIMIZE, ()) + + @is_maximize.setter + def is_maximize(self, is_maximize: bool) -> None: + self._elemental.set_attr(enums.BoolAttr0.MAXIMIZE, (), is_maximize) + + @property + def offset(self) -> float: + return self._elemental.get_attr(enums.DoubleAttr0.OBJECTIVE_OFFSET, ()) + + @offset.setter + def offset(self, value: float) -> None: + self._elemental.set_attr(enums.DoubleAttr0.OBJECTIVE_OFFSET, (), value) + + @property + def priority(self) -> int: + return self._elemental.get_attr(enums.IntAttr0.OBJECTIVE_PRIORITY, ()) + + @priority.setter + def priority(self, value: int) -> None: + self._elemental.set_attr(enums.IntAttr0.OBJECTIVE_PRIORITY, (), value) + + def set_linear_coefficient( + self, var: variables.Variable, coef: float + ) -> None: + from_model.model_is_same(self, var) + self._elemental.set_attr( + enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT, (var.id,), coef + ) + + def get_linear_coefficient(self, var: variables.Variable) -> float: + from_model.model_is_same(self, var) + return self._elemental.get_attr( + enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT, (var.id,) + ) + + def linear_terms(self) -> Iterator[variables.LinearTerm]: + keys = self._elemental.get_attr_non_defaults( + enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT + ) + var_index = 0 + coefs = self._elemental.get_attrs( + enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT, keys + ) + for i in range(len(keys)): + yield variables.LinearTerm( + variable=variables.Variable(self._elemental, int(keys[i, var_index])), + coefficient=float(coefs[i]), + ) + + def set_quadratic_coefficient( + self, + first_variable: variables.Variable, + second_variable: variables.Variable, + coef: float, + ) -> None: + from_model.model_is_same(self, first_variable) + from_model.model_is_same(self, second_variable) + self._elemental.set_attr( + enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT, + (first_variable.id, second_variable.id), + coef, + ) + + def get_quadratic_coefficient( + self, + first_variable: variables.Variable, + second_variable: variables.Variable, + ) -> float: + from_model.model_is_same(self, first_variable) + from_model.model_is_same(self, second_variable) + return self._elemental.get_attr( + enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT, + (first_variable.id, second_variable.id), + ) + + def quadratic_terms(self) -> Iterator[variables.QuadraticTerm]: + keys = self._elemental.get_attr_non_defaults( + enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT + ) + coefs = self._elemental.get_attrs( + enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT, keys + ) + for i in range(len(keys)): + yield variables.QuadraticTerm( + variables.QuadraticTermKey( + variables.Variable(self._elemental, int(keys[i, 0])), + variables.Variable(self._elemental, int(keys[i, 1])), + ), + coefficient=float(coefs[i]), + ) + + def clear(self) -> None: + self._elemental.clear_attr(enums.DoubleAttr0.OBJECTIVE_OFFSET) + self._elemental.clear_attr(enums.DoubleAttr1.OBJECTIVE_LINEAR_COEFFICIENT) + self._elemental.clear_attr( + enums.SymmetricDoubleAttr2.OBJECTIVE_QUADRATIC_COEFFICIENT + ) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, PrimaryObjective): + return self._elemental is other._elemental + return False + + def __hash__(self) -> int: + return hash(self._elemental) class AuxiliaryObjective(Objective): - """An additional objective that can be optimized after objectives.""" - - __slots__ = ("_id",) - - def __init__(self, elem: elemental.Elemental, obj_id: int) -> None: - """Internal only, prefer Model functions (add_auxiliary_objective() and get_auxiliary_objective()).""" - super().__init__(elem) - if not isinstance(obj_id, int): - raise TypeError( - f"obj_id type should be int, was: {type(obj_id).__name__!r}" - ) - self._id: int = obj_id - - @property - def name(self) -> str: - return self._elemental.get_element_name( - enums.ElementType.AUXILIARY_OBJECTIVE, self._id - ) - - @property - def id(self) -> int: - """Returns the id of this objective.""" - return self._id - - @property - def is_maximize(self) -> bool: - return self._elemental.get_attr( - enums.BoolAttr1.AUXILIARY_OBJECTIVE_MAXIMIZE, (self._id,) - ) - - @is_maximize.setter - def is_maximize(self, is_maximize: bool) -> None: - self._elemental.set_attr( - enums.BoolAttr1.AUXILIARY_OBJECTIVE_MAXIMIZE, - (self._id,), - is_maximize, - ) - - @property - def offset(self) -> float: - return self._elemental.get_attr( - enums.DoubleAttr1.AUXILIARY_OBJECTIVE_OFFSET, (self._id,) - ) - - @offset.setter - def offset(self, value: float) -> None: - self._elemental.set_attr( - enums.DoubleAttr1.AUXILIARY_OBJECTIVE_OFFSET, - (self._id,), - value, - ) - - @property - def priority(self) -> int: - return self._elemental.get_attr( - enums.IntAttr1.AUXILIARY_OBJECTIVE_PRIORITY, (self._id,) - ) - - @priority.setter - def priority(self, value: int) -> None: - self._elemental.set_attr( - enums.IntAttr1.AUXILIARY_OBJECTIVE_PRIORITY, - (self._id,), - value, - ) - - def set_linear_coefficient(self, var: variables.Variable, coef: float) -> None: - from_model.model_is_same(self, var) - self._elemental.set_attr( - enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT, - (self._id, var.id), - coef, - ) - - def get_linear_coefficient(self, var: variables.Variable) -> float: - from_model.model_is_same(self, var) - return self._elemental.get_attr( - enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT, - ( - self._id, - var.id, - ), - ) - - def linear_terms(self) -> Iterator[variables.LinearTerm]: - keys = self._elemental.slice_attr( - enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT, - 0, + """An additional objective that can be optimized after objectives.""" + + __slots__ = ("_id",) + + def __init__(self, elem: elemental.Elemental, obj_id: int) -> None: + """Internal only, prefer Model functions (add_auxiliary_objective() and get_auxiliary_objective()).""" + super().__init__(elem) + if not isinstance(obj_id, int): + raise TypeError( + f"obj_id type should be int, was: {type(obj_id).__name__!r}" + ) + self._id: int = obj_id + + @property + def name(self) -> str: + return self._elemental.get_element_name( + enums.ElementType.AUXILIARY_OBJECTIVE, self._id + ) + + @property + def id(self) -> int: + """Returns the id of this objective.""" + return self._id + + @property + def is_maximize(self) -> bool: + return self._elemental.get_attr( + enums.BoolAttr1.AUXILIARY_OBJECTIVE_MAXIMIZE, (self._id,) + ) + + @is_maximize.setter + def is_maximize(self, is_maximize: bool) -> None: + self._elemental.set_attr( + enums.BoolAttr1.AUXILIARY_OBJECTIVE_MAXIMIZE, + (self._id,), + is_maximize, + ) + + @property + def offset(self) -> float: + return self._elemental.get_attr( + enums.DoubleAttr1.AUXILIARY_OBJECTIVE_OFFSET, (self._id,) + ) + + @offset.setter + def offset(self, value: float) -> None: + self._elemental.set_attr( + enums.DoubleAttr1.AUXILIARY_OBJECTIVE_OFFSET, + (self._id,), + value, + ) + + @property + def priority(self) -> int: + return self._elemental.get_attr( + enums.IntAttr1.AUXILIARY_OBJECTIVE_PRIORITY, (self._id,) + ) + + @priority.setter + def priority(self, value: int) -> None: + self._elemental.set_attr( + enums.IntAttr1.AUXILIARY_OBJECTIVE_PRIORITY, + (self._id,), + value, + ) + + def set_linear_coefficient( + self, var: variables.Variable, coef: float + ) -> None: + from_model.model_is_same(self, var) + self._elemental.set_attr( + enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT, + (self._id, var.id), + coef, + ) + + def get_linear_coefficient(self, var: variables.Variable) -> float: + from_model.model_is_same(self, var) + return self._elemental.get_attr( + enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT, + ( self._id, - ) - var_index = 1 - coefs = self._elemental.get_attrs( - enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT, keys - ) - for i in range(len(keys)): - yield variables.LinearTerm( - variable=variables.Variable(self._elemental, int(keys[i, var_index])), - coefficient=float(coefs[i]), - ) - - def set_quadratic_coefficient( - self, - first_variable: variables.Variable, - second_variable: variables.Variable, - coef: float, - ) -> None: - raise ValueError("Quadratic auxiliary objectives are not supported.") - - def get_quadratic_coefficient( - self, - first_variable: variables.Variable, - second_variable: variables.Variable, - ) -> float: - from_model.model_is_same(self, first_variable) - from_model.model_is_same(self, second_variable) - if not self._elemental.element_exists( - enums.ElementType.VARIABLE, first_variable.id - ): - raise ValueError(f"Variable {first_variable} does not exist") - if not self._elemental.element_exists( - enums.ElementType.VARIABLE, second_variable.id - ): - raise ValueError(f"Variable {second_variable} does not exist") - return 0.0 - - def quadratic_terms(self) -> Iterator[variables.QuadraticTerm]: - return iter(()) - - def clear(self) -> None: - """Clears objective coefficients and offset. Does not change direction.""" - self._elemental.clear_attr(enums.DoubleAttr1.AUXILIARY_OBJECTIVE_OFFSET) - self._elemental.clear_attr( - enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT - ) - - def __eq__(self, other: Any) -> bool: - if isinstance(other, AuxiliaryObjective): - return self._elemental is other._elemental and self._id == other._id - return False - - def __hash__(self) -> int: - return hash((self._elemental, self._id)) + var.id, + ), + ) + + def linear_terms(self) -> Iterator[variables.LinearTerm]: + keys = self._elemental.slice_attr( + enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT, + 0, + self._id, + ) + var_index = 1 + coefs = self._elemental.get_attrs( + enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT, keys + ) + for i in range(len(keys)): + yield variables.LinearTerm( + variable=variables.Variable(self._elemental, int(keys[i, var_index])), + coefficient=float(coefs[i]), + ) + + def set_quadratic_coefficient( + self, + first_variable: variables.Variable, + second_variable: variables.Variable, + coef: float, + ) -> None: + raise ValueError("Quadratic auxiliary objectives are not supported.") + + def get_quadratic_coefficient( + self, + first_variable: variables.Variable, + second_variable: variables.Variable, + ) -> float: + from_model.model_is_same(self, first_variable) + from_model.model_is_same(self, second_variable) + if not self._elemental.element_exists( + enums.ElementType.VARIABLE, first_variable.id + ): + raise ValueError(f"Variable {first_variable} does not exist") + if not self._elemental.element_exists( + enums.ElementType.VARIABLE, second_variable.id + ): + raise ValueError(f"Variable {second_variable} does not exist") + return 0.0 + + def quadratic_terms(self) -> Iterator[variables.QuadraticTerm]: + return iter(()) + + def clear(self) -> None: + """Clears objective coefficients and offset. Does not change direction.""" + self._elemental.clear_attr(enums.DoubleAttr1.AUXILIARY_OBJECTIVE_OFFSET) + self._elemental.clear_attr( + enums.DoubleAttr2.AUXILIARY_OBJECTIVE_LINEAR_COEFFICIENT + ) + + def __eq__(self, other: Any) -> bool: + if isinstance(other, AuxiliaryObjective): + return self._elemental is other._elemental and self._id == other._id + return False + + def __hash__(self) -> int: + return hash((self._elemental, self._id)) diff --git a/ortools/math_opt/python/objectives_test.py b/ortools/math_opt/python/objectives_test.py index fcdc8ee1d03..079d44089f1 100644 --- a/ortools/math_opt/python/objectives_test.py +++ b/ortools/math_opt/python/objectives_test.py @@ -26,13 +26,13 @@ def _model_and_objective( primary: bool, obj_name="" ) -> Tuple[model.Model, objectives.Objective]: - mod = model.Model(primary_objective_name=(obj_name if primary else "")) - obj = ( - mod.objective - if primary - else mod.add_auxiliary_objective(priority=0, name=obj_name) - ) - return (mod, obj) + mod = model.Model(primary_objective_name=(obj_name if primary else "")) + obj = ( + mod.objective + if primary + else mod.add_auxiliary_objective(priority=0, name=obj_name) + ) + return (mod, obj) def _assert_linear_terms_equal_dict( @@ -40,10 +40,10 @@ def _assert_linear_terms_equal_dict( actual: Iterable[variables.LinearTerm], expected: Dict[variables.Variable, float], ) -> None: - actual = list(actual) - actual_dict = {term.variable: term.coefficient for term in actual} - test.assertDictEqual(actual_dict, expected) - test.assertEqual(len(actual), len(actual_dict)) + actual = list(actual) + actual_dict = {term.variable: term.coefficient for term in actual} + test.assertDictEqual(actual_dict, expected) + test.assertEqual(len(actual), len(actual_dict)) def _assert_quadratic_terms_equal_dict( @@ -51,494 +51,499 @@ def _assert_quadratic_terms_equal_dict( actual: Iterable[variables.QuadraticTerm], expected: Dict[Tuple[variables.Variable, variables.Variable], float], ) -> None: - actual = list(actual) - actual_dict = { - (term.key.first_var, term.key.second_var): term.coefficient for term in actual - } - test.assertDictEqual(actual_dict, expected) - test.assertEqual(len(actual), len(actual_dict)) + actual = list(actual) + actual_dict = { + (term.key.first_var, term.key.second_var): term.coefficient + for term in actual + } + test.assertDictEqual(actual_dict, expected) + test.assertEqual(len(actual), len(actual_dict)) @parameterized.named_parameters(("primary", True), ("auxiliary", False)) class LinearObjectiveTest(parameterized.TestCase): - """Tests that primary and auxiliary objectives handle linear terms.""" - - def test_same_model(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - mod.check_compatible(obj) - - def test_name(self, primary: bool) -> None: - _, obj = _model_and_objective(primary, "my_obj") - self.assertEqual(obj.name, "my_obj") - - def test_maximize(self, primary: bool) -> None: - _, obj = _model_and_objective(primary) - self.assertFalse(obj.is_maximize) - obj.is_maximize = True - self.assertTrue(obj.is_maximize) - - def test_offset(self, primary: bool) -> None: - _, obj = _model_and_objective(primary) - self.assertEqual(obj.offset, 0.0) - obj.offset = 3.2 - self.assertEqual(obj.offset, 3.2) - - def test_priority(self, primary: bool) -> None: - _, obj = _model_and_objective(primary) - self.assertEqual(obj.priority, 0) - obj.priority = 12 - self.assertEqual(obj.priority, 12) - - def test_linear_coefficients_basic(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - y = mod.add_variable() - z = mod.add_variable() - - self.assertEqual(obj.get_linear_coefficient(x), 0.0) - self.assertEqual(obj.get_linear_coefficient(y), 0.0) - self.assertEqual(obj.get_linear_coefficient(z), 0.0) - self.assertEmpty(list(obj.linear_terms())) - - obj.set_linear_coefficient(x, 2.1) - obj.set_linear_coefficient(z, 3.4) - - self.assertEqual(obj.get_linear_coefficient(x), 2.1) - self.assertEqual(obj.get_linear_coefficient(y), 0.0) - self.assertEqual(obj.get_linear_coefficient(z), 3.4) - _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 2.1, z: 3.4}) - - def test_linear_coefficients_restore_to_zero(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - obj.set_linear_coefficient(x, 2.1) - self.assertEqual(obj.get_linear_coefficient(x), 2.1) - - obj.set_linear_coefficient(x, 0.0) + """Tests that primary and auxiliary objectives handle linear terms.""" + + def test_same_model(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + mod.check_compatible(obj) + + def test_name(self, primary: bool) -> None: + _, obj = _model_and_objective(primary, "my_obj") + self.assertEqual(obj.name, "my_obj") + + def test_maximize(self, primary: bool) -> None: + _, obj = _model_and_objective(primary) + self.assertFalse(obj.is_maximize) + obj.is_maximize = True + self.assertTrue(obj.is_maximize) + + def test_offset(self, primary: bool) -> None: + _, obj = _model_and_objective(primary) + self.assertEqual(obj.offset, 0.0) + obj.offset = 3.2 + self.assertEqual(obj.offset, 3.2) + + def test_priority(self, primary: bool) -> None: + _, obj = _model_and_objective(primary) + self.assertEqual(obj.priority, 0) + obj.priority = 12 + self.assertEqual(obj.priority, 12) + + def test_linear_coefficients_basic(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + y = mod.add_variable() + z = mod.add_variable() + + self.assertEqual(obj.get_linear_coefficient(x), 0.0) + self.assertEqual(obj.get_linear_coefficient(y), 0.0) + self.assertEqual(obj.get_linear_coefficient(z), 0.0) + self.assertEmpty(list(obj.linear_terms())) + + obj.set_linear_coefficient(x, 2.1) + obj.set_linear_coefficient(z, 3.4) + + self.assertEqual(obj.get_linear_coefficient(x), 2.1) + self.assertEqual(obj.get_linear_coefficient(y), 0.0) + self.assertEqual(obj.get_linear_coefficient(z), 3.4) + _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 2.1, z: 3.4}) + + def test_linear_coefficients_restore_to_zero(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + obj.set_linear_coefficient(x, 2.1) + self.assertEqual(obj.get_linear_coefficient(x), 2.1) + + obj.set_linear_coefficient(x, 0.0) - self.assertEqual(obj.get_linear_coefficient(x), 0.0) - self.assertEmpty(list(obj.linear_terms())) + self.assertEqual(obj.get_linear_coefficient(x), 0.0) + self.assertEmpty(list(obj.linear_terms())) - def test_clear(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - obj.set_linear_coefficient(x, 2.1) - obj.is_maximize = True - obj.offset = 4.0 + def test_clear(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + obj.set_linear_coefficient(x, 2.1) + obj.is_maximize = True + obj.offset = 4.0 - obj.clear() + obj.clear() - self.assertTrue(obj.is_maximize) - self.assertEqual(obj.offset, 0.0) - self.assertEmpty(list(obj.linear_terms())) + self.assertTrue(obj.is_maximize) + self.assertEqual(obj.offset, 0.0) + self.assertEmpty(list(obj.linear_terms())) - def test_as_linear_expression(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - obj.set_linear_coefficient(x, 2.1) - obj.offset = 4.0 + def test_as_linear_expression(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + obj.set_linear_coefficient(x, 2.1) + obj.offset = 4.0 - expr = obj.as_linear_expression() + expr = obj.as_linear_expression() - self.assertEqual(expr.offset, 4.0) - self.assertDictEqual(dict(expr.terms), {x: 2.1}) + self.assertEqual(expr.offset, 4.0) + self.assertDictEqual(dict(expr.terms), {x: 2.1}) - def test_add_linear(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - y = mod.add_variable() - obj.set_linear_coefficient(x, 2.0) - obj.offset = 5.5 - - obj.add_linear(1.0 + x + 4.0 * y) - - self.assertAlmostEqual(obj.offset, 6.5, delta=1e-10) - _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 3.0, y: 4.0}) - - def test_add(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - obj.set_linear_coefficient(x, 2.0) - obj.offset = 5.5 - - obj.add(1.0 + x) - - self.assertAlmostEqual(obj.offset, 6.5, delta=1e-10) - _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 3.0}) - - def test_add_linear_rejects_quadratic(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, "Quadratic"): - obj.add_linear(x * x) # pytype: disable=wrong-arg-types - - def test_set_to_linear(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - y = mod.add_variable() - obj.set_linear_coefficient(x, 2.0) - obj.offset = 5.5 - - obj.set_to_linear_expression(1.0 + x + 4.0 * y) - - self.assertEqual(obj.offset, 1.0) - _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 1.0, y: 4.0}) - - def test_set_to_linear_rejects_quadratic(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - with self.assertRaisesRegex(TypeError, "Quadratic"): - obj.set_to_linear_expression(x * x) # pytype: disable=wrong-arg-types + def test_add_linear(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + y = mod.add_variable() + obj.set_linear_coefficient(x, 2.0) + obj.offset = 5.5 + + obj.add_linear(1.0 + x + 4.0 * y) + + self.assertAlmostEqual(obj.offset, 6.5, delta=1e-10) + _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 3.0, y: 4.0}) + + def test_add(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + obj.set_linear_coefficient(x, 2.0) + obj.offset = 5.5 + + obj.add(1.0 + x) + + self.assertAlmostEqual(obj.offset, 6.5, delta=1e-10) + _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 3.0}) + + def test_add_linear_rejects_quadratic(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, "Quadratic"): + obj.add_linear(x * x) # pytype: disable=wrong-arg-types + + def test_set_to_linear(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + y = mod.add_variable() + obj.set_linear_coefficient(x, 2.0) + obj.offset = 5.5 + + obj.set_to_linear_expression(1.0 + x + 4.0 * y) + + self.assertEqual(obj.offset, 1.0) + _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 1.0, y: 4.0}) + + def test_set_to_linear_rejects_quadratic(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + with self.assertRaisesRegex(TypeError, "Quadratic"): + obj.set_to_linear_expression(x * x) # pytype: disable=wrong-arg-types - def test_set_to_expression(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - obj.set_linear_coefficient(x, 2.0) - obj.offset = 5.5 + def test_set_to_expression(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + obj.set_linear_coefficient(x, 2.0) + obj.offset = 5.5 - obj.set_to_expression(1.0 + x) + obj.set_to_expression(1.0 + x) - self.assertEqual(obj.offset, 1.0) - _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 1.0}) + self.assertEqual(obj.offset, 1.0) + _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 1.0}) - def test_get_linear_coef_of_deleted_variable(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() + def test_get_linear_coef_of_deleted_variable(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() - mod.delete_variable(x) + mod.delete_variable(x) - with self.assertRaises(ValueError): - obj.get_linear_coefficient(x) + with self.assertRaises(ValueError): + obj.get_linear_coefficient(x) - def test_set_linear_coef_of_deleted_variable(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() + def test_set_linear_coef_of_deleted_variable(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() - mod.delete_variable(x) + mod.delete_variable(x) - with self.assertRaises(ValueError): - obj.set_linear_coefficient(x, 2.0) + with self.assertRaises(ValueError): + obj.set_linear_coefficient(x, 2.0) - def test_get_quadratic_coef_of_deleted_variable(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - y = mod.add_variable() + def test_get_quadratic_coef_of_deleted_variable(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + y = mod.add_variable() - mod.delete_variable(x) + mod.delete_variable(x) - with self.assertRaises(ValueError): - obj.get_quadratic_coefficient(x, x) + with self.assertRaises(ValueError): + obj.get_quadratic_coefficient(x, x) - with self.assertRaises(ValueError): - obj.get_quadratic_coefficient(x, y) + with self.assertRaises(ValueError): + obj.get_quadratic_coefficient(x, y) - def test_delete_variable_terms_removed(self, primary: bool) -> None: - mod, obj = _model_and_objective(primary) - x = mod.add_variable() - y = mod.add_variable() - obj.set_to_expression(x + y + 3.0) + def test_delete_variable_terms_removed(self, primary: bool) -> None: + mod, obj = _model_and_objective(primary) + x = mod.add_variable() + y = mod.add_variable() + obj.set_to_expression(x + y + 3.0) - mod.delete_variable(x) + mod.delete_variable(x) - self.assertEqual(obj.offset, 3.0) - _assert_linear_terms_equal_dict(self, obj.linear_terms(), {y: 1.0}) + self.assertEqual(obj.offset, 3.0) + _assert_linear_terms_equal_dict(self, obj.linear_terms(), {y: 1.0}) - def test_objective_wrong_model_linear(self, primary: bool) -> None: - mod1, _ = _model_and_objective(primary) - mod2, obj2 = _model_and_objective(primary) - x1 = mod1.add_variable() - mod2.add_variable() - with self.assertRaises(ValueError): - obj2.set_linear_coefficient(x1, 1.0) - with self.assertRaises(ValueError): - obj2.get_linear_coefficient(x1) + def test_objective_wrong_model_linear(self, primary: bool) -> None: + mod1, _ = _model_and_objective(primary) + mod2, obj2 = _model_and_objective(primary) + x1 = mod1.add_variable() + mod2.add_variable() + with self.assertRaises(ValueError): + obj2.set_linear_coefficient(x1, 1.0) + with self.assertRaises(ValueError): + obj2.get_linear_coefficient(x1) - def test_objective_wrong_model_get_quadratic(self, primary: bool) -> None: - mod1, _ = _model_and_objective(primary) - mod2, obj2 = _model_and_objective(primary) - x = mod1.add_variable() - other_x = mod2.add_variable(name="x") - with self.assertRaises(ValueError): - obj2.get_quadratic_coefficient(x, other_x) - with self.assertRaises(ValueError): - obj2.get_quadratic_coefficient(other_x, x) + def test_objective_wrong_model_get_quadratic(self, primary: bool) -> None: + mod1, _ = _model_and_objective(primary) + mod2, obj2 = _model_and_objective(primary) + x = mod1.add_variable() + other_x = mod2.add_variable(name="x") + with self.assertRaises(ValueError): + obj2.get_quadratic_coefficient(x, other_x) + with self.assertRaises(ValueError): + obj2.get_quadratic_coefficient(other_x, x) class PrimaryObjectiveTest(absltest.TestCase): - def test_eq(self) -> None: - mod1 = model.Model() - mod2 = model.Model() - - self.assertEqual(mod1.objective, mod1.objective) - self.assertNotEqual(mod1.objective, mod2.objective) - - def test_quadratic_coefficients_basic(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - y = mod.add_variable() - z = mod.add_variable() - - self.assertEqual(obj.get_quadratic_coefficient(x, x), 0.0) - self.assertEqual(obj.get_quadratic_coefficient(y, z), 0.0) - self.assertEqual(obj.get_quadratic_coefficient(z, x), 0.0) - self.assertEmpty(list(obj.quadratic_terms())) - - obj.set_quadratic_coefficient(x, x, 2.1) - obj.set_quadratic_coefficient(y, z, 3.1) - obj.set_quadratic_coefficient(z, x, 4.1) - - self.assertEqual(obj.get_quadratic_coefficient(x, x), 2.1) - self.assertEqual(obj.get_quadratic_coefficient(y, z), 3.1) - self.assertEqual(obj.get_quadratic_coefficient(z, y), 3.1) - self.assertEqual(obj.get_quadratic_coefficient(z, x), 4.1) - self.assertEqual(obj.get_quadratic_coefficient(x, z), 4.1) - self.assertEqual(obj.get_quadratic_coefficient(y, y), 0.0) - _assert_quadratic_terms_equal_dict( - self, obj.quadratic_terms(), {(x, x): 2.1, (y, z): 3.1, (x, z): 4.1} - ) - - def test_quadratic_coefficients_restore_to_zero(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - obj.set_quadratic_coefficient(x, x, 2.1) - self.assertEqual(obj.get_quadratic_coefficient(x, x), 2.1) - - obj.set_quadratic_coefficient(x, x, 0.0) - - self.assertEqual(obj.get_quadratic_coefficient(x, x), 0.0) - self.assertEmpty(list(obj.quadratic_terms())) - - def test_clear(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - obj.set_quadratic_coefficient(x, x, 2.1) - - obj.clear() - - self.assertEqual(obj.get_quadratic_coefficient(x, x), 0.0) - self.assertEmpty(list(obj.quadratic_terms())) - - def test_as_linear_expression_fails(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - obj.set_quadratic_coefficient(x, x, 2.1) - - with self.assertRaisesRegex(TypeError, "quadratic"): - obj.as_linear_expression() - - def test_as_quadratic_expression(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - y = mod.add_variable() - obj.offset = 2.1 - obj.set_linear_coefficient(y, 3.1) - obj.set_quadratic_coefficient(x, x, 4.1) - obj.set_quadratic_coefficient(x, y, 5.1) - - quad_expr = obj.as_quadratic_expression() - - self.assertEqual(quad_expr.offset, 2.1) - self.assertDictEqual(dict(quad_expr.linear_terms), {y: 3.1}) - self.assertDictEqual( - dict(quad_expr.quadratic_terms), - { - variables.QuadraticTermKey(x, x): 4.1, - variables.QuadraticTermKey(x, y): 5.1, - }, - ) - - def test_add_quadratic(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - y = mod.add_variable() - obj.offset = -2.0 - obj.set_linear_coefficient(y, 3.0) - obj.set_quadratic_coefficient(x, x, 4.0) - - obj.add_quadratic(1.0 + x + 2 * y + x * x + 3.0 * x * y) - - self.assertEqual(obj.offset, -1.0) - _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 1.0, y: 5.0}) - _assert_quadratic_terms_equal_dict( - self, obj.quadratic_terms(), {(x, x): 5.0, (x, y): 3.0} - ) - - def test_add(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - obj.set_linear_coefficient(x, 2.0) - - obj.add(x * x) - - self.assertEqual(obj.offset, 0.0) - _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 2.0}) - _assert_quadratic_terms_equal_dict(self, obj.quadratic_terms(), {(x, x): 1.0}) - - def test_set_to_quadratic_expression(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - y = mod.add_variable() - obj.offset = -2.0 - obj.set_linear_coefficient(y, 3.0) - obj.set_quadratic_coefficient(x, x, 4.0) - - obj.set_to_quadratic_expression(1.0 + x + 2 * y + x * x + 3.0 * x * y) - - self.assertEqual(obj.offset, 1.0) - _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 1.0, y: 2.0}) - _assert_quadratic_terms_equal_dict( - self, obj.quadratic_terms(), {(x, x): 1.0, (x, y): 3.0} - ) - - def test_set_to_expression(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - obj.set_linear_coefficient(x, 2.0) - - obj.set_to_expression(x * x) - - self.assertEqual(obj.offset, 0.0) - self.assertEmpty(list(obj.linear_terms())) - _assert_quadratic_terms_equal_dict(self, obj.quadratic_terms(), {(x, x): 1.0}) - - def test_set_quadratic_coef_of_deleted_variable(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - - mod.delete_variable(x) - - with self.assertRaises(ValueError): - mod.objective.set_quadratic_coefficient(x, x, 1.0) - - with self.assertRaises(ValueError): - mod.objective.set_quadratic_coefficient(x, y, 1.0) - - def test_delete_variable_quad_terms_removed(self) -> None: - mod = model.Model() - obj = mod.objective - x = mod.add_variable() - y = mod.add_variable() - obj.set_to_expression(x * x + x * y + y * y) - - mod.delete_variable(x) - - _assert_quadratic_terms_equal_dict( - self, mod.objective.quadratic_terms(), {(y, y): 1.0} - ) - - def test_objective_wrong_model_set_quadratic(self) -> None: - mod1 = model.Model() - x = mod1.add_variable() - mod2 = model.Model() - other_x = mod2.add_variable(name="x") - with self.assertRaises(ValueError): - mod2.objective.set_quadratic_coefficient(x, other_x, 1.0) - with self.assertRaises(ValueError): - mod2.objective.set_quadratic_coefficient(other_x, x, 1.0) + def test_eq(self) -> None: + mod1 = model.Model() + mod2 = model.Model() + + self.assertEqual(mod1.objective, mod1.objective) + self.assertNotEqual(mod1.objective, mod2.objective) + + def test_quadratic_coefficients_basic(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + y = mod.add_variable() + z = mod.add_variable() + + self.assertEqual(obj.get_quadratic_coefficient(x, x), 0.0) + self.assertEqual(obj.get_quadratic_coefficient(y, z), 0.0) + self.assertEqual(obj.get_quadratic_coefficient(z, x), 0.0) + self.assertEmpty(list(obj.quadratic_terms())) + + obj.set_quadratic_coefficient(x, x, 2.1) + obj.set_quadratic_coefficient(y, z, 3.1) + obj.set_quadratic_coefficient(z, x, 4.1) + + self.assertEqual(obj.get_quadratic_coefficient(x, x), 2.1) + self.assertEqual(obj.get_quadratic_coefficient(y, z), 3.1) + self.assertEqual(obj.get_quadratic_coefficient(z, y), 3.1) + self.assertEqual(obj.get_quadratic_coefficient(z, x), 4.1) + self.assertEqual(obj.get_quadratic_coefficient(x, z), 4.1) + self.assertEqual(obj.get_quadratic_coefficient(y, y), 0.0) + _assert_quadratic_terms_equal_dict( + self, obj.quadratic_terms(), {(x, x): 2.1, (y, z): 3.1, (x, z): 4.1} + ) + + def test_quadratic_coefficients_restore_to_zero(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + obj.set_quadratic_coefficient(x, x, 2.1) + self.assertEqual(obj.get_quadratic_coefficient(x, x), 2.1) + + obj.set_quadratic_coefficient(x, x, 0.0) + + self.assertEqual(obj.get_quadratic_coefficient(x, x), 0.0) + self.assertEmpty(list(obj.quadratic_terms())) + + def test_clear(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + obj.set_quadratic_coefficient(x, x, 2.1) + + obj.clear() + + self.assertEqual(obj.get_quadratic_coefficient(x, x), 0.0) + self.assertEmpty(list(obj.quadratic_terms())) + + def test_as_linear_expression_fails(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + obj.set_quadratic_coefficient(x, x, 2.1) + + with self.assertRaisesRegex(TypeError, "quadratic"): + obj.as_linear_expression() + + def test_as_quadratic_expression(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + y = mod.add_variable() + obj.offset = 2.1 + obj.set_linear_coefficient(y, 3.1) + obj.set_quadratic_coefficient(x, x, 4.1) + obj.set_quadratic_coefficient(x, y, 5.1) + + quad_expr = obj.as_quadratic_expression() + + self.assertEqual(quad_expr.offset, 2.1) + self.assertDictEqual(dict(quad_expr.linear_terms), {y: 3.1}) + self.assertDictEqual( + dict(quad_expr.quadratic_terms), + { + variables.QuadraticTermKey(x, x): 4.1, + variables.QuadraticTermKey(x, y): 5.1, + }, + ) + + def test_add_quadratic(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + y = mod.add_variable() + obj.offset = -2.0 + obj.set_linear_coefficient(y, 3.0) + obj.set_quadratic_coefficient(x, x, 4.0) + + obj.add_quadratic(1.0 + x + 2 * y + x * x + 3.0 * x * y) + + self.assertEqual(obj.offset, -1.0) + _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 1.0, y: 5.0}) + _assert_quadratic_terms_equal_dict( + self, obj.quadratic_terms(), {(x, x): 5.0, (x, y): 3.0} + ) + + def test_add(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + obj.set_linear_coefficient(x, 2.0) + + obj.add(x * x) + + self.assertEqual(obj.offset, 0.0) + _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 2.0}) + _assert_quadratic_terms_equal_dict( + self, obj.quadratic_terms(), {(x, x): 1.0} + ) + + def test_set_to_quadratic_expression(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + y = mod.add_variable() + obj.offset = -2.0 + obj.set_linear_coefficient(y, 3.0) + obj.set_quadratic_coefficient(x, x, 4.0) + + obj.set_to_quadratic_expression(1.0 + x + 2 * y + x * x + 3.0 * x * y) + + self.assertEqual(obj.offset, 1.0) + _assert_linear_terms_equal_dict(self, obj.linear_terms(), {x: 1.0, y: 2.0}) + _assert_quadratic_terms_equal_dict( + self, obj.quadratic_terms(), {(x, x): 1.0, (x, y): 3.0} + ) + + def test_set_to_expression(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + obj.set_linear_coefficient(x, 2.0) + + obj.set_to_expression(x * x) + + self.assertEqual(obj.offset, 0.0) + self.assertEmpty(list(obj.linear_terms())) + _assert_quadratic_terms_equal_dict( + self, obj.quadratic_terms(), {(x, x): 1.0} + ) + + def test_set_quadratic_coef_of_deleted_variable(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + + mod.delete_variable(x) + + with self.assertRaises(ValueError): + mod.objective.set_quadratic_coefficient(x, x, 1.0) + + with self.assertRaises(ValueError): + mod.objective.set_quadratic_coefficient(x, y, 1.0) + + def test_delete_variable_quad_terms_removed(self) -> None: + mod = model.Model() + obj = mod.objective + x = mod.add_variable() + y = mod.add_variable() + obj.set_to_expression(x * x + x * y + y * y) + + mod.delete_variable(x) + + _assert_quadratic_terms_equal_dict( + self, mod.objective.quadratic_terms(), {(y, y): 1.0} + ) + + def test_objective_wrong_model_set_quadratic(self) -> None: + mod1 = model.Model() + x = mod1.add_variable() + mod2 = model.Model() + other_x = mod2.add_variable(name="x") + with self.assertRaises(ValueError): + mod2.objective.set_quadratic_coefficient(x, other_x, 1.0) + with self.assertRaises(ValueError): + mod2.objective.set_quadratic_coefficient(other_x, x, 1.0) class AuxiliaryObjectiveTest(absltest.TestCase): - def test_invalid_id_type(self) -> None: - elemental = cpp_elemental.CppElemental() - with self.assertRaisesRegex(TypeError, "obj_id type"): - objectives.AuxiliaryObjective( - elemental, "dog" - ) # pytype: disable=wrong-arg-types + def test_invalid_id_type(self) -> None: + elemental = cpp_elemental.CppElemental() + with self.assertRaisesRegex(TypeError, "obj_id type"): + objectives.AuxiliaryObjective( + elemental, "dog" + ) # pytype: disable=wrong-arg-types - def test_eq(self) -> None: - mod1 = model.Model() - aux1 = mod1.add_auxiliary_objective(priority=1) - aux2 = mod1.add_auxiliary_objective(priority=2) - mod2 = model.Model() - aux3 = mod2.add_auxiliary_objective(priority=1) + def test_eq(self) -> None: + mod1 = model.Model() + aux1 = mod1.add_auxiliary_objective(priority=1) + aux2 = mod1.add_auxiliary_objective(priority=2) + mod2 = model.Model() + aux3 = mod2.add_auxiliary_objective(priority=1) - self.assertEqual(aux1, aux1) - self.assertEqual(aux1, mod1.get_auxiliary_objective(0)) - self.assertNotEqual(aux1, aux2) - self.assertNotEqual(aux1, aux3) - self.assertNotEqual(aux1, mod1.objective) + self.assertEqual(aux1, aux1) + self.assertEqual(aux1, mod1.get_auxiliary_objective(0)) + self.assertNotEqual(aux1, aux2) + self.assertNotEqual(aux1, aux3) + self.assertNotEqual(aux1, mod1.objective) - def test_id(self) -> None: - mod = model.Model() - aux1 = mod.add_auxiliary_objective(priority=1) - aux2 = mod.add_auxiliary_objective(priority=2) + def test_id(self) -> None: + mod = model.Model() + aux1 = mod.add_auxiliary_objective(priority=1) + aux2 = mod.add_auxiliary_objective(priority=2) - self.assertEqual(aux1.id, 0) - self.assertEqual(aux2.id, 1) + self.assertEqual(aux1.id, 0) + self.assertEqual(aux2.id, 1) - def test_get_quadratic_coefficients_is_zero(self) -> None: - mod = model.Model() - obj = mod.add_auxiliary_objective(priority=1) - x = mod.add_variable() + def test_get_quadratic_coefficients_is_zero(self) -> None: + mod = model.Model() + obj = mod.add_auxiliary_objective(priority=1) + x = mod.add_variable() - self.assertEqual(obj.get_quadratic_coefficient(x, x), 0.0) - self.assertEmpty(list(obj.quadratic_terms())) + self.assertEqual(obj.get_quadratic_coefficient(x, x), 0.0) + self.assertEmpty(list(obj.quadratic_terms())) - def test_set_quadratic_coefficients_is_error(self) -> None: - mod = model.Model() - obj = mod.add_auxiliary_objective(priority=1) - x = mod.add_variable() + def test_set_quadratic_coefficients_is_error(self) -> None: + mod = model.Model() + obj = mod.add_auxiliary_objective(priority=1) + x = mod.add_variable() - with self.assertRaisesRegex(ValueError, "Quadratic"): - obj.set_quadratic_coefficient(x, x, 2.1) + with self.assertRaisesRegex(ValueError, "Quadratic"): + obj.set_quadratic_coefficient(x, x, 2.1) - def test_as_quadratic_expression_with_linear_no_crash(self) -> None: - mod = model.Model() - obj = mod.add_auxiliary_objective(priority=1) - y = mod.add_variable() - obj.offset = 2.1 - obj.set_linear_coefficient(y, 3.1) + def test_as_quadratic_expression_with_linear_no_crash(self) -> None: + mod = model.Model() + obj = mod.add_auxiliary_objective(priority=1) + y = mod.add_variable() + obj.offset = 2.1 + obj.set_linear_coefficient(y, 3.1) - quad_expr = obj.as_quadratic_expression() + quad_expr = obj.as_quadratic_expression() - self.assertEqual(quad_expr.offset, 2.1) - self.assertDictEqual(dict(quad_expr.linear_terms), {y: 3.1}) - self.assertEmpty(list(quad_expr.quadratic_terms)) + self.assertEqual(quad_expr.offset, 2.1) + self.assertDictEqual(dict(quad_expr.linear_terms), {y: 3.1}) + self.assertEmpty(list(quad_expr.quadratic_terms)) - def test_add_quadratic_errors(self) -> None: - mod = model.Model() - obj = mod.add_auxiliary_objective(priority=1) - x = mod.add_variable() + def test_add_quadratic_errors(self) -> None: + mod = model.Model() + obj = mod.add_auxiliary_objective(priority=1) + x = mod.add_variable() - with self.assertRaisesRegex(ValueError, "Quadratic"): - obj.add_quadratic(x * x) + with self.assertRaisesRegex(ValueError, "Quadratic"): + obj.add_quadratic(x * x) - def test_add_is_error_if_quad(self) -> None: - mod = model.Model() - obj = mod.add_auxiliary_objective(priority=1) - x = mod.add_variable() + def test_add_is_error_if_quad(self) -> None: + mod = model.Model() + obj = mod.add_auxiliary_objective(priority=1) + x = mod.add_variable() - with self.assertRaisesRegex(ValueError, "Quadratic"): - obj.add(x * x) + with self.assertRaisesRegex(ValueError, "Quadratic"): + obj.add(x * x) - def test_set_to_quadratic_expression_error(self) -> None: - mod = model.Model() - obj = mod.add_auxiliary_objective(priority=1) - x = mod.add_variable() + def test_set_to_quadratic_expression_error(self) -> None: + mod = model.Model() + obj = mod.add_auxiliary_objective(priority=1) + x = mod.add_variable() - with self.assertRaisesRegex(ValueError, "Quadratic"): - obj.set_to_quadratic_expression(x * x) + with self.assertRaisesRegex(ValueError, "Quadratic"): + obj.set_to_quadratic_expression(x * x) - def test_set_to_expression_error_when_quadratic(self) -> None: - mod = model.Model() - obj = mod.add_auxiliary_objective(priority=1) - x = mod.add_variable() + def test_set_to_expression_error_when_quadratic(self) -> None: + mod = model.Model() + obj = mod.add_auxiliary_objective(priority=1) + x = mod.add_variable() - with self.assertRaisesRegex(ValueError, "Quadratic"): - obj.set_to_expression(x * x) + with self.assertRaisesRegex(ValueError, "Quadratic"): + obj.set_to_expression(x * x) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/parameters.py b/ortools/math_opt/python/parameters.py index f8df307d649..2ca3a10cc7a 100644 --- a/ortools/math_opt/python/parameters.py +++ b/ortools/math_opt/python/parameters.py @@ -31,243 +31,243 @@ @enum.unique class SolverType(enum.Enum): - """The underlying solver to use. - - This must stay synchronized with math_opt_parameters_pb2.SolverTypeProto. - - Attributes: - GSCIP: Solving Constraint Integer Programs (SCIP) solver (third party). - Supports LP, MIP, and nonconvex integer quadratic problems. No dual data - for LPs is returned though. Prefer GLOP for LPs. - GUROBI: Gurobi solver (third party). Supports LP, MIP, and nonconvex integer - quadratic problems. Generally the fastest option, but has special - licensing, see go/gurobi-google for details. - GLOP: Google's Glop linear solver. Supports LP with primal and dual simplex - methods. - CP_SAT: Google's CP-SAT solver. Supports problems where all variables are - integer and bounded (or implied to be after presolve). Experimental - support to rescale and discretize problems with continuous variables. - MOE:begin_intracomment_strip - PDLP: Google's PDLP solver. Supports LP and convex diagonal quadratic - objectives. Uses first order methods rather than simplex. Can solve very - large problems. - MOE:end_intracomment_strip - GLPK: GNU Linear Programming Kit (GLPK) (third party). Supports MIP and LP. - Thread-safety: GLPK use thread-local storage for memory allocations. As a - consequence when using IncrementalSolver, the user must make sure that - instances are closed on the same thread as they are created or GLPK will - crash. To do so, use `with` or call IncrementalSolver#close(). It seems OK - to call IncrementalSolver#Solve() from another thread than the one used to - create the Solver but it is not documented by GLPK and should be avoided. - Of course these limitations do not apply to the solve() function that - recreates a new GLPK problem in the calling thread and destroys before - returning. When solving a LP with the presolver, a solution (and the - unbound rays) are only returned if an optimal solution has been found. - Else nothing is returned. See glpk-5.0/doc/glpk.pdf page #40 available - from glpk-5.0.tar.gz for details. - OSQP: The Operator Splitting Quadratic Program (OSQP) solver (third party). - Supports continuous problems with linear constraints and linear or convex - quadratic objectives. Uses a first-order method. - ECOS: The Embedded Conic Solver (ECOS) (third party). Supports LP and SOCP - problems. Uses interior point methods (barrier). - SCS: The Splitting Conic Solver (SCS) (third party). Supports LP and SOCP - problems. Uses a first-order method. - HIGHS: The HiGHS Solver (third party). Supports LP and MIP problems (convex - QPs are unimplemented). - SANTORINI: The Santorini Solver (first party). Supports MIP. Experimental, - do not use in production. - """ - - GSCIP = math_opt_parameters_pb2.SOLVER_TYPE_GSCIP - GUROBI = math_opt_parameters_pb2.SOLVER_TYPE_GUROBI - GLOP = math_opt_parameters_pb2.SOLVER_TYPE_GLOP - CP_SAT = math_opt_parameters_pb2.SOLVER_TYPE_CP_SAT - PDLP = math_opt_parameters_pb2.SOLVER_TYPE_PDLP - GLPK = math_opt_parameters_pb2.SOLVER_TYPE_GLPK - OSQP = math_opt_parameters_pb2.SOLVER_TYPE_OSQP - ECOS = math_opt_parameters_pb2.SOLVER_TYPE_ECOS - SCS = math_opt_parameters_pb2.SOLVER_TYPE_SCS - HIGHS = math_opt_parameters_pb2.SOLVER_TYPE_HIGHS - SANTORINI = math_opt_parameters_pb2.SOLVER_TYPE_SANTORINI + """The underlying solver to use. + + This must stay synchronized with math_opt_parameters_pb2.SolverTypeProto. + + Attributes: + GSCIP: Solving Constraint Integer Programs (SCIP) solver (third party). + Supports LP, MIP, and nonconvex integer quadratic problems. No dual data + for LPs is returned though. Prefer GLOP for LPs. + GUROBI: Gurobi solver (third party). Supports LP, MIP, and nonconvex integer + quadratic problems. Generally the fastest option, but has special + licensing, see go/gurobi-google for details. + GLOP: Google's Glop linear solver. Supports LP with primal and dual simplex + methods. + CP_SAT: Google's CP-SAT solver. Supports problems where all variables are + integer and bounded (or implied to be after presolve). Experimental + support to rescale and discretize problems with continuous variables. + MOE:begin_intracomment_strip + PDLP: Google's PDLP solver. Supports LP and convex diagonal quadratic + objectives. Uses first order methods rather than simplex. Can solve very + large problems. + MOE:end_intracomment_strip + GLPK: GNU Linear Programming Kit (GLPK) (third party). Supports MIP and LP. + Thread-safety: GLPK use thread-local storage for memory allocations. As a + consequence when using IncrementalSolver, the user must make sure that + instances are closed on the same thread as they are created or GLPK will + crash. To do so, use `with` or call IncrementalSolver#close(). It seems OK + to call IncrementalSolver#Solve() from another thread than the one used to + create the Solver but it is not documented by GLPK and should be avoided. + Of course these limitations do not apply to the solve() function that + recreates a new GLPK problem in the calling thread and destroys before + returning. When solving a LP with the presolver, a solution (and the + unbound rays) are only returned if an optimal solution has been found. + Else nothing is returned. See glpk-5.0/doc/glpk.pdf page #40 available + from glpk-5.0.tar.gz for details. + OSQP: The Operator Splitting Quadratic Program (OSQP) solver (third party). + Supports continuous problems with linear constraints and linear or convex + quadratic objectives. Uses a first-order method. + ECOS: The Embedded Conic Solver (ECOS) (third party). Supports LP and SOCP + problems. Uses interior point methods (barrier). + SCS: The Splitting Conic Solver (SCS) (third party). Supports LP and SOCP + problems. Uses a first-order method. + HIGHS: The HiGHS Solver (third party). Supports LP and MIP problems (convex + QPs are unimplemented). + SANTORINI: The Santorini Solver (first party). Supports MIP. Experimental, + do not use in production. + """ + + GSCIP = math_opt_parameters_pb2.SOLVER_TYPE_GSCIP + GUROBI = math_opt_parameters_pb2.SOLVER_TYPE_GUROBI + GLOP = math_opt_parameters_pb2.SOLVER_TYPE_GLOP + CP_SAT = math_opt_parameters_pb2.SOLVER_TYPE_CP_SAT + PDLP = math_opt_parameters_pb2.SOLVER_TYPE_PDLP + GLPK = math_opt_parameters_pb2.SOLVER_TYPE_GLPK + OSQP = math_opt_parameters_pb2.SOLVER_TYPE_OSQP + ECOS = math_opt_parameters_pb2.SOLVER_TYPE_ECOS + SCS = math_opt_parameters_pb2.SOLVER_TYPE_SCS + HIGHS = math_opt_parameters_pb2.SOLVER_TYPE_HIGHS + SANTORINI = math_opt_parameters_pb2.SOLVER_TYPE_SANTORINI def solver_type_from_proto( proto_value: math_opt_parameters_pb2.SolverTypeProto, ) -> Optional[SolverType]: - if proto_value == math_opt_parameters_pb2.SOLVER_TYPE_UNSPECIFIED: - return None - return SolverType(proto_value) + if proto_value == math_opt_parameters_pb2.SOLVER_TYPE_UNSPECIFIED: + return None + return SolverType(proto_value) def solver_type_to_proto( solver_type: Optional[SolverType], ) -> math_opt_parameters_pb2.SolverTypeProto: - if solver_type is None: - return math_opt_parameters_pb2.SOLVER_TYPE_UNSPECIFIED - return solver_type.value + if solver_type is None: + return math_opt_parameters_pb2.SOLVER_TYPE_UNSPECIFIED + return solver_type.value @enum.unique class LPAlgorithm(enum.Enum): - """Selects an algorithm for solving linear programs. - - Attributes: - * UNPSECIFIED: No algorithm is selected. - * PRIMAL_SIMPLEX: The (primal) simplex method. Typically can provide primal - and dual solutions, primal/dual rays on primal/dual unbounded problems, - and a basis. - * DUAL_SIMPLEX: The dual simplex method. Typically can provide primal and - dual solutions, primal/dual rays on primal/dual unbounded problems, and a - basis. - * BARRIER: The barrier method, also commonly called an interior point method - (IPM). Can typically give both primal and dual solutions. Some - implementations can also produce rays on unbounded/infeasible problems. A - basis is not given unless the underlying solver does "crossover" and - finishes with simplex. - * FIRST_ORDER: An algorithm based around a first-order method. These will - typically produce both primal and dual solutions, and potentially also - certificates of primal and/or dual infeasibility. First-order methods - typically will provide solutions with lower accuracy, so users should take - care to set solution quality parameters (e.g., tolerances) and to validate - solutions. - - This must stay synchronized with math_opt_parameters_pb2.LPAlgorithmProto. - """ - - PRIMAL_SIMPLEX = math_opt_parameters_pb2.LP_ALGORITHM_PRIMAL_SIMPLEX - DUAL_SIMPLEX = math_opt_parameters_pb2.LP_ALGORITHM_DUAL_SIMPLEX - BARRIER = math_opt_parameters_pb2.LP_ALGORITHM_BARRIER - FIRST_ORDER = math_opt_parameters_pb2.LP_ALGORITHM_FIRST_ORDER + """Selects an algorithm for solving linear programs. + + Attributes: + * UNPSECIFIED: No algorithm is selected. + * PRIMAL_SIMPLEX: The (primal) simplex method. Typically can provide primal + and dual solutions, primal/dual rays on primal/dual unbounded problems, + and a basis. + * DUAL_SIMPLEX: The dual simplex method. Typically can provide primal and + dual solutions, primal/dual rays on primal/dual unbounded problems, and a + basis. + * BARRIER: The barrier method, also commonly called an interior point method + (IPM). Can typically give both primal and dual solutions. Some + implementations can also produce rays on unbounded/infeasible problems. A + basis is not given unless the underlying solver does "crossover" and + finishes with simplex. + * FIRST_ORDER: An algorithm based around a first-order method. These will + typically produce both primal and dual solutions, and potentially also + certificates of primal and/or dual infeasibility. First-order methods + typically will provide solutions with lower accuracy, so users should take + care to set solution quality parameters (e.g., tolerances) and to validate + solutions. + + This must stay synchronized with math_opt_parameters_pb2.LPAlgorithmProto. + """ + + PRIMAL_SIMPLEX = math_opt_parameters_pb2.LP_ALGORITHM_PRIMAL_SIMPLEX + DUAL_SIMPLEX = math_opt_parameters_pb2.LP_ALGORITHM_DUAL_SIMPLEX + BARRIER = math_opt_parameters_pb2.LP_ALGORITHM_BARRIER + FIRST_ORDER = math_opt_parameters_pb2.LP_ALGORITHM_FIRST_ORDER def lp_algorithm_from_proto( proto_value: math_opt_parameters_pb2.LPAlgorithmProto, ) -> Optional[LPAlgorithm]: - if proto_value == math_opt_parameters_pb2.LP_ALGORITHM_UNSPECIFIED: - return None - return LPAlgorithm(proto_value) + if proto_value == math_opt_parameters_pb2.LP_ALGORITHM_UNSPECIFIED: + return None + return LPAlgorithm(proto_value) def lp_algorithm_to_proto( lp_algorithm: Optional[LPAlgorithm], ) -> math_opt_parameters_pb2.LPAlgorithmProto: - if lp_algorithm is None: - return math_opt_parameters_pb2.LP_ALGORITHM_UNSPECIFIED - return lp_algorithm.value + if lp_algorithm is None: + return math_opt_parameters_pb2.LP_ALGORITHM_UNSPECIFIED + return lp_algorithm.value @enum.unique class Emphasis(enum.Enum): - """Effort level applied to an optional task while solving (see SolveParameters for use). - - - OFF: disable this task. - - LOW: apply reduced effort. - - MEDIUM: typically the default setting (unless the default is off). - - HIGH: apply extra effort beyond MEDIUM. - - VERY_HIGH: apply the maximum effort. - - Typically used as Optional[Emphasis]. It used to configure a solver feature as - follows: - * If a solver doesn't support the feature, only None will always be valid, - any other setting will give an invalid argument error (some solvers may - also accept OFF). - * If the solver supports the feature: - - When set to None, the underlying default is used. - - When the feature cannot be turned off, OFF will produce an error. - - If the feature is enabled by default, the solver default is typically - mapped to MEDIUM. - - If the feature is supported, LOW, MEDIUM, HIGH, and VERY HIGH will never - give an error, and will map onto their best match. - - This must stay synchronized with math_opt_parameters_pb2.EmphasisProto. - """ - - OFF = math_opt_parameters_pb2.EMPHASIS_OFF - LOW = math_opt_parameters_pb2.EMPHASIS_LOW - MEDIUM = math_opt_parameters_pb2.EMPHASIS_MEDIUM - HIGH = math_opt_parameters_pb2.EMPHASIS_HIGH - VERY_HIGH = math_opt_parameters_pb2.EMPHASIS_VERY_HIGH + """Effort level applied to an optional task while solving (see SolveParameters for use). + + - OFF: disable this task. + - LOW: apply reduced effort. + - MEDIUM: typically the default setting (unless the default is off). + - HIGH: apply extra effort beyond MEDIUM. + - VERY_HIGH: apply the maximum effort. + + Typically used as Optional[Emphasis]. It used to configure a solver feature as + follows: + * If a solver doesn't support the feature, only None will always be valid, + any other setting will give an invalid argument error (some solvers may + also accept OFF). + * If the solver supports the feature: + - When set to None, the underlying default is used. + - When the feature cannot be turned off, OFF will produce an error. + - If the feature is enabled by default, the solver default is typically + mapped to MEDIUM. + - If the feature is supported, LOW, MEDIUM, HIGH, and VERY HIGH will never + give an error, and will map onto their best match. + + This must stay synchronized with math_opt_parameters_pb2.EmphasisProto. + """ + + OFF = math_opt_parameters_pb2.EMPHASIS_OFF + LOW = math_opt_parameters_pb2.EMPHASIS_LOW + MEDIUM = math_opt_parameters_pb2.EMPHASIS_MEDIUM + HIGH = math_opt_parameters_pb2.EMPHASIS_HIGH + VERY_HIGH = math_opt_parameters_pb2.EMPHASIS_VERY_HIGH def emphasis_from_proto( proto_value: math_opt_parameters_pb2.EmphasisProto, ) -> Optional[Emphasis]: - if proto_value == math_opt_parameters_pb2.EMPHASIS_UNSPECIFIED: - return None - return Emphasis(proto_value) + if proto_value == math_opt_parameters_pb2.EMPHASIS_UNSPECIFIED: + return None + return Emphasis(proto_value) def emphasis_to_proto( emphasis: Optional[Emphasis], ) -> math_opt_parameters_pb2.EmphasisProto: - if emphasis is None: - return math_opt_parameters_pb2.EMPHASIS_UNSPECIFIED - return emphasis.value + if emphasis is None: + return math_opt_parameters_pb2.EMPHASIS_UNSPECIFIED + return emphasis.value @dataclasses.dataclass class GurobiParameters: - """Gurobi specific parameters for solving. - - See https://www.gurobi.com/documentation/9.1/refman/parameters.html for a list - of possible parameters. - - Example use: - gurobi=GurobiParameters(); - gurobi.param_values["BarIterLimit"] = "10"; - - With Gurobi, the order that parameters are applied can have an impact in rare - situations. Parameters are applied in the following order: - * LogToConsole is set from SolveParameters.enable_output. - * Any common parameters not overwritten by GurobiParameters. - * param_values in iteration order (insertion order). - We set LogToConsole first because setting other parameters can generate - output. - """ - - param_values: Dict[str, str] = dataclasses.field(default_factory=dict) - - def to_proto(self) -> gurobi_pb2.GurobiParametersProto: - return gurobi_pb2.GurobiParametersProto( - parameters=[ - gurobi_pb2.GurobiParametersProto.Parameter(name=key, value=val) - for key, val in self.param_values.items() - ] - ) + """Gurobi specific parameters for solving. + + See https://www.gurobi.com/documentation/9.1/refman/parameters.html for a list + of possible parameters. + + Example use: + gurobi=GurobiParameters(); + gurobi.param_values["BarIterLimit"] = "10"; + + With Gurobi, the order that parameters are applied can have an impact in rare + situations. Parameters are applied in the following order: + * LogToConsole is set from SolveParameters.enable_output. + * Any common parameters not overwritten by GurobiParameters. + * param_values in iteration order (insertion order). + We set LogToConsole first because setting other parameters can generate + output. + """ + + param_values: Dict[str, str] = dataclasses.field(default_factory=dict) + + def to_proto(self) -> gurobi_pb2.GurobiParametersProto: + return gurobi_pb2.GurobiParametersProto( + parameters=[ + gurobi_pb2.GurobiParametersProto.Parameter(name=key, value=val) + for key, val in self.param_values.items() + ] + ) @dataclasses.dataclass class GlpkParameters: - """GLPK specific parameters for solving. + """GLPK specific parameters for solving. - Fields are optional to enable to capture user intention; if they set - explicitly a value to then no generic solve parameters will overwrite this - parameter. User specified solver specific parameters have priority on generic - parameters. + Fields are optional to enable to capture user intention; if they set + explicitly a value to then no generic solve parameters will overwrite this + parameter. User specified solver specific parameters have priority on generic + parameters. - Attributes: - compute_unbound_rays_if_possible: Compute the primal or dual unbound ray - when the variable (structural or auxiliary) causing the unboundness is - identified (see glp_get_unbnd_ray()). The unset value is equivalent to - false. Rays are only available when solving linear programs, they are not - available for MIPs. On top of that they are only available when using a - simplex algorithm with the presolve disabled. A primal ray can only be - built if the chosen LP algorithm is LPAlgorithm.PRIMAL_SIMPLEX. Same for a - dual ray and LPAlgorithm.DUAL_SIMPLEX. The computation involves the basis - factorization to be available which may lead to extra computations/errors. - """ - - compute_unbound_rays_if_possible: Optional[bool] = None - - def to_proto(self) -> glpk_pb2.GlpkParametersProto: - return glpk_pb2.GlpkParametersProto( - compute_unbound_rays_if_possible=self.compute_unbound_rays_if_possible - ) + Attributes: + compute_unbound_rays_if_possible: Compute the primal or dual unbound ray + when the variable (structural or auxiliary) causing the unboundness is + identified (see glp_get_unbnd_ray()). The unset value is equivalent to + false. Rays are only available when solving linear programs, they are not + available for MIPs. On top of that they are only available when using a + simplex algorithm with the presolve disabled. A primal ray can only be + built if the chosen LP algorithm is LPAlgorithm.PRIMAL_SIMPLEX. Same for a + dual ray and LPAlgorithm.DUAL_SIMPLEX. The computation involves the basis + factorization to be available which may lead to extra computations/errors. + """ + + compute_unbound_rays_if_possible: Optional[bool] = None + + def to_proto(self) -> glpk_pb2.GlpkParametersProto: + return glpk_pb2.GlpkParametersProto( + compute_unbound_rays_if_possible=self.compute_unbound_rays_if_possible + ) @dataclasses.dataclass class SolveParameters: - """Parameters to control a single solve. + """Parameters to control a single solve. If a value is set in both common and solver specific field (e.g. gscip), the solver specific setting is used. @@ -376,85 +376,85 @@ class SolveParameters: highs: HiGHS specific solve parameters. """ # fmt: skip - time_limit: Optional[datetime.timedelta] = None - iteration_limit: Optional[int] = None - node_limit: Optional[int] = None - cutoff_limit: Optional[float] = None - objective_limit: Optional[float] = None - best_bound_limit: Optional[float] = None - solution_limit: Optional[int] = None - enable_output: bool = False - threads: Optional[int] = None - random_seed: Optional[int] = None - absolute_gap_tolerance: Optional[float] = None - relative_gap_tolerance: Optional[float] = None - solution_pool_size: Optional[int] = None - lp_algorithm: Optional[LPAlgorithm] = None - presolve: Optional[Emphasis] = None - cuts: Optional[Emphasis] = None - heuristics: Optional[Emphasis] = None - scaling: Optional[Emphasis] = None - gscip: gscip_pb2.GScipParameters = dataclasses.field( - default_factory=gscip_pb2.GScipParameters - ) - gurobi: GurobiParameters = dataclasses.field(default_factory=GurobiParameters) - glop: glop_parameters_pb2.GlopParameters = dataclasses.field( - default_factory=glop_parameters_pb2.GlopParameters + time_limit: Optional[datetime.timedelta] = None + iteration_limit: Optional[int] = None + node_limit: Optional[int] = None + cutoff_limit: Optional[float] = None + objective_limit: Optional[float] = None + best_bound_limit: Optional[float] = None + solution_limit: Optional[int] = None + enable_output: bool = False + threads: Optional[int] = None + random_seed: Optional[int] = None + absolute_gap_tolerance: Optional[float] = None + relative_gap_tolerance: Optional[float] = None + solution_pool_size: Optional[int] = None + lp_algorithm: Optional[LPAlgorithm] = None + presolve: Optional[Emphasis] = None + cuts: Optional[Emphasis] = None + heuristics: Optional[Emphasis] = None + scaling: Optional[Emphasis] = None + gscip: gscip_pb2.GScipParameters = dataclasses.field( + default_factory=gscip_pb2.GScipParameters + ) + gurobi: GurobiParameters = dataclasses.field(default_factory=GurobiParameters) + glop: glop_parameters_pb2.GlopParameters = dataclasses.field( + default_factory=glop_parameters_pb2.GlopParameters + ) + cp_sat: sat_parameters_pb2.SatParameters = dataclasses.field( + default_factory=sat_parameters_pb2.SatParameters + ) + pdlp: pdlp_solvers_pb2.PrimalDualHybridGradientParams = dataclasses.field( + default_factory=pdlp_solvers_pb2.PrimalDualHybridGradientParams + ) + osqp: osqp_pb2.OsqpSettingsProto = dataclasses.field( + default_factory=osqp_pb2.OsqpSettingsProto + ) + glpk: GlpkParameters = dataclasses.field(default_factory=GlpkParameters) + highs: highs_pb2.HighsOptionsProto = dataclasses.field( + default_factory=highs_pb2.HighsOptionsProto + ) + + def to_proto(self) -> math_opt_parameters_pb2.SolveParametersProto: + """Returns a protocol buffer equivalent to this.""" + result = math_opt_parameters_pb2.SolveParametersProto( + enable_output=self.enable_output, + lp_algorithm=lp_algorithm_to_proto(self.lp_algorithm), + presolve=emphasis_to_proto(self.presolve), + cuts=emphasis_to_proto(self.cuts), + heuristics=emphasis_to_proto(self.heuristics), + scaling=emphasis_to_proto(self.scaling), + gscip=self.gscip, + gurobi=self.gurobi.to_proto(), + glop=self.glop, + cp_sat=self.cp_sat, + pdlp=self.pdlp, + osqp=self.osqp, + glpk=self.glpk.to_proto(), + highs=self.highs, ) - cp_sat: sat_parameters_pb2.SatParameters = dataclasses.field( - default_factory=sat_parameters_pb2.SatParameters - ) - pdlp: pdlp_solvers_pb2.PrimalDualHybridGradientParams = dataclasses.field( - default_factory=pdlp_solvers_pb2.PrimalDualHybridGradientParams - ) - osqp: osqp_pb2.OsqpSettingsProto = dataclasses.field( - default_factory=osqp_pb2.OsqpSettingsProto - ) - glpk: GlpkParameters = dataclasses.field(default_factory=GlpkParameters) - highs: highs_pb2.HighsOptionsProto = dataclasses.field( - default_factory=highs_pb2.HighsOptionsProto - ) - - def to_proto(self) -> math_opt_parameters_pb2.SolveParametersProto: - """Returns a protocol buffer equivalent to this.""" - result = math_opt_parameters_pb2.SolveParametersProto( - enable_output=self.enable_output, - lp_algorithm=lp_algorithm_to_proto(self.lp_algorithm), - presolve=emphasis_to_proto(self.presolve), - cuts=emphasis_to_proto(self.cuts), - heuristics=emphasis_to_proto(self.heuristics), - scaling=emphasis_to_proto(self.scaling), - gscip=self.gscip, - gurobi=self.gurobi.to_proto(), - glop=self.glop, - cp_sat=self.cp_sat, - pdlp=self.pdlp, - osqp=self.osqp, - glpk=self.glpk.to_proto(), - highs=self.highs, - ) - if self.time_limit is not None: - result.time_limit.FromTimedelta(self.time_limit) - if self.iteration_limit is not None: - result.iteration_limit = self.iteration_limit - if self.node_limit is not None: - result.node_limit = self.node_limit - if self.cutoff_limit is not None: - result.cutoff_limit = self.cutoff_limit - if self.objective_limit is not None: - result.objective_limit = self.objective_limit - if self.best_bound_limit is not None: - result.best_bound_limit = self.best_bound_limit - if self.solution_limit is not None: - result.solution_limit = self.solution_limit - if self.threads is not None: - result.threads = self.threads - if self.random_seed is not None: - result.random_seed = self.random_seed - if self.absolute_gap_tolerance is not None: - result.absolute_gap_tolerance = self.absolute_gap_tolerance - if self.relative_gap_tolerance is not None: - result.relative_gap_tolerance = self.relative_gap_tolerance - if self.solution_pool_size is not None: - result.solution_pool_size = self.solution_pool_size - return result + if self.time_limit is not None: + result.time_limit.FromTimedelta(self.time_limit) + if self.iteration_limit is not None: + result.iteration_limit = self.iteration_limit + if self.node_limit is not None: + result.node_limit = self.node_limit + if self.cutoff_limit is not None: + result.cutoff_limit = self.cutoff_limit + if self.objective_limit is not None: + result.objective_limit = self.objective_limit + if self.best_bound_limit is not None: + result.best_bound_limit = self.best_bound_limit + if self.solution_limit is not None: + result.solution_limit = self.solution_limit + if self.threads is not None: + result.threads = self.threads + if self.random_seed is not None: + result.random_seed = self.random_seed + if self.absolute_gap_tolerance is not None: + result.absolute_gap_tolerance = self.absolute_gap_tolerance + if self.relative_gap_tolerance is not None: + result.relative_gap_tolerance = self.relative_gap_tolerance + if self.solution_pool_size is not None: + result.solution_pool_size = self.solution_pool_size + return result diff --git a/ortools/math_opt/python/parameters_test.py b/ortools/math_opt/python/parameters_test.py index 3fd057ed09d..8543c6b6aa3 100644 --- a/ortools/math_opt/python/parameters_test.py +++ b/ortools/math_opt/python/parameters_test.py @@ -32,202 +32,206 @@ class GurobiParameters(absltest.TestCase): - def test_to_proto(self) -> None: - gurobi_proto = parameters.GurobiParameters( - param_values={"x": "dog", "ab": "7"} - ).to_proto() - expected_proto = gurobi_pb2.GurobiParametersProto( - parameters=[ - gurobi_pb2.GurobiParametersProto.Parameter(name="x", value="dog"), - gurobi_pb2.GurobiParametersProto.Parameter(name="ab", value="7"), - ] - ) - self.assertEqual(expected_proto, gurobi_proto) + def test_to_proto(self) -> None: + gurobi_proto = parameters.GurobiParameters( + param_values={"x": "dog", "ab": "7"} + ).to_proto() + expected_proto = gurobi_pb2.GurobiParametersProto( + parameters=[ + gurobi_pb2.GurobiParametersProto.Parameter(name="x", value="dog"), + gurobi_pb2.GurobiParametersProto.Parameter(name="ab", value="7"), + ] + ) + self.assertEqual(expected_proto, gurobi_proto) class GlpkParameters(absltest.TestCase): - def test_to_proto(self) -> None: - # Test with `optional bool` set to true. - glpk_proto = parameters.GlpkParameters( - compute_unbound_rays_if_possible=True - ).to_proto() - expected_proto = glpk_pb2.GlpkParametersProto( - compute_unbound_rays_if_possible=True - ) - self.assertEqual(glpk_proto, expected_proto) - - # Test with `optional bool` set to false. - glpk_proto = parameters.GlpkParameters( - compute_unbound_rays_if_possible=False - ).to_proto() - expected_proto = glpk_pb2.GlpkParametersProto( - compute_unbound_rays_if_possible=False - ) - self.assertEqual(glpk_proto, expected_proto) + def test_to_proto(self) -> None: + # Test with `optional bool` set to true. + glpk_proto = parameters.GlpkParameters( + compute_unbound_rays_if_possible=True + ).to_proto() + expected_proto = glpk_pb2.GlpkParametersProto( + compute_unbound_rays_if_possible=True + ) + self.assertEqual(glpk_proto, expected_proto) + + # Test with `optional bool` set to false. + glpk_proto = parameters.GlpkParameters( + compute_unbound_rays_if_possible=False + ).to_proto() + expected_proto = glpk_pb2.GlpkParametersProto( + compute_unbound_rays_if_possible=False + ) + self.assertEqual(glpk_proto, expected_proto) - # Test with `optional bool` unset. - glpk_proto = parameters.GlpkParameters().to_proto() - expected_proto = glpk_pb2.GlpkParametersProto() - self.assertEqual(glpk_proto, expected_proto) + # Test with `optional bool` unset. + glpk_proto = parameters.GlpkParameters().to_proto() + expected_proto = glpk_pb2.GlpkParametersProto() + self.assertEqual(glpk_proto, expected_proto) class ProtoRoundTrip(absltest.TestCase): - def test_solver_type_round_trip(self) -> None: - for solver_type in parameters.SolverType: - self.assertEqual( - solver_type, - parameters.solver_type_from_proto( - parameters.solver_type_to_proto(solver_type) - ), - ) - self.assertEqual( - math_opt_parameters_pb2.SOLVER_TYPE_UNSPECIFIED, - parameters.solver_type_to_proto(None), - ) - self.assertIsNone( - parameters.solver_type_from_proto( - math_opt_parameters_pb2.SOLVER_TYPE_UNSPECIFIED - ) + def test_solver_type_round_trip(self) -> None: + for solver_type in parameters.SolverType: + self.assertEqual( + solver_type, + parameters.solver_type_from_proto( + parameters.solver_type_to_proto(solver_type) + ), + ) + self.assertEqual( + math_opt_parameters_pb2.SOLVER_TYPE_UNSPECIFIED, + parameters.solver_type_to_proto(None), + ) + self.assertIsNone( + parameters.solver_type_from_proto( + math_opt_parameters_pb2.SOLVER_TYPE_UNSPECIFIED ) + ) - def test_lp_algorithm_round_trip(self) -> None: - for lp_alg in parameters.LPAlgorithm: - self.assertEqual( - lp_alg, - parameters.lp_algorithm_from_proto( - parameters.lp_algorithm_to_proto(lp_alg) - ), - ) - self.assertEqual( - math_opt_parameters_pb2.LP_ALGORITHM_UNSPECIFIED, - parameters.lp_algorithm_to_proto(None), - ) - self.assertIsNone( - parameters.lp_algorithm_from_proto( - math_opt_parameters_pb2.LP_ALGORITHM_UNSPECIFIED - ) + def test_lp_algorithm_round_trip(self) -> None: + for lp_alg in parameters.LPAlgorithm: + self.assertEqual( + lp_alg, + parameters.lp_algorithm_from_proto( + parameters.lp_algorithm_to_proto(lp_alg) + ), + ) + self.assertEqual( + math_opt_parameters_pb2.LP_ALGORITHM_UNSPECIFIED, + parameters.lp_algorithm_to_proto(None), + ) + self.assertIsNone( + parameters.lp_algorithm_from_proto( + math_opt_parameters_pb2.LP_ALGORITHM_UNSPECIFIED ) + ) - def test_emphasis_round_trip(self) -> None: - for emph in parameters.Emphasis: - self.assertEqual( - emph, - parameters.emphasis_from_proto(parameters.emphasis_to_proto(emph)), - ) - self.assertEqual( - math_opt_parameters_pb2.EMPHASIS_UNSPECIFIED, - parameters.emphasis_to_proto(None), - ) - self.assertIsNone( - parameters.emphasis_from_proto(math_opt_parameters_pb2.EMPHASIS_UNSPECIFIED) + def test_emphasis_round_trip(self) -> None: + for emph in parameters.Emphasis: + self.assertEqual( + emph, + parameters.emphasis_from_proto(parameters.emphasis_to_proto(emph)), + ) + self.assertEqual( + math_opt_parameters_pb2.EMPHASIS_UNSPECIFIED, + parameters.emphasis_to_proto(None), + ) + self.assertIsNone( + parameters.emphasis_from_proto( + math_opt_parameters_pb2.EMPHASIS_UNSPECIFIED ) + ) -class SolveParametersTest(compare_proto.MathOptProtoAssertions, parameterized.TestCase): - """Test case for tests of SolveParameters.""" - - def test_common_to_proto(self) -> None: - params = parameters.SolveParameters( - time_limit=datetime.timedelta(seconds=10), - iteration_limit=7, - node_limit=3, - cutoff_limit=9.5, - objective_limit=10.5, - best_bound_limit=11.5, - solution_limit=2, - enable_output=True, - threads=3, - random_seed=12, - absolute_gap_tolerance=1.3, - relative_gap_tolerance=0.05, - solution_pool_size=17, - lp_algorithm=parameters.LPAlgorithm.BARRIER, - presolve=parameters.Emphasis.OFF, - cuts=parameters.Emphasis.LOW, - heuristics=parameters.Emphasis.MEDIUM, - scaling=parameters.Emphasis.HIGH, - ) - expected = math_opt_parameters_pb2.SolveParametersProto( - iteration_limit=7, - node_limit=3, - cutoff_limit=9.5, - objective_limit=10.5, - best_bound_limit=11.5, - solution_limit=2, - enable_output=True, - threads=3, - random_seed=12, - absolute_gap_tolerance=1.3, - relative_gap_tolerance=0.05, - solution_pool_size=17, - lp_algorithm=math_opt_parameters_pb2.LP_ALGORITHM_BARRIER, - presolve=math_opt_parameters_pb2.EMPHASIS_OFF, - cuts=math_opt_parameters_pb2.EMPHASIS_LOW, - heuristics=math_opt_parameters_pb2.EMPHASIS_MEDIUM, - scaling=math_opt_parameters_pb2.EMPHASIS_HIGH, - ) - expected.time_limit.FromTimedelta(datetime.timedelta(seconds=10)) - self.assert_protos_equiv(expected, params.to_proto()) - - def test_to_proto_with_none(self) -> None: - params = parameters.SolveParameters() - expected = math_opt_parameters_pb2.SolveParametersProto() - self.assert_protos_equiv(expected, params.to_proto()) - - @parameterized.named_parameters( - ( - "gscip", - "gscip", - gscip_pb2.GScipParameters(print_detailed_solving_stats=True), - ), - ( - "glop", - "glop", - glop_parameters_pb2.GlopParameters(refactorization_threshold=1e-5), - ), - ( - "gurobi", - "gurobi", - parameters.GurobiParameters(param_values={"NodeLimit": "30"}), - ), - ( - "cp_sat", - "cp_sat", - sat_parameters_pb2.SatParameters(random_branches_ratio=0.5), - ), - ("osqp", "osqp", osqp_pb2.OsqpSettingsProto(sigma=1.2)), - ( - "glpk", - "glpk", - parameters.GlpkParameters(compute_unbound_rays_if_possible=True), - ), - ( - "highs", - "highs", - highs_pb2.HighsOptionsProto(bool_options={"solve_relaxation": True}), - ), +class SolveParametersTest( + compare_proto.MathOptProtoAssertions, parameterized.TestCase +): + """Test case for tests of SolveParameters.""" + + def test_common_to_proto(self) -> None: + params = parameters.SolveParameters( + time_limit=datetime.timedelta(seconds=10), + iteration_limit=7, + node_limit=3, + cutoff_limit=9.5, + objective_limit=10.5, + best_bound_limit=11.5, + solution_limit=2, + enable_output=True, + threads=3, + random_seed=12, + absolute_gap_tolerance=1.3, + relative_gap_tolerance=0.05, + solution_pool_size=17, + lp_algorithm=parameters.LPAlgorithm.BARRIER, + presolve=parameters.Emphasis.OFF, + cuts=parameters.Emphasis.LOW, + heuristics=parameters.Emphasis.MEDIUM, + scaling=parameters.Emphasis.HIGH, ) - def test_to_proto_with_specifics( - self, field: str, solver_specific_param: Any - ) -> None: - solve_params = parameters.SolveParameters(threads=3) - setattr(solve_params, field, solver_specific_param) - expected = math_opt_parameters_pb2.SolveParametersProto(threads=3) - proto_solver_specific_param = ( - solver_specific_param.to_proto() - if field in ("gurobi", "glpk") - else solver_specific_param - ) - getattr(expected, field).CopyFrom(proto_solver_specific_param) - self.assert_protos_equiv(expected, solve_params.to_proto()) + expected = math_opt_parameters_pb2.SolveParametersProto( + iteration_limit=7, + node_limit=3, + cutoff_limit=9.5, + objective_limit=10.5, + best_bound_limit=11.5, + solution_limit=2, + enable_output=True, + threads=3, + random_seed=12, + absolute_gap_tolerance=1.3, + relative_gap_tolerance=0.05, + solution_pool_size=17, + lp_algorithm=math_opt_parameters_pb2.LP_ALGORITHM_BARRIER, + presolve=math_opt_parameters_pb2.EMPHASIS_OFF, + cuts=math_opt_parameters_pb2.EMPHASIS_LOW, + heuristics=math_opt_parameters_pb2.EMPHASIS_MEDIUM, + scaling=math_opt_parameters_pb2.EMPHASIS_HIGH, + ) + expected.time_limit.FromTimedelta(datetime.timedelta(seconds=10)) + self.assert_protos_equiv(expected, params.to_proto()) + + def test_to_proto_with_none(self) -> None: + params = parameters.SolveParameters() + expected = math_opt_parameters_pb2.SolveParametersProto() + self.assert_protos_equiv(expected, params.to_proto()) + + @parameterized.named_parameters( + ( + "gscip", + "gscip", + gscip_pb2.GScipParameters(print_detailed_solving_stats=True), + ), + ( + "glop", + "glop", + glop_parameters_pb2.GlopParameters(refactorization_threshold=1e-5), + ), + ( + "gurobi", + "gurobi", + parameters.GurobiParameters(param_values={"NodeLimit": "30"}), + ), + ( + "cp_sat", + "cp_sat", + sat_parameters_pb2.SatParameters(random_branches_ratio=0.5), + ), + ("osqp", "osqp", osqp_pb2.OsqpSettingsProto(sigma=1.2)), + ( + "glpk", + "glpk", + parameters.GlpkParameters(compute_unbound_rays_if_possible=True), + ), + ( + "highs", + "highs", + highs_pb2.HighsOptionsProto(bool_options={"solve_relaxation": True}), + ), + ) + def test_to_proto_with_specifics( + self, field: str, solver_specific_param: Any + ) -> None: + solve_params = parameters.SolveParameters(threads=3) + setattr(solve_params, field, solver_specific_param) + expected = math_opt_parameters_pb2.SolveParametersProto(threads=3) + proto_solver_specific_param = ( + solver_specific_param.to_proto() + if field in ("gurobi", "glpk") + else solver_specific_param + ) + getattr(expected, field).CopyFrom(proto_solver_specific_param) + self.assert_protos_equiv(expected, solve_params.to_proto()) - def test_to_proto_no_specifics(self) -> None: - solve_params = parameters.SolveParameters(threads=3) - expected = math_opt_parameters_pb2.SolveParametersProto(threads=3) - self.assert_protos_equiv(expected, solve_params.to_proto()) + def test_to_proto_no_specifics(self) -> None: + solve_params = parameters.SolveParameters(threads=3) + expected = math_opt_parameters_pb2.SolveParametersProto(threads=3) + self.assert_protos_equiv(expected, solve_params.to_proto()) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/quadratic_constraints.py b/ortools/math_opt/python/quadratic_constraints.py index b4642cf0787..b16bbb8d0b3 100644 --- a/ortools/math_opt/python/quadratic_constraints.py +++ b/ortools/math_opt/python/quadratic_constraints.py @@ -22,156 +22,156 @@ class QuadraticConstraint(from_model.FromModel): - """A quadratic constraint for an optimization model. - - A QuadraticConstraint adds the following restriction on feasible solutions to - an optimization model: - lb <= sum_i a_i x_i + sum_i sum_{j : i <= j} b_ij x_i x_j <= ub - where x_i are the decision variables of the problem. lb == ub is allowed, and - this models an equality constraint. lb > ub is also allowed, but the - optimization problem will be infeasible. - - Quadratic constraints have limited mutability. You can delete a variable - that the constraint uses, or you can delete the entire constraint. You - currently cannot update bounds or coefficients. This may change in future - versions. - - A QuadraticConstraint can be queried as follows: - * lower_bound: a float property, lb above. Should not be NaN nor +inf. - * upper_bound: a float property, ub above. Should not be NaN nor -inf. - * get_linear_coefficient(): get the a_i * x_i terms. The variable must be - from the same model as this constraint, and the a_i must be finite and not - NaN. The coefficient for any variable not set is 0.0. - * get_quadratic_coefficient(): like get_linear_coefficient() but for the - b_ij terms. Note that get_quadratic_coefficient(x, y, 8) and - get_quadratic_coefficient(y, x, 8) have the same result. - - The name is optional, read only, and used only for debugging. Non-empty names - should be distinct. - - Do not create a QuadraticConstraint directly, use - Model.add_quadratic_constraint() instead. Two QuadraticConstraint objects - can represent the same constraint (for the same model). They will have the - same underlying QuadraticConstraint.elemental for storing the data. The - QuadraticConstraint class is simply a reference to an Elemental. + """A quadratic constraint for an optimization model. + + A QuadraticConstraint adds the following restriction on feasible solutions to + an optimization model: + lb <= sum_i a_i x_i + sum_i sum_{j : i <= j} b_ij x_i x_j <= ub + where x_i are the decision variables of the problem. lb == ub is allowed, and + this models an equality constraint. lb > ub is also allowed, but the + optimization problem will be infeasible. + + Quadratic constraints have limited mutability. You can delete a variable + that the constraint uses, or you can delete the entire constraint. You + currently cannot update bounds or coefficients. This may change in future + versions. + + A QuadraticConstraint can be queried as follows: + * lower_bound: a float property, lb above. Should not be NaN nor +inf. + * upper_bound: a float property, ub above. Should not be NaN nor -inf. + * get_linear_coefficient(): get the a_i * x_i terms. The variable must be + from the same model as this constraint, and the a_i must be finite and not + NaN. The coefficient for any variable not set is 0.0. + * get_quadratic_coefficient(): like get_linear_coefficient() but for the + b_ij terms. Note that get_quadratic_coefficient(x, y, 8) and + get_quadratic_coefficient(y, x, 8) have the same result. + + The name is optional, read only, and used only for debugging. Non-empty names + should be distinct. + + Do not create a QuadraticConstraint directly, use + Model.add_quadratic_constraint() instead. Two QuadraticConstraint objects + can represent the same constraint (for the same model). They will have the + same underlying QuadraticConstraint.elemental for storing the data. The + QuadraticConstraint class is simply a reference to an Elemental. + """ + + __slots__ = "_elemental", "_id" + + def __init__(self, elem: elemental.Elemental, cid: int) -> None: + """Internal only, prefer Model functions (add_quadratic_constraint() and get_quadratic_constraint()).""" + if not isinstance(cid, int): + raise TypeError(f"cid type should be int, was:{type(cid).__name__!r}") + self._elemental: elemental.Elemental = elem + self._id: int = cid + + @property + def lower_bound(self) -> float: + """The quadratic expression of the constraint must be at least this.""" + return self._elemental.get_attr( + enums.DoubleAttr1.QUADRATIC_CONSTRAINT_LOWER_BOUND, (self._id,) + ) + + @property + def upper_bound(self) -> float: + """The quadratic expression of the constraint must be at most this.""" + return self._elemental.get_attr( + enums.DoubleAttr1.QUADRATIC_CONSTRAINT_UPPER_BOUND, (self._id,) + ) + + @property + def name(self) -> str: + """The name of this constraint.""" + return self._elemental.get_element_name( + enums.ElementType.QUADRATIC_CONSTRAINT, self._id + ) + + @property + def id(self) -> int: + """A unique (for the model) identifier for this constraint.""" + return self._id + + @property + def elemental(self) -> elemental.Elemental: + """Internal use only.""" + return self._elemental + + def get_linear_coefficient(self, var: variables.Variable) -> float: + """Returns the linear coefficient for var in the constraint's quadratic expression.""" + from_model.model_is_same(var, self) + return self._elemental.get_attr( + enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, + (self._id, var.id), + ) + + def linear_terms(self) -> Iterator[variables.LinearTerm]: + """Yields variable/coefficient pairs from the linear part of the constraint. + + Only the pairs with nonzero coefficient are returned. + + Yields: + The variable, coefficient pairs. """ - - __slots__ = "_elemental", "_id" - - def __init__(self, elem: elemental.Elemental, cid: int) -> None: - """Internal only, prefer Model functions (add_quadratic_constraint() and get_quadratic_constraint()).""" - if not isinstance(cid, int): - raise TypeError(f"cid type should be int, was:{type(cid).__name__!r}") - self._elemental: elemental.Elemental = elem - self._id: int = cid - - @property - def lower_bound(self) -> float: - """The quadratic expression of the constraint must be at least this.""" - return self._elemental.get_attr( - enums.DoubleAttr1.QUADRATIC_CONSTRAINT_LOWER_BOUND, (self._id,) - ) - - @property - def upper_bound(self) -> float: - """The quadratic expression of the constraint must be at most this.""" - return self._elemental.get_attr( - enums.DoubleAttr1.QUADRATIC_CONSTRAINT_UPPER_BOUND, (self._id,) - ) - - @property - def name(self) -> str: - """The name of this constraint.""" - return self._elemental.get_element_name( - enums.ElementType.QUADRATIC_CONSTRAINT, self._id - ) - - @property - def id(self) -> int: - """A unique (for the model) identifier for this constraint.""" - return self._id - - @property - def elemental(self) -> elemental.Elemental: - """Internal use only.""" - return self._elemental - - def get_linear_coefficient(self, var: variables.Variable) -> float: - """Returns the linear coefficient for var in the constraint's quadratic expression.""" - from_model.model_is_same(var, self) - return self._elemental.get_attr( - enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, - (self._id, var.id), - ) - - def linear_terms(self) -> Iterator[variables.LinearTerm]: - """Yields variable/coefficient pairs from the linear part of the constraint. - - Only the pairs with nonzero coefficient are returned. - - Yields: - The variable, coefficient pairs. - """ - keys = self._elemental.slice_attr( - enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, 0, self._id - ) - coefs = self._elemental.get_attrs( - enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, keys - ) - for i in range(len(keys)): - yield variables.LinearTerm( - variable=variables.Variable(self._elemental, int(keys[i, 1])), - coefficient=float(coefs[i]), - ) - - def get_quadratic_coefficient( - self, var1: variables.Variable, var2: variables.Variable - ) -> float: - """Returns the quadratic coefficient for the pair (var1, var2) in the constraint's quadratic expression.""" - from_model.model_is_same(var1, self) - from_model.model_is_same(var2, self) - return self._elemental.get_attr( - enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, - (self._id, var1.id, var2.id), - ) - - def quadratic_terms(self) -> Iterator[variables.QuadraticTerm]: - """Yields variable/coefficient pairs from the quadratic part of the constraint. - - Only the pairs with nonzero coefficient are returned. - - Yields: - The variable, coefficient pairs. - """ - keys = self._elemental.slice_attr( - enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, - 0, - self._id, - ) - coefs = self._elemental.get_attrs( - enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, - keys, - ) - for i in range(len(keys)): - yield variables.QuadraticTerm( - variables.QuadraticTermKey( - variables.Variable(self._elemental, int(keys[i, 1])), - variables.Variable(self._elemental, int(keys[i, 2])), - ), - coefficient=float(coefs[i]), - ) - - def __str__(self): - """Returns the name, or a string containing the id if the name is empty.""" - return self.name if self.name else f"quadratic_constraint_{self.id}" - - def __repr__(self): - return f"" - - def __eq__(self, other: Any) -> bool: - if isinstance(other, QuadraticConstraint): - return self._id == other._id and self._elemental is other._elemental - return False - - def __hash__(self) -> int: - return hash((self._id, self._elemental)) + keys = self._elemental.slice_attr( + enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, 0, self._id + ) + coefs = self._elemental.get_attrs( + enums.DoubleAttr2.QUADRATIC_CONSTRAINT_LINEAR_COEFFICIENT, keys + ) + for i in range(len(keys)): + yield variables.LinearTerm( + variable=variables.Variable(self._elemental, int(keys[i, 1])), + coefficient=float(coefs[i]), + ) + + def get_quadratic_coefficient( + self, var1: variables.Variable, var2: variables.Variable + ) -> float: + """Returns the quadratic coefficient for the pair (var1, var2) in the constraint's quadratic expression.""" + from_model.model_is_same(var1, self) + from_model.model_is_same(var2, self) + return self._elemental.get_attr( + enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, + (self._id, var1.id, var2.id), + ) + + def quadratic_terms(self) -> Iterator[variables.QuadraticTerm]: + """Yields variable/coefficient pairs from the quadratic part of the constraint. + + Only the pairs with nonzero coefficient are returned. + + Yields: + The variable, coefficient pairs. + """ + keys = self._elemental.slice_attr( + enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, + 0, + self._id, + ) + coefs = self._elemental.get_attrs( + enums.SymmetricDoubleAttr3.QUADRATIC_CONSTRAINT_QUADRATIC_COEFFICIENT, + keys, + ) + for i in range(len(keys)): + yield variables.QuadraticTerm( + variables.QuadraticTermKey( + variables.Variable(self._elemental, int(keys[i, 1])), + variables.Variable(self._elemental, int(keys[i, 2])), + ), + coefficient=float(coefs[i]), + ) + + def __str__(self): + """Returns the name, or a string containing the id if the name is empty.""" + return self.name if self.name else f"quadratic_constraint_{self.id}" + + def __repr__(self): + return f"" + + def __eq__(self, other: Any) -> bool: + if isinstance(other, QuadraticConstraint): + return self._id == other._id and self._elemental is other._elemental + return False + + def __hash__(self) -> int: + return hash((self._id, self._elemental)) diff --git a/ortools/math_opt/python/quadratic_constraints_test.py b/ortools/math_opt/python/quadratic_constraints_test.py index e869828af66..ef2263f1be6 100644 --- a/ortools/math_opt/python/quadratic_constraints_test.py +++ b/ortools/math_opt/python/quadratic_constraints_test.py @@ -20,76 +20,76 @@ class QuadraticConstraintsTest(absltest.TestCase): - def test_empty_constraint(self) -> None: - mod = model.Model() - x = mod.add_variable() - quad_con = mod.add_quadratic_constraint() - self.assertEqual(quad_con.lower_bound, -math.inf) - self.assertEqual(quad_con.upper_bound, math.inf) - self.assertEqual(quad_con.id, 0) - self.assertEqual(quad_con.name, "") - self.assertEqual(quad_con.get_linear_coefficient(x), 0.0) - self.assertEqual(quad_con.get_quadratic_coefficient(x, x), 0.0) - self.assertEmpty(list(quad_con.linear_terms())) - self.assertEmpty(list(quad_con.quadratic_terms())) + def test_empty_constraint(self) -> None: + mod = model.Model() + x = mod.add_variable() + quad_con = mod.add_quadratic_constraint() + self.assertEqual(quad_con.lower_bound, -math.inf) + self.assertEqual(quad_con.upper_bound, math.inf) + self.assertEqual(quad_con.id, 0) + self.assertEqual(quad_con.name, "") + self.assertEqual(quad_con.get_linear_coefficient(x), 0.0) + self.assertEqual(quad_con.get_quadratic_coefficient(x, x), 0.0) + self.assertEmpty(list(quad_con.linear_terms())) + self.assertEmpty(list(quad_con.quadratic_terms())) - def test_full_constraint(self) -> None: - mod = model.Model() - x = mod.add_variable() - y = mod.add_variable() - quad_con = mod.add_quadratic_constraint( - lb=3.0, ub=4.0, expr=5 * x + 6 * y + 7 * x * y + 8 * y * y, name="c" - ) - self.assertEqual(quad_con.lower_bound, 3.0) - self.assertEqual(quad_con.upper_bound, 4.0) - self.assertEqual(quad_con.id, 0) - self.assertEqual(quad_con.name, "c") - self.assertEqual(quad_con.get_linear_coefficient(x), 5.0) - self.assertEqual(quad_con.get_linear_coefficient(y), 6.0) - self.assertEqual(quad_con.get_quadratic_coefficient(x, y), 7.0) - self.assertEqual(quad_con.get_quadratic_coefficient(y, y), 8.0) - self.assertDictEqual( - {term.variable: term.coefficient for term in quad_con.linear_terms()}, - {x: 5.0, y: 6.0}, - ) - self.assertDictEqual( - { - (term.key.first_var, term.key.second_var): term.coefficient - for term in quad_con.quadratic_terms() - }, - {(x, y): 7.0, (y, y): 8.0}, - ) + def test_full_constraint(self) -> None: + mod = model.Model() + x = mod.add_variable() + y = mod.add_variable() + quad_con = mod.add_quadratic_constraint( + lb=3.0, ub=4.0, expr=5 * x + 6 * y + 7 * x * y + 8 * y * y, name="c" + ) + self.assertEqual(quad_con.lower_bound, 3.0) + self.assertEqual(quad_con.upper_bound, 4.0) + self.assertEqual(quad_con.id, 0) + self.assertEqual(quad_con.name, "c") + self.assertEqual(quad_con.get_linear_coefficient(x), 5.0) + self.assertEqual(quad_con.get_linear_coefficient(y), 6.0) + self.assertEqual(quad_con.get_quadratic_coefficient(x, y), 7.0) + self.assertEqual(quad_con.get_quadratic_coefficient(y, y), 8.0) + self.assertDictEqual( + {term.variable: term.coefficient for term in quad_con.linear_terms()}, + {x: 5.0, y: 6.0}, + ) + self.assertDictEqual( + { + (term.key.first_var, term.key.second_var): term.coefficient + for term in quad_con.quadratic_terms() + }, + {(x, y): 7.0, (y, y): 8.0}, + ) - def test_eq(self) -> None: - mod1 = model.Model() - mod2 = model.Model() - q1 = mod1.add_quadratic_constraint() - q2 = mod1.add_quadratic_constraint() - q3 = mod2.add_quadratic_constraint() - q1_other = mod1.get_quadratic_constraint(0) - self.assertEqual(q1, q1_other) - self.assertNotEqual(q1, q2) - self.assertNotEqual(q1, q3) - self.assertNotEqual(q1, "cat") + def test_eq(self) -> None: + mod1 = model.Model() + mod2 = model.Model() + q1 = mod1.add_quadratic_constraint() + q2 = mod1.add_quadratic_constraint() + q3 = mod2.add_quadratic_constraint() + q1_other = mod1.get_quadratic_constraint(0) + self.assertEqual(q1, q1_other) + self.assertNotEqual(q1, q2) + self.assertNotEqual(q1, q3) + self.assertNotEqual(q1, "cat") - def test_str(self) -> None: - mod = model.Model() - quad_con = mod.add_quadratic_constraint(name="qqq") - self.assertEqual(str(quad_con), "qqq") - self.assertEqual(repr(quad_con), "") + def test_str(self) -> None: + mod = model.Model() + quad_con = mod.add_quadratic_constraint(name="qqq") + self.assertEqual(str(quad_con), "qqq") + self.assertEqual(repr(quad_con), "") - def test_get_coefficient_variable_wrong_model(self) -> None: - mod1 = model.Model() - mod2 = model.Model() - q1 = mod1.add_quadratic_constraint() - # Ensure the bad model, not the bad id, causes the error. - mod1.add_variable() - x2 = mod2.add_variable() - with self.assertRaises(ValueError): - q1.get_linear_coefficient(x2) - with self.assertRaises(ValueError): - q1.get_quadratic_coefficient(x2, x2) + def test_get_coefficient_variable_wrong_model(self) -> None: + mod1 = model.Model() + mod2 = model.Model() + q1 = mod1.add_quadratic_constraint() + # Ensure the bad model, not the bad id, causes the error. + mod1.add_variable() + x2 = mod2.add_variable() + with self.assertRaises(ValueError): + q1.get_linear_coefficient(x2) + with self.assertRaises(ValueError): + q1.get_quadratic_coefficient(x2, x2) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/result.py b/ortools/math_opt/python/result.py index 9cb55c8c6c7..b3253961c2a 100644 --- a/ortools/math_opt/python/result.py +++ b/ortools/math_opt/python/result.py @@ -33,87 +33,87 @@ @enum.unique class FeasibilityStatus(enum.Enum): - """Problem feasibility status as claimed by the solver. + """Problem feasibility status as claimed by the solver. - (solver is not required to return a certificate for the claim.) + (solver is not required to return a certificate for the claim.) - Attributes: - UNDETERMINED: Solver does not claim a status. - FEASIBLE: Solver claims the problem is feasible. - INFEASIBLE: Solver claims the problem is infeasible. - """ + Attributes: + UNDETERMINED: Solver does not claim a status. + FEASIBLE: Solver claims the problem is feasible. + INFEASIBLE: Solver claims the problem is infeasible. + """ - UNDETERMINED = result_pb2.FEASIBILITY_STATUS_UNDETERMINED - FEASIBLE = result_pb2.FEASIBILITY_STATUS_FEASIBLE - INFEASIBLE = result_pb2.FEASIBILITY_STATUS_INFEASIBLE + UNDETERMINED = result_pb2.FEASIBILITY_STATUS_UNDETERMINED + FEASIBLE = result_pb2.FEASIBILITY_STATUS_FEASIBLE + INFEASIBLE = result_pb2.FEASIBILITY_STATUS_INFEASIBLE @dataclasses.dataclass(frozen=True) class ProblemStatus: - """Feasibility status of the primal problem and its dual (or dual relaxation). - - Statuses are as claimed by the solver and a dual relaxation is the dual of a - continuous relaxation for the original problem (e.g. the LP relaxation of a - MIP). The solver is not required to return a certificate for the feasibility - or infeasibility claims (e.g. the solver may claim primal feasibility without - returning a primal feasible solutuion). This combined status gives a - comprehensive description of a solver's claims about feasibility and - unboundedness of the solved problem. For instance, - * a feasible status for primal and dual problems indicates the primal is - feasible and bounded and likely has an optimal solution (guaranteed for - problems without non-linear constraints). - * a primal feasible and a dual infeasible status indicates the primal - problem is unbounded (i.e. has arbitrarily good solutions). - Note that a dual infeasible status by itself (i.e. accompanied by an - undetermined primal status) does not imply the primal problem is unbounded as - we could have both problems be infeasible. Also, while a primal and dual - feasible status may imply the existence of an optimal solution, it does not - guarantee the solver has actually found such optimal solution. - - Attributes: - primal_status: Status for the primal problem. - dual_status: Status for the dual problem (or for the dual of a continuous - relaxation). - primal_or_dual_infeasible: If true, the solver claims the primal or dual - problem is infeasible, but it does not know which (or if both are - infeasible). Can be true only when primal_problem_status = - dual_problem_status = kUndetermined. This extra information is often - needed when preprocessing determines there is no optimal solution to the - problem (but can't determine if it is due to infeasibility, unboundedness, - or both). - """ - - primal_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED - dual_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED - primal_or_dual_infeasible: bool = False + """Feasibility status of the primal problem and its dual (or dual relaxation). + + Statuses are as claimed by the solver and a dual relaxation is the dual of a + continuous relaxation for the original problem (e.g. the LP relaxation of a + MIP). The solver is not required to return a certificate for the feasibility + or infeasibility claims (e.g. the solver may claim primal feasibility without + returning a primal feasible solutuion). This combined status gives a + comprehensive description of a solver's claims about feasibility and + unboundedness of the solved problem. For instance, + * a feasible status for primal and dual problems indicates the primal is + feasible and bounded and likely has an optimal solution (guaranteed for + problems without non-linear constraints). + * a primal feasible and a dual infeasible status indicates the primal + problem is unbounded (i.e. has arbitrarily good solutions). + Note that a dual infeasible status by itself (i.e. accompanied by an + undetermined primal status) does not imply the primal problem is unbounded as + we could have both problems be infeasible. Also, while a primal and dual + feasible status may imply the existence of an optimal solution, it does not + guarantee the solver has actually found such optimal solution. - def to_proto(self) -> result_pb2.ProblemStatusProto: - """Returns an equivalent proto for a problem status.""" - return result_pb2.ProblemStatusProto( - primal_status=self.primal_status.value, - dual_status=self.dual_status.value, - primal_or_dual_infeasible=self.primal_or_dual_infeasible, - ) + Attributes: + primal_status: Status for the primal problem. + dual_status: Status for the dual problem (or for the dual of a continuous + relaxation). + primal_or_dual_infeasible: If true, the solver claims the primal or dual + problem is infeasible, but it does not know which (or if both are + infeasible). Can be true only when primal_problem_status = + dual_problem_status = kUndetermined. This extra information is often + needed when preprocessing determines there is no optimal solution to the + problem (but can't determine if it is due to infeasibility, unboundedness, + or both). + """ + + primal_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED + dual_status: FeasibilityStatus = FeasibilityStatus.UNDETERMINED + primal_or_dual_infeasible: bool = False + + def to_proto(self) -> result_pb2.ProblemStatusProto: + """Returns an equivalent proto for a problem status.""" + return result_pb2.ProblemStatusProto( + primal_status=self.primal_status.value, + dual_status=self.dual_status.value, + primal_or_dual_infeasible=self.primal_or_dual_infeasible, + ) def parse_problem_status(proto: result_pb2.ProblemStatusProto) -> ProblemStatus: - """Returns an equivalent ProblemStatus from the input proto.""" - primal_status_proto = proto.primal_status - if primal_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED: - raise ValueError("Primal feasibility status should not be UNSPECIFIED") - dual_status_proto = proto.dual_status - if dual_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED: - raise ValueError("Dual feasibility status should not be UNSPECIFIED") - return ProblemStatus( - primal_status=FeasibilityStatus(primal_status_proto), - dual_status=FeasibilityStatus(dual_status_proto), - primal_or_dual_infeasible=proto.primal_or_dual_infeasible, - ) + """Returns an equivalent ProblemStatus from the input proto.""" + primal_status_proto = proto.primal_status + if primal_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED: + raise ValueError("Primal feasibility status should not be UNSPECIFIED") + dual_status_proto = proto.dual_status + if dual_status_proto == result_pb2.FEASIBILITY_STATUS_UNSPECIFIED: + raise ValueError("Dual feasibility status should not be UNSPECIFIED") + return ProblemStatus( + primal_status=FeasibilityStatus(primal_status_proto), + dual_status=FeasibilityStatus(dual_status_proto), + primal_or_dual_infeasible=proto.primal_or_dual_infeasible, + ) @dataclasses.dataclass(frozen=True) class ObjectiveBounds: - """Bounds on the optimal objective value. + """Bounds on the optimal objective value. MOE:begin_intracomment_strip See go/mathopt-objective-bounds for more details. @@ -149,885 +149,921 @@ class ObjectiveBounds: MOE:end_intracomment_strip """ # fmt: skip - primal_bound: float = 0.0 - dual_bound: float = 0.0 + primal_bound: float = 0.0 + dual_bound: float = 0.0 - def to_proto(self) -> result_pb2.ObjectiveBoundsProto: - """Returns an equivalent proto for objective bounds.""" - return result_pb2.ObjectiveBoundsProto( - primal_bound=self.primal_bound, dual_bound=self.dual_bound - ) + def to_proto(self) -> result_pb2.ObjectiveBoundsProto: + """Returns an equivalent proto for objective bounds.""" + return result_pb2.ObjectiveBoundsProto( + primal_bound=self.primal_bound, dual_bound=self.dual_bound + ) def parse_objective_bounds( proto: result_pb2.ObjectiveBoundsProto, ) -> ObjectiveBounds: - """Returns an equivalent ObjectiveBounds from the input proto.""" - return ObjectiveBounds(primal_bound=proto.primal_bound, dual_bound=proto.dual_bound) + """Returns an equivalent ObjectiveBounds from the input proto.""" + return ObjectiveBounds( + primal_bound=proto.primal_bound, dual_bound=proto.dual_bound + ) @dataclasses.dataclass class SolveStats: - """Problem statuses and solve statistics returned by the solver. - - Attributes: - solve_time: Elapsed wall clock time as measured by math_opt, roughly the - time inside solve(). Note: this does not include work done building the - model. - simplex_iterations: Simplex iterations. - barrier_iterations: Barrier iterations. - first_order_iterations: First order iterations. - node_count: Node count. - """ + """Problem statuses and solve statistics returned by the solver. - solve_time: datetime.timedelta = datetime.timedelta() - simplex_iterations: int = 0 - barrier_iterations: int = 0 - first_order_iterations: int = 0 - node_count: int = 0 - - def to_proto(self) -> result_pb2.SolveStatsProto: - """Returns an equivalent proto for a solve stats.""" - result = result_pb2.SolveStatsProto( - simplex_iterations=self.simplex_iterations, - barrier_iterations=self.barrier_iterations, - first_order_iterations=self.first_order_iterations, - node_count=self.node_count, - ) - result.solve_time.FromTimedelta(self.solve_time) - return result + Attributes: + solve_time: Elapsed wall clock time as measured by math_opt, roughly the + time inside solve(). Note: this does not include work done building the + model. + simplex_iterations: Simplex iterations. + barrier_iterations: Barrier iterations. + first_order_iterations: First order iterations. + node_count: Node count. + """ + + solve_time: datetime.timedelta = datetime.timedelta() + simplex_iterations: int = 0 + barrier_iterations: int = 0 + first_order_iterations: int = 0 + node_count: int = 0 + + def to_proto(self) -> result_pb2.SolveStatsProto: + """Returns an equivalent proto for a solve stats.""" + result = result_pb2.SolveStatsProto( + simplex_iterations=self.simplex_iterations, + barrier_iterations=self.barrier_iterations, + first_order_iterations=self.first_order_iterations, + node_count=self.node_count, + ) + result.solve_time.FromTimedelta(self.solve_time) + return result def parse_solve_stats(proto: result_pb2.SolveStatsProto) -> SolveStats: - """Returns an equivalent SolveStats from the input proto.""" - result = SolveStats() - result.solve_time = proto.solve_time.ToTimedelta() - result.simplex_iterations = proto.simplex_iterations - result.barrier_iterations = proto.barrier_iterations - result.first_order_iterations = proto.first_order_iterations - result.node_count = proto.node_count - return result + """Returns an equivalent SolveStats from the input proto.""" + result = SolveStats() + result.solve_time = proto.solve_time.ToTimedelta() + result.simplex_iterations = proto.simplex_iterations + result.barrier_iterations = proto.barrier_iterations + result.first_order_iterations = proto.first_order_iterations + result.node_count = proto.node_count + return result @enum.unique class TerminationReason(enum.Enum): - """The reason a solve of a model terminated. - - These reasons are typically as reported by the underlying solver, e.g. we do - not attempt to verify the precision of the solution returned. - - The values are: - * OPTIMAL: A provably optimal solution (up to numerical tolerances) has - been found. - * INFEASIBLE: The primal problem has no feasible solutions. - * UNBOUNDED: The primal problem is feasible and arbitrarily good solutions - can be found along a primal ray. - * INFEASIBLE_OR_UNBOUNDED: The primal problem is either infeasible or - unbounded. More details on the problem status may be available in - solve_stats.problem_status. Note that Gurobi's unbounded status may be - mapped here as explained in - go/mathopt-solver-specific#gurobi-inf-or-unb. - * IMPRECISE: The problem was solved to one of the criteria above (Optimal, - Infeasible, Unbounded, or InfeasibleOrUnbounded), but one or more - tolerances was not met. Some primal/dual solutions/rays may be present, - but either they will be slightly infeasible, or (if the problem was - nearly optimal) their may be a gap between the best solution objective - and best objective bound. - - Users can still query primal/dual solutions/rays and solution stats, - but they are responsible for dealing with the numerical imprecision. - * FEASIBLE: The optimizer reached some kind of limit and a primal feasible - solution is returned. See SolveResultProto.limit_detail for detailed - description of the kind of limit that was reached. - * NO_SOLUTION_FOUND: The optimizer reached some kind of limit and it did - not find a primal feasible solution. See SolveResultProto.limit_detail - for detailed description of the kind of limit that was reached. - * NUMERICAL_ERROR: The algorithm stopped because it encountered - unrecoverable numerical error. No solution information is present. - * OTHER_ERROR: The algorithm stopped because of an error not covered by one - of the statuses defined above. No solution information is present. - """ - - OPTIMAL = result_pb2.TERMINATION_REASON_OPTIMAL - INFEASIBLE = result_pb2.TERMINATION_REASON_INFEASIBLE - UNBOUNDED = result_pb2.TERMINATION_REASON_UNBOUNDED - INFEASIBLE_OR_UNBOUNDED = result_pb2.TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED - IMPRECISE = result_pb2.TERMINATION_REASON_IMPRECISE - FEASIBLE = result_pb2.TERMINATION_REASON_FEASIBLE - NO_SOLUTION_FOUND = result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND - NUMERICAL_ERROR = result_pb2.TERMINATION_REASON_NUMERICAL_ERROR - OTHER_ERROR = result_pb2.TERMINATION_REASON_OTHER_ERROR + """The reason a solve of a model terminated. + + These reasons are typically as reported by the underlying solver, e.g. we do + not attempt to verify the precision of the solution returned. + + The values are: + * OPTIMAL: A provably optimal solution (up to numerical tolerances) has + been found. + * INFEASIBLE: The primal problem has no feasible solutions. + * UNBOUNDED: The primal problem is feasible and arbitrarily good solutions + can be found along a primal ray. + * INFEASIBLE_OR_UNBOUNDED: The primal problem is either infeasible or + unbounded. More details on the problem status may be available in + solve_stats.problem_status. Note that Gurobi's unbounded status may be + mapped here as explained in + go/mathopt-solver-specific#gurobi-inf-or-unb. + * IMPRECISE: The problem was solved to one of the criteria above (Optimal, + Infeasible, Unbounded, or InfeasibleOrUnbounded), but one or more + tolerances was not met. Some primal/dual solutions/rays may be present, + but either they will be slightly infeasible, or (if the problem was + nearly optimal) their may be a gap between the best solution objective + and best objective bound. + + Users can still query primal/dual solutions/rays and solution stats, + but they are responsible for dealing with the numerical imprecision. + * FEASIBLE: The optimizer reached some kind of limit and a primal feasible + solution is returned. See SolveResultProto.limit_detail for detailed + description of the kind of limit that was reached. + * NO_SOLUTION_FOUND: The optimizer reached some kind of limit and it did + not find a primal feasible solution. See SolveResultProto.limit_detail + for detailed description of the kind of limit that was reached. + * NUMERICAL_ERROR: The algorithm stopped because it encountered + unrecoverable numerical error. No solution information is present. + * OTHER_ERROR: The algorithm stopped because of an error not covered by one + of the statuses defined above. No solution information is present. + """ + + OPTIMAL = result_pb2.TERMINATION_REASON_OPTIMAL + INFEASIBLE = result_pb2.TERMINATION_REASON_INFEASIBLE + UNBOUNDED = result_pb2.TERMINATION_REASON_UNBOUNDED + INFEASIBLE_OR_UNBOUNDED = ( + result_pb2.TERMINATION_REASON_INFEASIBLE_OR_UNBOUNDED + ) + IMPRECISE = result_pb2.TERMINATION_REASON_IMPRECISE + FEASIBLE = result_pb2.TERMINATION_REASON_FEASIBLE + NO_SOLUTION_FOUND = result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND + NUMERICAL_ERROR = result_pb2.TERMINATION_REASON_NUMERICAL_ERROR + OTHER_ERROR = result_pb2.TERMINATION_REASON_OTHER_ERROR @enum.unique class Limit(enum.Enum): - """The optimizer reached a limit, partial solution information may be present. - - Values are: - * UNDETERMINED: The underlying solver does not expose which limit was - reached. - * ITERATION: An iterative algorithm stopped after conducting the - maximum number of iterations (e.g. simplex or barrier iterations). - * TIME: The algorithm stopped after a user-specified amount of - computation time. - * NODE: A branch-and-bound algorithm stopped because it explored a - maximum number of nodes in the branch-and-bound tree. - * SOLUTION: The algorithm stopped because it found the required - number of solutions. This is often used in MIPs to get the solver to - return the first feasible solution it encounters. - * MEMORY: The algorithm stopped because it ran out of memory. - * OBJECTIVE: The algorithm stopped because it found a solution better - than a minimum limit set by the user. - * NORM: The algorithm stopped because the norm of an iterate became - too large. - * INTERRUPTED: The algorithm stopped because of an interrupt signal or a - user interrupt request. - * SLOW_PROGRESS: The algorithm stopped because it was unable to continue - making progress towards the solution. - * OTHER: The algorithm stopped due to a limit not covered by one of the - above. Note that UNDETERMINED is used when the reason cannot be - determined, and OTHER is used when the reason is known but does not fit - into any of the above alternatives. - """ - - UNDETERMINED = result_pb2.LIMIT_UNDETERMINED - ITERATION = result_pb2.LIMIT_ITERATION - TIME = result_pb2.LIMIT_TIME - NODE = result_pb2.LIMIT_NODE - SOLUTION = result_pb2.LIMIT_SOLUTION - MEMORY = result_pb2.LIMIT_MEMORY - OBJECTIVE = result_pb2.LIMIT_OBJECTIVE - NORM = result_pb2.LIMIT_NORM - INTERRUPTED = result_pb2.LIMIT_INTERRUPTED - SLOW_PROGRESS = result_pb2.LIMIT_SLOW_PROGRESS - OTHER = result_pb2.LIMIT_OTHER + """The optimizer reached a limit, partial solution information may be present. + + Values are: + * UNDETERMINED: The underlying solver does not expose which limit was + reached. + * ITERATION: An iterative algorithm stopped after conducting the + maximum number of iterations (e.g. simplex or barrier iterations). + * TIME: The algorithm stopped after a user-specified amount of + computation time. + * NODE: A branch-and-bound algorithm stopped because it explored a + maximum number of nodes in the branch-and-bound tree. + * SOLUTION: The algorithm stopped because it found the required + number of solutions. This is often used in MIPs to get the solver to + return the first feasible solution it encounters. + * MEMORY: The algorithm stopped because it ran out of memory. + * OBJECTIVE: The algorithm stopped because it found a solution better + than a minimum limit set by the user. + * NORM: The algorithm stopped because the norm of an iterate became + too large. + * INTERRUPTED: The algorithm stopped because of an interrupt signal or a + user interrupt request. + * SLOW_PROGRESS: The algorithm stopped because it was unable to continue + making progress towards the solution. + * OTHER: The algorithm stopped due to a limit not covered by one of the + above. Note that UNDETERMINED is used when the reason cannot be + determined, and OTHER is used when the reason is known but does not fit + into any of the above alternatives. + """ + + UNDETERMINED = result_pb2.LIMIT_UNDETERMINED + ITERATION = result_pb2.LIMIT_ITERATION + TIME = result_pb2.LIMIT_TIME + NODE = result_pb2.LIMIT_NODE + SOLUTION = result_pb2.LIMIT_SOLUTION + MEMORY = result_pb2.LIMIT_MEMORY + OBJECTIVE = result_pb2.LIMIT_OBJECTIVE + NORM = result_pb2.LIMIT_NORM + INTERRUPTED = result_pb2.LIMIT_INTERRUPTED + SLOW_PROGRESS = result_pb2.LIMIT_SLOW_PROGRESS + OTHER = result_pb2.LIMIT_OTHER @dataclasses.dataclass class Termination: - """An explanation of why the solver stopped. - - Attributes: - reason: Why the solver stopped, e.g. it found a provably optimal solution. - Additional information in `limit` when value is FEASIBLE or - NO_SOLUTION_FOUND, see `limit` for details. - limit: If the solver stopped early, what caused it to stop. Have value - UNSPECIFIED when reason is not NO_SOLUTION_FOUND or FEASIBLE. May still be - UNSPECIFIED when reason is NO_SOLUTION_FOUND or FEASIBLE, some solvers - cannot fill this in. - detail: Additional, information beyond reason about why the solver stopped, - typically solver specific. - problem_status: Feasibility statuses for primal and dual problems. - objective_bounds: Bounds on the optimal objective value. - """ + """An explanation of why the solver stopped. - reason: TerminationReason = TerminationReason.OPTIMAL - limit: Optional[Limit] = None - detail: str = "" - problem_status: ProblemStatus = ProblemStatus() - objective_bounds: ObjectiveBounds = ObjectiveBounds() - - def to_proto(self) -> result_pb2.TerminationProto: - """Returns an equivalent protocol buffer to this Termination.""" - return result_pb2.TerminationProto( - reason=self.reason.value, - limit=( - result_pb2.LIMIT_UNSPECIFIED if self.limit is None else self.limit.value - ), - detail=self.detail, - problem_status=self.problem_status.to_proto(), - objective_bounds=self.objective_bounds.to_proto(), - ) + Attributes: + reason: Why the solver stopped, e.g. it found a provably optimal solution. + Additional information in `limit` when value is FEASIBLE or + NO_SOLUTION_FOUND, see `limit` for details. + limit: If the solver stopped early, what caused it to stop. Have value + UNSPECIFIED when reason is not NO_SOLUTION_FOUND or FEASIBLE. May still be + UNSPECIFIED when reason is NO_SOLUTION_FOUND or FEASIBLE, some solvers + cannot fill this in. + detail: Additional, information beyond reason about why the solver stopped, + typically solver specific. + problem_status: Feasibility statuses for primal and dual problems. + objective_bounds: Bounds on the optimal objective value. + """ + + reason: TerminationReason = TerminationReason.OPTIMAL + limit: Optional[Limit] = None + detail: str = "" + problem_status: ProblemStatus = ProblemStatus() + objective_bounds: ObjectiveBounds = ObjectiveBounds() + + def to_proto(self) -> result_pb2.TerminationProto: + """Returns an equivalent protocol buffer to this Termination.""" + return result_pb2.TerminationProto( + reason=self.reason.value, + limit=( + result_pb2.LIMIT_UNSPECIFIED + if self.limit is None + else self.limit.value + ), + detail=self.detail, + problem_status=self.problem_status.to_proto(), + objective_bounds=self.objective_bounds.to_proto(), + ) def parse_termination( termination_proto: result_pb2.TerminationProto, ) -> Termination: - """Returns a Termination that is equivalent to termination_proto.""" - reason_proto = termination_proto.reason - limit_proto = termination_proto.limit - if reason_proto == result_pb2.TERMINATION_REASON_UNSPECIFIED: - raise ValueError("Termination reason should not be UNSPECIFIED") - reason_is_limit = ( - reason_proto == result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND - ) or (reason_proto == result_pb2.TERMINATION_REASON_FEASIBLE) - limit_set = limit_proto != result_pb2.LIMIT_UNSPECIFIED - if reason_is_limit != limit_set: - raise ValueError( - f"Termination limit (={limit_proto})) should take value other than " - f"UNSPECIFIED if and only if termination reason (={reason_proto}) is " - "FEASIBLE or NO_SOLUTION_FOUND" - ) - termination = Termination() - termination.reason = TerminationReason(reason_proto) - termination.limit = Limit(limit_proto) if limit_set else None - termination.detail = termination_proto.detail - termination.problem_status = parse_problem_status(termination_proto.problem_status) - termination.objective_bounds = parse_objective_bounds( - termination_proto.objective_bounds + """Returns a Termination that is equivalent to termination_proto.""" + reason_proto = termination_proto.reason + limit_proto = termination_proto.limit + if reason_proto == result_pb2.TERMINATION_REASON_UNSPECIFIED: + raise ValueError("Termination reason should not be UNSPECIFIED") + reason_is_limit = ( + reason_proto == result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND + ) or (reason_proto == result_pb2.TERMINATION_REASON_FEASIBLE) + limit_set = limit_proto != result_pb2.LIMIT_UNSPECIFIED + if reason_is_limit != limit_set: + raise ValueError( + f"Termination limit (={limit_proto})) should take value other than " + f"UNSPECIFIED if and only if termination reason (={reason_proto}) is " + "FEASIBLE or NO_SOLUTION_FOUND" ) - return termination + termination = Termination() + termination.reason = TerminationReason(reason_proto) + termination.limit = Limit(limit_proto) if limit_set else None + termination.detail = termination_proto.detail + termination.problem_status = parse_problem_status( + termination_proto.problem_status + ) + termination.objective_bounds = parse_objective_bounds( + termination_proto.objective_bounds + ) + return termination @dataclasses.dataclass class SolveResult: - """The result of solving an optimization problem defined by a Model. - - We attempt to return as much solution information (primal_solutions, - primal_rays, dual_solutions, dual_rays) as each underlying solver will provide - given its return status. Differences in the underlying solvers result in a - weak contract on what fields will be populated for a given termination - reason. This is discussed in detail in termination_reasons.md, and the most - important points are summarized below: - * When the termination reason is optimal, there will be at least one primal - solution provided that will be feasible up to the underlying solver's - tolerances. - * Dual solutions are only given for convex optimization problems (e.g. - linear programs, not integer programs). - * A basis is only given for linear programs when solved by the simplex - method (e.g., not with PDLP). - * Solvers have widely varying support for returning primal and dual rays. - E.g. a termination_reason of unbounded does not ensure that a feasible - solution or a primal ray is returned, check termination_reasons.md for - solver specific guarantees if this is needed. Further, many solvers will - provide the ray but not the feasible solution when returning an unbounded - status. - * When the termination reason is that a limit was reached or that the result - is imprecise, a solution may or may not be present. Further, for some - solvers (generally, convex optimization solvers, not MIP solvers), the - primal or dual solution may not be feasible. - - Solver specific output is also returned for some solvers (and only information - for the solver used will be populated). - - Attributes: - termination: The reason the solver stopped. - solve_stats: Statistics on the solve process, e.g. running time, iterations. - solutions: Lexicographically by primal feasibility status, dual feasibility - status, (basic dual feasibility for simplex solvers), primal objective - value and dual objective value. - primal_rays: Directions of unbounded primal improvement, or equivalently, - dual infeasibility certificates. Typically provided for terminal reasons - UNBOUNDED and DUAL_INFEASIBLE. - dual_rays: Directions of unbounded dual improvement, or equivalently, primal - infeasibility certificates. Typically provided for termination reason - INFEASIBLE. - gscip_specific_output: statistics returned by the gSCIP solver, if used. - osqp_specific_output: statistics returned by the OSQP solver, if used. - pdlp_specific_output: statistics returned by the PDLP solver, if used. + """The result of solving an optimization problem defined by a Model. + + We attempt to return as much solution information (primal_solutions, + primal_rays, dual_solutions, dual_rays) as each underlying solver will provide + given its return status. Differences in the underlying solvers result in a + weak contract on what fields will be populated for a given termination + reason. This is discussed in detail in termination_reasons.md, and the most + important points are summarized below: + * When the termination reason is optimal, there will be at least one primal + solution provided that will be feasible up to the underlying solver's + tolerances. + * Dual solutions are only given for convex optimization problems (e.g. + linear programs, not integer programs). + * A basis is only given for linear programs when solved by the simplex + method (e.g., not with PDLP). + * Solvers have widely varying support for returning primal and dual rays. + E.g. a termination_reason of unbounded does not ensure that a feasible + solution or a primal ray is returned, check termination_reasons.md for + solver specific guarantees if this is needed. Further, many solvers will + provide the ray but not the feasible solution when returning an unbounded + status. + * When the termination reason is that a limit was reached or that the result + is imprecise, a solution may or may not be present. Further, for some + solvers (generally, convex optimization solvers, not MIP solvers), the + primal or dual solution may not be feasible. + + Solver specific output is also returned for some solvers (and only information + for the solver used will be populated). + + Attributes: + termination: The reason the solver stopped. + solve_stats: Statistics on the solve process, e.g. running time, iterations. + solutions: Lexicographically by primal feasibility status, dual feasibility + status, (basic dual feasibility for simplex solvers), primal objective + value and dual objective value. + primal_rays: Directions of unbounded primal improvement, or equivalently, + dual infeasibility certificates. Typically provided for terminal reasons + UNBOUNDED and DUAL_INFEASIBLE. + dual_rays: Directions of unbounded dual improvement, or equivalently, primal + infeasibility certificates. Typically provided for termination reason + INFEASIBLE. + gscip_specific_output: statistics returned by the gSCIP solver, if used. + osqp_specific_output: statistics returned by the OSQP solver, if used. + pdlp_specific_output: statistics returned by the PDLP solver, if used. + """ + + termination: Termination = dataclasses.field(default_factory=Termination) + solve_stats: SolveStats = dataclasses.field(default_factory=SolveStats) + solutions: List[solution.Solution] = dataclasses.field(default_factory=list) + primal_rays: List[solution.PrimalRay] = dataclasses.field( + default_factory=list + ) + dual_rays: List[solution.DualRay] = dataclasses.field(default_factory=list) + # At most one of the below will be set + gscip_specific_output: Optional[gscip_pb2.GScipOutput] = None + osqp_specific_output: Optional[osqp_pb2.OsqpOutput] = None + pdlp_specific_output: Optional[result_pb2.SolveResultProto.PdlpOutput] = None + + def solve_time(self) -> datetime.timedelta: + """Shortcut for SolveResult.solve_stats.solve_time.""" + return self.solve_stats.solve_time + + def primal_bound(self) -> float: + """Returns a primal bound on the optimal objective value as described in ObjectiveBounds. + + Will return a valid (possibly infinite) bound even if no primal feasible + solutions are available. """ + return self.termination.objective_bounds.primal_bound - termination: Termination = dataclasses.field(default_factory=Termination) - solve_stats: SolveStats = dataclasses.field(default_factory=SolveStats) - solutions: List[solution.Solution] = dataclasses.field(default_factory=list) - primal_rays: List[solution.PrimalRay] = dataclasses.field(default_factory=list) - dual_rays: List[solution.DualRay] = dataclasses.field(default_factory=list) - # At most one of the below will be set - gscip_specific_output: Optional[gscip_pb2.GScipOutput] = None - osqp_specific_output: Optional[osqp_pb2.OsqpOutput] = None - pdlp_specific_output: Optional[result_pb2.SolveResultProto.PdlpOutput] = None - - def solve_time(self) -> datetime.timedelta: - """Shortcut for SolveResult.solve_stats.solve_time.""" - return self.solve_stats.solve_time - - def primal_bound(self) -> float: - """Returns a primal bound on the optimal objective value as described in ObjectiveBounds. - - Will return a valid (possibly infinite) bound even if no primal feasible - solutions are available. - """ - return self.termination.objective_bounds.primal_bound - - def dual_bound(self) -> float: - """Returns a dual bound on the optimal objective value as described in ObjectiveBounds. - - Will return a valid (possibly infinite) bound even if no dual feasible - solutions are available. - """ - return self.termination.objective_bounds.dual_bound - - def has_primal_feasible_solution(self) -> bool: - """Indicates if at least one primal feasible solution is available. - - When termination.reason is TerminationReason.OPTIMAL or - TerminationReason.FEASIBLE, this is guaranteed to be true and need not be - checked. - - Returns: - True if there is at least one primal feasible solution is available, - False, otherwise. - """ - if not self.solutions: - return False - sol = self.solutions[0] - return ( - sol.primal_solution is not None - and sol.primal_solution.feasibility_status - == solution.SolutionStatus.FEASIBLE - ) + def dual_bound(self) -> float: + """Returns a dual bound on the optimal objective value as described in ObjectiveBounds. - def objective_value(self) -> float: - """Returns the objective value of the best primal feasible solution. - - An error will be raised if there are no primal feasible solutions. - primal_bound() above is guaranteed to be at least as good (larger or equal - for max problems and smaller or equal for min problems) as objective_value() - and will never raise an error, so it may be preferable in some cases. Note - that primal_bound() could be better than objective_value() even for optimal - terminations, but on such optimal termination, both should satisfy the - optimality tolerances. - - Returns: - The objective value of the best primal feasible solution. - - Raises: - ValueError: There are no primal feasible solutions. - """ - if not self.has_primal_feasible_solution(): - raise ValueError("No primal feasible solution available.") - sol = self.solutions[0] - assert sol.primal_solution is not None - return sol.primal_solution.objective_value - - def best_objective_bound(self) -> float: - """Returns a bound on the best possible objective value. - - best_objective_bound() is always equal to dual_bound(), so they can be - used interchangeably. - """ - return self.termination.objective_bounds.dual_bound - - @overload - def variable_values( - self, variables: None = ... - ) -> Dict[variables_mod.Variable, float]: ... - - @overload - def variable_values(self, variables: variables_mod.Variable) -> float: ... - - @overload - def variable_values( - self, variables: Iterable[variables_mod.Variable] - ) -> List[float]: ... - - def variable_values(self, variables=None): - """The variable values from the best primal feasible solution. - - An error will be raised if there are no primal feasible solutions. - - Args: - variables: an optional Variable or iterator of Variables indicating what - variable values to return. If not provided, variable_values returns a - dictionary with all the variable values for all variables. - - Returns: - The variable values from the best primal feasible solution. - - Raises: - ValueError: There are no primal feasible solutions. - TypeError: Argument is not None, a Variable or an iterable of Variables. - KeyError: Variable values requested for an invalid variable (e.g. is not a - Variable or is a variable for another model). - """ - if not self.has_primal_feasible_solution(): - raise ValueError("No primal feasible solution available.") - sol = self.solutions[0] - assert sol.primal_solution is not None - if variables is None: - return sol.primal_solution.variable_values - if isinstance(variables, variables_mod.Variable): - return sol.primal_solution.variable_values[variables] - if isinstance(variables, Iterable): - return [sol.primal_solution.variable_values[v] for v in variables] - raise TypeError( - "unsupported type in argument for " - f"variable_values: {type(variables).__name__!r}" - ) + Will return a valid (possibly infinite) bound even if no dual feasible + solutions are available. + """ + return self.termination.objective_bounds.dual_bound - def bounded(self) -> bool: - """Returns true only if the problem has been shown to be feasible and bounded.""" - return ( - self.termination.problem_status.primal_status == FeasibilityStatus.FEASIBLE - and self.termination.problem_status.dual_status - == FeasibilityStatus.FEASIBLE - ) + def has_primal_feasible_solution(self) -> bool: + """Indicates if at least one primal feasible solution is available. - def has_ray(self) -> bool: - """Indicates if at least one primal ray is available. - - This is NOT guaranteed to be true when termination.reason is - TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded. - - Returns: - True if at least one primal ray is available. - """ - return bool(self.primal_rays) - - @overload - def ray_variable_values( - self, variables: None = ... - ) -> Dict[variables_mod.Variable, float]: ... - - @overload - def ray_variable_values(self, variables: variables_mod.Variable) -> float: ... - - @overload - def ray_variable_values( - self, variables: Iterable[variables_mod.Variable] - ) -> List[float]: ... - - def ray_variable_values(self, variables=None): - """The variable values from the first primal ray. - - An error will be raised if there are no primal rays. - - Args: - variables: an optional Variable or iterator of Variables indicating what - variable values to return. If not provided, variable_values() returns a - dictionary with the variable values for all variables. - - Returns: - The variable values from the first primal ray. - - Raises: - ValueError: There are no primal rays. - TypeError: Argument is not None, a Variable or an iterable of Variables. - KeyError: Variable values requested for an invalid variable (e.g. is not a - Variable or is a variable for another model). - """ - if not self.has_ray(): - raise ValueError("No primal ray available.") - if variables is None: - return self.primal_rays[0].variable_values - if isinstance(variables, variables_mod.Variable): - return self.primal_rays[0].variable_values[variables] - if isinstance(variables, Iterable): - return [self.primal_rays[0].variable_values[v] for v in variables] - raise TypeError( - "unsupported type in argument for " - f"ray_variable_values: {type(variables).__name__!r}" - ) + When termination.reason is TerminationReason.OPTIMAL or + TerminationReason.FEASIBLE, this is guaranteed to be true and need not be + checked. - def has_dual_feasible_solution(self) -> bool: - """Indicates if the best solution has an associated dual feasible solution. - - This is NOT guaranteed to be true when termination.reason is - TerminationReason.Optimal. It also may be true even when the best solution - does not have an associated primal feasible solution. - - Returns: - True if the best solution has an associated dual feasible solution. - """ - if not self.solutions: - return False - sol = self.solutions[0] - return ( - sol.dual_solution is not None - and sol.dual_solution.feasibility_status == solution.SolutionStatus.FEASIBLE - ) + Returns: + True if there is at least one primal feasible solution is available, + False, otherwise. + """ + if not self.solutions: + return False + sol = self.solutions[0] + return ( + sol.primal_solution is not None + and sol.primal_solution.feasibility_status + == solution.SolutionStatus.FEASIBLE + ) + + def objective_value(self) -> float: + """Returns the objective value of the best primal feasible solution. + + An error will be raised if there are no primal feasible solutions. + primal_bound() above is guaranteed to be at least as good (larger or equal + for max problems and smaller or equal for min problems) as objective_value() + and will never raise an error, so it may be preferable in some cases. Note + that primal_bound() could be better than objective_value() even for optimal + terminations, but on such optimal termination, both should satisfy the + optimality tolerances. + + Returns: + The objective value of the best primal feasible solution. + + Raises: + ValueError: There are no primal feasible solutions. + """ + if not self.has_primal_feasible_solution(): + raise ValueError("No primal feasible solution available.") + sol = self.solutions[0] + assert sol.primal_solution is not None + return sol.primal_solution.objective_value + + def best_objective_bound(self) -> float: + """Returns a bound on the best possible objective value. + + best_objective_bound() is always equal to dual_bound(), so they can be + used interchangeably. + """ + return self.termination.objective_bounds.dual_bound + + @overload + def variable_values( + self, variables: None = ... + ) -> Dict[variables_mod.Variable, float]: + ... + + @overload + def variable_values(self, variables: variables_mod.Variable) -> float: + ... + + @overload + def variable_values( + self, variables: Iterable[variables_mod.Variable] + ) -> List[float]: + ... + + def variable_values(self, variables=None): + """The variable values from the best primal feasible solution. + + An error will be raised if there are no primal feasible solutions. + + Args: + variables: an optional Variable or iterator of Variables indicating what + variable values to return. If not provided, variable_values returns a + dictionary with all the variable values for all variables. + + Returns: + The variable values from the best primal feasible solution. + + Raises: + ValueError: There are no primal feasible solutions. + TypeError: Argument is not None, a Variable or an iterable of Variables. + KeyError: Variable values requested for an invalid variable (e.g. is not a + Variable or is a variable for another model). + """ + if not self.has_primal_feasible_solution(): + raise ValueError("No primal feasible solution available.") + sol = self.solutions[0] + assert sol.primal_solution is not None + if variables is None: + return sol.primal_solution.variable_values + if isinstance(variables, variables_mod.Variable): + return sol.primal_solution.variable_values[variables] + if isinstance(variables, Iterable): + return [sol.primal_solution.variable_values[v] for v in variables] + raise TypeError( + "unsupported type in argument for " + f"variable_values: {type(variables).__name__!r}" + ) + + def bounded(self) -> bool: + """Returns true only if the problem has been shown to be feasible and bounded.""" + return ( + self.termination.problem_status.primal_status + == FeasibilityStatus.FEASIBLE + and self.termination.problem_status.dual_status + == FeasibilityStatus.FEASIBLE + ) + + def has_ray(self) -> bool: + """Indicates if at least one primal ray is available. + + This is NOT guaranteed to be true when termination.reason is + TerminationReason.kUnbounded or TerminationReason.kInfeasibleOrUnbounded. - @overload - def dual_values( - self, linear_constraints: None = ... - ) -> Dict[linear_constraints_mod.LinearConstraint, float]: ... + Returns: + True if at least one primal ray is available. + """ + return bool(self.primal_rays) + + @overload + def ray_variable_values( + self, variables: None = ... + ) -> Dict[variables_mod.Variable, float]: + ... + + @overload + def ray_variable_values(self, variables: variables_mod.Variable) -> float: + ... + + @overload + def ray_variable_values( + self, variables: Iterable[variables_mod.Variable] + ) -> List[float]: + ... + + def ray_variable_values(self, variables=None): + """The variable values from the first primal ray. + + An error will be raised if there are no primal rays. + + Args: + variables: an optional Variable or iterator of Variables indicating what + variable values to return. If not provided, variable_values() returns a + dictionary with the variable values for all variables. + + Returns: + The variable values from the first primal ray. + + Raises: + ValueError: There are no primal rays. + TypeError: Argument is not None, a Variable or an iterable of Variables. + KeyError: Variable values requested for an invalid variable (e.g. is not a + Variable or is a variable for another model). + """ + if not self.has_ray(): + raise ValueError("No primal ray available.") + if variables is None: + return self.primal_rays[0].variable_values + if isinstance(variables, variables_mod.Variable): + return self.primal_rays[0].variable_values[variables] + if isinstance(variables, Iterable): + return [self.primal_rays[0].variable_values[v] for v in variables] + raise TypeError( + "unsupported type in argument for " + f"ray_variable_values: {type(variables).__name__!r}" + ) - @overload - def dual_values( - self, linear_constraints: linear_constraints_mod.LinearConstraint - ) -> float: ... + def has_dual_feasible_solution(self) -> bool: + """Indicates if the best solution has an associated dual feasible solution. - @overload - def dual_values( - self, - linear_constraints: Iterable[linear_constraints_mod.LinearConstraint], - ) -> List[float]: ... + This is NOT guaranteed to be true when termination.reason is + TerminationReason.Optimal. It also may be true even when the best solution + does not have an associated primal feasible solution. - def dual_values(self, linear_constraints=None): - """The dual values associated to the best solution. + Returns: + True if the best solution has an associated dual feasible solution. + """ + if not self.solutions: + return False + sol = self.solutions[0] + return ( + sol.dual_solution is not None + and sol.dual_solution.feasibility_status + == solution.SolutionStatus.FEASIBLE + ) - If there is at least one primal feasible solution, this corresponds to the - dual values associated to the best primal feasible solution. An error will - be raised if the best solution does not have an associated dual feasible + @overload + def dual_values( + self, linear_constraints: None = ... + ) -> Dict[linear_constraints_mod.LinearConstraint, float]: + ... + + @overload + def dual_values( + self, linear_constraints: linear_constraints_mod.LinearConstraint + ) -> float: + ... + + @overload + def dual_values( + self, + linear_constraints: Iterable[linear_constraints_mod.LinearConstraint], + ) -> List[float]: + ... + + def dual_values(self, linear_constraints=None): + """The dual values associated to the best solution. + + If there is at least one primal feasible solution, this corresponds to the + dual values associated to the best primal feasible solution. An error will + be raised if the best solution does not have an associated dual feasible + solution. + + Args: + linear_constraints: an optional LinearConstraint or iterator of + LinearConstraint indicating what dual values to return. If not provided, + dual_values() returns a dictionary with the dual values for all linear + constraints. + + Returns: + The dual values associated to the best solution. + + Raises: + ValueError: The best solution does not have an associated dual feasible solution. + TypeError: Argument is not None, a LinearConstraint or an iterable of + LinearConstraint. + KeyError: LinearConstraint values requested for an invalid + linear constraint (e.g. is not a LinearConstraint or is a linear + constraint for another model). + """ + if not self.has_dual_feasible_solution(): + raise ValueError(_NO_DUAL_SOLUTION_ERROR) + sol = self.solutions[0] + assert sol.dual_solution is not None + if linear_constraints is None: + return sol.dual_solution.dual_values + if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint): + return sol.dual_solution.dual_values[linear_constraints] + if isinstance(linear_constraints, Iterable): + return [sol.dual_solution.dual_values[c] for c in linear_constraints] + raise TypeError( + "unsupported type in argument for " + f"dual_values: {type(linear_constraints).__name__!r}" + ) - Args: - linear_constraints: an optional LinearConstraint or iterator of - LinearConstraint indicating what dual values to return. If not provided, - dual_values() returns a dictionary with the dual values for all linear - constraints. - - Returns: - The dual values associated to the best solution. - - Raises: - ValueError: The best solution does not have an associated dual feasible - solution. - TypeError: Argument is not None, a LinearConstraint or an iterable of - LinearConstraint. - KeyError: LinearConstraint values requested for an invalid - linear constraint (e.g. is not a LinearConstraint or is a linear - constraint for another model). - """ - if not self.has_dual_feasible_solution(): - raise ValueError(_NO_DUAL_SOLUTION_ERROR) - sol = self.solutions[0] - assert sol.dual_solution is not None - if linear_constraints is None: - return sol.dual_solution.dual_values - if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint): - return sol.dual_solution.dual_values[linear_constraints] - if isinstance(linear_constraints, Iterable): - return [sol.dual_solution.dual_values[c] for c in linear_constraints] - raise TypeError( - "unsupported type in argument for " - f"dual_values: {type(linear_constraints).__name__!r}" - ) + @overload + def reduced_costs( + self, variables: None = ... + ) -> Dict[variables_mod.Variable, float]: + ... + + @overload + def reduced_costs(self, variables: variables_mod.Variable) -> float: + ... + + @overload + def reduced_costs( + self, variables: Iterable[variables_mod.Variable] + ) -> List[float]: + ... - @overload - def reduced_costs( - self, variables: None = ... - ) -> Dict[variables_mod.Variable, float]: ... + def reduced_costs(self, variables=None): + """The reduced costs associated to the best solution. - @overload - def reduced_costs(self, variables: variables_mod.Variable) -> float: ... + If there is at least one primal feasible solution, this corresponds to the + reduced costs associated to the best primal feasible solution. An error will + be raised if the best solution does not have an associated dual feasible + solution. - @overload - def reduced_costs( - self, variables: Iterable[variables_mod.Variable] - ) -> List[float]: ... + Args: + variables: an optional Variable or iterator of Variables indicating what + reduced costs to return. If not provided, reduced_costs() returns a + dictionary with the reduced costs for all variables. - def reduced_costs(self, variables=None): - """The reduced costs associated to the best solution. + Returns: + The reduced costs associated to the best solution. - If there is at least one primal feasible solution, this corresponds to the - reduced costs associated to the best primal feasible solution. An error will - be raised if the best solution does not have an associated dual feasible + Raises: + ValueError: The best solution does not have an associated dual feasible solution. + TypeError: Argument is not None, a Variable or an iterable of Variables. + KeyError: Variable values requested for an invalid variable (e.g. is not a + Variable or is a variable for another model). + """ + if not self.has_dual_feasible_solution(): + raise ValueError(_NO_DUAL_SOLUTION_ERROR) + sol = self.solutions[0] + assert sol.dual_solution is not None + if variables is None: + return sol.dual_solution.reduced_costs + if isinstance(variables, variables_mod.Variable): + return sol.dual_solution.reduced_costs[variables] + if isinstance(variables, Iterable): + return [sol.dual_solution.reduced_costs[v] for v in variables] + raise TypeError( + "unsupported type in argument for " + f"reduced_costs: {type(variables).__name__!r}" + ) - Args: - variables: an optional Variable or iterator of Variables indicating what - reduced costs to return. If not provided, reduced_costs() returns a - dictionary with the reduced costs for all variables. - - Returns: - The reduced costs associated to the best solution. - - Raises: - ValueError: The best solution does not have an associated dual feasible - solution. - TypeError: Argument is not None, a Variable or an iterable of Variables. - KeyError: Variable values requested for an invalid variable (e.g. is not a - Variable or is a variable for another model). - """ - if not self.has_dual_feasible_solution(): - raise ValueError(_NO_DUAL_SOLUTION_ERROR) - sol = self.solutions[0] - assert sol.dual_solution is not None - if variables is None: - return sol.dual_solution.reduced_costs - if isinstance(variables, variables_mod.Variable): - return sol.dual_solution.reduced_costs[variables] - if isinstance(variables, Iterable): - return [sol.dual_solution.reduced_costs[v] for v in variables] - raise TypeError( - "unsupported type in argument for " - f"reduced_costs: {type(variables).__name__!r}" - ) + def has_dual_ray(self) -> bool: + """Indicates if at least one dual ray is available. - def has_dual_ray(self) -> bool: - """Indicates if at least one dual ray is available. - - This is NOT guaranteed to be true when termination.reason is - TerminationReason.Infeasible. - - Returns: - True if at least one dual ray is available. - """ - return bool(self.dual_rays) - - @overload - def ray_dual_values( - self, linear_constraints: None = ... - ) -> Dict[linear_constraints_mod.LinearConstraint, float]: ... - - @overload - def ray_dual_values( - self, linear_constraints: linear_constraints_mod.LinearConstraint - ) -> float: ... - - @overload - def ray_dual_values( - self, - linear_constraints: Iterable[linear_constraints_mod.LinearConstraint], - ) -> List[float]: ... - - def ray_dual_values(self, linear_constraints=None): - """The dual values from the first dual ray. - - An error will be raised if there are no dual rays. - - Args: - linear_constraints: an optional LinearConstraint or iterator of - LinearConstraint indicating what dual values to return. If not provided, - ray_dual_values() returns a dictionary with the dual values for all - linear constraints. - - Returns: - The dual values from the first dual ray. - - Raises: - ValueError: There are no dual rays. - TypeError: Argument is not None, a LinearConstraint or an iterable of - LinearConstraint. - KeyError: LinearConstraint values requested for an invalid - linear constraint (e.g. is not a LinearConstraint or is a linear - constraint for another model). - """ - if not self.has_dual_ray(): - raise ValueError("No dual ray available.") - ray = self.dual_rays[0] - if linear_constraints is None: - return ray.dual_values - if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint): - return ray.dual_values[linear_constraints] - if isinstance(linear_constraints, Iterable): - return [ray.dual_values[v] for v in linear_constraints] - raise TypeError( - "unsupported type in argument for " - f"ray_dual_values: {type(linear_constraints).__name__!r}" - ) + This is NOT guaranteed to be true when termination.reason is + TerminationReason.Infeasible. - @overload - def ray_reduced_costs( - self, variables: None = ... - ) -> Dict[variables_mod.Variable, float]: ... - - @overload - def ray_reduced_costs(self, variables: variables_mod.Variable) -> float: ... - - @overload - def ray_reduced_costs( - self, variables: Iterable[variables_mod.Variable] - ) -> List[float]: ... - - def ray_reduced_costs(self, variables=None): - """The reduced costs from the first dual ray. - - An error will be raised if there are no dual rays. - - Args: - variables: an optional Variable or iterator of Variables indicating what - reduced costs to return. If not provided, ray_reduced_costs() returns a - dictionary with the reduced costs for all variables. - - Returns: - The reduced costs from the first dual ray. - - Raises: - ValueError: There are no dual rays. - TypeError: Argument is not None, a Variable or an iterable of Variables. - KeyError: Variable values requested for an invalid variable (e.g. is not a - Variable or is a variable for another model). - """ - if not self.has_dual_ray(): - raise ValueError("No dual ray available.") - ray = self.dual_rays[0] - if variables is None: - return ray.reduced_costs - if isinstance(variables, variables_mod.Variable): - return ray.reduced_costs[variables] - if isinstance(variables, Iterable): - return [ray.reduced_costs[v] for v in variables] - raise TypeError( - "unsupported type in argument for " - f"ray_reduced_costs: {type(variables).__name__!r}" - ) + Returns: + True if at least one dual ray is available. + """ + return bool(self.dual_rays) + + @overload + def ray_dual_values( + self, linear_constraints: None = ... + ) -> Dict[linear_constraints_mod.LinearConstraint, float]: + ... + + @overload + def ray_dual_values( + self, linear_constraints: linear_constraints_mod.LinearConstraint + ) -> float: + ... + + @overload + def ray_dual_values( + self, + linear_constraints: Iterable[linear_constraints_mod.LinearConstraint], + ) -> List[float]: + ... + + def ray_dual_values(self, linear_constraints=None): + """The dual values from the first dual ray. + + An error will be raised if there are no dual rays. + + Args: + linear_constraints: an optional LinearConstraint or iterator of + LinearConstraint indicating what dual values to return. If not provided, + ray_dual_values() returns a dictionary with the dual values for all + linear constraints. + + Returns: + The dual values from the first dual ray. + + Raises: + ValueError: There are no dual rays. + TypeError: Argument is not None, a LinearConstraint or an iterable of + LinearConstraint. + KeyError: LinearConstraint values requested for an invalid + linear constraint (e.g. is not a LinearConstraint or is a linear + constraint for another model). + """ + if not self.has_dual_ray(): + raise ValueError("No dual ray available.") + ray = self.dual_rays[0] + if linear_constraints is None: + return ray.dual_values + if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint): + return ray.dual_values[linear_constraints] + if isinstance(linear_constraints, Iterable): + return [ray.dual_values[v] for v in linear_constraints] + raise TypeError( + "unsupported type in argument for " + f"ray_dual_values: {type(linear_constraints).__name__!r}" + ) - def has_basis(self) -> bool: - """Indicates if the best solution has an associated basis. - - This is NOT guaranteed to be true when termination.reason is - TerminationReason.Optimal. It also may be true even when the best solution - does not have an associated primal feasible solution. - - Returns: - True if the best solution has an associated basis. - """ - if not self.solutions: - return False - return self.solutions[0].basis is not None - - @overload - def constraint_status( - self, linear_constraints: None = ... - ) -> Dict[linear_constraints_mod.LinearConstraint, solution.BasisStatus]: ... - - @overload - def constraint_status( - self, linear_constraints: linear_constraints_mod.LinearConstraint - ) -> solution.BasisStatus: ... - - @overload - def constraint_status( - self, - linear_constraints: Iterable[linear_constraints_mod.LinearConstraint], - ) -> List[solution.BasisStatus]: ... - - def constraint_status(self, linear_constraints=None): - """The constraint basis status associated to the best solution. - - If there is at least one primal feasible solution, this corresponds to the - basis associated to the best primal feasible solution. An error will - be raised if the best solution does not have an associated basis. - - - Args: - linear_constraints: an optional LinearConstraint or iterator of - LinearConstraint indicating what constraint statuses to return. If not - provided, returns a dictionary with the constraint statuses for all - linear constraints. - - Returns: - The constraint basis status associated to the best solution. - - Raises: - ValueError: The best solution does not have an associated basis. - TypeError: Argument is not None, a LinearConstraint or an iterable of - LinearConstraint. - KeyError: LinearConstraint values requested for an invalid - linear constraint (e.g. is not a LinearConstraint or is a linear - constraint for another model). - """ - if not self.has_basis(): - raise ValueError(_NO_BASIS_ERROR) - basis = self.solutions[0].basis - assert basis is not None - if linear_constraints is None: - return basis.constraint_status - if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint): - return basis.constraint_status[linear_constraints] - if isinstance(linear_constraints, Iterable): - return [basis.constraint_status[c] for c in linear_constraints] - raise TypeError( - "unsupported type in argument for " - f"constraint_status: {type(linear_constraints).__name__!r}" - ) + @overload + def ray_reduced_costs( + self, variables: None = ... + ) -> Dict[variables_mod.Variable, float]: + ... - @overload - def variable_status( - self, variables: None = ... - ) -> Dict[variables_mod.Variable, solution.BasisStatus]: ... - - @overload - def variable_status( - self, variables: variables_mod.Variable - ) -> solution.BasisStatus: ... - - @overload - def variable_status( - self, variables: Iterable[variables_mod.Variable] - ) -> List[solution.BasisStatus]: ... - - def variable_status(self, variables=None): - """The variable basis status associated to the best solution. - - If there is at least one primal feasible solution, this corresponds to the - basis associated to the best primal feasible solution. An error will - be raised if the best solution does not have an associated basis. - - Args: - variables: an optional Variable or iterator of Variables indicating what - reduced costs to return. If not provided, variable_status() returns a - dictionary with the reduced costs for all variables. - - Returns: - The variable basis status associated to the best solution. - - Raises: - ValueError: The best solution does not have an associated basis. - TypeError: Argument is not None, a Variable or an iterable of Variables. - KeyError: Variable values requested for an invalid variable (e.g. is not a - Variable or is a variable for another model). - """ - if not self.has_basis(): - raise ValueError(_NO_BASIS_ERROR) - basis = self.solutions[0].basis - assert basis is not None - if variables is None: - return basis.variable_status - if isinstance(variables, variables_mod.Variable): - return basis.variable_status[variables] - if isinstance(variables, Iterable): - return [basis.variable_status[v] for v in variables] - raise TypeError( - "unsupported type in argument for " - f"variable_status: {type(variables).__name__!r}" - ) + @overload + def ray_reduced_costs(self, variables: variables_mod.Variable) -> float: + ... + + @overload + def ray_reduced_costs( + self, variables: Iterable[variables_mod.Variable] + ) -> List[float]: + ... + + def ray_reduced_costs(self, variables=None): + """The reduced costs from the first dual ray. + + An error will be raised if there are no dual rays. + + Args: + variables: an optional Variable or iterator of Variables indicating what + reduced costs to return. If not provided, ray_reduced_costs() returns a + dictionary with the reduced costs for all variables. + + Returns: + The reduced costs from the first dual ray. + + Raises: + ValueError: There are no dual rays. + TypeError: Argument is not None, a Variable or an iterable of Variables. + KeyError: Variable values requested for an invalid variable (e.g. is not a + Variable or is a variable for another model). + """ + if not self.has_dual_ray(): + raise ValueError("No dual ray available.") + ray = self.dual_rays[0] + if variables is None: + return ray.reduced_costs + if isinstance(variables, variables_mod.Variable): + return ray.reduced_costs[variables] + if isinstance(variables, Iterable): + return [ray.reduced_costs[v] for v in variables] + raise TypeError( + "unsupported type in argument for " + f"ray_reduced_costs: {type(variables).__name__!r}" + ) + + def has_basis(self) -> bool: + """Indicates if the best solution has an associated basis. + + This is NOT guaranteed to be true when termination.reason is + TerminationReason.Optimal. It also may be true even when the best solution + does not have an associated primal feasible solution. + + Returns: + True if the best solution has an associated basis. + """ + if not self.solutions: + return False + return self.solutions[0].basis is not None + + @overload + def constraint_status( + self, linear_constraints: None = ... + ) -> Dict[linear_constraints_mod.LinearConstraint, solution.BasisStatus]: + ... + + @overload + def constraint_status( + self, linear_constraints: linear_constraints_mod.LinearConstraint + ) -> solution.BasisStatus: + ... + + @overload + def constraint_status( + self, + linear_constraints: Iterable[linear_constraints_mod.LinearConstraint], + ) -> List[solution.BasisStatus]: + ... + + def constraint_status(self, linear_constraints=None): + """The constraint basis status associated to the best solution. + + If there is at least one primal feasible solution, this corresponds to the + basis associated to the best primal feasible solution. An error will + be raised if the best solution does not have an associated basis. + + + Args: + linear_constraints: an optional LinearConstraint or iterator of + LinearConstraint indicating what constraint statuses to return. If not + provided, returns a dictionary with the constraint statuses for all + linear constraints. + + Returns: + The constraint basis status associated to the best solution. + + Raises: + ValueError: The best solution does not have an associated basis. + TypeError: Argument is not None, a LinearConstraint or an iterable of + LinearConstraint. + KeyError: LinearConstraint values requested for an invalid + linear constraint (e.g. is not a LinearConstraint or is a linear + constraint for another model). + """ + if not self.has_basis(): + raise ValueError(_NO_BASIS_ERROR) + basis = self.solutions[0].basis + assert basis is not None + if linear_constraints is None: + return basis.constraint_status + if isinstance(linear_constraints, linear_constraints_mod.LinearConstraint): + return basis.constraint_status[linear_constraints] + if isinstance(linear_constraints, Iterable): + return [basis.constraint_status[c] for c in linear_constraints] + raise TypeError( + "unsupported type in argument for " + f"constraint_status: {type(linear_constraints).__name__!r}" + ) + + @overload + def variable_status( + self, variables: None = ... + ) -> Dict[variables_mod.Variable, solution.BasisStatus]: + ... + + @overload + def variable_status( + self, variables: variables_mod.Variable + ) -> solution.BasisStatus: + ... + + @overload + def variable_status( + self, variables: Iterable[variables_mod.Variable] + ) -> List[solution.BasisStatus]: + ... + + def variable_status(self, variables=None): + """The variable basis status associated to the best solution. + + If there is at least one primal feasible solution, this corresponds to the + basis associated to the best primal feasible solution. An error will + be raised if the best solution does not have an associated basis. + + Args: + variables: an optional Variable or iterator of Variables indicating what + reduced costs to return. If not provided, variable_status() returns a + dictionary with the reduced costs for all variables. + + Returns: + The variable basis status associated to the best solution. + + Raises: + ValueError: The best solution does not have an associated basis. + TypeError: Argument is not None, a Variable or an iterable of Variables. + KeyError: Variable values requested for an invalid variable (e.g. is not a + Variable or is a variable for another model). + """ + if not self.has_basis(): + raise ValueError(_NO_BASIS_ERROR) + basis = self.solutions[0].basis + assert basis is not None + if variables is None: + return basis.variable_status + if isinstance(variables, variables_mod.Variable): + return basis.variable_status[variables] + if isinstance(variables, Iterable): + return [basis.variable_status[v] for v in variables] + raise TypeError( + "unsupported type in argument for " + f"variable_status: {type(variables).__name__!r}" + ) + + def to_proto(self) -> result_pb2.SolveResultProto: + """Returns an equivalent protocol buffer for a SolveResult.""" + proto = result_pb2.SolveResultProto( + termination=self.termination.to_proto(), + solutions=[s.to_proto() for s in self.solutions], + primal_rays=[r.to_proto() for r in self.primal_rays], + dual_rays=[r.to_proto() for r in self.dual_rays], + solve_stats=self.solve_stats.to_proto(), + ) - def to_proto(self) -> result_pb2.SolveResultProto: - """Returns an equivalent protocol buffer for a SolveResult.""" - proto = result_pb2.SolveResultProto( - termination=self.termination.to_proto(), - solutions=[s.to_proto() for s in self.solutions], - primal_rays=[r.to_proto() for r in self.primal_rays], - dual_rays=[r.to_proto() for r in self.dual_rays], - solve_stats=self.solve_stats.to_proto(), + # Ensure that at most solver has solver specific output. + existing_solver_specific_output = None + + def has_solver_specific_output(solver_name: str) -> None: + nonlocal existing_solver_specific_output + if existing_solver_specific_output is not None: + raise ValueError( + "found solver specific output for both" + f" {existing_solver_specific_output} and {solver_name}" ) + existing_solver_specific_output = solver_name - # Ensure that at most solver has solver specific output. - existing_solver_specific_output = None - - def has_solver_specific_output(solver_name: str) -> None: - nonlocal existing_solver_specific_output - if existing_solver_specific_output is not None: - raise ValueError( - "found solver specific output for both" - f" {existing_solver_specific_output} and {solver_name}" - ) - existing_solver_specific_output = solver_name - - if self.gscip_specific_output is not None: - has_solver_specific_output("gscip") - proto.gscip_output.CopyFrom(self.gscip_specific_output) - if self.osqp_specific_output is not None: - has_solver_specific_output("osqp") - proto.osqp_output.CopyFrom(self.osqp_specific_output) - if self.pdlp_specific_output is not None: - has_solver_specific_output("pdlp") - proto.pdlp_output.CopyFrom(self.pdlp_specific_output) - return proto + if self.gscip_specific_output is not None: + has_solver_specific_output("gscip") + proto.gscip_output.CopyFrom(self.gscip_specific_output) + if self.osqp_specific_output is not None: + has_solver_specific_output("osqp") + proto.osqp_output.CopyFrom(self.osqp_specific_output) + if self.pdlp_specific_output is not None: + has_solver_specific_output("pdlp") + proto.pdlp_output.CopyFrom(self.pdlp_specific_output) + return proto def _get_problem_status( result_proto: result_pb2.SolveResultProto, ) -> result_pb2.ProblemStatusProto: - if result_proto.termination.HasField("problem_status"): - return result_proto.termination.problem_status - return result_proto.solve_stats.problem_status + if result_proto.termination.HasField("problem_status"): + return result_proto.termination.problem_status + return result_proto.solve_stats.problem_status def _get_objective_bounds( result_proto: result_pb2.SolveResultProto, ) -> result_pb2.ObjectiveBoundsProto: - if result_proto.termination.HasField("objective_bounds"): - return result_proto.termination.objective_bounds - return result_pb2.ObjectiveBoundsProto( - primal_bound=result_proto.solve_stats.best_primal_bound, - dual_bound=result_proto.solve_stats.best_dual_bound, - ) + if result_proto.termination.HasField("objective_bounds"): + return result_proto.termination.objective_bounds + return result_pb2.ObjectiveBoundsProto( + primal_bound=result_proto.solve_stats.best_primal_bound, + dual_bound=result_proto.solve_stats.best_dual_bound, + ) def _upgrade_termination( result_proto: result_pb2.SolveResultProto, ) -> result_pb2.TerminationProto: - return result_pb2.TerminationProto( - reason=result_proto.termination.reason, - limit=result_proto.termination.limit, - detail=result_proto.termination.detail, - problem_status=_get_problem_status(result_proto), - objective_bounds=_get_objective_bounds(result_proto), - ) + return result_pb2.TerminationProto( + reason=result_proto.termination.reason, + limit=result_proto.termination.limit, + detail=result_proto.termination.detail, + problem_status=_get_problem_status(result_proto), + objective_bounds=_get_objective_bounds(result_proto), + ) def parse_solve_result( @@ -1036,30 +1072,30 @@ def parse_solve_result( *, validate: bool = True, ) -> SolveResult: - """Returns a SolveResult equivalent to the input proto.""" - result = SolveResult() - # TODO(b/290091715): change to parse_termination(proto.termination) - # once solve_stats proto no longer has best_primal/dual_bound/problem_status - # and problem_status/objective_bounds are guaranteed to be present in - # termination proto. - result.termination = parse_termination(_upgrade_termination(proto)) - result.solve_stats = parse_solve_stats(proto.solve_stats) - for solution_proto in proto.solutions: - result.solutions.append( - solution.parse_solution(solution_proto, mod, validate=validate) - ) - for primal_ray_proto in proto.primal_rays: - result.primal_rays.append( - solution.parse_primal_ray(primal_ray_proto, mod, validate=validate) - ) - for dual_ray_proto in proto.dual_rays: - result.dual_rays.append( - solution.parse_dual_ray(dual_ray_proto, mod, validate=validate) - ) - if proto.HasField("gscip_output"): - result.gscip_specific_output = proto.gscip_output - elif proto.HasField("osqp_output"): - result.osqp_specific_output = proto.osqp_output - elif proto.HasField("pdlp_output"): - result.pdlp_specific_output = proto.pdlp_output - return result + """Returns a SolveResult equivalent to the input proto.""" + result = SolveResult() + # TODO(b/290091715): change to parse_termination(proto.termination) + # once solve_stats proto no longer has best_primal/dual_bound/problem_status + # and problem_status/objective_bounds are guaranteed to be present in + # termination proto. + result.termination = parse_termination(_upgrade_termination(proto)) + result.solve_stats = parse_solve_stats(proto.solve_stats) + for solution_proto in proto.solutions: + result.solutions.append( + solution.parse_solution(solution_proto, mod, validate=validate) + ) + for primal_ray_proto in proto.primal_rays: + result.primal_rays.append( + solution.parse_primal_ray(primal_ray_proto, mod, validate=validate) + ) + for dual_ray_proto in proto.dual_rays: + result.dual_rays.append( + solution.parse_dual_ray(dual_ray_proto, mod, validate=validate) + ) + if proto.HasField("gscip_output"): + result.gscip_specific_output = proto.gscip_output + elif proto.HasField("osqp_output"): + result.osqp_specific_output = proto.osqp_output + elif proto.HasField("pdlp_output"): + result.pdlp_specific_output = proto.pdlp_output + return result diff --git a/ortools/math_opt/python/result_test.py b/ortools/math_opt/python/result_test.py index b000bc6ce43..9919e5b75ef 100644 --- a/ortools/math_opt/python/result_test.py +++ b/ortools/math_opt/python/result_test.py @@ -30,1204 +30,1228 @@ class TerminationTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_termination_unspecified(self) -> None: - termination_proto = result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_UNSPECIFIED - ) - with self.assertRaisesRegex(ValueError, "Termination.*UNSPECIFIED"): - result.parse_termination(termination_proto) - - def test_termination_limit_but_not_limit_reason(self) -> None: - termination_proto = result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_OPTIMAL, - limit=result_pb2.LIMIT_OTHER, - ) - with self.assertRaisesRegex( - ValueError, "Termination limit.*FEASIBLE or NO_SOLUTION_FOUND" - ): - result.parse_termination(termination_proto) - - def test_termination_limit_reason_but_no_limit(self) -> None: - termination_proto = result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND, - limit=result_pb2.LIMIT_UNSPECIFIED, - ) - with self.assertRaisesRegex( - ValueError, "Termination limit.*FEASIBLE or NO_SOLUTION_FOUND" - ): - result.parse_termination(termination_proto) - - def test_termination_ok_proto_round_trip(self) -> None: - termination = result.Termination( - reason=result.TerminationReason.NO_SOLUTION_FOUND, - limit=result.Limit.OTHER, - detail="detail", - problem_status=result.ProblemStatus( - primal_status=result.FeasibilityStatus.FEASIBLE, - dual_status=result.FeasibilityStatus.INFEASIBLE, - primal_or_dual_infeasible=False, - ), - objective_bounds=result.ObjectiveBounds(primal_bound=10, dual_bound=20), - ) - - termination_proto = result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND, - limit=result_pb2.LIMIT_OTHER, - detail="detail", - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - primal_or_dual_infeasible=False, - ), - objective_bounds=result_pb2.ObjectiveBoundsProto( - primal_bound=10, dual_bound=20 - ), - ) - - # Test proto-> Termination - self.assertEqual(result.parse_termination(termination_proto), termination) - - # Test Termination -> proto - self.assert_protos_equiv(termination.to_proto(), termination_proto) - - -class ParseProblemStatus(compare_proto.MathOptProtoAssertions, absltest.TestCase): + def test_termination_unspecified(self) -> None: + termination_proto = result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_UNSPECIFIED + ) + with self.assertRaisesRegex(ValueError, "Termination.*UNSPECIFIED"): + result.parse_termination(termination_proto) - def test_problem_status_round_trip(self) -> None: - problem_status = result.ProblemStatus( + def test_termination_limit_but_not_limit_reason(self) -> None: + termination_proto = result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_OPTIMAL, + limit=result_pb2.LIMIT_OTHER, + ) + with self.assertRaisesRegex( + ValueError, "Termination limit.*FEASIBLE or NO_SOLUTION_FOUND" + ): + result.parse_termination(termination_proto) + + def test_termination_limit_reason_but_no_limit(self) -> None: + termination_proto = result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND, + limit=result_pb2.LIMIT_UNSPECIFIED, + ) + with self.assertRaisesRegex( + ValueError, "Termination limit.*FEASIBLE or NO_SOLUTION_FOUND" + ): + result.parse_termination(termination_proto) + + def test_termination_ok_proto_round_trip(self) -> None: + termination = result.Termination( + reason=result.TerminationReason.NO_SOLUTION_FOUND, + limit=result.Limit.OTHER, + detail="detail", + problem_status=result.ProblemStatus( primal_status=result.FeasibilityStatus.FEASIBLE, dual_status=result.FeasibilityStatus.INFEASIBLE, primal_or_dual_infeasible=False, - ) - problem_status_proto = problem_status.to_proto() - expected_proto = result_pb2.ProblemStatusProto( + ), + objective_bounds=result.ObjectiveBounds(primal_bound=10, dual_bound=20), + ) + + termination_proto = result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND, + limit=result_pb2.LIMIT_OTHER, + detail="detail", + problem_status=result_pb2.ProblemStatusProto( primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, primal_or_dual_infeasible=False, - ) - self.assert_protos_equiv(expected_proto, problem_status_proto) - round_trip_status = result.parse_problem_status(problem_status_proto) - self.assertEqual(problem_status, round_trip_status) + ), + objective_bounds=result_pb2.ObjectiveBoundsProto( + primal_bound=10, dual_bound=20 + ), + ) - def test_problem_status_unspecified_primal_status(self) -> None: - proto = result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_UNSPECIFIED, - dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - primal_or_dual_infeasible=False, - ) - with self.assertRaisesRegex( - ValueError, "Primal feasibility status.*UNSPECIFIED" - ): - result.parse_problem_status(proto) - - def test_problem_status_unspecified_dual_status(self) -> None: - proto = result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_UNSPECIFIED, - primal_or_dual_infeasible=False, - ) - with self.assertRaisesRegex(ValueError, "Dual feasibility status.*UNSPECIFIED"): - result.parse_problem_status(proto) + # Test proto-> Termination + self.assertEqual(result.parse_termination(termination_proto), termination) + # Test Termination -> proto + self.assert_protos_equiv(termination.to_proto(), termination_proto) -class ParseObjectiveBounds(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_objective_bounds_round_trip(self) -> None: - objective_bounds = result.ObjectiveBounds(primal_bound=10, dual_bound=20) - objective_bounds_proto = objective_bounds.to_proto() - expected_proto = result_pb2.ObjectiveBoundsProto(primal_bound=10, dual_bound=20) - self.assert_protos_equiv(expected_proto, objective_bounds_proto) - round_trip_objective_bounds = result.parse_objective_bounds( - objective_bounds_proto - ) - self.assertEqual(objective_bounds, round_trip_objective_bounds) +class ParseProblemStatus( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_problem_status_round_trip(self) -> None: + problem_status = result.ProblemStatus( + primal_status=result.FeasibilityStatus.FEASIBLE, + dual_status=result.FeasibilityStatus.INFEASIBLE, + primal_or_dual_infeasible=False, + ) + problem_status_proto = problem_status.to_proto() + expected_proto = result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, + primal_or_dual_infeasible=False, + ) + self.assert_protos_equiv(expected_proto, problem_status_proto) + round_trip_status = result.parse_problem_status(problem_status_proto) + self.assertEqual(problem_status, round_trip_status) + + def test_problem_status_unspecified_primal_status(self) -> None: + proto = result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_UNSPECIFIED, + dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, + primal_or_dual_infeasible=False, + ) + with self.assertRaisesRegex( + ValueError, "Primal feasibility status.*UNSPECIFIED" + ): + result.parse_problem_status(proto) + + def test_problem_status_unspecified_dual_status(self) -> None: + proto = result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_UNSPECIFIED, + primal_or_dual_infeasible=False, + ) + with self.assertRaisesRegex( + ValueError, "Dual feasibility status.*UNSPECIFIED" + ): + result.parse_problem_status(proto) + + +class ParseObjectiveBounds( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_objective_bounds_round_trip(self) -> None: + objective_bounds = result.ObjectiveBounds(primal_bound=10, dual_bound=20) + objective_bounds_proto = objective_bounds.to_proto() + expected_proto = result_pb2.ObjectiveBoundsProto( + primal_bound=10, dual_bound=20 + ) + self.assert_protos_equiv(expected_proto, objective_bounds_proto) + round_trip_objective_bounds = result.parse_objective_bounds( + objective_bounds_proto + ) + self.assertEqual(objective_bounds, round_trip_objective_bounds) class ParseSolveStats(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_problem_status_round_trip(self) -> None: - solve_stats = result.SolveStats( - solve_time=datetime.timedelta(seconds=10), - simplex_iterations=10, - barrier_iterations=20, - first_order_iterations=30, - node_count=40, - ) - solve_stats_proto = solve_stats.to_proto() - expected_proto = result_pb2.SolveStatsProto() - expected_proto.solve_time.seconds = 10 - expected_proto.simplex_iterations = 10 - expected_proto.barrier_iterations = 20 - expected_proto.first_order_iterations = 30 - expected_proto.node_count = 40 - self.assert_protos_equiv(expected_proto, solve_stats_proto) - round_trip_solve_stats = result.parse_solve_stats(solve_stats_proto) - self.assertEqual(solve_stats, round_trip_solve_stats) + def test_problem_status_round_trip(self) -> None: + solve_stats = result.SolveStats( + solve_time=datetime.timedelta(seconds=10), + simplex_iterations=10, + barrier_iterations=20, + first_order_iterations=30, + node_count=40, + ) + solve_stats_proto = solve_stats.to_proto() + expected_proto = result_pb2.SolveStatsProto() + expected_proto.solve_time.seconds = 10 + expected_proto.simplex_iterations = 10 + expected_proto.barrier_iterations = 20 + expected_proto.first_order_iterations = 30 + expected_proto.node_count = 40 + self.assert_protos_equiv(expected_proto, solve_stats_proto) + round_trip_solve_stats = result.parse_solve_stats(solve_stats_proto) + self.assertEqual(solve_stats, round_trip_solve_stats) class SolveResultAuxiliaryFunctionsTest(absltest.TestCase): - def test_solve_time(self) -> None: - res = result.SolveResult( - solve_stats=result.SolveStats(solve_time=datetime.timedelta(seconds=10)) - ) - self.assertEqual(res.solve_time(), datetime.timedelta(seconds=10)) + def test_solve_time(self) -> None: + res = result.SolveResult( + solve_stats=result.SolveStats(solve_time=datetime.timedelta(seconds=10)) + ) + self.assertEqual(res.solve_time(), datetime.timedelta(seconds=10)) - def test_best_objective_bound(self) -> None: - res = result.SolveResult( - termination=result.Termination( - objective_bounds=result.ObjectiveBounds(dual_bound=10.0) - ) + def test_best_objective_bound(self) -> None: + res = result.SolveResult( + termination=result.Termination( + objective_bounds=result.ObjectiveBounds(dual_bound=10.0) ) - self.assertEqual(res.best_objective_bound(), 10.0) - - def test_primal_solution_has_feasible(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - other_mod = model.Model(name="other_test_model") - other_x = other_mod.add_binary_variable(name="other_x") - res = result.SolveResult() - res.solutions.append( - solution.Solution( - primal_solution=solution.PrimalSolution( - variable_values={x: 2.0, y: 1.0}, - objective_value=3.0, - feasibility_status=solution.SolutionStatus.FEASIBLE, - ) + ) + self.assertEqual(res.best_objective_bound(), 10.0) + + def test_primal_solution_has_feasible(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + other_mod = model.Model(name="other_test_model") + other_x = other_mod.add_binary_variable(name="other_x") + res = result.SolveResult() + res.solutions.append( + solution.Solution( + primal_solution=solution.PrimalSolution( + variable_values={x: 2.0, y: 1.0}, + objective_value=3.0, + feasibility_status=solution.SolutionStatus.FEASIBLE, ) ) - self.assertTrue(res.has_primal_feasible_solution()) - self.assertEqual(res.objective_value(), 3.0) - self.assertDictEqual(res.variable_values(), {x: 2.0, y: 1.0}) - self.assertEqual(res.variable_values()[x], 2.0) - self.assertEqual(res.variable_values([]), []) - self.assertEqual(res.variable_values([y, x]), [1.0, 2.0]) - self.assertEqual(res.variable_values(y), 1.0) - with self.assertRaisesRegex(KeyError, ".*other_x"): - res.variable_values(other_x) - with self.assertRaisesRegex(KeyError, ".*string"): - res.variable_values([y, "string"]) - with self.assertRaisesRegex(TypeError, ".*int"): - res.variable_values(20) # pytype: disable=wrong-arg-types - - def test_primal_solution_no_feasible(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - res = result.SolveResult() - res.solutions.append( - solution.Solution( - primal_solution=solution.PrimalSolution( - variable_values={ - x: 2.0, - }, - objective_value=3.0, - feasibility_status=solution.SolutionStatus.UNDETERMINED, - ) + ) + self.assertTrue(res.has_primal_feasible_solution()) + self.assertEqual(res.objective_value(), 3.0) + self.assertDictEqual(res.variable_values(), {x: 2.0, y: 1.0}) + self.assertEqual(res.variable_values()[x], 2.0) + self.assertEqual(res.variable_values([]), []) + self.assertEqual(res.variable_values([y, x]), [1.0, 2.0]) + self.assertEqual(res.variable_values(y), 1.0) + with self.assertRaisesRegex(KeyError, ".*other_x"): + res.variable_values(other_x) + with self.assertRaisesRegex(KeyError, ".*string"): + res.variable_values([y, "string"]) + with self.assertRaisesRegex(TypeError, ".*int"): + res.variable_values(20) # pytype: disable=wrong-arg-types + + def test_primal_solution_no_feasible(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + res = result.SolveResult() + res.solutions.append( + solution.Solution( + primal_solution=solution.PrimalSolution( + variable_values={ + x: 2.0, + }, + objective_value=3.0, + feasibility_status=solution.SolutionStatus.UNDETERMINED, ) ) - self.assertFalse(res.has_primal_feasible_solution()) - with self.assertRaisesRegex(ValueError, "No primal feasible.*"): - res.objective_value() - with self.assertRaisesRegex(ValueError, "No primal feasible.*"): - res.variable_values() - - def test_primal_solution_no_primal(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - res = result.SolveResult() - res.solutions.append( - solution.Solution( - dual_solution=solution.DualSolution( - dual_values={c: 3.0}, - reduced_costs={x: 1.0}, - objective_value=2.0, - feasibility_status=solution.SolutionStatus.FEASIBLE, - ) + ) + self.assertFalse(res.has_primal_feasible_solution()) + with self.assertRaisesRegex(ValueError, "No primal feasible.*"): + res.objective_value() + with self.assertRaisesRegex(ValueError, "No primal feasible.*"): + res.variable_values() + + def test_primal_solution_no_primal(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + res = result.SolveResult() + res.solutions.append( + solution.Solution( + dual_solution=solution.DualSolution( + dual_values={c: 3.0}, + reduced_costs={x: 1.0}, + objective_value=2.0, + feasibility_status=solution.SolutionStatus.FEASIBLE, ) ) - self.assertFalse(res.has_primal_feasible_solution()) - with self.assertRaisesRegex(ValueError, "No primal feasible.*"): - res.objective_value() - with self.assertRaisesRegex(ValueError, "No primal feasible.*"): - res.variable_values() - - def test_primal_solution_no_solution(self) -> None: - res = result.SolveResult() - self.assertFalse(res.has_primal_feasible_solution()) - with self.assertRaisesRegex(ValueError, "No primal feasible.*"): - res.objective_value() - with self.assertRaisesRegex(ValueError, "No primal feasible.*"): - res.variable_values() - - def test_dual_solution_has_feasible(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - other_mod = model.Model(name="other_test_model") - other_x = other_mod.add_binary_variable(name="other_x") - other_c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="other_c") - res = result.SolveResult() - res.solutions.append( - solution.Solution( - dual_solution=solution.DualSolution( - dual_values={c: 3.0, d: 4.0}, - reduced_costs={x: 1.0, y: -2.0}, - objective_value=2.0, - feasibility_status=solution.SolutionStatus.FEASIBLE, - ) + ) + self.assertFalse(res.has_primal_feasible_solution()) + with self.assertRaisesRegex(ValueError, "No primal feasible.*"): + res.objective_value() + with self.assertRaisesRegex(ValueError, "No primal feasible.*"): + res.variable_values() + + def test_primal_solution_no_solution(self) -> None: + res = result.SolveResult() + self.assertFalse(res.has_primal_feasible_solution()) + with self.assertRaisesRegex(ValueError, "No primal feasible.*"): + res.objective_value() + with self.assertRaisesRegex(ValueError, "No primal feasible.*"): + res.variable_values() + + def test_dual_solution_has_feasible(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + other_mod = model.Model(name="other_test_model") + other_x = other_mod.add_binary_variable(name="other_x") + other_c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="other_c") + res = result.SolveResult() + res.solutions.append( + solution.Solution( + dual_solution=solution.DualSolution( + dual_values={c: 3.0, d: 4.0}, + reduced_costs={x: 1.0, y: -2.0}, + objective_value=2.0, + feasibility_status=solution.SolutionStatus.FEASIBLE, ) ) - self.assertTrue(res.has_dual_feasible_solution()) - # Reduced costs. - self.assertDictEqual(res.reduced_costs(), {x: 1.0, y: -2.0}) - self.assertEqual(res.reduced_costs()[x], 1.0) - self.assertEqual(res.reduced_costs([]), []) - self.assertEqual(res.reduced_costs([y, x]), [-2.0, 1.0]) - self.assertEqual(res.reduced_costs(y), -2.0) - with self.assertRaisesRegex(KeyError, ".*other_x"): - res.reduced_costs(other_x) - with self.assertRaisesRegex(KeyError, ".*string"): - res.reduced_costs([y, "string"]) - with self.assertRaisesRegex(TypeError, ".*int"): - res.reduced_costs(20) # pytype: disable=wrong-arg-types - # Dual values. - self.assertDictEqual(res.dual_values(), {c: 3.0, d: 4.0}) - self.assertEqual(res.dual_values()[c], 3.0) - self.assertEqual(res.dual_values([]), []) - self.assertEqual(res.dual_values([d, c]), [4.0, 3.0]) - self.assertEqual(res.dual_values(c), 3.0) - with self.assertRaisesRegex(KeyError, ".*other_c"): - res.dual_values(other_c) - with self.assertRaisesRegex(KeyError, ".*string"): - res.dual_values([d, "string"]) - with self.assertRaisesRegex(TypeError, ".*int"): - res.dual_values(20) # pytype: disable=wrong-arg-types - - def test_dual_solution_no_feasible(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - res = result.SolveResult() - res.solutions.append( - solution.Solution( - dual_solution=solution.DualSolution( - dual_values={c: 3.0}, - reduced_costs={ - x: 1.0, - }, - objective_value=2.0, - feasibility_status=solution.SolutionStatus.UNDETERMINED, - ) + ) + self.assertTrue(res.has_dual_feasible_solution()) + # Reduced costs. + self.assertDictEqual(res.reduced_costs(), {x: 1.0, y: -2.0}) + self.assertEqual(res.reduced_costs()[x], 1.0) + self.assertEqual(res.reduced_costs([]), []) + self.assertEqual(res.reduced_costs([y, x]), [-2.0, 1.0]) + self.assertEqual(res.reduced_costs(y), -2.0) + with self.assertRaisesRegex(KeyError, ".*other_x"): + res.reduced_costs(other_x) + with self.assertRaisesRegex(KeyError, ".*string"): + res.reduced_costs([y, "string"]) + with self.assertRaisesRegex(TypeError, ".*int"): + res.reduced_costs(20) # pytype: disable=wrong-arg-types + # Dual values. + self.assertDictEqual(res.dual_values(), {c: 3.0, d: 4.0}) + self.assertEqual(res.dual_values()[c], 3.0) + self.assertEqual(res.dual_values([]), []) + self.assertEqual(res.dual_values([d, c]), [4.0, 3.0]) + self.assertEqual(res.dual_values(c), 3.0) + with self.assertRaisesRegex(KeyError, ".*other_c"): + res.dual_values(other_c) + with self.assertRaisesRegex(KeyError, ".*string"): + res.dual_values([d, "string"]) + with self.assertRaisesRegex(TypeError, ".*int"): + res.dual_values(20) # pytype: disable=wrong-arg-types + + def test_dual_solution_no_feasible(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + res = result.SolveResult() + res.solutions.append( + solution.Solution( + dual_solution=solution.DualSolution( + dual_values={c: 3.0}, + reduced_costs={ + x: 1.0, + }, + objective_value=2.0, + feasibility_status=solution.SolutionStatus.UNDETERMINED, ) ) - self.assertFalse(res.has_dual_feasible_solution()) - with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): - res.reduced_costs() - with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): - res.dual_values() - - def test_dual_solution_no_dual_in_best_solution(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - res = result.SolveResult() - res.solutions.append( - solution.Solution( - primal_solution=solution.PrimalSolution( - variable_values={ - x: 2.0, - }, - objective_value=3.0, - feasibility_status=solution.SolutionStatus.FEASIBLE, - ) + ) + self.assertFalse(res.has_dual_feasible_solution()) + with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): + res.reduced_costs() + with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): + res.dual_values() + + def test_dual_solution_no_dual_in_best_solution(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + res = result.SolveResult() + res.solutions.append( + solution.Solution( + primal_solution=solution.PrimalSolution( + variable_values={ + x: 2.0, + }, + objective_value=3.0, + feasibility_status=solution.SolutionStatus.FEASIBLE, ) ) - res.solutions.append( - solution.Solution( - dual_solution=solution.DualSolution( - dual_values={c: 3.0}, - reduced_costs={ - x: 1.0, - }, - objective_value=2.0, - feasibility_status=solution.SolutionStatus.FEASIBLE, - ) + ) + res.solutions.append( + solution.Solution( + dual_solution=solution.DualSolution( + dual_values={c: 3.0}, + reduced_costs={ + x: 1.0, + }, + objective_value=2.0, + feasibility_status=solution.SolutionStatus.FEASIBLE, ) ) - self.assertFalse(res.has_dual_feasible_solution()) - with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): - res.reduced_costs() - with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): - res.dual_values() - - def test_dual_solution_no_solution(self) -> None: - res = result.SolveResult() - self.assertFalse(res.has_dual_feasible_solution()) - with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): - res.reduced_costs() - with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): - res.dual_values() - - def test_primal_ray_has_ray(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - other_mod = model.Model(name="other_test_model") - other_x = other_mod.add_binary_variable(name="other_x") - res = result.SolveResult() - res.primal_rays.append(solution.PrimalRay(variable_values={x: 2.0, y: 1.0})) - self.assertTrue(res.has_ray()) - self.assertDictEqual(res.ray_variable_values(), {x: 2.0, y: 1.0}) - self.assertEqual(res.ray_variable_values()[x], 2.0) - self.assertEqual(res.ray_variable_values([]), []) - self.assertEqual(res.ray_variable_values([y, x]), [1.0, 2.0]) - self.assertEqual(res.ray_variable_values(y), 1.0) - with self.assertRaisesRegex(KeyError, ".*other_x"): - res.ray_variable_values(other_x) - with self.assertRaisesRegex(KeyError, ".*string"): - res.ray_variable_values([y, "string"]) - with self.assertRaisesRegex(TypeError, ".*int"): - res.ray_variable_values(20) # pytype: disable=wrong-arg-types - - def test_primal_ray_no_ray(self) -> None: - res = result.SolveResult() - self.assertFalse(res.has_ray()) - with self.assertRaisesRegex(ValueError, ".*primal ray.*"): - res.ray_variable_values() - - def test_dual_ray_has_ray(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - other_mod = model.Model(name="other_test_model") - other_x = other_mod.add_binary_variable(name="other_x") - other_c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="other_c") - res = result.SolveResult() - res.dual_rays.append( - solution.DualRay( - dual_values={c: 3.0, d: 4.0}, reduced_costs={x: 1.0, y: -2.0} - ) + ) + self.assertFalse(res.has_dual_feasible_solution()) + with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): + res.reduced_costs() + with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): + res.dual_values() + + def test_dual_solution_no_solution(self) -> None: + res = result.SolveResult() + self.assertFalse(res.has_dual_feasible_solution()) + with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): + res.reduced_costs() + with self.assertRaisesRegex(ValueError, "Best solution.*dual feasible.*"): + res.dual_values() + + def test_primal_ray_has_ray(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + other_mod = model.Model(name="other_test_model") + other_x = other_mod.add_binary_variable(name="other_x") + res = result.SolveResult() + res.primal_rays.append(solution.PrimalRay(variable_values={x: 2.0, y: 1.0})) + self.assertTrue(res.has_ray()) + self.assertDictEqual(res.ray_variable_values(), {x: 2.0, y: 1.0}) + self.assertEqual(res.ray_variable_values()[x], 2.0) + self.assertEqual(res.ray_variable_values([]), []) + self.assertEqual(res.ray_variable_values([y, x]), [1.0, 2.0]) + self.assertEqual(res.ray_variable_values(y), 1.0) + with self.assertRaisesRegex(KeyError, ".*other_x"): + res.ray_variable_values(other_x) + with self.assertRaisesRegex(KeyError, ".*string"): + res.ray_variable_values([y, "string"]) + with self.assertRaisesRegex(TypeError, ".*int"): + res.ray_variable_values(20) # pytype: disable=wrong-arg-types + + def test_primal_ray_no_ray(self) -> None: + res = result.SolveResult() + self.assertFalse(res.has_ray()) + with self.assertRaisesRegex(ValueError, ".*primal ray.*"): + res.ray_variable_values() + + def test_dual_ray_has_ray(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + other_mod = model.Model(name="other_test_model") + other_x = other_mod.add_binary_variable(name="other_x") + other_c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="other_c") + res = result.SolveResult() + res.dual_rays.append( + solution.DualRay( + dual_values={c: 3.0, d: 4.0}, reduced_costs={x: 1.0, y: -2.0} ) - self.assertTrue(res.has_dual_ray()) - self.assertDictEqual(res.ray_reduced_costs(), {x: 1.0, y: -2.0}) - # Reduced costs. - self.assertEqual(res.ray_reduced_costs()[x], 1.0) - self.assertEqual(res.ray_reduced_costs([]), []) - self.assertEqual(res.ray_reduced_costs([y, x]), [-2.0, 1.0]) - self.assertEqual(res.ray_reduced_costs(y), -2.0) - with self.assertRaisesRegex(KeyError, ".*other_x"): - res.ray_reduced_costs(other_x) - with self.assertRaisesRegex(KeyError, ".*string"): - res.ray_reduced_costs([y, "string"]) - with self.assertRaisesRegex(TypeError, ".*int"): - res.ray_reduced_costs(20) # pytype: disable=wrong-arg-types - # Dual values. - self.assertDictEqual(res.ray_dual_values(), {c: 3.0, d: 4.0}) - self.assertEqual(res.ray_dual_values()[c], 3.0) - self.assertEqual(res.ray_dual_values([]), []) - self.assertEqual(res.ray_dual_values([d, c]), [4.0, 3.0]) - self.assertEqual(res.ray_dual_values(c), 3.0) - with self.assertRaisesRegex(KeyError, ".*other_c"): - res.ray_dual_values(other_c) - with self.assertRaisesRegex(KeyError, ".*string"): - res.ray_dual_values([d, "string"]) - with self.assertRaisesRegex(TypeError, ".*int"): - res.ray_dual_values(20) # pytype: disable=wrong-arg-types - - def test_dual_ray_no_ray(self) -> None: - res = result.SolveResult() - self.assertFalse(res.has_dual_ray()) - with self.assertRaisesRegex(ValueError, ".*dual ray.*"): - res.ray_dual_values() - with self.assertRaisesRegex(ValueError, ".*dual ray.*"): - res.ray_reduced_costs() - - def test_basis_has_basis(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - other_mod = model.Model(name="other_test_model") - other_x = other_mod.add_binary_variable(name="other_x") - other_c = other_mod.add_linear_constraint(name="other_c") - res = result.SolveResult() - res.solutions.append( - solution.Solution( - basis=solution.Basis( - variable_status={ - x: solution.BasisStatus.AT_LOWER_BOUND, - y: solution.BasisStatus.AT_UPPER_BOUND, - }, - constraint_status={ - c: solution.BasisStatus.BASIC, - d: solution.BasisStatus.FIXED_VALUE, - }, - ) + ) + self.assertTrue(res.has_dual_ray()) + self.assertDictEqual(res.ray_reduced_costs(), {x: 1.0, y: -2.0}) + # Reduced costs. + self.assertEqual(res.ray_reduced_costs()[x], 1.0) + self.assertEqual(res.ray_reduced_costs([]), []) + self.assertEqual(res.ray_reduced_costs([y, x]), [-2.0, 1.0]) + self.assertEqual(res.ray_reduced_costs(y), -2.0) + with self.assertRaisesRegex(KeyError, ".*other_x"): + res.ray_reduced_costs(other_x) + with self.assertRaisesRegex(KeyError, ".*string"): + res.ray_reduced_costs([y, "string"]) + with self.assertRaisesRegex(TypeError, ".*int"): + res.ray_reduced_costs(20) # pytype: disable=wrong-arg-types + # Dual values. + self.assertDictEqual(res.ray_dual_values(), {c: 3.0, d: 4.0}) + self.assertEqual(res.ray_dual_values()[c], 3.0) + self.assertEqual(res.ray_dual_values([]), []) + self.assertEqual(res.ray_dual_values([d, c]), [4.0, 3.0]) + self.assertEqual(res.ray_dual_values(c), 3.0) + with self.assertRaisesRegex(KeyError, ".*other_c"): + res.ray_dual_values(other_c) + with self.assertRaisesRegex(KeyError, ".*string"): + res.ray_dual_values([d, "string"]) + with self.assertRaisesRegex(TypeError, ".*int"): + res.ray_dual_values(20) # pytype: disable=wrong-arg-types + + def test_dual_ray_no_ray(self) -> None: + res = result.SolveResult() + self.assertFalse(res.has_dual_ray()) + with self.assertRaisesRegex(ValueError, ".*dual ray.*"): + res.ray_dual_values() + with self.assertRaisesRegex(ValueError, ".*dual ray.*"): + res.ray_reduced_costs() + + def test_basis_has_basis(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + other_mod = model.Model(name="other_test_model") + other_x = other_mod.add_binary_variable(name="other_x") + other_c = other_mod.add_linear_constraint(name="other_c") + res = result.SolveResult() + res.solutions.append( + solution.Solution( + basis=solution.Basis( + variable_status={ + x: solution.BasisStatus.AT_LOWER_BOUND, + y: solution.BasisStatus.AT_UPPER_BOUND, + }, + constraint_status={ + c: solution.BasisStatus.BASIC, + d: solution.BasisStatus.FIXED_VALUE, + }, ) ) - self.assertTrue(res.has_basis()) - # Variable status - self.assertDictEqual( - res.variable_status(), - { - x: solution.BasisStatus.AT_LOWER_BOUND, - y: solution.BasisStatus.AT_UPPER_BOUND, - }, - ) - self.assertEqual(res.variable_status()[x], solution.BasisStatus.AT_LOWER_BOUND) - self.assertEqual(res.variable_status([]), []) - self.assertEqual( - res.variable_status([y, x]), - [ - solution.BasisStatus.AT_UPPER_BOUND, - solution.BasisStatus.AT_LOWER_BOUND, - ], - ) - self.assertEqual(res.variable_status(y), solution.BasisStatus.AT_UPPER_BOUND) - with self.assertRaisesRegex(KeyError, ".*other_x"): - res.variable_status(other_x) - with self.assertRaisesRegex(KeyError, ".*string"): - res.variable_status([y, "string"]) - with self.assertRaisesRegex(TypeError, ".*int"): - res.variable_status(20) # pytype: disable=wrong-arg-types - # Constraint status - self.assertDictEqual( - res.constraint_status(), - {c: solution.BasisStatus.BASIC, d: solution.BasisStatus.FIXED_VALUE}, - ) - self.assertEqual(res.constraint_status()[c], solution.BasisStatus.BASIC) - self.assertEqual(res.constraint_status([]), []) - self.assertEqual( - res.constraint_status([d, c]), - [solution.BasisStatus.FIXED_VALUE, solution.BasisStatus.BASIC], - ) - self.assertEqual(res.constraint_status(c), solution.BasisStatus.BASIC) - with self.assertRaisesRegex(KeyError, ".*other_c"): - res.constraint_status(other_c) - with self.assertRaisesRegex(KeyError, ".*string"): - res.constraint_status([d, "string"]) - with self.assertRaisesRegex(TypeError, ".*int"): - res.constraint_status(20) # pytype: disable=wrong-arg-types - - def test_basis_no_basis_in_best_solution(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - res = result.SolveResult() - res.solutions.append( - solution.Solution( - primal_solution=solution.PrimalSolution( - variable_values={x: 2.0, y: 1.0}, - objective_value=3.0, - feasibility_status=solution.SolutionStatus.FEASIBLE, - ) + ) + self.assertTrue(res.has_basis()) + # Variable status + self.assertDictEqual( + res.variable_status(), + { + x: solution.BasisStatus.AT_LOWER_BOUND, + y: solution.BasisStatus.AT_UPPER_BOUND, + }, + ) + self.assertEqual( + res.variable_status()[x], solution.BasisStatus.AT_LOWER_BOUND + ) + self.assertEqual(res.variable_status([]), []) + self.assertEqual( + res.variable_status([y, x]), + [ + solution.BasisStatus.AT_UPPER_BOUND, + solution.BasisStatus.AT_LOWER_BOUND, + ], + ) + self.assertEqual( + res.variable_status(y), solution.BasisStatus.AT_UPPER_BOUND + ) + with self.assertRaisesRegex(KeyError, ".*other_x"): + res.variable_status(other_x) + with self.assertRaisesRegex(KeyError, ".*string"): + res.variable_status([y, "string"]) + with self.assertRaisesRegex(TypeError, ".*int"): + res.variable_status(20) # pytype: disable=wrong-arg-types + # Constraint status + self.assertDictEqual( + res.constraint_status(), + {c: solution.BasisStatus.BASIC, d: solution.BasisStatus.FIXED_VALUE}, + ) + self.assertEqual(res.constraint_status()[c], solution.BasisStatus.BASIC) + self.assertEqual(res.constraint_status([]), []) + self.assertEqual( + res.constraint_status([d, c]), + [solution.BasisStatus.FIXED_VALUE, solution.BasisStatus.BASIC], + ) + self.assertEqual(res.constraint_status(c), solution.BasisStatus.BASIC) + with self.assertRaisesRegex(KeyError, ".*other_c"): + res.constraint_status(other_c) + with self.assertRaisesRegex(KeyError, ".*string"): + res.constraint_status([d, "string"]) + with self.assertRaisesRegex(TypeError, ".*int"): + res.constraint_status(20) # pytype: disable=wrong-arg-types + + def test_basis_no_basis_in_best_solution(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + res = result.SolveResult() + res.solutions.append( + solution.Solution( + primal_solution=solution.PrimalSolution( + variable_values={x: 2.0, y: 1.0}, + objective_value=3.0, + feasibility_status=solution.SolutionStatus.FEASIBLE, ) ) - res.solutions.append( - solution.Solution( - basis=solution.Basis( - variable_status={ - x: solution.BasisStatus.AT_LOWER_BOUND, - y: solution.BasisStatus.AT_UPPER_BOUND, - }, - constraint_status={c: solution.BasisStatus.BASIC}, - ) + ) + res.solutions.append( + solution.Solution( + basis=solution.Basis( + variable_status={ + x: solution.BasisStatus.AT_LOWER_BOUND, + y: solution.BasisStatus.AT_UPPER_BOUND, + }, + constraint_status={c: solution.BasisStatus.BASIC}, ) ) - self.assertFalse(res.has_basis()) - with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"): - res.variable_status() - with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"): - res.constraint_status() - - def test_basis_no_solution(self) -> None: - res = result.SolveResult() - self.assertFalse(res.has_basis()) - with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"): - res.variable_status() - with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"): - res.constraint_status() - - def test_bounded(self) -> None: - res = result.SolveResult( - termination=result.Termination( - reason=result.TerminationReason.NO_SOLUTION_FOUND, - problem_status=result.ProblemStatus( - primal_status=result.FeasibilityStatus.FEASIBLE, - dual_status=result.FeasibilityStatus.FEASIBLE, - primal_or_dual_infeasible=False, - ), - objective_bounds=result.ObjectiveBounds( - primal_bound=math.inf, - dual_bound=-math.inf, - ), - ), - ) - self.assertTrue(res.bounded()) - - def test_not_bounded_primal_infeasible(self) -> None: - res = result.SolveResult( - termination=result.Termination( - reason=result.TerminationReason.NO_SOLUTION_FOUND, - problem_status=result.ProblemStatus( - primal_status=result.FeasibilityStatus.INFEASIBLE, - dual_status=result.FeasibilityStatus.FEASIBLE, - primal_or_dual_infeasible=False, - ), - objective_bounds=result.ObjectiveBounds( - primal_bound=math.inf, - dual_bound=-math.inf, - ), + ) + self.assertFalse(res.has_basis()) + with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"): + res.variable_status() + with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"): + res.constraint_status() + + def test_basis_no_solution(self) -> None: + res = result.SolveResult() + self.assertFalse(res.has_basis()) + with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"): + res.variable_status() + with self.assertRaisesRegex(ValueError, "Best solution.*basis.*"): + res.constraint_status() + + def test_bounded(self) -> None: + res = result.SolveResult( + termination=result.Termination( + reason=result.TerminationReason.NO_SOLUTION_FOUND, + problem_status=result.ProblemStatus( + primal_status=result.FeasibilityStatus.FEASIBLE, + dual_status=result.FeasibilityStatus.FEASIBLE, + primal_or_dual_infeasible=False, ), - ) - self.assertFalse(res.bounded()) - - def test_not_bounded_dual_infeasible(self) -> None: - res = result.SolveResult( - termination=result.Termination( - reason=result.TerminationReason.NO_SOLUTION_FOUND, - problem_status=result.ProblemStatus( - primal_status=result.FeasibilityStatus.FEASIBLE, - dual_status=result.FeasibilityStatus.INFEASIBLE, - primal_or_dual_infeasible=False, - ), - objective_bounds=result.ObjectiveBounds( - primal_bound=math.inf, - dual_bound=-math.inf, - ), + objective_bounds=result.ObjectiveBounds( + primal_bound=math.inf, + dual_bound=-math.inf, ), - ) - self.assertFalse(res.bounded()) - + ), + ) + self.assertTrue(res.bounded()) -def _make_undetermined_result_proto() -> result_pb2.SolveResultProto: - proto = result_pb2.SolveResultProto( - termination=result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND, - limit=result_pb2.LIMIT_TIME, - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_UNDETERMINED, - dual_status=result_pb2.FEASIBILITY_STATUS_UNDETERMINED, + def test_not_bounded_primal_infeasible(self) -> None: + res = result.SolveResult( + termination=result.Termination( + reason=result.TerminationReason.NO_SOLUTION_FOUND, + problem_status=result.ProblemStatus( + primal_status=result.FeasibilityStatus.INFEASIBLE, + dual_status=result.FeasibilityStatus.FEASIBLE, primal_or_dual_infeasible=False, ), - objective_bounds=result_pb2.ObjectiveBoundsProto( + objective_bounds=result.ObjectiveBounds( primal_bound=math.inf, dual_bound=-math.inf, ), - ) + ), ) - proto.solve_stats.solve_time.FromTimedelta(datetime.timedelta(minutes=2)) - return proto - + self.assertFalse(res.bounded()) -def _make_undetermined_solve_result() -> result.SolveResult: - return result.SolveResult( + def test_not_bounded_dual_infeasible(self) -> None: + res = result.SolveResult( termination=result.Termination( reason=result.TerminationReason.NO_SOLUTION_FOUND, - limit=result.Limit.TIME, problem_status=result.ProblemStatus( - primal_status=result.FeasibilityStatus.UNDETERMINED, - dual_status=result.FeasibilityStatus.UNDETERMINED, + primal_status=result.FeasibilityStatus.FEASIBLE, + dual_status=result.FeasibilityStatus.INFEASIBLE, + primal_or_dual_infeasible=False, ), objective_bounds=result.ObjectiveBounds( - primal_bound=math.inf, dual_bound=-math.inf + primal_bound=math.inf, + dual_bound=-math.inf, ), ), - solve_stats=result.SolveStats(solve_time=datetime.timedelta(minutes=2)), ) + self.assertFalse(res.bounded()) + + +def _make_undetermined_result_proto() -> result_pb2.SolveResultProto: + proto = result_pb2.SolveResultProto( + termination=result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_NO_SOLUTION_FOUND, + limit=result_pb2.LIMIT_TIME, + problem_status=result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_UNDETERMINED, + dual_status=result_pb2.FEASIBILITY_STATUS_UNDETERMINED, + primal_or_dual_infeasible=False, + ), + objective_bounds=result_pb2.ObjectiveBoundsProto( + primal_bound=math.inf, + dual_bound=-math.inf, + ), + ) + ) + proto.solve_stats.solve_time.FromTimedelta(datetime.timedelta(minutes=2)) + return proto + + +def _make_undetermined_solve_result() -> result.SolveResult: + return result.SolveResult( + termination=result.Termination( + reason=result.TerminationReason.NO_SOLUTION_FOUND, + limit=result.Limit.TIME, + problem_status=result.ProblemStatus( + primal_status=result.FeasibilityStatus.UNDETERMINED, + dual_status=result.FeasibilityStatus.UNDETERMINED, + ), + objective_bounds=result.ObjectiveBounds( + primal_bound=math.inf, dual_bound=-math.inf + ), + ), + solve_stats=result.SolveStats(solve_time=datetime.timedelta(minutes=2)), + ) class SolveResultTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_solve_result_gscip_output(self) -> None: - mod = model.Model(name="test_model") - res = _make_undetermined_solve_result() - res.gscip_specific_output = gscip_pb2.GScipOutput(status_detail="gscip_detail") - - proto = _make_undetermined_result_proto() - proto.gscip_output.status_detail = "gscip_detail" - - # proto -> result - actual_res = result.parse_solve_result(proto, mod) - self.assertIsNotNone(actual_res.gscip_specific_output) - assert actual_res.gscip_specific_output is not None - self.assertEqual("gscip_detail", actual_res.gscip_specific_output.status_detail) - self.assertIsNone(actual_res.pdlp_specific_output) - self.assertIsNone(actual_res.osqp_specific_output) - - # result -> proto - self.assert_protos_equiv(res.to_proto(), proto) - - def test_solve_result_osqp_output(self) -> None: - mod = model.Model(name="test_model") - res = _make_undetermined_solve_result() - res.osqp_specific_output = osqp_pb2.OsqpOutput( - initialized_underlying_solver=True - ) + def test_solve_result_gscip_output(self) -> None: + mod = model.Model(name="test_model") + res = _make_undetermined_solve_result() + res.gscip_specific_output = gscip_pb2.GScipOutput( + status_detail="gscip_detail" + ) - proto = _make_undetermined_result_proto() - proto.osqp_output.initialized_underlying_solver = True - - # proto -> result - actual_res = result.parse_solve_result(proto, mod) - self.assertIsNotNone(actual_res.osqp_specific_output) - assert actual_res.osqp_specific_output is not None - self.assertTrue(actual_res.osqp_specific_output.initialized_underlying_solver) - self.assertIsNone(actual_res.pdlp_specific_output) - self.assertIsNone(actual_res.gscip_specific_output) - - # result -> proto - self.assert_protos_equiv(res.to_proto(), proto) - - def test_solve_result_pdlp_output(self) -> None: - mod = model.Model(name="test_model") - res = _make_undetermined_solve_result() - res.pdlp_specific_output = result_pb2.SolveResultProto.PdlpOutput( - convergence_information=solve_log_pb2.ConvergenceInformation( - primal_objective=1.0 - ) - ) + proto = _make_undetermined_result_proto() + proto.gscip_output.status_detail = "gscip_detail" + + # proto -> result + actual_res = result.parse_solve_result(proto, mod) + self.assertIsNotNone(actual_res.gscip_specific_output) + assert actual_res.gscip_specific_output is not None + self.assertEqual( + "gscip_detail", actual_res.gscip_specific_output.status_detail + ) + self.assertIsNone(actual_res.pdlp_specific_output) + self.assertIsNone(actual_res.osqp_specific_output) - proto = _make_undetermined_result_proto() - proto.pdlp_output.convergence_information.primal_objective = 1.0 + # result -> proto + self.assert_protos_equiv(res.to_proto(), proto) - # proto -> result - actual_res = result.parse_solve_result(proto, mod) - self.assertIsNotNone(actual_res.pdlp_specific_output) - assert actual_res.pdlp_specific_output is not None - self.assertEqual( - actual_res.pdlp_specific_output.convergence_information.primal_objective, - 1.0, - ) - self.assertIsNone(actual_res.osqp_specific_output) - self.assertIsNone(actual_res.gscip_specific_output) + def test_solve_result_osqp_output(self) -> None: + mod = model.Model(name="test_model") + res = _make_undetermined_solve_result() + res.osqp_specific_output = osqp_pb2.OsqpOutput( + initialized_underlying_solver=True + ) + + proto = _make_undetermined_result_proto() + proto.osqp_output.initialized_underlying_solver = True + + # proto -> result + actual_res = result.parse_solve_result(proto, mod) + self.assertIsNotNone(actual_res.osqp_specific_output) + assert actual_res.osqp_specific_output is not None + self.assertTrue( + actual_res.osqp_specific_output.initialized_underlying_solver + ) + self.assertIsNone(actual_res.pdlp_specific_output) + self.assertIsNone(actual_res.gscip_specific_output) - # result -> proto - self.assert_protos_equiv(res.to_proto(), proto) + # result -> proto + self.assert_protos_equiv(res.to_proto(), proto) - def test_multiple_solver_specific_outputs_error(self) -> None: - res = _make_undetermined_solve_result() - res.gscip_specific_output = gscip_pb2.GScipOutput(status_detail="gscip_detail") - res.osqp_specific_output = osqp_pb2.OsqpOutput( - initialized_underlying_solver=False + def test_solve_result_pdlp_output(self) -> None: + mod = model.Model(name="test_model") + res = _make_undetermined_solve_result() + res.pdlp_specific_output = result_pb2.SolveResultProto.PdlpOutput( + convergence_information=solve_log_pb2.ConvergenceInformation( + primal_objective=1.0 ) - with self.assertRaisesRegex(ValueError, "solver specific output"): - res.to_proto() - - def test_solve_result_from_proto_missing_bounds_in_termination( - self, - ) -> None: - mod = model.Model(name="test_model") - proto = result_pb2.SolveResultProto( - termination=result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_INFEASIBLE, - detail="", - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - primal_or_dual_infeasible=False, - ), + ) + + proto = _make_undetermined_result_proto() + proto.pdlp_output.convergence_information.primal_objective = 1.0 + + # proto -> result + actual_res = result.parse_solve_result(proto, mod) + self.assertIsNotNone(actual_res.pdlp_specific_output) + assert actual_res.pdlp_specific_output is not None + self.assertEqual( + actual_res.pdlp_specific_output.convergence_information.primal_objective, + 1.0, + ) + self.assertIsNone(actual_res.osqp_specific_output) + self.assertIsNone(actual_res.gscip_specific_output) + + # result -> proto + self.assert_protos_equiv(res.to_proto(), proto) + + def test_multiple_solver_specific_outputs_error(self) -> None: + res = _make_undetermined_solve_result() + res.gscip_specific_output = gscip_pb2.GScipOutput( + status_detail="gscip_detail" + ) + res.osqp_specific_output = osqp_pb2.OsqpOutput( + initialized_underlying_solver=False + ) + with self.assertRaisesRegex(ValueError, "solver specific output"): + res.to_proto() + + def test_solve_result_from_proto_missing_bounds_in_termination( + self, + ) -> None: + mod = model.Model(name="test_model") + proto = result_pb2.SolveResultProto( + termination=result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_INFEASIBLE, + detail="", + problem_status=result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, + primal_or_dual_infeasible=False, ), - solve_stats=result_pb2.SolveStatsProto( - best_primal_bound=10.0, - best_dual_bound=20.0, + ), + solve_stats=result_pb2.SolveStatsProto( + best_primal_bound=10.0, + best_dual_bound=20.0, + ), + ) + res = result.parse_solve_result(proto, mod) + self.assertEqual(10.0, res.termination.objective_bounds.primal_bound) + self.assertEqual(20.0, res.termination.objective_bounds.dual_bound) + self.assertFalse(res.termination.problem_status.primal_or_dual_infeasible) + + def test_solve_result_from_proto_missing_status_in_termination( + self, + ) -> None: + mod = model.Model(name="test_model") + proto = result_pb2.SolveResultProto( + termination=result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_INFEASIBLE, + detail="", + objective_bounds=result_pb2.ObjectiveBoundsProto( + primal_bound=10.0, dual_bound=20.0 ), - ) - res = result.parse_solve_result(proto, mod) - self.assertEqual(10.0, res.termination.objective_bounds.primal_bound) - self.assertEqual(20.0, res.termination.objective_bounds.dual_bound) - self.assertFalse(res.termination.problem_status.primal_or_dual_infeasible) - - def test_solve_result_from_proto_missing_status_in_termination( - self, - ) -> None: - mod = model.Model(name="test_model") - proto = result_pb2.SolveResultProto( - termination=result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_INFEASIBLE, - detail="", - objective_bounds=result_pb2.ObjectiveBoundsProto( - primal_bound=10.0, dual_bound=20.0 - ), + ), + solve_stats=result_pb2.SolveStatsProto( + problem_status=result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, + primal_or_dual_infeasible=False, ), - solve_stats=result_pb2.SolveStatsProto( - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, - primal_or_dual_infeasible=False, + ), + ) + res = result.parse_solve_result(proto, mod) + self.assertEqual( + result.FeasibilityStatus.INFEASIBLE, + res.termination.problem_status.primal_status, + ) + self.assertEqual( + result.FeasibilityStatus.FEASIBLE, + res.termination.problem_status.dual_status, + ) + + def test_solve_result_from_proto_double_infeasible_multiple_rays( + self, + ) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + + proto = result_pb2.SolveResultProto( + termination=result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_INFEASIBLE, + detail="", + problem_status=result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, + primal_or_dual_infeasible=False, + ), + objective_bounds=result_pb2.ObjectiveBoundsProto( + primal_bound=math.inf, dual_bound=-math.inf + ), + ), + primal_rays=[ + solution_pb2.PrimalRayProto( + variable_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1], values=[2.0, 1.0] + ) + ), + solution_pb2.PrimalRayProto( + variable_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1], values=[3.0, 2.0] + ) + ), + ], + dual_rays=[ + solution_pb2.DualRayProto( + dual_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[4.0] + ), + reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1], values=[10.0, 11.0] ), ), - ) - res = result.parse_solve_result(proto, mod) - self.assertEqual( - result.FeasibilityStatus.INFEASIBLE, - res.termination.problem_status.primal_status, - ) - self.assertEqual( - result.FeasibilityStatus.FEASIBLE, - res.termination.problem_status.dual_status, - ) - - def test_solve_result_from_proto_double_infeasible_multiple_rays( - self, - ) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - - proto = result_pb2.SolveResultProto( - termination=result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_INFEASIBLE, - detail="", - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - primal_or_dual_infeasible=False, + solution_pb2.DualRayProto( + dual_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[5.0] ), - objective_bounds=result_pb2.ObjectiveBoundsProto( - primal_bound=math.inf, dual_bound=-math.inf + reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1], values=[11.0, 12.0] ), ), - primal_rays=[ - solution_pb2.PrimalRayProto( + ], + ) + proto.solve_stats.node_count = 10 + proto.solve_stats.problem_status.primal_status = ( + result_pb2.FEASIBILITY_STATUS_INFEASIBLE + ) + proto.solve_stats.problem_status.dual_status = ( + result_pb2.FEASIBILITY_STATUS_INFEASIBLE + ) + proto.solve_stats.problem_status.primal_or_dual_infeasible = False + proto.solve_stats.best_primal_bound = math.inf + proto.solve_stats.best_dual_bound = -math.inf + res = result.parse_solve_result(proto, mod) + + self.assertEqual( + result.TerminationReason.INFEASIBLE, res.termination.reason + ) + self.assertEqual("", res.termination.detail) + self.assertIsNone(res.termination.limit) + self.assertEmpty(res.solutions) + self.assertLen(res.primal_rays, 2) + self.assertLen(res.dual_rays, 2) + self.assertDictEqual({x: 2.0, y: 1.0}, res.primal_rays[0].variable_values) + self.assertDictEqual({x: 10.0, y: 11.0}, res.dual_rays[0].reduced_costs) + self.assertDictEqual({c: 4.0}, res.dual_rays[0].dual_values) + self.assertDictEqual({x: 3.0, y: 2.0}, res.primal_rays[1].variable_values) + self.assertDictEqual({x: 11.0, y: 12.0}, res.dual_rays[1].reduced_costs) + self.assertDictEqual({c: 5.0}, res.dual_rays[1].dual_values) + + # solve_stats + self.assertEqual(10, res.solve_stats.node_count) + self.assertEqual( + result.FeasibilityStatus.INFEASIBLE, + res.termination.problem_status.primal_status, + ) + self.assertEqual( + result.FeasibilityStatus.INFEASIBLE, + res.termination.problem_status.dual_status, + ) + self.assertFalse(res.termination.problem_status.primal_or_dual_infeasible) + self.assertEqual(math.inf, res.termination.objective_bounds.primal_bound) + self.assertEqual(-math.inf, res.termination.objective_bounds.dual_bound) + self.assertIsNone(res.gscip_specific_output) + + def test_solve_result_from_feasible_multiple_solutions(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + + proto = result_pb2.SolveResultProto( + termination=result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_OPTIMAL, + problem_status=result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, + ), + objective_bounds=result_pb2.ObjectiveBoundsProto( + primal_bound=10.0, dual_bound=20.0 + ), + ), + solutions=[ + solution_pb2.SolutionProto( + primal_solution=solution_pb2.PrimalSolutionProto( + objective_value=2.0, variable_values=sparse_containers_pb2.SparseDoubleVectorProto( ids=[0, 1], values=[2.0, 1.0] - ) - ), - solution_pb2.PrimalRayProto( - variable_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], values=[3.0, 2.0] - ) + ), + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, ), - ], - dual_rays=[ - solution_pb2.DualRayProto( + dual_solution=solution_pb2.DualSolutionProto( + objective_value=2.0, dual_values=sparse_containers_pb2.SparseDoubleVectorProto( ids=[0], values=[4.0] ), reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( ids=[0, 1], values=[10.0, 11.0] ), + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, ), - solution_pb2.DualRayProto( + basis=solution_pb2.BasisProto( + constraint_status=solution_pb2.SparseBasisStatusVector( + ids=[0], + values=[solution_pb2.BASIS_STATUS_AT_UPPER_BOUND], + ), + variable_status=solution_pb2.SparseBasisStatusVector( + ids=[0, 1], + values=[ + solution_pb2.BASIS_STATUS_BASIC, + solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, + ], + ), + basic_dual_feasibility=solution_pb2.SOLUTION_STATUS_FEASIBLE, + ), + ), + solution_pb2.SolutionProto( + primal_solution=solution_pb2.PrimalSolutionProto( + objective_value=3.0, + variable_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1], values=[3.0, 2.0] + ), + feasibility_status=solution_pb2.SOLUTION_STATUS_INFEASIBLE, + ) + ), + solution_pb2.SolutionProto( + dual_solution=solution_pb2.DualSolutionProto( + objective_value=3.0, dual_values=sparse_containers_pb2.SparseDoubleVectorProto( ids=[0], values=[5.0] ), reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( ids=[0, 1], values=[11.0, 12.0] ), - ), - ], - ) - proto.solve_stats.node_count = 10 - proto.solve_stats.problem_status.primal_status = ( - result_pb2.FEASIBILITY_STATUS_INFEASIBLE - ) - proto.solve_stats.problem_status.dual_status = ( - result_pb2.FEASIBILITY_STATUS_INFEASIBLE - ) - proto.solve_stats.problem_status.primal_or_dual_infeasible = False - proto.solve_stats.best_primal_bound = math.inf - proto.solve_stats.best_dual_bound = -math.inf - res = result.parse_solve_result(proto, mod) - - self.assertEqual(result.TerminationReason.INFEASIBLE, res.termination.reason) - self.assertEqual("", res.termination.detail) - self.assertIsNone(res.termination.limit) - self.assertEmpty(res.solutions) - self.assertLen(res.primal_rays, 2) - self.assertLen(res.dual_rays, 2) - self.assertDictEqual({x: 2.0, y: 1.0}, res.primal_rays[0].variable_values) - self.assertDictEqual({x: 10.0, y: 11.0}, res.dual_rays[0].reduced_costs) - self.assertDictEqual({c: 4.0}, res.dual_rays[0].dual_values) - self.assertDictEqual({x: 3.0, y: 2.0}, res.primal_rays[1].variable_values) - self.assertDictEqual({x: 11.0, y: 12.0}, res.dual_rays[1].reduced_costs) - self.assertDictEqual({c: 5.0}, res.dual_rays[1].dual_values) - - # solve_stats - self.assertEqual(10, res.solve_stats.node_count) - self.assertEqual( - result.FeasibilityStatus.INFEASIBLE, - res.termination.problem_status.primal_status, - ) - self.assertEqual( - result.FeasibilityStatus.INFEASIBLE, - res.termination.problem_status.dual_status, - ) - self.assertFalse(res.termination.problem_status.primal_or_dual_infeasible) - self.assertEqual(math.inf, res.termination.objective_bounds.primal_bound) - self.assertEqual(-math.inf, res.termination.objective_bounds.dual_bound) - self.assertIsNone(res.gscip_specific_output) - - def test_solve_result_from_feasible_multiple_solutions(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - - proto = result_pb2.SolveResultProto( - termination=result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_OPTIMAL, - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, - ), - objective_bounds=result_pb2.ObjectiveBoundsProto( - primal_bound=10.0, dual_bound=20.0 - ), + feasibility_status=solution_pb2.SOLUTION_STATUS_INFEASIBLE, + ) ), - solutions=[ - solution_pb2.SolutionProto( - primal_solution=solution_pb2.PrimalSolutionProto( - objective_value=2.0, - variable_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], values=[2.0, 1.0] - ), - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ), - dual_solution=solution_pb2.DualSolutionProto( - objective_value=2.0, - dual_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[4.0] - ), - reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], values=[10.0, 11.0] - ), - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + solution_pb2.SolutionProto( + basis=solution_pb2.BasisProto( + constraint_status=solution_pb2.SparseBasisStatusVector( + ids=[0], values=[solution_pb2.BASIS_STATUS_BASIC] ), - basis=solution_pb2.BasisProto( - constraint_status=solution_pb2.SparseBasisStatusVector( - ids=[0], - values=[solution_pb2.BASIS_STATUS_AT_UPPER_BOUND], - ), - variable_status=solution_pb2.SparseBasisStatusVector( - ids=[0, 1], - values=[ - solution_pb2.BASIS_STATUS_BASIC, - solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, - ], - ), - basic_dual_feasibility=solution_pb2.SOLUTION_STATUS_FEASIBLE, + variable_status=solution_pb2.SparseBasisStatusVector( + ids=[0, 1], + values=[ + solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, + solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, + ], ), - ), - solution_pb2.SolutionProto( - primal_solution=solution_pb2.PrimalSolutionProto( - objective_value=3.0, - variable_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], values=[3.0, 2.0] - ), - feasibility_status=solution_pb2.SOLUTION_STATUS_INFEASIBLE, - ) - ), - solution_pb2.SolutionProto( - dual_solution=solution_pb2.DualSolutionProto( - objective_value=3.0, - dual_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[5.0] - ), - reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], values=[11.0, 12.0] - ), - feasibility_status=solution_pb2.SOLUTION_STATUS_INFEASIBLE, - ) - ), - solution_pb2.SolutionProto( - basis=solution_pb2.BasisProto( - constraint_status=solution_pb2.SparseBasisStatusVector( - ids=[0], values=[solution_pb2.BASIS_STATUS_BASIC] - ), - variable_status=solution_pb2.SparseBasisStatusVector( - ids=[0, 1], - values=[ - solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, - solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, - ], - ), - basic_dual_feasibility=solution_pb2.SOLUTION_STATUS_INFEASIBLE, - ) - ), - ], - ) - - proto.solve_stats.node_count = 10 - proto.solve_stats.problem_status.primal_status = ( - result_pb2.FEASIBILITY_STATUS_FEASIBLE - ) - proto.solve_stats.problem_status.dual_status = ( - result_pb2.FEASIBILITY_STATUS_FEASIBLE - ) - proto.solve_stats.problem_status.primal_or_dual_infeasible = False - proto.solve_stats.best_primal_bound = 10 - proto.solve_stats.best_dual_bound = 10 - res = result.parse_solve_result(proto, mod) - - self.assertEqual(result.TerminationReason.OPTIMAL, res.termination.reason) - self.assertEqual("", res.termination.detail) - self.assertIsNone(res.termination.limit) - self.assertLen(res.solutions, 4) - self.assertEmpty(res.primal_rays) - self.assertEmpty(res.dual_rays) - - # Solution 0 - assert ( - res.solutions[0].primal_solution is not None - and res.solutions[0].dual_solution is not None - and res.solutions[0].basis is not None - ) - self.assertEqual(2.0, res.solutions[0].primal_solution.objective_value) - self.assertDictEqual( - {x: 2.0, y: 1.0}, res.solutions[0].primal_solution.variable_values - ) - self.assertEqual( - solution.SolutionStatus.FEASIBLE, - res.solutions[0].primal_solution.feasibility_status, - ) - self.assertEqual(2.0, res.solutions[0].dual_solution.objective_value) - self.assertDictEqual( - {x: 10.0, y: 11.0}, res.solutions[0].dual_solution.reduced_costs - ) - self.assertDictEqual({c: 4.0}, res.solutions[0].dual_solution.dual_values) - self.assertEqual( - solution.SolutionStatus.FEASIBLE, - res.solutions[0].dual_solution.feasibility_status, - ) - self.assertDictEqual( - {x: solution.BasisStatus.BASIC, y: solution.BasisStatus.AT_LOWER_BOUND}, - res.solutions[0].basis.variable_status, - ) - self.assertDictEqual( - {c: solution.BasisStatus.AT_UPPER_BOUND}, - res.solutions[0].basis.constraint_status, - ) - self.assertEqual( - solution.SolutionStatus.FEASIBLE, - res.solutions[0].basis.basic_dual_feasibility, - ) - - # Solution 1 - assert res.solutions[1].primal_solution is not None - self.assertEqual(3.0, res.solutions[1].primal_solution.objective_value) - self.assertDictEqual( - {x: 3.0, y: 2.0}, res.solutions[1].primal_solution.variable_values - ) - self.assertEqual( - solution.SolutionStatus.INFEASIBLE, - res.solutions[1].primal_solution.feasibility_status, - ) - self.assertIsNone(res.solutions[1].dual_solution) - self.assertIsNone(res.solutions[1].basis) - - # Solution 2 - assert res.solutions[2].dual_solution is not None - self.assertIsNone(res.solutions[2].primal_solution) - self.assertEqual(3.0, res.solutions[2].dual_solution.objective_value) - self.assertDictEqual( - {x: 11.0, y: 12.0}, res.solutions[2].dual_solution.reduced_costs - ) - self.assertDictEqual({c: 5.0}, res.solutions[2].dual_solution.dual_values) - self.assertEqual( - solution.SolutionStatus.INFEASIBLE, - res.solutions[2].dual_solution.feasibility_status, - ) - self.assertIsNone(res.solutions[2].basis) - - # Solution 3 - assert res.solutions[3].basis is not None - self.assertIsNone(res.solutions[3].primal_solution) - self.assertIsNone(res.solutions[3].dual_solution) - self.assertDictEqual( - { - x: solution.BasisStatus.AT_LOWER_BOUND, - y: solution.BasisStatus.AT_UPPER_BOUND, - }, - res.solutions[3].basis.variable_status, - ) - self.assertDictEqual( - {c: solution.BasisStatus.BASIC}, - res.solutions[3].basis.constraint_status, - ) - self.assertEqual( - solution.SolutionStatus.INFEASIBLE, - res.solutions[3].basis.basic_dual_feasibility, - ) + basic_dual_feasibility=solution_pb2.SOLUTION_STATUS_INFEASIBLE, + ) + ), + ], + ) - # solve_stats - self.assertEqual(10, res.solve_stats.node_count) - self.assertEqual( - result.FeasibilityStatus.FEASIBLE, - res.termination.problem_status.primal_status, - ) - self.assertEqual( - result.FeasibilityStatus.FEASIBLE, - res.termination.problem_status.dual_status, - ) - self.assertFalse(res.termination.problem_status.primal_or_dual_infeasible) - self.assertEqual(10, res.termination.objective_bounds.primal_bound) - self.assertEqual(20, res.termination.objective_bounds.dual_bound) - self.assertIsNone(res.gscip_specific_output) + proto.solve_stats.node_count = 10 + proto.solve_stats.problem_status.primal_status = ( + result_pb2.FEASIBILITY_STATUS_FEASIBLE + ) + proto.solve_stats.problem_status.dual_status = ( + result_pb2.FEASIBILITY_STATUS_FEASIBLE + ) + proto.solve_stats.problem_status.primal_or_dual_infeasible = False + proto.solve_stats.best_primal_bound = 10 + proto.solve_stats.best_dual_bound = 10 + res = result.parse_solve_result(proto, mod) + + self.assertEqual(result.TerminationReason.OPTIMAL, res.termination.reason) + self.assertEqual("", res.termination.detail) + self.assertIsNone(res.termination.limit) + self.assertLen(res.solutions, 4) + self.assertEmpty(res.primal_rays) + self.assertEmpty(res.dual_rays) + + # Solution 0 + assert ( + res.solutions[0].primal_solution is not None + and res.solutions[0].dual_solution is not None + and res.solutions[0].basis is not None + ) + self.assertEqual(2.0, res.solutions[0].primal_solution.objective_value) + self.assertDictEqual( + {x: 2.0, y: 1.0}, res.solutions[0].primal_solution.variable_values + ) + self.assertEqual( + solution.SolutionStatus.FEASIBLE, + res.solutions[0].primal_solution.feasibility_status, + ) + self.assertEqual(2.0, res.solutions[0].dual_solution.objective_value) + self.assertDictEqual( + {x: 10.0, y: 11.0}, res.solutions[0].dual_solution.reduced_costs + ) + self.assertDictEqual({c: 4.0}, res.solutions[0].dual_solution.dual_values) + self.assertEqual( + solution.SolutionStatus.FEASIBLE, + res.solutions[0].dual_solution.feasibility_status, + ) + self.assertDictEqual( + {x: solution.BasisStatus.BASIC, y: solution.BasisStatus.AT_LOWER_BOUND}, + res.solutions[0].basis.variable_status, + ) + self.assertDictEqual( + {c: solution.BasisStatus.AT_UPPER_BOUND}, + res.solutions[0].basis.constraint_status, + ) + self.assertEqual( + solution.SolutionStatus.FEASIBLE, + res.solutions[0].basis.basic_dual_feasibility, + ) - def test_to_proto_round_trip(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + # Solution 1 + assert res.solutions[1].primal_solution is not None + self.assertEqual(3.0, res.solutions[1].primal_solution.objective_value) + self.assertDictEqual( + {x: 3.0, y: 2.0}, res.solutions[1].primal_solution.variable_values + ) + self.assertEqual( + solution.SolutionStatus.INFEASIBLE, + res.solutions[1].primal_solution.feasibility_status, + ) + self.assertIsNone(res.solutions[1].dual_solution) + self.assertIsNone(res.solutions[1].basis) + + # Solution 2 + assert res.solutions[2].dual_solution is not None + self.assertIsNone(res.solutions[2].primal_solution) + self.assertEqual(3.0, res.solutions[2].dual_solution.objective_value) + self.assertDictEqual( + {x: 11.0, y: 12.0}, res.solutions[2].dual_solution.reduced_costs + ) + self.assertDictEqual({c: 5.0}, res.solutions[2].dual_solution.dual_values) + self.assertEqual( + solution.SolutionStatus.INFEASIBLE, + res.solutions[2].dual_solution.feasibility_status, + ) + self.assertIsNone(res.solutions[2].basis) + + # Solution 3 + assert res.solutions[3].basis is not None + self.assertIsNone(res.solutions[3].primal_solution) + self.assertIsNone(res.solutions[3].dual_solution) + self.assertDictEqual( + { + x: solution.BasisStatus.AT_LOWER_BOUND, + y: solution.BasisStatus.AT_UPPER_BOUND, + }, + res.solutions[3].basis.variable_status, + ) + self.assertDictEqual( + {c: solution.BasisStatus.BASIC}, + res.solutions[3].basis.constraint_status, + ) + self.assertEqual( + solution.SolutionStatus.INFEASIBLE, + res.solutions[3].basis.basic_dual_feasibility, + ) - s = solution.Solution( - primal_solution=solution.PrimalSolution( - variable_values={x: 1.0}, - objective_value=2.0, - feasibility_status=solution.SolutionStatus.FEASIBLE, - ) + # solve_stats + self.assertEqual(10, res.solve_stats.node_count) + self.assertEqual( + result.FeasibilityStatus.FEASIBLE, + res.termination.problem_status.primal_status, + ) + self.assertEqual( + result.FeasibilityStatus.FEASIBLE, + res.termination.problem_status.dual_status, + ) + self.assertFalse(res.termination.problem_status.primal_or_dual_infeasible) + self.assertEqual(10, res.termination.objective_bounds.primal_bound) + self.assertEqual(20, res.termination.objective_bounds.dual_bound) + self.assertIsNone(res.gscip_specific_output) + + def test_to_proto_round_trip(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + + s = solution.Solution( + primal_solution=solution.PrimalSolution( + variable_values={x: 1.0}, + objective_value=2.0, + feasibility_status=solution.SolutionStatus.FEASIBLE, ) - r = result.SolveResult( - termination=result.Termination( - reason=result.TerminationReason.FEASIBLE, - limit=result.Limit.TIME, - problem_status=result.ProblemStatus( - primal_status=result.FeasibilityStatus.FEASIBLE, - dual_status=result.FeasibilityStatus.UNDETERMINED, - ), + ) + r = result.SolveResult( + termination=result.Termination( + reason=result.TerminationReason.FEASIBLE, + limit=result.Limit.TIME, + problem_status=result.ProblemStatus( + primal_status=result.FeasibilityStatus.FEASIBLE, + dual_status=result.FeasibilityStatus.UNDETERMINED, ), - solve_stats=result.SolveStats( - node_count=3, solve_time=datetime.timedelta(seconds=4) + ), + solve_stats=result.SolveStats( + node_count=3, solve_time=datetime.timedelta(seconds=4) + ), + solutions=[s], + primal_rays=[solution.PrimalRay(variable_values={x: 4.0})], + dual_rays=[ + solution.DualRay(reduced_costs={x: 5.0}, dual_values={c: 6.0}) + ], + ) + + s_proto = solution_pb2.SolutionProto( + primal_solution=solution_pb2.PrimalSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + variable_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[1.0] ), - solutions=[s], - primal_rays=[solution.PrimalRay(variable_values={x: 4.0})], - dual_rays=[solution.DualRay(reduced_costs={x: 5.0}, dual_values={c: 6.0})], ) - - s_proto = solution_pb2.SolutionProto( - primal_solution=solution_pb2.PrimalSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + ) + r_proto = result_pb2.SolveResultProto( + termination=result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_FEASIBLE, + limit=result_pb2.LIMIT_TIME, + problem_status=result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_UNDETERMINED, + ), + ), + solve_stats=result_pb2.SolveStatsProto(node_count=3), + solutions=[s_proto], + primal_rays=[ + solution_pb2.PrimalRayProto( variable_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[1.0] - ), + ids=[0], values=[4.0] + ) ) - ) - r_proto = result_pb2.SolveResultProto( - termination=result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_FEASIBLE, - limit=result_pb2.LIMIT_TIME, - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_UNDETERMINED, + ], + dual_rays=[ + solution_pb2.DualRayProto( + reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[5.0] + ), + dual_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0], values=[6.0] ), + ) + ], + ) + r_proto.solve_stats.solve_time.FromTimedelta(datetime.timedelta(seconds=4)) + + self.assert_protos_equiv(r.to_proto(), r_proto) + self.assertEqual(result.parse_solve_result(r_proto, mod), r) + + def test_solution_validation(self) -> None: + mod = model.Model(name="test_model") + proto = result_pb2.SolveResultProto( + termination=result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_OPTIMAL, + problem_status=result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, ), - solve_stats=result_pb2.SolveStatsProto(node_count=3), - solutions=[s_proto], - primal_rays=[ - solution_pb2.PrimalRayProto( + ), + solutions=[ + solution_pb2.SolutionProto( + primal_solution=solution_pb2.PrimalSolutionProto( variable_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[4.0] - ) - ) - ], - dual_rays=[ - solution_pb2.DualRayProto( - reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[5.0] - ), - dual_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0], values=[6.0] + ids=[2], values=[4.0] ), + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, ) - ], - ) - r_proto.solve_stats.solve_time.FromTimedelta(datetime.timedelta(seconds=4)) - - self.assert_protos_equiv(r.to_proto(), r_proto) - self.assertEqual(result.parse_solve_result(r_proto, mod), r) - - def test_solution_validation(self) -> None: - mod = model.Model(name="test_model") - proto = result_pb2.SolveResultProto( - termination=result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_OPTIMAL, - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, - ), - ), - solutions=[ - solution_pb2.SolutionProto( - primal_solution=solution_pb2.PrimalSolutionProto( - variable_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[2], values=[4.0] - ), - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) - ) - ], - ) - res = result.parse_solve_result(proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - self.assertLen(res.solutions, 1) - # TODO: b/215588365 - make a local variable so pytype is happy - primal = res.solutions[0].primal_solution - self.assertIsNotNone(primal) - self.assertDictEqual(primal.variable_values, {bad_var: 4.0}) - with self.assertRaises(KeyError): - result.parse_solve_result(proto, mod, validate=True) - - def test_primal_ray_validation(self) -> None: - mod = model.Model(name="test_model") - proto = result_pb2.SolveResultProto( - termination=result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_UNBOUNDED, - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - ), + ) + ], + ) + res = result.parse_solve_result(proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + self.assertLen(res.solutions, 1) + # TODO: b/215588365 - make a local variable so pytype is happy + primal = res.solutions[0].primal_solution + self.assertIsNotNone(primal) + self.assertDictEqual(primal.variable_values, {bad_var: 4.0}) + with self.assertRaises(KeyError): + result.parse_solve_result(proto, mod, validate=True) + + def test_primal_ray_validation(self) -> None: + mod = model.Model(name="test_model") + proto = result_pb2.SolveResultProto( + termination=result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_UNBOUNDED, + problem_status=result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, ), - primal_rays=[ - solution_pb2.PrimalRayProto( - variable_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[2], values=[4.0] - ) + ), + primal_rays=[ + solution_pb2.PrimalRayProto( + variable_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[2], values=[4.0] ) - ], - ) - res = result.parse_solve_result(proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - self.assertLen(res.primal_rays, 1) - self.assertDictEqual(res.primal_rays[0].variable_values, {bad_var: 4.0}) - with self.assertRaises(KeyError): - result.parse_solve_result(proto, mod, validate=True) - - def test_dual_ray_validation(self) -> None: - mod = model.Model(name="test_model") - proto = result_pb2.SolveResultProto( - termination=result_pb2.TerminationProto( - reason=result_pb2.TERMINATION_REASON_INFEASIBLE, - problem_status=result_pb2.ProblemStatusProto( - primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, - dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, - ), + ) + ], + ) + res = result.parse_solve_result(proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + self.assertLen(res.primal_rays, 1) + self.assertDictEqual(res.primal_rays[0].variable_values, {bad_var: 4.0}) + with self.assertRaises(KeyError): + result.parse_solve_result(proto, mod, validate=True) + + def test_dual_ray_validation(self) -> None: + mod = model.Model(name="test_model") + proto = result_pb2.SolveResultProto( + termination=result_pb2.TerminationProto( + reason=result_pb2.TERMINATION_REASON_INFEASIBLE, + problem_status=result_pb2.ProblemStatusProto( + primal_status=result_pb2.FEASIBILITY_STATUS_INFEASIBLE, + dual_status=result_pb2.FEASIBILITY_STATUS_FEASIBLE, ), - dual_rays=[ - solution_pb2.DualRayProto( - reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[2], values=[4.0] - ) + ), + dual_rays=[ + solution_pb2.DualRayProto( + reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[2], values=[4.0] ) - ], - ) - res = result.parse_solve_result(proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - self.assertLen(res.dual_rays, 1) - self.assertDictEqual(res.dual_rays[0].reduced_costs, {bad_var: 4.0}) - with self.assertRaises(KeyError): - result.parse_solve_result(proto, mod, validate=True) + ) + ], + ) + res = result.parse_solve_result(proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + self.assertLen(res.dual_rays, 1) + self.assertDictEqual(res.dual_rays[0].reduced_costs, {bad_var: 4.0}) + with self.assertRaises(KeyError): + result.parse_solve_result(proto, mod, validate=True) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/solution.py b/ortools/math_opt/python/solution.py index 54966a74711..5d28b3c3f75 100644 --- a/ortools/math_opt/python/solution.py +++ b/ortools/math_opt/python/solution.py @@ -27,107 +27,111 @@ @enum.unique class BasisStatus(enum.Enum): - """Status of a variable/constraint in a LP basis. - - Attributes: - FREE: The variable/constraint is free (it has no finite bounds). - AT_LOWER_BOUND: The variable/constraint is at its lower bound (which must be - finite). - AT_UPPER_BOUND: The variable/constraint is at its upper bound (which must be - finite). - FIXED_VALUE: The variable/constraint has identical finite lower and upper - bounds. - BASIC: The variable/constraint is basic. - """ - - FREE = solution_pb2.BASIS_STATUS_FREE - AT_LOWER_BOUND = solution_pb2.BASIS_STATUS_AT_LOWER_BOUND - AT_UPPER_BOUND = solution_pb2.BASIS_STATUS_AT_UPPER_BOUND - FIXED_VALUE = solution_pb2.BASIS_STATUS_FIXED_VALUE - BASIC = solution_pb2.BASIS_STATUS_BASIC + """Status of a variable/constraint in a LP basis. + + Attributes: + FREE: The variable/constraint is free (it has no finite bounds). + AT_LOWER_BOUND: The variable/constraint is at its lower bound (which must be + finite). + AT_UPPER_BOUND: The variable/constraint is at its upper bound (which must be + finite). + FIXED_VALUE: The variable/constraint has identical finite lower and upper + bounds. + BASIC: The variable/constraint is basic. + """ + + FREE = solution_pb2.BASIS_STATUS_FREE + AT_LOWER_BOUND = solution_pb2.BASIS_STATUS_AT_LOWER_BOUND + AT_UPPER_BOUND = solution_pb2.BASIS_STATUS_AT_UPPER_BOUND + FIXED_VALUE = solution_pb2.BASIS_STATUS_FIXED_VALUE + BASIC = solution_pb2.BASIS_STATUS_BASIC @enum.unique class SolutionStatus(enum.Enum): - """Feasibility of a primal or dual solution as claimed by the solver. + """Feasibility of a primal or dual solution as claimed by the solver. - Attributes: - UNDETERMINED: Solver does not claim a feasibility status. - FEASIBLE: Solver claims the solution is feasible. - INFEASIBLE: Solver claims the solution is infeasible. - """ + Attributes: + UNDETERMINED: Solver does not claim a feasibility status. + FEASIBLE: Solver claims the solution is feasible. + INFEASIBLE: Solver claims the solution is infeasible. + """ - UNDETERMINED = solution_pb2.SOLUTION_STATUS_UNDETERMINED - FEASIBLE = solution_pb2.SOLUTION_STATUS_FEASIBLE - INFEASIBLE = solution_pb2.SOLUTION_STATUS_INFEASIBLE + UNDETERMINED = solution_pb2.SOLUTION_STATUS_UNDETERMINED + FEASIBLE = solution_pb2.SOLUTION_STATUS_FEASIBLE + INFEASIBLE = solution_pb2.SOLUTION_STATUS_INFEASIBLE def parse_optional_solution_status( proto: solution_pb2.SolutionStatusProto, ) -> Optional[SolutionStatus]: - """Converts a proto SolutionStatus to an optional Python SolutionStatus.""" - return ( - None - if proto == solution_pb2.SOLUTION_STATUS_UNSPECIFIED - else SolutionStatus(proto) - ) + """Converts a proto SolutionStatus to an optional Python SolutionStatus.""" + return ( + None + if proto == solution_pb2.SOLUTION_STATUS_UNSPECIFIED + else SolutionStatus(proto) + ) def optional_solution_status_to_proto( status: Optional[SolutionStatus], ) -> solution_pb2.SolutionStatusProto: - """Converts an optional Python SolutionStatus to a proto SolutionStatus.""" - return solution_pb2.SOLUTION_STATUS_UNSPECIFIED if status is None else status.value + """Converts an optional Python SolutionStatus to a proto SolutionStatus.""" + return ( + solution_pb2.SOLUTION_STATUS_UNSPECIFIED + if status is None + else status.value + ) @dataclasses.dataclass class PrimalSolution: - """A solution to the optimization problem in a Model. - - E.g. consider a simple linear program: - min c * x - s.t. A * x >= b - x >= 0. - A primal solution is assignment values to x. It is feasible if it satisfies - A * x >= b and x >= 0 from above. In the class PrimalSolution variable_values - is x and objective_value is c * x. - - For the general case of a MathOpt optimization model, see go/mathopt-solutions - for details. - - Attributes: - variable_values: The value assigned for each Variable in the model. - objective_value: The value of the objective value at this solution. This - value may not be always populated. - auxiliary_objective_values: Set only for multi objective problems, the - objective value for each auxiliary objective, as computed by the solver. - This value will not always be populated. - feasibility_status: The feasibility of the solution as claimed by the - solver. - """ - - variable_values: Dict[variables.Variable, float] = dataclasses.field( - default_factory=dict - ) - objective_value: float = 0.0 - auxiliary_objective_values: Dict[objectives.AuxiliaryObjective, float] = ( - dataclasses.field(default_factory=dict) + """A solution to the optimization problem in a Model. + + E.g. consider a simple linear program: + min c * x + s.t. A * x >= b + x >= 0. + A primal solution is assignment values to x. It is feasible if it satisfies + A * x >= b and x >= 0 from above. In the class PrimalSolution variable_values + is x and objective_value is c * x. + + For the general case of a MathOpt optimization model, see go/mathopt-solutions + for details. + + Attributes: + variable_values: The value assigned for each Variable in the model. + objective_value: The value of the objective value at this solution. This + value may not be always populated. + auxiliary_objective_values: Set only for multi objective problems, the + objective value for each auxiliary objective, as computed by the solver. + This value will not always be populated. + feasibility_status: The feasibility of the solution as claimed by the + solver. + """ + + variable_values: Dict[variables.Variable, float] = dataclasses.field( + default_factory=dict + ) + objective_value: float = 0.0 + auxiliary_objective_values: Dict[objectives.AuxiliaryObjective, float] = ( + dataclasses.field(default_factory=dict) + ) + feasibility_status: SolutionStatus = SolutionStatus.UNDETERMINED + + def to_proto(self) -> solution_pb2.PrimalSolutionProto: + """Returns an equivalent proto for a primal solution.""" + return solution_pb2.PrimalSolutionProto( + variable_values=sparse_containers.to_sparse_double_vector_proto( + self.variable_values + ), + objective_value=self.objective_value, + auxiliary_objective_values={ + obj.id: obj_value + for obj, obj_value in self.auxiliary_objective_values.items() + }, + feasibility_status=self.feasibility_status.value, ) - feasibility_status: SolutionStatus = SolutionStatus.UNDETERMINED - - def to_proto(self) -> solution_pb2.PrimalSolutionProto: - """Returns an equivalent proto for a primal solution.""" - return solution_pb2.PrimalSolutionProto( - variable_values=sparse_containers.to_sparse_double_vector_proto( - self.variable_values - ), - objective_value=self.objective_value, - auxiliary_objective_values={ - obj.id: obj_value - for obj, obj_value in self.auxiliary_objective_values.items() - }, - feasibility_status=self.feasibility_status.value, - ) def parse_primal_solution( @@ -136,62 +140,64 @@ def parse_primal_solution( *, validate: bool = True, ) -> PrimalSolution: - """Returns an equivalent PrimalSolution from the input proto.""" - result = PrimalSolution() - result.objective_value = proto.objective_value - for aux_id, obj_value in proto.auxiliary_objective_values.items(): - result.auxiliary_objective_values[ - mod.get_auxiliary_objective(aux_id, validate=validate) - ] = obj_value - result.variable_values = sparse_containers.parse_variable_map( - proto.variable_values, mod, validate=validate + """Returns an equivalent PrimalSolution from the input proto.""" + result = PrimalSolution() + result.objective_value = proto.objective_value + for aux_id, obj_value in proto.auxiliary_objective_values.items(): + result.auxiliary_objective_values[ + mod.get_auxiliary_objective(aux_id, validate=validate) + ] = obj_value + result.variable_values = sparse_containers.parse_variable_map( + proto.variable_values, mod, validate=validate + ) + status_proto = proto.feasibility_status + if status_proto == solution_pb2.SOLUTION_STATUS_UNSPECIFIED: + raise ValueError( + "Primal solution feasibility status should not be UNSPECIFIED" ) - status_proto = proto.feasibility_status - if status_proto == solution_pb2.SOLUTION_STATUS_UNSPECIFIED: - raise ValueError("Primal solution feasibility status should not be UNSPECIFIED") - result.feasibility_status = SolutionStatus(status_proto) - return result + result.feasibility_status = SolutionStatus(status_proto) + return result @dataclasses.dataclass class PrimalRay: - """A direction of unbounded objective improvement in an optimization Model. - - Equivalently, a certificate of infeasibility for the dual of the optimization - problem. - - E.g. consider a simple linear program: - min c * x - s.t. A * x >= b - x >= 0. - A primal ray is an x that satisfies: - c * x < 0 - A * x >= 0 - x >= 0. - Observe that given a feasible solution, any positive multiple of the primal - ray plus that solution is still feasible, and gives a better objective - value. A primal ray also proves the dual optimization problem infeasible. - - In the class PrimalRay, variable_values is this x. - - For the general case of a MathOpt optimization model, see - go/mathopt-solutions for details. - - Attributes: - variable_values: The value assigned for each Variable in the model. - """ - - variable_values: Dict[variables.Variable, float] = dataclasses.field( - default_factory=dict - ) + """A direction of unbounded objective improvement in an optimization Model. + + Equivalently, a certificate of infeasibility for the dual of the optimization + problem. + + E.g. consider a simple linear program: + min c * x + s.t. A * x >= b + x >= 0. + A primal ray is an x that satisfies: + c * x < 0 + A * x >= 0 + x >= 0. + Observe that given a feasible solution, any positive multiple of the primal + ray plus that solution is still feasible, and gives a better objective + value. A primal ray also proves the dual optimization problem infeasible. + + In the class PrimalRay, variable_values is this x. + + For the general case of a MathOpt optimization model, see + go/mathopt-solutions for details. - def to_proto(self) -> solution_pb2.PrimalRayProto: - """Returns an equivalent proto to this PrimalRay.""" - return solution_pb2.PrimalRayProto( - variable_values=sparse_containers.to_sparse_double_vector_proto( - self.variable_values - ) + Attributes: + variable_values: The value assigned for each Variable in the model. + """ + + variable_values: Dict[variables.Variable, float] = dataclasses.field( + default_factory=dict + ) + + def to_proto(self) -> solution_pb2.PrimalRayProto: + """Returns an equivalent proto to this PrimalRay.""" + return solution_pb2.PrimalRayProto( + variable_values=sparse_containers.to_sparse_double_vector_proto( + self.variable_values ) + ) def parse_primal_ray( @@ -200,69 +206,69 @@ def parse_primal_ray( *, validate: bool = True, ) -> PrimalRay: - """Returns an equivalent PrimalRay from the input proto.""" - result = PrimalRay() - result.variable_values = sparse_containers.parse_variable_map( - proto.variable_values, mod, validate=validate - ) - return result + """Returns an equivalent PrimalRay from the input proto.""" + result = PrimalRay() + result.variable_values = sparse_containers.parse_variable_map( + proto.variable_values, mod, validate=validate + ) + return result @dataclasses.dataclass class DualSolution: - """A solution to the dual of the optimization problem given by a Model. - - E.g. consider the primal dual pair linear program pair: - (Primal)            (Dual) - min c * x             max b * y - s.t. A * x >= b       s.t. y * A + r = c - x >= 0              y, r >= 0. - The dual solution is the pair (y, r). It is feasible if it satisfies the - constraints from (Dual) above. - - Below, y is dual_values, r is reduced_costs, and b * y is objective_value. - - For the general case, see go/mathopt-solutions and go/mathopt-dual (and note - that the dual objective depends on r in the general case). - - Attributes: - dual_values: The value assigned for each LinearConstraint in the model. - quadratic_dual_values: The value assigned for each QuadraticConstraint in - the model. - reduced_costs: The value assigned for each Variable in the model. - objective_value: The value of the dual objective value at this solution. - This value may not be always populated. - feasibility_status: The feasibility of the solution as claimed by the - solver. - """ - - dual_values: Dict[linear_constraints.LinearConstraint, float] = dataclasses.field( - default_factory=dict - ) - quadratic_dual_values: Dict[quadratic_constraints.QuadraticConstraint, float] = ( - dataclasses.field(default_factory=dict) - ) - reduced_costs: Dict[variables.Variable, float] = dataclasses.field( - default_factory=dict + """A solution to the dual of the optimization problem given by a Model. + + E.g. consider the primal dual pair linear program pair: + (Primal)            (Dual) + min c * x             max b * y + s.t. A * x >= b       s.t. y * A + r = c + x >= 0              y, r >= 0. + The dual solution is the pair (y, r). It is feasible if it satisfies the + constraints from (Dual) above. + + Below, y is dual_values, r is reduced_costs, and b * y is objective_value. + + For the general case, see go/mathopt-solutions and go/mathopt-dual (and note + that the dual objective depends on r in the general case). + + Attributes: + dual_values: The value assigned for each LinearConstraint in the model. + quadratic_dual_values: The value assigned for each QuadraticConstraint in + the model. + reduced_costs: The value assigned for each Variable in the model. + objective_value: The value of the dual objective value at this solution. + This value may not be always populated. + feasibility_status: The feasibility of the solution as claimed by the + solver. + """ + + dual_values: Dict[linear_constraints.LinearConstraint, float] = ( + dataclasses.field(default_factory=dict) + ) + quadratic_dual_values: Dict[ + quadratic_constraints.QuadraticConstraint, float + ] = dataclasses.field(default_factory=dict) + reduced_costs: Dict[variables.Variable, float] = dataclasses.field( + default_factory=dict + ) + objective_value: Optional[float] = None + feasibility_status: SolutionStatus = SolutionStatus.UNDETERMINED + + def to_proto(self) -> solution_pb2.DualSolutionProto: + """Returns an equivalent proto for a dual solution.""" + return solution_pb2.DualSolutionProto( + dual_values=sparse_containers.to_sparse_double_vector_proto( + self.dual_values + ), + reduced_costs=sparse_containers.to_sparse_double_vector_proto( + self.reduced_costs + ), + quadratic_dual_values=sparse_containers.to_sparse_double_vector_proto( + self.quadratic_dual_values + ), + objective_value=self.objective_value, + feasibility_status=self.feasibility_status.value, ) - objective_value: Optional[float] = None - feasibility_status: SolutionStatus = SolutionStatus.UNDETERMINED - - def to_proto(self) -> solution_pb2.DualSolutionProto: - """Returns an equivalent proto for a dual solution.""" - return solution_pb2.DualSolutionProto( - dual_values=sparse_containers.to_sparse_double_vector_proto( - self.dual_values - ), - reduced_costs=sparse_containers.to_sparse_double_vector_proto( - self.reduced_costs - ), - quadratic_dual_values=sparse_containers.to_sparse_double_vector_proto( - self.quadratic_dual_values - ), - objective_value=self.objective_value, - feasibility_status=self.feasibility_status.value, - ) def parse_dual_solution( @@ -271,183 +277,189 @@ def parse_dual_solution( *, validate: bool = True, ) -> DualSolution: - """Returns an equivalent DualSolution from the input proto.""" - result = DualSolution() - result.objective_value = ( - proto.objective_value if proto.HasField("objective_value") else None - ) - result.dual_values = sparse_containers.parse_linear_constraint_map( - proto.dual_values, mod, validate=validate - ) - result.quadratic_dual_values = sparse_containers.parse_quadratic_constraint_map( - proto.quadratic_dual_values, mod, validate=validate + """Returns an equivalent DualSolution from the input proto.""" + result = DualSolution() + result.objective_value = ( + proto.objective_value if proto.HasField("objective_value") else None + ) + result.dual_values = sparse_containers.parse_linear_constraint_map( + proto.dual_values, mod, validate=validate + ) + result.quadratic_dual_values = ( + sparse_containers.parse_quadratic_constraint_map( + proto.quadratic_dual_values, mod, validate=validate + ) + ) + result.reduced_costs = sparse_containers.parse_variable_map( + proto.reduced_costs, mod, validate=validate + ) + status_proto = proto.feasibility_status + if status_proto == solution_pb2.SOLUTION_STATUS_UNSPECIFIED: + raise ValueError( + "Dual solution feasibility status should not be UNSPECIFIED" ) - result.reduced_costs = sparse_containers.parse_variable_map( - proto.reduced_costs, mod, validate=validate - ) - status_proto = proto.feasibility_status - if status_proto == solution_pb2.SOLUTION_STATUS_UNSPECIFIED: - raise ValueError("Dual solution feasibility status should not be UNSPECIFIED") - result.feasibility_status = SolutionStatus(status_proto) - return result + result.feasibility_status = SolutionStatus(status_proto) + return result @dataclasses.dataclass class DualRay: - """A direction of unbounded objective improvement in an optimization Model. - - A direction of unbounded improvement to the dual of an optimization, - problem; equivalently, a certificate of primal infeasibility. - - E.g. consider the primal dual pair linear program pair: - (Primal)            (Dual) - min c * x             max b * y - s.t. A * x >= b       s.t. y * A + r = c - x >= 0              y, r >= 0. - - The dual ray is the pair (y, r) satisfying: - b * y > 0 - y * A + r = 0 - y, r >= 0. - Observe that adding a positive multiple of (y, r) to dual feasible solution - maintains dual feasibility and improves the objective (proving the dual is - unbounded). The dual ray also proves the primal problem is infeasible. - - In the class DualRay below, y is dual_values and r is reduced_costs. - - For the general case, see go/mathopt-solutions and go/mathopt-dual (and note - that the dual objective depends on r in the general case). - - Attributes: - dual_values: The value assigned for each LinearConstraint in the model. - reduced_costs: The value assigned for each Variable in the model. - """ - - dual_values: Dict[linear_constraints.LinearConstraint, float] = dataclasses.field( - default_factory=dict - ) - reduced_costs: Dict[variables.Variable, float] = dataclasses.field( - default_factory=dict + """A direction of unbounded objective improvement in an optimization Model. + + A direction of unbounded improvement to the dual of an optimization, + problem; equivalently, a certificate of primal infeasibility. + + E.g. consider the primal dual pair linear program pair: + (Primal)            (Dual) + min c * x             max b * y + s.t. A * x >= b       s.t. y * A + r = c + x >= 0              y, r >= 0. + + The dual ray is the pair (y, r) satisfying: + b * y > 0 + y * A + r = 0 + y, r >= 0. + Observe that adding a positive multiple of (y, r) to dual feasible solution + maintains dual feasibility and improves the objective (proving the dual is + unbounded). The dual ray also proves the primal problem is infeasible. + + In the class DualRay below, y is dual_values and r is reduced_costs. + + For the general case, see go/mathopt-solutions and go/mathopt-dual (and note + that the dual objective depends on r in the general case). + + Attributes: + dual_values: The value assigned for each LinearConstraint in the model. + reduced_costs: The value assigned for each Variable in the model. + """ + + dual_values: Dict[linear_constraints.LinearConstraint, float] = ( + dataclasses.field(default_factory=dict) + ) + reduced_costs: Dict[variables.Variable, float] = dataclasses.field( + default_factory=dict + ) + + def to_proto(self) -> solution_pb2.DualRayProto: + """Returns an equivalent proto to this PrimalRay.""" + return solution_pb2.DualRayProto( + dual_values=sparse_containers.to_sparse_double_vector_proto( + self.dual_values + ), + reduced_costs=sparse_containers.to_sparse_double_vector_proto( + self.reduced_costs + ), ) - def to_proto(self) -> solution_pb2.DualRayProto: - """Returns an equivalent proto to this PrimalRay.""" - return solution_pb2.DualRayProto( - dual_values=sparse_containers.to_sparse_double_vector_proto( - self.dual_values - ), - reduced_costs=sparse_containers.to_sparse_double_vector_proto( - self.reduced_costs - ), - ) - def parse_dual_ray( proto: solution_pb2.DualRayProto, mod: model.Model, *, validate: bool = True ) -> DualRay: - """Returns an equivalent DualRay from the input proto.""" - result = DualRay() - result.dual_values = sparse_containers.parse_linear_constraint_map( - proto.dual_values, mod, validate=validate - ) - result.reduced_costs = sparse_containers.parse_variable_map( - proto.reduced_costs, mod, validate=validate - ) - return result + """Returns an equivalent DualRay from the input proto.""" + result = DualRay() + result.dual_values = sparse_containers.parse_linear_constraint_map( + proto.dual_values, mod, validate=validate + ) + result.reduced_costs = sparse_containers.parse_variable_map( + proto.reduced_costs, mod, validate=validate + ) + return result @dataclasses.dataclass class Basis: - """A combinatorial characterization for a solution to a linear program. - - The simplex method for solving linear programs always returns a "basic - feasible solution" which can be described combinatorially as a Basis. A basis - assigns a BasisStatus for every variable and linear constraint. - - E.g. consider a standard form LP: - min c * x - s.t. A * x = b - x >= 0 - that has more variables than constraints and with full row rank A. - - Let n be the number of variables and m the number of linear constraints. A - valid basis for this problem can be constructed as follows: - * All constraints will have basis status FIXED. - * Pick m variables such that the columns of A are linearly independent and - assign the status BASIC. - * Assign the status AT_LOWER for the remaining n - m variables. - - The basic solution for this basis is the unique solution of A * x = b that has - all variables with status AT_LOWER fixed to their lower bounds (all zero). The - resulting solution is called a basic feasible solution if it also satisfies - x >= 0. - - See go/mathopt-basis for treatment of the general case and an explanation of - how a dual solution is determined for a basis. - - Attributes: - variable_status: The basis status for each variable in the model. - constraint_status: The basis status for each linear constraint in the model. - basic_dual_feasibility: This is an advanced feature used by MathOpt to - characterize feasibility of suboptimal LP solutions (optimal solutions - will always have status SolutionStatus.FEASIBLE). For single-sided LPs it - should be equal to the feasibility status of the associated dual solution. - For two-sided LPs it may be different in some edge cases (e.g. incomplete - solves with primal simplex). For more details see - go/mathopt-basis-advanced#dualfeasibility. If you are providing a starting - basis via ModelSolveParameters.initial_basis, this value is ignored and - can be None. It is only relevant for the basis returned by Solution.basis, - and it is never None when returned from solve(). This is an advanced - status. For single-sided LPs it should be equal to the feasibility status - of the associated dual solution. For two-sided LPs it may be different in - some edge cases (e.g. incomplete solves with primal simplex). For more - details see go/mathopt-basis-advanced#dualfeasibility. - """ - - variable_status: Dict[variables.Variable, BasisStatus] = dataclasses.field( - default_factory=dict + """A combinatorial characterization for a solution to a linear program. + + The simplex method for solving linear programs always returns a "basic + feasible solution" which can be described combinatorially as a Basis. A basis + assigns a BasisStatus for every variable and linear constraint. + + E.g. consider a standard form LP: + min c * x + s.t. A * x = b + x >= 0 + that has more variables than constraints and with full row rank A. + + Let n be the number of variables and m the number of linear constraints. A + valid basis for this problem can be constructed as follows: + * All constraints will have basis status FIXED. + * Pick m variables such that the columns of A are linearly independent and + assign the status BASIC. + * Assign the status AT_LOWER for the remaining n - m variables. + + The basic solution for this basis is the unique solution of A * x = b that has + all variables with status AT_LOWER fixed to their lower bounds (all zero). The + resulting solution is called a basic feasible solution if it also satisfies + x >= 0. + + See go/mathopt-basis for treatment of the general case and an explanation of + how a dual solution is determined for a basis. + + Attributes: + variable_status: The basis status for each variable in the model. + constraint_status: The basis status for each linear constraint in the model. + basic_dual_feasibility: This is an advanced feature used by MathOpt to + characterize feasibility of suboptimal LP solutions (optimal solutions + will always have status SolutionStatus.FEASIBLE). For single-sided LPs it + should be equal to the feasibility status of the associated dual solution. + For two-sided LPs it may be different in some edge cases (e.g. incomplete + solves with primal simplex). For more details see + go/mathopt-basis-advanced#dualfeasibility. If you are providing a starting + basis via ModelSolveParameters.initial_basis, this value is ignored and + can be None. It is only relevant for the basis returned by Solution.basis, + and it is never None when returned from solve(). This is an advanced + status. For single-sided LPs it should be equal to the feasibility status + of the associated dual solution. For two-sided LPs it may be different in + some edge cases (e.g. incomplete solves with primal simplex). For more + details see go/mathopt-basis-advanced#dualfeasibility. + """ + + variable_status: Dict[variables.Variable, BasisStatus] = dataclasses.field( + default_factory=dict + ) + constraint_status: Dict[linear_constraints.LinearConstraint, BasisStatus] = ( + dataclasses.field(default_factory=dict) + ) + basic_dual_feasibility: Optional[SolutionStatus] = None + + def to_proto(self) -> solution_pb2.BasisProto: + """Returns an equivalent proto for the basis.""" + return solution_pb2.BasisProto( + variable_status=_to_sparse_basis_status_vector_proto( + self.variable_status + ), + constraint_status=_to_sparse_basis_status_vector_proto( + self.constraint_status + ), + basic_dual_feasibility=optional_solution_status_to_proto( + self.basic_dual_feasibility + ), ) - constraint_status: Dict[linear_constraints.LinearConstraint, BasisStatus] = ( - dataclasses.field(default_factory=dict) - ) - basic_dual_feasibility: Optional[SolutionStatus] = None - - def to_proto(self) -> solution_pb2.BasisProto: - """Returns an equivalent proto for the basis.""" - return solution_pb2.BasisProto( - variable_status=_to_sparse_basis_status_vector_proto(self.variable_status), - constraint_status=_to_sparse_basis_status_vector_proto( - self.constraint_status - ), - basic_dual_feasibility=optional_solution_status_to_proto( - self.basic_dual_feasibility - ), - ) def parse_basis( proto: solution_pb2.BasisProto, mod: model.Model, *, validate: bool = True ) -> Basis: - """Returns an equivalent Basis to the input proto.""" - result = Basis() - for index, vid in enumerate(proto.variable_status.ids): - status_proto = proto.variable_status.values[index] - if status_proto == solution_pb2.BASIS_STATUS_UNSPECIFIED: - raise ValueError("Variable basis status should not be UNSPECIFIED") - result.variable_status[mod.get_variable(vid, validate=validate)] = BasisStatus( - status_proto - ) - for index, cid in enumerate(proto.constraint_status.ids): - status_proto = proto.constraint_status.values[index] - if status_proto == solution_pb2.BASIS_STATUS_UNSPECIFIED: - raise ValueError("Constraint basis status should not be UNSPECIFIED") - result.constraint_status[mod.get_linear_constraint(cid, validate=validate)] = ( - BasisStatus(status_proto) - ) - result.basic_dual_feasibility = parse_optional_solution_status( - proto.basic_dual_feasibility + """Returns an equivalent Basis to the input proto.""" + result = Basis() + for index, vid in enumerate(proto.variable_status.ids): + status_proto = proto.variable_status.values[index] + if status_proto == solution_pb2.BASIS_STATUS_UNSPECIFIED: + raise ValueError("Variable basis status should not be UNSPECIFIED") + result.variable_status[mod.get_variable(vid, validate=validate)] = ( + BasisStatus(status_proto) ) - return result + for index, cid in enumerate(proto.constraint_status.ids): + status_proto = proto.constraint_status.values[index] + if status_proto == solution_pb2.BASIS_STATUS_UNSPECIFIED: + raise ValueError("Constraint basis status should not be UNSPECIFIED") + result.constraint_status[ + mod.get_linear_constraint(cid, validate=validate) + ] = BasisStatus(status_proto) + result.basic_dual_feasibility = parse_optional_solution_status( + proto.basic_dual_feasibility + ) + return result T = TypeVar("T", variables.Variable, linear_constraints.LinearConstraint) @@ -456,41 +468,41 @@ def parse_basis( def _to_sparse_basis_status_vector_proto( terms: Dict[T, BasisStatus], ) -> solution_pb2.SparseBasisStatusVector: - """Converts a basis vector from a python Dict to a protocol buffer.""" - result = solution_pb2.SparseBasisStatusVector() - if terms: - id_and_status = sorted( - (key.id, status.value) for (key, status) in terms.items() - ) - ids, values = zip(*id_and_status) - result.ids[:] = ids - result.values[:] = values - return result + """Converts a basis vector from a python Dict to a protocol buffer.""" + result = solution_pb2.SparseBasisStatusVector() + if terms: + id_and_status = sorted( + (key.id, status.value) for (key, status) in terms.items() + ) + ids, values = zip(*id_and_status) + result.ids[:] = ids + result.values[:] = values + return result @dataclasses.dataclass class Solution: - """A solution to the optimization problem in a Model.""" - - primal_solution: Optional[PrimalSolution] = None - dual_solution: Optional[DualSolution] = None - basis: Optional[Basis] = None - - def to_proto(self) -> solution_pb2.SolutionProto: - """Returns an equivalent proto for a solution.""" - return solution_pb2.SolutionProto( - primal_solution=( - self.primal_solution.to_proto() - if self.primal_solution is not None - else None - ), - dual_solution=( - self.dual_solution.to_proto() - if self.dual_solution is not None - else None - ), - basis=self.basis.to_proto() if self.basis is not None else None, - ) + """A solution to the optimization problem in a Model.""" + + primal_solution: Optional[PrimalSolution] = None + dual_solution: Optional[DualSolution] = None + basis: Optional[Basis] = None + + def to_proto(self) -> solution_pb2.SolutionProto: + """Returns an equivalent proto for a solution.""" + return solution_pb2.SolutionProto( + primal_solution=( + self.primal_solution.to_proto() + if self.primal_solution is not None + else None + ), + dual_solution=( + self.dual_solution.to_proto() + if self.dual_solution is not None + else None + ), + basis=self.basis.to_proto() if self.basis is not None else None, + ) def parse_solution( @@ -499,19 +511,19 @@ def parse_solution( *, validate: bool = True, ) -> Solution: - """Returns a Solution equivalent to the input proto.""" - result = Solution() - if proto.HasField("primal_solution"): - result.primal_solution = parse_primal_solution( - proto.primal_solution, mod, validate=validate - ) - if proto.HasField("dual_solution"): - result.dual_solution = parse_dual_solution( - proto.dual_solution, mod, validate=validate - ) - result.basis = ( - parse_basis(proto.basis, mod, validate=validate) - if proto.HasField("basis") - else None + """Returns a Solution equivalent to the input proto.""" + result = Solution() + if proto.HasField("primal_solution"): + result.primal_solution = parse_primal_solution( + proto.primal_solution, mod, validate=validate + ) + if proto.HasField("dual_solution"): + result.dual_solution = parse_dual_solution( + proto.dual_solution, mod, validate=validate ) - return result + result.basis = ( + parse_basis(proto.basis, mod, validate=validate) + if proto.HasField("basis") + else None + ) + return result diff --git a/ortools/math_opt/python/solution_test.py b/ortools/math_opt/python/solution_test.py index 6573a61f4fa..daede1ccaf7 100644 --- a/ortools/math_opt/python/solution_test.py +++ b/ortools/math_opt/python/solution_test.py @@ -22,511 +22,529 @@ class SolutionStatusTest(absltest.TestCase): - def test_optional_status_round_trip(self): - for status in solution_pb2.SolutionStatusProto.values(): - self.assertEqual( - status, - solution.optional_solution_status_to_proto( - solution.parse_optional_solution_status(status) - ), - ) - - -class ParsePrimalSolutionTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - - def test_empty_primal_solution_proto_round_trip(self) -> None: - mod = model.Model(name="test_model") - empty_solution = solution.PrimalSolution( - objective_value=2.0, - feasibility_status=solution.SolutionStatus.UNDETERMINED, - ) - empty_proto = empty_solution.to_proto() - expected_proto = solution_pb2.PrimalSolutionProto() - expected_proto.objective_value = 2.0 - expected_proto.feasibility_status = solution_pb2.SOLUTION_STATUS_UNDETERMINED - self.assert_protos_equiv(expected_proto, empty_proto) - round_trip_solution = solution.parse_primal_solution(empty_proto, mod) - self.assertEmpty(round_trip_solution.variable_values) - self.assertEmpty(round_trip_solution.auxiliary_objective_values) - - def test_primal_solution_proto_round_trip(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - a = mod.add_auxiliary_objective(priority=1) - proto = solution_pb2.PrimalSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) - proto.variable_values.ids[:] = [0, 2] - proto.variable_values.values[:] = [1.0, 0.0] - proto.auxiliary_objective_values[0] = 12.1 - actual = solution.parse_primal_solution(proto, mod) - self.assertDictEqual({x: 1.0, z: 0.0}, actual.variable_values) - self.assertEqual(2.0, actual.objective_value) - self.assertEqual(solution.SolutionStatus.FEASIBLE, actual.feasibility_status) - self.assertDictEqual(actual.auxiliary_objective_values, {a: 12.1}) - self.assert_protos_equiv(proto, actual.to_proto()) - - def test_primal_solution_unspecified_feasibility(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.PrimalSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_UNSPECIFIED, - ) - with self.assertRaisesRegex( - ValueError, "Primal solution feasibility.*UNSPECIFIED" - ): - solution.parse_primal_solution(proto, mod) - - def test_id_validation_variables(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.PrimalSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) - proto.variable_values.ids[:] = [2] - proto.variable_values.values[:] = [4.0] - actual = solution.parse_primal_solution(proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - self.assertDictEqual(actual.variable_values, {bad_var: 4.0}) - with self.assertRaises(KeyError): - solution.parse_primal_solution(proto, mod, validate=True) - - def test_id_validation_auxiliary_objectives(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.PrimalSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) - proto.auxiliary_objective_values[2] = 12.1 - actual = solution.parse_primal_solution(proto, mod, validate=False) - bad_obj = mod.get_auxiliary_objective(2, validate=False) - self.assertDictEqual(actual.auxiliary_objective_values, {bad_obj: 12.1}) - with self.assertRaises(KeyError): - solution.parse_primal_solution(proto, mod, validate=True) + def test_optional_status_round_trip(self): + for status in solution_pb2.SolutionStatusProto.values(): + self.assertEqual( + status, + solution.optional_solution_status_to_proto( + solution.parse_optional_solution_status(status) + ), + ) + + +class ParsePrimalSolutionTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_empty_primal_solution_proto_round_trip(self) -> None: + mod = model.Model(name="test_model") + empty_solution = solution.PrimalSolution( + objective_value=2.0, + feasibility_status=solution.SolutionStatus.UNDETERMINED, + ) + empty_proto = empty_solution.to_proto() + expected_proto = solution_pb2.PrimalSolutionProto() + expected_proto.objective_value = 2.0 + expected_proto.feasibility_status = ( + solution_pb2.SOLUTION_STATUS_UNDETERMINED + ) + self.assert_protos_equiv(expected_proto, empty_proto) + round_trip_solution = solution.parse_primal_solution(empty_proto, mod) + self.assertEmpty(round_trip_solution.variable_values) + self.assertEmpty(round_trip_solution.auxiliary_objective_values) + + def test_primal_solution_proto_round_trip(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + a = mod.add_auxiliary_objective(priority=1) + proto = solution_pb2.PrimalSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + ) + proto.variable_values.ids[:] = [0, 2] + proto.variable_values.values[:] = [1.0, 0.0] + proto.auxiliary_objective_values[0] = 12.1 + actual = solution.parse_primal_solution(proto, mod) + self.assertDictEqual({x: 1.0, z: 0.0}, actual.variable_values) + self.assertEqual(2.0, actual.objective_value) + self.assertEqual( + solution.SolutionStatus.FEASIBLE, actual.feasibility_status + ) + self.assertDictEqual(actual.auxiliary_objective_values, {a: 12.1}) + self.assert_protos_equiv(proto, actual.to_proto()) + + def test_primal_solution_unspecified_feasibility(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.PrimalSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_UNSPECIFIED, + ) + with self.assertRaisesRegex( + ValueError, "Primal solution feasibility.*UNSPECIFIED" + ): + solution.parse_primal_solution(proto, mod) + + def test_id_validation_variables(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.PrimalSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + ) + proto.variable_values.ids[:] = [2] + proto.variable_values.values[:] = [4.0] + actual = solution.parse_primal_solution(proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + self.assertDictEqual(actual.variable_values, {bad_var: 4.0}) + with self.assertRaises(KeyError): + solution.parse_primal_solution(proto, mod, validate=True) + + def test_id_validation_auxiliary_objectives(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.PrimalSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + ) + proto.auxiliary_objective_values[2] = 12.1 + actual = solution.parse_primal_solution(proto, mod, validate=False) + bad_obj = mod.get_auxiliary_objective(2, validate=False) + self.assertDictEqual(actual.auxiliary_objective_values, {bad_obj: 12.1}) + with self.assertRaises(KeyError): + solution.parse_primal_solution(proto, mod, validate=True) class PrimalRayTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_proto_round_trip(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - ray = solution.PrimalRay(variable_values={x: 1.0, y: 1.0}) - ray_proto = solution_pb2.PrimalRayProto() - ray_proto.variable_values.ids[:] = [0, 1] - ray_proto.variable_values.values[:] = [1.0, 1.0] - - # Test proto -> model - parsed_ray = solution.parse_primal_ray(ray_proto, mod) - self.assertDictEqual({x: 1.0, y: 1.0}, parsed_ray.variable_values) - - # Test model -> proto - exported_ray = ray.to_proto() - self.assert_protos_equiv(exported_ray, ray_proto) - - def test_id_validation(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.PrimalRayProto() - proto.variable_values.ids[:] = [2] - proto.variable_values.values[:] = [4.0] - actual = solution.parse_primal_ray(proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - self.assertDictEqual(actual.variable_values, {bad_var: 4.0}) - with self.assertRaises(KeyError): - solution.parse_primal_ray(proto, mod, validate=True) - - -class ParseDualSolutionTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - - def test_empty_primal_solution_proto_round_trip(self) -> None: - mod = model.Model(name="test_model") - empty_solution = solution.DualSolution( - objective_value=2.0, - feasibility_status=solution.SolutionStatus.UNDETERMINED, - ) - empty_proto = empty_solution.to_proto() - expected_proto = solution_pb2.DualSolutionProto() - expected_proto.objective_value = 2.0 - expected_proto.feasibility_status = solution_pb2.SOLUTION_STATUS_UNDETERMINED - self.assert_protos_equiv(expected_proto, empty_proto) - round_trip_solution = solution.parse_dual_solution(empty_proto, mod) - self.assertEmpty(round_trip_solution.dual_values) - self.assertEmpty(round_trip_solution.reduced_costs) - - def test_no_obj(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - e = mod.add_quadratic_constraint(name="e") - f = mod.add_quadratic_constraint(name="f") - proto = solution_pb2.DualSolutionProto() - proto.dual_values.ids[:] = [0, 1] - proto.dual_values.values[:] = [0.0, 1.0] - proto.quadratic_dual_values.ids[:] = [0, 1] - proto.quadratic_dual_values.values[:] = [100.0, 101.0] - proto.reduced_costs.ids[:] = [0, 1] - proto.reduced_costs.values[:] = [10.0, 0.0] - proto.feasibility_status = solution_pb2.SOLUTION_STATUS_FEASIBLE - actual = solution.parse_dual_solution(proto, mod) - self.assertDictEqual({x: 10.0, y: 0.0}, actual.reduced_costs) - self.assertDictEqual({c: 0.0, d: 1.0}, actual.dual_values) - self.assertDictEqual({e: 100, f: 101}, actual.quadratic_dual_values) - self.assertIsNone(actual.objective_value) - self.assertEqual(solution.SolutionStatus.FEASIBLE, actual.feasibility_status) - self.assert_protos_equiv(proto, actual.to_proto()) - - def test_with_obj(self) -> None: - mod = model.Model(name="test_model") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - proto = solution_pb2.DualSolutionProto(objective_value=3.0) - proto.dual_values.ids[:] = [0] - proto.dual_values.values[:] = [5.0] - proto.feasibility_status = solution_pb2.SOLUTION_STATUS_INFEASIBLE - actual = solution.parse_dual_solution(proto, mod) - self.assertEmpty(actual.reduced_costs) - self.assertDictEqual({c: 5.0}, actual.dual_values) - self.assertEqual(3.0, actual.objective_value) - self.assertEqual(solution.SolutionStatus.INFEASIBLE, actual.feasibility_status) - self.assert_protos_equiv(proto, actual.to_proto()) - - def test_dual_solution_unspecified_feasibility(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.DualSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_UNSPECIFIED, - ) - with self.assertRaisesRegex( - ValueError, "Dual solution feasibility.*UNSPECIFIED" - ): - solution.parse_dual_solution(proto, mod) - - def test_id_validation_reduced_costs(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.DualSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) - proto.reduced_costs.ids[:] = [2] - proto.reduced_costs.values[:] = [4.0] - actual = solution.parse_dual_solution(proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - self.assertDictEqual(actual.reduced_costs, {bad_var: 4.0}) - with self.assertRaises(KeyError): - solution.parse_dual_solution(proto, mod, validate=True) - - def test_id_validation_dual_values(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.DualSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) - proto.dual_values.ids[:] = [2] - proto.dual_values.values[:] = [4.0] - actual = solution.parse_dual_solution(proto, mod, validate=False) - bad_lin_con = mod.get_linear_constraint(2, validate=False) - self.assertDictEqual(actual.dual_values, {bad_lin_con: 4.0}) - with self.assertRaises(KeyError): - solution.parse_dual_solution(proto, mod, validate=True) - - def test_id_validation_quadratic_dual_values(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.DualSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) - proto.quadratic_dual_values.ids[:] = [2] - proto.quadratic_dual_values.values[:] = [4.0] - actual = solution.parse_dual_solution(proto, mod, validate=False) - bad_quad_con = mod.get_quadratic_constraint(2, validate=False) - self.assertDictEqual(actual.quadratic_dual_values, {bad_quad_con: 4.0}) - with self.assertRaises(KeyError): - solution.parse_dual_solution(proto, mod, validate=True) + def test_proto_round_trip(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + ray = solution.PrimalRay(variable_values={x: 1.0, y: 1.0}) + ray_proto = solution_pb2.PrimalRayProto() + ray_proto.variable_values.ids[:] = [0, 1] + ray_proto.variable_values.values[:] = [1.0, 1.0] + + # Test proto -> model + parsed_ray = solution.parse_primal_ray(ray_proto, mod) + self.assertDictEqual({x: 1.0, y: 1.0}, parsed_ray.variable_values) + + # Test model -> proto + exported_ray = ray.to_proto() + self.assert_protos_equiv(exported_ray, ray_proto) + + def test_id_validation(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.PrimalRayProto() + proto.variable_values.ids[:] = [2] + proto.variable_values.values[:] = [4.0] + actual = solution.parse_primal_ray(proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + self.assertDictEqual(actual.variable_values, {bad_var: 4.0}) + with self.assertRaises(KeyError): + solution.parse_primal_ray(proto, mod, validate=True) + + +class ParseDualSolutionTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_empty_primal_solution_proto_round_trip(self) -> None: + mod = model.Model(name="test_model") + empty_solution = solution.DualSolution( + objective_value=2.0, + feasibility_status=solution.SolutionStatus.UNDETERMINED, + ) + empty_proto = empty_solution.to_proto() + expected_proto = solution_pb2.DualSolutionProto() + expected_proto.objective_value = 2.0 + expected_proto.feasibility_status = ( + solution_pb2.SOLUTION_STATUS_UNDETERMINED + ) + self.assert_protos_equiv(expected_proto, empty_proto) + round_trip_solution = solution.parse_dual_solution(empty_proto, mod) + self.assertEmpty(round_trip_solution.dual_values) + self.assertEmpty(round_trip_solution.reduced_costs) + + def test_no_obj(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + e = mod.add_quadratic_constraint(name="e") + f = mod.add_quadratic_constraint(name="f") + proto = solution_pb2.DualSolutionProto() + proto.dual_values.ids[:] = [0, 1] + proto.dual_values.values[:] = [0.0, 1.0] + proto.quadratic_dual_values.ids[:] = [0, 1] + proto.quadratic_dual_values.values[:] = [100.0, 101.0] + proto.reduced_costs.ids[:] = [0, 1] + proto.reduced_costs.values[:] = [10.0, 0.0] + proto.feasibility_status = solution_pb2.SOLUTION_STATUS_FEASIBLE + actual = solution.parse_dual_solution(proto, mod) + self.assertDictEqual({x: 10.0, y: 0.0}, actual.reduced_costs) + self.assertDictEqual({c: 0.0, d: 1.0}, actual.dual_values) + self.assertDictEqual({e: 100, f: 101}, actual.quadratic_dual_values) + self.assertIsNone(actual.objective_value) + self.assertEqual( + solution.SolutionStatus.FEASIBLE, actual.feasibility_status + ) + self.assert_protos_equiv(proto, actual.to_proto()) + + def test_with_obj(self) -> None: + mod = model.Model(name="test_model") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + proto = solution_pb2.DualSolutionProto(objective_value=3.0) + proto.dual_values.ids[:] = [0] + proto.dual_values.values[:] = [5.0] + proto.feasibility_status = solution_pb2.SOLUTION_STATUS_INFEASIBLE + actual = solution.parse_dual_solution(proto, mod) + self.assertEmpty(actual.reduced_costs) + self.assertDictEqual({c: 5.0}, actual.dual_values) + self.assertEqual(3.0, actual.objective_value) + self.assertEqual( + solution.SolutionStatus.INFEASIBLE, actual.feasibility_status + ) + self.assert_protos_equiv(proto, actual.to_proto()) + + def test_dual_solution_unspecified_feasibility(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.DualSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_UNSPECIFIED, + ) + with self.assertRaisesRegex( + ValueError, "Dual solution feasibility.*UNSPECIFIED" + ): + solution.parse_dual_solution(proto, mod) + + def test_id_validation_reduced_costs(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.DualSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + ) + proto.reduced_costs.ids[:] = [2] + proto.reduced_costs.values[:] = [4.0] + actual = solution.parse_dual_solution(proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + self.assertDictEqual(actual.reduced_costs, {bad_var: 4.0}) + with self.assertRaises(KeyError): + solution.parse_dual_solution(proto, mod, validate=True) + + def test_id_validation_dual_values(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.DualSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + ) + proto.dual_values.ids[:] = [2] + proto.dual_values.values[:] = [4.0] + actual = solution.parse_dual_solution(proto, mod, validate=False) + bad_lin_con = mod.get_linear_constraint(2, validate=False) + self.assertDictEqual(actual.dual_values, {bad_lin_con: 4.0}) + with self.assertRaises(KeyError): + solution.parse_dual_solution(proto, mod, validate=True) + + def test_id_validation_quadratic_dual_values(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.DualSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + ) + proto.quadratic_dual_values.ids[:] = [2] + proto.quadratic_dual_values.values[:] = [4.0] + actual = solution.parse_dual_solution(proto, mod, validate=False) + bad_quad_con = mod.get_quadratic_constraint(2, validate=False) + self.assertDictEqual(actual.quadratic_dual_values, {bad_quad_con: 4.0}) + with self.assertRaises(KeyError): + solution.parse_dual_solution(proto, mod, validate=True) class DualRayTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_proto_round_trip(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - - dual_ray = solution.DualRay( - dual_values={c: 0.0, d: 1.0}, reduced_costs={x: 10.0, y: 0.0} - ) - - dual_ray_proto = solution_pb2.DualRayProto() - dual_ray_proto.dual_values.ids[:] = [0, 1] - dual_ray_proto.dual_values.values[:] = [0.0, 1.0] - dual_ray_proto.reduced_costs.ids[:] = [0, 1] - dual_ray_proto.reduced_costs.values[:] = [10.0, 0.0] - - # Test proto -> dual ray - parsed_ray = solution.parse_dual_ray(dual_ray_proto, mod) - self.assertDictEqual(dual_ray.reduced_costs, parsed_ray.reduced_costs) - self.assertDictEqual(dual_ray.dual_values, parsed_ray.dual_values) - - # Test dual ray -> proto - exported_proto = dual_ray.to_proto() - self.assert_protos_equiv(exported_proto, dual_ray_proto) - - def test_id_validation_reduced_costs(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.DualRayProto() - proto.reduced_costs.ids[:] = [2] - proto.reduced_costs.values[:] = [4.0] - actual = solution.parse_dual_ray(proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - self.assertDictEqual(actual.reduced_costs, {bad_var: 4.0}) - with self.assertRaises(KeyError): - solution.parse_dual_ray(proto, mod, validate=True) - - def test_id_validation_dual_values(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.DualRayProto() - proto.dual_values.ids[:] = [2] - proto.dual_values.values[:] = [4.0] - actual = solution.parse_dual_ray(proto, mod, validate=False) - bad_lin_con = mod.get_linear_constraint(2, validate=False) - self.assertDictEqual(actual.dual_values, {bad_lin_con: 4.0}) - with self.assertRaises(KeyError): - solution.parse_dual_ray(proto, mod, validate=True) + def test_proto_round_trip(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + + dual_ray = solution.DualRay( + dual_values={c: 0.0, d: 1.0}, reduced_costs={x: 10.0, y: 0.0} + ) + + dual_ray_proto = solution_pb2.DualRayProto() + dual_ray_proto.dual_values.ids[:] = [0, 1] + dual_ray_proto.dual_values.values[:] = [0.0, 1.0] + dual_ray_proto.reduced_costs.ids[:] = [0, 1] + dual_ray_proto.reduced_costs.values[:] = [10.0, 0.0] + + # Test proto -> dual ray + parsed_ray = solution.parse_dual_ray(dual_ray_proto, mod) + self.assertDictEqual(dual_ray.reduced_costs, parsed_ray.reduced_costs) + self.assertDictEqual(dual_ray.dual_values, parsed_ray.dual_values) + + # Test dual ray -> proto + exported_proto = dual_ray.to_proto() + self.assert_protos_equiv(exported_proto, dual_ray_proto) + + def test_id_validation_reduced_costs(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.DualRayProto() + proto.reduced_costs.ids[:] = [2] + proto.reduced_costs.values[:] = [4.0] + actual = solution.parse_dual_ray(proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + self.assertDictEqual(actual.reduced_costs, {bad_var: 4.0}) + with self.assertRaises(KeyError): + solution.parse_dual_ray(proto, mod, validate=True) + + def test_id_validation_dual_values(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.DualRayProto() + proto.dual_values.ids[:] = [2] + proto.dual_values.values[:] = [4.0] + actual = solution.parse_dual_ray(proto, mod, validate=False) + bad_lin_con = mod.get_linear_constraint(2, validate=False) + self.assertDictEqual(actual.dual_values, {bad_lin_con: 4.0}) + with self.assertRaises(KeyError): + solution.parse_dual_ray(proto, mod, validate=True) class BasisTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - def test_empty_basis_proto_round_trip(self) -> None: - mod = model.Model(name="test_model") - empty_basis = solution.Basis() - empty_proto = empty_basis.to_proto() - expected_proto = solution_pb2.BasisProto() - self.assert_protos_equiv(expected_proto, empty_proto) - round_trip_basis = solution.parse_basis(empty_proto, mod) - self.assertEmpty(round_trip_basis.constraint_status) - self.assertEmpty(round_trip_basis.variable_status) - self.assertIsNone(round_trip_basis.basic_dual_feasibility) - - def test_basis_proto_round_trip(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - basis = solution.Basis() - basis.variable_status[x] = solution.BasisStatus.AT_LOWER_BOUND - basis.variable_status[y] = solution.BasisStatus.BASIC - basis.constraint_status[c] = solution.BasisStatus.BASIC - basis.constraint_status[d] = solution.BasisStatus.AT_UPPER_BOUND - basis.basic_dual_feasibility = solution.SolutionStatus.FEASIBLE - basis_proto = basis.to_proto() - expected_proto = solution_pb2.BasisProto() - expected_proto.constraint_status.ids[:] = [0, 1] - expected_proto.constraint_status.values[:] = [ - solution_pb2.BASIS_STATUS_BASIC, - solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, - ] - expected_proto.variable_status.ids[:] = [0, 1] - expected_proto.variable_status.values[:] = [ - solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, - solution_pb2.BASIS_STATUS_BASIC, - ] - expected_proto.basic_dual_feasibility = solution_pb2.SOLUTION_STATUS_FEASIBLE - self.assert_protos_equiv(expected_proto, basis_proto) - round_trip_basis = solution.parse_basis(basis_proto, mod) - self.assertDictEqual( - {c: solution.BasisStatus.BASIC, d: solution.BasisStatus.AT_UPPER_BOUND}, - round_trip_basis.constraint_status, - ) - self.assertDictEqual( - {x: solution.BasisStatus.AT_LOWER_BOUND, y: solution.BasisStatus.BASIC}, - round_trip_basis.variable_status, - ) - - def test_constraint_status_unspecified(self) -> None: - mod = model.Model(name="test_model") - mod.add_binary_variable(name="x") - mod.add_binary_variable(name="y") - mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - basis_proto = solution_pb2.BasisProto() - basis_proto.constraint_status.ids[:] = [0, 1] - basis_proto.constraint_status.values[:] = [ - solution_pb2.BASIS_STATUS_UNSPECIFIED, - solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, - ] - basis_proto.variable_status.ids[:] = [0, 1] - basis_proto.variable_status.values[:] = [ - solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, - solution_pb2.BASIS_STATUS_BASIC, - ] - basis_proto.basic_dual_feasibility = solution_pb2.SOLUTION_STATUS_FEASIBLE - with self.assertRaisesRegex(ValueError, "Constraint basis.*UNSPECIFIED"): - solution.parse_basis(basis_proto, mod) - - def test_variable_status_unspecified(self) -> None: - mod = model.Model(name="test_model") - mod.add_binary_variable(name="x") - mod.add_binary_variable(name="y") - mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - basis_proto = solution_pb2.BasisProto() - basis_proto.constraint_status.ids[:] = [0, 1] - basis_proto.constraint_status.values[:] = [ - solution_pb2.BASIS_STATUS_BASIC, - solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, - ] - basis_proto.variable_status.ids[:] = [0, 1] - basis_proto.variable_status.values[:] = [ - solution_pb2.BASIS_STATUS_UNSPECIFIED, - solution_pb2.BASIS_STATUS_BASIC, - ] - basis_proto.basic_dual_feasibility = solution_pb2.SOLUTION_STATUS_FEASIBLE - with self.assertRaisesRegex(ValueError, "Variable basis.*UNSPECIFIED"): - solution.parse_basis(basis_proto, mod) - - def test_basic_dual_feasibility_unspecified(self) -> None: - mod = model.Model(name="test_model") - basis_proto = solution_pb2.BasisProto() - basis = solution.parse_basis(basis_proto, mod) - self.assertIsNone(basis.basic_dual_feasibility) - - def test_variable_id_validation(self) -> None: - mod = model.Model(name="test_model") - basis_proto = solution_pb2.BasisProto() - basis_proto.variable_status.ids[:] = [2] - basis_proto.variable_status.values[:] = [ - solution_pb2.BASIS_STATUS_BASIC, - ] - basis = solution.parse_basis(basis_proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - self.assertDictEqual( - basis.variable_status, {bad_var: solution.BasisStatus.BASIC} - ) - with self.assertRaises(KeyError): - solution.parse_basis(basis_proto, mod, validate=True) - - def test_linear_constraint_id_validation(self) -> None: - mod = model.Model(name="test_model") - basis_proto = solution_pb2.BasisProto() - basis_proto.constraint_status.ids[:] = [2] - basis_proto.constraint_status.values[:] = [ - solution_pb2.BASIS_STATUS_BASIC, - ] - basis = solution.parse_basis(basis_proto, mod, validate=False) - bad_con = mod.get_linear_constraint(2, validate=False) - self.assertDictEqual( - basis.constraint_status, {bad_con: solution.BasisStatus.BASIC} - ) - with self.assertRaises(KeyError): - solution.parse_basis(basis_proto, mod, validate=True) - - -class ParseSolutionTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - - def test_solution_proto_round_trip(self) -> None: - mod = model.Model(name="test_model") - mod.add_variable() - mod.add_variable() - mod.add_linear_constraint() - mod.add_linear_constraint() - primal_solution = solution_pb2.PrimalSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_INFEASIBLE, - ) - primal_solution.variable_values.ids[:] = [0, 1] - primal_solution.variable_values.values[:] = [1.0, 0.0] - dual_solution = solution_pb2.DualSolutionProto( - objective_value=2.0, - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) - dual_solution.dual_values.ids[:] = [0, 1] - dual_solution.dual_values.values[:] = [0.0, 1.0] - dual_solution.reduced_costs.ids[:] = [0, 1] - dual_solution.reduced_costs.values[:] = [10.0, 0.0] - basis = solution_pb2.BasisProto() - basis.constraint_status.ids[:] = [0, 1] - basis.constraint_status.values[:] = [ - solution_pb2.BASIS_STATUS_BASIC, - solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, - ] - basis.variable_status.ids[:] = [0, 1] - basis.variable_status.values[:] = [ - solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, - solution_pb2.BASIS_STATUS_BASIC, - ] - basis.basic_dual_feasibility = solution_pb2.SOLUTION_STATUS_FEASIBLE - proto = solution_pb2.SolutionProto( - primal_solution=primal_solution, - dual_solution=dual_solution, - basis=basis, - ) - actual = solution.parse_solution(proto, mod) - self.assert_protos_equiv(proto, actual.to_proto()) - - def test_basis_id_validation(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.SolutionProto( - basis=solution_pb2.BasisProto( - constraint_status=solution_pb2.SparseBasisStatusVector( - ids=[2], values=[solution_pb2.BASIS_STATUS_BASIC] - ) + def test_empty_basis_proto_round_trip(self) -> None: + mod = model.Model(name="test_model") + empty_basis = solution.Basis() + empty_proto = empty_basis.to_proto() + expected_proto = solution_pb2.BasisProto() + self.assert_protos_equiv(expected_proto, empty_proto) + round_trip_basis = solution.parse_basis(empty_proto, mod) + self.assertEmpty(round_trip_basis.constraint_status) + self.assertEmpty(round_trip_basis.variable_status) + self.assertIsNone(round_trip_basis.basic_dual_feasibility) + + def test_basis_proto_round_trip(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + basis = solution.Basis() + basis.variable_status[x] = solution.BasisStatus.AT_LOWER_BOUND + basis.variable_status[y] = solution.BasisStatus.BASIC + basis.constraint_status[c] = solution.BasisStatus.BASIC + basis.constraint_status[d] = solution.BasisStatus.AT_UPPER_BOUND + basis.basic_dual_feasibility = solution.SolutionStatus.FEASIBLE + basis_proto = basis.to_proto() + expected_proto = solution_pb2.BasisProto() + expected_proto.constraint_status.ids[:] = [0, 1] + expected_proto.constraint_status.values[:] = [ + solution_pb2.BASIS_STATUS_BASIC, + solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, + ] + expected_proto.variable_status.ids[:] = [0, 1] + expected_proto.variable_status.values[:] = [ + solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, + solution_pb2.BASIS_STATUS_BASIC, + ] + expected_proto.basic_dual_feasibility = ( + solution_pb2.SOLUTION_STATUS_FEASIBLE + ) + self.assert_protos_equiv(expected_proto, basis_proto) + round_trip_basis = solution.parse_basis(basis_proto, mod) + self.assertDictEqual( + {c: solution.BasisStatus.BASIC, d: solution.BasisStatus.AT_UPPER_BOUND}, + round_trip_basis.constraint_status, + ) + self.assertDictEqual( + {x: solution.BasisStatus.AT_LOWER_BOUND, y: solution.BasisStatus.BASIC}, + round_trip_basis.variable_status, + ) + + def test_constraint_status_unspecified(self) -> None: + mod = model.Model(name="test_model") + mod.add_binary_variable(name="x") + mod.add_binary_variable(name="y") + mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + basis_proto = solution_pb2.BasisProto() + basis_proto.constraint_status.ids[:] = [0, 1] + basis_proto.constraint_status.values[:] = [ + solution_pb2.BASIS_STATUS_UNSPECIFIED, + solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, + ] + basis_proto.variable_status.ids[:] = [0, 1] + basis_proto.variable_status.values[:] = [ + solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, + solution_pb2.BASIS_STATUS_BASIC, + ] + basis_proto.basic_dual_feasibility = solution_pb2.SOLUTION_STATUS_FEASIBLE + with self.assertRaisesRegex(ValueError, "Constraint basis.*UNSPECIFIED"): + solution.parse_basis(basis_proto, mod) + + def test_variable_status_unspecified(self) -> None: + mod = model.Model(name="test_model") + mod.add_binary_variable(name="x") + mod.add_binary_variable(name="y") + mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + basis_proto = solution_pb2.BasisProto() + basis_proto.constraint_status.ids[:] = [0, 1] + basis_proto.constraint_status.values[:] = [ + solution_pb2.BASIS_STATUS_BASIC, + solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, + ] + basis_proto.variable_status.ids[:] = [0, 1] + basis_proto.variable_status.values[:] = [ + solution_pb2.BASIS_STATUS_UNSPECIFIED, + solution_pb2.BASIS_STATUS_BASIC, + ] + basis_proto.basic_dual_feasibility = solution_pb2.SOLUTION_STATUS_FEASIBLE + with self.assertRaisesRegex(ValueError, "Variable basis.*UNSPECIFIED"): + solution.parse_basis(basis_proto, mod) + + def test_basic_dual_feasibility_unspecified(self) -> None: + mod = model.Model(name="test_model") + basis_proto = solution_pb2.BasisProto() + basis = solution.parse_basis(basis_proto, mod) + self.assertIsNone(basis.basic_dual_feasibility) + + def test_variable_id_validation(self) -> None: + mod = model.Model(name="test_model") + basis_proto = solution_pb2.BasisProto() + basis_proto.variable_status.ids[:] = [2] + basis_proto.variable_status.values[:] = [ + solution_pb2.BASIS_STATUS_BASIC, + ] + basis = solution.parse_basis(basis_proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + self.assertDictEqual( + basis.variable_status, {bad_var: solution.BasisStatus.BASIC} + ) + with self.assertRaises(KeyError): + solution.parse_basis(basis_proto, mod, validate=True) + + def test_linear_constraint_id_validation(self) -> None: + mod = model.Model(name="test_model") + basis_proto = solution_pb2.BasisProto() + basis_proto.constraint_status.ids[:] = [2] + basis_proto.constraint_status.values[:] = [ + solution_pb2.BASIS_STATUS_BASIC, + ] + basis = solution.parse_basis(basis_proto, mod, validate=False) + bad_con = mod.get_linear_constraint(2, validate=False) + self.assertDictEqual( + basis.constraint_status, {bad_con: solution.BasisStatus.BASIC} + ) + with self.assertRaises(KeyError): + solution.parse_basis(basis_proto, mod, validate=True) + + +class ParseSolutionTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_solution_proto_round_trip(self) -> None: + mod = model.Model(name="test_model") + mod.add_variable() + mod.add_variable() + mod.add_linear_constraint() + mod.add_linear_constraint() + primal_solution = solution_pb2.PrimalSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_INFEASIBLE, + ) + primal_solution.variable_values.ids[:] = [0, 1] + primal_solution.variable_values.values[:] = [1.0, 0.0] + dual_solution = solution_pb2.DualSolutionProto( + objective_value=2.0, + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, + ) + dual_solution.dual_values.ids[:] = [0, 1] + dual_solution.dual_values.values[:] = [0.0, 1.0] + dual_solution.reduced_costs.ids[:] = [0, 1] + dual_solution.reduced_costs.values[:] = [10.0, 0.0] + basis = solution_pb2.BasisProto() + basis.constraint_status.ids[:] = [0, 1] + basis.constraint_status.values[:] = [ + solution_pb2.BASIS_STATUS_BASIC, + solution_pb2.BASIS_STATUS_AT_UPPER_BOUND, + ] + basis.variable_status.ids[:] = [0, 1] + basis.variable_status.values[:] = [ + solution_pb2.BASIS_STATUS_AT_LOWER_BOUND, + solution_pb2.BASIS_STATUS_BASIC, + ] + basis.basic_dual_feasibility = solution_pb2.SOLUTION_STATUS_FEASIBLE + proto = solution_pb2.SolutionProto( + primal_solution=primal_solution, + dual_solution=dual_solution, + basis=basis, + ) + actual = solution.parse_solution(proto, mod) + self.assert_protos_equiv(proto, actual.to_proto()) + + def test_basis_id_validation(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.SolutionProto( + basis=solution_pb2.BasisProto( + constraint_status=solution_pb2.SparseBasisStatusVector( + ids=[2], values=[solution_pb2.BASIS_STATUS_BASIC] ) ) - sol = solution.parse_solution(proto, mod, validate=False) - bad_con = mod.get_linear_constraint(2, validate=False) - # TODO: b/215588365 - make a local variable so pytype is happy - basis = sol.basis - self.assertIsNotNone(basis) - self.assertDictEqual( - basis.constraint_status, {bad_con: solution.BasisStatus.BASIC} - ) - with self.assertRaises(KeyError): - solution.parse_solution(proto, mod, validate=True) - - def test_primal_solution_id_validation(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.SolutionProto( - primal_solution=solution_pb2.PrimalSolutionProto( - variable_values=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[2], values=[4.0] - ), - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) + ) + sol = solution.parse_solution(proto, mod, validate=False) + bad_con = mod.get_linear_constraint(2, validate=False) + # TODO: b/215588365 - make a local variable so pytype is happy + basis = sol.basis + self.assertIsNotNone(basis) + self.assertDictEqual( + basis.constraint_status, {bad_con: solution.BasisStatus.BASIC} + ) + with self.assertRaises(KeyError): + solution.parse_solution(proto, mod, validate=True) + + def test_primal_solution_id_validation(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.SolutionProto( + primal_solution=solution_pb2.PrimalSolutionProto( + variable_values=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[2], values=[4.0] + ), + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, ) - sol = solution.parse_solution(proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - # TODO: b/215588365 - make a local variable so pytype is happy - primal = sol.primal_solution - self.assertIsNotNone(primal) - self.assertDictEqual(primal.variable_values, {bad_var: 4.0}) - with self.assertRaises(KeyError): - solution.parse_solution(proto, mod, validate=True) - - def test_dual_solution_id_validation(self) -> None: - mod = model.Model(name="test_model") - proto = solution_pb2.SolutionProto( - dual_solution=solution_pb2.DualSolutionProto( - reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( - ids=[2], values=[4.0] - ), - feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, - ) + ) + sol = solution.parse_solution(proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + # TODO: b/215588365 - make a local variable so pytype is happy + primal = sol.primal_solution + self.assertIsNotNone(primal) + self.assertDictEqual(primal.variable_values, {bad_var: 4.0}) + with self.assertRaises(KeyError): + solution.parse_solution(proto, mod, validate=True) + + def test_dual_solution_id_validation(self) -> None: + mod = model.Model(name="test_model") + proto = solution_pb2.SolutionProto( + dual_solution=solution_pb2.DualSolutionProto( + reduced_costs=sparse_containers_pb2.SparseDoubleVectorProto( + ids=[2], values=[4.0] + ), + feasibility_status=solution_pb2.SOLUTION_STATUS_FEASIBLE, ) - sol = solution.parse_solution(proto, mod, validate=False) - bad_var = mod.get_variable(2, validate=False) - # TODO: b/215588365 - make a local variable so pytype is happy - dual = sol.dual_solution - self.assertIsNotNone(dual) - self.assertDictEqual(dual.reduced_costs, {bad_var: 4.0}) - with self.assertRaises(KeyError): - solution.parse_solution(proto, mod, validate=True) + ) + sol = solution.parse_solution(proto, mod, validate=False) + bad_var = mod.get_variable(2, validate=False) + # TODO: b/215588365 - make a local variable so pytype is happy + dual = sol.dual_solution + self.assertIsNotNone(dual) + self.assertDictEqual(dual.reduced_costs, {bad_var: 4.0}) + with self.assertRaises(KeyError): + solution.parse_solution(proto, mod, validate=True) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/solve.py b/ortools/math_opt/python/solve.py index b2f57706d72..874a57620b4 100644 --- a/ortools/math_opt/python/solve.py +++ b/ortools/math_opt/python/solve.py @@ -41,61 +41,63 @@ def solve( msg_cb: Optional[message_callback.SolveMessageCallback] = None, callback_reg: Optional[callback.CallbackRegistration] = None, cb: Optional[SolveCallback] = None, - streamable_init_args: Optional[init_arguments.StreamableSolverInitArguments] = None, + streamable_init_args: Optional[ + init_arguments.StreamableSolverInitArguments + ] = None, ) -> result.SolveResult: - """Solves an optimization model. - - Thread-safety: this function must not be called while modifying the Model - (adding variables...). Some solvers may add more restriction regarding - threading. Please see SolverType::XXX documentation for details. - - Args: - opt_model: The optimization model. - solver_type: The underlying solver to use. - params: Configuration of the underlying solver. - model_params: Configuration of the solver that is model specific. - msg_cb: A callback that gives back the underlying solver's logs by the line. - callback_reg: Configures when the callback will be invoked (if provided) and - what data will be collected to access in the callback. - cb: A callback that will be called periodically as the solver runs. - streamable_init_args: Configuration for initializing the underlying solver. - - Returns: - A SolveResult containing the termination reason, solution(s) and stats. - - Raises: - RuntimeError: On a solve error. - """ - # First, initialize optional arguments that were not set to default values. - # Note that in python, default arguments must be immutable, and these are not. - params = params or parameters.SolveParameters() - model_params = model_params or model_parameters.ModelSolveParameters() - callback_reg = callback_reg or callback.CallbackRegistration() - streamable_init_args = ( - streamable_init_args or init_arguments.StreamableSolverInitArguments() + """Solves an optimization model. + + Thread-safety: this function must not be called while modifying the Model + (adding variables...). Some solvers may add more restriction regarding + threading. Please see SolverType::XXX documentation for details. + + Args: + opt_model: The optimization model. + solver_type: The underlying solver to use. + params: Configuration of the underlying solver. + model_params: Configuration of the solver that is model specific. + msg_cb: A callback that gives back the underlying solver's logs by the line. + callback_reg: Configures when the callback will be invoked (if provided) and + what data will be collected to access in the callback. + cb: A callback that will be called periodically as the solver runs. + streamable_init_args: Configuration for initializing the underlying solver. + + Returns: + A SolveResult containing the termination reason, solution(s) and stats. + + Raises: + RuntimeError: On a solve error. + """ + # First, initialize optional arguments that were not set to default values. + # Note that in python, default arguments must be immutable, and these are not. + params = params or parameters.SolveParameters() + model_params = model_params or model_parameters.ModelSolveParameters() + callback_reg = callback_reg or callback.CallbackRegistration() + streamable_init_args = ( + streamable_init_args or init_arguments.StreamableSolverInitArguments() + ) + model_proto = opt_model.export_model() + proto_cb = None + if cb is not None: + proto_cb = lambda x: cb( # pylint: disable=g-long-lambda + callback.parse_callback_data(x, opt_model) + ).to_proto() + # Solve + try: + proto_result = solver.solve( + model_proto, + solver_type.value, + streamable_init_args.to_proto(), + params.to_proto(), + model_params.to_proto(), + msg_cb, + callback_reg.to_proto(), + proto_cb, + None, ) - model_proto = opt_model.export_model() - proto_cb = None - if cb is not None: - proto_cb = lambda x: cb( # pylint: disable=g-long-lambda - callback.parse_callback_data(x, opt_model) - ).to_proto() - # Solve - try: - proto_result = solver.solve( - model_proto, - solver_type.value, - streamable_init_args.to_proto(), - params.to_proto(), - model_params.to_proto(), - msg_cb, - callback_reg.to_proto(), - proto_cb, - None, - ) - except StatusNotOk as e: - raise _status_not_ok_to_exception(e) from None - return result.parse_solve_result(proto_result, opt_model, validate=False) + except StatusNotOk as e: + raise _status_not_ok_to_exception(e) from None + return result.parse_solve_result(proto_result, opt_model, validate=False) def compute_infeasible_subsystem( @@ -104,208 +106,208 @@ def compute_infeasible_subsystem( *, params: Optional[parameters.SolveParameters] = None, msg_cb: Optional[message_callback.SolveMessageCallback] = None, - streamable_init_args: Optional[init_arguments.StreamableSolverInitArguments] = None, + streamable_init_args: Optional[ + init_arguments.StreamableSolverInitArguments + ] = None, ) -> compute_infeasible_subsystem_result.ComputeInfeasibleSubsystemResult: - """Computes an infeasible subsystem of the input model. + """Computes an infeasible subsystem of the input model. + + Args: + opt_model: The optimization model to check for infeasibility. + solver_type: Which solver to use to compute the infeasible subsystem. As of + August 2023, the only supported solver is Gurobi. + params: Configuration of the underlying solver. + msg_cb: A callback that gives back the underlying solver's logs by the line. + streamable_init_args: Configuration for initializing the underlying solver. + + Returns: + An `ComputeInfeasibleSubsystemResult` where `feasibility` indicates if the + problem was proven infeasible. + + Throws: + RuntimeError: on invalid inputs or an internal solver error. + """ + params = params or parameters.SolveParameters() + streamable_init_args = ( + streamable_init_args or init_arguments.StreamableSolverInitArguments() + ) + model_proto = opt_model.export_model() + # Solve + try: + proto_result = solver.compute_infeasible_subsystem( + model_proto, + solver_type.value, + streamable_init_args.to_proto(), + params.to_proto(), + msg_cb, + None, + ) + except StatusNotOk as e: + raise _status_not_ok_to_exception(e) from None + return compute_infeasible_subsystem_result.parse_compute_infeasible_subsystem_result( + proto_result, opt_model + ) + + +class IncrementalSolver: + """Solve an optimization multiple times, with modifications between solves. + + Prefer calling simply solve() above in most cases when incrementalism is not + needed. + + Thread-safety: The __init__(), solve() methods must not be called while + modifying the Model (adding variables...). The user is expected to use proper + synchronization primitives to serialize changes to the model and the use of + this object. Note though that it is safe to call methods from different + IncrementalSolver instances on the same Model concurrently. The solve() method + must not be called concurrently on different threads for the same + IncrementalSolver. Some solvers may add more restriction regarding + threading. Please see to SolverType::XXX documentation for details. + + This class references some resources that are freed when it is garbage + collected (which should usually happen when the last reference is lost). In + particular, it references some C++ objects. Although it is not mandatory, it + is recommended to free those as soon as possible. To do so it is possible to + use this class in the `with` statement: + + with IncrementalSolver(model, SolverType.GLOP) as solver: + ... + + When it is not possible to use `with`, the close() method can be called. + """ + + def __init__( + self, + opt_model: model.Model, + solver_type: parameters.SolverType, + *, + streamable_init_args: Optional[ + init_arguments.StreamableSolverInitArguments + ] = None, + ): + streamable_init_args = ( + streamable_init_args or init_arguments.StreamableSolverInitArguments() + ) + self._model = opt_model + self._solver_type = solver_type + self._update_tracker = self._model.add_update_tracker() + try: + self._proto_solver = solver.new( + solver_type.value, + self._model.export_model(), + streamable_init_args.to_proto(), + ) + except StatusNotOk as e: + raise _status_not_ok_to_exception(e) from None + self._closed = False + + def solve( + self, + *, + params: Optional[parameters.SolveParameters] = None, + model_params: Optional[model_parameters.ModelSolveParameters] = None, + msg_cb: Optional[message_callback.SolveMessageCallback] = None, + callback_reg: Optional[callback.CallbackRegistration] = None, + cb: Optional[SolveCallback] = None, + ) -> result.SolveResult: + """Solves the current optimization model. Args: - opt_model: The optimization model to check for infeasibility. - solver_type: Which solver to use to compute the infeasible subsystem. As of - August 2023, the only supported solver is Gurobi. - params: Configuration of the underlying solver. - msg_cb: A callback that gives back the underlying solver's logs by the line. - streamable_init_args: Configuration for initializing the underlying solver. + params: The non-model specific solve parameters. + model_params: The model specific solve parameters. + msg_cb: An optional callback for solver messages. + callback_reg: The parameters controlling when cb is called. + cb: An optional callback for LP/MIP events. Returns: - An `ComputeInfeasibleSubsystemResult` where `feasibility` indicates if the - problem was proven infeasible. + The result of the solve. - Throws: - RuntimeError: on invalid inputs or an internal solver error. + Raises: + RuntimeError: If called after being closed, or on a solve error. """ + if self._closed: + raise RuntimeError("the solver is closed") + + update = self._update_tracker.export_update() + if update is not None: + try: + if not self._proto_solver.update(update): + self._proto_solver = solver.new( + self._solver_type.value, + self._model.export_model(), + parameters_pb2.SolverInitializerProto(), + ) + except StatusNotOk as e: + raise _status_not_ok_to_exception(e) from None + self._update_tracker.advance_checkpoint() params = params or parameters.SolveParameters() - streamable_init_args = ( - streamable_init_args or init_arguments.StreamableSolverInitArguments() - ) - model_proto = opt_model.export_model() - # Solve + model_params = model_params or model_parameters.ModelSolveParameters() + callback_reg = callback_reg or callback.CallbackRegistration() + proto_cb = None + if cb is not None: + proto_cb = lambda x: cb( # pylint: disable=g-long-lambda + callback.parse_callback_data(x, self._model) + ).to_proto() try: - proto_result = solver.compute_infeasible_subsystem( - model_proto, - solver_type.value, - streamable_init_args.to_proto(), - params.to_proto(), - msg_cb, - None, - ) + result_proto = self._proto_solver.solve( + params.to_proto(), + model_params.to_proto(), + msg_cb, + callback_reg.to_proto(), + proto_cb, + None, + ) except StatusNotOk as e: - raise _status_not_ok_to_exception(e) from None - return ( - compute_infeasible_subsystem_result.parse_compute_infeasible_subsystem_result( - proto_result, opt_model - ) - ) + raise _status_not_ok_to_exception(e) from None + return result.parse_solve_result(result_proto, self._model, validate=False) + def close(self) -> None: + """Closes this solver, freeing all its resources. -class IncrementalSolver: - """Solve an optimization multiple times, with modifications between solves. - - Prefer calling simply solve() above in most cases when incrementalism is not - needed. - - Thread-safety: The __init__(), solve() methods must not be called while - modifying the Model (adding variables...). The user is expected to use proper - synchronization primitives to serialize changes to the model and the use of - this object. Note though that it is safe to call methods from different - IncrementalSolver instances on the same Model concurrently. The solve() method - must not be called concurrently on different threads for the same - IncrementalSolver. Some solvers may add more restriction regarding - threading. Please see to SolverType::XXX documentation for details. - - This class references some resources that are freed when it is garbage - collected (which should usually happen when the last reference is lost). In - particular, it references some C++ objects. Although it is not mandatory, it - is recommended to free those as soon as possible. To do so it is possible to - use this class in the `with` statement: + This is optional, the code is correct without calling this function. See the + class documentation for details. + + After a solver has been closed, it can't be used anymore. Prefer using the + context manager API when possible instead of calling close() directly: with IncrementalSolver(model, SolverType.GLOP) as solver: ... - - When it is not possible to use `with`, the close() method can be called. """ + if self._closed: + return + self._closed = True - def __init__( - self, - opt_model: model.Model, - solver_type: parameters.SolverType, - *, - streamable_init_args: Optional[ - init_arguments.StreamableSolverInitArguments - ] = None, - ): - streamable_init_args = ( - streamable_init_args or init_arguments.StreamableSolverInitArguments() - ) - self._model = opt_model - self._solver_type = solver_type - self._update_tracker = self._model.add_update_tracker() - try: - self._proto_solver = solver.new( - solver_type.value, - self._model.export_model(), - streamable_init_args.to_proto(), - ) - except StatusNotOk as e: - raise _status_not_ok_to_exception(e) from None - self._closed = False - - def solve( - self, - *, - params: Optional[parameters.SolveParameters] = None, - model_params: Optional[model_parameters.ModelSolveParameters] = None, - msg_cb: Optional[message_callback.SolveMessageCallback] = None, - callback_reg: Optional[callback.CallbackRegistration] = None, - cb: Optional[SolveCallback] = None, - ) -> result.SolveResult: - """Solves the current optimization model. - - Args: - params: The non-model specific solve parameters. - model_params: The model specific solve parameters. - msg_cb: An optional callback for solver messages. - callback_reg: The parameters controlling when cb is called. - cb: An optional callback for LP/MIP events. - - Returns: - The result of the solve. - - Raises: - RuntimeError: If called after being closed, or on a solve error. - """ - if self._closed: - raise RuntimeError("the solver is closed") - - update = self._update_tracker.export_update() - if update is not None: - try: - if not self._proto_solver.update(update): - self._proto_solver = solver.new( - self._solver_type.value, - self._model.export_model(), - parameters_pb2.SolverInitializerProto(), - ) - except StatusNotOk as e: - raise _status_not_ok_to_exception(e) from None - self._update_tracker.advance_checkpoint() - params = params or parameters.SolveParameters() - model_params = model_params or model_parameters.ModelSolveParameters() - callback_reg = callback_reg or callback.CallbackRegistration() - proto_cb = None - if cb is not None: - proto_cb = lambda x: cb( # pylint: disable=g-long-lambda - callback.parse_callback_data(x, self._model) - ).to_proto() - try: - result_proto = self._proto_solver.solve( - params.to_proto(), - model_params.to_proto(), - msg_cb, - callback_reg.to_proto(), - proto_cb, - None, - ) - except StatusNotOk as e: - raise _status_not_ok_to_exception(e) from None - return result.parse_solve_result(result_proto, self._model, validate=False) - - def close(self) -> None: - """Closes this solver, freeing all its resources. - - This is optional, the code is correct without calling this function. See the - class documentation for details. - - After a solver has been closed, it can't be used anymore. Prefer using the - context manager API when possible instead of calling close() directly: - - with IncrementalSolver(model, SolverType.GLOP) as solver: - ... - """ - if self._closed: - return - self._closed = True - - del self._model - del self._solver_type - del self._update_tracker - del self._proto_solver - - def __enter__(self) -> "IncrementalSolver": - """Returns the solver itself.""" - return self - - def __exit__( - self, - exc_type: Optional[type[BaseException]], - exc_val: Optional[BaseException], - exc_tb: Optional[types.TracebackType], - ) -> None: - """Closes the solver.""" - self.close() + del self._model + del self._solver_type + del self._update_tracker + del self._proto_solver + def __enter__(self) -> "IncrementalSolver": + """Returns the solver itself.""" + return self -def _status_not_ok_to_exception(err: StatusNotOk) -> Exception: - """Converts a StatusNotOk to the best matching Python exception. + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[types.TracebackType], + ) -> None: + """Closes the solver.""" + self.close() - Args: - err: The input errors. - Returns: - The corresponding exception. - """ - ret = errors.status_proto_to_exception( - rpc_pb2.StatusProto(code=err.canonical_code, message=err.message) - ) - # We never expect StatusNotOk to be OK. - assert ret is not None, err - return ret +def _status_not_ok_to_exception(err: StatusNotOk) -> Exception: + """Converts a StatusNotOk to the best matching Python exception. + + Args: + err: The input errors. + + Returns: + The corresponding exception. + """ + ret = errors.status_proto_to_exception( + rpc_pb2.StatusProto(code=err.canonical_code, message=err.message) + ) + # We never expect StatusNotOk to be OK. + assert ret is not None, err + return ret diff --git a/ortools/math_opt/python/solve_gurobi_test.py b/ortools/math_opt/python/solve_gurobi_test.py index 4321eceb7ca..8e9813948d3 100644 --- a/ortools/math_opt/python/solve_gurobi_test.py +++ b/ortools/math_opt/python/solve_gurobi_test.py @@ -41,240 +41,240 @@ def _init_args( gurobi_key: init_arguments.GurobiISVKey, ) -> init_arguments.StreamableSolverInitArguments: - return init_arguments.StreamableSolverInitArguments( - gurobi=init_arguments.StreamableGurobiInitArguments(isv_key=gurobi_key) - ) + return init_arguments.StreamableSolverInitArguments( + gurobi=init_arguments.StreamableGurobiInitArguments(isv_key=gurobi_key) + ) class SolveTest(absltest.TestCase): - def test_callback(self) -> None: - mod = model.Model(name="test_model") - # Solve the problem: - # max x + 2 * y - # s.t. x + y <= 1 (added in callback) - # x, y in {0, 1} - # Primal optimal: [x, y] = [0.0, 1.0] - x = mod.add_binary_variable(name="x") - y = mod.add_binary_variable(name="y") - mod.objective.is_maximize = True - mod.objective.set_linear_coefficient(x, 1.0) - mod.objective.set_linear_coefficient(y, 2.0) + def test_callback(self) -> None: + mod = model.Model(name="test_model") + # Solve the problem: + # max x + 2 * y + # s.t. x + y <= 1 (added in callback) + # x, y in {0, 1} + # Primal optimal: [x, y] = [0.0, 1.0] + x = mod.add_binary_variable(name="x") + y = mod.add_binary_variable(name="y") + mod.objective.is_maximize = True + mod.objective.set_linear_coefficient(x, 1.0) + mod.objective.set_linear_coefficient(y, 2.0) - def cb(cb_data: callback.CallbackData) -> callback.CallbackResult: - cb_res = callback.CallbackResult() - if cb_data.solution[x] + cb_data.solution[y] >= 1 + 1e-4: - cb_res.add_lazy_constraint(x + y <= 1.0) - return cb_res + def cb(cb_data: callback.CallbackData) -> callback.CallbackResult: + cb_res = callback.CallbackResult() + if cb_data.solution[x] + cb_data.solution[y] >= 1 + 1e-4: + cb_res.add_lazy_constraint(x + y <= 1.0) + return cb_res - cb_reg = callback.CallbackRegistration() - cb_reg.events.add(callback.Event.MIP_SOLUTION) - cb_reg.add_lazy_constraints = True - params = parameters.SolveParameters(enable_output=True) - res = solve.solve( - mod, - parameters.SolverType.GUROBI, - params=params, - callback_reg=cb_reg, - cb=cb, - ) - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertAlmostEqual(2.0, res.termination.objective_bounds.primal_bound) + cb_reg = callback.CallbackRegistration() + cb_reg.events.add(callback.Event.MIP_SOLUTION) + cb_reg.add_lazy_constraints = True + params = parameters.SolveParameters(enable_output=True) + res = solve.solve( + mod, + parameters.SolverType.GUROBI, + params=params, + callback_reg=cb_reg, + cb=cb, + ) + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertAlmostEqual(2.0, res.termination.objective_bounds.primal_bound) - def test_hierarchical_objectives(self) -> None: - mod = model.Model() - # The model is: - # max x + y + 2 z - # s.t. x + y + z <= 1.5 - # x, y in [0, 1] - # z binary - # With secondary objective - # max y - # - # The first problem is solved by any convex combination of: - # (0.5, 0, 1) and (0, 0.5, 1) - # But with the secondary objective, the unique solution is (0, 0.5, 1), with - # a primary objective value of 2.5 and secondary objective value of 0.5. - x = mod.add_variable(lb=0, ub=1) - y = mod.add_variable(lb=0, ub=1) - z = mod.add_binary_variable() - mod.add_linear_constraint(x + y + z <= 1.5) - mod.maximize(x + y + 2 * z) - aux = mod.add_maximization_objective(y, priority=1) + def test_hierarchical_objectives(self) -> None: + mod = model.Model() + # The model is: + # max x + y + 2 z + # s.t. x + y + z <= 1.5 + # x, y in [0, 1] + # z binary + # With secondary objective + # max y + # + # The first problem is solved by any convex combination of: + # (0.5, 0, 1) and (0, 0.5, 1) + # But with the secondary objective, the unique solution is (0, 0.5, 1), with + # a primary objective value of 2.5 and secondary objective value of 0.5. + x = mod.add_variable(lb=0, ub=1) + y = mod.add_variable(lb=0, ub=1) + z = mod.add_binary_variable() + mod.add_linear_constraint(x + y + z <= 1.5) + mod.maximize(x + y + 2 * z) + aux = mod.add_maximization_objective(y, priority=1) - res = solve.solve(mod, parameters.SolverType.GUROBI) - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertAlmostEqual(res.objective_value(), 2.5, delta=1e-4) - self.assertAlmostEqual(res.variable_values(x), 0.0, delta=1e-4) - self.assertAlmostEqual(res.variable_values(y), 0.5, delta=1e-4) - self.assertAlmostEqual(res.variable_values(z), 1.0, delta=1e-4) - prim_sol = res.solutions[0].primal_solution - self.assertIsNotNone(prim_sol) - self.assertDictEqual(prim_sol.auxiliary_objective_values, {aux: 0.5}) + res = solve.solve(mod, parameters.SolverType.GUROBI) + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertAlmostEqual(res.objective_value(), 2.5, delta=1e-4) + self.assertAlmostEqual(res.variable_values(x), 0.0, delta=1e-4) + self.assertAlmostEqual(res.variable_values(y), 0.5, delta=1e-4) + self.assertAlmostEqual(res.variable_values(z), 1.0, delta=1e-4) + prim_sol = res.solutions[0].primal_solution + self.assertIsNotNone(prim_sol) + self.assertDictEqual(prim_sol.auxiliary_objective_values, {aux: 0.5}) - def test_quadratic_dual(self) -> None: - mod = model.Model() - x = mod.add_variable() - mod.minimize(x) - c = mod.add_quadratic_constraint(expr=x * x, ub=1.0) - params = parameters.SolveParameters() - params.gurobi.param_values["QCPDual"] = "1" - res = solve.solve(mod, parameters.SolverType.GUROBI, params=params) - self.assertEqual(res.termination.reason, result.TerminationReason.OPTIMAL) - sol = res.solutions[0] - primal = sol.primal_solution - dual = sol.dual_solution - self.assertIsNotNone(primal) - self.assertIsNotNone(dual) - self.assertAlmostEqual(primal.variable_values[x], -1.0) - self.assertAlmostEqual(dual.quadratic_dual_values[c], -0.5) + def test_quadratic_dual(self) -> None: + mod = model.Model() + x = mod.add_variable() + mod.minimize(x) + c = mod.add_quadratic_constraint(expr=x * x, ub=1.0) + params = parameters.SolveParameters() + params.gurobi.param_values["QCPDual"] = "1" + res = solve.solve(mod, parameters.SolverType.GUROBI, params=params) + self.assertEqual(res.termination.reason, result.TerminationReason.OPTIMAL) + sol = res.solutions[0] + primal = sol.primal_solution + dual = sol.dual_solution + self.assertIsNotNone(primal) + self.assertIsNotNone(dual) + self.assertAlmostEqual(primal.variable_values[x], -1.0) + self.assertAlmostEqual(dual.quadratic_dual_values[c], -0.5) - def test_quadratic_dual_filter(self) -> None: - # Same as the previous test, but now with a filter on the quadratic duals - # that are returned. - mod = model.Model() - x = mod.add_variable() - mod.minimize(x) - mod.add_quadratic_constraint(expr=x * x, ub=1.0) - params = parameters.SolveParameters() - params.gurobi.param_values["QCPDual"] = "1" - mod_params = model_parameters.ModelSolveParameters( - quadratic_dual_values_filter=sparse_containers.QuadraticConstraintFilter( - filtered_items={} - ) - ) - res = solve.solve( - mod, - parameters.SolverType.GUROBI, - params=params, - model_params=mod_params, + def test_quadratic_dual_filter(self) -> None: + # Same as the previous test, but now with a filter on the quadratic duals + # that are returned. + mod = model.Model() + x = mod.add_variable() + mod.minimize(x) + mod.add_quadratic_constraint(expr=x * x, ub=1.0) + params = parameters.SolveParameters() + params.gurobi.param_values["QCPDual"] = "1" + mod_params = model_parameters.ModelSolveParameters( + quadratic_dual_values_filter=sparse_containers.QuadraticConstraintFilter( + filtered_items={} ) - self.assertEqual(res.termination.reason, result.TerminationReason.OPTIMAL) - sol = res.solutions[0] - primal = sol.primal_solution - dual = sol.dual_solution - self.assertIsNotNone(primal) - self.assertIsNotNone(dual) - self.assertAlmostEqual(primal.variable_values[x], -1.0) - self.assertEmpty(dual.quadratic_dual_values) + ) + res = solve.solve( + mod, + parameters.SolverType.GUROBI, + params=params, + model_params=mod_params, + ) + self.assertEqual(res.termination.reason, result.TerminationReason.OPTIMAL) + sol = res.solutions[0] + primal = sol.primal_solution + dual = sol.dual_solution + self.assertIsNotNone(primal) + self.assertIsNotNone(dual) + self.assertAlmostEqual(primal.variable_values[x], -1.0) + self.assertEmpty(dual.quadratic_dual_values) - def test_compute_infeasible_subsystem_infeasible(self): - mod = model.Model() - x = mod.add_variable(lb=0.0, ub=1.0) - y = mod.add_variable(lb=0.0, ub=1.0) - z = mod.add_variable(lb=0.0, ub=1.0) - mod.add_linear_constraint(x + y <= 4.0) - d = mod.add_linear_constraint(x + z >= 3.0) + def test_compute_infeasible_subsystem_infeasible(self): + mod = model.Model() + x = mod.add_variable(lb=0.0, ub=1.0) + y = mod.add_variable(lb=0.0, ub=1.0) + z = mod.add_variable(lb=0.0, ub=1.0) + mod.add_linear_constraint(x + y <= 4.0) + d = mod.add_linear_constraint(x + z >= 3.0) - iis = solve.compute_infeasible_subsystem(mod, parameters.SolverType.GUROBI) - self.assertTrue(iis.is_minimal) - self.assertEqual(iis.feasibility, result.FeasibilityStatus.INFEASIBLE) - self.assertDictEqual( - iis.infeasible_subsystem.variable_bounds, - {x: _Bounds(upper=True), z: _Bounds(upper=True)}, - ) - self.assertDictEqual( - iis.infeasible_subsystem.linear_constraints, {d: _Bounds(lower=True)} - ) - self.assertEmpty(iis.infeasible_subsystem.variable_integrality) + iis = solve.compute_infeasible_subsystem(mod, parameters.SolverType.GUROBI) + self.assertTrue(iis.is_minimal) + self.assertEqual(iis.feasibility, result.FeasibilityStatus.INFEASIBLE) + self.assertDictEqual( + iis.infeasible_subsystem.variable_bounds, + {x: _Bounds(upper=True), z: _Bounds(upper=True)}, + ) + self.assertDictEqual( + iis.infeasible_subsystem.linear_constraints, {d: _Bounds(lower=True)} + ) + self.assertEmpty(iis.infeasible_subsystem.variable_integrality) - def test_solve_valid_isv_success(self): - mod = model.Model() - x = mod.add_binary_variable() - mod.maximize(x) - res = solve.solve( - mod, - parameters.SolverType.GUROBI, - streamable_init_args=_init_args( - gurobi_test_isv_key.google_test_isv_key_placeholder() - ), - ) - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertAlmostEqual(1.0, res.termination.objective_bounds.primal_bound) + def test_solve_valid_isv_success(self): + mod = model.Model() + x = mod.add_binary_variable() + mod.maximize(x) + res = solve.solve( + mod, + parameters.SolverType.GUROBI, + streamable_init_args=_init_args( + gurobi_test_isv_key.google_test_isv_key_placeholder() + ), + ) + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertAlmostEqual(1.0, res.termination.objective_bounds.primal_bound) - def test_solve_wrong_isv_error(self): - mod = model.Model() - x = mod.add_binary_variable() - mod.maximize(x) - with self.assertRaisesRegex( - ValueError, "failed to create Gurobi primary environment with ISV key" - ): - solve.solve( - mod, - parameters.SolverType.GUROBI, - streamable_init_args=_init_args(_bad_isv_key), - ) + def test_solve_wrong_isv_error(self): + mod = model.Model() + x = mod.add_binary_variable() + mod.maximize(x) + with self.assertRaisesRegex( + ValueError, "failed to create Gurobi primary environment with ISV key" + ): + solve.solve( + mod, + parameters.SolverType.GUROBI, + streamable_init_args=_init_args(_bad_isv_key), + ) - def test_incremental_solver_valid_isv_success(self): - mod = model.Model() - x = mod.add_binary_variable() - mod.maximize(x) - s = solve.IncrementalSolver( - mod, - parameters.SolverType.GUROBI, - streamable_init_args=_init_args( - gurobi_test_isv_key.google_test_isv_key_placeholder() - ), - ) - res = s.solve() - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertAlmostEqual(1.0, res.termination.objective_bounds.primal_bound) + def test_incremental_solver_valid_isv_success(self): + mod = model.Model() + x = mod.add_binary_variable() + mod.maximize(x) + s = solve.IncrementalSolver( + mod, + parameters.SolverType.GUROBI, + streamable_init_args=_init_args( + gurobi_test_isv_key.google_test_isv_key_placeholder() + ), + ) + res = s.solve() + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertAlmostEqual(1.0, res.termination.objective_bounds.primal_bound) - def test_incremental_solver_wrong_isv_error(self): - mod = model.Model() - x = mod.add_binary_variable() - mod.maximize(x) - with self.assertRaisesRegex( - ValueError, "failed to create Gurobi primary environment with ISV key" - ): - solve.IncrementalSolver( - mod, - parameters.SolverType.GUROBI, - streamable_init_args=_init_args(_bad_isv_key), - ) + def test_incremental_solver_wrong_isv_error(self): + mod = model.Model() + x = mod.add_binary_variable() + mod.maximize(x) + with self.assertRaisesRegex( + ValueError, "failed to create Gurobi primary environment with ISV key" + ): + solve.IncrementalSolver( + mod, + parameters.SolverType.GUROBI, + streamable_init_args=_init_args(_bad_isv_key), + ) - def test_compute_infeasible_subsystem_valid_isv_success(self): - mod = model.Model() - x = mod.add_binary_variable() - mod.add_linear_constraint(x >= 3.0) - res = solve.compute_infeasible_subsystem( - mod, - parameters.SolverType.GUROBI, - streamable_init_args=_init_args( - gurobi_test_isv_key.google_test_isv_key_placeholder() - ), - ) - self.assertEqual(res.feasibility, result.FeasibilityStatus.INFEASIBLE) + def test_compute_infeasible_subsystem_valid_isv_success(self): + mod = model.Model() + x = mod.add_binary_variable() + mod.add_linear_constraint(x >= 3.0) + res = solve.compute_infeasible_subsystem( + mod, + parameters.SolverType.GUROBI, + streamable_init_args=_init_args( + gurobi_test_isv_key.google_test_isv_key_placeholder() + ), + ) + self.assertEqual(res.feasibility, result.FeasibilityStatus.INFEASIBLE) - def test_compute_infeasible_subsystem_wrong_isv_error(self): - mod = model.Model() - x = mod.add_binary_variable() - mod.add_linear_constraint(x >= 3.0) - with self.assertRaisesRegex( - ValueError, "failed to create Gurobi primary environment with ISV key" - ): - solve.compute_infeasible_subsystem( - mod, - parameters.SolverType.GUROBI, - streamable_init_args=_init_args(_bad_isv_key), - ) + def test_compute_infeasible_subsystem_wrong_isv_error(self): + mod = model.Model() + x = mod.add_binary_variable() + mod.add_linear_constraint(x >= 3.0) + with self.assertRaisesRegex( + ValueError, "failed to create Gurobi primary environment with ISV key" + ): + solve.compute_infeasible_subsystem( + mod, + parameters.SolverType.GUROBI, + streamable_init_args=_init_args(_bad_isv_key), + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/solve_test.py b/ortools/math_opt/python/solve_test.py index 95b77eeb82a..4a6b77528b3 100644 --- a/ortools/math_opt/python/solve_test.py +++ b/ortools/math_opt/python/solve_test.py @@ -39,497 +39,530 @@ ) -def _list_is_near(v1: List[float], v2: List[float], tolerance: float = 1e-5) -> bool: - if len(v1) != len(v2): - return False - for i, v in enumerate(v1): - if abs(v2[i] - v) > tolerance: - return False - return True +def _list_is_near( + v1: List[float], v2: List[float], tolerance: float = 1e-5 +) -> bool: + if len(v1) != len(v2): + return False + for i, v in enumerate(v1): + if abs(v2[i] - v) > tolerance: + return False + return True class SolveTest(absltest.TestCase): - def _assert_dict_almost_equal( - self, expected: VarOrConstraintDict, actual: VarOrConstraintDict, places=5 + def _assert_dict_almost_equal( + self, expected: VarOrConstraintDict, actual: VarOrConstraintDict, places=5 + ): + act_keys = set(actual.keys()) + exp_keys = set(expected.keys()) + self.assertSetEqual( + exp_keys, + act_keys, + msg=f"actual keys: {act_keys} not equal expected keys: {exp_keys}", + ) + for k, v in expected.items(): + self.assertAlmostEqual( + v, + actual[k], + places, + msg=f"actual: {actual} and expected: {expected} disagree on key: {k}", + ) + + def test_solve_error(self) -> None: + mod = model.Model(name="test_model") + mod.add_variable(lb=1.0, ub=-1.0, name="x1") + with self.assertRaisesRegex( + ValueError, "variables.*lower_bound > upper_bound" ): - act_keys = set(actual.keys()) - exp_keys = set(expected.keys()) - self.assertSetEqual( - exp_keys, - act_keys, - msg=f"actual keys: {act_keys} not equal expected keys: {exp_keys}", - ) - for k, v in expected.items(): - self.assertAlmostEqual( - v, - actual[k], - places, - msg=f"actual: {actual} and expected: {expected} disagree on key: {k}", - ) - - def test_solve_error(self) -> None: - mod = model.Model(name="test_model") - mod.add_variable(lb=1.0, ub=-1.0, name="x1") - with self.assertRaisesRegex(ValueError, "variables.*lower_bound > upper_bound"): - solve.solve(mod, parameters.SolverType.GLOP) - - def test_lp_solve(self) -> None: - mod = model.Model(name="test_model") - # Solve the problem: - # max x1 + 2 * x2 - # s.t. x1 + x2 <= 1 - # x1, x2 in [0, 1] - # Primal optimal: [x1, x2] = [0.0, 1.0] - # - # Following: go/mathopt-dual#primal-dual-optimal-pairs, the optimal dual - # solution [y, r1, r2] for [x1, x2] = [0.0, 1.0] must satisfy: - # y + r1 = 1 - # y + r2 = 2 - # y >= 0 - # r1 <= 0 - # r2 >= 0 - # We see that any convex combination of [1, 0, 1] and [2, -1, 0] gives a - # dual optimal solution. - x1 = mod.add_variable(lb=0.0, ub=1.0, name="x1") - x2 = mod.add_variable(lb=0.0, ub=1.0, name="x2") - mod.objective.is_maximize = True - mod.objective.set_linear_coefficient(x1, 1.0) - mod.objective.set_linear_coefficient(x2, 2.0) - c = mod.add_linear_constraint(ub=1.0, name="c") - c.set_coefficient(x1, 1.0) - c.set_coefficient(x2, 1.0) - res = solve.solve(mod, parameters.SolverType.GLOP) - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertAlmostEqual(2.0, res.termination.objective_bounds.primal_bound) - self.assertGreaterEqual(len(res.solutions), 1) - assert ( - res.solutions[0].primal_solution is not None - and res.solutions[0].dual_solution is not None - ) - self.assertAlmostEqual(2.0, res.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - {x1: 0.0, x2: 1.0}, res.solutions[0].primal_solution.variable_values - ) - dual = res.solutions[0].dual_solution - self.assertAlmostEqual(2.0, dual.objective_value) - self.assertSetEqual({c}, set(dual.dual_values.keys())) - self.assertSetEqual({x1, x2}, set(dual.reduced_costs.keys())) - # Possible values for [y, r1, r2] are [1, 0, 1] and [2, -1, 0]. - dual_vec = [ - dual.dual_values[c], - dual.reduced_costs[x1], - dual.reduced_costs[x2], - ] - # Warning: the code below assumes the returned solution is a basic solution. - # If a non-simplex solver was used, we could get any convex combination of - # the vectors below. - expected1 = [1.0, 0.0, 1.0] - expected2 = [2.0, -1.0, 0.0] - self.assertTrue( - _list_is_near(expected1, dual_vec) or _list_is_near(expected2, dual_vec), - msg=f"dual_vec is {dual_vec}; expected {expected1} or {expected2}", - ) - - def test_indicator(self) -> None: - # min 2 * x + y + 10 z - # s.t. if not z then x + y >= 6 - # x, y >= 0 - # z binary - # - # Optimal solution is (x, y, z) = (0, 6, 0), objective value 6. - mod = model.Model() - x = mod.add_variable(lb=0) - y = mod.add_variable(lb=0) - z = mod.add_binary_variable() - mod.add_indicator_constraint( - indicator=z, activate_on_zero=True, implied_constraint=x + y >= 6.0 - ) - mod.minimize(2 * x + y + 10 * z) - - res = solve.solve(mod, parameters.SolverType.GSCIP) - self.assertEqual(res.termination.reason, result.TerminationReason.OPTIMAL) - self.assertAlmostEqual(res.objective_value(), 6.0, delta=1e-5) - self.assertAlmostEqual(res.variable_values(x), 0.0, delta=1e-5) - self.assertAlmostEqual(res.variable_values(y), 6.0, delta=1e-5) - self.assertAlmostEqual(res.variable_values(z), 0.0, delta=1e-5) - - def test_filters(self) -> None: - mod = model.Model(name="test_model") - # Solve the problem: - # max x + y + z - # s.t. x + y <= 1 - # y + z <= 1 - # x, y, z in [0, 1] - # Primal optimal: [x, y, z] = [1.0, 0.0, 1.0] - x = mod.add_variable(lb=0.0, ub=1.0, name="x") - y = mod.add_variable(lb=0.0, ub=1.0, name="y") - z = mod.add_variable(lb=0.0, ub=1.0, name="z") - mod.objective.is_maximize = True - mod.objective.set_linear_coefficient(x, 1.0) - mod.objective.set_linear_coefficient(y, 1.0) - mod.objective.set_linear_coefficient(z, 1.0) - c = mod.add_linear_constraint(ub=1.0, name="c") - c.set_coefficient(x, 1.0) - c.set_coefficient(y, 1.0) - d = mod.add_linear_constraint(ub=1.0, name="d") - d.set_coefficient(y, 1.0) - d.set_coefficient(z, 1.0) - model_params = model_parameters.ModelSolveParameters() - model_params.variable_values_filter = sparse_containers.SparseVectorFilter( - skip_zero_values=True - ) - model_params.dual_values_filter = sparse_containers.SparseVectorFilter( - filtered_items=[d] - ) - model_params.reduced_costs_filter = sparse_containers.SparseVectorFilter( - filtered_items=[y, z] - ) - res = solve.solve(mod, parameters.SolverType.GLOP, model_params=model_params) - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertAlmostEqual(2.0, res.termination.objective_bounds.primal_bound) - self.assertGreaterEqual(len(res.solutions), 1) - assert ( - res.solutions[0].primal_solution is not None - and res.solutions[0].dual_solution is not None - ) - self.assertAlmostEqual(2.0, res.solutions[0].primal_solution.objective_value) - # y is zero and thus filtered. - self._assert_dict_almost_equal( - {x: 1.0, z: 1.0}, res.solutions[0].primal_solution.variable_values - ) - self.assertGreaterEqual(len(res.solutions), 1) - dual = res.solutions[0].dual_solution - self.assertAlmostEqual(2.0, dual.objective_value) - # The dual was filtered by id - self.assertSetEqual({d}, set(dual.dual_values.keys())) - self.assertSetEqual({y, z}, set(dual.reduced_costs.keys())) - - def test_message_callback(self): - opt_model = model.Model() - x = opt_model.add_binary_variable(name="x") - opt_model.objective.is_maximize = True - opt_model.objective.set_linear_coefficient(x, 2.0) - - logs = io.StringIO() - res = solve.solve( - opt_model, - parameters.SolverType.GSCIP, - msg_cb=message_callback.printer_message_callback(file=logs), - ) - - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertIn("problem is solved", logs.getvalue()) - - def test_incremental_solve_init_error(self) -> None: - mod = model.Model(name="test_model") - mod.add_variable(lb=1.0, ub=1.0, name="x1") - mod.add_variable(lb=1.0, ub=1.0, name="x1") - with self.assertRaisesRegex(ValueError, "duplicate name*"): - solve.IncrementalSolver(mod, parameters.SolverType.GLOP) - - def test_incremental_solve_error(self) -> None: - mod = model.Model(name="test_model") - mod.add_variable(lb=1.0, ub=-1.0, name="x1") - solver = solve.IncrementalSolver(mod, parameters.SolverType.GLOP) - with self.assertRaisesRegex(ValueError, "variables.*lower_bound > upper_bound"): - solver.solve() - - def test_incremental_solve_error_on_reject(self) -> None: - opt_model = model.Model() - x = opt_model.add_binary_variable(name="x") - opt_model.objective.set_linear_coefficient(x, 2.0) - opt_model.objective.is_maximize = True - # CP-SAT rejects all model changes and solves from scratch each time. - solver = solve.IncrementalSolver(opt_model, parameters.SolverType.CP_SAT) - - res = solver.solve( - msg_cb=message_callback.printer_message_callback(prefix="[solve 1] ") - ) - - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res.solutions), 1) - assert res.solutions[0].primal_solution is not None - self.assertAlmostEqual(2.0, res.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - {x: 1.0}, res.solutions[0].primal_solution.variable_values - ) - - opt_model.add_binary_variable(name="x") - with self.assertRaisesRegex(ValueError, "duplicate name*"): - solver.solve( - msg_cb=message_callback.printer_message_callback(prefix="[solve 2] ") - ) - - def test_incremental_lp(self) -> None: - opt_model = model.Model() - x = opt_model.add_variable(lb=0, ub=1, name="x") - opt_model.objective.set_linear_coefficient(x, 2.0) - opt_model.objective.is_maximize = True - solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) - params = parameters.SolveParameters() - params.enable_output = True - res = solver.solve(params=params) - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res.solutions), 1) - assert res.solutions[0].primal_solution is not None - self.assertAlmostEqual(2.0, res.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - {x: 1.0}, res.solutions[0].primal_solution.variable_values - ) - - x.upper_bound = 3.0 - res2 = solver.solve(params=params) - self.assertEqual( - res2.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res2.solutions), 1) - self.assertAlmostEqual(6.0, res2.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - {x: 3.0}, res2.solutions[0].primal_solution.variable_values - ) - - def test_incremental_mip(self) -> None: - opt_model = model.Model() - x = opt_model.add_binary_variable(name="x") - y = opt_model.add_binary_variable(name="y") - c = opt_model.add_linear_constraint(ub=1.0, name="c") - c.set_coefficient(x, 1.0) - c.set_coefficient(y, 1.0) - opt_model.objective.set_linear_coefficient(x, 2.0) - opt_model.objective.set_linear_coefficient(y, 3.0) - opt_model.objective.is_maximize = True - solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GSCIP) - params = parameters.SolveParameters() - params.enable_output = True - res = solver.solve(params=params) - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res.solutions), 1) - assert res.solutions[0].primal_solution is not None - self.assertAlmostEqual(3.0, res.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - {x: 0.0, y: 1.0}, res.solutions[0].primal_solution.variable_values - ) - - c.upper_bound = 2.0 - res2 = solver.solve(params=params) - self.assertEqual( - res2.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res2.solutions), 1) - assert res2.solutions[0].primal_solution is not None - self.assertAlmostEqual(5.0, res2.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - {x: 1.0, y: 1.0}, res2.solutions[0].primal_solution.variable_values - ) - - def test_incremental_mip_with_message_cb(self) -> None: - opt_model = model.Model() - x = opt_model.add_binary_variable(name="x") - y = opt_model.add_binary_variable(name="y") - c = opt_model.add_linear_constraint(ub=1.0, name="c") - c.set_coefficient(x, 1.0) - c.set_coefficient(y, 1.0) - opt_model.objective.set_linear_coefficient(x, 2.0) - opt_model.objective.set_linear_coefficient(y, 3.0) - opt_model.objective.is_maximize = True - solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GSCIP) - params = parameters.SolveParameters() - params.enable_output = True - - logs = io.StringIO() - res = solver.solve( - params=params, - msg_cb=message_callback.printer_message_callback(file=logs), - ) - - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res.solutions), 1) - assert res.solutions[0].primal_solution is not None - self.assertAlmostEqual(3.0, res.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - {x: 0.0, y: 1.0}, res.solutions[0].primal_solution.variable_values - ) - self.assertNotIn(_SCIP_LOG_SOLVE_WAS_INCREMENTAL, logs.getvalue()) - - c.upper_bound = 2.0 - - logs = io.StringIO() - res2 = solver.solve( - params=params, - msg_cb=message_callback.printer_message_callback(file=logs), - ) - - self.assertEqual( - res2.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res2.solutions), 1) - assert res2.solutions[0].primal_solution is not None - self.assertAlmostEqual(5.0, res2.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - {x: 1.0, y: 1.0}, res2.solutions[0].primal_solution.variable_values - ) - self.assertIn(_SCIP_LOG_SOLVE_WAS_INCREMENTAL, logs.getvalue()) - - def test_incremental_solve_rejected(self) -> None: - opt_model = model.Model() - x = opt_model.add_binary_variable(name="x") - opt_model.objective.set_linear_coefficient(x, 2.0) - opt_model.objective.is_maximize = True - # CP-SAT rejects all model changes and solves from scratch each time. - solver = solve.IncrementalSolver(opt_model, parameters.SolverType.CP_SAT) - - res = solver.solve( - msg_cb=message_callback.printer_message_callback(prefix="[solve 1] ") - ) - - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res.solutions), 1) - assert res.solutions[0].primal_solution is not None - self.assertAlmostEqual(2.0, res.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - {x: 1.0}, res.solutions[0].primal_solution.variable_values - ) - - x.upper_bound = 3.0 - - res2 = solver.solve( - msg_cb=message_callback.printer_message_callback(prefix="[solve 2] ") - ) - - self.assertEqual( - res2.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res2.solutions), 1) - assert res2.solutions[0].primal_solution is not None - self.assertAlmostEqual(6.0, res2.solutions[0].primal_solution.objective_value) - self._assert_dict_almost_equal( - { - x: 3.0, - }, - res2.solutions[0].primal_solution.variable_values, - ) - - def test_multiple_incremental_lps(self) -> None: - opt_model = model.Model() - x = opt_model.add_variable(lb=0, ub=1, name="x") - opt_model.objective.set_linear_coefficient(x, 2.0) - opt_model.objective.is_maximize = True - solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) - params = parameters.SolveParameters() - params.presolve = parameters.Emphasis.OFF - params.enable_output = True - for ub in [2.0, 3.0, 4.0, 5.0]: - x.upper_bound = ub - res = solver.solve(params=params) - self.assertEqual( - res.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res.termination, - ) - self.assertGreaterEqual(len(res.solutions), 1) - assert res.solutions[0].primal_solution is not None - self.assertAlmostEqual( - 2.0 * ub, res.solutions[0].primal_solution.objective_value - ) - self._assert_dict_almost_equal( - {x: ub}, res.solutions[0].primal_solution.variable_values - ) - - def test_incremental_solver_delete(self) -> None: - opt_model = model.Model() - start_num_solver = core_solver.debug_num_solver() - solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) - self.assertEqual(core_solver.debug_num_solver(), start_num_solver + 1) - del solver - self.assertEqual(core_solver.debug_num_solver(), start_num_solver) - - def test_incremental_solver_close(self) -> None: - opt_model = model.Model() - start_num_solver = core_solver.debug_num_solver() - solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) - self.assertEqual(core_solver.debug_num_solver(), start_num_solver + 1) - solver.close() - self.assertEqual(core_solver.debug_num_solver(), start_num_solver) - with self.assertRaisesRegex(RuntimeError, "closed"): - solver.solve() - - def test_incremental_solver_close_twice(self) -> None: - opt_model = model.Model() - start_num_solver = core_solver.debug_num_solver() - solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) - self.assertEqual(core_solver.debug_num_solver(), start_num_solver + 1) - solver.close() - solver.close() - self.assertEqual(core_solver.debug_num_solver(), start_num_solver) - - def test_incremental_solver_context_manager(self) -> None: - opt_model = model.Model() - start_num_solver = core_solver.debug_num_solver() - with solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) as solver: - self.assertEqual(core_solver.debug_num_solver(), start_num_solver + 1) - res1 = solver.solve() - self.assertEqual( - res1.termination.reason, - result.TerminationReason.OPTIMAL, - msg=res1.termination, - ) - res2 = solver.solve() - self.assertEqual( - res2.termination.reason, result.TerminationReason.OPTIMAL, msg=res2 - ) - - self.assertEqual(core_solver.debug_num_solver(), start_num_solver) - with self.assertRaisesRegex(RuntimeError, "closed"): - solver.solve() - - def test_incremental_solver_context_manager_exception(self) -> None: - """Tests that exceptions raised in the context manager are not lost.""" - opt_model = model.Model() - with self.assertRaisesRegex(NotImplementedError, "some message"): - with solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP): - raise NotImplementedError("some message") + solve.solve(mod, parameters.SolverType.GLOP) + + def test_lp_solve(self) -> None: + mod = model.Model(name="test_model") + # Solve the problem: + # max x1 + 2 * x2 + # s.t. x1 + x2 <= 1 + # x1, x2 in [0, 1] + # Primal optimal: [x1, x2] = [0.0, 1.0] + # + # Following: go/mathopt-dual#primal-dual-optimal-pairs, the optimal dual + # solution [y, r1, r2] for [x1, x2] = [0.0, 1.0] must satisfy: + # y + r1 = 1 + # y + r2 = 2 + # y >= 0 + # r1 <= 0 + # r2 >= 0 + # We see that any convex combination of [1, 0, 1] and [2, -1, 0] gives a + # dual optimal solution. + x1 = mod.add_variable(lb=0.0, ub=1.0, name="x1") + x2 = mod.add_variable(lb=0.0, ub=1.0, name="x2") + mod.objective.is_maximize = True + mod.objective.set_linear_coefficient(x1, 1.0) + mod.objective.set_linear_coefficient(x2, 2.0) + c = mod.add_linear_constraint(ub=1.0, name="c") + c.set_coefficient(x1, 1.0) + c.set_coefficient(x2, 1.0) + res = solve.solve(mod, parameters.SolverType.GLOP) + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertAlmostEqual(2.0, res.termination.objective_bounds.primal_bound) + self.assertGreaterEqual(len(res.solutions), 1) + assert ( + res.solutions[0].primal_solution is not None + and res.solutions[0].dual_solution is not None + ) + self.assertAlmostEqual( + 2.0, res.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x1: 0.0, x2: 1.0}, res.solutions[0].primal_solution.variable_values + ) + dual = res.solutions[0].dual_solution + self.assertAlmostEqual(2.0, dual.objective_value) + self.assertSetEqual({c}, set(dual.dual_values.keys())) + self.assertSetEqual({x1, x2}, set(dual.reduced_costs.keys())) + # Possible values for [y, r1, r2] are [1, 0, 1] and [2, -1, 0]. + dual_vec = [ + dual.dual_values[c], + dual.reduced_costs[x1], + dual.reduced_costs[x2], + ] + # Warning: the code below assumes the returned solution is a basic solution. + # If a non-simplex solver was used, we could get any convex combination of + # the vectors below. + expected1 = [1.0, 0.0, 1.0] + expected2 = [2.0, -1.0, 0.0] + self.assertTrue( + _list_is_near(expected1, dual_vec) + or _list_is_near(expected2, dual_vec), + msg=f"dual_vec is {dual_vec}; expected {expected1} or {expected2}", + ) + + def test_indicator(self) -> None: + # min 2 * x + y + 10 z + # s.t. if not z then x + y >= 6 + # x, y >= 0 + # z binary + # + # Optimal solution is (x, y, z) = (0, 6, 0), objective value 6. + mod = model.Model() + x = mod.add_variable(lb=0) + y = mod.add_variable(lb=0) + z = mod.add_binary_variable() + mod.add_indicator_constraint( + indicator=z, activate_on_zero=True, implied_constraint=x + y >= 6.0 + ) + mod.minimize(2 * x + y + 10 * z) + + res = solve.solve(mod, parameters.SolverType.GSCIP) + self.assertEqual(res.termination.reason, result.TerminationReason.OPTIMAL) + self.assertAlmostEqual(res.objective_value(), 6.0, delta=1e-5) + self.assertAlmostEqual(res.variable_values(x), 0.0, delta=1e-5) + self.assertAlmostEqual(res.variable_values(y), 6.0, delta=1e-5) + self.assertAlmostEqual(res.variable_values(z), 0.0, delta=1e-5) + + def test_filters(self) -> None: + mod = model.Model(name="test_model") + # Solve the problem: + # max x + y + z + # s.t. x + y <= 1 + # y + z <= 1 + # x, y, z in [0, 1] + # Primal optimal: [x, y, z] = [1.0, 0.0, 1.0] + x = mod.add_variable(lb=0.0, ub=1.0, name="x") + y = mod.add_variable(lb=0.0, ub=1.0, name="y") + z = mod.add_variable(lb=0.0, ub=1.0, name="z") + mod.objective.is_maximize = True + mod.objective.set_linear_coefficient(x, 1.0) + mod.objective.set_linear_coefficient(y, 1.0) + mod.objective.set_linear_coefficient(z, 1.0) + c = mod.add_linear_constraint(ub=1.0, name="c") + c.set_coefficient(x, 1.0) + c.set_coefficient(y, 1.0) + d = mod.add_linear_constraint(ub=1.0, name="d") + d.set_coefficient(y, 1.0) + d.set_coefficient(z, 1.0) + model_params = model_parameters.ModelSolveParameters() + model_params.variable_values_filter = sparse_containers.SparseVectorFilter( + skip_zero_values=True + ) + model_params.dual_values_filter = sparse_containers.SparseVectorFilter( + filtered_items=[d] + ) + model_params.reduced_costs_filter = sparse_containers.SparseVectorFilter( + filtered_items=[y, z] + ) + res = solve.solve( + mod, parameters.SolverType.GLOP, model_params=model_params + ) + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertAlmostEqual(2.0, res.termination.objective_bounds.primal_bound) + self.assertGreaterEqual(len(res.solutions), 1) + assert ( + res.solutions[0].primal_solution is not None + and res.solutions[0].dual_solution is not None + ) + self.assertAlmostEqual( + 2.0, res.solutions[0].primal_solution.objective_value + ) + # y is zero and thus filtered. + self._assert_dict_almost_equal( + {x: 1.0, z: 1.0}, res.solutions[0].primal_solution.variable_values + ) + self.assertGreaterEqual(len(res.solutions), 1) + dual = res.solutions[0].dual_solution + self.assertAlmostEqual(2.0, dual.objective_value) + # The dual was filtered by id + self.assertSetEqual({d}, set(dual.dual_values.keys())) + self.assertSetEqual({y, z}, set(dual.reduced_costs.keys())) + + def test_message_callback(self): + opt_model = model.Model() + x = opt_model.add_binary_variable(name="x") + opt_model.objective.is_maximize = True + opt_model.objective.set_linear_coefficient(x, 2.0) + + logs = io.StringIO() + res = solve.solve( + opt_model, + parameters.SolverType.GSCIP, + msg_cb=message_callback.printer_message_callback(file=logs), + ) + + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertIn("problem is solved", logs.getvalue()) + + def test_incremental_solve_init_error(self) -> None: + mod = model.Model(name="test_model") + mod.add_variable(lb=1.0, ub=1.0, name="x1") + mod.add_variable(lb=1.0, ub=1.0, name="x1") + with self.assertRaisesRegex(ValueError, "duplicate name*"): + solve.IncrementalSolver(mod, parameters.SolverType.GLOP) + + def test_incremental_solve_error(self) -> None: + mod = model.Model(name="test_model") + mod.add_variable(lb=1.0, ub=-1.0, name="x1") + solver = solve.IncrementalSolver(mod, parameters.SolverType.GLOP) + with self.assertRaisesRegex( + ValueError, "variables.*lower_bound > upper_bound" + ): + solver.solve() + + def test_incremental_solve_error_on_reject(self) -> None: + opt_model = model.Model() + x = opt_model.add_binary_variable(name="x") + opt_model.objective.set_linear_coefficient(x, 2.0) + opt_model.objective.is_maximize = True + # CP-SAT rejects all model changes and solves from scratch each time. + solver = solve.IncrementalSolver(opt_model, parameters.SolverType.CP_SAT) + + res = solver.solve( + msg_cb=message_callback.printer_message_callback(prefix="[solve 1] ") + ) + + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res.solutions), 1) + assert res.solutions[0].primal_solution is not None + self.assertAlmostEqual( + 2.0, res.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x: 1.0}, res.solutions[0].primal_solution.variable_values + ) + + opt_model.add_binary_variable(name="x") + with self.assertRaisesRegex(ValueError, "duplicate name*"): + solver.solve( + msg_cb=message_callback.printer_message_callback(prefix="[solve 2] ") + ) + + def test_incremental_lp(self) -> None: + opt_model = model.Model() + x = opt_model.add_variable(lb=0, ub=1, name="x") + opt_model.objective.set_linear_coefficient(x, 2.0) + opt_model.objective.is_maximize = True + solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) + params = parameters.SolveParameters() + params.enable_output = True + res = solver.solve(params=params) + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res.solutions), 1) + assert res.solutions[0].primal_solution is not None + self.assertAlmostEqual( + 2.0, res.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x: 1.0}, res.solutions[0].primal_solution.variable_values + ) + + x.upper_bound = 3.0 + res2 = solver.solve(params=params) + self.assertEqual( + res2.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res2.solutions), 1) + self.assertAlmostEqual( + 6.0, res2.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x: 3.0}, res2.solutions[0].primal_solution.variable_values + ) + + def test_incremental_mip(self) -> None: + opt_model = model.Model() + x = opt_model.add_binary_variable(name="x") + y = opt_model.add_binary_variable(name="y") + c = opt_model.add_linear_constraint(ub=1.0, name="c") + c.set_coefficient(x, 1.0) + c.set_coefficient(y, 1.0) + opt_model.objective.set_linear_coefficient(x, 2.0) + opt_model.objective.set_linear_coefficient(y, 3.0) + opt_model.objective.is_maximize = True + solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GSCIP) + params = parameters.SolveParameters() + params.enable_output = True + res = solver.solve(params=params) + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res.solutions), 1) + assert res.solutions[0].primal_solution is not None + self.assertAlmostEqual( + 3.0, res.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x: 0.0, y: 1.0}, res.solutions[0].primal_solution.variable_values + ) + + c.upper_bound = 2.0 + res2 = solver.solve(params=params) + self.assertEqual( + res2.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res2.solutions), 1) + assert res2.solutions[0].primal_solution is not None + self.assertAlmostEqual( + 5.0, res2.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x: 1.0, y: 1.0}, res2.solutions[0].primal_solution.variable_values + ) + + def test_incremental_mip_with_message_cb(self) -> None: + opt_model = model.Model() + x = opt_model.add_binary_variable(name="x") + y = opt_model.add_binary_variable(name="y") + c = opt_model.add_linear_constraint(ub=1.0, name="c") + c.set_coefficient(x, 1.0) + c.set_coefficient(y, 1.0) + opt_model.objective.set_linear_coefficient(x, 2.0) + opt_model.objective.set_linear_coefficient(y, 3.0) + opt_model.objective.is_maximize = True + solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GSCIP) + params = parameters.SolveParameters() + params.enable_output = True + + logs = io.StringIO() + res = solver.solve( + params=params, + msg_cb=message_callback.printer_message_callback(file=logs), + ) + + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res.solutions), 1) + assert res.solutions[0].primal_solution is not None + self.assertAlmostEqual( + 3.0, res.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x: 0.0, y: 1.0}, res.solutions[0].primal_solution.variable_values + ) + self.assertNotIn(_SCIP_LOG_SOLVE_WAS_INCREMENTAL, logs.getvalue()) + + c.upper_bound = 2.0 + + logs = io.StringIO() + res2 = solver.solve( + params=params, + msg_cb=message_callback.printer_message_callback(file=logs), + ) + + self.assertEqual( + res2.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res2.solutions), 1) + assert res2.solutions[0].primal_solution is not None + self.assertAlmostEqual( + 5.0, res2.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x: 1.0, y: 1.0}, res2.solutions[0].primal_solution.variable_values + ) + self.assertIn(_SCIP_LOG_SOLVE_WAS_INCREMENTAL, logs.getvalue()) + + def test_incremental_solve_rejected(self) -> None: + opt_model = model.Model() + x = opt_model.add_binary_variable(name="x") + opt_model.objective.set_linear_coefficient(x, 2.0) + opt_model.objective.is_maximize = True + # CP-SAT rejects all model changes and solves from scratch each time. + solver = solve.IncrementalSolver(opt_model, parameters.SolverType.CP_SAT) + + res = solver.solve( + msg_cb=message_callback.printer_message_callback(prefix="[solve 1] ") + ) + + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res.solutions), 1) + assert res.solutions[0].primal_solution is not None + self.assertAlmostEqual( + 2.0, res.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x: 1.0}, res.solutions[0].primal_solution.variable_values + ) + + x.upper_bound = 3.0 + + res2 = solver.solve( + msg_cb=message_callback.printer_message_callback(prefix="[solve 2] ") + ) + + self.assertEqual( + res2.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res2.solutions), 1) + assert res2.solutions[0].primal_solution is not None + self.assertAlmostEqual( + 6.0, res2.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + { + x: 3.0, + }, + res2.solutions[0].primal_solution.variable_values, + ) + + def test_multiple_incremental_lps(self) -> None: + opt_model = model.Model() + x = opt_model.add_variable(lb=0, ub=1, name="x") + opt_model.objective.set_linear_coefficient(x, 2.0) + opt_model.objective.is_maximize = True + solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) + params = parameters.SolveParameters() + params.presolve = parameters.Emphasis.OFF + params.enable_output = True + for ub in [2.0, 3.0, 4.0, 5.0]: + x.upper_bound = ub + res = solver.solve(params=params) + self.assertEqual( + res.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res.termination, + ) + self.assertGreaterEqual(len(res.solutions), 1) + assert res.solutions[0].primal_solution is not None + self.assertAlmostEqual( + 2.0 * ub, res.solutions[0].primal_solution.objective_value + ) + self._assert_dict_almost_equal( + {x: ub}, res.solutions[0].primal_solution.variable_values + ) + + def test_incremental_solver_delete(self) -> None: + opt_model = model.Model() + start_num_solver = core_solver.debug_num_solver() + solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) + self.assertEqual(core_solver.debug_num_solver(), start_num_solver + 1) + del solver + self.assertEqual(core_solver.debug_num_solver(), start_num_solver) + + def test_incremental_solver_close(self) -> None: + opt_model = model.Model() + start_num_solver = core_solver.debug_num_solver() + solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) + self.assertEqual(core_solver.debug_num_solver(), start_num_solver + 1) + solver.close() + self.assertEqual(core_solver.debug_num_solver(), start_num_solver) + with self.assertRaisesRegex(RuntimeError, "closed"): + solver.solve() + + def test_incremental_solver_close_twice(self) -> None: + opt_model = model.Model() + start_num_solver = core_solver.debug_num_solver() + solver = solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP) + self.assertEqual(core_solver.debug_num_solver(), start_num_solver + 1) + solver.close() + solver.close() + self.assertEqual(core_solver.debug_num_solver(), start_num_solver) + + def test_incremental_solver_context_manager(self) -> None: + opt_model = model.Model() + start_num_solver = core_solver.debug_num_solver() + with solve.IncrementalSolver( + opt_model, parameters.SolverType.GLOP + ) as solver: + self.assertEqual(core_solver.debug_num_solver(), start_num_solver + 1) + res1 = solver.solve() + self.assertEqual( + res1.termination.reason, + result.TerminationReason.OPTIMAL, + msg=res1.termination, + ) + res2 = solver.solve() + self.assertEqual( + res2.termination.reason, result.TerminationReason.OPTIMAL, msg=res2 + ) + + self.assertEqual(core_solver.debug_num_solver(), start_num_solver) + with self.assertRaisesRegex(RuntimeError, "closed"): + solver.solve() + + def test_incremental_solver_context_manager_exception(self) -> None: + """Tests that exceptions raised in the context manager are not lost.""" + opt_model = model.Model() + with self.assertRaisesRegex(NotImplementedError, "some message"): + with solve.IncrementalSolver(opt_model, parameters.SolverType.GLOP): + raise NotImplementedError("some message") if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/solver_resources.py b/ortools/math_opt/python/solver_resources.py index 4ff7926b506..3686b75ff66 100644 --- a/ortools/math_opt/python/solver_resources.py +++ b/ortools/math_opt/python/solver_resources.py @@ -21,50 +21,50 @@ @dataclasses.dataclass class SolverResources: - """The hints on the resources a remote solve is expected to use. + """The hints on the resources a remote solve is expected to use. - These parameters are hints and may be ignored by the remote server (in - particular in case of solve in a local subprocess, for example). + These parameters are hints and may be ignored by the remote server (in + particular in case of solve in a local subprocess, for example). - When using remote_solve() and remote_compute_infeasible_subsystem(), these - hints are mostly optional as some defaults will be computed based on the other - parameters. + When using remote_solve() and remote_compute_infeasible_subsystem(), these + hints are mostly optional as some defaults will be computed based on the other + parameters. - When using remote_streaming_solve() these hints are used to dimension the - resources available during the execution of every action; thus it is - recommended to set them. + When using remote_streaming_solve() these hints are used to dimension the + resources available during the execution of every action; thus it is + recommended to set them. - MOE:begin_intracomment_strip + MOE:begin_intracomment_strip - The go/uoss server will use these parameters to do a bin-packing of all - requests. Parameter cpu is a soft-limit, the solve may still be able to use - more CPUs. The ram parameter is an hard-limit, an out-of-memory error will - occur if the solve attempts to use more memory. + The go/uoss server will use these parameters to do a bin-packing of all + requests. Parameter cpu is a soft-limit, the solve may still be able to use + more CPUs. The ram parameter is an hard-limit, an out-of-memory error will + occur if the solve attempts to use more memory. - MOE:end_intracomment_strip + MOE:end_intracomment_strip - Attributes: - cpu: The number of solver threads that are expected to actually execute in - parallel. Must be finite and >0.0. For example a value of 3.0 means that - if the solver has 5 threads that can execute we expect at least 3 of these - threads to be scheduled in parallel for any given time slice of the - operating system scheduler. A fractional value indicates that we don't - expect the operating system to constantly schedule the solver's work. For - example with 0.5 we would expect the solver's threads to be scheduled half - the time. This parameter is usually used in conjunction with - SolveParameters.threads. For some solvers like Gurobi it makes sense to - use SolverResources.cpu = SolveParameters.threads. For other solvers like - CP-SAT, it may makes sense to use a value lower than the number of threads - as not all threads may be ready to be scheduled at the same time. It is - better to consult each solver documentation to set this parameter. Note - that if the SolveParameters.threads is not set then this parameter should - also be left unset. - ram: The limit of RAM for the solve in bytes. Must be finite and >=1.0 (even - though it should in practice be much larger). - """ + Attributes: + cpu: The number of solver threads that are expected to actually execute in + parallel. Must be finite and >0.0. For example a value of 3.0 means that + if the solver has 5 threads that can execute we expect at least 3 of these + threads to be scheduled in parallel for any given time slice of the + operating system scheduler. A fractional value indicates that we don't + expect the operating system to constantly schedule the solver's work. For + example with 0.5 we would expect the solver's threads to be scheduled half + the time. This parameter is usually used in conjunction with + SolveParameters.threads. For some solvers like Gurobi it makes sense to + use SolverResources.cpu = SolveParameters.threads. For other solvers like + CP-SAT, it may makes sense to use a value lower than the number of threads + as not all threads may be ready to be scheduled at the same time. It is + better to consult each solver documentation to set this parameter. Note + that if the SolveParameters.threads is not set then this parameter should + also be left unset. + ram: The limit of RAM for the solve in bytes. Must be finite and >=1.0 (even + though it should in practice be much larger). + """ - cpu: Optional[float] = None - ram: Optional[float] = None + cpu: Optional[float] = None + ram: Optional[float] = None - def to_proto(self) -> rpc_pb2.SolverResourcesProto: - return rpc_pb2.SolverResourcesProto(cpu=self.cpu, ram=self.ram) + def to_proto(self) -> rpc_pb2.SolverResourcesProto: + return rpc_pb2.SolverResourcesProto(cpu=self.cpu, ram=self.ram) diff --git a/ortools/math_opt/python/solver_resources_test.py b/ortools/math_opt/python/solver_resources_test.py index 631ce538c1d..322dd5a4f77 100644 --- a/ortools/math_opt/python/solver_resources_test.py +++ b/ortools/math_opt/python/solver_resources_test.py @@ -20,26 +20,28 @@ from ortools.math_opt.python.testing import compare_proto -class SolverResourcesTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): +class SolverResourcesTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): - def test_to_proto_empty(self): - self.assert_protos_equiv( - solver_resources.SolverResources().to_proto(), - rpc_pb2.SolverResourcesProto(), - ) + def test_to_proto_empty(self): + self.assert_protos_equiv( + solver_resources.SolverResources().to_proto(), + rpc_pb2.SolverResourcesProto(), + ) - def test_to_proto_with_cpu(self): - self.assert_protos_equiv( - solver_resources.SolverResources(cpu=3.5).to_proto(), - rpc_pb2.SolverResourcesProto(cpu=3.5), - ) + def test_to_proto_with_cpu(self): + self.assert_protos_equiv( + solver_resources.SolverResources(cpu=3.5).to_proto(), + rpc_pb2.SolverResourcesProto(cpu=3.5), + ) - def test_to_proto_with_ram(self): - self.assert_protos_equiv( - solver_resources.SolverResources(ram=50 * 1024 * 1024).to_proto(), - rpc_pb2.SolverResourcesProto(ram=50 * 1024 * 1024), - ) + def test_to_proto_with_ram(self): + self.assert_protos_equiv( + solver_resources.SolverResources(ram=50 * 1024 * 1024).to_proto(), + rpc_pb2.SolverResourcesProto(ram=50 * 1024 * 1024), + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/sparse_containers.py b/ortools/math_opt/python/sparse_containers.py index 5dd10b2218d..5c71139c1e3 100644 --- a/ortools/math_opt/python/sparse_containers.py +++ b/ortools/math_opt/python/sparse_containers.py @@ -35,29 +35,29 @@ def to_sparse_double_vector_proto( terms: Mapping[VarOrConstraintType, float], ) -> sparse_containers_pb2.SparseDoubleVectorProto: - """Converts a sparse vector from proto to dict representation.""" - result = sparse_containers_pb2.SparseDoubleVectorProto() - if terms: - id_and_values = [(key.id, value) for (key, value) in terms.items()] - id_and_values.sort() - ids, values = zip(*id_and_values) - result.ids[:] = ids - result.values[:] = values - return result + """Converts a sparse vector from proto to dict representation.""" + result = sparse_containers_pb2.SparseDoubleVectorProto() + if terms: + id_and_values = [(key.id, value) for (key, value) in terms.items()] + id_and_values.sort() + ids, values = zip(*id_and_values) + result.ids[:] = ids + result.values[:] = values + return result def to_sparse_int32_vector_proto( terms: Mapping[VarOrConstraintType, int], ) -> sparse_containers_pb2.SparseInt32VectorProto: - """Converts a sparse vector from proto to dict representation.""" - result = sparse_containers_pb2.SparseInt32VectorProto() - if terms: - id_and_values = [(key.id, value) for (key, value) in terms.items()] - id_and_values.sort() - ids, values = zip(*id_and_values) - result.ids[:] = ids - result.values[:] = values - return result + """Converts a sparse vector from proto to dict representation.""" + result = sparse_containers_pb2.SparseInt32VectorProto() + if terms: + id_and_values = [(key.id, value) for (key, value) in terms.items()] + id_and_values.sort() + ids, values = zip(*id_and_values) + result.ids[:] = ids + result.values[:] = values + return result def parse_variable_map( @@ -65,11 +65,11 @@ def parse_variable_map( mod: model.Model, validate: bool = True, ) -> Dict[variables.Variable, float]: - """Converts a sparse vector of variables from proto to dict representation.""" - result = {} - for index, var_id in enumerate(proto.ids): - result[mod.get_variable(var_id, validate=validate)] = proto.values[index] - return result + """Converts a sparse vector of variables from proto to dict representation.""" + result = {} + for index, var_id in enumerate(proto.ids): + result[mod.get_variable(var_id, validate=validate)] = proto.values[index] + return result def parse_linear_constraint_map( @@ -77,13 +77,13 @@ def parse_linear_constraint_map( mod: model.Model, validate: bool = True, ) -> Dict[linear_constraints.LinearConstraint, float]: - """Converts a sparse vector of linear constraints from proto to dict representation.""" - result = {} - for index, lin_con_id in enumerate(proto.ids): - result[mod.get_linear_constraint(lin_con_id, validate=validate)] = proto.values[ - index - ] - return result + """Converts a sparse vector of linear constraints from proto to dict representation.""" + result = {} + for index, lin_con_id in enumerate(proto.ids): + result[mod.get_linear_constraint(lin_con_id, validate=validate)] = ( + proto.values[index] + ) + return result def parse_quadratic_constraint_map( @@ -91,59 +91,59 @@ def parse_quadratic_constraint_map( mod: model.Model, validate: bool = True, ) -> Dict[quadratic_constraints.QuadraticConstraint, float]: - """Converts a sparse vector of quadratic constraints from proto to dict representation.""" - result = {} - for index, quad_con_id in enumerate(proto.ids): - result[mod.get_quadratic_constraint(quad_con_id, validate=validate)] = ( - proto.values[index] - ) - return result + """Converts a sparse vector of quadratic constraints from proto to dict representation.""" + result = {} + for index, quad_con_id in enumerate(proto.ids): + result[mod.get_quadratic_constraint(quad_con_id, validate=validate)] = ( + proto.values[index] + ) + return result class SparseVectorFilter(Generic[VarOrConstraintType]): - """Restricts the variables or constraints returned in a sparse vector. - - The default behavior is to return entries for all variables/constraints. - - E.g. when requesting the solution to an optimization problem, use this class - to restrict the variables that values are returned for. - - Attributes: - skip_zero_values: Do not include key value pairs with value zero. - filtered_items: If not None, include only key value pairs these keys. Note - that the empty set is different (don't return any keys) from None (return - all keys). - """ - - def __init__( - self, - *, - skip_zero_values: bool = False, - filtered_items: Optional[Iterable[VarOrConstraintType]] = None, - ): - self._skip_zero_values: bool = skip_zero_values - self._filtered_items: Optional[Set[VarOrConstraintType]] = ( - None if filtered_items is None else frozenset(filtered_items) - ) # pytype: disable=annotation-type-mismatch # attribute-variable-annotations - - @property - def skip_zero_values(self) -> bool: - return self._skip_zero_values - - @property - def filtered_items(self) -> Optional[FrozenSet[VarOrConstraintType]]: - return ( - self._filtered_items - ) # pytype: disable=bad-return-type # attribute-variable-annotations - - def to_proto(self) -> sparse_containers_pb2.SparseVectorFilterProto: - """Returns an equivalent proto representation.""" - result = sparse_containers_pb2.SparseVectorFilterProto() - result.skip_zero_values = self._skip_zero_values - if self._filtered_items is not None: - result.filter_by_ids = True - result.filtered_ids[:] = sorted(t.id for t in self._filtered_items) - return result + """Restricts the variables or constraints returned in a sparse vector. + + The default behavior is to return entries for all variables/constraints. + + E.g. when requesting the solution to an optimization problem, use this class + to restrict the variables that values are returned for. + + Attributes: + skip_zero_values: Do not include key value pairs with value zero. + filtered_items: If not None, include only key value pairs these keys. Note + that the empty set is different (don't return any keys) from None (return + all keys). + """ + + def __init__( + self, + *, + skip_zero_values: bool = False, + filtered_items: Optional[Iterable[VarOrConstraintType]] = None, + ): + self._skip_zero_values: bool = skip_zero_values + self._filtered_items: Optional[Set[VarOrConstraintType]] = ( + None if filtered_items is None else frozenset(filtered_items) + ) # pytype: disable=annotation-type-mismatch # attribute-variable-annotations + + @property + def skip_zero_values(self) -> bool: + return self._skip_zero_values + + @property + def filtered_items(self) -> Optional[FrozenSet[VarOrConstraintType]]: + return ( + self._filtered_items + ) # pytype: disable=bad-return-type # attribute-variable-annotations + + def to_proto(self) -> sparse_containers_pb2.SparseVectorFilterProto: + """Returns an equivalent proto representation.""" + result = sparse_containers_pb2.SparseVectorFilterProto() + result.skip_zero_values = self._skip_zero_values + if self._filtered_items is not None: + result.filter_by_ids = True + result.filtered_ids[:] = sorted(t.id for t in self._filtered_items) + return result VariableFilter = SparseVectorFilter[variables.Variable] diff --git a/ortools/math_opt/python/sparse_containers_test.py b/ortools/math_opt/python/sparse_containers_test.py index 92f62c7fdf9..46b8371fc57 100644 --- a/ortools/math_opt/python/sparse_containers_test.py +++ b/ortools/math_opt/python/sparse_containers_test.py @@ -27,36 +27,38 @@ from ortools.math_opt.python.testing import compare_proto -class SparseDoubleVectorTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - - def test_to_proto_empty(self) -> None: - actual = sparse_containers.to_sparse_double_vector_proto({}) - self.assert_protos_equiv( - actual, sparse_containers_pb2.SparseDoubleVectorProto() - ) - - def test_to_proto_vars(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - self.assert_protos_equiv( - sparse_containers.to_sparse_double_vector_proto({z: 4.0, x: 1.0}), - sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 2], values=[1.0, 4.0] - ), - ) - - def test_to_proto_lin_cons(self) -> None: - mod = model.Model(name="test_model") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - self.assert_protos_equiv( - sparse_containers.to_sparse_double_vector_proto({c: 4.0, d: 1.0}), - sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 1], values=[4.0, 1.0] - ), - ) +class SparseDoubleVectorTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_to_proto_empty(self) -> None: + actual = sparse_containers.to_sparse_double_vector_proto({}) + self.assert_protos_equiv( + actual, sparse_containers_pb2.SparseDoubleVectorProto() + ) + + def test_to_proto_vars(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + self.assert_protos_equiv( + sparse_containers.to_sparse_double_vector_proto({z: 4.0, x: 1.0}), + sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 2], values=[1.0, 4.0] + ), + ) + + def test_to_proto_lin_cons(self) -> None: + mod = model.Model(name="test_model") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + self.assert_protos_equiv( + sparse_containers.to_sparse_double_vector_proto({c: 4.0, d: 1.0}), + sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 1], values=[4.0, 1.0] + ), + ) T = TypeVar("T") @@ -65,20 +67,21 @@ def test_to_proto_lin_cons(self) -> None: # We cannot use Callable here because we need to support a named argument. class ParseMap(Protocol, Generic[T]): - def __call__( - self, - vec: sparse_containers_pb2.SparseDoubleVectorProto, - mod: model.Model, - *, - validate: bool = True, - ) -> Dict[T, float]: ... + def __call__( + self, + vec: sparse_containers_pb2.SparseDoubleVectorProto, + mod: model.Model, + *, + validate: bool = True, + ) -> Dict[T, float]: + ... @dataclasses.dataclass(frozen=True) class ParseMapAdapater(Generic[T]): - add_element: Callable[[model.Model], T] - get_element_no_validate: Callable[[model.Model, int], T] - parse_map: ParseMap[T] + add_element: Callable[[model.Model], T] + get_element_no_validate: Callable[[model.Model, int], T] + parse_map: ParseMap[T] _VAR_ADAPTER = ParseMapAdapater( @@ -113,112 +116,120 @@ class ParseVariableMapTest( compare_proto.MathOptProtoAssertions, parameterized.TestCase ): - def test_parse_map(self, adapter: _ADAPTERS) -> None: - mod = model.Model() - x = adapter.add_element(mod) - adapter.add_element(mod) - z = adapter.add_element(mod) - actual = adapter.parse_map( - sparse_containers_pb2.SparseDoubleVectorProto( - ids=[0, 2], values=[1.0, 4.0] - ), - mod, - ) - self.assertDictEqual(actual, {x: 1.0, z: 4.0}) - - def test_parse_map_empty(self, adapter: _ADAPTERS) -> None: - mod = model.Model() - adapter.add_element(mod) - adapter.add_element(mod) - actual = adapter.parse_map(sparse_containers_pb2.SparseDoubleVectorProto(), mod) - self.assertDictEqual(actual, {}) - - def test_parse_var_map_bad_var(self, adapter: _ADAPTERS) -> None: - mod = model.Model() - bad_proto = sparse_containers_pb2.SparseDoubleVectorProto(ids=[2], values=[4.0]) - actual = adapter.parse_map(bad_proto, mod, validate=False) - bad_elem = adapter.get_element_no_validate(mod, 2) - self.assertDictEqual(actual, {bad_elem: 4.0}) - with self.assertRaises(KeyError): - adapter.parse_map(bad_proto, mod, validate=True) - - -class SparseInt32VectorTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - - def test_to_proto_empty(self) -> None: - self.assert_protos_equiv( - sparse_containers.to_sparse_int32_vector_proto({}), - sparse_containers_pb2.SparseInt32VectorProto(), - ) - - def test_to_proto_vars(self) -> None: - mod = model.Model(name="test_model") - x = mod.add_binary_variable(name="x") - mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - self.assert_protos_equiv( - sparse_containers.to_sparse_int32_vector_proto({z: 4, x: 1}), - sparse_containers_pb2.SparseInt32VectorProto(ids=[0, 2], values=[1, 4]), - ) - - def test_to_proto_lin_cons(self) -> None: - mod = model.Model(name="test_model") - c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - self.assert_protos_equiv( - sparse_containers.to_sparse_int32_vector_proto({c: 4, d: 1}), - sparse_containers_pb2.SparseInt32VectorProto(ids=[0, 1], values=[4, 1]), - ) - - -class SparseVectorFilterTest(compare_proto.MathOptProtoAssertions, absltest.TestCase): - - def test_is_none(self) -> None: - f = sparse_containers.SparseVectorFilter(skip_zero_values=True) - self.assertTrue(f.skip_zero_values) - self.assertIsNone(f.filtered_items) - expected_proto = sparse_containers_pb2.SparseVectorFilterProto( - skip_zero_values=True - ) - self.assert_protos_equiv(f.to_proto(), expected_proto) - - def test_ids_is_empty(self) -> None: - f = sparse_containers.SparseVectorFilter(filtered_items=[]) - self.assertFalse(f.skip_zero_values) - self.assertEmpty(f.filtered_items) - expected_proto = sparse_containers_pb2.SparseVectorFilterProto( - filter_by_ids=True - ) - self.assert_protos_equiv(f.to_proto(), expected_proto) - - def test_ids_are_lin_cons(self) -> None: - mod = model.Model(name="test_model") - mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") - d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") - f = sparse_containers.LinearConstraintFilter( - skip_zero_values=True, filtered_items=[d] - ) - self.assertTrue(f.skip_zero_values) - self.assertSetEqual(f.filtered_items, {d}) - expected_proto = sparse_containers_pb2.SparseVectorFilterProto( - skip_zero_values=True, filter_by_ids=True, filtered_ids=[1] - ) - self.assert_protos_equiv(f.to_proto(), expected_proto) - - def test_ids_are_vars(self) -> None: - mod = model.Model(name="test_model") - w = mod.add_binary_variable(name="w") - x = mod.add_binary_variable(name="x") - mod.add_binary_variable(name="y") - z = mod.add_binary_variable(name="z") - f = sparse_containers.VariableFilter(filtered_items=(z, w, x)) - self.assertFalse(f.skip_zero_values) - self.assertSetEqual(f.filtered_items, {w, x, z}) - expected_proto = sparse_containers_pb2.SparseVectorFilterProto( - filter_by_ids=True, filtered_ids=[0, 1, 3] - ) - self.assert_protos_equiv(f.to_proto(), expected_proto) + def test_parse_map(self, adapter: _ADAPTERS) -> None: + mod = model.Model() + x = adapter.add_element(mod) + adapter.add_element(mod) + z = adapter.add_element(mod) + actual = adapter.parse_map( + sparse_containers_pb2.SparseDoubleVectorProto( + ids=[0, 2], values=[1.0, 4.0] + ), + mod, + ) + self.assertDictEqual(actual, {x: 1.0, z: 4.0}) + + def test_parse_map_empty(self, adapter: _ADAPTERS) -> None: + mod = model.Model() + adapter.add_element(mod) + adapter.add_element(mod) + actual = adapter.parse_map( + sparse_containers_pb2.SparseDoubleVectorProto(), mod + ) + self.assertDictEqual(actual, {}) + + def test_parse_var_map_bad_var(self, adapter: _ADAPTERS) -> None: + mod = model.Model() + bad_proto = sparse_containers_pb2.SparseDoubleVectorProto( + ids=[2], values=[4.0] + ) + actual = adapter.parse_map(bad_proto, mod, validate=False) + bad_elem = adapter.get_element_no_validate(mod, 2) + self.assertDictEqual(actual, {bad_elem: 4.0}) + with self.assertRaises(KeyError): + adapter.parse_map(bad_proto, mod, validate=True) + + +class SparseInt32VectorTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_to_proto_empty(self) -> None: + self.assert_protos_equiv( + sparse_containers.to_sparse_int32_vector_proto({}), + sparse_containers_pb2.SparseInt32VectorProto(), + ) + + def test_to_proto_vars(self) -> None: + mod = model.Model(name="test_model") + x = mod.add_binary_variable(name="x") + mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + self.assert_protos_equiv( + sparse_containers.to_sparse_int32_vector_proto({z: 4, x: 1}), + sparse_containers_pb2.SparseInt32VectorProto(ids=[0, 2], values=[1, 4]), + ) + + def test_to_proto_lin_cons(self) -> None: + mod = model.Model(name="test_model") + c = mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + self.assert_protos_equiv( + sparse_containers.to_sparse_int32_vector_proto({c: 4, d: 1}), + sparse_containers_pb2.SparseInt32VectorProto(ids=[0, 1], values=[4, 1]), + ) + + +class SparseVectorFilterTest( + compare_proto.MathOptProtoAssertions, absltest.TestCase +): + + def test_is_none(self) -> None: + f = sparse_containers.SparseVectorFilter(skip_zero_values=True) + self.assertTrue(f.skip_zero_values) + self.assertIsNone(f.filtered_items) + expected_proto = sparse_containers_pb2.SparseVectorFilterProto( + skip_zero_values=True + ) + self.assert_protos_equiv(f.to_proto(), expected_proto) + + def test_ids_is_empty(self) -> None: + f = sparse_containers.SparseVectorFilter(filtered_items=[]) + self.assertFalse(f.skip_zero_values) + self.assertEmpty(f.filtered_items) + expected_proto = sparse_containers_pb2.SparseVectorFilterProto( + filter_by_ids=True + ) + self.assert_protos_equiv(f.to_proto(), expected_proto) + + def test_ids_are_lin_cons(self) -> None: + mod = model.Model(name="test_model") + mod.add_linear_constraint(lb=0.0, ub=1.0, name="c") + d = mod.add_linear_constraint(lb=0.0, ub=1.0, name="d") + f = sparse_containers.LinearConstraintFilter( + skip_zero_values=True, filtered_items=[d] + ) + self.assertTrue(f.skip_zero_values) + self.assertSetEqual(f.filtered_items, {d}) + expected_proto = sparse_containers_pb2.SparseVectorFilterProto( + skip_zero_values=True, filter_by_ids=True, filtered_ids=[1] + ) + self.assert_protos_equiv(f.to_proto(), expected_proto) + + def test_ids_are_vars(self) -> None: + mod = model.Model(name="test_model") + w = mod.add_binary_variable(name="w") + x = mod.add_binary_variable(name="x") + mod.add_binary_variable(name="y") + z = mod.add_binary_variable(name="z") + f = sparse_containers.VariableFilter(filtered_items=(z, w, x)) + self.assertFalse(f.skip_zero_values) + self.assertSetEqual(f.filtered_items, {w, x, z}) + expected_proto = sparse_containers_pb2.SparseVectorFilterProto( + filter_by_ids=True, filtered_ids=[0, 1, 3] + ) + self.assert_protos_equiv(f.to_proto(), expected_proto) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/statistics.py b/ortools/math_opt/python/statistics.py index a62074a6162..ba5dba8aaf4 100644 --- a/ortools/math_opt/python/statistics.py +++ b/ortools/math_opt/python/statistics.py @@ -23,150 +23,154 @@ @dataclasses.dataclass(frozen=True) class Range: - """A close range of values [min, max]. + """A close range of values [min, max]. - Attributes: - minimum: The minimum value. - maximum: The maximum value. - """ + Attributes: + minimum: The minimum value. + maximum: The maximum value. + """ - minimum: float - maximum: float + minimum: float + maximum: float def merge_optional_ranges( lhs: Optional[Range], rhs: Optional[Range] ) -> Optional[Range]: - """Merges the two optional ranges. + """Merges the two optional ranges. - Args: - lhs: The left hand side range. - rhs: The right hand side range. + Args: + lhs: The left hand side range. + rhs: The right hand side range. - Returns: - A merged range (None if both lhs and rhs are None). - """ - if lhs is None: - return rhs - if rhs is None: - return lhs - return Range( - minimum=min(lhs.minimum, rhs.minimum), - maximum=max(lhs.maximum, rhs.maximum), - ) + Returns: + A merged range (None if both lhs and rhs are None). + """ + if lhs is None: + return rhs + if rhs is None: + return lhs + return Range( + minimum=min(lhs.minimum, rhs.minimum), + maximum=max(lhs.maximum, rhs.maximum), + ) def absolute_finite_non_zeros_range(values: Iterable[float]) -> Optional[Range]: - """Returns the range of the absolute values of the finite non-zeros. - - Args: - values: An iterable object of float values. + """Returns the range of the absolute values of the finite non-zeros. + + Args: + values: An iterable object of float values. + + Returns: + The range of the absolute values of the finite non-zeros, None if no such + value is found. + """ + minimum: Optional[float] = None + maximum: Optional[float] = None + for v in values: + v = abs(v) + if math.isinf(v) or v == 0.0: + continue + if minimum is None: + minimum = v + maximum = v + else: + minimum = min(minimum, v) + maximum = max(maximum, v) - Returns: - The range of the absolute values of the finite non-zeros, None if no such - value is found. - """ - minimum: Optional[float] = None - maximum: Optional[float] = None - for v in values: - v = abs(v) - if math.isinf(v) or v == 0.0: - continue - if minimum is None: - minimum = v - maximum = v - else: - minimum = min(minimum, v) - maximum = max(maximum, v) - - assert (maximum is None) == (minimum is None), (minimum, maximum) + assert (maximum is None) == (minimum is None), (minimum, maximum) - if minimum is None: - return None - return Range(minimum=minimum, maximum=maximum) + if minimum is None: + return None + return Range(minimum=minimum, maximum=maximum) @dataclasses.dataclass(frozen=True) class ModelRanges: - """The ranges of the absolute values of the finite non-zero values in the model. - - Each range is optional since there may be no finite non-zero values - (e.g. empty model, empty objective, all variables unbounded, ...). - - Attributes: - objective_terms: The linear and quadratic objective terms (not including the - offset). - variable_bounds: The variables' lower and upper bounds. - linear_constraint_bounds: The linear constraints' lower and upper bounds. - linear_constraint_coefficients: The coefficients of the variables in linear - constraints. - """ + """The ranges of the absolute values of the finite non-zero values in the model. - objective_terms: Optional[Range] - variable_bounds: Optional[Range] - linear_constraint_bounds: Optional[Range] - linear_constraint_coefficients: Optional[Range] - - def __str__(self) -> str: - """Prints the ranges in scientific format with 2 digits (i.e. - - f'{x:.2e}'). - - It returns a multi-line table list of ranges. The last line does NOT end - with a new line. - - Returns: - The ranges in multiline string. - """ - buf = io.StringIO() - - def print_range(prefix: str, value: Optional[Range]) -> None: - buf.write(prefix) - if value is None: - buf.write("no finite values") - return - # Numbers are printed in scientific notation with a precision of 2. Since - # they are expected to be positive we can ignore the optional leading - # minus sign. We thus expects `d.dde[+-]dd(d)?` (the exponent is at least - # 2 digits but double can require 3 digits, with max +308 and min - # -308). Thus we can use a width of 9 to align the ranges properly. - buf.write(f"[{value.minimum:<9.2e}, {value.maximum:<9.2e}]") - - print_range("Objective terms : ", self.objective_terms) - print_range("\nVariable bounds : ", self.variable_bounds) - print_range("\nLinear constraints bounds : ", self.linear_constraint_bounds) - print_range( - "\nLinear constraints coeffs : ", self.linear_constraint_coefficients - ) - return buf.getvalue() + Each range is optional since there may be no finite non-zero values + (e.g. empty model, empty objective, all variables unbounded, ...). + Attributes: + objective_terms: The linear and quadratic objective terms (not including the + offset). + variable_bounds: The variables' lower and upper bounds. + linear_constraint_bounds: The linear constraints' lower and upper bounds. + linear_constraint_coefficients: The coefficients of the variables in linear + constraints. + """ -def compute_model_ranges(mdl: model.Model) -> ModelRanges: - """Returns the ranges of the finite non-zero values in the given model. + objective_terms: Optional[Range] + variable_bounds: Optional[Range] + linear_constraint_bounds: Optional[Range] + linear_constraint_coefficients: Optional[Range] + + def __str__(self) -> str: + """Prints the ranges in scientific format with 2 digits (i.e. - Args: - mdl: The input model. + f'{x:.2e}'). + + It returns a multi-line table list of ranges. The last line does NOT end + with a new line. Returns: - The ranges of the finite non-zero values in the model. + The ranges in multiline string. """ - return ModelRanges( - objective_terms=absolute_finite_non_zeros_range( - term.coefficient for term in mdl.objective.linear_terms() - ), - variable_bounds=merge_optional_ranges( - absolute_finite_non_zeros_range(v.lower_bound for v in mdl.variables()), - absolute_finite_non_zeros_range(v.upper_bound for v in mdl.variables()), - ), - linear_constraint_bounds=merge_optional_ranges( - absolute_finite_non_zeros_range( - c.lower_bound for c in mdl.linear_constraints() - ), - absolute_finite_non_zeros_range( - c.upper_bound for c in mdl.linear_constraints() - ), - ), - linear_constraint_coefficients=absolute_finite_non_zeros_range( - e.coefficient for e in mdl.linear_constraint_matrix_entries() - ), + buf = io.StringIO() + + def print_range(prefix: str, value: Optional[Range]) -> None: + buf.write(prefix) + if value is None: + buf.write("no finite values") + return + # Numbers are printed in scientific notation with a precision of 2. Since + # they are expected to be positive we can ignore the optional leading + # minus sign. We thus expects `d.dde[+-]dd(d)?` (the exponent is at least + # 2 digits but double can require 3 digits, with max +308 and min + # -308). Thus we can use a width of 9 to align the ranges properly. + buf.write(f"[{value.minimum:<9.2e}, {value.maximum:<9.2e}]") + + print_range("Objective terms : ", self.objective_terms) + print_range("\nVariable bounds : ", self.variable_bounds) + print_range("\nLinear constraints bounds : ", self.linear_constraint_bounds) + print_range( + "\nLinear constraints coeffs : ", self.linear_constraint_coefficients ) + return buf.getvalue() + + +def compute_model_ranges(mdl: model.Model) -> ModelRanges: + """Returns the ranges of the finite non-zero values in the given model. + + Args: + mdl: The input model. + + Returns: + The ranges of the finite non-zero values in the model. + """ + return ModelRanges( + objective_terms=absolute_finite_non_zeros_range( + term.coefficient for term in mdl.objective.linear_terms() + ), + variable_bounds=merge_optional_ranges( + absolute_finite_non_zeros_range( + v.lower_bound for v in mdl.variables() + ), + absolute_finite_non_zeros_range( + v.upper_bound for v in mdl.variables() + ), + ), + linear_constraint_bounds=merge_optional_ranges( + absolute_finite_non_zeros_range( + c.lower_bound for c in mdl.linear_constraints() + ), + absolute_finite_non_zeros_range( + c.upper_bound for c in mdl.linear_constraints() + ), + ), + linear_constraint_coefficients=absolute_finite_non_zeros_range( + e.coefficient for e in mdl.linear_constraint_matrix_entries() + ), + ) diff --git a/ortools/math_opt/python/statistics_test.py b/ortools/math_opt/python/statistics_test.py index 5e3cae51a6a..09b8f715819 100644 --- a/ortools/math_opt/python/statistics_test.py +++ b/ortools/math_opt/python/statistics_test.py @@ -21,178 +21,188 @@ class RangeTest(absltest.TestCase): - def test_merge_optional_ranges(self) -> None: - self.assertIsNone(statistics.merge_optional_ranges(None, None)) - r = statistics.Range(1.0, 3.0) - self.assertEqual(statistics.merge_optional_ranges(r, None), r) - self.assertEqual(statistics.merge_optional_ranges(None, r), r) - # We also test that, since Range is a frozen class, we return the non-None - # input when only one input is not None. - self.assertIs(statistics.merge_optional_ranges(r, None), r) - self.assertIs(statistics.merge_optional_ranges(None, r), r) - self.assertEqual( - statistics.merge_optional_ranges( - statistics.Range(1.0, 3.0), statistics.Range(-2.0, 2.0) - ), - statistics.Range(-2.0, 3.0), - ) - - def test_absolute_finite_non_zeros_range(self) -> None: - self.assertIsNone(statistics.absolute_finite_non_zeros_range(())) - self.assertIsNone( - statistics.absolute_finite_non_zeros_range((math.inf, 0.0, -0.0, -math.inf)) - ) - self.assertEqual( - statistics.absolute_finite_non_zeros_range( - (math.inf, -5.0e2, 0.0, 1.5e-3, -0.0, -math.inf, 1.25e-6, 3.0e2) - ), - statistics.Range(minimum=1.25e-6, maximum=5.0e2), + def test_merge_optional_ranges(self) -> None: + self.assertIsNone(statistics.merge_optional_ranges(None, None)) + r = statistics.Range(1.0, 3.0) + self.assertEqual(statistics.merge_optional_ranges(r, None), r) + self.assertEqual(statistics.merge_optional_ranges(None, r), r) + # We also test that, since Range is a frozen class, we return the non-None + # input when only one input is not None. + self.assertIs(statistics.merge_optional_ranges(r, None), r) + self.assertIs(statistics.merge_optional_ranges(None, r), r) + self.assertEqual( + statistics.merge_optional_ranges( + statistics.Range(1.0, 3.0), statistics.Range(-2.0, 2.0) + ), + statistics.Range(-2.0, 3.0), + ) + + def test_absolute_finite_non_zeros_range(self) -> None: + self.assertIsNone(statistics.absolute_finite_non_zeros_range(())) + self.assertIsNone( + statistics.absolute_finite_non_zeros_range( + (math.inf, 0.0, -0.0, -math.inf) ) + ) + self.assertEqual( + statistics.absolute_finite_non_zeros_range( + (math.inf, -5.0e2, 0.0, 1.5e-3, -0.0, -math.inf, 1.25e-6, 3.0e2) + ), + statistics.Range(minimum=1.25e-6, maximum=5.0e2), + ) class ModelRangesTest(absltest.TestCase): - def test_printing(self) -> None: - self.assertMultiLineEqual( - str( - statistics.ModelRanges( - objective_terms=None, - variable_bounds=None, - linear_constraint_bounds=None, - linear_constraint_coefficients=None, - ) - ), - "Objective terms : no finite values\n" - "Variable bounds : no finite values\n" - "Linear constraints bounds : no finite values\n" - "Linear constraints coeffs : no finite values", - ) - - self.assertMultiLineEqual( - str( - statistics.ModelRanges( - objective_terms=statistics.Range(2.12345e-99, 1.12345e3), - variable_bounds=statistics.Range(9.12345e-2, 1.12345e2), - linear_constraint_bounds=statistics.Range(2.12345e6, 5.12345e99), - linear_constraint_coefficients=statistics.Range(0.0, 0.0), - ) - ), - "Objective terms : [2.12e-99 , 1.12e+03 ]\n" - "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" - "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" - "Linear constraints coeffs : [0.00e+00 , 0.00e+00 ]", - ) - - self.assertMultiLineEqual( - str( - statistics.ModelRanges( - objective_terms=statistics.Range(2.12345e-1, 1.12345e3), - variable_bounds=statistics.Range(9.12345e-2, 1.12345e2), - linear_constraint_bounds=statistics.Range(2.12345e6, 5.12345e99), - linear_constraint_coefficients=statistics.Range(0.0, 1.0e100), - ) - ), - "Objective terms : [2.12e-01 , 1.12e+03 ]\n" - "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" - "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" - "Linear constraints coeffs : [0.00e+00 , 1.00e+100]", - ) - - self.assertMultiLineEqual( - str( - statistics.ModelRanges( - objective_terms=statistics.Range(2.12345e-100, 1.12345e3), - variable_bounds=statistics.Range(9.12345e-2, 1.12345e2), - linear_constraint_bounds=statistics.Range(2.12345e6, 5.12345e99), - linear_constraint_coefficients=statistics.Range(0.0, 0.0), - ) - ), - "Objective terms : [2.12e-100, 1.12e+03 ]\n" - "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" - "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" - "Linear constraints coeffs : [0.00e+00 , 0.00e+00 ]", - ) - - self.assertMultiLineEqual( - str( - statistics.ModelRanges( - objective_terms=statistics.Range(2.12345e-100, 1.12345e3), - variable_bounds=statistics.Range(9.12345e-2, 1.12345e2), - linear_constraint_bounds=statistics.Range(2.12345e6, 5.12345e99), - linear_constraint_coefficients=statistics.Range(0.0, 1.0e100), - ) - ), - "Objective terms : [2.12e-100, 1.12e+03 ]\n" - "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" - "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" - "Linear constraints coeffs : [0.00e+00 , 1.00e+100]", - ) - - -class ComputeModelRangesTest(absltest.TestCase): - - def test_empty(self) -> None: - mdl = model.Model(name="model") - self.assertEqual( - statistics.compute_model_ranges(mdl), + def test_printing(self) -> None: + self.assertMultiLineEqual( + str( statistics.ModelRanges( objective_terms=None, variable_bounds=None, linear_constraint_bounds=None, linear_constraint_coefficients=None, - ), - ) - - def test_only_zero_and_infinite_values(self) -> None: - mdl = model.Model(name="model") - mdl.add_variable(lb=0.0, ub=math.inf) - mdl.add_variable(lb=-math.inf, ub=0.0) - mdl.add_variable(lb=-math.inf, ub=math.inf) - mdl.add_linear_constraint(lb=0.0, ub=math.inf) - mdl.add_linear_constraint(lb=-math.inf, ub=0.0) - mdl.add_linear_constraint(lb=-math.inf, ub=math.inf) - - self.assertEqual( - statistics.compute_model_ranges(mdl), + ) + ), + "Objective terms : no finite values\n" + "Variable bounds : no finite values\n" + "Linear constraints bounds : no finite values\n" + "Linear constraints coeffs : no finite values", + ) + + self.assertMultiLineEqual( + str( statistics.ModelRanges( - objective_terms=None, - variable_bounds=None, - linear_constraint_bounds=None, - linear_constraint_coefficients=None, - ), - ) - - def test_mixed_values(self) -> None: - mdl = model.Model(name="model") - x = mdl.add_variable(lb=0.0, ub=0.0, name="x") - y = mdl.add_variable(lb=-math.inf, ub=1e-3, name="y") - mdl.add_variable(lb=-3e2, ub=math.inf, name="z") - - mdl.objective.is_maximize = False - mdl.objective.set_linear_coefficient(x, -5.0e4) - # TODO(b/225219234): add the quadratic term `1.0e-6 * z * x` when the - # support of quadratic objective is added to the Python API. - mdl.objective.set_linear_coefficient(y, 3.0) - - c = mdl.add_linear_constraint(lb=0.0, name="c") - c.set_coefficient(y, 1.25e-3) - c.set_coefficient(x, -4.5e3) - mdl.add_linear_constraint(lb=-math.inf, ub=3e4) - d = mdl.add_linear_constraint(lb=-1e-5, ub=0.0, name="d") - d.set_coefficient(y, 2.5e-3) - - self.assertEqual( - statistics.compute_model_ranges(mdl), + objective_terms=statistics.Range(2.12345e-99, 1.12345e3), + variable_bounds=statistics.Range(9.12345e-2, 1.12345e2), + linear_constraint_bounds=statistics.Range( + 2.12345e6, 5.12345e99 + ), + linear_constraint_coefficients=statistics.Range(0.0, 0.0), + ) + ), + "Objective terms : [2.12e-99 , 1.12e+03 ]\n" + "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" + "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" + "Linear constraints coeffs : [0.00e+00 , 0.00e+00 ]", + ) + + self.assertMultiLineEqual( + str( statistics.ModelRanges( - # TODO(b/225219234): update this to Range(1.0e-6, 5.0e4) once the - # quadratic term is added. - objective_terms=statistics.Range(3.0, 5.0e4), - variable_bounds=statistics.Range(1e-3, 3e2), - linear_constraint_bounds=statistics.Range(1e-5, 3e4), - linear_constraint_coefficients=statistics.Range(1.25e-3, 4.5e3), - ), - ) + objective_terms=statistics.Range(2.12345e-1, 1.12345e3), + variable_bounds=statistics.Range(9.12345e-2, 1.12345e2), + linear_constraint_bounds=statistics.Range( + 2.12345e6, 5.12345e99 + ), + linear_constraint_coefficients=statistics.Range(0.0, 1.0e100), + ) + ), + "Objective terms : [2.12e-01 , 1.12e+03 ]\n" + "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" + "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" + "Linear constraints coeffs : [0.00e+00 , 1.00e+100]", + ) + + self.assertMultiLineEqual( + str( + statistics.ModelRanges( + objective_terms=statistics.Range(2.12345e-100, 1.12345e3), + variable_bounds=statistics.Range(9.12345e-2, 1.12345e2), + linear_constraint_bounds=statistics.Range( + 2.12345e6, 5.12345e99 + ), + linear_constraint_coefficients=statistics.Range(0.0, 0.0), + ) + ), + "Objective terms : [2.12e-100, 1.12e+03 ]\n" + "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" + "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" + "Linear constraints coeffs : [0.00e+00 , 0.00e+00 ]", + ) + + self.assertMultiLineEqual( + str( + statistics.ModelRanges( + objective_terms=statistics.Range(2.12345e-100, 1.12345e3), + variable_bounds=statistics.Range(9.12345e-2, 1.12345e2), + linear_constraint_bounds=statistics.Range( + 2.12345e6, 5.12345e99 + ), + linear_constraint_coefficients=statistics.Range(0.0, 1.0e100), + ) + ), + "Objective terms : [2.12e-100, 1.12e+03 ]\n" + "Variable bounds : [9.12e-02 , 1.12e+02 ]\n" + "Linear constraints bounds : [2.12e+06 , 5.12e+99 ]\n" + "Linear constraints coeffs : [0.00e+00 , 1.00e+100]", + ) + + +class ComputeModelRangesTest(absltest.TestCase): + + def test_empty(self) -> None: + mdl = model.Model(name="model") + self.assertEqual( + statistics.compute_model_ranges(mdl), + statistics.ModelRanges( + objective_terms=None, + variable_bounds=None, + linear_constraint_bounds=None, + linear_constraint_coefficients=None, + ), + ) + + def test_only_zero_and_infinite_values(self) -> None: + mdl = model.Model(name="model") + mdl.add_variable(lb=0.0, ub=math.inf) + mdl.add_variable(lb=-math.inf, ub=0.0) + mdl.add_variable(lb=-math.inf, ub=math.inf) + mdl.add_linear_constraint(lb=0.0, ub=math.inf) + mdl.add_linear_constraint(lb=-math.inf, ub=0.0) + mdl.add_linear_constraint(lb=-math.inf, ub=math.inf) + + self.assertEqual( + statistics.compute_model_ranges(mdl), + statistics.ModelRanges( + objective_terms=None, + variable_bounds=None, + linear_constraint_bounds=None, + linear_constraint_coefficients=None, + ), + ) + + def test_mixed_values(self) -> None: + mdl = model.Model(name="model") + x = mdl.add_variable(lb=0.0, ub=0.0, name="x") + y = mdl.add_variable(lb=-math.inf, ub=1e-3, name="y") + mdl.add_variable(lb=-3e2, ub=math.inf, name="z") + + mdl.objective.is_maximize = False + mdl.objective.set_linear_coefficient(x, -5.0e4) + # TODO(b/225219234): add the quadratic term `1.0e-6 * z * x` when the + # support of quadratic objective is added to the Python API. + mdl.objective.set_linear_coefficient(y, 3.0) + + c = mdl.add_linear_constraint(lb=0.0, name="c") + c.set_coefficient(y, 1.25e-3) + c.set_coefficient(x, -4.5e3) + mdl.add_linear_constraint(lb=-math.inf, ub=3e4) + d = mdl.add_linear_constraint(lb=-1e-5, ub=0.0, name="d") + d.set_coefficient(y, 2.5e-3) + + self.assertEqual( + statistics.compute_model_ranges(mdl), + statistics.ModelRanges( + # TODO(b/225219234): update this to Range(1.0e-6, 5.0e4) once the + # quadratic term is added. + objective_terms=statistics.Range(3.0, 5.0e4), + variable_bounds=statistics.Range(1e-3, 3e2), + linear_constraint_bounds=statistics.Range(1e-5, 3e4), + linear_constraint_coefficients=statistics.Range(1.25e-3, 4.5e3), + ), + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/testing/compare_proto.py b/ortools/math_opt/python/testing/compare_proto.py index 1a02b4d9fd3..1c0e49851c9 100644 --- a/ortools/math_opt/python/testing/compare_proto.py +++ b/ortools/math_opt/python/testing/compare_proto.py @@ -53,8 +53,8 @@ def assert_protos_equal( actual: message.Message, expected: message.Message, ) -> None: - """Asserts the input protos are equal, see module doc for details.""" - test.assertEqual(str(actual), str(expected)) + """Asserts the input protos are equal, see module doc for details.""" + test.assertEqual(str(actual), str(expected)) def assert_protos_equiv( @@ -62,29 +62,29 @@ def assert_protos_equiv( actual: message.Message, expected: message.Message, ) -> None: - """Asserts the input protos are equivalent, see module doc for details.""" - normalized_actual = copy.deepcopy(actual) - normalize.math_opt_normalize_proto(normalized_actual) - normalized_expected = copy.deepcopy(expected) - normalize.math_opt_normalize_proto(normalized_expected) - test.assertEqual(str(normalized_actual), str(normalized_expected)) + """Asserts the input protos are equivalent, see module doc for details.""" + normalized_actual = copy.deepcopy(actual) + normalize.math_opt_normalize_proto(normalized_actual) + normalized_expected = copy.deepcopy(expected) + normalize.math_opt_normalize_proto(normalized_expected) + test.assertEqual(str(normalized_actual), str(normalized_expected)) class MathOptProtoAssertions(unittest.TestCase): - """Provides a custom MathOpt proto equivalence assertion for tests.""" - - def assert_protos_equal( - self, - actual: message.Message, - expected: message.Message, - ) -> None: - """Asserts the input protos are equal, see module doc for details.""" - return assert_protos_equal(self, actual, expected) - - def assert_protos_equiv( - self, - actual: message.Message, - expected: message.Message, - ) -> None: - """Asserts the input protos are equivalent, see module doc for details.""" - return assert_protos_equiv(self, actual, expected) + """Provides a custom MathOpt proto equivalence assertion for tests.""" + + def assert_protos_equal( + self, + actual: message.Message, + expected: message.Message, + ) -> None: + """Asserts the input protos are equal, see module doc for details.""" + return assert_protos_equal(self, actual, expected) + + def assert_protos_equiv( + self, + actual: message.Message, + expected: message.Message, + ) -> None: + """Asserts the input protos are equivalent, see module doc for details.""" + return assert_protos_equiv(self, actual, expected) diff --git a/ortools/math_opt/python/testing/compare_proto_test.py b/ortools/math_opt/python/testing/compare_proto_test.py index 0943bc002d5..ca109c1917a 100644 --- a/ortools/math_opt/python/testing/compare_proto_test.py +++ b/ortools/math_opt/python/testing/compare_proto_test.py @@ -23,39 +23,39 @@ class MathOptProtoAssertionsTest( absltest.TestCase, compare_proto.MathOptProtoAssertions ): - def test_assertions_match_but_not_equal(self) -> None: - model_with_empty_vars = model_pb2.ModelProto() - model_with_empty_vars.variables.SetInParent() - empty = model_pb2.ModelProto() - with self.assertRaisesRegex(AssertionError, ".*variables.*"): - self.assert_protos_equal(model_with_empty_vars, empty) - with self.assertRaisesRegex(AssertionError, ".*variables.*"): - self.assert_protos_equal(empty, model_with_empty_vars) - - self.assert_protos_equiv(model_with_empty_vars, empty) - self.assert_protos_equiv(empty, model_with_empty_vars) - - normalize.math_opt_normalize_proto(model_with_empty_vars) - self.assert_protos_equal(model_with_empty_vars, empty) - self.assert_protos_equal(empty, model_with_empty_vars) - self.assert_protos_equiv(model_with_empty_vars, empty) - self.assert_protos_equiv(empty, model_with_empty_vars) - - def test_do_not_match(self) -> None: - with_maximize = model_pb2.ModelProto() - with_maximize.objective.maximize = True - empty = model_pb2.ModelProto() - - with self.assertRaisesRegex(AssertionError, ".*maximize.*"): - self.assert_protos_equal(with_maximize, empty) - with self.assertRaisesRegex(AssertionError, ".*maximize.*"): - self.assert_protos_equal(empty, with_maximize) - - with self.assertRaisesRegex(AssertionError, ".*maximize.*"): - self.assert_protos_equiv(with_maximize, empty) - with self.assertRaisesRegex(AssertionError, ".*maximize.*"): - self.assert_protos_equiv(empty, with_maximize) + def test_assertions_match_but_not_equal(self) -> None: + model_with_empty_vars = model_pb2.ModelProto() + model_with_empty_vars.variables.SetInParent() + empty = model_pb2.ModelProto() + with self.assertRaisesRegex(AssertionError, ".*variables.*"): + self.assert_protos_equal(model_with_empty_vars, empty) + with self.assertRaisesRegex(AssertionError, ".*variables.*"): + self.assert_protos_equal(empty, model_with_empty_vars) + + self.assert_protos_equiv(model_with_empty_vars, empty) + self.assert_protos_equiv(empty, model_with_empty_vars) + + normalize.math_opt_normalize_proto(model_with_empty_vars) + self.assert_protos_equal(model_with_empty_vars, empty) + self.assert_protos_equal(empty, model_with_empty_vars) + self.assert_protos_equiv(model_with_empty_vars, empty) + self.assert_protos_equiv(empty, model_with_empty_vars) + + def test_do_not_match(self) -> None: + with_maximize = model_pb2.ModelProto() + with_maximize.objective.maximize = True + empty = model_pb2.ModelProto() + + with self.assertRaisesRegex(AssertionError, ".*maximize.*"): + self.assert_protos_equal(with_maximize, empty) + with self.assertRaisesRegex(AssertionError, ".*maximize.*"): + self.assert_protos_equal(empty, with_maximize) + + with self.assertRaisesRegex(AssertionError, ".*maximize.*"): + self.assert_protos_equiv(with_maximize, empty) + with self.assertRaisesRegex(AssertionError, ".*maximize.*"): + self.assert_protos_equiv(empty, with_maximize) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/testing/proto_matcher.py b/ortools/math_opt/python/testing/proto_matcher.py index d94f21b74b8..3bad2516e0c 100644 --- a/ortools/math_opt/python/testing/proto_matcher.py +++ b/ortools/math_opt/python/testing/proto_matcher.py @@ -30,19 +30,19 @@ class MathOptProtoEquivMatcher: - """Matcher that checks if protos are equivalent in the MathOpt sense. + """Matcher that checks if protos are equivalent in the MathOpt sense. - See normalize.py for technical definitions. - """ + See normalize.py for technical definitions. + """ - def __init__(self, expected: message.Message): - self._expected = copy.deepcopy(expected) - normalize.math_opt_normalize_proto(self._expected) + def __init__(self, expected: message.Message): + self._expected = copy.deepcopy(expected) + normalize.math_opt_normalize_proto(self._expected) - def __eq__(self, actual: message.Message) -> bool: - actual = copy.deepcopy(actual) - normalize.math_opt_normalize_proto(actual) - return str(actual) == str(self._expected) + def __eq__(self, actual: message.Message) -> bool: + actual = copy.deepcopy(actual) + normalize.math_opt_normalize_proto(actual) + return str(actual) == str(self._expected) - def __ne__(self, other: message.Message) -> bool: - return not self == other + def __ne__(self, other: message.Message) -> bool: + return not self == other diff --git a/ortools/math_opt/python/testing/proto_matcher_test.py b/ortools/math_opt/python/testing/proto_matcher_test.py index cdc5c9878e4..d5431a3837f 100644 --- a/ortools/math_opt/python/testing/proto_matcher_test.py +++ b/ortools/math_opt/python/testing/proto_matcher_test.py @@ -21,46 +21,50 @@ class _ConsumesUpdate: - def __init__(self): - pass + def __init__(self): + pass - def on_update(self, update: model_update_pb2.ModelUpdateProto): - pass + def on_update(self, update: model_update_pb2.ModelUpdateProto): + pass class MathOptProtoAssertionsTest(absltest.TestCase): - def test_mock_eq(self): - update1 = model_update_pb2.ObjectiveUpdatesProto( - direction_update=True, offset_update=0.0 - ) - update2 = model_update_pb2.ObjectiveUpdatesProto(direction_update=True) - update3 = model_update_pb2.ObjectiveUpdatesProto( - direction_update=True, - linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto(), - ) - self.assertFalse( - proto_matcher.MathOptProtoEquivMatcher(update1).__eq__(update2) - ) - self.assertTrue(proto_matcher.MathOptProtoEquivMatcher(update1).__ne__(update2)) - self.assertTrue(proto_matcher.MathOptProtoEquivMatcher(update2).__eq__(update3)) - self.assertFalse( - proto_matcher.MathOptProtoEquivMatcher(update2).__ne__(update3) - ) + def test_mock_eq(self): + update1 = model_update_pb2.ObjectiveUpdatesProto( + direction_update=True, offset_update=0.0 + ) + update2 = model_update_pb2.ObjectiveUpdatesProto(direction_update=True) + update3 = model_update_pb2.ObjectiveUpdatesProto( + direction_update=True, + linear_coefficients=sparse_containers_pb2.SparseDoubleVectorProto(), + ) + self.assertFalse( + proto_matcher.MathOptProtoEquivMatcher(update1).__eq__(update2) + ) + self.assertTrue( + proto_matcher.MathOptProtoEquivMatcher(update1).__ne__(update2) + ) + self.assertTrue( + proto_matcher.MathOptProtoEquivMatcher(update2).__eq__(update3) + ) + self.assertFalse( + proto_matcher.MathOptProtoEquivMatcher(update2).__ne__(update3) + ) - def test_mock_function_when_equal(self): - consumer = _ConsumesUpdate() - consumer.on_update = mock.MagicMock() + def test_mock_function_when_equal(self): + consumer = _ConsumesUpdate() + consumer.on_update = mock.MagicMock() - update = model_update_pb2.ModelUpdateProto(deleted_variable_ids=[0, 4, 5]) + update = model_update_pb2.ModelUpdateProto(deleted_variable_ids=[0, 4, 5]) - consumer.on_update(update) + consumer.on_update(update) - expected = model_update_pb2.ModelUpdateProto(deleted_variable_ids=[0, 4, 5]) - consumer.on_update.assert_called_with( - proto_matcher.MathOptProtoEquivMatcher(expected) - ) + expected = model_update_pb2.ModelUpdateProto(deleted_variable_ids=[0, 4, 5]) + consumer.on_update.assert_called_with( + proto_matcher.MathOptProtoEquivMatcher(expected) + ) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/math_opt/python/variables.py b/ortools/math_opt/python/variables.py index 802f0d29fa0..4ed3d0314b1 100644 --- a/ortools/math_opt/python/variables.py +++ b/ortools/math_opt/python/variables.py @@ -63,68 +63,72 @@ def _raise_binary_operator_type_error( rhs: Type[Any], extra_message: Optional[str] = None, ) -> NoReturn: - """Raises TypeError on unsupported operators.""" - message = ( - f"unsupported operand type(s) for {operator}: {lhs.__name__!r} and" - f" {rhs.__name__!r}" - ) - if extra_message is not None: - message += "\n" + extra_message - raise TypeError(message) + """Raises TypeError on unsupported operators.""" + message = ( + f"unsupported operand type(s) for {operator}: {lhs.__name__!r} and" + f" {rhs.__name__!r}" + ) + if extra_message is not None: + message += "\n" + extra_message + raise TypeError(message) def _raise_ne_not_supported() -> NoReturn: - raise TypeError("!= constraints are not supported") + raise TypeError("!= constraints are not supported") -LowerBoundedLinearExpression = bounded_expressions.LowerBoundedExpression["LinearBase"] -UpperBoundedLinearExpression = bounded_expressions.UpperBoundedExpression["LinearBase"] +LowerBoundedLinearExpression = bounded_expressions.LowerBoundedExpression[ + "LinearBase" +] +UpperBoundedLinearExpression = bounded_expressions.UpperBoundedExpression[ + "LinearBase" +] BoundedLinearExpression = bounded_expressions.BoundedExpression["LinearBase"] class VarEqVar: - """The result of the equality comparison between two Variable. + """The result of the equality comparison between two Variable. - We use an object here to delay the evaluation of equality so that we can use - the operator== in two use-cases: + We use an object here to delay the evaluation of equality so that we can use + the operator== in two use-cases: - 1. when the user want to test that two Variable values references the same - variable. This is supported by having this object support implicit - conversion to bool. + 1. when the user want to test that two Variable values references the same + variable. This is supported by having this object support implicit + conversion to bool. - 2. when the user want to use the equality to create a constraint of equality - between two variables. - """ + 2. when the user want to use the equality to create a constraint of equality + between two variables. + """ - __slots__ = "_first_variable", "_second_variable" + __slots__ = "_first_variable", "_second_variable" - def __init__( - self, - first_variable: "Variable", - second_variable: "Variable", - ) -> None: - self._first_variable: "Variable" = first_variable - self._second_variable: "Variable" = second_variable + def __init__( + self, + first_variable: "Variable", + second_variable: "Variable", + ) -> None: + self._first_variable: "Variable" = first_variable + self._second_variable: "Variable" = second_variable - @property - def first_variable(self) -> "Variable": - return self._first_variable + @property + def first_variable(self) -> "Variable": + return self._first_variable - @property - def second_variable(self) -> "Variable": - return self._second_variable + @property + def second_variable(self) -> "Variable": + return self._second_variable - def __bool__(self) -> bool: - return ( - self._first_variable.elemental is self._second_variable.elemental - and self._first_variable.id == self._second_variable.id - ) + def __bool__(self) -> bool: + return ( + self._first_variable.elemental is self._second_variable.elemental + and self._first_variable.id == self._second_variable.id + ) - def __str__(self): - return f"{self.first_variable!s} == {self._second_variable!s}" + def __str__(self): + return f"{self.first_variable!s} == {self._second_variable!s}" - def __repr__(self): - return f"{self.first_variable!r} == {self._second_variable!r}" + def __repr__(self): + return f"{self.first_variable!r} == {self._second_variable!r}" BoundedLinearTypesList = ( @@ -141,7 +145,9 @@ def __repr__(self): UpperBoundedQuadraticExpression = bounded_expressions.UpperBoundedExpression[ "QuadraticBase" ] -BoundedQuadraticExpression = bounded_expressions.BoundedExpression["QuadraticBase"] +BoundedQuadraticExpression = bounded_expressions.BoundedExpression[ + "QuadraticBase" +] BoundedQuadraticTypesList = ( LowerBoundedQuadraticExpression, @@ -153,88 +159,88 @@ def __repr__(self): # TODO(b/231426528): consider using a frozen dataclass. class QuadraticTermKey: - """An id-ordered pair of variables used as a key for quadratic terms.""" + """An id-ordered pair of variables used as a key for quadratic terms.""" - __slots__ = "_first_var", "_second_var" + __slots__ = "_first_var", "_second_var" - def __init__(self, a: "Variable", b: "Variable"): - """Variables a and b will be ordered internally by their ids.""" - self._first_var: "Variable" = a - self._second_var: "Variable" = b - if self._first_var.id > self._second_var.id: - self._first_var, self._second_var = self._second_var, self._first_var + def __init__(self, a: "Variable", b: "Variable"): + """Variables a and b will be ordered internally by their ids.""" + self._first_var: "Variable" = a + self._second_var: "Variable" = b + if self._first_var.id > self._second_var.id: + self._first_var, self._second_var = self._second_var, self._first_var - @property - def first_var(self) -> "Variable": - return self._first_var + @property + def first_var(self) -> "Variable": + return self._first_var - @property - def second_var(self) -> "Variable": - return self._second_var + @property + def second_var(self) -> "Variable": + return self._second_var - def __eq__(self, other: "QuadraticTermKey") -> bool: - return bool( - self._first_var == other._first_var - and self._second_var == other._second_var - ) + def __eq__(self, other: "QuadraticTermKey") -> bool: + return bool( + self._first_var == other._first_var + and self._second_var == other._second_var + ) - def __hash__(self) -> int: - return hash((self._first_var, self._second_var)) + def __hash__(self) -> int: + return hash((self._first_var, self._second_var)) - def __str__(self): - return f"{self._first_var!s} * {self._second_var!s}" + def __str__(self): + return f"{self._first_var!s} * {self._second_var!s}" - def __repr__(self): - return f"QuadraticTermKey({self._first_var!r}, {self._second_var!r})" + def __repr__(self): + return f"QuadraticTermKey({self._first_var!r}, {self._second_var!r})" @dataclasses.dataclass class _ProcessedElements: - """Auxiliary data class for LinearBase._flatten_once_and_add_to().""" + """Auxiliary data class for LinearBase._flatten_once_and_add_to().""" - terms: DefaultDict["Variable", float] = dataclasses.field( - default_factory=lambda: collections.defaultdict(float) - ) - offset: float = 0.0 + terms: DefaultDict["Variable", float] = dataclasses.field( + default_factory=lambda: collections.defaultdict(float) + ) + offset: float = 0.0 @dataclasses.dataclass class _QuadraticProcessedElements(_ProcessedElements): - """Auxiliary data class for QuadraticBase._quadratic_flatten_once_and_add_to().""" + """Auxiliary data class for QuadraticBase._quadratic_flatten_once_and_add_to().""" - quadratic_terms: DefaultDict["QuadraticTermKey", float] = dataclasses.field( - default_factory=lambda: collections.defaultdict(float) - ) + quadratic_terms: DefaultDict["QuadraticTermKey", float] = dataclasses.field( + default_factory=lambda: collections.defaultdict(float) + ) class _ToProcessElements(Protocol): - """Auxiliary to-process stack interface for LinearBase._flatten_once_and_add_to() and QuadraticBase._quadratic_flatten_once_and_add_to().""" + """Auxiliary to-process stack interface for LinearBase._flatten_once_and_add_to() and QuadraticBase._quadratic_flatten_once_and_add_to().""" - __slots__ = () + __slots__ = () - def append(self, term: "LinearBase", scale: float) -> None: - """Add a linear object and scale to the to-process stack.""" + def append(self, term: "LinearBase", scale: float) -> None: + """Add a linear object and scale to the to-process stack.""" _T = TypeVar("_T", "LinearBase", Union["LinearBase", "QuadraticBase"]) class _ToProcessElementsImplementation(Generic[_T]): - """Auxiliary data class for LinearBase._flatten_once_and_add_to().""" + """Auxiliary data class for LinearBase._flatten_once_and_add_to().""" - __slots__ = ("_queue",) + __slots__ = ("_queue",) - def __init__(self, term: _T, scale: float) -> None: - self._queue: Deque[Tuple[_T, float]] = collections.deque([(term, scale)]) + def __init__(self, term: _T, scale: float) -> None: + self._queue: Deque[Tuple[_T, float]] = collections.deque([(term, scale)]) - def append(self, term: _T, scale: float) -> None: - self._queue.append((term, scale)) + def append(self, term: _T, scale: float) -> None: + self._queue.append((term, scale)) - def pop(self) -> Tuple[_T, float]: - return self._queue.popleft() + def pop(self) -> Tuple[_T, float]: + return self._queue.popleft() - def __bool__(self) -> bool: - return bool(self._queue) + def __bool__(self) -> bool: + return bool(self._queue) _LinearToProcessElements = _ToProcessElementsImplementation["LinearBase"] @@ -244,15 +250,211 @@ def __bool__(self) -> bool: class LinearBase(metaclass=abc.ABCMeta): - """Interface for types that can build linear expressions with +, -, * and /. + """Interface for types that can build linear expressions with +, -, * and /. + + Classes derived from LinearBase (plus float and int scalars) are used to + build expression trees describing a linear expression. Operations nodes of the + expression tree include: + + * LinearSum: describes a deferred sum of LinearTypes objects. + * LinearProduct: describes a deferred product of a scalar and a + LinearTypes object. + + Leaf nodes of the expression tree include: + + * float and int scalars. + * Variable: a single variable. + * LinearTerm: the product of a scalar and a Variable object. + * LinearExpression: the sum of a scalar and LinearTerm objects. + + LinearBase objects/expression-trees can be used directly to create + constraints or objective functions. However, to facilitate their inspection, + any LinearTypes object can be flattened to a LinearExpression + through: + + as_flat_linear_expression(value: LinearTypes) -> LinearExpression: + + In addition, all LinearBase objects are immutable. + + Performance notes: + + Using an expression tree representation instead of an eager construction of + LinearExpression objects reduces known inefficiencies associated with the + use of operator overloading to construct linear expressions. In particular, we + expect the runtime of as_flat_linear_expression() to be linear in the size of + the expression tree. Additional performance can gained by using LinearSum(c) + instead of sum(c) for a container c, as the latter creates len(c) LinearSum + objects. + """ + + __slots__ = () + + # TODO(b/216492143): explore requirements for this function so calculation of + # coefficients and offsets follow expected associativity rules (so approximate + # float calculations are as expected). + # TODO(b/216492143): add more details of what subclasses need to do in + # developers guide. + @abc.abstractmethod + def _flatten_once_and_add_to( + self, + scale: float, + processed_elements: _ProcessedElements, + target_stack: _ToProcessElements, + ) -> None: + """Flatten one level of tree if needed and add to targets. + + Classes derived from LinearBase only need to implement this function + to enable transformation to LinearExpression through + as_flat_linear_expression(). + + Args: + scale: multiply elements by this number when processing or adding to + stack. + processed_elements: where to add LinearTerms and scalars that can be + processed immediately. + target_stack: where to add LinearBase elements that are not scalars or + LinearTerms (i.e. elements that need further flattening). + Implementations should append() to this stack to avoid being recursive. + """ + + def __eq__( + self, rhs: LinearTypes + ) -> ( + BoundedLinearExpression + ): # pytype: disable=signature-mismatch # overriding-return-type-checks + # Note: when rhs is a QuadraticBase, this will cause rhs.__eq__(self) to be + # invoked, which is defined. + if isinstance(rhs, QuadraticBase): + return NotImplemented + if isinstance(rhs, (int, float)): + return BoundedLinearExpression(rhs, self, rhs) + if not isinstance(rhs, LinearBase): + _raise_binary_operator_type_error("==", type(self), type(rhs)) + return BoundedLinearExpression(0.0, self - rhs, 0.0) + + def __ne__( + self, rhs: LinearTypes + ) -> ( + NoReturn + ): # pytype: disable=signature-mismatch # overriding-return-type-checks + _raise_ne_not_supported() + + @typing.overload + def __le__(self, rhs: float) -> "UpperBoundedLinearExpression": + ... + + @typing.overload + def __le__(self, rhs: "LinearBase") -> "BoundedLinearExpression": + ... + + @typing.overload + def __le__(self, rhs: "BoundedLinearExpression") -> NoReturn: + ... + + def __le__(self, rhs): + # Note: when rhs is a QuadraticBase, this will cause rhs.__ge__(self) to be + # invoked, which is defined. + if isinstance(rhs, QuadraticBase): + return NotImplemented + if isinstance(rhs, (int, float)): + return UpperBoundedLinearExpression(self, rhs) + if isinstance(rhs, LinearBase): + return BoundedLinearExpression(-math.inf, self - rhs, 0.0) + if isinstance(rhs, bounded_expressions.BoundedExpression): + _raise_binary_operator_type_error( + "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE + ) + _raise_binary_operator_type_error("<=", type(self), type(rhs)) + + @typing.overload + def __ge__(self, lhs: float) -> "LowerBoundedLinearExpression": + ... + + @typing.overload + def __ge__(self, lhs: "LinearBase") -> "BoundedLinearExpression": + ... + + @typing.overload + def __ge__(self, lhs: "BoundedLinearExpression") -> NoReturn: + ... + + def __ge__(self, lhs): + # Note: when lhs is a QuadraticBase, this will cause lhs.__le__(self) to be + # invoked, which is defined. + if isinstance(lhs, QuadraticBase): + return NotImplemented + if isinstance(lhs, (int, float)): + return LowerBoundedLinearExpression(self, lhs) + if isinstance(lhs, LinearBase): + return BoundedLinearExpression(0.0, self - lhs, math.inf) + if isinstance(lhs, bounded_expressions.BoundedExpression): + _raise_binary_operator_type_error( + ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE + ) + _raise_binary_operator_type_error(">=", type(self), type(lhs)) + + def __add__(self, expr: LinearTypes) -> "LinearSum": + if not isinstance(expr, (int, float, LinearBase)): + return NotImplemented + return LinearSum((self, expr)) + + def __radd__(self, expr: LinearTypes) -> "LinearSum": + if not isinstance(expr, (int, float, LinearBase)): + return NotImplemented + return LinearSum((expr, self)) + + def __sub__(self, expr: LinearTypes) -> "LinearSum": + if not isinstance(expr, (int, float, LinearBase)): + return NotImplemented + return LinearSum((self, -expr)) + + def __rsub__(self, expr: LinearTypes) -> "LinearSum": + if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): + return NotImplemented + return LinearSum((expr, -self)) + + @typing.overload + def __mul__(self, other: float) -> "LinearProduct": + ... + + @typing.overload + def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": + ... + + def __mul__(self, other): + if not isinstance(other, (int, float, LinearBase)): + return NotImplemented + if isinstance(other, LinearBase): + return LinearLinearProduct(self, other) + return LinearProduct(other, self) + + def __rmul__(self, constant: float) -> "LinearProduct": + if not isinstance(constant, (int, float)): + return NotImplemented + return LinearProduct(constant, self) + + # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. + def __truediv__(self, constant: float) -> "LinearProduct": + if not isinstance(constant, (int, float)): + return NotImplemented + return LinearProduct(1.0 / constant, self) + + def __neg__(self) -> "LinearProduct": + return LinearProduct(-1.0, self) + - Classes derived from LinearBase (plus float and int scalars) are used to - build expression trees describing a linear expression. Operations nodes of the - expression tree include: +class QuadraticBase(metaclass=abc.ABCMeta): + """Interface for types that can build quadratic expressions with +, -, * and /. + + Classes derived from QuadraticBase and LinearBase (plus float and int scalars) + are used to build expression trees describing a quadratic expression. + Operations nodes of the expression tree include: - * LinearSum: describes a deferred sum of LinearTypes objects. - * LinearProduct: describes a deferred product of a scalar and a - LinearTypes object. + * QuadraticSum: describes a deferred sum of QuadraticTypes objects. + * QuadraticProduct: describes a deferred product of a scalar and a + QuadraticTypes object. + * LinearLinearProduct: describes a deferred product of two LinearTypes + objects. Leaf nodes of the expression tree include: @@ -260,1159 +462,1000 @@ class LinearBase(metaclass=abc.ABCMeta): * Variable: a single variable. * LinearTerm: the product of a scalar and a Variable object. * LinearExpression: the sum of a scalar and LinearTerm objects. - - LinearBase objects/expression-trees can be used directly to create - constraints or objective functions. However, to facilitate their inspection, - any LinearTypes object can be flattened to a LinearExpression - through: - - as_flat_linear_expression(value: LinearTypes) -> LinearExpression: - - In addition, all LinearBase objects are immutable. - - Performance notes: - - Using an expression tree representation instead of an eager construction of - LinearExpression objects reduces known inefficiencies associated with the - use of operator overloading to construct linear expressions. In particular, we - expect the runtime of as_flat_linear_expression() to be linear in the size of - the expression tree. Additional performance can gained by using LinearSum(c) - instead of sum(c) for a container c, as the latter creates len(c) LinearSum - objects. - """ - - __slots__ = () - - # TODO(b/216492143): explore requirements for this function so calculation of - # coefficients and offsets follow expected associativity rules (so approximate - # float calculations are as expected). - # TODO(b/216492143): add more details of what subclasses need to do in - # developers guide. - @abc.abstractmethod - def _flatten_once_and_add_to( - self, - scale: float, - processed_elements: _ProcessedElements, - target_stack: _ToProcessElements, - ) -> None: - """Flatten one level of tree if needed and add to targets. - - Classes derived from LinearBase only need to implement this function - to enable transformation to LinearExpression through - as_flat_linear_expression(). - - Args: - scale: multiply elements by this number when processing or adding to - stack. - processed_elements: where to add LinearTerms and scalars that can be - processed immediately. - target_stack: where to add LinearBase elements that are not scalars or - LinearTerms (i.e. elements that need further flattening). - Implementations should append() to this stack to avoid being recursive. - """ - - def __eq__( - self, rhs: LinearTypes - ) -> ( - BoundedLinearExpression - ): # pytype: disable=signature-mismatch # overriding-return-type-checks - # Note: when rhs is a QuadraticBase, this will cause rhs.__eq__(self) to be - # invoked, which is defined. - if isinstance(rhs, QuadraticBase): - return NotImplemented - if isinstance(rhs, (int, float)): - return BoundedLinearExpression(rhs, self, rhs) - if not isinstance(rhs, LinearBase): - _raise_binary_operator_type_error("==", type(self), type(rhs)) - return BoundedLinearExpression(0.0, self - rhs, 0.0) - - def __ne__( - self, rhs: LinearTypes - ) -> ( - NoReturn - ): # pytype: disable=signature-mismatch # overriding-return-type-checks - _raise_ne_not_supported() - - @typing.overload - def __le__(self, rhs: float) -> "UpperBoundedLinearExpression": ... - - @typing.overload - def __le__(self, rhs: "LinearBase") -> "BoundedLinearExpression": ... - - @typing.overload - def __le__(self, rhs: "BoundedLinearExpression") -> NoReturn: ... - - def __le__(self, rhs): - # Note: when rhs is a QuadraticBase, this will cause rhs.__ge__(self) to be - # invoked, which is defined. - if isinstance(rhs, QuadraticBase): - return NotImplemented - if isinstance(rhs, (int, float)): - return UpperBoundedLinearExpression(self, rhs) - if isinstance(rhs, LinearBase): - return BoundedLinearExpression(-math.inf, self - rhs, 0.0) - if isinstance(rhs, bounded_expressions.BoundedExpression): - _raise_binary_operator_type_error( - "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE - ) - _raise_binary_operator_type_error("<=", type(self), type(rhs)) - - @typing.overload - def __ge__(self, lhs: float) -> "LowerBoundedLinearExpression": ... - - @typing.overload - def __ge__(self, lhs: "LinearBase") -> "BoundedLinearExpression": ... - - @typing.overload - def __ge__(self, lhs: "BoundedLinearExpression") -> NoReturn: ... - - def __ge__(self, lhs): - # Note: when lhs is a QuadraticBase, this will cause lhs.__le__(self) to be - # invoked, which is defined. - if isinstance(lhs, QuadraticBase): - return NotImplemented - if isinstance(lhs, (int, float)): - return LowerBoundedLinearExpression(self, lhs) - if isinstance(lhs, LinearBase): - return BoundedLinearExpression(0.0, self - lhs, math.inf) - if isinstance(lhs, bounded_expressions.BoundedExpression): - _raise_binary_operator_type_error( - ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE - ) - _raise_binary_operator_type_error(">=", type(self), type(lhs)) - - def __add__(self, expr: LinearTypes) -> "LinearSum": - if not isinstance(expr, (int, float, LinearBase)): - return NotImplemented - return LinearSum((self, expr)) - - def __radd__(self, expr: LinearTypes) -> "LinearSum": - if not isinstance(expr, (int, float, LinearBase)): - return NotImplemented - return LinearSum((expr, self)) - - def __sub__(self, expr: LinearTypes) -> "LinearSum": - if not isinstance(expr, (int, float, LinearBase)): - return NotImplemented - return LinearSum((self, -expr)) - - def __rsub__(self, expr: LinearTypes) -> "LinearSum": - if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): - return NotImplemented - return LinearSum((expr, -self)) - - @typing.overload - def __mul__(self, other: float) -> "LinearProduct": ... - - @typing.overload - def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... - - def __mul__(self, other): - if not isinstance(other, (int, float, LinearBase)): - return NotImplemented - if isinstance(other, LinearBase): - return LinearLinearProduct(self, other) - return LinearProduct(other, self) - - def __rmul__(self, constant: float) -> "LinearProduct": - if not isinstance(constant, (int, float)): - return NotImplemented - return LinearProduct(constant, self) - - # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. - def __truediv__(self, constant: float) -> "LinearProduct": - if not isinstance(constant, (int, float)): - return NotImplemented - return LinearProduct(1.0 / constant, self) - - def __neg__(self) -> "LinearProduct": - return LinearProduct(-1.0, self) - - -class QuadraticBase(metaclass=abc.ABCMeta): - """Interface for types that can build quadratic expressions with +, -, * and /. - - Classes derived from QuadraticBase and LinearBase (plus float and int scalars) - are used to build expression trees describing a quadratic expression. - Operations nodes of the expression tree include: - - * QuadraticSum: describes a deferred sum of QuadraticTypes objects. - * QuadraticProduct: describes a deferred product of a scalar and a - QuadraticTypes object. - * LinearLinearProduct: describes a deferred product of two LinearTypes - objects. - - Leaf nodes of the expression tree include: - - * float and int scalars. - * Variable: a single variable. - * LinearTerm: the product of a scalar and a Variable object. - * LinearExpression: the sum of a scalar and LinearTerm objects. - * QuadraticTerm: the product of a scalar and two Variable objects. - * QuadraticExpression: the sum of a scalar, LinearTerm objects and - QuadraticTerm objects. - - QuadraticBase objects/expression-trees can be used directly to create - objective functions. However, to facilitate their inspection, any - QuadraticTypes object can be flattened to a QuadraticExpression - through: - - as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: - - In addition, all QuadraticBase objects are immutable. - - Performance notes: - - Using an expression tree representation instead of an eager construction of - QuadraticExpression objects reduces known inefficiencies associated with the - use of operator overloading to construct quadratic expressions. In particular, - we expect the runtime of as_flat_quadratic_expression() to be linear in the - size of the expression tree. Additional performance can gained by using - QuadraticSum(c) instead of sum(c) for a container c, as the latter creates - len(c) QuadraticSum objects. + * QuadraticTerm: the product of a scalar and two Variable objects. + * QuadraticExpression: the sum of a scalar, LinearTerm objects and + QuadraticTerm objects. + + QuadraticBase objects/expression-trees can be used directly to create + objective functions. However, to facilitate their inspection, any + QuadraticTypes object can be flattened to a QuadraticExpression + through: + + as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: + + In addition, all QuadraticBase objects are immutable. + + Performance notes: + + Using an expression tree representation instead of an eager construction of + QuadraticExpression objects reduces known inefficiencies associated with the + use of operator overloading to construct quadratic expressions. In particular, + we expect the runtime of as_flat_quadratic_expression() to be linear in the + size of the expression tree. Additional performance can gained by using + QuadraticSum(c) instead of sum(c) for a container c, as the latter creates + len(c) QuadraticSum objects. + """ + + __slots__ = () + + # TODO(b/216492143): explore requirements for this function so calculation of + # coefficients and offsets follow expected associativity rules (so approximate + # float calculations are as expected). + # TODO(b/216492143): add more details of what subclasses need to do in + # developers guide. + @abc.abstractmethod + def _quadratic_flatten_once_and_add_to( + self, + scale: float, + processed_elements: _QuadraticProcessedElements, + target_stack: _QuadraticToProcessElements, + ) -> None: + """Flatten one level of tree if needed and add to targets. + + Classes derived from QuadraticBase only need to implement this function + to enable transformation to QuadraticExpression through + as_flat_quadratic_expression(). + + Args: + scale: multiply elements by this number when processing or adding to + stack. + processed_elements: where to add linear terms, quadratic terms and scalars + that can be processed immediately. + target_stack: where to add LinearBase and QuadraticBase elements that are + not scalars or linear terms or quadratic terms (i.e. elements that need + further flattening). Implementations should append() to this stack to + avoid being recursive. """ - __slots__ = () - - # TODO(b/216492143): explore requirements for this function so calculation of - # coefficients and offsets follow expected associativity rules (so approximate - # float calculations are as expected). - # TODO(b/216492143): add more details of what subclasses need to do in - # developers guide. - @abc.abstractmethod - def _quadratic_flatten_once_and_add_to( - self, - scale: float, - processed_elements: _QuadraticProcessedElements, - target_stack: _QuadraticToProcessElements, - ) -> None: - """Flatten one level of tree if needed and add to targets. - - Classes derived from QuadraticBase only need to implement this function - to enable transformation to QuadraticExpression through - as_flat_quadratic_expression(). - - Args: - scale: multiply elements by this number when processing or adding to - stack. - processed_elements: where to add linear terms, quadratic terms and scalars - that can be processed immediately. - target_stack: where to add LinearBase and QuadraticBase elements that are - not scalars or linear terms or quadratic terms (i.e. elements that need - further flattening). Implementations should append() to this stack to - avoid being recursive. - """ - - def __eq__( - self, rhs: QuadraticTypes - ) -> ( - BoundedQuadraticExpression - ): # pytype: disable=signature-mismatch # overriding-return-type-checks - if isinstance(rhs, (int, float)): - return BoundedQuadraticExpression(rhs, self, rhs) - if not isinstance(rhs, (LinearBase, QuadraticBase)): - _raise_binary_operator_type_error("==", type(self), type(rhs)) - return BoundedQuadraticExpression(0.0, self - rhs, 0.0) - - def __ne__( - self, rhs: QuadraticTypes - ) -> ( - NoReturn - ): # pytype: disable=signature-mismatch # overriding-return-type-checks - _raise_ne_not_supported() - - @typing.overload - def __le__(self, rhs: float) -> UpperBoundedQuadraticExpression: ... - - @typing.overload - def __le__( - self, rhs: Union[LinearBase, "QuadraticBase"] - ) -> BoundedQuadraticExpression: ... - - @typing.overload - def __le__(self, rhs: BoundedQuadraticExpression) -> NoReturn: ... - - def __le__(self, rhs): - if isinstance(rhs, (int, float)): - return UpperBoundedQuadraticExpression(self, rhs) - if isinstance(rhs, (LinearBase, QuadraticBase)): - return BoundedQuadraticExpression(-math.inf, self - rhs, 0.0) - if isinstance(rhs, bounded_expressions.BoundedExpression): - _raise_binary_operator_type_error( - "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE - ) - _raise_binary_operator_type_error("<=", type(self), type(rhs)) - - @typing.overload - def __ge__(self, lhs: float) -> LowerBoundedQuadraticExpression: ... - - @typing.overload - def __ge__( - self, lhs: Union[LinearBase, "QuadraticBase"] - ) -> BoundedQuadraticExpression: ... - - @typing.overload - def __ge__(self, lhs: BoundedQuadraticExpression) -> NoReturn: ... - - def __ge__(self, lhs): - if isinstance(lhs, (int, float)): - return LowerBoundedQuadraticExpression(self, lhs) - if isinstance(lhs, (LinearBase, QuadraticBase)): - return BoundedQuadraticExpression(0.0, self - lhs, math.inf) - if isinstance(lhs, bounded_expressions.BoundedExpression): - _raise_binary_operator_type_error( - ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE - ) - _raise_binary_operator_type_error(">=", type(self), type(lhs)) - - def __add__(self, expr: QuadraticTypes) -> "QuadraticSum": - if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): - return NotImplemented - return QuadraticSum((self, expr)) - - def __radd__(self, expr: QuadraticTypes) -> "QuadraticSum": - if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): - return NotImplemented - return QuadraticSum((expr, self)) - - def __sub__(self, expr: QuadraticTypes) -> "QuadraticSum": - if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): - return NotImplemented - return QuadraticSum((self, -expr)) - - def __rsub__(self, expr: QuadraticTypes) -> "QuadraticSum": - if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): - return NotImplemented - return QuadraticSum((expr, -self)) - - def __mul__(self, other: float) -> "QuadraticProduct": - if not isinstance(other, (int, float)): - return NotImplemented - return QuadraticProduct(other, self) - - def __rmul__(self, other: float) -> "QuadraticProduct": - if not isinstance(other, (int, float)): - return NotImplemented - return QuadraticProduct(other, self) - - # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. - def __truediv__(self, constant: float) -> "QuadraticProduct": - if not isinstance(constant, (int, float)): - return NotImplemented - return QuadraticProduct(1.0 / constant, self) - - def __neg__(self) -> "QuadraticProduct": - return QuadraticProduct(-1.0, self) + def __eq__( + self, rhs: QuadraticTypes + ) -> ( + BoundedQuadraticExpression + ): # pytype: disable=signature-mismatch # overriding-return-type-checks + if isinstance(rhs, (int, float)): + return BoundedQuadraticExpression(rhs, self, rhs) + if not isinstance(rhs, (LinearBase, QuadraticBase)): + _raise_binary_operator_type_error("==", type(self), type(rhs)) + return BoundedQuadraticExpression(0.0, self - rhs, 0.0) + + def __ne__( + self, rhs: QuadraticTypes + ) -> ( + NoReturn + ): # pytype: disable=signature-mismatch # overriding-return-type-checks + _raise_ne_not_supported() + + @typing.overload + def __le__(self, rhs: float) -> UpperBoundedQuadraticExpression: + ... + + @typing.overload + def __le__( + self, rhs: Union[LinearBase, "QuadraticBase"] + ) -> BoundedQuadraticExpression: + ... + + @typing.overload + def __le__(self, rhs: BoundedQuadraticExpression) -> NoReturn: + ... + + def __le__(self, rhs): + if isinstance(rhs, (int, float)): + return UpperBoundedQuadraticExpression(self, rhs) + if isinstance(rhs, (LinearBase, QuadraticBase)): + return BoundedQuadraticExpression(-math.inf, self - rhs, 0.0) + if isinstance(rhs, bounded_expressions.BoundedExpression): + _raise_binary_operator_type_error( + "<=", type(self), type(rhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE + ) + _raise_binary_operator_type_error("<=", type(self), type(rhs)) + + @typing.overload + def __ge__(self, lhs: float) -> LowerBoundedQuadraticExpression: + ... + + @typing.overload + def __ge__( + self, lhs: Union[LinearBase, "QuadraticBase"] + ) -> BoundedQuadraticExpression: + ... + + @typing.overload + def __ge__(self, lhs: BoundedQuadraticExpression) -> NoReturn: + ... + + def __ge__(self, lhs): + if isinstance(lhs, (int, float)): + return LowerBoundedQuadraticExpression(self, lhs) + if isinstance(lhs, (LinearBase, QuadraticBase)): + return BoundedQuadraticExpression(0.0, self - lhs, math.inf) + if isinstance(lhs, bounded_expressions.BoundedExpression): + _raise_binary_operator_type_error( + ">=", type(self), type(lhs), _EXPRESSION_COMP_EXPRESSION_MESSAGE + ) + _raise_binary_operator_type_error(">=", type(self), type(lhs)) + + def __add__(self, expr: QuadraticTypes) -> "QuadraticSum": + if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): + return NotImplemented + return QuadraticSum((self, expr)) + + def __radd__(self, expr: QuadraticTypes) -> "QuadraticSum": + if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): + return NotImplemented + return QuadraticSum((expr, self)) + + def __sub__(self, expr: QuadraticTypes) -> "QuadraticSum": + if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): + return NotImplemented + return QuadraticSum((self, -expr)) + + def __rsub__(self, expr: QuadraticTypes) -> "QuadraticSum": + if not isinstance(expr, (int, float, LinearBase, QuadraticBase)): + return NotImplemented + return QuadraticSum((expr, -self)) + + def __mul__(self, other: float) -> "QuadraticProduct": + if not isinstance(other, (int, float)): + return NotImplemented + return QuadraticProduct(other, self) + + def __rmul__(self, other: float) -> "QuadraticProduct": + if not isinstance(other, (int, float)): + return NotImplemented + return QuadraticProduct(other, self) + + # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. + def __truediv__(self, constant: float) -> "QuadraticProduct": + if not isinstance(constant, (int, float)): + return NotImplemented + return QuadraticProduct(1.0 / constant, self) + + def __neg__(self) -> "QuadraticProduct": + return QuadraticProduct(-1.0, self) class Variable(LinearBase, from_model.FromModel): - """A decision variable for an optimization model. - - A decision variable takes a value from a domain, either the real numbers or - the integers, and restricted to be in some interval [lb, ub] (where lb and ub - can be infinite). The case of lb == ub is allowed, this means the variable - must take a single value. The case of lb > ub is also allowed, this implies - that the problem is infeasible. - - A Variable is configured as follows: - * lower_bound: a float property, lb above. Should not be NaN nor +inf. - * upper_bound: a float property, ub above. Should not be NaN nor -inf. - * integer: a bool property, if the domain is integer or continuous. - - The name is optional, read only, and used only for debugging. Non-empty names - should be distinct. - - Every Variable is associated with a Model (defined below). Note that data - describing the variable (e.g. lower_bound) is owned by Model.storage, this - class is simply a reference to that data. Do not create a Variable directly, - use Model.add_variable() instead. - """ - - __slots__ = "_elemental", "_id" - - def __init__(self, elem: elemental.Elemental, vid: int) -> None: - """Internal only, prefer Model functions (add_variable() and get_variable()).""" - if not isinstance(vid, int): - raise TypeError(f"vid type should be int, was:{type(vid)}") - self._elemental: elemental.Elemental = elem - self._id: int = vid - - @property - def lower_bound(self) -> float: - return self._elemental.get_attr( - enums.DoubleAttr1.VARIABLE_LOWER_BOUND, (self._id,) - ) + """A decision variable for an optimization model. + + A decision variable takes a value from a domain, either the real numbers or + the integers, and restricted to be in some interval [lb, ub] (where lb and ub + can be infinite). The case of lb == ub is allowed, this means the variable + must take a single value. The case of lb > ub is also allowed, this implies + that the problem is infeasible. + + A Variable is configured as follows: + * lower_bound: a float property, lb above. Should not be NaN nor +inf. + * upper_bound: a float property, ub above. Should not be NaN nor -inf. + * integer: a bool property, if the domain is integer or continuous. + + The name is optional, read only, and used only for debugging. Non-empty names + should be distinct. + + Every Variable is associated with a Model (defined below). Note that data + describing the variable (e.g. lower_bound) is owned by Model.storage, this + class is simply a reference to that data. Do not create a Variable directly, + use Model.add_variable() instead. + """ + + __slots__ = "_elemental", "_id" + + def __init__(self, elem: elemental.Elemental, vid: int) -> None: + """Internal only, prefer Model functions (add_variable() and get_variable()).""" + if not isinstance(vid, int): + raise TypeError(f"vid type should be int, was:{type(vid)}") + self._elemental: elemental.Elemental = elem + self._id: int = vid + + @property + def lower_bound(self) -> float: + return self._elemental.get_attr( + enums.DoubleAttr1.VARIABLE_LOWER_BOUND, (self._id,) + ) - @lower_bound.setter - def lower_bound(self, value: float) -> None: - self._elemental.set_attr( - enums.DoubleAttr1.VARIABLE_LOWER_BOUND, - (self._id,), - value, - ) + @lower_bound.setter + def lower_bound(self, value: float) -> None: + self._elemental.set_attr( + enums.DoubleAttr1.VARIABLE_LOWER_BOUND, + (self._id,), + value, + ) - @property - def upper_bound(self) -> float: - return self._elemental.get_attr( - enums.DoubleAttr1.VARIABLE_UPPER_BOUND, (self._id,) - ) + @property + def upper_bound(self) -> float: + return self._elemental.get_attr( + enums.DoubleAttr1.VARIABLE_UPPER_BOUND, (self._id,) + ) - @upper_bound.setter - def upper_bound(self, value: float) -> None: - self._elemental.set_attr( - enums.DoubleAttr1.VARIABLE_UPPER_BOUND, - (self._id,), - value, - ) + @upper_bound.setter + def upper_bound(self, value: float) -> None: + self._elemental.set_attr( + enums.DoubleAttr1.VARIABLE_UPPER_BOUND, + (self._id,), + value, + ) - @property - def integer(self) -> bool: - return self._elemental.get_attr(enums.BoolAttr1.VARIABLE_INTEGER, (self._id,)) + @property + def integer(self) -> bool: + return self._elemental.get_attr( + enums.BoolAttr1.VARIABLE_INTEGER, (self._id,) + ) - @integer.setter - def integer(self, value: bool) -> None: - self._elemental.set_attr(enums.BoolAttr1.VARIABLE_INTEGER, (self._id,), value) + @integer.setter + def integer(self, value: bool) -> None: + self._elemental.set_attr( + enums.BoolAttr1.VARIABLE_INTEGER, (self._id,), value + ) - @property - def name(self) -> str: - return self._elemental.get_element_name(enums.ElementType.VARIABLE, self._id) + @property + def name(self) -> str: + return self._elemental.get_element_name( + enums.ElementType.VARIABLE, self._id + ) - @property - def id(self) -> int: - return self._id + @property + def id(self) -> int: + return self._id + + @property + def elemental(self) -> elemental.Elemental: + """Internal use only.""" + return self._elemental + + def __str__(self): + """Returns the name, or a string containing the id if the name is empty.""" + return self.name if self.name else f"variable_{self.id}" + + def __repr__(self): + return f"" + + @typing.overload + def __eq__(self, rhs: "Variable") -> "VarEqVar": + ... + + @typing.overload + def __eq__(self, rhs: LinearTypesExceptVariable) -> "BoundedLinearExpression": + ... + + def __eq__(self, rhs): + if isinstance(rhs, Variable): + return VarEqVar(self, rhs) + return super().__eq__(rhs) + + @typing.overload + def __ne__(self, rhs: "Variable") -> bool: + ... + + @typing.overload + def __ne__(self, rhs: LinearTypesExceptVariable) -> NoReturn: + ... + + def __ne__(self, rhs): + if isinstance(rhs, Variable): + return not self == rhs + _raise_ne_not_supported() + + def __hash__(self) -> int: + return hash(self._id) + + @typing.overload + def __mul__(self, other: float) -> "LinearTerm": + ... + + @typing.overload + def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": + ... + + @typing.overload + def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": + ... + + def __mul__(self, other): + if not isinstance(other, (int, float, LinearBase)): + return NotImplemented + if isinstance(other, Variable): + return QuadraticTerm(QuadraticTermKey(self, other), 1.0) + if isinstance(other, LinearTerm): + return QuadraticTerm( + QuadraticTermKey(self, other.variable), other.coefficient + ) + if isinstance(other, LinearBase): + return LinearLinearProduct(self, other) # pytype: disable=bad-return-type + return LinearTerm(self, other) + + def __rmul__(self, constant: float) -> "LinearTerm": + if not isinstance(constant, (int, float)): + return NotImplemented + return LinearTerm(self, constant) + + # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. + def __truediv__(self, constant: float) -> "LinearTerm": + if not isinstance(constant, (int, float)): + return NotImplemented + return LinearTerm(self, 1.0 / constant) + + def __neg__(self) -> "LinearTerm": + return LinearTerm(self, -1.0) + + def _flatten_once_and_add_to( + self, + scale: float, + processed_elements: _ProcessedElements, + target_stack: _ToProcessElements, + ) -> None: + processed_elements.terms[self] += scale - @property - def elemental(self) -> elemental.Elemental: - """Internal use only.""" - return self._elemental - def __str__(self): - """Returns the name, or a string containing the id if the name is empty.""" - return self.name if self.name else f"variable_{self.id}" +class LinearTerm(LinearBase): + """The product of a scalar and a variable. + + This class is immutable. + """ + + __slots__ = "_variable", "_coefficient" + + def __init__(self, variable: Variable, coefficient: float) -> None: + self._variable: Variable = variable + self._coefficient: float = coefficient + + @property + def variable(self) -> Variable: + return self._variable + + @property + def coefficient(self) -> float: + return self._coefficient + + def _flatten_once_and_add_to( + self, + scale: float, + processed_elements: _ProcessedElements, + target_stack: _ToProcessElements, + ) -> None: + processed_elements.terms[self._variable] += self._coefficient * scale + + @typing.overload + def __mul__(self, other: float) -> "LinearTerm": + ... + + @typing.overload + def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": + ... + + @typing.overload + def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": + ... + + def __mul__(self, other): + if not isinstance(other, (int, float, LinearBase)): + return NotImplemented + if isinstance(other, Variable): + return QuadraticTerm( + QuadraticTermKey(self._variable, other), self._coefficient + ) + if isinstance(other, LinearTerm): + return QuadraticTerm( + QuadraticTermKey(self.variable, other.variable), + self._coefficient * other.coefficient, + ) + if isinstance(other, LinearBase): + return LinearLinearProduct(self, other) # pytype: disable=bad-return-type + return LinearTerm(self._variable, self._coefficient * other) + + def __rmul__(self, constant: float) -> "LinearTerm": + if not isinstance(constant, (int, float)): + return NotImplemented + return LinearTerm(self._variable, self._coefficient * constant) + + def __truediv__(self, constant: float) -> "LinearTerm": + if not isinstance(constant, (int, float)): + return NotImplemented + return LinearTerm(self._variable, self._coefficient / constant) + + def __neg__(self) -> "LinearTerm": + return LinearTerm(self._variable, -self._coefficient) + + def __str__(self): + return f"{self._coefficient} * {self._variable}" + + def __repr__(self): + return f"LinearTerm({self._variable!r}, {self._coefficient!r})" - def __repr__(self): - return f"" - @typing.overload - def __eq__(self, rhs: "Variable") -> "VarEqVar": ... +class QuadraticTerm(QuadraticBase): + """The product of a scalar and two variables. - @typing.overload - def __eq__(self, rhs: LinearTypesExceptVariable) -> "BoundedLinearExpression": ... + This class is immutable. + """ - def __eq__(self, rhs): - if isinstance(rhs, Variable): - return VarEqVar(self, rhs) - return super().__eq__(rhs) + __slots__ = "_key", "_coefficient" - @typing.overload - def __ne__(self, rhs: "Variable") -> bool: ... + def __init__(self, key: QuadraticTermKey, coefficient: float) -> None: + self._key: QuadraticTermKey = key + self._coefficient: float = coefficient - @typing.overload - def __ne__(self, rhs: LinearTypesExceptVariable) -> NoReturn: ... + @property + def key(self) -> QuadraticTermKey: + return self._key - def __ne__(self, rhs): - if isinstance(rhs, Variable): - return not self == rhs - _raise_ne_not_supported() + @property + def coefficient(self) -> float: + return self._coefficient - def __hash__(self) -> int: - return hash(self._id) + def _quadratic_flatten_once_and_add_to( + self, + scale: float, + processed_elements: _QuadraticProcessedElements, + target_stack: _ToProcessElements, + ) -> None: + processed_elements.quadratic_terms[self._key] += self._coefficient * scale - @typing.overload - def __mul__(self, other: float) -> "LinearTerm": ... + def __mul__(self, constant: float) -> "QuadraticTerm": + if not isinstance(constant, (int, float)): + return NotImplemented + return QuadraticTerm(self._key, self._coefficient * constant) - @typing.overload - def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ... + def __rmul__(self, constant: float) -> "QuadraticTerm": + if not isinstance(constant, (int, float)): + return NotImplemented + return QuadraticTerm(self._key, self._coefficient * constant) - @typing.overload - def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... + def __truediv__(self, constant: float) -> "QuadraticTerm": + if not isinstance(constant, (int, float)): + return NotImplemented + return QuadraticTerm(self._key, self._coefficient / constant) - def __mul__(self, other): - if not isinstance(other, (int, float, LinearBase)): - return NotImplemented - if isinstance(other, Variable): - return QuadraticTerm(QuadraticTermKey(self, other), 1.0) - if isinstance(other, LinearTerm): - return QuadraticTerm( - QuadraticTermKey(self, other.variable), other.coefficient - ) - if isinstance(other, LinearBase): - return LinearLinearProduct(self, other) # pytype: disable=bad-return-type - return LinearTerm(self, other) + def __neg__(self) -> "QuadraticTerm": + return QuadraticTerm(self._key, -self._coefficient) - def __rmul__(self, constant: float) -> "LinearTerm": - if not isinstance(constant, (int, float)): - return NotImplemented - return LinearTerm(self, constant) + def __str__(self): + return f"{self._coefficient} * {self._key!s}" - # TODO(b/216492143): explore numerical consequences of 1.0 / constant below. - def __truediv__(self, constant: float) -> "LinearTerm": - if not isinstance(constant, (int, float)): - return NotImplemented - return LinearTerm(self, 1.0 / constant) + def __repr__(self): + return f"QuadraticTerm({self._key!r}, {self._coefficient})" - def __neg__(self) -> "LinearTerm": - return LinearTerm(self, -1.0) - def _flatten_once_and_add_to( - self, - scale: float, - processed_elements: _ProcessedElements, - target_stack: _ToProcessElements, - ) -> None: - processed_elements.terms[self] += scale +class LinearExpression(LinearBase): + """For variables x, an expression: b + sum_{i in I} a_i * x_i. + + This class is immutable. + """ + + __slots__ = "__weakref__", "_terms", "_offset" + + # TODO(b/216492143): consider initializing from a dictionary. + def __init__(self, /, other: LinearTypes = 0) -> None: + self._offset: float = 0.0 + if isinstance(other, (int, float)): + self._offset = float(other) + self._terms: Mapping[Variable, float] = immutabledict.immutabledict() + return + + to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0) + processed_elements = _ProcessedElements() + while to_process: + linear, coef = to_process.pop() + linear._flatten_once_and_add_to(coef, processed_elements, to_process) + # TODO(b/216492143): explore avoiding this copy. + self._terms: Mapping[Variable, float] = immutabledict.immutabledict( + processed_elements.terms + ) + self._offset = processed_elements.offset + @property + def terms(self) -> Mapping[Variable, float]: + return self._terms -class LinearTerm(LinearBase): - """The product of a scalar and a variable. + @property + def offset(self) -> float: + return self._offset - This class is immutable. - """ + def evaluate(self, variable_values: Mapping[Variable, float]) -> float: + """Returns the value of this expression for given variable values. - __slots__ = "_variable", "_coefficient" - - def __init__(self, variable: Variable, coefficient: float) -> None: - self._variable: Variable = variable - self._coefficient: float = coefficient - - @property - def variable(self) -> Variable: - return self._variable - - @property - def coefficient(self) -> float: - return self._coefficient - - def _flatten_once_and_add_to( - self, - scale: float, - processed_elements: _ProcessedElements, - target_stack: _ToProcessElements, - ) -> None: - processed_elements.terms[self._variable] += self._coefficient * scale - - @typing.overload - def __mul__(self, other: float) -> "LinearTerm": ... - - @typing.overload - def __mul__(self, other: Union["Variable", "LinearTerm"]) -> "QuadraticTerm": ... - - @typing.overload - def __mul__(self, other: "LinearBase") -> "LinearLinearProduct": ... - - def __mul__(self, other): - if not isinstance(other, (int, float, LinearBase)): - return NotImplemented - if isinstance(other, Variable): - return QuadraticTerm( - QuadraticTermKey(self._variable, other), self._coefficient - ) - if isinstance(other, LinearTerm): - return QuadraticTerm( - QuadraticTermKey(self.variable, other.variable), - self._coefficient * other.coefficient, - ) - if isinstance(other, LinearBase): - return LinearLinearProduct(self, other) # pytype: disable=bad-return-type - return LinearTerm(self._variable, self._coefficient * other) - - def __rmul__(self, constant: float) -> "LinearTerm": - if not isinstance(constant, (int, float)): - return NotImplemented - return LinearTerm(self._variable, self._coefficient * constant) - - def __truediv__(self, constant: float) -> "LinearTerm": - if not isinstance(constant, (int, float)): - return NotImplemented - return LinearTerm(self._variable, self._coefficient / constant) - - def __neg__(self) -> "LinearTerm": - return LinearTerm(self._variable, -self._coefficient) - - def __str__(self): - return f"{self._coefficient} * {self._variable}" - - def __repr__(self): - return f"LinearTerm({self._variable!r}, {self._coefficient!r})" + E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then + evaluate(variable_values) equals 10.0. + See also mathopt.evaluate_expression(), which works on any type in + QuadraticTypes. -class QuadraticTerm(QuadraticBase): - """The product of a scalar and two variables. + Args: + variable_values: Must contain a value for every variable in expression. - This class is immutable. + Returns: + The value of this expression when replacing variables by their value. """ + result = self._offset + for var, coef in sorted( + self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id + ): + result += coef * variable_values[var] + return result + + def _flatten_once_and_add_to( + self, + scale: float, + processed_elements: _ProcessedElements, + target_stack: _ToProcessElements, + ) -> None: + for var, val in self._terms.items(): + processed_elements.terms[var] += val * scale + processed_elements.offset += scale * self.offset + + # TODO(b/216492143): change __str__ to match C++ implementation in + # cl/421649402. + def __str__(self): + """Returns the name, or a string containing the id if the name is empty.""" + result = str(self.offset) + sorted_keys = sorted(self._terms.keys(), key=str) + for var in sorted_keys: + # TODO(b/216492143): consider how to better deal with `NaN` and try to + # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in + # linear_expression_test.py. + coefficient = self._terms[var] + if coefficient == 0.0: + continue + if coefficient > 0: + result += " + " + else: + result += " - " + result += str(abs(coefficient)) + " * " + str(var) + return result + + def __repr__(self): + result = f"LinearExpression({self.offset}, " + "{" + result += ", ".join( + f"{var!r}: {coefficient}" for var, coefficient in self._terms.items() + ) + result += "})" + return result - __slots__ = "_key", "_coefficient" - - def __init__(self, key: QuadraticTermKey, coefficient: float) -> None: - self._key: QuadraticTermKey = key - self._coefficient: float = coefficient - - @property - def key(self) -> QuadraticTermKey: - return self._key - - @property - def coefficient(self) -> float: - return self._coefficient - - def _quadratic_flatten_once_and_add_to( - self, - scale: float, - processed_elements: _QuadraticProcessedElements, - target_stack: _ToProcessElements, - ) -> None: - processed_elements.quadratic_terms[self._key] += self._coefficient * scale - def __mul__(self, constant: float) -> "QuadraticTerm": - if not isinstance(constant, (int, float)): - return NotImplemented - return QuadraticTerm(self._key, self._coefficient * constant) +class QuadraticExpression(QuadraticBase): + """For variables x, an expression: b + sum_{i in I} a_i * x_i + sum_{i,j in I, i<=j} a_i,j * x_i * x_j. + + This class is immutable. + """ + + __slots__ = "__weakref__", "_linear_terms", "_quadratic_terms", "_offset" + + # TODO(b/216492143): consider initializing from a dictionary. + def __init__(self, other: QuadraticTypes) -> None: + self._offset: float = 0.0 + if isinstance(other, (int, float)): + self._offset = float(other) + self._linear_terms: Mapping[Variable, float] = ( + immutabledict.immutabledict() + ) + self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( + immutabledict.immutabledict() + ) + return + + to_process: _QuadraticToProcessElements = _QuadraticToProcessElements( + other, 1.0 + ) + processed_elements = _QuadraticProcessedElements() + while to_process: + linear_or_quadratic, coef = to_process.pop() + if isinstance(linear_or_quadratic, LinearBase): + linear_or_quadratic._flatten_once_and_add_to( + coef, processed_elements, to_process + ) + else: + linear_or_quadratic._quadratic_flatten_once_and_add_to( + coef, processed_elements, to_process + ) + # TODO(b/216492143): explore avoiding this copy. + self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict( + processed_elements.terms + ) + self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( + immutabledict.immutabledict(processed_elements.quadratic_terms) + ) + self._offset = processed_elements.offset - def __rmul__(self, constant: float) -> "QuadraticTerm": - if not isinstance(constant, (int, float)): - return NotImplemented - return QuadraticTerm(self._key, self._coefficient * constant) + @property + def linear_terms(self) -> Mapping[Variable, float]: + return self._linear_terms - def __truediv__(self, constant: float) -> "QuadraticTerm": - if not isinstance(constant, (int, float)): - return NotImplemented - return QuadraticTerm(self._key, self._coefficient / constant) + @property + def quadratic_terms(self) -> Mapping[QuadraticTermKey, float]: + return self._quadratic_terms - def __neg__(self) -> "QuadraticTerm": - return QuadraticTerm(self._key, -self._coefficient) + @property + def offset(self) -> float: + return self._offset - def __str__(self): - return f"{self._coefficient} * {self._key!s}" + def evaluate(self, variable_values: Mapping[Variable, float]) -> float: + """Returns the value of this expression for given variable values. - def __repr__(self): - return f"QuadraticTerm({self._key!r}, {self._coefficient})" + E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then + evaluate(variable_values) equals 16.0. + See also mathopt.evaluate_expression(), which works on any type in + QuadraticTypes. -class LinearExpression(LinearBase): - """For variables x, an expression: b + sum_{i in I} a_i * x_i. + Args: + variable_values: Must contain a value for every variable in expression. - This class is immutable. + Returns: + The value of this expression when replacing variables by their value. """ + result = self._offset + for var, coef in sorted( + self._linear_terms.items(), + key=lambda var_coef_pair: var_coef_pair[0].id, + ): + result += coef * variable_values[var] + for key, coef in sorted( + self._quadratic_terms.items(), + key=lambda quad_coef_pair: ( + quad_coef_pair[0].first_var.id, + quad_coef_pair[0].second_var.id, + ), + ): + result += ( + coef + * variable_values[key.first_var] + * variable_values[key.second_var] + ) + return result + + def _quadratic_flatten_once_and_add_to( + self, + scale: float, + processed_elements: _QuadraticProcessedElements, + target_stack: _QuadraticToProcessElements, + ) -> None: + for var, val in self._linear_terms.items(): + processed_elements.terms[var] += val * scale + for key, val in self._quadratic_terms.items(): + processed_elements.quadratic_terms[key] += val * scale + processed_elements.offset += scale * self.offset + + # TODO(b/216492143): change __str__ to match C++ implementation in + # cl/421649402. + def __str__(self): + result = str(self.offset) + sorted_linear_keys = sorted(self._linear_terms.keys(), key=str) + for var in sorted_linear_keys: + # TODO(b/216492143): consider how to better deal with `NaN` and try to + # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in + # linear_expression_test.py. + coefficient = self._linear_terms[var] + if coefficient == 0.0: + continue + if coefficient > 0: + result += " + " + else: + result += " - " + result += str(abs(coefficient)) + " * " + str(var) + sorted_quadratic_keys = sorted(self._quadratic_terms.keys(), key=str) + for key in sorted_quadratic_keys: + # TODO(b/216492143): consider how to better deal with `NaN` and try to + # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in + # linear_expression_test.py. + coefficient = self._quadratic_terms[key] + if coefficient == 0.0: + continue + if coefficient > 0: + result += " + " + else: + result += " - " + result += str(abs(coefficient)) + " * " + str(key) + return result + + def __repr__(self): + result = f"QuadraticExpression({self.offset}, " + "{" + result += ", ".join( + f"{var!r}: {coefficient}" + for var, coefficient in self._linear_terms.items() + ) + result += "}, {" + result += ", ".join( + f"{key!r}: {coefficient}" + for key, coefficient in self._quadratic_terms.items() + ) + result += "})" + return result - __slots__ = "__weakref__", "_terms", "_offset" - - # TODO(b/216492143): consider initializing from a dictionary. - def __init__(self, /, other: LinearTypes = 0) -> None: - self._offset: float = 0.0 - if isinstance(other, (int, float)): - self._offset = float(other) - self._terms: Mapping[Variable, float] = immutabledict.immutabledict() - return - - to_process: _LinearToProcessElements = _LinearToProcessElements(other, 1.0) - processed_elements = _ProcessedElements() - while to_process: - linear, coef = to_process.pop() - linear._flatten_once_and_add_to(coef, processed_elements, to_process) - # TODO(b/216492143): explore avoiding this copy. - self._terms: Mapping[Variable, float] = immutabledict.immutabledict( - processed_elements.terms - ) - self._offset = processed_elements.offset - - @property - def terms(self) -> Mapping[Variable, float]: - return self._terms - - @property - def offset(self) -> float: - return self._offset - - def evaluate(self, variable_values: Mapping[Variable, float]) -> float: - """Returns the value of this expression for given variable values. - - E.g. if this is 3 * x + 4 and variable_values = {x: 2.0}, then - evaluate(variable_values) equals 10.0. - - See also mathopt.evaluate_expression(), which works on any type in - QuadraticTypes. - - Args: - variable_values: Must contain a value for every variable in expression. - - Returns: - The value of this expression when replacing variables by their value. - """ - result = self._offset - for var, coef in sorted( - self._terms.items(), key=lambda var_coef_pair: var_coef_pair[0].id - ): - result += coef * variable_values[var] - return result - - def _flatten_once_and_add_to( - self, - scale: float, - processed_elements: _ProcessedElements, - target_stack: _ToProcessElements, - ) -> None: - for var, val in self._terms.items(): - processed_elements.terms[var] += val * scale - processed_elements.offset += scale * self.offset - - # TODO(b/216492143): change __str__ to match C++ implementation in - # cl/421649402. - def __str__(self): - """Returns the name, or a string containing the id if the name is empty.""" - result = str(self.offset) - sorted_keys = sorted(self._terms.keys(), key=str) - for var in sorted_keys: - # TODO(b/216492143): consider how to better deal with `NaN` and try to - # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in - # linear_expression_test.py. - coefficient = self._terms[var] - if coefficient == 0.0: - continue - if coefficient > 0: - result += " + " - else: - result += " - " - result += str(abs(coefficient)) + " * " + str(var) - return result - - def __repr__(self): - result = f"LinearExpression({self.offset}, " + "{" - result += ", ".join( - f"{var!r}: {coefficient}" for var, coefficient in self._terms.items() - ) - result += "})" - return result - - -class QuadraticExpression(QuadraticBase): - """For variables x, an expression: b + sum_{i in I} a_i * x_i + sum_{i,j in I, i<=j} a_i,j * x_i * x_j. - - This class is immutable. - """ - __slots__ = "__weakref__", "_linear_terms", "_quadratic_terms", "_offset" - - # TODO(b/216492143): consider initializing from a dictionary. - def __init__(self, other: QuadraticTypes) -> None: - self._offset: float = 0.0 - if isinstance(other, (int, float)): - self._offset = float(other) - self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict() - self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( - immutabledict.immutabledict() - ) - return - - to_process: _QuadraticToProcessElements = _QuadraticToProcessElements( - other, 1.0 - ) - processed_elements = _QuadraticProcessedElements() - while to_process: - linear_or_quadratic, coef = to_process.pop() - if isinstance(linear_or_quadratic, LinearBase): - linear_or_quadratic._flatten_once_and_add_to( - coef, processed_elements, to_process - ) - else: - linear_or_quadratic._quadratic_flatten_once_and_add_to( - coef, processed_elements, to_process - ) - # TODO(b/216492143): explore avoiding this copy. - self._linear_terms: Mapping[Variable, float] = immutabledict.immutabledict( - processed_elements.terms - ) - self._quadratic_terms: Mapping[QuadraticTermKey, float] = ( - immutabledict.immutabledict(processed_elements.quadratic_terms) - ) - self._offset = processed_elements.offset - - @property - def linear_terms(self) -> Mapping[Variable, float]: - return self._linear_terms - - @property - def quadratic_terms(self) -> Mapping[QuadraticTermKey, float]: - return self._quadratic_terms - - @property - def offset(self) -> float: - return self._offset - - def evaluate(self, variable_values: Mapping[Variable, float]) -> float: - """Returns the value of this expression for given variable values. - - E.g. if this is 3 * x * x + 4 and variable_values = {x: 2.0}, then - evaluate(variable_values) equals 16.0. - - See also mathopt.evaluate_expression(), which works on any type in - QuadraticTypes. - - Args: - variable_values: Must contain a value for every variable in expression. - - Returns: - The value of this expression when replacing variables by their value. - """ - result = self._offset - for var, coef in sorted( - self._linear_terms.items(), - key=lambda var_coef_pair: var_coef_pair[0].id, - ): - result += coef * variable_values[var] - for key, coef in sorted( - self._quadratic_terms.items(), - key=lambda quad_coef_pair: ( - quad_coef_pair[0].first_var.id, - quad_coef_pair[0].second_var.id, - ), - ): - result += ( - coef * variable_values[key.first_var] * variable_values[key.second_var] - ) - return result - - def _quadratic_flatten_once_and_add_to( - self, - scale: float, - processed_elements: _QuadraticProcessedElements, - target_stack: _QuadraticToProcessElements, - ) -> None: - for var, val in self._linear_terms.items(): - processed_elements.terms[var] += val * scale - for key, val in self._quadratic_terms.items(): - processed_elements.quadratic_terms[key] += val * scale - processed_elements.offset += scale * self.offset - - # TODO(b/216492143): change __str__ to match C++ implementation in - # cl/421649402. - def __str__(self): - result = str(self.offset) - sorted_linear_keys = sorted(self._linear_terms.keys(), key=str) - for var in sorted_linear_keys: - # TODO(b/216492143): consider how to better deal with `NaN` and try to - # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in - # linear_expression_test.py. - coefficient = self._linear_terms[var] - if coefficient == 0.0: - continue - if coefficient > 0: - result += " + " - else: - result += " - " - result += str(abs(coefficient)) + " * " + str(var) - sorted_quadratic_keys = sorted(self._quadratic_terms.keys(), key=str) - for key in sorted_quadratic_keys: - # TODO(b/216492143): consider how to better deal with `NaN` and try to - # match C++ implementation in cl/421649402. See TODO for StrAndReprTest in - # linear_expression_test.py. - coefficient = self._quadratic_terms[key] - if coefficient == 0.0: - continue - if coefficient > 0: - result += " + " - else: - result += " - " - result += str(abs(coefficient)) + " * " + str(key) - return result - - def __repr__(self): - result = f"QuadraticExpression({self.offset}, " + "{" - result += ", ".join( - f"{var!r}: {coefficient}" for var, coefficient in self._linear_terms.items() - ) - result += "}, {" - result += ", ".join( - f"{key!r}: {coefficient}" - for key, coefficient in self._quadratic_terms.items() +class LinearSum(LinearBase): + # TODO(b/216492143): consider what details to move elsewhere and/or replace + # by examples, and do complexity analysis. + """A deferred sum of LinearBase objects. + + LinearSum objects are automatically created when two linear objects are added + and, as noted in the documentation for Linear, can reduce the inefficiencies. + In particular, they are created when calling sum(iterable) when iterable is + an Iterable[LinearTypes]. However, using LinearSum(iterable) instead + can result in additional performance improvements: + + * sum(iterable): creates a nested set of LinearSum objects (e.g. + `sum([a, b, c])` is `LinearSum(0, LinearSum(a, LinearSum(b, c)))`). + * LinearSum(iterable): creates a single LinearSum that saves a tuple with + all the LinearTypes objects in iterable (e.g. + `LinearSum([a, b, c])` does not create additional objects). + + This class is immutable. + """ + + __slots__ = "__weakref__", "_elements" + + # Potentially unsafe use of Iterable argument is handled by immediate local + # storage as tuple. + def __init__(self, iterable: Iterable[LinearTypes]) -> None: + """Creates a LinearSum object. A copy of iterable is saved as a tuple.""" + + self._elements = tuple(iterable) + for item in self._elements: + if not isinstance(item, (LinearBase, int, float)): + raise TypeError( + "unsupported type in iterable argument for " + f"LinearSum: {type(item).__name__!r}" ) - result += "})" - return result + @property + def elements(self) -> Tuple[LinearTypes, ...]: + return self._elements -class LinearSum(LinearBase): - # TODO(b/216492143): consider what details to move elsewhere and/or replace - # by examples, and do complexity analysis. - """A deferred sum of LinearBase objects. - - LinearSum objects are automatically created when two linear objects are added - and, as noted in the documentation for Linear, can reduce the inefficiencies. - In particular, they are created when calling sum(iterable) when iterable is - an Iterable[LinearTypes]. However, using LinearSum(iterable) instead - can result in additional performance improvements: - - * sum(iterable): creates a nested set of LinearSum objects (e.g. - `sum([a, b, c])` is `LinearSum(0, LinearSum(a, LinearSum(b, c)))`). - * LinearSum(iterable): creates a single LinearSum that saves a tuple with - all the LinearTypes objects in iterable (e.g. - `LinearSum([a, b, c])` does not create additional objects). - - This class is immutable. - """ + def _flatten_once_and_add_to( + self, + scale: float, + processed_elements: _ProcessedElements, + target_stack: _ToProcessElements, + ) -> None: + for term in self._elements: + if isinstance(term, (int, float)): + processed_elements.offset += scale * float(term) + else: + target_stack.append(term, scale) + + def __str__(self): + return str(as_flat_linear_expression(self)) - __slots__ = "__weakref__", "_elements" - - # Potentially unsafe use of Iterable argument is handled by immediate local - # storage as tuple. - def __init__(self, iterable: Iterable[LinearTypes]) -> None: - """Creates a LinearSum object. A copy of iterable is saved as a tuple.""" - - self._elements = tuple(iterable) - for item in self._elements: - if not isinstance(item, (LinearBase, int, float)): - raise TypeError( - "unsupported type in iterable argument for " - f"LinearSum: {type(item).__name__!r}" - ) - - @property - def elements(self) -> Tuple[LinearTypes, ...]: - return self._elements - - def _flatten_once_and_add_to( - self, - scale: float, - processed_elements: _ProcessedElements, - target_stack: _ToProcessElements, - ) -> None: - for term in self._elements: - if isinstance(term, (int, float)): - processed_elements.offset += scale * float(term) - else: - target_stack.append(term, scale) - - def __str__(self): - return str(as_flat_linear_expression(self)) - - def __repr__(self): - result = "LinearSum((" - result += ", ".join(repr(linear) for linear in self._elements) - result += "))" - return result + def __repr__(self): + result = "LinearSum((" + result += ", ".join(repr(linear) for linear in self._elements) + result += "))" + return result class QuadraticSum(QuadraticBase): - # TODO(b/216492143): consider what details to move elsewhere and/or replace - # by examples, and do complexity analysis. - """A deferred sum of QuadraticTypes objects. + # TODO(b/216492143): consider what details to move elsewhere and/or replace + # by examples, and do complexity analysis. + """A deferred sum of QuadraticTypes objects. + + QuadraticSum objects are automatically created when a quadratic object is + added to quadratic or linear objects and, as has performance optimizations + similar to LinearSum. + + This class is immutable. + """ + + __slots__ = "__weakref__", "_elements" + + # Potentially unsafe use of Iterable argument is handled by immediate local + # storage as tuple. + def __init__(self, iterable: Iterable[QuadraticTypes]) -> None: + """Creates a QuadraticSum object. A copy of iterable is saved as a tuple.""" + + self._elements = tuple(iterable) + for item in self._elements: + if not isinstance(item, (LinearBase, QuadraticBase, int, float)): + raise TypeError( + "unsupported type in iterable argument for " + f"QuadraticSum: {type(item).__name__!r}" + ) - QuadraticSum objects are automatically created when a quadratic object is - added to quadratic or linear objects and, as has performance optimizations - similar to LinearSum. + @property + def elements(self) -> Tuple[QuadraticTypes, ...]: + return self._elements - This class is immutable. - """ + def _quadratic_flatten_once_and_add_to( + self, + scale: float, + processed_elements: _QuadraticProcessedElements, + target_stack: _QuadraticToProcessElements, + ) -> None: + for term in self._elements: + if isinstance(term, (int, float)): + processed_elements.offset += scale * float(term) + else: + target_stack.append(term, scale) - __slots__ = "__weakref__", "_elements" - - # Potentially unsafe use of Iterable argument is handled by immediate local - # storage as tuple. - def __init__(self, iterable: Iterable[QuadraticTypes]) -> None: - """Creates a QuadraticSum object. A copy of iterable is saved as a tuple.""" - - self._elements = tuple(iterable) - for item in self._elements: - if not isinstance(item, (LinearBase, QuadraticBase, int, float)): - raise TypeError( - "unsupported type in iterable argument for " - f"QuadraticSum: {type(item).__name__!r}" - ) - - @property - def elements(self) -> Tuple[QuadraticTypes, ...]: - return self._elements - - def _quadratic_flatten_once_and_add_to( - self, - scale: float, - processed_elements: _QuadraticProcessedElements, - target_stack: _QuadraticToProcessElements, - ) -> None: - for term in self._elements: - if isinstance(term, (int, float)): - processed_elements.offset += scale * float(term) - else: - target_stack.append(term, scale) - - def __str__(self): - return str(as_flat_quadratic_expression(self)) - - def __repr__(self): - result = "QuadraticSum((" - result += ", ".join(repr(element) for element in self._elements) - result += "))" - return result + def __str__(self): + return str(as_flat_quadratic_expression(self)) + def __repr__(self): + result = "QuadraticSum((" + result += ", ".join(repr(element) for element in self._elements) + result += "))" + return result -class LinearProduct(LinearBase): - """A deferred multiplication computation for linear expressions. - This class is immutable. - """ - - __slots__ = "_scalar", "_linear" - - def __init__(self, scalar: float, linear: LinearBase) -> None: - if not isinstance(scalar, (float, int)): - raise TypeError( - "unsupported type for scalar argument in " - f"LinearProduct: {type(scalar).__name__!r}" - ) - if not isinstance(linear, LinearBase): - raise TypeError( - "unsupported type for linear argument in " - f"LinearProduct: {type(linear).__name__!r}" - ) - self._scalar: float = float(scalar) - self._linear: LinearBase = linear - - @property - def scalar(self) -> float: - return self._scalar - - @property - def linear(self) -> LinearBase: - return self._linear - - def _flatten_once_and_add_to( - self, - scale: float, - processed_elements: _ProcessedElements, - target_stack: _ToProcessElements, - ) -> None: - target_stack.append(self._linear, self._scalar * scale) - - def __str__(self): - return str(as_flat_linear_expression(self)) - - def __repr__(self): - result = f"LinearProduct({self._scalar!r}, " - result += f"{self._linear!r})" - return result +class LinearProduct(LinearBase): + """A deferred multiplication computation for linear expressions. + + This class is immutable. + """ + + __slots__ = "_scalar", "_linear" + + def __init__(self, scalar: float, linear: LinearBase) -> None: + if not isinstance(scalar, (float, int)): + raise TypeError( + "unsupported type for scalar argument in " + f"LinearProduct: {type(scalar).__name__!r}" + ) + if not isinstance(linear, LinearBase): + raise TypeError( + "unsupported type for linear argument in " + f"LinearProduct: {type(linear).__name__!r}" + ) + self._scalar: float = float(scalar) + self._linear: LinearBase = linear + + @property + def scalar(self) -> float: + return self._scalar + + @property + def linear(self) -> LinearBase: + return self._linear + + def _flatten_once_and_add_to( + self, + scale: float, + processed_elements: _ProcessedElements, + target_stack: _ToProcessElements, + ) -> None: + target_stack.append(self._linear, self._scalar * scale) + + def __str__(self): + return str(as_flat_linear_expression(self)) + + def __repr__(self): + result = f"LinearProduct({self._scalar!r}, " + result += f"{self._linear!r})" + return result class QuadraticProduct(QuadraticBase): - """A deferred multiplication computation for quadratic expressions. - - This class is immutable. - """ - - __slots__ = "_scalar", "_quadratic" - - def __init__(self, scalar: float, quadratic: QuadraticBase) -> None: - if not isinstance(scalar, (float, int)): - raise TypeError( - "unsupported type for scalar argument in " - f"QuadraticProduct: {type(scalar).__name__!r}" - ) - if not isinstance(quadratic, QuadraticBase): - raise TypeError( - "unsupported type for linear argument in " - f"QuadraticProduct: {type(quadratic).__name__!r}" - ) - self._scalar: float = float(scalar) - self._quadratic: QuadraticBase = quadratic - - @property - def scalar(self) -> float: - return self._scalar - - @property - def quadratic(self) -> QuadraticBase: - return self._quadratic - - def _quadratic_flatten_once_and_add_to( - self, - scale: float, - processed_elements: _QuadraticProcessedElements, - target_stack: _QuadraticToProcessElements, - ) -> None: - target_stack.append(self._quadratic, self._scalar * scale) - - def __str__(self): - return str(as_flat_quadratic_expression(self)) - - def __repr__(self): - return f"QuadraticProduct({self._scalar}, {self._quadratic!r})" + """A deferred multiplication computation for quadratic expressions. + + This class is immutable. + """ + + __slots__ = "_scalar", "_quadratic" + + def __init__(self, scalar: float, quadratic: QuadraticBase) -> None: + if not isinstance(scalar, (float, int)): + raise TypeError( + "unsupported type for scalar argument in " + f"QuadraticProduct: {type(scalar).__name__!r}" + ) + if not isinstance(quadratic, QuadraticBase): + raise TypeError( + "unsupported type for linear argument in " + f"QuadraticProduct: {type(quadratic).__name__!r}" + ) + self._scalar: float = float(scalar) + self._quadratic: QuadraticBase = quadratic + + @property + def scalar(self) -> float: + return self._scalar + + @property + def quadratic(self) -> QuadraticBase: + return self._quadratic + + def _quadratic_flatten_once_and_add_to( + self, + scale: float, + processed_elements: _QuadraticProcessedElements, + target_stack: _QuadraticToProcessElements, + ) -> None: + target_stack.append(self._quadratic, self._scalar * scale) + + def __str__(self): + return str(as_flat_quadratic_expression(self)) + + def __repr__(self): + return f"QuadraticProduct({self._scalar}, {self._quadratic!r})" class LinearLinearProduct(QuadraticBase): - """A deferred multiplication of two linear expressions. - - This class is immutable. - """ - - __slots__ = "_first_linear", "_second_linear" - - def __init__(self, first_linear: LinearBase, second_linear: LinearBase) -> None: - if not isinstance(first_linear, LinearBase): - raise TypeError( - "unsupported type for first_linear argument in " - f"LinearLinearProduct: {type(first_linear).__name__!r}" - ) - if not isinstance(second_linear, LinearBase): - raise TypeError( - "unsupported type for second_linear argument in " - f"LinearLinearProduct: {type(second_linear).__name__!r}" - ) - self._first_linear: LinearBase = first_linear - self._second_linear: LinearBase = second_linear - - @property - def first_linear(self) -> LinearBase: - return self._first_linear - - @property - def second_linear(self) -> LinearBase: - return self._second_linear - - def _quadratic_flatten_once_and_add_to( - self, - scale: float, - processed_elements: _QuadraticProcessedElements, - target_stack: _QuadraticToProcessElements, - ) -> None: - # A recursion is avoided here because as_flat_linear_expression() must never - # call _quadratic_flatten_once_and_add_to(). - first_expression = as_flat_linear_expression(self._first_linear) - second_expression = as_flat_linear_expression(self._second_linear) - processed_elements.offset += ( - first_expression.offset * second_expression.offset * scale - ) - for first_var, first_val in first_expression.terms.items(): - processed_elements.terms[first_var] += ( - second_expression.offset * first_val * scale - ) - for second_var, second_val in second_expression.terms.items(): - processed_elements.terms[second_var] += ( - first_expression.offset * second_val * scale - ) - - for first_var, first_val in first_expression.terms.items(): - for second_var, second_val in second_expression.terms.items(): - processed_elements.quadratic_terms[ - QuadraticTermKey(first_var, second_var) - ] += (first_val * second_val * scale) - - def __str__(self): - return str(as_flat_quadratic_expression(self)) - - def __repr__(self): - result = "LinearLinearProduct(" - result += f"{self._first_linear!r}, " - result += f"{self._second_linear!r})" - return result + """A deferred multiplication of two linear expressions. + + This class is immutable. + """ + + __slots__ = "_first_linear", "_second_linear" + + def __init__( + self, first_linear: LinearBase, second_linear: LinearBase + ) -> None: + if not isinstance(first_linear, LinearBase): + raise TypeError( + "unsupported type for first_linear argument in " + f"LinearLinearProduct: {type(first_linear).__name__!r}" + ) + if not isinstance(second_linear, LinearBase): + raise TypeError( + "unsupported type for second_linear argument in " + f"LinearLinearProduct: {type(second_linear).__name__!r}" + ) + self._first_linear: LinearBase = first_linear + self._second_linear: LinearBase = second_linear + + @property + def first_linear(self) -> LinearBase: + return self._first_linear + + @property + def second_linear(self) -> LinearBase: + return self._second_linear + + def _quadratic_flatten_once_and_add_to( + self, + scale: float, + processed_elements: _QuadraticProcessedElements, + target_stack: _QuadraticToProcessElements, + ) -> None: + # A recursion is avoided here because as_flat_linear_expression() must never + # call _quadratic_flatten_once_and_add_to(). + first_expression = as_flat_linear_expression(self._first_linear) + second_expression = as_flat_linear_expression(self._second_linear) + processed_elements.offset += ( + first_expression.offset * second_expression.offset * scale + ) + for first_var, first_val in first_expression.terms.items(): + processed_elements.terms[first_var] += ( + second_expression.offset * first_val * scale + ) + for second_var, second_val in second_expression.terms.items(): + processed_elements.terms[second_var] += ( + first_expression.offset * second_val * scale + ) + + for first_var, first_val in first_expression.terms.items(): + for second_var, second_val in second_expression.terms.items(): + processed_elements.quadratic_terms[ + QuadraticTermKey(first_var, second_var) + ] += (first_val * second_val * scale) + + def __str__(self): + return str(as_flat_quadratic_expression(self)) + + def __repr__(self): + result = "LinearLinearProduct(" + result += f"{self._first_linear!r}, " + result += f"{self._second_linear!r})" + return result def as_flat_linear_expression(value: LinearTypes) -> LinearExpression: - """Converts floats, ints and Linear objects to a LinearExpression.""" - if isinstance(value, LinearExpression): - return value - return LinearExpression(value) + """Converts floats, ints and Linear objects to a LinearExpression.""" + if isinstance(value, LinearExpression): + return value + return LinearExpression(value) def as_flat_quadratic_expression(value: QuadraticTypes) -> QuadraticExpression: - """Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression.""" - if isinstance(value, QuadraticExpression): - return value - return QuadraticExpression(value) + """Converts floats, ints, LinearBase and QuadraticBase objects to a QuadraticExpression.""" + if isinstance(value, QuadraticExpression): + return value + return QuadraticExpression(value) diff --git a/ortools/math_opt/samples/python/advanced_linear_programming.py b/ortools/math_opt/samples/python/advanced_linear_programming.py index 618a165831f..b7c5647dd36 100644 --- a/ortools/math_opt/samples/python/advanced_linear_programming.py +++ b/ortools/math_opt/samples/python/advanced_linear_programming.py @@ -31,56 +31,62 @@ # x2 in [0, infinity) # def main(argv: Sequence[str]) -> None: - del argv # Unused. - - model = mathopt.Model(name="Advanced linear programming example") - - # Variables - x = [model.add_variable(lb=0.0, name=f"x{j}") for j in range(3)] - - # Constraints - constraints = [ - model.add_linear_constraint(10 * x[0] + 4 * x[1] + 5 * x[2] <= 600, name="c1"), - model.add_linear_constraint(2 * x[0] + 2 * x[1] + 6 * x[2] <= 300, name="c2"), - model.add_linear_constraint(sum(x) <= 100, name="c3"), - ] - - # Objective - model.maximize(10 * x[0] + 6 * x[1] + 4 * x[2]) - - # May raise a RuntimeError on invalid input or internal solver errors. - result = mathopt.solve(model, mathopt.SolverType.GLOP) - - if result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise RuntimeError(f"model failed to solve to optimality: {result.termination}") - - print(f"Problem solved in {result.solve_time()}") - print(f"Objective value: {result.objective_value()}") - variable_values = [result.variable_values()[v] for v in x] - print(f"Variable values: {variable_values}") - - if not result.has_dual_feasible_solution(): - # MathOpt does not require solvers to return a dual solution on optimal, - # but most LP solvers always will, see go/mathopt-solver-contracts for - # details. - raise RuntimeError("no dual solution was returned on optimal") - - dual_values = [result.dual_values()[c] for c in constraints] - print(f"Constraint duals: {dual_values}") - reduced_costs = [result.reduced_costs()[v] for v in x] - print(f"Reduced costs: {reduced_costs}") - - if not result.has_basis(): - # MathOpt does not require solvers to return a basis on optimal, but most - # Simplex LP solvers (like Glop) always will, see - # go/mathopt-solver-contracts for detail - raise RuntimeError("no basis was returned on optimal") - - constraint_status = [result.constraint_status()[c] for c in constraints] - print(f"Constraint basis status: {constraint_status}") - variable_status = [result.variable_status()[v] for v in x] - print(f"Variable basis status: {variable_status}") + del argv # Unused. + + model = mathopt.Model(name="Advanced linear programming example") + + # Variables + x = [model.add_variable(lb=0.0, name=f"x{j}") for j in range(3)] + + # Constraints + constraints = [ + model.add_linear_constraint( + 10 * x[0] + 4 * x[1] + 5 * x[2] <= 600, name="c1" + ), + model.add_linear_constraint( + 2 * x[0] + 2 * x[1] + 6 * x[2] <= 300, name="c2" + ), + model.add_linear_constraint(sum(x) <= 100, name="c3"), + ] + + # Objective + model.maximize(10 * x[0] + 6 * x[1] + 4 * x[2]) + + # May raise a RuntimeError on invalid input or internal solver errors. + result = mathopt.solve(model, mathopt.SolverType.GLOP) + + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise RuntimeError( + f"model failed to solve to optimality: {result.termination}" + ) + + print(f"Problem solved in {result.solve_time()}") + print(f"Objective value: {result.objective_value()}") + variable_values = [result.variable_values()[v] for v in x] + print(f"Variable values: {variable_values}") + + if not result.has_dual_feasible_solution(): + # MathOpt does not require solvers to return a dual solution on optimal, + # but most LP solvers always will, see go/mathopt-solver-contracts for + # details. + raise RuntimeError("no dual solution was returned on optimal") + + dual_values = [result.dual_values()[c] for c in constraints] + print(f"Constraint duals: {dual_values}") + reduced_costs = [result.reduced_costs()[v] for v in x] + print(f"Reduced costs: {reduced_costs}") + + if not result.has_basis(): + # MathOpt does not require solvers to return a basis on optimal, but most + # Simplex LP solvers (like Glop) always will, see + # go/mathopt-solver-contracts for detail + raise RuntimeError("no basis was returned on optimal") + + constraint_status = [result.constraint_status()[c] for c in constraints] + print(f"Constraint basis status: {constraint_status}") + variable_status = [result.variable_status()[v] for v in x] + print(f"Variable basis status: {variable_status}") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/basic_example.py b/ortools/math_opt/samples/python/basic_example.py index 9cb61614131..6e3683f41b5 100644 --- a/ortools/math_opt/samples/python/basic_example.py +++ b/ortools/math_opt/samples/python/basic_example.py @@ -27,31 +27,31 @@ # y in [0.0, 2.5] # def main(argv: Sequence[str]) -> None: - del argv # Unused. + del argv # Unused. - model = mathopt.Model(name="my_model") - x = model.add_binary_variable(name="x") - y = model.add_variable(lb=0.0, ub=2.5, name="y") - # We can directly use linear combinations of variables ... - model.add_linear_constraint(x + y <= 1.5, name="c") - # ... or build them incrementally. - objective_expression = 0 - objective_expression += 2 * x - objective_expression += y - model.maximize(objective_expression) + model = mathopt.Model(name="my_model") + x = model.add_binary_variable(name="x") + y = model.add_variable(lb=0.0, ub=2.5, name="y") + # We can directly use linear combinations of variables ... + model.add_linear_constraint(x + y <= 1.5, name="c") + # ... or build them incrementally. + objective_expression = 0 + objective_expression += 2 * x + objective_expression += y + model.maximize(objective_expression) - # May raise a RuntimeError on invalid input or internal solver errors. - result = mathopt.solve(model, mathopt.SolverType.GSCIP) + # May raise a RuntimeError on invalid input or internal solver errors. + result = mathopt.solve(model, mathopt.SolverType.GSCIP) - if result.termination.reason not in ( - mathopt.TerminationReason.OPTIMAL, - mathopt.TerminationReason.FEASIBLE, - ): - raise RuntimeError(f"model failed to solve: {result.termination}") + if result.termination.reason not in ( + mathopt.TerminationReason.OPTIMAL, + mathopt.TerminationReason.FEASIBLE, + ): + raise RuntimeError(f"model failed to solve: {result.termination}") - print(f"Objective value: {result.objective_value()}") - print(f"Value for variable x: {result.variable_values()[x]}") + print(f"Objective value: {result.objective_value()}") + print(f"Value for variable x: {result.variable_values()[x]}") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/cutting_stock.py b/ortools/math_opt/samples/python/cutting_stock.py index 1015353b04d..61b160d6c32 100644 --- a/ortools/math_opt/samples/python/cutting_stock.py +++ b/ortools/math_opt/samples/python/cutting_stock.py @@ -82,225 +82,229 @@ @dataclasses.dataclass class CuttingStockInstance: - """Data for a cutting stock instance. + """Data for a cutting stock instance. - Attributes: - piece_sizes: The size of each piece with non-zero demand. Must have the same - length as piece_demands, and each size must be in [0, board_length]. - piece_demands: The demand for a given piece. Must have the same length as - piece_sizes. - board_length: The length of each board. - """ + Attributes: + piece_sizes: The size of each piece with non-zero demand. Must have the same + length as piece_demands, and each size must be in [0, board_length]. + piece_demands: The demand for a given piece. Must have the same length as + piece_sizes. + board_length: The length of each board. + """ - piece_sizes: List[int] = dataclasses.field(default_factory=list) - piece_demands: List[int] = dataclasses.field(default_factory=list) - board_length: int = 0 + piece_sizes: List[int] = dataclasses.field(default_factory=list) + piece_demands: List[int] = dataclasses.field(default_factory=list) + board_length: int = 0 @dataclasses.dataclass class Configuration: - """Describes a size-configuration that can be cut out of a board. + """Describes a size-configuration that can be cut out of a board. - Attributes: - pieces: The size of each piece in the configuration. Must have the same - length as piece_demands, and the total sum of pieces (sum of piece sizes - times quantity of pieces) must not exceed the board length of the - associated cutting stock instance. - quantity: The qualtity of pieces of a given size. Must have the same length - as pieces. - """ + Attributes: + pieces: The size of each piece in the configuration. Must have the same + length as piece_demands, and the total sum of pieces (sum of piece sizes + times quantity of pieces) must not exceed the board length of the + associated cutting stock instance. + quantity: The qualtity of pieces of a given size. Must have the same length + as pieces. + """ - pieces: List[int] = dataclasses.field(default_factory=list) - quantity: List[int] = dataclasses.field(default_factory=list) + pieces: List[int] = dataclasses.field(default_factory=list) + quantity: List[int] = dataclasses.field(default_factory=list) @dataclasses.dataclass class CuttingStockSolution: - """Describes a solution to a cutting stock problem. + """Describes a solution to a cutting stock problem. - To be feasible, the demand for each piece type must be met by the produced - configurations + To be feasible, the demand for each piece type must be met by the produced + configurations - Attributes: - configurations: The configurations used by the solution. Must have the same - length as quantity. - quantity: The number of each configuration in the solution. Must have the - same length as configurations. - objective_value: The objective value of the configuration, which is equal to - sum(quantity). - """ + Attributes: + configurations: The configurations used by the solution. Must have the same + length as quantity. + quantity: The number of each configuration in the solution. Must have the + same length as configurations. + objective_value: The objective value of the configuration, which is equal to + sum(quantity). + """ - configurations: List[Configuration] = dataclasses.field(default_factory=list) - quantity: List[int] = dataclasses.field(default_factory=list) - objective_value: int = 0 + configurations: List[Configuration] = dataclasses.field(default_factory=list) + quantity: List[int] = dataclasses.field(default_factory=list) + objective_value: int = 0 def best_configuration( piece_prices: List[float], piece_sizes: List[int], board_size: int ) -> Tuple[Configuration, float]: - """Solves the worker problem. - - Solves the problem on finding the configuration (with its objective value) to - add the to model that will give the greatest improvement in the LP - relaxation. This is equivalent to a knapsack problem. - - Args: - piece_prices: The price for each piece with non-zero demand. Must have the - same length as piece_sizes. - piece_sizes: The size of each piece with non-zero demand. Must have the same - length as piece_prices, and each size must be in [0, board_length]. - board_size: The length of each board. - - Returns: - The best configuration and its cost. - - Raises: - RuntimeError: On solve errors. - """ - num_pieces = len(piece_sizes) - assert len(piece_sizes) == num_pieces - model = mathopt.Model(name="knapsack") - pieces = [ - model.add_integer_variable(lb=0, name=f"item_{i}") for i in range(num_pieces) - ] - model.maximize(sum(piece_prices[i] * pieces[i] for i in range(num_pieces))) - model.add_linear_constraint( - sum(piece_sizes[i] * pieces[i] for i in range(num_pieces)) <= board_size + """Solves the worker problem. + + Solves the problem on finding the configuration (with its objective value) to + add the to model that will give the greatest improvement in the LP + relaxation. This is equivalent to a knapsack problem. + + Args: + piece_prices: The price for each piece with non-zero demand. Must have the + same length as piece_sizes. + piece_sizes: The size of each piece with non-zero demand. Must have the same + length as piece_prices, and each size must be in [0, board_length]. + board_size: The length of each board. + + Returns: + The best configuration and its cost. + + Raises: + RuntimeError: On solve errors. + """ + num_pieces = len(piece_sizes) + assert len(piece_sizes) == num_pieces + model = mathopt.Model(name="knapsack") + pieces = [ + model.add_integer_variable(lb=0, name=f"item_{i}") + for i in range(num_pieces) + ] + model.maximize(sum(piece_prices[i] * pieces[i] for i in range(num_pieces))) + model.add_linear_constraint( + sum(piece_sizes[i] * pieces[i] for i in range(num_pieces)) <= board_size + ) + solve_result = mathopt.solve(model, mathopt.SolverType.CP_SAT) + if solve_result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise RuntimeError( + "Failed to solve knapsack pricing problem to " + f" optimality: {solve_result.termination}" ) - solve_result = mathopt.solve(model, mathopt.SolverType.CP_SAT) - if solve_result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise RuntimeError( - "Failed to solve knapsack pricing problem to " - f" optimality: {solve_result.termination}" - ) - config = Configuration() - for i in range(num_pieces): - use = round(solve_result.variable_values()[pieces[i]]) - if use > 0: - config.pieces.append(i) - config.quantity.append(use) - return config, solve_result.objective_value() + config = Configuration() + for i in range(num_pieces): + use = round(solve_result.variable_values()[pieces[i]]) + if use > 0: + config.pieces.append(i) + config.quantity.append(use) + return config, solve_result.objective_value() def solve_cutting_stock(instance: CuttingStockInstance) -> CuttingStockSolution: - """Solves the full cutting stock problem by decomposition. - - Args: - instance: A cutting stock instance. - - Returns: - A solution to the cutting stock instance. - - Raises: - RuntimeError: On solve errors. - """ - model = mathopt.Model(name="cutting_stock") - model.objective.is_maximize = False - n = len(instance.piece_sizes) - demands = instance.piece_demands - demand_met = [ - model.add_linear_constraint(lb=demands[i], ub=demands[i]) for i in range(n) - ] - - configs: List[Tuple[Configuration, mathopt.Variable]] = [] - - def add_config(config: Configuration) -> None: - v = model.add_variable(lb=0.0) - model.objective.set_linear_coefficient(v, 1) - for item, use in zip(config.pieces, config.quantity): - if use >= 1: - demand_met[item].set_coefficient(v, use) - configs.append((config, v)) - - # To ensure the leader problem is always feasible, begin a configuration for - # every item that has a single copy of the item. - for i in range(n): - add_config(Configuration(pieces=[i], quantity=[1])) - - solver = mathopt.IncrementalSolver(model, mathopt.SolverType.GLOP) - - pricing_round = 0 - while True: - solve_result = solver.solve() - if solve_result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise RuntimeError( - "Failed to solve leader LP problem to optimality at " - f"iteration {pricing_round} termination: " - f"{solve_result.termination}" - ) - if not solve_result.has_dual_feasible_solution: - # MathOpt does not require solvers to return a dual solution on optimal, - # but most LP solvers always will, see go/mathopt-solver-contracts for - # details. - raise RuntimeError( - "no dual solution was returned with optimal solution " - f"at iteration {pricing_round}" - ) - prices = [solve_result.dual_values()[d] for d in demand_met] - config, value = best_configuration( - prices, instance.piece_sizes, instance.board_length - ) - if value < 1 + 1e-3: - # The LP relaxation is solved, we can stop adding columns. - break - add_config(config) - print( - f"round: {pricing_round}, " - f"lp objective: {solve_result.objective_value()}", - flush=True, - ) - pricing_round += 1 - print("Done adding columns, switching to MIP") - for _, var in configs: - var.integer = True - - solve_result = mathopt.solve(model, mathopt.SolverType.CP_SAT) - if solve_result.termination.reason not in ( - mathopt.TerminationReason.OPTIMAL, - mathopt.TerminationReason.FEASIBLE, - ): - raise RuntimeError( - "Failed to solve final cutting stock MIP, " - f"termination: {solve_result.termination}" - ) - - solution = CuttingStockSolution() - for config, var in configs: - use = round(solve_result.variable_values()[var]) - if use > 0: - solution.configurations.append(config) - solution.quantity.append(use) - solution.objective_value += use - return solution + """Solves the full cutting stock problem by decomposition. + + Args: + instance: A cutting stock instance. + + Returns: + A solution to the cutting stock instance. + + Raises: + RuntimeError: On solve errors. + """ + model = mathopt.Model(name="cutting_stock") + model.objective.is_maximize = False + n = len(instance.piece_sizes) + demands = instance.piece_demands + demand_met = [ + model.add_linear_constraint(lb=demands[i], ub=demands[i]) + for i in range(n) + ] + + configs: List[Tuple[Configuration, mathopt.Variable]] = [] + + def add_config(config: Configuration) -> None: + v = model.add_variable(lb=0.0) + model.objective.set_linear_coefficient(v, 1) + for item, use in zip(config.pieces, config.quantity): + if use >= 1: + demand_met[item].set_coefficient(v, use) + configs.append((config, v)) + + # To ensure the leader problem is always feasible, begin a configuration for + # every item that has a single copy of the item. + for i in range(n): + add_config(Configuration(pieces=[i], quantity=[1])) + + solver = mathopt.IncrementalSolver(model, mathopt.SolverType.GLOP) + + pricing_round = 0 + while True: + solve_result = solver.solve() + if solve_result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise RuntimeError( + "Failed to solve leader LP problem to optimality at " + f"iteration {pricing_round} termination: " + f"{solve_result.termination}" + ) + if not solve_result.has_dual_feasible_solution: + # MathOpt does not require solvers to return a dual solution on optimal, + # but most LP solvers always will, see go/mathopt-solver-contracts for + # details. + raise RuntimeError( + "no dual solution was returned with optimal solution " + f"at iteration {pricing_round}" + ) + prices = [solve_result.dual_values()[d] for d in demand_met] + config, value = best_configuration( + prices, instance.piece_sizes, instance.board_length + ) + if value < 1 + 1e-3: + # The LP relaxation is solved, we can stop adding columns. + break + add_config(config) + print( + f"round: {pricing_round}, " + f"lp objective: {solve_result.objective_value()}", + flush=True, + ) + pricing_round += 1 + print("Done adding columns, switching to MIP") + for _, var in configs: + var.integer = True + + solve_result = mathopt.solve(model, mathopt.SolverType.CP_SAT) + if solve_result.termination.reason not in ( + mathopt.TerminationReason.OPTIMAL, + mathopt.TerminationReason.FEASIBLE, + ): + raise RuntimeError( + "Failed to solve final cutting stock MIP, " + f"termination: {solve_result.termination}" + ) + + solution = CuttingStockSolution() + for config, var in configs: + use = round(solve_result.variable_values()[var]) + if use > 0: + solution.configurations.append(config) + solution.quantity.append(use) + solution.objective_value += use + return solution def main(argv: Sequence[str]) -> None: - del argv # Unused. - - # Data from https://en.wikipedia.org/wiki/Cutting_stock_problem - instance = CuttingStockInstance( - board_length=5600, - piece_sizes=[ - 1380, - 1520, - 1560, - 1710, - 1820, - 1880, - 1930, - 2000, - 2050, - 2100, - 2140, - 2150, - 2200, - ], - piece_demands=[22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20], - ) - solution = solve_cutting_stock(instance) - print("Best known solution uses 73 rolls.") - print(f"Total rolls used in actual solution found: {solution.objective_value}") + del argv # Unused. + + # Data from https://en.wikipedia.org/wiki/Cutting_stock_problem + instance = CuttingStockInstance( + board_length=5600, + piece_sizes=[ + 1380, + 1520, + 1560, + 1710, + 1820, + 1880, + 1930, + 2000, + 2050, + 2100, + 2140, + 2150, + 2200, + ], + piece_demands=[22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20], + ) + solution = solve_cutting_stock(instance) + print("Best known solution uses 73 rolls.") + print( + f"Total rolls used in actual solution found: {solution.objective_value}" + ) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/facility_location.py b/ortools/math_opt/samples/python/facility_location.py index d720ae79142..8f9441565fd 100644 --- a/ortools/math_opt/samples/python/facility_location.py +++ b/ortools/math_opt/samples/python/facility_location.py @@ -69,73 +69,73 @@ @dataclasses.dataclass(frozen=True) class FacilityLocationSolution: - """A solution to a FacilityLocationInstance. + """A solution to a FacilityLocationInstance. - Attributes: - facility_open: For each facility, a bool indicating if it is open. - customer_assignment: For each customer, the index of the facility serving - that customer. - terminal: Is the last solution returned by _solve_facility_location. - """ + Attributes: + facility_open: For each facility, a bool indicating if it is open. + customer_assignment: For each customer, the index of the facility serving + that customer. + terminal: Is the last solution returned by _solve_facility_location. + """ - facility_open: Tuple[bool, ...] - customer_assignment: Tuple[int, ...] - terminal: bool + facility_open: Tuple[bool, ...] + customer_assignment: Tuple[int, ...] + terminal: bool @dataclasses.dataclass(frozen=True) class FacilityLocationInstance: - """Data for a facility location instance. - - Falicities and customers are points in the 2D plane and the cost function is - Euclidean distance. - - Attributes: - facilities: The locations of potential facility sites as (x, y) coordinates. - customers: The locations of customers to serve as (x, y) coordinates. - facility_limit: A limit on the number of facilities that can be opened. - """ - - facilities: Tuple[Tuple[float, float], ...] - customers: Tuple[Tuple[float, float], ...] - facility_limit: int - - def distance(self, facility: int, customer: int) -> float: - """Returns the distance between a facility and a customer.""" - fx, fy = self.facilities[facility] - cx, cy = self.customers[customer] - dx = fx - cx - dy = fy - cy - return math.sqrt(dx * dx + dy * dy) - - def evaluate(self, solution: FacilityLocationSolution) -> float: - """Returns the objective value of a solution, or throws if infeasible.""" - open_fac = sum(solution.facility_open) - if open_fac > self.facility_limit: - raise ValueError( - f"Too many open facilities: {open_fac}, limit was:" - f" {self.facility_limit}" - ) - obj = 0 - for cust, f in enumerate(solution.customer_assignment): - if not solution.facility_open[f]: - raise ValueError( - f"Solution used facility: {f} for customer: {cust}, but it was not" - " open" - ) - obj += self.distance(f, cust) - return obj + """Data for a facility location instance. + + Falicities and customers are points in the 2D plane and the cost function is + Euclidean distance. + + Attributes: + facilities: The locations of potential facility sites as (x, y) coordinates. + customers: The locations of customers to serve as (x, y) coordinates. + facility_limit: A limit on the number of facilities that can be opened. + """ + + facilities: Tuple[Tuple[float, float], ...] + customers: Tuple[Tuple[float, float], ...] + facility_limit: int + + def distance(self, facility: int, customer: int) -> float: + """Returns the distance between a facility and a customer.""" + fx, fy = self.facilities[facility] + cx, cy = self.customers[customer] + dx = fx - cx + dy = fy - cy + return math.sqrt(dx * dx + dy * dy) + + def evaluate(self, solution: FacilityLocationSolution) -> float: + """Returns the objective value of a solution, or throws if infeasible.""" + open_fac = sum(solution.facility_open) + if open_fac > self.facility_limit: + raise ValueError( + f"Too many open facilities: {open_fac}, limit was:" + f" {self.facility_limit}" + ) + obj = 0 + for cust, f in enumerate(solution.customer_assignment): + if not solution.facility_open[f]: + raise ValueError( + f"Solution used facility: {f} for customer: {cust}, but it was not" + " open" + ) + obj += self.distance(f, cust) + return obj def _rand_instance( num_facilities: int, num_customers: int, facility_limit: int ) -> FacilityLocationInstance: - rand_point = lambda: (random.random(), random.random()) - return FacilityLocationInstance( - facilities=tuple(rand_point() for _ in range(num_facilities)), - customers=tuple(rand_point() for _ in range(num_customers)), - facility_limit=facility_limit, - ) + rand_point = lambda: (random.random(), random.random()) + return FacilityLocationInstance( + facilities=tuple(rand_point() for _ in range(num_facilities)), + customers=tuple(rand_point() for _ in range(num_customers)), + facility_limit=facility_limit, + ) def _draw( @@ -143,116 +143,118 @@ def _draw( solution: FacilityLocationSolution, header: str, ) -> None: - """Draws the solution to this instance with matplotlib.""" - ax = plt.gca() - ax.clear() - ax.set_aspect("equal") - obj = instance.evaluate(solution) - ax.set_title(f"{header}, objective value: {obj:.2f}") - for i, location in enumerate(instance.facilities): - color = "g" if solution.facility_open[i] else "r" - ax.add_patch(plt.Circle(location, 0.03, color=color)) - - for j, location in enumerate(instance.customers): - ax.add_patch(plt.Circle(location, 0.01, color="b")) - cx, cy = location - facility = solution.customer_assignment[j] - fx, fy = instance.facilities[facility] - ax.add_line(plt.Line2D([cx, fx], [cy, fy])) - plt.show(block=solution.terminal) - if not solution.terminal: - plt.pause(0.5) + """Draws the solution to this instance with matplotlib.""" + ax = plt.gca() + ax.clear() + ax.set_aspect("equal") + obj = instance.evaluate(solution) + ax.set_title(f"{header}, objective value: {obj:.2f}") + for i, location in enumerate(instance.facilities): + color = "g" if solution.facility_open[i] else "r" + ax.add_patch(plt.Circle(location, 0.03, color=color)) + + for j, location in enumerate(instance.customers): + ax.add_patch(plt.Circle(location, 0.01, color="b")) + cx, cy = location + facility = solution.customer_assignment[j] + fx, fy = instance.facilities[facility] + ax.add_line(plt.Line2D([cx, fx], [cy, fy])) + plt.show(block=solution.terminal) + if not solution.terminal: + plt.pause(0.5) def _solve_facility_location( instance: FacilityLocationInstance, solution_queue: queue.Queue[FacilityLocationSolution], ) -> None: - """Solves instance pushing observed solutions into solution_queue.""" - m = len(instance.facilities) - n = len(instance.customers) - # The optimization model for the facility location problem is: - # - # Data: - # c_ij: The cost of using facility i to serve customer j. - # L: a limit on how many facilities can be opened. - # - # Variables: - # x_i: A binary variable, if facility i is opened. - # y_ij: Is customer j served from facility i, implied integer. - # - # Model: - # min sum_i sum_j c_ij y_ij - # s.t. sum_i x_i <= L (facility limit) - # sum_i y_ij >= 1 for all j (serve each customer) - # y_ij <= x_i for all i for all j (only use open facilities) - # x_i in {0, 1} for all i - # y_ij >= 0 for all i, for all j - model = mathopt.Model() - # Variable x_i: do we open facility i - x = tuple(model.add_binary_variable(name=f"x_{i}") for i in range(m)) - - # Variable y_ij: is customer j served from faciltiy i - y = tuple( - tuple(model.add_variable(lb=0, name=f"y_{i}_{j}") for j in range(n)) - for i in range(m) - ) + """Solves instance pushing observed solutions into solution_queue.""" + m = len(instance.facilities) + n = len(instance.customers) + # The optimization model for the facility location problem is: + # + # Data: + # c_ij: The cost of using facility i to serve customer j. + # L: a limit on how many facilities can be opened. + # + # Variables: + # x_i: A binary variable, if facility i is opened. + # y_ij: Is customer j served from facility i, implied integer. + # + # Model: + # min sum_i sum_j c_ij y_ij + # s.t. sum_i x_i <= L (facility limit) + # sum_i y_ij >= 1 for all j (serve each customer) + # y_ij <= x_i for all i for all j (only use open facilities) + # x_i in {0, 1} for all i + # y_ij >= 0 for all i, for all j + model = mathopt.Model() + # Variable x_i: do we open facility i + x = tuple(model.add_binary_variable(name=f"x_{i}") for i in range(m)) + + # Variable y_ij: is customer j served from faciltiy i + y = tuple( + tuple(model.add_variable(lb=0, name=f"y_{i}_{j}") for j in range(n)) + for i in range(m) + ) + + # Objective: minimize distance to serve customers + total_distance = mathopt.LinearExpression() + for i in range(m): + for j in range(n): + total_distance += y[i][j] * instance.distance(i, j) + model.minimize(total_distance) - # Objective: minimize distance to serve customers - total_distance = mathopt.LinearExpression() - for i in range(m): - for j in range(n): - total_distance += y[i][j] * instance.distance(i, j) - model.minimize(total_distance) + # Constraint (1): sum_i x_i <= L + model.add_linear_constraint(mathopt.fast_sum(x) <= instance.facility_limit) - # Constraint (1): sum_i x_i <= L - model.add_linear_constraint(mathopt.fast_sum(x) <= instance.facility_limit) + # Constraint (2): sum_i y_ij >= 1 for all j + for j in range(n): + model.add_linear_constraint( + mathopt.fast_sum(y[i][j] for i in range(m)) >= 1 + ) - # Constraint (2): sum_i y_ij >= 1 for all j + # Constraint (3): y_ij <= x_i for all i, for all j + for i in range(m): for j in range(n): - model.add_linear_constraint(mathopt.fast_sum(y[i][j] for i in range(m)) >= 1) - - # Constraint (3): y_ij <= x_i for all i, for all j - for i in range(m): - for j in range(n): - model.add_linear_constraint(y[i][j] <= x[i]) - - def extract_solution( - var_values: Dict[mathopt.Variable, float], terminal: bool - ) -> FacilityLocationSolution: - is_open = tuple(var_values[x[i]] > 0.5 for i in range(m)) - customer_assignment = [] - for j in range(n): - for i in range(m): - if var_values[y[i][j]] > 0.5: - customer_assignment.append(i) - break - assert len(customer_assignment) == n - return FacilityLocationSolution( - facility_open=is_open, - customer_assignment=tuple(customer_assignment), - terminal=terminal, - ) + model.add_linear_constraint(y[i][j] <= x[i]) - def draw_cb(cb_data: mathopt.CallbackData) -> mathopt.CallbackResult: - if cb_data.event != mathopt.Event.MIP_SOLUTION: - raise ValueError(f"event should be MIP_SOLUTION was: {cb_data.event}") - assert cb_data.solution is not None - assert solution_queue is not None - solution = extract_solution(cb_data.solution, terminal=False) - solution_queue.put(solution) - return mathopt.CallbackResult() - - cb_reg = mathopt.CallbackRegistration(events={mathopt.Event.MIP_SOLUTION}) - result = mathopt.solve( - model, - _SOLVER.value, - callback_reg=cb_reg, - cb=draw_cb, + def extract_solution( + var_values: Dict[mathopt.Variable, float], terminal: bool + ) -> FacilityLocationSolution: + is_open = tuple(var_values[x[i]] > 0.5 for i in range(m)) + customer_assignment = [] + for j in range(n): + for i in range(m): + if var_values[y[i][j]] > 0.5: + customer_assignment.append(i) + break + assert len(customer_assignment) == n + return FacilityLocationSolution( + facility_open=is_open, + customer_assignment=tuple(customer_assignment), + terminal=terminal, ) - if result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise ValueError(f"expected optimal solution, found {result.termination}") - solution_queue.put(extract_solution(result.variable_values(), terminal=True)) + + def draw_cb(cb_data: mathopt.CallbackData) -> mathopt.CallbackResult: + if cb_data.event != mathopt.Event.MIP_SOLUTION: + raise ValueError(f"event should be MIP_SOLUTION was: {cb_data.event}") + assert cb_data.solution is not None + assert solution_queue is not None + solution = extract_solution(cb_data.solution, terminal=False) + solution_queue.put(solution) + return mathopt.CallbackResult() + + cb_reg = mathopt.CallbackRegistration(events={mathopt.Event.MIP_SOLUTION}) + result = mathopt.solve( + model, + _SOLVER.value, + callback_reg=cb_reg, + cb=draw_cb, + ) + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise ValueError(f"expected optimal solution, found {result.termination}") + solution_queue.put(extract_solution(result.variable_values(), terminal=True)) def _plot_solution( @@ -260,8 +262,8 @@ def _plot_solution( solution: FacilityLocationSolution, sol_num: int, ) -> None: - header = "Solved" if solution.terminal else f"solution: {sol_num}" - _draw(instance, solution, header) + header = "Solved" if solution.terminal else f"solution: {sol_num}" + _draw(instance, solution, header) def _log_solution( @@ -269,32 +271,32 @@ def _log_solution( solution: FacilityLocationSolution, sol_num: int, ) -> None: - obj = instance.evaluate(solution) - if solution.terminal: - print(f"best solution: {obj}") - else: - print(f"solution {sol_num}: {obj}") + obj = instance.evaluate(solution) + if solution.terminal: + print(f"best solution: {obj}") + else: + print(f"solution {sol_num}: {obj}") def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - instance = _rand_instance( - _NUM_FACILITIES.value, _NUM_CUSTOMERS.value, _FACILITY_LIMIT.value - ) - consumer = _plot_solution if _PLOT.value else _log_solution - solution_queue = queue.Queue() - threading.Thread( - target=lambda: _solve_facility_location(instance, solution_queue) - ).start() - sol_num = 0 - while True: - solution = solution_queue.get() - sol_num += 1 - consumer(instance, solution, sol_num) - if solution.terminal: - break + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + instance = _rand_instance( + _NUM_FACILITIES.value, _NUM_CUSTOMERS.value, _FACILITY_LIMIT.value + ) + consumer = _plot_solution if _PLOT.value else _log_solution + solution_queue = queue.Queue() + threading.Thread( + target=lambda: _solve_facility_location(instance, solution_queue) + ).start() + sol_num = 0 + while True: + solution = solution_queue.get() + sol_num += 1 + consumer(instance, solution, sol_num) + if solution.terminal: + break if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/facility_lp_benders.py b/ortools/math_opt/samples/python/facility_lp_benders.py index 7c250366645..d36e615be09 100644 --- a/ortools/math_opt/samples/python/facility_lp_benders.py +++ b/ortools/math_opt/samples/python/facility_lp_benders.py @@ -58,14 +58,22 @@ from ortools.math_opt.python import mathopt -_NUM_FACILITIES = flags.DEFINE_integer("num_facilities", 3000, "Number of facilities.") -_NUM_LOCATIONS = flags.DEFINE_integer("num_locations", 50, "Number of locations.") -_EDGE_PROBABILITY = flags.DEFINE_float("edge_probability", 0.99, "Edge probability.") +_NUM_FACILITIES = flags.DEFINE_integer( + "num_facilities", 3000, "Number of facilities." +) +_NUM_LOCATIONS = flags.DEFINE_integer( + "num_locations", 50, "Number of locations." +) +_EDGE_PROBABILITY = flags.DEFINE_float( + "edge_probability", 0.99, "Edge probability." +) _BENDERS_PRECISSION = flags.DEFINE_float( "benders_precission", 1e-9, "Benders target precission." ) _LOCATION_DEMAND = flags.DEFINE_float("location_demand", 1, "Client demands.") -_FACILITY_COST = flags.DEFINE_float("facility_cost", 100, "Facility capacity cost.") +_FACILITY_COST = flags.DEFINE_float( + "facility_cost", 100, "Facility capacity cost." +) _LOCATION_FRACTION = flags.DEFINE_float( "location_fraction", 0.001, @@ -89,59 +97,59 @@ class Network: - """A simple randomly-generated facility-location network.""" - - def __init__( - self, num_facilities: int, num_locations: int, edge_probability: float - ) -> None: - rng = np.random.default_rng(123) - self.num_facilities: int = num_facilities - self.num_locations: int = num_locations - self.facility_edge_incidence: List[List[Edge]] = [ - [] for facility in range(num_facilities) - ] - self.location_edge_incidence: List[List[Edge]] = [ - [] for location in range(num_locations) - ] - self.edges: List[Edge] = [] - self.edge_costs: Dict[Edge, float] = {} - for facility in range(num_facilities): - for location in range(num_locations): - if rng.binomial(1, edge_probability): - edge: Edge = (facility, location) - self.facility_edge_incidence[facility].append(edge) - self.location_edge_incidence[location].append(edge) - self.edges.append(edge) - self.edge_costs[edge] = rng.uniform() - - # Ensure every facility is connected to at least one location and every - # location is connected to at least one facility. - for facility in range(num_facilities): - if not self.facility_edge_incidence[facility]: - location = rng.integer(num_locations) - edge: Edge = (facility, location) - self.facility_edge_incidence[facility].append(edge) - self.location_edge_incidence[location].append(edge) - self.edges.append(edge) - self.edge_costs[edge] = rng.uniform() - for location in range(num_locations): - if not self.location_edge_incidence[location]: - facility = rng.integer(num_facilities) - edge: Edge = (facility, location) - self.facility_edge_incidence[facility].append(edge) - self.location_edge_incidence[location].append(edge) - self.edges.append(edge) - self.edge_costs[edge] = rng.uniform() + """A simple randomly-generated facility-location network.""" + + def __init__( + self, num_facilities: int, num_locations: int, edge_probability: float + ) -> None: + rng = np.random.default_rng(123) + self.num_facilities: int = num_facilities + self.num_locations: int = num_locations + self.facility_edge_incidence: List[List[Edge]] = [ + [] for facility in range(num_facilities) + ] + self.location_edge_incidence: List[List[Edge]] = [ + [] for location in range(num_locations) + ] + self.edges: List[Edge] = [] + self.edge_costs: Dict[Edge, float] = {} + for facility in range(num_facilities): + for location in range(num_locations): + if rng.binomial(1, edge_probability): + edge: Edge = (facility, location) + self.facility_edge_incidence[facility].append(edge) + self.location_edge_incidence[location].append(edge) + self.edges.append(edge) + self.edge_costs[edge] = rng.uniform() + + # Ensure every facility is connected to at least one location and every + # location is connected to at least one facility. + for facility in range(num_facilities): + if not self.facility_edge_incidence[facility]: + location = rng.integer(num_locations) + edge: Edge = (facility, location) + self.facility_edge_incidence[facility].append(edge) + self.location_edge_incidence[location].append(edge) + self.edges.append(edge) + self.edge_costs[edge] = rng.uniform() + for location in range(num_locations): + if not self.location_edge_incidence[location]: + facility = rng.integer(num_facilities) + edge: Edge = (facility, location) + self.facility_edge_incidence[facility].append(edge) + self.location_edge_incidence[location].append(edge) + self.edges.append(edge) + self.edge_costs[edge] = rng.uniform() @dataclasses.dataclass class FacilityLocationInstance: - """A facility location instance.""" + """A facility location instance.""" - network: Network - location_demand: float - facility_cost: float - location_fraction: float + network: Network + location_demand: float + facility_cost: float + location_fraction: float ################################################################################ @@ -152,58 +160,61 @@ class FacilityLocationInstance: def full_problem( instance: FacilityLocationInstance, solver_type: mathopt.SolverType ) -> None: - """Solves the full formulation of the facility location problem. - - See file level comment for problem description and formulation. - - Args: - instance: a facility location instance. - solver_type: what solver to use. - - Raises: - RuntimeError: On solve errors. - """ - num_facilities = instance.network.num_facilities - num_locations = instance.network.num_locations - model = mathopt.Model(name="Full network design problem") - - # Capacity variables - z = [model.add_variable(lb=0.0) for j in range(num_facilities)] - - # Flow variables - x = {edge: model.add_variable(lb=0.0) for edge in instance.network.edges} - - # Objective Function - objective_for_edges = sum( - instance.network.edge_costs[edge] * x[edge] for edge in instance.network.edges + """Solves the full formulation of the facility location problem. + + See file level comment for problem description and formulation. + + Args: + instance: a facility location instance. + solver_type: what solver to use. + + Raises: + RuntimeError: On solve errors. + """ + num_facilities = instance.network.num_facilities + num_locations = instance.network.num_locations + model = mathopt.Model(name="Full network design problem") + + # Capacity variables + z = [model.add_variable(lb=0.0) for j in range(num_facilities)] + + # Flow variables + x = {edge: model.add_variable(lb=0.0) for edge in instance.network.edges} + + # Objective Function + objective_for_edges = sum( + instance.network.edge_costs[edge] * x[edge] + for edge in instance.network.edges + ) + model.minimize(objective_for_edges + instance.facility_cost * sum(z)) + + # Demand constraints + for location in range(num_locations): + incoming_supply = sum( + x[edge] for edge in instance.network.location_edge_incidence[location] ) - model.minimize(objective_for_edges + instance.facility_cost * sum(z)) - - # Demand constraints - for location in range(num_locations): - incoming_supply = sum( - x[edge] for edge in instance.network.location_edge_incidence[location] - ) - model.add_linear_constraint(incoming_supply >= instance.location_demand) + model.add_linear_constraint(incoming_supply >= instance.location_demand) - # Supply constraints - for facility in range(num_facilities): - outgoing_supply = sum( - x[edge] for edge in instance.network.facility_edge_incidence[facility] - ) - model.add_linear_constraint(outgoing_supply <= z[facility]) - - # Arc constraints - for facility in range(num_facilities): - for edge in instance.network.facility_edge_incidence[facility]: - model.add_linear_constraint( - x[edge] <= instance.location_fraction * z[facility] - ) - - result = mathopt.solve(model, solver_type) - if result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise RuntimeError(f"failed to find an optimal solution: {result.termination}") - print(f"Fulll problem optimal objective: {result.objective_value():.9f}") + # Supply constraints + for facility in range(num_facilities): + outgoing_supply = sum( + x[edge] for edge in instance.network.facility_edge_incidence[facility] + ) + model.add_linear_constraint(outgoing_supply <= z[facility]) + + # Arc constraints + for facility in range(num_facilities): + for edge in instance.network.facility_edge_incidence[facility]: + model.add_linear_constraint( + x[edge] <= instance.location_fraction * z[facility] + ) + + result = mathopt.solve(model, solver_type) + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise RuntimeError( + f"failed to find an optimal solution: {result.termination}" + ) + print(f"Fulll problem optimal objective: {result.objective_value():.9f}") ################################################################################ @@ -219,296 +230,298 @@ def full_problem( # sum(fcut_f^i z_f) + fcut_const^i <= 0 for i = 1,... # sum(ocut_f^j z_f) + ocut_const^j <= w for j = 1,... class FirstStageProblem: - """First stage model and variables.""" + """First stage model and variables.""" - model: mathopt.Model - z: List[mathopt.Variable] - w: mathopt.Variable + model: mathopt.Model + z: List[mathopt.Variable] + w: mathopt.Variable - def __init__(self, network: Network, facility_cost: float) -> None: - self.model: mathopt.Model = mathopt.Model(name="First stage problem") + def __init__(self, network: Network, facility_cost: float) -> None: + self.model: mathopt.Model = mathopt.Model(name="First stage problem") - # Capacity variables - self.z: List[mathopt.Variable] = [ - self.model.add_variable(lb=0.0) for j in range(network.num_facilities) - ] + # Capacity variables + self.z: List[mathopt.Variable] = [ + self.model.add_variable(lb=0.0) for j in range(network.num_facilities) + ] - # Objective variable - self.w: mathopt.Variable = self.model.add_variable(lb=0.0) + # Objective variable + self.w: mathopt.Variable = self.model.add_variable(lb=0.0) - # First stage objective - self.model.minimize(self.w + facility_cost * sum(self.z)) + # First stage objective + self.model.minimize(self.w + facility_cost * sum(self.z)) @dataclasses.dataclass class Cut: - """A feasibility or optimality cut. + """A feasibility or optimality cut. - The cut is of the form: + The cut is of the form: - z_coefficients^T z + constant <= w_coefficient * w + z_coefficients^T z + constant <= w_coefficient * w - This will be a feasibility cut if w_coefficient = 0.0 and an optimality - cut if w_coefficient = 1. - """ + This will be a feasibility cut if w_coefficient = 0.0 and an optimality + cut if w_coefficient = 1. + """ - z_coefficients: List[float] = dataclasses.field(default_factory=list) - constant: float = 0.0 - w_coefficient: float = 0.0 + z_coefficients: List[float] = dataclasses.field(default_factory=list) + constant: float = 0.0 + w_coefficient: float = 0.0 def ensure_dual_ray_solve_parameters( solver_type: mathopt.SolverType, ) -> mathopt.SolveParameters: - """Set parameters to ensure a dual ray is returned for infeasible problems.""" - if solver_type == mathopt.SolverType.GUROBI: - return mathopt.SolveParameters( - gurobi=mathopt.GurobiParameters(param_values={"InfUnbdInfo": "1"}) - ) - if solver_type == mathopt.SolverType.GLOP: - return mathopt.SolveParameters( - presolve=mathopt.Emphasis.OFF, - scaling=mathopt.Emphasis.OFF, - lp_algorithm=mathopt.LPAlgorithm.DUAL_SIMPLEX, - ) - if solver_type == mathopt.SolverType.GLPK: - return mathopt.SolveParameters( - presolve=mathopt.Emphasis.OFF, - lp_algorithm=mathopt.LPAlgorithm.DUAL_SIMPLEX, - glpk=mathopt.GlpkParameters(compute_unbound_rays_if_possible=True), - ) - if solver_type == mathopt.SolverType.PDLP: - # No arguments needed. - return mathopt.SolveParameters() - raise ValueError(f"unsupported solver: {solver_type}") + """Set parameters to ensure a dual ray is returned for infeasible problems.""" + if solver_type == mathopt.SolverType.GUROBI: + return mathopt.SolveParameters( + gurobi=mathopt.GurobiParameters(param_values={"InfUnbdInfo": "1"}) + ) + if solver_type == mathopt.SolverType.GLOP: + return mathopt.SolveParameters( + presolve=mathopt.Emphasis.OFF, + scaling=mathopt.Emphasis.OFF, + lp_algorithm=mathopt.LPAlgorithm.DUAL_SIMPLEX, + ) + if solver_type == mathopt.SolverType.GLPK: + return mathopt.SolveParameters( + presolve=mathopt.Emphasis.OFF, + lp_algorithm=mathopt.LPAlgorithm.DUAL_SIMPLEX, + glpk=mathopt.GlpkParameters(compute_unbound_rays_if_possible=True), + ) + if solver_type == mathopt.SolverType.PDLP: + # No arguments needed. + return mathopt.SolveParameters() + raise ValueError(f"unsupported solver: {solver_type}") class SecondStageSolver: - """Solves the second stage model. + """Solves the second stage model. + + The model is: + + min sum(h_e * x_e : e in E) + s.t. + x_(f,l) <= a * zz_f for all (f,l) in E + sum(x_(f,l) : l such that (f,l) in E) <= zz_f for all f in F + sum(x_(f,l) : f such that (f,l) in E) >= d for all l in L + x_e >= 0 for all e in E + + where zz_f are fixed values for z_f from the first stage model, and generates + an infeasibility or optimality cut as needed. + """ + + def __init__( + self, instance: FacilityLocationInstance, solver_type: mathopt.SolverType + ) -> None: + self._network: Network = instance.network + self._second_stage_params: mathopt.SolveParameters = ( + ensure_dual_ray_solve_parameters(solver_type) + ) + self._location_fraction: float = instance.location_fraction + + num_facilities = self._network.num_facilities + num_locations = self._network.num_locations + self._second_stage_model = mathopt.Model(name="Second stage model") - The model is: + # Flow variables + self._x = { + edge: self._second_stage_model.add_variable(lb=0.0) + for edge in self._network.edges + } - min sum(h_e * x_e : e in E) - s.t. - x_(f,l) <= a * zz_f for all (f,l) in E - sum(x_(f,l) : l such that (f,l) in E) <= zz_f for all f in F - sum(x_(f,l) : f such that (f,l) in E) >= d for all l in L - x_e >= 0 for all e in E + # Objective Function + objective_for_edges = sum( + self._network.edge_costs[edge] * self._x[edge] + for edge in self._network.edges + ) + self._second_stage_model.minimize(objective_for_edges) - where zz_f are fixed values for z_f from the first stage model, and generates - an infeasibility or optimality cut as needed. - """ + # Demand constraints + self._demand_constraints: List[mathopt.LinearConstraint] = [] + for location in range(num_locations): + incoming_supply = sum( + self._x[edge] + for edge in self._network.location_edge_incidence[location] + ) + self._demand_constraints.append( + self._second_stage_model.add_linear_constraint( + incoming_supply >= instance.location_demand + ) + ) - def __init__( - self, instance: FacilityLocationInstance, solver_type: mathopt.SolverType - ) -> None: - self._network: Network = instance.network - self._second_stage_params: mathopt.SolveParameters = ( - ensure_dual_ray_solve_parameters(solver_type) - ) - self._location_fraction: float = instance.location_fraction - - num_facilities = self._network.num_facilities - num_locations = self._network.num_locations - self._second_stage_model = mathopt.Model(name="Second stage model") - - # Flow variables - self._x = { - edge: self._second_stage_model.add_variable(lb=0.0) - for edge in self._network.edges - } - - # Objective Function - objective_for_edges = sum( - self._network.edge_costs[edge] * self._x[edge] - for edge in self._network.edges - ) - self._second_stage_model.minimize(objective_for_edges) - - # Demand constraints - self._demand_constraints: List[mathopt.LinearConstraint] = [] - for location in range(num_locations): - incoming_supply = sum( - self._x[edge] - for edge in self._network.location_edge_incidence[location] - ) - self._demand_constraints.append( - self._second_stage_model.add_linear_constraint( - incoming_supply >= instance.location_demand - ) - ) - - # Supply constraints - self._supply_constraint: List[mathopt.LinearConstraint] = [] - for facility in range(num_facilities): - outgoing_supply = sum( - self._x[edge] - for edge in self._network.facility_edge_incidence[facility] - ) - # Set supply constraint with trivial upper bound to be updated with first - # stage information. - self._supply_constraint.append( - self._second_stage_model.add_linear_constraint( - outgoing_supply <= math.inf - ) - ) - - self._second_stage_solver = mathopt.IncrementalSolver( - self._second_stage_model, solver_type - ) + # Supply constraints + self._supply_constraint: List[mathopt.LinearConstraint] = [] + for facility in range(num_facilities): + outgoing_supply = sum( + self._x[edge] + for edge in self._network.facility_edge_incidence[facility] + ) + # Set supply constraint with trivial upper bound to be updated with first + # stage information. + self._supply_constraint.append( + self._second_stage_model.add_linear_constraint( + outgoing_supply <= math.inf + ) + ) + + self._second_stage_solver = mathopt.IncrementalSolver( + self._second_stage_model, solver_type + ) - def solve( - self, z_values: List[float], w_value: float, first_stage_objective: float - ) -> Tuple[float, Cut]: - """Solve the second stage problem.""" - num_facilities = self._network.num_facilities - # Update second stage with first stage solution. - for facility in range(num_facilities): - if z_values[facility] < -_ZERO_TOL: - raise RuntimeError( - "negative z_value in first stage: " - f"{z_values[facility]} for facility {facility}" - ) - # Make sure variable bounds are valid (lb <= ub). - capacity_value = max(z_values[facility], 0.0) - for edge in self._network.facility_edge_incidence[facility]: - self._x[edge].upper_bound = self._location_fraction * capacity_value - self._supply_constraint[facility].upper_bound = capacity_value - # Solve and process second stage. - second_stage_result = self._second_stage_solver.solve( - params=self._second_stage_params + def solve( + self, z_values: List[float], w_value: float, first_stage_objective: float + ) -> Tuple[float, Cut]: + """Solve the second stage problem.""" + num_facilities = self._network.num_facilities + # Update second stage with first stage solution. + for facility in range(num_facilities): + if z_values[facility] < -_ZERO_TOL: + raise RuntimeError( + "negative z_value in first stage: " + f"{z_values[facility]} for facility {facility}" ) - if second_stage_result.termination.reason not in ( - mathopt.TerminationReason.OPTIMAL, - mathopt.TerminationReason.INFEASIBLE, - ): - raise RuntimeError( - "second stage was not solved to optimality or infeasibility: " - f"{second_stage_result.termination}" - ) - if ( - second_stage_result.termination.reason - == mathopt.TerminationReason.INFEASIBLE - ): - # If the second stage problem is infeasible we can construct a - # feasibility cut from a returned dual ray. - return math.inf, self.feasibility_cut(second_stage_result) - else: - # If the second stage problem is optimal we can construct an optimality - # cut from a returned dual optimal solution. We can also update the upper - # bound, which is obtained by switching predicted second stage objective - # value w with the true second stage objective value. - upper_bound = ( - first_stage_objective - w_value + second_stage_result.objective_value() - ) - return upper_bound, self.optimality_cut(second_stage_result) - - def feasibility_cut(self, second_stage_result: mathopt.SolveResult) -> Cut: - """Build and return a feasibility cut. - - If the second stage problem is infeasible we get a dual ray (r, y) such - that - - sum(r_(f,l)*a*zz_f : (f,l) in E, r_(f,l) < 0) - + sum(y_f*zz_f : f in F, y_f < 0) - + sum(y_l*d : l in L, y_l > 0) > 0. - - Then we get the feasibility cut (go/math_opt-advanced-dual-use#benders) - - sum(fcut_f*z_f) + fcut_const <= 0, - - where - - fcut_f = sum(r_(f,l)*a : (f,l) in E, r_(f,l) < 0) - + min{y_f, 0} - fcut_const = sum*(y_l*d : l in L, y_l > 0) - - Args: - second_stage_result: The result from the second stage solve. - - Raises: - RuntimeError: if dual ray is not available. - - Returns: - A feasibility cut. - """ - num_facilities = self._network.num_facilities - result = Cut() - if not second_stage_result.has_dual_ray(): - # MathOpt does not require solvers to return a dual ray on infeasible, - # but most LP solvers always will, see go/mathopt-solver-contracts for - # details. - raise RuntimeError("no dual ray available for feasibility cut") - - for facility in range(num_facilities): - coefficient = 0.0 - for edge in self._network.facility_edge_incidence[facility]: - reduced_cost = second_stage_result.ray_reduced_costs(self._x[edge]) - coefficient += self._location_fraction * min(reduced_cost, 0.0) - dual_value = second_stage_result.ray_dual_values( - self._supply_constraint[facility] - ) - coefficient += min(dual_value, 0.0) - result.z_coefficients.append(coefficient) - result.constant = 0.0 - for constraint in self._demand_constraints: - dual_value = second_stage_result.ray_dual_values(constraint) - result.constant += max(dual_value, 0.0) - result.w_coefficient = 0.0 - return result - - def optimality_cut(self, second_stage_result: mathopt.SolveResult) -> Cut: - """Build and return an optimality cut. - - If the second stage problem is optimal we get a dual solution (r, y) such - that the optimal objective value is equal to - - sum(r_(f,l)*a*zz_f : (f,l) in E, r_(f,l) < 0) - + sum(y_f*zz_f : f in F, y_f < 0) - + sum*(y_l*d : l in L, y_l > 0) > 0. - - Then we get the optimality cut (go/math_opt-advanced-dual-use#benders) - - sum(ocut_f*z_f) + ocut_const <= w, - - where - - ocut_f = sum(r_(f,l)*a : (f,l) in E, r_(f,l) < 0) - + min{y_f, 0} - ocut_const = sum*(y_l*d : l in L, y_l > 0) - - Args: - second_stage_result: The result from the second stage solve. - - Raises: - RuntimeError: if dual solution is not available. - - Returns: - An optimality cut. - """ - num_facilities = self._network.num_facilities - result = Cut() - if not second_stage_result.has_dual_feasible_solution(): - # MathOpt does not require solvers to return a dual solution on optimal, - # but most LP solvers always will, see go/mathopt-solver-contracts for - # details. - raise RuntimeError("no dual solution available for optimality cut") - for facility in range(num_facilities): - coefficient = 0.0 - for edge in self._network.facility_edge_incidence[facility]: - reduced_cost = second_stage_result.reduced_costs(self._x[edge]) - coefficient += self._location_fraction * min(reduced_cost, 0.0) - dual_value = second_stage_result.dual_values( - self._supply_constraint[facility] - ) - coefficient += min(dual_value, 0.0) - result.z_coefficients.append(coefficient) - result.constant = 0.0 - for constraint in self._demand_constraints: - dual_value = second_stage_result.dual_values(constraint) - result.constant += max(dual_value, 0.0) - result.w_coefficient = 1.0 - return result + # Make sure variable bounds are valid (lb <= ub). + capacity_value = max(z_values[facility], 0.0) + for edge in self._network.facility_edge_incidence[facility]: + self._x[edge].upper_bound = self._location_fraction * capacity_value + self._supply_constraint[facility].upper_bound = capacity_value + # Solve and process second stage. + second_stage_result = self._second_stage_solver.solve( + params=self._second_stage_params + ) + if second_stage_result.termination.reason not in ( + mathopt.TerminationReason.OPTIMAL, + mathopt.TerminationReason.INFEASIBLE, + ): + raise RuntimeError( + "second stage was not solved to optimality or infeasibility: " + f"{second_stage_result.termination}" + ) + if ( + second_stage_result.termination.reason + == mathopt.TerminationReason.INFEASIBLE + ): + # If the second stage problem is infeasible we can construct a + # feasibility cut from a returned dual ray. + return math.inf, self.feasibility_cut(second_stage_result) + else: + # If the second stage problem is optimal we can construct an optimality + # cut from a returned dual optimal solution. We can also update the upper + # bound, which is obtained by switching predicted second stage objective + # value w with the true second stage objective value. + upper_bound = ( + first_stage_objective + - w_value + + second_stage_result.objective_value() + ) + return upper_bound, self.optimality_cut(second_stage_result) + + def feasibility_cut(self, second_stage_result: mathopt.SolveResult) -> Cut: + """Build and return a feasibility cut. + + If the second stage problem is infeasible we get a dual ray (r, y) such + that + + sum(r_(f,l)*a*zz_f : (f,l) in E, r_(f,l) < 0) + + sum(y_f*zz_f : f in F, y_f < 0) + + sum(y_l*d : l in L, y_l > 0) > 0. + + Then we get the feasibility cut (go/math_opt-advanced-dual-use#benders) + + sum(fcut_f*z_f) + fcut_const <= 0, + + where + + fcut_f = sum(r_(f,l)*a : (f,l) in E, r_(f,l) < 0) + + min{y_f, 0} + fcut_const = sum*(y_l*d : l in L, y_l > 0) + + Args: + second_stage_result: The result from the second stage solve. + + Raises: + RuntimeError: if dual ray is not available. + + Returns: + A feasibility cut. + """ + num_facilities = self._network.num_facilities + result = Cut() + if not second_stage_result.has_dual_ray(): + # MathOpt does not require solvers to return a dual ray on infeasible, + # but most LP solvers always will, see go/mathopt-solver-contracts for + # details. + raise RuntimeError("no dual ray available for feasibility cut") + + for facility in range(num_facilities): + coefficient = 0.0 + for edge in self._network.facility_edge_incidence[facility]: + reduced_cost = second_stage_result.ray_reduced_costs(self._x[edge]) + coefficient += self._location_fraction * min(reduced_cost, 0.0) + dual_value = second_stage_result.ray_dual_values( + self._supply_constraint[facility] + ) + coefficient += min(dual_value, 0.0) + result.z_coefficients.append(coefficient) + result.constant = 0.0 + for constraint in self._demand_constraints: + dual_value = second_stage_result.ray_dual_values(constraint) + result.constant += max(dual_value, 0.0) + result.w_coefficient = 0.0 + return result + + def optimality_cut(self, second_stage_result: mathopt.SolveResult) -> Cut: + """Build and return an optimality cut. + + If the second stage problem is optimal we get a dual solution (r, y) such + that the optimal objective value is equal to + + sum(r_(f,l)*a*zz_f : (f,l) in E, r_(f,l) < 0) + + sum(y_f*zz_f : f in F, y_f < 0) + + sum*(y_l*d : l in L, y_l > 0) > 0. + + Then we get the optimality cut (go/math_opt-advanced-dual-use#benders) + + sum(ocut_f*z_f) + ocut_const <= w, + + where + + ocut_f = sum(r_(f,l)*a : (f,l) in E, r_(f,l) < 0) + + min{y_f, 0} + ocut_const = sum*(y_l*d : l in L, y_l > 0) + + Args: + second_stage_result: The result from the second stage solve. + + Raises: + RuntimeError: if dual solution is not available. + + Returns: + An optimality cut. + """ + num_facilities = self._network.num_facilities + result = Cut() + if not second_stage_result.has_dual_feasible_solution(): + # MathOpt does not require solvers to return a dual solution on optimal, + # but most LP solvers always will, see go/mathopt-solver-contracts for + # details. + raise RuntimeError("no dual solution available for optimality cut") + for facility in range(num_facilities): + coefficient = 0.0 + for edge in self._network.facility_edge_incidence[facility]: + reduced_cost = second_stage_result.reduced_costs(self._x[edge]) + coefficient += self._location_fraction * min(reduced_cost, 0.0) + dual_value = second_stage_result.dual_values( + self._supply_constraint[facility] + ) + coefficient += min(dual_value, 0.0) + result.z_coefficients.append(coefficient) + result.constant = 0.0 + for constraint in self._demand_constraints: + dual_value = second_stage_result.dual_values(constraint) + result.constant += max(dual_value, 0.0) + result.w_coefficient = 1.0 + return result def benders( @@ -517,87 +530,90 @@ def benders( solver_type: mathopt.SolverType, maximum_iterations: int = 30000, ) -> None: - """Solves the facility location problem using Benders decomposition. - - Args: - instance: a facility location instance. - target_precission: the target difference between upper and lower bounds. - solver_type: what solver to use. - maximum_iterations: limit on the number of iterations. - - Raises: - RuntimeError: On solve errors. - """ - num_facilities = instance.network.num_facilities - - # Setup first stage model and solver. - first_stage = FirstStageProblem(instance.network, instance.facility_cost) - first_stage_solver = mathopt.IncrementalSolver(first_stage.model, solver_type) - - # Setup second stage solver. - second_stage_solver = SecondStageSolver(instance, solver_type) - - # Start Benders - iteration = 0 - best_upper_bound = math.inf - while True: - print(f"Iteration: {iteration}", flush=True) - - # Solve and process first stage. - first_stage_result = first_stage_solver.solve() - if first_stage_result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise RuntimeError( - "could not solve first stage problem to optimality: " - f"{first_stage_result.termination}" - ) - z_values = [ - first_stage_result.variable_values(first_stage.z[j]) - for j in range(num_facilities) - ] - lower_bound = first_stage_result.objective_value() - print(f"LB = {lower_bound}", flush=True) - # Solve and process second stage. - upper_bound, cut = second_stage_solver.solve( - z_values, - first_stage_result.variable_values(first_stage.w), - first_stage_result.objective_value(), - ) - cut_expression = sum( - cut.z_coefficients[j] * first_stage.z[j] for j in range(num_facilities) - ) - cut_expression += cut.constant - first_stage.model.add_linear_constraint( - cut_expression <= cut.w_coefficient * first_stage.w - ) - best_upper_bound = min(upper_bound, best_upper_bound) - print(f"UB = {best_upper_bound}", flush=True) - iteration += 1 - if best_upper_bound - lower_bound < target_precission: - print(f"Total iterations = {iteration}", flush=True) - print(f"Final LB = {lower_bound:.9f}", flush=True) - print(f"Final UB = {best_upper_bound:.9f}", flush=True) - break - if iteration > maximum_iterations: - break + """Solves the facility location problem using Benders decomposition. + + Args: + instance: a facility location instance. + target_precission: the target difference between upper and lower bounds. + solver_type: what solver to use. + maximum_iterations: limit on the number of iterations. + + Raises: + RuntimeError: On solve errors. + """ + num_facilities = instance.network.num_facilities + + # Setup first stage model and solver. + first_stage = FirstStageProblem(instance.network, instance.facility_cost) + first_stage_solver = mathopt.IncrementalSolver(first_stage.model, solver_type) + + # Setup second stage solver. + second_stage_solver = SecondStageSolver(instance, solver_type) + + # Start Benders + iteration = 0 + best_upper_bound = math.inf + while True: + print(f"Iteration: {iteration}", flush=True) + + # Solve and process first stage. + first_stage_result = first_stage_solver.solve() + if ( + first_stage_result.termination.reason + != mathopt.TerminationReason.OPTIMAL + ): + raise RuntimeError( + "could not solve first stage problem to optimality: " + f"{first_stage_result.termination}" + ) + z_values = [ + first_stage_result.variable_values(first_stage.z[j]) + for j in range(num_facilities) + ] + lower_bound = first_stage_result.objective_value() + print(f"LB = {lower_bound}", flush=True) + # Solve and process second stage. + upper_bound, cut = second_stage_solver.solve( + z_values, + first_stage_result.variable_values(first_stage.w), + first_stage_result.objective_value(), + ) + cut_expression = sum( + cut.z_coefficients[j] * first_stage.z[j] for j in range(num_facilities) + ) + cut_expression += cut.constant + first_stage.model.add_linear_constraint( + cut_expression <= cut.w_coefficient * first_stage.w + ) + best_upper_bound = min(upper_bound, best_upper_bound) + print(f"UB = {best_upper_bound}", flush=True) + iteration += 1 + if best_upper_bound - lower_bound < target_precission: + print(f"Total iterations = {iteration}", flush=True) + print(f"Final LB = {lower_bound:.9f}", flush=True) + print(f"Final UB = {best_upper_bound:.9f}", flush=True) + break + if iteration > maximum_iterations: + break def main(argv: Sequence[str]) -> None: - del argv # Unused. - instance = FacilityLocationInstance( - network=Network( - _NUM_FACILITIES.value, _NUM_LOCATIONS.value, _EDGE_PROBABILITY.value - ), - location_demand=_LOCATION_DEMAND.value, - facility_cost=_FACILITY_COST.value, - location_fraction=_LOCATION_FRACTION.value, - ) - start = time.monotonic() - full_problem(instance, _SOLVER_TYPE.value) - print(f"Full solve time: {time.monotonic() - start}s") - start = time.monotonic() - benders(instance, _BENDERS_PRECISSION.value, _SOLVER_TYPE.value) - print(f"Benders solve time: {time.monotonic() - start}s") + del argv # Unused. + instance = FacilityLocationInstance( + network=Network( + _NUM_FACILITIES.value, _NUM_LOCATIONS.value, _EDGE_PROBABILITY.value + ), + location_demand=_LOCATION_DEMAND.value, + facility_cost=_FACILITY_COST.value, + location_fraction=_LOCATION_FRACTION.value, + ) + start = time.monotonic() + full_problem(instance, _SOLVER_TYPE.value) + print(f"Full solve time: {time.monotonic() - start}s") + start = time.monotonic() + benders(instance, _BENDERS_PRECISSION.value, _SOLVER_TYPE.value) + print(f"Benders solve time: {time.monotonic() - start}s") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/hierarchical_objectives.py b/ortools/math_opt/samples/python/hierarchical_objectives.py index 68ac722936b..124afad29e9 100644 --- a/ortools/math_opt/samples/python/hierarchical_objectives.py +++ b/ortools/math_opt/samples/python/hierarchical_objectives.py @@ -27,45 +27,45 @@ def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - model = mathopt.Model() - # The model is: - # max x + y + 2 z - # s.t. x + y + z <= 1.5 - # x, y in [0, 1] - # z binary - # With secondary objective - # max y - # - # The first problem is solved by any convex combination of: - # (0.5, 0, 1) and (0, 0.5, 1) - # But with the secondary objective, the unique solution is (0, 0.5, 1), with - # a primary objective value of 2.5 and secondary objective value of 0.5. - x = model.add_variable(lb=0, ub=1) - y = model.add_variable(lb=0, ub=1) - z = model.add_binary_variable() - model.add_linear_constraint(x + y + z <= 1.5) - model.maximize(x + y + 2 * z) - aux = model.add_maximization_objective(y, priority=1) + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + model = mathopt.Model() + # The model is: + # max x + y + 2 z + # s.t. x + y + z <= 1.5 + # x, y in [0, 1] + # z binary + # With secondary objective + # max y + # + # The first problem is solved by any convex combination of: + # (0.5, 0, 1) and (0, 0.5, 1) + # But with the secondary objective, the unique solution is (0, 0.5, 1), with + # a primary objective value of 2.5 and secondary objective value of 0.5. + x = model.add_variable(lb=0, ub=1) + y = model.add_variable(lb=0, ub=1) + z = model.add_binary_variable() + model.add_linear_constraint(x + y + z <= 1.5) + model.maximize(x + y + 2 * z) + aux = model.add_maximization_objective(y, priority=1) - result = mathopt.solve( - model, - mathopt.SolverType.GUROBI, - params=mathopt.SolveParameters(enable_output=_ENABLE_OUTPUT.value), + result = mathopt.solve( + model, + mathopt.SolverType.GUROBI, + params=mathopt.SolveParameters(enable_output=_ENABLE_OUTPUT.value), + ) + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise ValueError( + f"Expected an optimal termination, found: {result.termination}" ) - if result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise ValueError( - f"Expected an optimal termination, found: {result.termination}" - ) - print(f"primary objective: {result.objective_value()}") - print(f"x: {result.variable_values(x)}") - print(f"y: {result.variable_values(y)}") - print(f"z: {result.variable_values(z)}") - prim_sol = result.solutions[0].primal_solution - assert prim_sol is not None - print(f"secondary objective: {prim_sol.auxiliary_objective_values[aux]}") + print(f"primary objective: {result.objective_value()}") + print(f"x: {result.variable_values(x)}") + print(f"y: {result.variable_values(y)}") + print(f"z: {result.variable_values(z)}") + prim_sol = result.solutions[0].primal_solution + assert prim_sol is not None + print(f"secondary objective: {prim_sol.auxiliary_objective_values[aux]}") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/integer_programming.py b/ortools/math_opt/samples/python/integer_programming.py index fbe252e376b..dcd07d0e719 100644 --- a/ortools/math_opt/samples/python/integer_programming.py +++ b/ortools/math_opt/samples/python/integer_programming.py @@ -29,39 +29,39 @@ # y in {0.0, 1.0, 2.0, ..., # def main(argv: Sequence[str]) -> None: - del argv # Unused. + del argv # Unused. - model = mathopt.Model(name="Linear programming example") + model = mathopt.Model(name="Linear programming example") - # Variables - x = model.add_integer_variable(lb=0.0, name="x") - y = model.add_integer_variable(lb=0.0, name="y") + # Variables + x = model.add_integer_variable(lb=0.0, name="x") + y = model.add_integer_variable(lb=0.0, name="y") - # Constraints - model.add_linear_constraint(x + 7 * y <= 17.5, name="c1") - model.add_linear_constraint(x <= 3.5, name="c2") + # Constraints + model.add_linear_constraint(x + 7 * y <= 17.5, name="c1") + model.add_linear_constraint(x <= 3.5, name="c2") - # Objective - model.maximize(x + 10 * y) + # Objective + model.maximize(x + 10 * y) - # May raise a RuntimeError on invalid input or internal solver errors. - result = mathopt.solve(model, mathopt.SolverType.GSCIP) + # May raise a RuntimeError on invalid input or internal solver errors. + result = mathopt.solve(model, mathopt.SolverType.GSCIP) - # A feasible solution is always available on termination reason kOptimal, - # and kFeasible, but in the later case the solution may be sub-optimal. - if result.termination.reason not in ( - mathopt.TerminationReason.OPTIMAL, - mathopt.TerminationReason.FEASIBLE, - ): - raise RuntimeError(f"model failed to solve: {result.termination}") + # A feasible solution is always available on termination reason kOptimal, + # and kFeasible, but in the later case the solution may be sub-optimal. + if result.termination.reason not in ( + mathopt.TerminationReason.OPTIMAL, + mathopt.TerminationReason.FEASIBLE, + ): + raise RuntimeError(f"model failed to solve: {result.termination}") - print(f"Problem solved in {result.solve_time()}") - print(f"Objective value: {result.objective_value()}") - print( - f"Variable values: [x={round(result.variable_values()[x])}, " - f"y={round(result.variable_values()[y])}]" - ) + print(f"Problem solved in {result.solve_time()}") + print(f"Objective value: {result.objective_value()}") + print( + f"Variable values: [x={round(result.variable_values()[x])}, " + f"y={round(result.variable_values()[y])}]" + ) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/linear_programming.py b/ortools/math_opt/samples/python/linear_programming.py index c75ed6fcef6..11b7801da2e 100644 --- a/ortools/math_opt/samples/python/linear_programming.py +++ b/ortools/math_opt/samples/python/linear_programming.py @@ -31,32 +31,34 @@ # x2 in [0, infinity) # def main(argv: Sequence[str]) -> None: - del argv # Unused. + del argv # Unused. - model = mathopt.Model(name="Linear programming example") + model = mathopt.Model(name="Linear programming example") - # Variables - x = [model.add_variable(lb=0.0, name=f"x{j}") for j in range(3)] + # Variables + x = [model.add_variable(lb=0.0, name=f"x{j}") for j in range(3)] - # Constraints - model.add_linear_constraint(10 * x[0] + 4 * x[1] + 5 * x[2] <= 600, name="c1") - model.add_linear_constraint(2 * x[0] + 2 * x[1] + 6 * x[2] <= 300, name="c2") - model.add_linear_constraint(sum(x) <= 100, name="c3") + # Constraints + model.add_linear_constraint(10 * x[0] + 4 * x[1] + 5 * x[2] <= 600, name="c1") + model.add_linear_constraint(2 * x[0] + 2 * x[1] + 6 * x[2] <= 300, name="c2") + model.add_linear_constraint(sum(x) <= 100, name="c3") - # Objective - model.maximize(10 * x[0] + 6 * x[1] + 4 * x[2]) + # Objective + model.maximize(10 * x[0] + 6 * x[1] + 4 * x[2]) - # May raise a RuntimeError on invalid input or internal solver errors. - result = mathopt.solve(model, mathopt.SolverType.GLOP) + # May raise a RuntimeError on invalid input or internal solver errors. + result = mathopt.solve(model, mathopt.SolverType.GLOP) - if result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise RuntimeError(f"model failed to solve to optimality: {result.termination}") + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise RuntimeError( + f"model failed to solve to optimality: {result.termination}" + ) - print(f"Problem solved in {result.solve_time()}") - print(f"Objective value: {result.objective_value()}") - variable_values = [result.variable_values()[v] for v in x] - print(f"Variable values: {variable_values}") + print(f"Problem solved in {result.solve_time()}") + print(f"Objective value: {result.objective_value()}") + variable_values = [result.variable_values()[v] for v in x] + print(f"Variable values: {variable_values}") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/linear_regression.py b/ortools/math_opt/samples/python/linear_regression.py index 78560a1edfa..34de1f41506 100644 --- a/ortools/math_opt/samples/python/linear_regression.py +++ b/ortools/math_opt/samples/python/linear_regression.py @@ -56,7 +56,8 @@ "solver_type", mathopt.SolverType.PDLP, mathopt.SolverType, - "The solver needs to support quadratic objectives, e.g. pdlp, gurobi, or " "osqp.", + "The solver needs to support quadratic objectives, e.g. pdlp, gurobi, or " + "osqp.", ) _NUM_FEATURES = flags.DEFINE_integer( @@ -76,8 +77,8 @@ @dataclasses.dataclass class LabeledData: - xs: np.ndarray - ys: np.ndarray + xs: np.ndarray + ys: np.ndarray def random_data( @@ -86,74 +87,76 @@ def random_data( noise_stddev: float, rng: np.random._generator.Generator, ) -> LabeledData: - """Creates randomly perturbed labeled data from a ground truth beta.""" - num_features = betas.shape[0] - xs = rng.standard_normal(size=(num_examples, num_features)) - ys = xs @ betas + rng.normal(0, noise_stddev, size=(num_examples)) - return LabeledData(xs=xs, ys=ys) + """Creates randomly perturbed labeled data from a ground truth beta.""" + num_features = betas.shape[0] + xs = rng.standard_normal(size=(num_examples, num_features)) + ys = xs @ betas + rng.normal(0, noise_stddev, size=(num_examples)) + return LabeledData(xs=xs, ys=ys) def l2_loss(betas: np.ndarray, labeled_data: LabeledData) -> float: - """Computes the average squared error between model(labeled_data.xs) and labeled_data.y.""" - num_examples = labeled_data.xs.shape[0] - if num_examples == 0: - return 0 - residuals = labeled_data.xs @ betas - labeled_data.ys - return np.inner(residuals, residuals) / num_examples - - -def train(labeled_data: LabeledData, solver_type: mathopt.SolverType) -> np.ndarray: - """Returns minimum L2Loss beta on labeled_data by solving a quadratic optimization problem.""" - num_examples, num_features = labeled_data.xs.shape - - model = mathopt.Model(name="linear_regression") - - # Create the decision variables: beta, and z. - betas = [model.add_variable(name=f"beta_{j}") for j in range(num_features)] - zs = [model.add_variable(name=f"z_{i}") for i in range(num_examples)] - - # Set the objective function: - model.minimize(sum(z * z for z in zs)) - - # Add the constraints: - # z_i = y_i - x_i * beta - for i in range(num_examples): - model.add_linear_constraint( - zs[i] - == labeled_data.ys[i] - - sum(betas[j] * labeled_data.xs[i, j] for j in range(num_features)) - ) - - # Done building the model, now solve. - result = mathopt.solve( - model, solver_type, params=mathopt.SolveParameters(enable_output=True) + """Computes the average squared error between model(labeled_data.xs) and labeled_data.y.""" + num_examples = labeled_data.xs.shape[0] + if num_examples == 0: + return 0 + residuals = labeled_data.xs @ betas - labeled_data.ys + return np.inner(residuals, residuals) / num_examples + + +def train( + labeled_data: LabeledData, solver_type: mathopt.SolverType +) -> np.ndarray: + """Returns minimum L2Loss beta on labeled_data by solving a quadratic optimization problem.""" + num_examples, num_features = labeled_data.xs.shape + + model = mathopt.Model(name="linear_regression") + + # Create the decision variables: beta, and z. + betas = [model.add_variable(name=f"beta_{j}") for j in range(num_features)] + zs = [model.add_variable(name=f"z_{i}") for i in range(num_examples)] + + # Set the objective function: + model.minimize(sum(z * z for z in zs)) + + # Add the constraints: + # z_i = y_i - x_i * beta + for i in range(num_examples): + model.add_linear_constraint( + zs[i] + == labeled_data.ys[i] + - sum(betas[j] * labeled_data.xs[i, j] for j in range(num_features)) ) - if result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise RuntimeError( - "Expected termination reason optimal, but termination was: " - f"{result.termination}" - ) - print(f"Training time: {result.solve_time()}") - return np.array(result.variable_values(betas)) + + # Done building the model, now solve. + result = mathopt.solve( + model, solver_type, params=mathopt.SolveParameters(enable_output=True) + ) + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise RuntimeError( + "Expected termination reason optimal, but termination was: " + f"{result.termination}" + ) + print(f"Training time: {result.solve_time()}") + return np.array(result.variable_values(betas)) def main(argv: Sequence[str]) -> None: - del argv # Unused. + del argv # Unused. - num_features = _NUM_FEATURES.value - num_examples = _NUM_EXAMPLES.value - noise_stddev = _NOISE.value + num_features = _NUM_FEATURES.value + num_examples = _NUM_EXAMPLES.value + noise_stddev = _NOISE.value - rng = np.random.default_rng(123) + rng = np.random.default_rng(123) - ground_truth_betas = rng.standard_normal(size=(num_features)) - train_data = random_data(ground_truth_betas, num_examples, noise_stddev, rng) - test_data = random_data(ground_truth_betas, num_examples, noise_stddev, rng) + ground_truth_betas = rng.standard_normal(size=(num_features)) + train_data = random_data(ground_truth_betas, num_examples, noise_stddev, rng) + test_data = random_data(ground_truth_betas, num_examples, noise_stddev, rng) - learned_beta = train(train_data, _SOLVER_TYPE.value) - print(f"In sample loss: {l2_loss(learned_beta, train_data)}") - print(f"Out of sample loss: {l2_loss(learned_beta, test_data)}") + learned_beta = train(train_data, _SOLVER_TYPE.value) + print(f"In sample loss: {l2_loss(learned_beta, train_data)}") + print(f"Out of sample loss: {l2_loss(learned_beta, test_data)}") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/remote_solve.py b/ortools/math_opt/samples/python/remote_solve.py index a3c2261f913..48f2a816041 100644 --- a/ortools/math_opt/samples/python/remote_solve.py +++ b/ortools/math_opt/samples/python/remote_solve.py @@ -40,43 +40,43 @@ def main(argv: Sequence[str]) -> None: - del argv # Unused. + del argv # Unused. - model = mathopt.Model(name="my_model") - x = model.add_variable(lb=0.0, ub=1.0, is_integer=_INTEGER.value, name="x") - y = model.add_variable(lb=0.0, ub=1.0, is_integer=_INTEGER.value, name="y") - model.add_linear_constraint(x + y <= 1.0, name="c") - model.maximize(2 * x + 3 * y) + model = mathopt.Model(name="my_model") + x = model.add_variable(lb=0.0, ub=1.0, is_integer=_INTEGER.value, name="x") + y = model.add_variable(lb=0.0, ub=1.0, is_integer=_INTEGER.value, name="y") + model.add_linear_constraint(x + y <= 1.0, name="c") + model.maximize(2 * x + 3 * y) - stub = None - if _MODE.value in ( - remote_solve.RemoteSolveMode.DEFAULT, - remote_solve.RemoteSolveMode.STREAMING, - ): - stub = solve_service_stubby_client.solve_server_stub() + stub = None + if _MODE.value in ( + remote_solve.RemoteSolveMode.DEFAULT, + remote_solve.RemoteSolveMode.STREAMING, + ): + stub = solve_service_stubby_client.solve_server_stub() - msg_cb = None - if _LOGS.value: - msg_cb = mathopt.printer_message_callback(prefix="Solver log: ") - # Raises exceptions on invalid input, internal solver error, rpc timeout etc. - result = remote_solve.remote_solve( - stub, - model, - _SOLVER.value, - deadline=datetime.timedelta(minutes=1), - mode=_MODE.value, - msg_cb=msg_cb, - ) + msg_cb = None + if _LOGS.value: + msg_cb = mathopt.printer_message_callback(prefix="Solver log: ") + # Raises exceptions on invalid input, internal solver error, rpc timeout etc. + result = remote_solve.remote_solve( + stub, + model, + _SOLVER.value, + deadline=datetime.timedelta(minutes=1), + mode=_MODE.value, + msg_cb=msg_cb, + ) - if result.termination.reason not in ( - mathopt.TerminationReason.OPTIMAL, - mathopt.TerminationReason.FEASIBLE, - ): - raise RuntimeError(f"model failed to solve: {result.termination}") + if result.termination.reason not in ( + mathopt.TerminationReason.OPTIMAL, + mathopt.TerminationReason.FEASIBLE, + ): + raise RuntimeError(f"model failed to solve: {result.termination}") - print(f"Objective value: {result.objective_value()}") - print(f"Value for variable x: {result.variable_values()[x]}") + print(f"Objective value: {result.objective_value()}") + print(f"Value for variable x: {result.variable_values()[x]}") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/smallest_circle.py b/ortools/math_opt/samples/python/smallest_circle.py index 61530260824..31a2075ed01 100644 --- a/ortools/math_opt/samples/python/smallest_circle.py +++ b/ortools/math_opt/samples/python/smallest_circle.py @@ -72,33 +72,33 @@ def main(argv: Sequence[str]) -> None: - if len(argv) > 1: - raise app.UsageError("Too many command-line arguments.") - n = _NUM_POINTS.value - rng = np.random.default_rng() - points = rng.random(size=(n, 2)) - print(f"points:\n{points}") - - model = mathopt.Model() - x = model.add_variable(name="x") - y = model.add_variable(name="y") - z = model.add_variable(name="z") - h = [model.add_variable(name=f"h_{i}") for i in range(n)] - v = [model.add_variable(name=f"v_{i}") for i in range(n)] - for i in range(n): - model.add_linear_constraint(h[i] == x - points[i, 0]) - model.add_linear_constraint(v[i] == y - points[i, 1]) - model.add_quadratic_constraint(h[i] * h[i] + v[i] * v[i] <= z) - model.minimize(z) - params = mathopt.SolveParameters(enable_output=True) - result = mathopt.solve(model, _SOLVER_TYPE.value, params=params) - if result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise ValueError("Expected Optimal Solution") - print(f"circle center x: {result.variable_values(x)}") - print(f"circle center y: {result.variable_values(y)}") - radius = math.sqrt(result.variable_values(z)) - print(f"circle radius: {radius}") + if len(argv) > 1: + raise app.UsageError("Too many command-line arguments.") + n = _NUM_POINTS.value + rng = np.random.default_rng() + points = rng.random(size=(n, 2)) + print(f"points:\n{points}") + + model = mathopt.Model() + x = model.add_variable(name="x") + y = model.add_variable(name="y") + z = model.add_variable(name="z") + h = [model.add_variable(name=f"h_{i}") for i in range(n)] + v = [model.add_variable(name=f"v_{i}") for i in range(n)] + for i in range(n): + model.add_linear_constraint(h[i] == x - points[i, 0]) + model.add_linear_constraint(v[i] == y - points[i, 1]) + model.add_quadratic_constraint(h[i] * h[i] + v[i] * v[i] <= z) + model.minimize(z) + params = mathopt.SolveParameters(enable_output=True) + result = mathopt.solve(model, _SOLVER_TYPE.value, params=params) + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise ValueError("Expected Optimal Solution") + print(f"circle center x: {result.variable_values(x)}") + print(f"circle center y: {result.variable_values(y)}") + radius = math.sqrt(result.variable_values(z)) + print(f"circle radius: {radius}") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/time_indexed_scheduling.py b/ortools/math_opt/samples/python/time_indexed_scheduling.py index ad3f91bebe7..3b4b6ee0069 100644 --- a/ortools/math_opt/samples/python/time_indexed_scheduling.py +++ b/ortools/math_opt/samples/python/time_indexed_scheduling.py @@ -71,33 +71,33 @@ @dataclasses.dataclass(frozen=True) class Jobs: - """Data for jobs in a time-indexed scheduling problem. + """Data for jobs in a time-indexed scheduling problem. - Attributes: - processing_times: The duration of each job. - release_times: The earliest time at which each job can begin. - """ + Attributes: + processing_times: The duration of each job. + release_times: The earliest time at which each job can begin. + """ - processing_times: Tuple[int, ...] - release_times: Tuple[int, ...] + processing_times: Tuple[int, ...] + release_times: Tuple[int, ...] def random_jobs(num_jobs: int) -> Jobs: - """Generates a random set of jobs to be scheduled.""" - # Processing times are uniform in [1, processing_time_ub]. - processing_time_ub = 20 + """Generates a random set of jobs to be scheduled.""" + # Processing times are uniform in [1, processing_time_ub]. + processing_time_ub = 20 - # Release times are uniform in [0, release_time_ub]. - release_time_ub = num_jobs * processing_time_ub // 2 + # Release times are uniform in [0, release_time_ub]. + release_time_ub = num_jobs * processing_time_ub // 2 - processing_times: Tuple[int, ...] = tuple( - random.randrange(1, processing_time_ub) for _ in range(num_jobs) - ) - release_times: Tuple[int, ...] = tuple( - random.randrange(1, release_time_ub) for _ in range(num_jobs) - ) + processing_times: Tuple[int, ...] = tuple( + random.randrange(1, processing_time_ub) for _ in range(num_jobs) + ) + release_times: Tuple[int, ...] = tuple( + random.randrange(1, release_time_ub) for _ in range(num_jobs) + ) - return Jobs(processing_times=processing_times, release_times=release_times) + return Jobs(processing_times=processing_times, release_times=release_times) # A small instance for testing. The optimal solution is to run: @@ -112,131 +112,131 @@ def random_jobs(num_jobs: int) -> Jobs: # Job 0 at time 6 # This gives a sum of completion times of 5 + 6 + 16 = 27. def _test_instance() -> Jobs: - return Jobs(processing_times=(10, 1, 5), release_times=(0, 1, 0)) + return Jobs(processing_times=(10, 1, 5), release_times=(0, 1, 0)) def time_horizon(jobs: Jobs) -> int: - """Computes the time horizon of the problem.""" - max_release = max(jobs.release_times, default=0) - sum_processing = sum(jobs.processing_times) - return max_release + sum_processing + """Computes the time horizon of the problem.""" + max_release = max(jobs.release_times, default=0) + sum_processing = sum(jobs.processing_times) + return max_release + sum_processing @dataclasses.dataclass(frozen=True) class Schedule: - """The solution to a time-indexed scheduling problem. + """The solution to a time-indexed scheduling problem. - Attributes: - start_times: The time at which each job begins. - sum_of_completion_times: The sum of times at which jobs complete. - """ + Attributes: + start_times: The time at which each job begins. + sum_of_completion_times: The sum of times at which jobs complete. + """ - start_times: Tuple[int, ...] = () - sum_of_completion_times: int = 0 + start_times: Tuple[int, ...] = () + sum_of_completion_times: int = 0 def solve(jobs: Jobs, solver_type: mathopt.SolverType) -> Schedule: - """Solves a time indexed scheduling problem, returning the best schedule. + """Solves a time indexed scheduling problem, returning the best schedule. - Args: - jobs: The jobs to be scheduled, each with processing and release times. - solver_type: The IP solver used to solve the problem. + Args: + jobs: The jobs to be scheduled, each with processing and release times. + solver_type: The IP solver used to solve the problem. - Returns: - The schedule of jobs that minimizes the sum of completion times. + Returns: + The schedule of jobs that minimizes the sum of completion times. - Raises: - RuntimeError: On solve errors. - """ - processing_times = jobs.processing_times - release_times = jobs.release_times - assert len(processing_times) == len(release_times) - num_jobs = len(processing_times) + Raises: + RuntimeError: On solve errors. + """ + processing_times = jobs.processing_times + release_times = jobs.release_times + assert len(processing_times) == len(release_times) + num_jobs = len(processing_times) - horizon = time_horizon(jobs) - model = mathopt.Model(name="time_indexed_scheduling") + horizon = time_horizon(jobs) + model = mathopt.Model(name="time_indexed_scheduling") - sum_completion_times = 0 + sum_completion_times = 0 - # x[i][t] == 1 indicates that we start job i at time t. - x = [[] for i in range(num_jobs)] + # x[i][t] == 1 indicates that we start job i at time t. + x = [[] for i in range(num_jobs)] - for i in range(num_jobs): - for t in range(horizon): - v = model.add_binary_variable(name=f"x_{i}_{t}") - completion_time = t + processing_times[i] - sum_completion_times += completion_time * v - if t < release_times[i]: - v.upper_bound = 0 - x[i].append(v) + for i in range(num_jobs): + for t in range(horizon): + v = model.add_binary_variable(name=f"x_{i}_{t}") + completion_time = t + processing_times[i] + sum_completion_times += completion_time * v + if t < release_times[i]: + v.upper_bound = 0 + x[i].append(v) - # Pick one time to run job i. - model.add_linear_constraint(sum(x[i]) == 1) + # Pick one time to run job i. + model.add_linear_constraint(sum(x[i]) == 1) - model.minimize(sum_completion_times) + model.minimize(sum_completion_times) - # Run at most one job at a time. - for t in range(horizon): - conflicts = 0 - for i in range(num_jobs): - for s in range(max(0, t - processing_times[i] + 1), t + 1): - conflicts += x[i][s] - model.add_linear_constraint(conflicts <= 1) + # Run at most one job at a time. + for t in range(horizon): + conflicts = 0 + for i in range(num_jobs): + for s in range(max(0, t - processing_times[i] + 1), t + 1): + conflicts += x[i][s] + model.add_linear_constraint(conflicts <= 1) - result = mathopt.solve(model, solver_type) + result = mathopt.solve(model, solver_type) - if result.termination.reason != mathopt.TerminationReason.OPTIMAL: - raise RuntimeError( - "Failed to solve time-indexed scheduling problem to " - f" optimality: {result.termination}" - ) + if result.termination.reason != mathopt.TerminationReason.OPTIMAL: + raise RuntimeError( + "Failed to solve time-indexed scheduling problem to " + f" optimality: {result.termination}" + ) - start_times = [] + start_times = [] - # Add the start times for the jobs. - for i in range(num_jobs): - for t in range(horizon): - var_value = result.variable_values(x[i][t]) - if var_value > 0.5: - start_times.append(t) - break + # Add the start times for the jobs. + for i in range(num_jobs): + for t in range(horizon): + var_value = result.variable_values(x[i][t]) + if var_value > 0.5: + start_times.append(t) + break - return Schedule(tuple(start_times), int(round(result.objective_value()))) + return Schedule(tuple(start_times), int(round(result.objective_value()))) def print_schedule(jobs: Jobs, schedule: Schedule) -> None: - """Displays the schedule, one job per line.""" - processing_times = jobs.processing_times - release_times = jobs.release_times - num_jobs = len(processing_times) - start_times = schedule.start_times - - print("Sum of completion times:", schedule.sum_of_completion_times) - jobs_by_start_time = [] - - for i in range(num_jobs): - jobs_by_start_time.append( - (start_times[i], processing_times[i], release_times[i]) - ) + """Displays the schedule, one job per line.""" + processing_times = jobs.processing_times + release_times = jobs.release_times + num_jobs = len(processing_times) + start_times = schedule.start_times + + print("Sum of completion times:", schedule.sum_of_completion_times) + jobs_by_start_time = [] + + for i in range(num_jobs): + jobs_by_start_time.append( + (start_times[i], processing_times[i], release_times[i]) + ) - jobs_by_start_time.sort(key=lambda job: job[0]) + jobs_by_start_time.sort(key=lambda job: job[0]) - print("Start time, processing time, release time:") - for job in jobs_by_start_time: - print(job[0], job[1], job[2]) + print("Start time, processing time, release time:") + for job in jobs_by_start_time: + print(job[0], job[1], job[2]) def main(argv: Sequence[str]) -> None: - del argv # Unused. - if _USE_TEST_DATA.value: - jobs = _test_instance() - schedule = solve(jobs, _SOLVER_TYPE.value) - else: - jobs = random_jobs(_NUM_JOBS.value) - schedule = solve(jobs, _SOLVER_TYPE.value) + del argv # Unused. + if _USE_TEST_DATA.value: + jobs = _test_instance() + schedule = solve(jobs, _SOLVER_TYPE.value) + else: + jobs = random_jobs(_NUM_JOBS.value) + schedule = solve(jobs, _SOLVER_TYPE.value) - print_schedule(jobs, schedule) + print_schedule(jobs, schedule) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/tsp.py b/ortools/math_opt/samples/python/tsp.py index b212029450c..87b7a413591 100644 --- a/ortools/math_opt/samples/python/tsp.py +++ b/ortools/math_opt/samples/python/tsp.py @@ -63,8 +63,12 @@ from ortools.math_opt.python import mathopt -_NUM_CITIES = flags.DEFINE_integer("num_cities", 100, "The size of the TSP instance.") -_OUTPUT = flags.DEFINE_string("output", "", "Where to write an output SVG, if nonempty") +_NUM_CITIES = flags.DEFINE_integer( + "num_cities", 100, "The size of the TSP instance." +) +_OUTPUT = flags.DEFINE_string( + "output", "", "Where to write an output SVG, if nonempty" +) _TEST_INSTANCE = flags.DEFINE_boolean( "test_instance", False, @@ -78,178 +82,182 @@ def _random_cities(num_cities: int) -> Cities: - """Returns a list random entries distributed U[0,1]^2 i.i.d.""" - return [(random.random(), random.random()) for _ in range(num_cities)] + """Returns a list random entries distributed U[0,1]^2 i.i.d.""" + return [(random.random(), random.random()) for _ in range(num_cities)] def _test_instance() -> Cities: - return [ - (0.0, 0.0), - (0.1, 0.0), - (0.0, 0.1), - (0.1, 0.1), - (0.0, 0.9), - (0.1, 0.9), - (0.0, 1.0), - (0.1, 1.0), - ] + return [ + (0.0, 0.0), + (0.1, 0.0), + (0.0, 0.1), + (0.1, 0.1), + (0.0, 0.9), + (0.1, 0.9), + (0.0, 1.0), + (0.1, 1.0), + ] def _distance_matrix(cities: Cities) -> List[List[float]]: - """Converts a list of (x,y) pairs into a a matrix of Eucledian distances.""" - n = len(cities) - res = [[0.0] * n for _ in range(n)] - for i in range(n): - for j in range(i + 1, n): - xi, yi = cities[i] - xj, yj = cities[j] - dx = xi - xj - dy = yi - yj - dist = math.sqrt(dx * dx + dy * dy) - res[i][j] = dist - res[j][i] = dist - return res + """Converts a list of (x,y) pairs into a a matrix of Eucledian distances.""" + n = len(cities) + res = [[0.0] * n for _ in range(n)] + for i in range(n): + for j in range(i + 1, n): + xi, yi = cities[i] + xj, yj = cities[j] + dx = xi - xj + dy = yi - yj + dist = math.sqrt(dx * dx + dy * dy) + res[i][j] = dist + res[j][i] = dist + return res def _edge_values( edge_vars: List[List[Optional[mathopt.Variable]]], var_values: Dict[mathopt.Variable, float], ) -> List[List[bool]]: - """Converts edge decision variables into an adjacency matrix.""" - n = len(edge_vars) - res = [[False] * n for _ in range(n)] - for i in range(n): - for j in range(n): - if i != j: - res[i][j] = var_values[edge_vars[i][j]] > 0.5 - return res + """Converts edge decision variables into an adjacency matrix.""" + n = len(edge_vars) + res = [[False] * n for _ in range(n)] + for i in range(n): + for j in range(n): + if i != j: + res[i][j] = var_values[edge_vars[i][j]] > 0.5 + return res def _find_cycles(edges: List[List[bool]]) -> List[List[int]]: - """Finds the cycle decomposition for a degree two graph as adjacenty matrix.""" - n = len(edges) - cycles = [] - visited = [False] * n - # Algorithm: maintain a "visited" bit for each city indicating if we have - # formed a cycle containing this city. Consider the cities in order. When you - # find an unvisited city, start a new cycle beginning at this city. Then, - # build the cycle by finding an unvisited neighbor until no such neighbor - # exists (every city will have two neighbors, but eventually both will be - # visited). To find the "unvisited neighbor", we simply do a linear scan - # over the cities, checking both the adjacency matrix and the visited bit. - # - # Note that for this algorithm, in each cycle, the city with lowest index - # will be first, and the cycles will be sorted by their city of lowest index. - # This is an implementation detail and should not be relied upon. - for i in range(n): - if visited[i]: - continue - cycle = [] - next_city = i - while next_city is not None: - cycle.append(next_city) - visited[next_city] = True - current = next_city - next_city = None - # Scan for an unvisited neighbor. We can start at i+1 since we know that - # everything from i back is visited. - for j in range(i + 1, n): - if (not visited[j]) and edges[current][j]: - next_city = j - break - cycles.append(cycle) - return cycles + """Finds the cycle decomposition for a degree two graph as adjacenty matrix.""" + n = len(edges) + cycles = [] + visited = [False] * n + # Algorithm: maintain a "visited" bit for each city indicating if we have + # formed a cycle containing this city. Consider the cities in order. When you + # find an unvisited city, start a new cycle beginning at this city. Then, + # build the cycle by finding an unvisited neighbor until no such neighbor + # exists (every city will have two neighbors, but eventually both will be + # visited). To find the "unvisited neighbor", we simply do a linear scan + # over the cities, checking both the adjacency matrix and the visited bit. + # + # Note that for this algorithm, in each cycle, the city with lowest index + # will be first, and the cycles will be sorted by their city of lowest index. + # This is an implementation detail and should not be relied upon. + for i in range(n): + if visited[i]: + continue + cycle = [] + next_city = i + while next_city is not None: + cycle.append(next_city) + visited[next_city] = True + current = next_city + next_city = None + # Scan for an unvisited neighbor. We can start at i+1 since we know that + # everything from i back is visited. + for j in range(i + 1, n): + if (not visited[j]) and edges[current][j]: + next_city = j + break + cycles.append(cycle) + return cycles def solve_tsp(cities: Cities) -> List[int]: - """Solves the traveling salesperson problem and returns the best route.""" - n = len(cities) - dist = _distance_matrix(cities) - model = mathopt.Model(name="tsp") - edges = [[None] * n for _ in range(n)] - for i in range(n): - for j in range(i + 1, n): - v = model.add_binary_variable(name=f"x_{i}_{j}") - edges[i][j] = v - edges[j][i] = v - obj = 0 - for i in range(n): - obj += sum(dist[i][j] * edges[i][j] for j in range(i + 1, n)) - model.minimize(obj) - for i in range(n): - model.add_linear_constraint(sum(edges[i][j] for j in range(n) if j != i) == 2.0) - - def cb(cb_data: mathopt.CallbackData) -> mathopt.CallbackResult: - assert cb_data.solution is not None - cycles = _find_cycles(_edge_values(edges, cb_data.solution)) - result = mathopt.CallbackResult() - if len(cycles) > 1: - for cycle in cycles: - cycle_as_set = set(cycle) - not_in_cycle = [i for i in range(n) if i not in cycle_as_set] - result.add_lazy_constraint( - sum( - edges[i][j] for (i, j) in itertools.product(cycle, not_in_cycle) - ) - >= 2.0 - ) - return result - - result = mathopt.solve( - model, - mathopt.SolverType.GUROBI, - params=mathopt.SolveParameters(enable_output=_SOLVE_LOGS.value), - callback_reg=mathopt.CallbackRegistration( - events={mathopt.Event.MIP_SOLUTION}, add_lazy_constraints=True - ), - cb=cb, - ) - assert ( - result.termination.reason == mathopt.TerminationReason.OPTIMAL - ), result.termination - assert result.solutions[0].primal_solution is not None - print(f"Route length: {result.solutions[0].primal_solution.objective_value}") - cycles = _find_cycles( - _edge_values(edges, result.solutions[0].primal_solution.variable_values) + """Solves the traveling salesperson problem and returns the best route.""" + n = len(cities) + dist = _distance_matrix(cities) + model = mathopt.Model(name="tsp") + edges = [[None] * n for _ in range(n)] + for i in range(n): + for j in range(i + 1, n): + v = model.add_binary_variable(name=f"x_{i}_{j}") + edges[i][j] = v + edges[j][i] = v + obj = 0 + for i in range(n): + obj += sum(dist[i][j] * edges[i][j] for j in range(i + 1, n)) + model.minimize(obj) + for i in range(n): + model.add_linear_constraint( + sum(edges[i][j] for j in range(n) if j != i) == 2.0 ) - assert len(cycles) == 1, len(cycles) - route = cycles[0] - assert len(route) == n, (len(route), n) - return route + + def cb(cb_data: mathopt.CallbackData) -> mathopt.CallbackResult: + assert cb_data.solution is not None + cycles = _find_cycles(_edge_values(edges, cb_data.solution)) + result = mathopt.CallbackResult() + if len(cycles) > 1: + for cycle in cycles: + cycle_as_set = set(cycle) + not_in_cycle = [i for i in range(n) if i not in cycle_as_set] + result.add_lazy_constraint( + sum( + edges[i][j] for (i, j) in itertools.product(cycle, not_in_cycle) + ) + >= 2.0 + ) + return result + + result = mathopt.solve( + model, + mathopt.SolverType.GUROBI, + params=mathopt.SolveParameters(enable_output=_SOLVE_LOGS.value), + callback_reg=mathopt.CallbackRegistration( + events={mathopt.Event.MIP_SOLUTION}, add_lazy_constraints=True + ), + cb=cb, + ) + assert ( + result.termination.reason == mathopt.TerminationReason.OPTIMAL + ), result.termination + assert result.solutions[0].primal_solution is not None + print(f"Route length: {result.solutions[0].primal_solution.objective_value}") + cycles = _find_cycles( + _edge_values(edges, result.solutions[0].primal_solution.variable_values) + ) + assert len(cycles) == 1, len(cycles) + route = cycles[0] + assert len(route) == n, (len(route), n) + return route def route_svg(filename: str, cities: Cities, route: List[int]): - """Draws the route as an SVG and writes to disk (or prints if no filename).""" - resolution = 1000 - r = 5 - drawing = svgwrite.Drawing( - filename=filename, - size=(resolution + 2 * r, resolution + 2 * r), - profile="tiny", - ) - polygon_points = [] - scale = lambda x: int(round(x * resolution)) + r - for city in route: - raw_x, raw_y = cities[city] - c = (scale(raw_x), scale(raw_y)) - polygon_points.append(c) - drawing.add(drawing.circle(center=c, r=r, fill="blue")) - drawing.add(drawing.polygon(points=polygon_points, stroke="blue", fill="none")) - if not filename: - print(drawing.tostring()) - else: - drawing.save() + """Draws the route as an SVG and writes to disk (or prints if no filename).""" + resolution = 1000 + r = 5 + drawing = svgwrite.Drawing( + filename=filename, + size=(resolution + 2 * r, resolution + 2 * r), + profile="tiny", + ) + polygon_points = [] + scale = lambda x: int(round(x * resolution)) + r + for city in route: + raw_x, raw_y = cities[city] + c = (scale(raw_x), scale(raw_y)) + polygon_points.append(c) + drawing.add(drawing.circle(center=c, r=r, fill="blue")) + drawing.add( + drawing.polygon(points=polygon_points, stroke="blue", fill="none") + ) + if not filename: + print(drawing.tostring()) + else: + drawing.save() def main(args): - del args # Unused. - if _TEST_INSTANCE.value: - cities = _test_instance() - else: - cities = _random_cities(_NUM_CITIES.value) - route = solve_tsp(cities) - route_svg(_OUTPUT.value, cities, route) + del args # Unused. + if _TEST_INSTANCE.value: + cities = _test_instance() + else: + cities = _random_cities(_NUM_CITIES.value) + route = solve_tsp(cities) + route_svg(_OUTPUT.value, cities, route) if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/samples/python/writing_seminar.py b/ortools/math_opt/samples/python/writing_seminar.py index 81dbfa80701..2e47ad49d3e 100644 --- a/ortools/math_opt/samples/python/writing_seminar.py +++ b/ortools/math_opt/samples/python/writing_seminar.py @@ -70,40 +70,42 @@ "lp_relax", False, "Solve the LP relaxation of the problem." ) -_TEST_DATA = flags.DEFINE_boolean("test_data", False, "Use the small test instance.") +_TEST_DATA = flags.DEFINE_boolean( + "test_data", False, "Use the small test instance." +) @dataclasses.dataclass(frozen=True) class Student: - preferred_classes: Tuple[int, ...] - name: str = "" + preferred_classes: Tuple[int, ...] + name: str = "" @dataclasses.dataclass(frozen=True) class Seminar: - capacity: int - name: str = "" + capacity: int + name: str = "" @dataclasses.dataclass(frozen=True) class WritingSeminarAssignmentProblem: - seminars: Tuple[Seminar, ...] - students: Tuple[Student, ...] - rank_penalty: Tuple[float, ...] - unmatched_penalty: float + seminars: Tuple[Seminar, ...] + students: Tuple[Student, ...] + rank_penalty: Tuple[float, ...] + unmatched_penalty: float def _test_problem() -> WritingSeminarAssignmentProblem: - """A small deterministic instance for testing only.""" - return WritingSeminarAssignmentProblem( - seminars=(Seminar(capacity=1, name="c1"), Seminar(capacity=1, name="c2")), - students=( - Student(preferred_classes=(1, 0), name="s1"), - Student(preferred_classes=(0, 1), name="s2"), - ), - rank_penalty=(0, 10), - unmatched_penalty=100, - ) + """A small deterministic instance for testing only.""" + return WritingSeminarAssignmentProblem( + seminars=(Seminar(capacity=1, name="c1"), Seminar(capacity=1, name="c2")), + students=( + Student(preferred_classes=(1, 0), name="s1"), + Student(preferred_classes=(0, 1), name="s2"), + ), + rank_penalty=(0, 10), + unmatched_penalty=100, + ) def _random_writing_seminar_assignment_problem( @@ -115,26 +117,27 @@ def _random_writing_seminar_assignment_problem( unmatched_penalty: float, rank_penalty: Tuple[float, ...], ) -> WritingSeminarAssignmentProblem: - """Generates a random instance of the WritingSeminarAssignmentProblem.""" - if len(rank_penalty) != selections_per_student: - raise ValueError( - f"len(rank_penalty): {len(rank_penalty)} must equal" - f" selections_per_student: {selections_per_student}" - ) - seminars = tuple( - Seminar(capacity=class_capacity, name=f"c_{i}") for i in range(num_classes) - ) - students = [] - all_class_ids = list(range(num_classes)) - for s in range(num_students): - preferred = tuple(random.sample(all_class_ids, selections_per_student)) - students.append(Student(preferred_classes=preferred, name=f"s_{s}")) - return WritingSeminarAssignmentProblem( - seminars=seminars, - students=tuple(students), - rank_penalty=rank_penalty, - unmatched_penalty=unmatched_penalty, + """Generates a random instance of the WritingSeminarAssignmentProblem.""" + if len(rank_penalty) != selections_per_student: + raise ValueError( + f"len(rank_penalty): {len(rank_penalty)} must equal" + f" selections_per_student: {selections_per_student}" ) + seminars = tuple( + Seminar(capacity=class_capacity, name=f"c_{i}") + for i in range(num_classes) + ) + students = [] + all_class_ids = list(range(num_classes)) + for s in range(num_students): + preferred = tuple(random.sample(all_class_ids, selections_per_student)) + students.append(Student(preferred_classes=preferred, name=f"s_{s}")) + return WritingSeminarAssignmentProblem( + seminars=seminars, + students=tuple(students), + rank_penalty=rank_penalty, + unmatched_penalty=unmatched_penalty, + ) def _assign_students( @@ -142,125 +145,125 @@ def _assign_students( solver_type: mathopt.SolverType, time_limit: datetime.timedelta, ) -> Dict[Student, Seminar]: - """Optimally assigns students to classes by solving an IP.""" - # Problem data: - # * i in S: the students. - # * j in C: the classes (writing seminars). - # * c_j: the capacity of class j. - # * K: how many classes each student ranks. - # * R_i for i in S, an ordered list K classes ranked for student i. - # * p_k for k = 1,...,K the penalty for giving a student their kth choice. - # * P: the penalty for not assigning a student. - # - # Decision variables: - # x_i_j: student i takes seminar j - # y_i: student i is not assigned - # - # Problem formulation: - # min sum_i P y_i + sum_{k, j in enumerate(R_i)} p_k x_i_j - # s.t. y_i + sum_{j in R_i} x_i_j = 1 for all i in S - # sum_{i : j in R_i} x_i_j <= c_j for all j in C - # x_i_j in {0, 1} for all i in S, for all j in R_i - # y_i in {0, 1} for all i in S - model = mathopt.Model() - use_int_vars = not _LP_RELAX.value - # The y_i variables. - unassigned = [ - model.add_variable(lb=0, ub=1, is_integer=use_int_vars, name=f"y_{i}") - for i, _ in enumerate(problem.students) - ] - - # The x_i_j variables. - assignment_vars = [{} for _ in range(len(problem.students))] - for i, student in enumerate(problem.students): - for rank, j in enumerate(student.preferred_classes): - assignment_vars[i][j] = model.add_variable( - lb=0, ub=1, is_integer=use_int_vars, name=f"x_{i}_{j}" - ) - # Transpose the variables in x. The first index of students_in_seminar - # is the class (j). The value is an unordered list of the variables for - # assigning students into this class. - students_in_seminar = [[] for _ in range(len(problem.seminars))] - for seminar_to_x in assignment_vars: - for j, x in seminar_to_x.items(): - students_in_seminar[j].append(x) - - # Create the objective - penalties = mathopt.fast_sum(unassigned) * problem.unmatched_penalty - for i, student in enumerate(problem.students): - penalties += mathopt.fast_sum( - problem.rank_penalty[rank] * assignment_vars[i][j] - for rank, j in enumerate(student.preferred_classes) - ) - model.minimize(penalties) + """Optimally assigns students to classes by solving an IP.""" + # Problem data: + # * i in S: the students. + # * j in C: the classes (writing seminars). + # * c_j: the capacity of class j. + # * K: how many classes each student ranks. + # * R_i for i in S, an ordered list K classes ranked for student i. + # * p_k for k = 1,...,K the penalty for giving a student their kth choice. + # * P: the penalty for not assigning a student. + # + # Decision variables: + # x_i_j: student i takes seminar j + # y_i: student i is not assigned + # + # Problem formulation: + # min sum_i P y_i + sum_{k, j in enumerate(R_i)} p_k x_i_j + # s.t. y_i + sum_{j in R_i} x_i_j = 1 for all i in S + # sum_{i : j in R_i} x_i_j <= c_j for all j in C + # x_i_j in {0, 1} for all i in S, for all j in R_i + # y_i in {0, 1} for all i in S + model = mathopt.Model() + use_int_vars = not _LP_RELAX.value + # The y_i variables. + unassigned = [ + model.add_variable(lb=0, ub=1, is_integer=use_int_vars, name=f"y_{i}") + for i, _ in enumerate(problem.students) + ] + + # The x_i_j variables. + assignment_vars = [{} for _ in range(len(problem.students))] + for i, student in enumerate(problem.students): + for rank, j in enumerate(student.preferred_classes): + assignment_vars[i][j] = model.add_variable( + lb=0, ub=1, is_integer=use_int_vars, name=f"x_{i}_{j}" + ) + # Transpose the variables in x. The first index of students_in_seminar + # is the class (j). The value is an unordered list of the variables for + # assigning students into this class. + students_in_seminar = [[] for _ in range(len(problem.seminars))] + for seminar_to_x in assignment_vars: + for j, x in seminar_to_x.items(): + students_in_seminar[j].append(x) + + # Create the objective + penalties = mathopt.fast_sum(unassigned) * problem.unmatched_penalty + for i, student in enumerate(problem.students): + penalties += mathopt.fast_sum( + problem.rank_penalty[rank] * assignment_vars[i][j] + for rank, j in enumerate(student.preferred_classes) + ) + model.minimize(penalties) - # Each student is in at most one class - for i, student in enumerate(problem.students): - model.add_linear_constraint( - unassigned[i] + mathopt.fast_sum(assignment_vars[i].values()) == 1.0 - ) + # Each student is in at most one class + for i, student in enumerate(problem.students): + model.add_linear_constraint( + unassigned[i] + mathopt.fast_sum(assignment_vars[i].values()) == 1.0 + ) - # Each class does not exceed its capacity - for j, seminar in enumerate(problem.seminars): - model.add_linear_constraint( - mathopt.fast_sum(students_in_seminar[j]) <= seminar.capacity - ) + # Each class does not exceed its capacity + for j, seminar in enumerate(problem.seminars): + model.add_linear_constraint( + mathopt.fast_sum(students_in_seminar[j]) <= seminar.capacity + ) - solve_result = mathopt.solve( - model, - solver_type, - params=mathopt.SolveParameters(enable_output=True, time_limit=time_limit), + solve_result = mathopt.solve( + model, + solver_type, + params=mathopt.SolveParameters(enable_output=True, time_limit=time_limit), + ) + if solve_result.termination.reason not in { + mathopt.TerminationReason.OPTIMAL, + mathopt.TerminationReason.FEASIBLE, + }: + raise RuntimeError( + f"failed to find a feasible solution: {solve_result.termination}" ) - if solve_result.termination.reason not in { - mathopt.TerminationReason.OPTIMAL, - mathopt.TerminationReason.FEASIBLE, - }: + + assignment = {} + for i, student in enumerate(problem.students): + for sem, x in assignment_vars[i].items(): + x_val = solve_result.variable_values(x) + int_err = min(abs(x_val), abs(1 - x_val)) + if int_err > 1e-3: raise RuntimeError( - f"failed to find a feasible solution: {solve_result.termination}" + "all variables should be within 1e-3 of either 0 or 1, but found" + f" value: {x_val}" ) - - assignment = {} - for i, student in enumerate(problem.students): - for sem, x in assignment_vars[i].items(): - x_val = solve_result.variable_values(x) - int_err = min(abs(x_val), abs(1 - x_val)) - if int_err > 1e-3: - raise RuntimeError( - "all variables should be within 1e-3 of either 0 or 1, but found" - f" value: {x_val}" - ) - if solve_result.variable_values(x) > 0.5: - assignment[student] = problem.seminars[sem] - return assignment + if solve_result.variable_values(x) > 0.5: + assignment[student] = problem.seminars[sem] + return assignment def main(args: Sequence[str]) -> None: - del args # Unused. - if _TEST_DATA.value: - problem = _test_problem() - else: - num_classes = _NUM_CLASSES.value - class_capacity = 15 - num_students = num_classes * 12 - selections_per_student = 5 - unmatched_penalty = 1000 - rank_penalty = (0, 1, 5, 20, 100) - problem = _random_writing_seminar_assignment_problem( - num_classes=num_classes, - class_capacity=class_capacity, - num_students=num_students, - selections_per_student=selections_per_student, - unmatched_penalty=unmatched_penalty, - rank_penalty=rank_penalty, - ) - assignment = _assign_students( - problem, _SOLVER_TYPE.value, datetime.timedelta(minutes=1) + del args # Unused. + if _TEST_DATA.value: + problem = _test_problem() + else: + num_classes = _NUM_CLASSES.value + class_capacity = 15 + num_students = num_classes * 12 + selections_per_student = 5 + unmatched_penalty = 1000 + rank_penalty = (0, 1, 5, 20, 100) + problem = _random_writing_seminar_assignment_problem( + num_classes=num_classes, + class_capacity=class_capacity, + num_students=num_students, + selections_per_student=selections_per_student, + unmatched_penalty=unmatched_penalty, + rank_penalty=rank_penalty, ) - for student, seminar in assignment.items(): - print(f"{student.name}: {seminar.name}") - num_unassigned = len(problem.students) - len(assignment) - print(f"Unassigned students: {num_unassigned}") + assignment = _assign_students( + problem, _SOLVER_TYPE.value, datetime.timedelta(minutes=1) + ) + for student, seminar in assignment.items(): + print(f"{student.name}: {seminar.name}") + num_unassigned = len(problem.students) - len(assignment) + print(f"Unassigned students: {num_unassigned}") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/math_opt/solvers/BUILD.bazel b/ortools/math_opt/solvers/BUILD.bazel index 1c4a8990949..ec2c9e34b7d 100644 --- a/ortools/math_opt/solvers/BUILD.bazel +++ b/ortools/math_opt/solvers/BUILD.bazel @@ -115,7 +115,6 @@ cc_library( "//ortools/base:logging", "//ortools/base:protoutil", "//ortools/base:status_macros", - "//ortools/gurobi:environment", "//ortools/math_opt:callback_cc_proto", "//ortools/math_opt:solution_cc_proto", "//ortools/math_opt:sparse_containers_cc_proto", @@ -123,6 +122,7 @@ cc_library( "//ortools/math_opt/core:solver_interface", "//ortools/math_opt/core:sparse_vector_view", "//ortools/math_opt/solvers/gurobi:g_gurobi", + "//ortools/third_party_solvers:gurobi_environment", "//ortools/util:solve_interrupter", "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/status", @@ -152,7 +152,6 @@ cc_library( "//ortools/base:map_util", "//ortools/base:protoutil", "//ortools/base:status_macros", - "//ortools/gurobi:environment", "//ortools/gurobi/isv_public:gurobi_isv", "//ortools/math_opt:callback_cc_proto", "//ortools/math_opt:infeasible_subsystem_cc_proto", @@ -173,6 +172,7 @@ cc_library( "//ortools/math_opt/solvers/gurobi:g_gurobi", "//ortools/math_opt/validators:callback_validator", "//ortools/port:proto_utils", + "//ortools/third_party_solvers:gurobi_environment", "//ortools/util:solve_interrupter", "//ortools/util:testing_utils", "@abseil-cpp//absl/algorithm:container", @@ -710,8 +710,8 @@ cc_library( "//ortools/math_opt/solvers/xpress:g_xpress", "//ortools/math_opt/validators:callback_validator", "//ortools/port:proto_utils", + "//ortools/third_party_solvers:xpress_environment", "//ortools/util:solve_interrupter", - "//ortools/xpress:environment", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/memory", "@abseil-cpp//absl/status", @@ -744,7 +744,7 @@ cc_test( "//ortools/math_opt/solver_tests:qp_tests", "//ortools/math_opt/solver_tests:second_order_cone_tests", "//ortools/math_opt/solver_tests:status_tests", - "//ortools/xpress:environment", + "//ortools/third_party_solvers:xpress_environment", "@abseil-cpp//absl/log", ], ) diff --git a/ortools/math_opt/solvers/cp_sat_solver.cc b/ortools/math_opt/solvers/cp_sat_solver.cc index df36fea70ee..436a89661dd 100644 --- a/ortools/math_opt/solvers/cp_sat_solver.cc +++ b/ortools/math_opt/solvers/cp_sat_solver.cc @@ -137,7 +137,7 @@ std::vector SetSolveParameters( sat_parameters.set_random_seed(parameters.random_seed()); } if (parameters.has_threads()) { - sat_parameters.set_num_search_workers(parameters.threads()); + sat_parameters.set_num_workers(parameters.threads()); } if (parameters.has_relative_gap_tolerance()) { sat_parameters.set_relative_gap_limit(parameters.relative_gap_tolerance()); diff --git a/ortools/math_opt/solvers/cp_sat_solver_test.cc b/ortools/math_opt/solvers/cp_sat_solver_test.cc index e1372726c3a..fb91ec639a7 100644 --- a/ortools/math_opt/solvers/cp_sat_solver_test.cc +++ b/ortools/math_opt/solvers/cp_sat_solver_test.cc @@ -225,7 +225,7 @@ SolutionHintTestParams MakeCpsatSolutionHintParams() { solve_params.cuts = Emphasis::kOff; solve_params.presolve = Emphasis::kOff; solve_params.cp_sat.set_stop_after_first_solution(true); - solve_params.cp_sat.set_num_search_workers(1); + solve_params.cp_sat.set_num_workers(1); // Matches "best:", "next:" and "hint" appearing in the same line std::string hint_message_regex = "best:.*next:.*hint"; return SolutionHintTestParams(SolverType::kCpSat, solve_params, std::nullopt, diff --git a/ortools/math_opt/solvers/glpk/rays.cc b/ortools/math_opt/solvers/glpk/rays.cc index 281345b7297..9907907d9fb 100644 --- a/ortools/math_opt/solvers/glpk/rays.cc +++ b/ortools/math_opt/solvers/glpk/rays.cc @@ -14,7 +14,6 @@ #include "ortools/math_opt/solvers/glpk/rays.h" #include -#include #include #include diff --git a/ortools/math_opt/solvers/gscip_solver.cc b/ortools/math_opt/solvers/gscip_solver.cc index 03503fdb219..8b1317374a9 100644 --- a/ortools/math_opt/solvers/gscip_solver.cc +++ b/ortools/math_opt/solvers/gscip_solver.cc @@ -1093,8 +1093,7 @@ absl::StatusOr GScipSolver::Solve( ASSIGN_OR_RETURN( GScipResult gscip_result, - gscip_->Solve(gscip_parameters, - /*legacy_params=*/"", std::move(gscip_msg_cb), + gscip_->Solve(gscip_parameters, std::move(gscip_msg_cb), use_interrupter ? &gscip_interrupter : nullptr)); // Flush the potential last unfinished line. diff --git a/ortools/math_opt/solvers/gurobi/BUILD.bazel b/ortools/math_opt/solvers/gurobi/BUILD.bazel index f70c68d9ab4..5a35646323a 100644 --- a/ortools/math_opt/solvers/gurobi/BUILD.bazel +++ b/ortools/math_opt/solvers/gurobi/BUILD.bazel @@ -29,9 +29,9 @@ cc_library( "//ortools/base:logging", "//ortools/base:source_location", "//ortools/base:status_macros", - "//ortools/gurobi:environment", "//ortools/gurobi/isv_public:gurobi_isv", "//ortools/math_opt/solvers:gurobi_cc_proto", + "//ortools/third_party_solvers:gurobi_environment", "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/log:die_if_null", diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.cc b/ortools/math_opt/solvers/gurobi/g_gurobi.cc index 76eadc1cdb0..128badd522e 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.cc +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.cc @@ -32,9 +32,9 @@ #include "ortools/base/source_location.h" #include "ortools/base/status_builder.h" #include "ortools/base/status_macros.h" -#include "ortools/gurobi/environment.h" #include "ortools/gurobi/isv_public/gurobi_isv.h" #include "ortools/math_opt/solvers/gurobi.pb.h" +#include "ortools/third_party_solvers/gurobi_environment.h" namespace operations_research::math_opt { @@ -47,6 +47,12 @@ struct UserCallbackData { Gurobi* gurobi = nullptr; }; +#if defined(_MSC_VER) +#define GUROBI_STDCALL __stdcall +#else +#define GUROBI_STDCALL +#endif + int GUROBI_STDCALL GurobiCallback(GRBmodel* const model, void* const cbdata, const int where, void* const usrdata) { CHECK(usrdata != nullptr); diff --git a/ortools/math_opt/solvers/gurobi/g_gurobi.h b/ortools/math_opt/solvers/gurobi/g_gurobi.h index ed4e9d222e8..c6bd7c98dfe 100644 --- a/ortools/math_opt/solvers/gurobi/g_gurobi.h +++ b/ortools/math_opt/solvers/gurobi/g_gurobi.h @@ -43,8 +43,8 @@ #include "absl/status/statusor.h" #include "absl/types/span.h" #include "ortools/base/source_location.h" -#include "ortools/gurobi/environment.h" #include "ortools/gurobi/isv_public/gurobi_isv.h" +#include "ortools/third_party_solvers/gurobi_environment.h" namespace operations_research::math_opt { diff --git a/ortools/math_opt/solvers/gurobi_callback.cc b/ortools/math_opt/solvers/gurobi_callback.cc index 50f11f293ff..2e678cb080f 100644 --- a/ortools/math_opt/solvers/gurobi_callback.cc +++ b/ortools/math_opt/solvers/gurobi_callback.cc @@ -14,7 +14,6 @@ #include "ortools/math_opt/solvers/gurobi_callback.h" #include -#include #include #include #include @@ -32,7 +31,6 @@ #include "ortools/base/logging.h" #include "ortools/base/protoutil.h" #include "ortools/base/status_macros.h" -#include "ortools/gurobi/environment.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/math_opt_proto_utils.h" #include "ortools/math_opt/core/solver_interface.h" @@ -40,6 +38,7 @@ #include "ortools/math_opt/solution.pb.h" #include "ortools/math_opt/solvers/message_callback_data.h" #include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/third_party_solvers/gurobi_environment.h" #include "ortools/util/solve_interrupter.h" namespace operations_research { diff --git a/ortools/math_opt/solvers/gurobi_callback.h b/ortools/math_opt/solvers/gurobi_callback.h index 1524f310579..f9223bdc30d 100644 --- a/ortools/math_opt/solvers/gurobi_callback.h +++ b/ortools/math_opt/solvers/gurobi_callback.h @@ -21,12 +21,12 @@ #include "absl/status/status.h" #include "absl/time/time.h" #include "ortools/base/linked_hash_map.h" -#include "ortools/gurobi/environment.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/solver_interface.h" #include "ortools/math_opt/solvers/gurobi/g_gurobi.h" #include "ortools/math_opt/solvers/message_callback_data.h" #include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/third_party_solvers/gurobi_environment.h" #include "ortools/util/solve_interrupter.h" namespace operations_research { diff --git a/ortools/math_opt/solvers/gurobi_solver.cc b/ortools/math_opt/solvers/gurobi_solver.cc index 72c0c713282..9c809f5e3fc 100644 --- a/ortools/math_opt/solvers/gurobi_solver.cc +++ b/ortools/math_opt/solvers/gurobi_solver.cc @@ -31,7 +31,6 @@ #include "absl/log/check.h" #include "absl/log/log.h" #include "absl/memory/memory.h" -#include "absl/meta/type_traits.h" #include "absl/status/status.h" #include "absl/status/statusor.h" #include "absl/strings/escaping.h" @@ -2756,9 +2755,7 @@ absl::StatusOr GurobiSolver::Update( absl::StatusOr> GurobiSolver::New( const ModelProto& input_model, const SolverInterface::InitArgs& init_args) { - if (!GurobiIsCorrectlyInstalled()) { - return absl::InvalidArgumentError("Gurobi is not correctly installed."); - } + // TODO(user): Correctly load the gurobi library in open source. RETURN_IF_ERROR( ModelIsSupported(input_model, kGurobiSupportedStructures, "Gurobi")); if (!input_model.auxiliary_objectives().empty() && diff --git a/ortools/math_opt/solvers/gurobi_solver.h b/ortools/math_opt/solvers/gurobi_solver.h index 7789afe062a..81a8b245584 100644 --- a/ortools/math_opt/solvers/gurobi_solver.h +++ b/ortools/math_opt/solvers/gurobi_solver.h @@ -28,7 +28,6 @@ #include "absl/time/time.h" #include "absl/types/span.h" #include "ortools/base/linked_hash_map.h" -#include "ortools/gurobi/environment.h" #include "ortools/math_opt/callback.pb.h" #include "ortools/math_opt/core/invalid_indicators.h" #include "ortools/math_opt/core/inverted_bounds.h" @@ -44,6 +43,7 @@ #include "ortools/math_opt/solvers/gurobi_callback.h" #include "ortools/math_opt/solvers/message_callback_data.h" #include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/third_party_solvers/gurobi_environment.h" #include "ortools/util/solve_interrupter.h" namespace operations_research { diff --git a/ortools/math_opt/solvers/highs_solver.cc b/ortools/math_opt/solvers/highs_solver.cc index b3c664c97c6..25f66f1e7fa 100644 --- a/ortools/math_opt/solvers/highs_solver.cc +++ b/ortools/math_opt/solvers/highs_solver.cc @@ -544,30 +544,35 @@ absl::StatusOr HighsSolver::MakeTermination( optional_finite_primal_objective, optional_dual_objective); case HighsModelStatus::kIterationLimit: { - if (is_integer) { - if (had_node_limit && had_solution_limit) { - return LimitTerminationProto( - is_maximize, LIMIT_UNDETERMINED, optional_finite_primal_objective, - optional_dual_objective, - "Both node limit and solution limit were requested, cannot " - "determine reason for termination"); - } else if (had_node_limit) { - return LimitTerminationProto(is_maximize, LIMIT_NODE, - optional_finite_primal_objective, - optional_dual_objective); - } else if (had_solution_limit) { - return LimitTerminationProto(is_maximize, LIMIT_SOLUTION, - optional_finite_primal_objective, - optional_dual_objective); - } - } else { - // For LP, only the MathOpt iteration limit can cause highs to return - // HighsModelStatus::kIterationLimit. - return LimitTerminationProto(is_maximize, LIMIT_ITERATION, + return LimitTerminationProto(is_maximize, LIMIT_ITERATION, + optional_finite_primal_objective, + optional_dual_objective); + } + case HighsModelStatus::kSolutionLimit: { + if (had_node_limit && !had_solution_limit) { + return LimitTerminationProto(is_maximize, LIMIT_NODE, optional_finite_primal_objective, optional_dual_objective); + } else if (had_solution_limit && !had_node_limit) { + return LimitTerminationProto(is_maximize, LIMIT_SOLUTION, + optional_finite_primal_objective, + optional_dual_objective); + } else { + return LimitTerminationProto( + is_maximize, LIMIT_UNDETERMINED, optional_finite_primal_objective, + optional_dual_objective, + "HighsModelStatus was kSolutionLimit but cannot infer a MathOpt " + "Limit, could be NODE_LIMIT or SOLUTION_LIMIT"); } } + case HighsModelStatus::kInterrupt: + return LimitTerminationProto(is_maximize, LIMIT_INTERRUPTED, + optional_finite_primal_objective, + optional_dual_objective); + case HighsModelStatus::kMemoryLimit: + return LimitTerminationProto( + is_maximize, LIMIT_OTHER, optional_finite_primal_objective, + optional_dual_objective, "Highs hit kMemoryLimit"); } return util::InternalErrorBuilder() << "HighsModelStatus unimplemented: " << static_cast(highs_model_status); diff --git a/ortools/math_opt/solvers/xpress/BUILD.bazel b/ortools/math_opt/solvers/xpress/BUILD.bazel index 77d89b809ff..b339b3b6e7b 100644 --- a/ortools/math_opt/solvers/xpress/BUILD.bazel +++ b/ortools/math_opt/solvers/xpress/BUILD.bazel @@ -22,7 +22,7 @@ cc_library( "//ortools/base:logging", "//ortools/base:source_location", "//ortools/base:status_macros", - "//ortools/xpress:environment", + "//ortools/third_party_solvers:xpress_environment", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/log:die_if_null", "@abseil-cpp//absl/memory", diff --git a/ortools/math_opt/solvers/xpress/g_xpress.cc b/ortools/math_opt/solvers/xpress/g_xpress.cc index 7f5782bbcf9..e94b674dec2 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.cc +++ b/ortools/math_opt/solvers/xpress/g_xpress.cc @@ -30,7 +30,7 @@ #include "ortools/base/logging.h" #include "ortools/base/status_builder.h" #include "ortools/base/status_macros.h" -#include "ortools/xpress/environment.h" +#include "ortools/third_party_solvers/xpress_environment.h" namespace operations_research::math_opt { diff --git a/ortools/math_opt/solvers/xpress/g_xpress.h b/ortools/math_opt/solvers/xpress/g_xpress.h index 23a6e2f46ca..70f5ba74c68 100644 --- a/ortools/math_opt/solvers/xpress/g_xpress.h +++ b/ortools/math_opt/solvers/xpress/g_xpress.h @@ -34,7 +34,7 @@ #include "absl/status/statusor.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" -#include "ortools/xpress/environment.h" +#include "ortools/third_party_solvers/xpress_environment.h" namespace operations_research::math_opt { diff --git a/ortools/math_opt/solvers/xpress_solver.cc b/ortools/math_opt/solvers/xpress_solver.cc index 5f0a0f413f5..b94b31763d9 100644 --- a/ortools/math_opt/solvers/xpress_solver.cc +++ b/ortools/math_opt/solvers/xpress_solver.cc @@ -40,8 +40,8 @@ #include "ortools/math_opt/solvers/xpress/g_xpress.h" #include "ortools/math_opt/validators/callback_validator.h" #include "ortools/port/proto_utils.h" +#include "ortools/third_party_solvers/xpress_environment.h" #include "ortools/util/solve_interrupter.h" -#include "ortools/xpress/environment.h" namespace operations_research { namespace math_opt { diff --git a/ortools/math_opt/solvers/xpress_solver.h b/ortools/math_opt/solvers/xpress_solver.h index 81c4547ab3f..65c98b456fe 100644 --- a/ortools/math_opt/solvers/xpress_solver.h +++ b/ortools/math_opt/solvers/xpress_solver.h @@ -36,8 +36,8 @@ #include "ortools/math_opt/solution.pb.h" #include "ortools/math_opt/solvers/xpress/g_xpress.h" #include "ortools/math_opt/sparse_containers.pb.h" +#include "ortools/third_party_solvers/xpress_environment.h" #include "ortools/util/solve_interrupter.h" -#include "ortools/xpress/environment.h" namespace operations_research::math_opt { diff --git a/ortools/math_opt/solvers/xpress_solver_test.cc b/ortools/math_opt/solvers/xpress_solver_test.cc index 41dd81f8606..23e4c8d767b 100644 --- a/ortools/math_opt/solvers/xpress_solver_test.cc +++ b/ortools/math_opt/solvers/xpress_solver_test.cc @@ -35,7 +35,7 @@ #include "ortools/math_opt/solver_tests/qp_tests.h" #include "ortools/math_opt/solver_tests/second_order_cone_tests.h" #include "ortools/math_opt/solver_tests/status_tests.h" -#include "ortools/xpress/environment.h" +#include "ortools/third_party_solvers/xpress_environment.h" namespace operations_research { namespace math_opt { diff --git a/ortools/pdlp/BUILD.bazel b/ortools/pdlp/BUILD.bazel index 7a19ebc39a6..0059d6d1223 100644 --- a/ortools/pdlp/BUILD.bazel +++ b/ortools/pdlp/BUILD.bazel @@ -144,6 +144,7 @@ cc_library( "//ortools/lp_data:proto_utils", "//ortools/util:logging", "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/base:nullability", "@abseil-cpp//absl/status", "@abseil-cpp//absl/status:statusor", "@abseil-cpp//absl/strings", diff --git a/ortools/pdlp/primal_dual_hybrid_gradient.cc b/ortools/pdlp/primal_dual_hybrid_gradient.cc index d166ef77001..b86f3e9f1c1 100644 --- a/ortools/pdlp/primal_dual_hybrid_gradient.cc +++ b/ortools/pdlp/primal_dual_hybrid_gradient.cc @@ -53,6 +53,7 @@ #include "Eigen/Core" #include "Eigen/SparseCore" #include "absl/algorithm/container.h" +#include "absl/base/nullability.h" #include "absl/log/check.h" #include "absl/log/log.h" #include "absl/status/status.h" @@ -619,7 +620,8 @@ class Solver { NextSolutionAndDelta ComputeNextDualSolution( double dual_step_size, double extrapolation_factor, - const NextSolutionAndDelta& next_primal) const; + const NextSolutionAndDelta& next_primal_solution, + const VectorXd* absl_nullable next_primal_product = nullptr) const; std::pair ComputeMovementTerms( const VectorXd& delta_primal, const VectorXd& delta_dual) const; @@ -630,6 +632,10 @@ class Solver { double ComputeNonlinearity(const VectorXd& delta_primal, const VectorXd& next_dual_product) const; + // Sets current_primal_product_ and current_dual_product_ based on + // current_primal_solution_ and current_dual_solution_ respectively. + void SetCurrentPrimalAndDualProducts(); + // Creates all the simple-to-compute statistics in stats. IterationStats CreateSimpleIterationStats(RestartChoice restart_used) const; @@ -734,6 +740,9 @@ class Solver { WallTimer timer_; int iterations_completed_; int num_rejected_steps_; + // A cache of `constraint_matrix * current_primal_solution_`. + // Malitsky-Pock linesearch only. + std::optional current_primal_product_; // A cache of `constraint_matrix.transpose() * current_dual_solution_`. VectorXd current_dual_product_; // The primal point at which the algorithm was last restarted from, or @@ -1870,31 +1879,41 @@ Solver::NextSolutionAndDelta Solver::ComputeNextPrimalSolution( Solver::NextSolutionAndDelta Solver::ComputeNextDualSolution( double dual_step_size, double extrapolation_factor, - const NextSolutionAndDelta& next_primal_solution) const { + const NextSolutionAndDelta& next_primal_solution, + const VectorXd* absl_nullable next_primal_product) const { const int64_t dual_size = ShardedWorkingQp().DualSize(); NextSolutionAndDelta result = { .value = VectorXd(dual_size), .delta = VectorXd(dual_size), }; const QuadraticProgram& qp = WorkingQp(); - VectorXd extrapolated_primal(ShardedWorkingQp().PrimalSize()); - ShardedWorkingQp().PrimalSharder().ParallelForEachShard( - [&](const Sharder::Shard& shard) { - shard(extrapolated_primal) = - (shard(next_primal_solution.value) + - extrapolation_factor * shard(next_primal_solution.delta)); - }); - // TODO(user): Refactor this multiplication so that we only do one matrix - // vector multiply for the primal variable. This only applies to Malitsky and - // Pock and not to the adaptive step size rule. + std::optional extrapolated_primal; + if (!next_primal_product) { + extrapolated_primal.emplace(ShardedWorkingQp().PrimalSize()); + ShardedWorkingQp().PrimalSharder().ParallelForEachShard( + [&](const Sharder::Shard& shard) { + shard(*extrapolated_primal) = + (shard(next_primal_solution.value) + + extrapolation_factor * shard(next_primal_solution.delta)); + }); + } ShardedWorkingQp().TransposedConstraintMatrixSharder().ParallelForEachShard( [&](const Sharder::Shard& shard) { - VectorXd temp = - shard(current_dual_solution_) - - dual_step_size * - shard(ShardedWorkingQp().TransposedConstraintMatrix()) - .transpose() * - extrapolated_primal; + VectorXd temp; + if (next_primal_product) { + CHECK(current_primal_product_.has_value()); + temp = shard(current_dual_solution_) - + dual_step_size * + (-extrapolation_factor * shard(*current_primal_product_) + + (extrapolation_factor + 1) * shard(*next_primal_product)); + } else { + temp = shard(current_dual_solution_) - + dual_step_size * + shard(ShardedWorkingQp().TransposedConstraintMatrix()) + .transpose() * + extrapolated_primal.value(); + } + // Each element of the argument of `.cwiseMin()` is the critical point // of the respective 1D minimization problem if it's negative. // Likewise the argument to the `.cwiseMax()` is the critical point if @@ -1937,6 +1956,21 @@ double Solver::ComputeNonlinearity(const VectorXd& delta_primal, }); } +void Solver::SetCurrentPrimalAndDualProducts() { + if (params_.linesearch_rule() == + PrimalDualHybridGradientParams::MALITSKY_POCK_LINESEARCH_RULE) { + current_primal_product_ = TransposedMatrixVectorProduct( + ShardedWorkingQp().TransposedConstraintMatrix(), + current_primal_solution_, + ShardedWorkingQp().TransposedConstraintMatrixSharder()); + } else { + current_primal_product_.reset(); + } + current_dual_product_ = TransposedMatrixVectorProduct( + WorkingQp().constraint_matrix, current_dual_solution_, + ShardedWorkingQp().ConstraintMatrixSharder()); +} + IterationStats Solver::CreateSimpleIterationStats( RestartChoice restart_used) const { IterationStats stats; @@ -1977,8 +2011,10 @@ LocalizedLagrangianBounds Solver::ComputeLocalizedBoundsAtCurrent() const { ShardedWorkingQp(), current_primal_solution_, current_dual_solution_, PrimalDualNorm::kEuclideanNorm, primal_weight_, distance_traveled_by_current, - /*primal_product=*/nullptr, ¤t_dual_product_, - params_.use_diagonal_qp_trust_region_solver(), + /*primal_product=*/current_primal_product_.has_value() + ? ¤t_primal_product_.value() + : nullptr, + ¤t_dual_product_, params_.use_diagonal_qp_trust_region_solver(), params_.diagonal_qp_trust_region_solver_tolerance()); } @@ -2225,9 +2261,7 @@ void Solver::ApplyRestartChoice(const RestartChoice restart_to_apply) { } current_primal_solution_ = primal_average_.ComputeAverage(); current_dual_solution_ = dual_average_.ComputeAverage(); - current_dual_product_ = TransposedMatrixVectorProduct( - WorkingQp().constraint_matrix, current_dual_solution_, - ShardedWorkingQp().ConstraintMatrixSharder()); + SetCurrentPrimalAndDualProducts(); break; } primal_weight_ = ComputeNewPrimalWeight(); @@ -2443,6 +2477,14 @@ InnerStepOutcome Solver::TakeMalitskyPockStep() { params_.malitsky_pock_parameters().linesearch_contraction_factor(); const double dual_weight = primal_weight_ * primal_weight_; int inner_iterations = 0; + VectorXd next_primal_product(current_dual_solution_.size()); + ShardedWorkingQp().TransposedConstraintMatrixSharder().ParallelForEachShard( + [&](const Sharder::Shard& shard) { + shard(next_primal_product) = + shard(ShardedWorkingQp().TransposedConstraintMatrix()).transpose() * + next_primal_solution.value; + }); + for (bool accepted_step = false; !accepted_step; ++inner_iterations) { if (inner_iterations >= 60) { LogInnerIterationLimitHit(); @@ -2454,7 +2496,7 @@ InnerStepOutcome Solver::TakeMalitskyPockStep() { new_primal_step_size / primal_step_size; NextSolutionAndDelta next_dual_solution = ComputeNextDualSolution( dual_weight * new_primal_step_size, new_last_two_step_sizes_ratio, - next_primal_solution); + next_primal_solution, &next_primal_product); VectorXd next_dual_product = TransposedMatrixVectorProduct( WorkingQp().constraint_matrix, next_dual_solution.value, @@ -2482,6 +2524,7 @@ InnerStepOutcome Solver::TakeMalitskyPockStep() { current_primal_solution_ = std::move(next_primal_solution.value); current_dual_solution_ = std::move(next_dual_solution.value); current_dual_product_ = std::move(next_dual_product); + current_primal_product_ = std::move(next_primal_product); primal_average_.Add(current_primal_solution_, /*weight=*/new_primal_step_size); dual_average_.Add(current_dual_solution_, @@ -2555,6 +2598,7 @@ InnerStepOutcome Solver::TakeAdaptiveStep() { current_primal_solution_ = std::move(next_primal_solution.value); current_dual_solution_ = std::move(next_dual_solution.value); current_dual_product_ = std::move(next_dual_product); + current_primal_product_.reset(); current_primal_delta_ = std::move(next_primal_solution.delta); current_dual_delta_ = std::move(next_dual_solution.delta); primal_average_.Add(current_primal_solution_, /*weight=*/step_size_); @@ -2620,6 +2664,7 @@ InnerStepOutcome Solver::TakeConstantSizeStep() { current_primal_solution_ = std::move(next_primal_solution.value); current_dual_solution_ = std::move(next_dual_solution.value); current_dual_product_ = std::move(next_dual_product); + current_primal_product_.reset(); current_primal_delta_ = std::move(next_primal_solution.delta); current_dual_delta_ = std::move(next_dual_solution.delta); primal_average_.Add(current_primal_solution_, /*weight=*/step_size_); @@ -2980,9 +3025,7 @@ SolverResult Solver::Solve(const IterationType iteration_type, // restart. ratio_last_two_step_sizes_ = 1; - current_dual_product_ = TransposedMatrixVectorProduct( - WorkingQp().constraint_matrix, current_dual_solution_, - ShardedWorkingQp().ConstraintMatrixSharder()); + SetCurrentPrimalAndDualProducts(); // This is set to true if we can't proceed any more because of numerical // issues. We may or may not have found the optimal solution. diff --git a/ortools/pdlp/python/pdlp_test.py b/ortools/pdlp/python/pdlp_test.py index 5464385a82f..72c8897d38e 100644 --- a/ortools/pdlp/python/pdlp_test.py +++ b/ortools/pdlp/python/pdlp_test.py @@ -25,230 +25,248 @@ def small_proto_lp(): - # min -2y - # s.t. x + y <= 1 - # x, y >= 0 - return linear_solver_pb2.MPModelProto( - # Defaults are specified for the benefit of assertProto2Equal. - maximize=False, - objective_offset=0.0, - variable=[ - linear_solver_pb2.MPVariableProto( - lower_bound=0, upper_bound=np.inf, objective_coefficient=0, name="x" - ), - linear_solver_pb2.MPVariableProto( - lower_bound=0, upper_bound=np.inf, objective_coefficient=-2, name="y" - ), - ], - constraint=[ - linear_solver_pb2.MPConstraintProto( - var_index=[0, 1], coefficient=[1, 1], lower_bound=-np.inf, upper_bound=1 - ) - ], - ) + # min -2y + # s.t. x + y <= 1 + # x, y >= 0 + return linear_solver_pb2.MPModelProto( + # Defaults are specified for the benefit of assertProto2Equal. + maximize=False, + objective_offset=0.0, + variable=[ + linear_solver_pb2.MPVariableProto( + lower_bound=0, + upper_bound=np.inf, + objective_coefficient=0, + name="x", + ), + linear_solver_pb2.MPVariableProto( + lower_bound=0, + upper_bound=np.inf, + objective_coefficient=-2, + name="y", + ), + ], + constraint=[ + linear_solver_pb2.MPConstraintProto( + var_index=[0, 1], + coefficient=[1, 1], + lower_bound=-np.inf, + upper_bound=1, + ) + ], + ) def small_proto_qp(): - # min 2 x*x - # s.t. x + y <= 1 - # x, y >= 0 - return linear_solver_pb2.MPModelProto( - # Defaults are specified for the benefit of assertProto2Equal. - maximize=False, - objective_offset=0.0, - variable=[ - linear_solver_pb2.MPVariableProto( - lower_bound=0, upper_bound=np.inf, objective_coefficient=0, name="x" - ), - linear_solver_pb2.MPVariableProto( - lower_bound=0, upper_bound=np.inf, objective_coefficient=0, name="y" - ), - ], - constraint=[ - linear_solver_pb2.MPConstraintProto( - var_index=[0, 1], coefficient=[1, 1], lower_bound=-np.inf, upper_bound=1 - ) - ], - quadratic_objective=linear_solver_pb2.MPQuadraticObjective( - qvar1_index=[0], qvar2_index=[0], coefficient=[2] - ), - ) + # min 2 x*x + # s.t. x + y <= 1 + # x, y >= 0 + return linear_solver_pb2.MPModelProto( + # Defaults are specified for the benefit of assertProto2Equal. + maximize=False, + objective_offset=0.0, + variable=[ + linear_solver_pb2.MPVariableProto( + lower_bound=0, + upper_bound=np.inf, + objective_coefficient=0, + name="x", + ), + linear_solver_pb2.MPVariableProto( + lower_bound=0, + upper_bound=np.inf, + objective_coefficient=0, + name="y", + ), + ], + constraint=[ + linear_solver_pb2.MPConstraintProto( + var_index=[0, 1], + coefficient=[1, 1], + lower_bound=-np.inf, + upper_bound=1, + ) + ], + quadratic_objective=linear_solver_pb2.MPQuadraticObjective( + qvar1_index=[0], qvar2_index=[0], coefficient=[2] + ), + ) class QuadraticProgramTest(absltest.TestCase): - def test_validate_quadratic_program_dimensions_for_empty_qp(self): - qp = pdlp.QuadraticProgram() - qp.resize_and_initialize(3, 2) - pdlp.validate_quadratic_program_dimensions(qp) - self.assertTrue(pdlp.is_linear_program(qp)) - - def test_converts_from_tiny_mpmodel_lp(self): - lp_proto = small_proto_lp() - qp = pdlp.qp_from_mpmodel_proto(lp_proto, relax_integer_variables=False) - pdlp.validate_quadratic_program_dimensions(qp) - self.assertTrue(pdlp.is_linear_program(qp)) - self.assertSameElements(qp.objective_vector, [0, -2]) - - def test_converts_from_tiny_mpmodel_qp(self): - qp_proto = small_proto_qp() - qp = pdlp.qp_from_mpmodel_proto(qp_proto, relax_integer_variables=False) - pdlp.validate_quadratic_program_dimensions(qp) - self.assertFalse(pdlp.is_linear_program(qp)) - self.assertSameElements(qp.objective_vector, [0, 0]) - - def test_build_lp(self): - qp = pdlp.QuadraticProgram() - qp.objective_vector = [0, -2] - qp.constraint_matrix = scipy.sparse.csr_matrix(np.array([[1.0, 1.0]])) - qp.constraint_lower_bounds = [-np.inf] - qp.constraint_upper_bounds = [1.0] - qp.variable_lower_bounds = [0.0, 0.0] - qp.variable_upper_bounds = [np.inf, np.inf] - qp.variable_names = ["x", "y"] - self.assertEqual( - pdlp.qp_to_mpmodel_proto(qp), - small_proto_lp(), - ) - - def test_build_qp(self): - qp = pdlp.QuadraticProgram() - qp.objective_vector = [0, 0] - qp.constraint_matrix = scipy.sparse.csr_matrix(np.array([[1.0, 1.0]])) - qp.set_objective_matrix_diagonal([4.0]) - qp.constraint_lower_bounds = [-np.inf] - qp.constraint_upper_bounds = [1.0] - qp.variable_lower_bounds = [0.0, 0.0] - qp.variable_upper_bounds = [np.inf, np.inf] - qp.variable_names = ["x", "y"] - self.assertEqual( - pdlp.qp_to_mpmodel_proto(qp), - small_proto_qp(), - ) + def test_validate_quadratic_program_dimensions_for_empty_qp(self): + qp = pdlp.QuadraticProgram() + qp.resize_and_initialize(3, 2) + pdlp.validate_quadratic_program_dimensions(qp) + self.assertTrue(pdlp.is_linear_program(qp)) + def test_converts_from_tiny_mpmodel_lp(self): + lp_proto = small_proto_lp() + qp = pdlp.qp_from_mpmodel_proto(lp_proto, relax_integer_variables=False) + pdlp.validate_quadratic_program_dimensions(qp) + self.assertTrue(pdlp.is_linear_program(qp)) + self.assertSameElements(qp.objective_vector, [0, -2]) -def tiny_lp(): - """Returns a small test LP. - - The LP: - min 5 x_1 + 2 x_2 + x_3 + x_4 - 14 s.t. - 2 x_1 + x_2 + x_3 + 2 x_4 = 12 - x_1 + x_3 >= 7 - x_3 - x_4 >= 1 - 0 <= x_1 <= 2 - 0 <= x_2 <= 4 - 0 <= x_3 <= 6 - 0 <= x_4 <= 3 - - Optimum solutions: - Primal: x_1 = 1, x_2 = 0, x_3 = 6, x_4 = 2. Value: 5 + 0 + 6 + 2 - 14 = -1. - Dual: [0.5, 4.0, 0.0] Value: 6 + 28 - 3.5*6 - 14 = -1 - Reduced costs: [0.0, 1.5, -3.5, 0.0] - """ - qp = pdlp.QuadraticProgram() - qp.objective_offset = -14 - qp.objective_vector = [5, 2, 1, 1] - qp.constraint_lower_bounds = [12, 7, 1] - qp.constraint_upper_bounds = [12, np.inf, np.inf] - qp.variable_lower_bounds = np.zeros(4) - qp.variable_upper_bounds = [2, 4, 6, 3] - constraint_matrix = np.array([[2, 1, 1, 2], [1, 0, 1, 0], [0, 0, 1, -1]]) - qp.constraint_matrix = scipy.sparse.csr_matrix(constraint_matrix) - return qp + def test_converts_from_tiny_mpmodel_qp(self): + qp_proto = small_proto_qp() + qp = pdlp.qp_from_mpmodel_proto(qp_proto, relax_integer_variables=False) + pdlp.validate_quadratic_program_dimensions(qp) + self.assertFalse(pdlp.is_linear_program(qp)) + self.assertSameElements(qp.objective_vector, [0, 0]) + def test_build_lp(self): + qp = pdlp.QuadraticProgram() + qp.objective_vector = [0, -2] + qp.constraint_matrix = scipy.sparse.csr_matrix(np.array([[1.0, 1.0]])) + qp.constraint_lower_bounds = [-np.inf] + qp.constraint_upper_bounds = [1.0] + qp.variable_lower_bounds = [0.0, 0.0] + qp.variable_upper_bounds = [np.inf, np.inf] + qp.variable_names = ["x", "y"] + self.assertEqual( + pdlp.qp_to_mpmodel_proto(qp), + small_proto_lp(), + ) -def small_lp(): - """Returns a small LP with all 4 patterns lower and upper bounds. - - min 5.5 x_0 - 2 x_1 - x_2 + x_3 - 14 s.t. - 2 x_0 + x_1 + x_2 + 2 x_3 = 12 - x_0 + x_2 <= 7 - 4 x_0 >= -4 - -1 <= 1.5 x_2 - x_3 <= 1 - -infinity <= x_0 <= infinity - -2 <= x_1 <= infinity - -infinity <= x_2 <= 6 - 2.5 <= x_3 <= 3.5 - - Optimal solutions: - Primal: [-1, 8, 1, 2.5] - Dual: [-2, 0, 2.375, 2.0/3] - Value: -5.5 - 16 -1 + 2.5 - 14 = -34 - """ + def test_build_qp(self): qp = pdlp.QuadraticProgram() - qp.objective_offset = -14 - qp.objective_vector = [5.5, -2, -1, 1] - qp.constraint_lower_bounds = [12, -np.inf, -4, -1] - qp.constraint_upper_bounds = [12, 7, np.inf, 1] - qp.variable_lower_bounds = [-np.inf, -2, -np.inf, 2.5] - qp.variable_upper_bounds = [np.inf, np.inf, 6, 3.5] - constraint_matrix = np.array( - [[2, 1, 1, 2], [1, 0, 1, 0], [4, 0, 0, 0], [0, 0, 1.5, -1]] + qp.objective_vector = [0, 0] + qp.constraint_matrix = scipy.sparse.csr_matrix(np.array([[1.0, 1.0]])) + qp.set_objective_matrix_diagonal([4.0]) + qp.constraint_lower_bounds = [-np.inf] + qp.constraint_upper_bounds = [1.0] + qp.variable_lower_bounds = [0.0, 0.0] + qp.variable_upper_bounds = [np.inf, np.inf] + qp.variable_names = ["x", "y"] + self.assertEqual( + pdlp.qp_to_mpmodel_proto(qp), + small_proto_qp(), ) - qp.constraint_matrix = scipy.sparse.csr_matrix(constraint_matrix) - return qp + + +def tiny_lp(): + """Returns a small test LP. + + The LP: + min 5 x_1 + 2 x_2 + x_3 + x_4 - 14 s.t. + 2 x_1 + x_2 + x_3 + 2 x_4 = 12 + x_1 + x_3 >= 7 + x_3 - x_4 >= 1 + 0 <= x_1 <= 2 + 0 <= x_2 <= 4 + 0 <= x_3 <= 6 + 0 <= x_4 <= 3 + + Optimum solutions: + Primal: x_1 = 1, x_2 = 0, x_3 = 6, x_4 = 2. Value: 5 + 0 + 6 + 2 - 14 = -1. + Dual: [0.5, 4.0, 0.0] Value: 6 + 28 - 3.5*6 - 14 = -1 + Reduced costs: [0.0, 1.5, -3.5, 0.0] + """ + qp = pdlp.QuadraticProgram() + qp.objective_offset = -14 + qp.objective_vector = [5, 2, 1, 1] + qp.constraint_lower_bounds = [12, 7, 1] + qp.constraint_upper_bounds = [12, np.inf, np.inf] + qp.variable_lower_bounds = np.zeros(4) + qp.variable_upper_bounds = [2, 4, 6, 3] + constraint_matrix = np.array([[2, 1, 1, 2], [1, 0, 1, 0], [0, 0, 1, -1]]) + qp.constraint_matrix = scipy.sparse.csr_matrix(constraint_matrix) + return qp + + +def small_lp(): + """Returns a small LP with all 4 patterns lower and upper bounds. + + min 5.5 x_0 - 2 x_1 - x_2 + x_3 - 14 s.t. + 2 x_0 + x_1 + x_2 + 2 x_3 = 12 + x_0 + x_2 <= 7 + 4 x_0 >= -4 + -1 <= 1.5 x_2 - x_3 <= 1 + -infinity <= x_0 <= infinity + -2 <= x_1 <= infinity + -infinity <= x_2 <= 6 + 2.5 <= x_3 <= 3.5 + + Optimal solutions: + Primal: [-1, 8, 1, 2.5] + Dual: [-2, 0, 2.375, 2.0/3] + Value: -5.5 - 16 -1 + 2.5 - 14 = -34 + """ + qp = pdlp.QuadraticProgram() + qp.objective_offset = -14 + qp.objective_vector = [5.5, -2, -1, 1] + qp.constraint_lower_bounds = [12, -np.inf, -4, -1] + qp.constraint_upper_bounds = [12, 7, np.inf, 1] + qp.variable_lower_bounds = [-np.inf, -2, -np.inf, 2.5] + qp.variable_upper_bounds = [np.inf, np.inf, 6, 3.5] + constraint_matrix = np.array( + [[2, 1, 1, 2], [1, 0, 1, 0], [4, 0, 0, 0], [0, 0, 1.5, -1]] + ) + qp.constraint_matrix = scipy.sparse.csr_matrix(constraint_matrix) + return qp class PrimalDualHybridGradientTest(absltest.TestCase): - def test_iteration_limit(self): - params = solvers_pb2.PrimalDualHybridGradientParams() - params.termination_criteria.iteration_limit = 1 - params.termination_check_frequency = 1 - result = pdlp.primal_dual_hybrid_gradient(tiny_lp(), params) - self.assertLessEqual(result.solve_log.iteration_count, 1) - self.assertEqual( - result.solve_log.termination_reason, - solve_log_pb2.TERMINATION_REASON_ITERATION_LIMIT, - ) - - def test_solution(self): - params = solvers_pb2.PrimalDualHybridGradientParams() - opt_criteria = params.termination_criteria.simple_optimality_criteria - opt_criteria.eps_optimal_relative = 0.0 - opt_criteria.eps_optimal_absolute = 1.0e-10 - result = pdlp.primal_dual_hybrid_gradient(tiny_lp(), params) - self.assertEqual( - result.solve_log.termination_reason, - solve_log_pb2.TERMINATION_REASON_OPTIMAL, - ) - self.assertSequenceAlmostEqual(result.primal_solution, [1.0, 0.0, 6.0, 2.0]) - self.assertSequenceAlmostEqual(result.dual_solution, [0.5, 4.0, 0.0]) - self.assertSequenceAlmostEqual(result.reduced_costs, [0.0, 1.5, -3.5, 0.0]) - - def test_solution_2(self): - params = solvers_pb2.PrimalDualHybridGradientParams() - opt_criteria = params.termination_criteria.simple_optimality_criteria - opt_criteria.eps_optimal_relative = 0.0 - opt_criteria.eps_optimal_absolute = 1.0e-10 - result = pdlp.primal_dual_hybrid_gradient(small_lp(), params) - self.assertEqual( - result.solve_log.termination_reason, - solve_log_pb2.TERMINATION_REASON_OPTIMAL, - ) - self.assertSequenceAlmostEqual(result.primal_solution, [-1, 8, 1, 2.5]) - self.assertSequenceAlmostEqual(result.dual_solution, [-2, 0, 2.375, 2 / 3]) - - def test_starting_point(self): - params = solvers_pb2.PrimalDualHybridGradientParams() - opt_criteria = params.termination_criteria.simple_optimality_criteria - opt_criteria.eps_optimal_relative = 0.0 - opt_criteria.eps_optimal_absolute = 1.0e-10 - params.l_inf_ruiz_iterations = 0 - params.l2_norm_rescaling = False - - start = pdlp.PrimalAndDualSolution() - start.primal_solution = [1.0, 0.0, 6.0, 2.0] - start.dual_solution = [0.5, 4.0, 0.0] - result = pdlp.primal_dual_hybrid_gradient( - tiny_lp(), params, initial_solution=start - ) - self.assertEqual( - result.solve_log.termination_reason, - solve_log_pb2.TERMINATION_REASON_OPTIMAL, - ) - self.assertEqual(result.solve_log.iteration_count, 0) + def test_iteration_limit(self): + params = solvers_pb2.PrimalDualHybridGradientParams() + params.termination_criteria.iteration_limit = 1 + params.termination_check_frequency = 1 + result = pdlp.primal_dual_hybrid_gradient(tiny_lp(), params) + self.assertLessEqual(result.solve_log.iteration_count, 1) + self.assertEqual( + result.solve_log.termination_reason, + solve_log_pb2.TERMINATION_REASON_ITERATION_LIMIT, + ) + + def test_solution(self): + params = solvers_pb2.PrimalDualHybridGradientParams() + opt_criteria = params.termination_criteria.simple_optimality_criteria + opt_criteria.eps_optimal_relative = 0.0 + opt_criteria.eps_optimal_absolute = 1.0e-10 + result = pdlp.primal_dual_hybrid_gradient(tiny_lp(), params) + self.assertEqual( + result.solve_log.termination_reason, + solve_log_pb2.TERMINATION_REASON_OPTIMAL, + ) + self.assertSequenceAlmostEqual(result.primal_solution, [1.0, 0.0, 6.0, 2.0]) + self.assertSequenceAlmostEqual(result.dual_solution, [0.5, 4.0, 0.0]) + self.assertSequenceAlmostEqual(result.reduced_costs, [0.0, 1.5, -3.5, 0.0]) + + def test_solution_2(self): + params = solvers_pb2.PrimalDualHybridGradientParams() + opt_criteria = params.termination_criteria.simple_optimality_criteria + opt_criteria.eps_optimal_relative = 0.0 + opt_criteria.eps_optimal_absolute = 1.0e-10 + result = pdlp.primal_dual_hybrid_gradient(small_lp(), params) + self.assertEqual( + result.solve_log.termination_reason, + solve_log_pb2.TERMINATION_REASON_OPTIMAL, + ) + self.assertSequenceAlmostEqual(result.primal_solution, [-1, 8, 1, 2.5]) + self.assertSequenceAlmostEqual(result.dual_solution, [-2, 0, 2.375, 2 / 3]) + + def test_starting_point(self): + params = solvers_pb2.PrimalDualHybridGradientParams() + opt_criteria = params.termination_criteria.simple_optimality_criteria + opt_criteria.eps_optimal_relative = 0.0 + opt_criteria.eps_optimal_absolute = 1.0e-10 + params.l_inf_ruiz_iterations = 0 + params.l2_norm_rescaling = False + + start = pdlp.PrimalAndDualSolution() + start.primal_solution = [1.0, 0.0, 6.0, 2.0] + start.dual_solution = [0.5, 4.0, 0.0] + result = pdlp.primal_dual_hybrid_gradient( + tiny_lp(), params, initial_solution=start + ) + self.assertEqual( + result.solve_log.termination_reason, + solve_log_pb2.TERMINATION_REASON_OPTIMAL, + ) + self.assertEqual(result.solve_log.iteration_count, 0) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/pdlp/samples/simple_pdlp_program.py b/ortools/pdlp/samples/simple_pdlp_program.py index 7af98d18a01..8e598d4ddd8 100644 --- a/ortools/pdlp/samples/simple_pdlp_program.py +++ b/ortools/pdlp/samples/simple_pdlp_program.py @@ -30,82 +30,82 @@ def simple_lp() -> pdlp.QuadraticProgram: - """Returns a small LP. - - min 5.5 x_0 - 2 x_1 - x_2 + x_3 - 14 s.t. - 2 x_0 + x_1 + x_2 + 2 x_3 = 12 - x_0 + x_2 <= 7 - 4 x_0 >= -4 - -1 <= 1.5 x_2 - x_3 <= 1 - -infinity <= x_0 <= infinity - -2 <= x_1 <= infinity - -infinity <= x_2 <= 6 - 2.5 <= x_3 <= 3.5 - """ - lp = pdlp.QuadraticProgram() - lp.objective_offset = -14 - lp.objective_vector = [5.5, -2, -1, 1] - lp.constraint_lower_bounds = [12, -np.inf, -4, -1] - lp.constraint_upper_bounds = [12, 7, np.inf, 1] - lp.variable_lower_bounds = [-np.inf, -2, -np.inf, 2.5] - lp.variable_upper_bounds = [np.inf, np.inf, 6, 3.5] - # Most use cases should initialize the sparse constraint matrix without - # constructing a dense matrix first! We use a np.array here for convenience - # only. - constraint_matrix = np.array( - [[2, 1, 1, 2], [1, 0, 1, 0], [4, 0, 0, 0], [0, 0, 1.5, -1]] - ) - lp.constraint_matrix = scipy.sparse.csc_matrix(constraint_matrix) - return lp + """Returns a small LP. + + min 5.5 x_0 - 2 x_1 - x_2 + x_3 - 14 s.t. + 2 x_0 + x_1 + x_2 + 2 x_3 = 12 + x_0 + x_2 <= 7 + 4 x_0 >= -4 + -1 <= 1.5 x_2 - x_3 <= 1 + -infinity <= x_0 <= infinity + -2 <= x_1 <= infinity + -infinity <= x_2 <= 6 + 2.5 <= x_3 <= 3.5 + """ + lp = pdlp.QuadraticProgram() + lp.objective_offset = -14 + lp.objective_vector = [5.5, -2, -1, 1] + lp.constraint_lower_bounds = [12, -np.inf, -4, -1] + lp.constraint_upper_bounds = [12, 7, np.inf, 1] + lp.variable_lower_bounds = [-np.inf, -2, -np.inf, 2.5] + lp.variable_upper_bounds = [np.inf, np.inf, 6, 3.5] + # Most use cases should initialize the sparse constraint matrix without + # constructing a dense matrix first! We use a np.array here for convenience + # only. + constraint_matrix = np.array( + [[2, 1, 1, 2], [1, 0, 1, 0], [4, 0, 0, 0], [0, 0, 1.5, -1]] + ) + lp.constraint_matrix = scipy.sparse.csc_matrix(constraint_matrix) + return lp def main() -> None: - params = solvers_pb2.PrimalDualHybridGradientParams() - # Below are some common parameters to modify. Here, we just re-assign the - # defaults. - optimality_criteria = params.termination_criteria.simple_optimality_criteria - optimality_criteria.eps_optimal_relative = 1.0e-6 - optimality_criteria.eps_optimal_absolute = 1.0e-6 - params.termination_criteria.time_sec_limit = np.inf - params.num_threads = 1 - params.verbosity_level = 0 - params.presolve_options.use_glop = False - - # Call the main solve function. - result = pdlp.primal_dual_hybrid_gradient(simple_lp(), params) - solve_log = result.solve_log - - if solve_log.termination_reason == solve_log_pb2.TERMINATION_REASON_OPTIMAL: - print("Solve successful") - else: - print( - "Solve not successful. Status:", - solve_log_pb2.TerminationReason.Name(solve_log.termination_reason), - ) - - # Solutions vectors are always returned. *However*, their interpretation - # depends on termination_reason! See primal_dual_hybrid_gradient.h for more - # details on what the vectors mean if termination_reason is not - # TERMINATION_REASON_OPTIMAL. - print("Primal solution:", result.primal_solution) - print("Dual solution:", result.dual_solution) - print("Reduced costs:", result.reduced_costs) - - solution_type = solve_log.solution_type - print("Solution type:", solve_log_pb2.PointType.Name(solution_type)) - for ci in solve_log.solution_stats.convergence_information: - if ci.candidate_type == solution_type: - print("Primal objective:", ci.primal_objective) - print("Dual objective:", ci.dual_objective) - - print("Iterations:", solve_log.iteration_count) - print("Solve time (sec):", solve_log.solve_time_sec) + params = solvers_pb2.PrimalDualHybridGradientParams() + # Below are some common parameters to modify. Here, we just re-assign the + # defaults. + optimality_criteria = params.termination_criteria.simple_optimality_criteria + optimality_criteria.eps_optimal_relative = 1.0e-6 + optimality_criteria.eps_optimal_absolute = 1.0e-6 + params.termination_criteria.time_sec_limit = np.inf + params.num_threads = 1 + params.verbosity_level = 0 + params.presolve_options.use_glop = False + + # Call the main solve function. + result = pdlp.primal_dual_hybrid_gradient(simple_lp(), params) + solve_log = result.solve_log + + if solve_log.termination_reason == solve_log_pb2.TERMINATION_REASON_OPTIMAL: + print("Solve successful") + else: + print( + "Solve not successful. Status:", + solve_log_pb2.TerminationReason.Name(solve_log.termination_reason), + ) + + # Solutions vectors are always returned. *However*, their interpretation + # depends on termination_reason! See primal_dual_hybrid_gradient.h for more + # details on what the vectors mean if termination_reason is not + # TERMINATION_REASON_OPTIMAL. + print("Primal solution:", result.primal_solution) + print("Dual solution:", result.dual_solution) + print("Reduced costs:", result.reduced_costs) + + solution_type = solve_log.solution_type + print("Solution type:", solve_log_pb2.PointType.Name(solution_type)) + for ci in solve_log.solution_stats.convergence_information: + if ci.candidate_type == solution_type: + print("Primal objective:", ci.primal_objective) + print("Dual objective:", ci.dual_objective) + + print("Iterations:", solve_log.iteration_count) + print("Solve time (sec):", solve_log.solve_time_sec) if __name__ == "__main__": - init.CppBridge.init_logging("simple_pdlp_program.py") - cpp_flags = init.CppFlags() - cpp_flags.stderrthreshold = 0 - cpp_flags.log_prefix = False - init.CppBridge.set_flags(cpp_flags) - main() + init.CppBridge.init_logging("simple_pdlp_program.py") + cpp_flags = init.CppFlags() + cpp_flags.stderrthreshold = 0 + cpp_flags.log_prefix = False + init.CppBridge.set_flags(cpp_flags) + main() diff --git a/ortools/python/ortools_notebook.py b/ortools/python/ortools_notebook.py index 301d6f2b093..d8501e94352 100644 --- a/ortools/python/ortools_notebook.py +++ b/ortools/python/ortools_notebook.py @@ -20,5 +20,5 @@ if __name__ == "__main__": - sys.argv[0] = re.sub(r"(-script\.pyw?|\.exe)?$", "", sys.argv[0]) - sys.exit(app.main()) + sys.argv[0] = re.sub(r"(-script\.pyw?|\.exe)?$", "", sys.argv[0]) + sys.exit(app.main()) diff --git a/ortools/python/setup.py.in b/ortools/python/setup.py.in index befa15ce04e..5d67c5fa127 100644 --- a/ortools/python/setup.py.in +++ b/ortools/python/setup.py.in @@ -46,7 +46,7 @@ setup( 'absl-py >= 2.0.0', 'numpy >= 1.13.3', 'pandas >= 2.0.0', - 'protobuf >= 6.31.0,<6.32', + 'protobuf >= 6.31.1,<6.32', 'typing-extensions >= 4.12', 'immutabledict >= 3.0.0', ], @@ -74,6 +74,10 @@ setup( '$', '*.pyi' ], + '@PYTHON_PROJECT@.constraint_solver.python':[ + '$', + '*.pyi' + ], '@PYTHON_PROJECT@.linear_solver':[ '$', '*.pyi' @@ -110,6 +114,14 @@ setup( '$', '*.pyi' ], + '@PYTHON_PROJECT@.routing':[ + '$', + '*.pyi' + ], + '@PYTHON_PROJECT@.routing.python':[ + '$', + '*.pyi' + ], '@PYTHON_PROJECT@.sat':['*.pyi'], '@PYTHON_PROJECT@.sat.colab':['*.pyi', 'py.typed'], '@PYTHON_PROJECT@.sat.python':[ diff --git a/ortools/routing/BUILD.bazel b/ortools/routing/BUILD.bazel new file mode 100644 index 00000000000..f17f7d0235b --- /dev/null +++ b/ortools/routing/BUILD.bazel @@ -0,0 +1,345 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +load("@protobuf//bazel:cc_proto_library.bzl", "cc_proto_library") +load("@protobuf//bazel:java_proto_library.bzl", "java_proto_library") +load("@protobuf//bazel:proto_library.bzl", "proto_library") +load("@protobuf//bazel:py_proto_library.bzl", "py_proto_library") +load("@rules_cc//cc:defs.bzl", "cc_library") + +package(default_visibility = ["//visibility:public"]) + +config_setting( + name = "on_linux", + constraint_values = ["@platforms//os:linux"], +) + +config_setting( + name = "on_macos", + constraint_values = ["@platforms//os:macos"], +) + +config_setting( + name = "on_windows", + constraint_values = ["@platforms//os:windows"], +) + +proto_library( + name = "enums_proto", + srcs = ["enums.proto"], +) + +cc_proto_library( + name = "enums_cc_proto", + deps = [":enums_proto"], +) + +java_proto_library( + name = "enums_java_proto", + deps = [":enums_proto"], +) + +proto_library( + name = "ils_proto", + srcs = ["ils.proto"], + deps = [ + ":enums_proto", + ":heuristic_parameters_proto", + ], +) + +cc_proto_library( + name = "ils_cc_proto", + deps = [":ils_proto"], +) + +py_proto_library( + name = "ils_py_pb2", + deps = [":ils_proto"], +) + +java_proto_library( + name = "ils_java_proto", + deps = [":ils_proto"], +) + +proto_library( + name = "parameters_proto", + srcs = ["parameters.proto"], + deps = [ + ":enums_proto", + ":heuristic_parameters_proto", + ":ils_proto", + "//ortools/constraint_solver:solver_parameters_proto", + "//ortools/sat:sat_parameters_proto", + "//ortools/util:optional_boolean_proto", + "@protobuf//:duration_proto", + ], +) + +cc_proto_library( + name = "parameters_cc_proto", + deps = [":parameters_proto"], +) + +java_proto_library( + name = "parameters_java_proto", + deps = [":parameters_proto"], +) + +py_proto_library( + name = "parameters_py_pb2", + deps = [":parameters_proto"], +) + +py_proto_library( + name = "enums_py_pb2", + deps = [":enums_proto"], +) + +cc_library( + name = "parameters", + srcs = ["parameters.cc"], + hdrs = ["parameters.h"], + deps = [ + ":enums_cc_proto", + ":heuristic_parameters_cc_proto", + ":ils_cc_proto", + ":parameters_cc_proto", + "//ortools/base", + "//ortools/base:proto_enum_utils", + "//ortools/base:protoutil", + "//ortools/base:types", + "//ortools/constraint_solver:cp", + "//ortools/constraint_solver:solver_parameters_cc_proto", + "//ortools/port:proto_utils", + "//ortools/sat:sat_parameters_cc_proto", + "//ortools/util:optional_boolean_cc_proto", + "//ortools/util:testing_utils", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/time", + "@protobuf", + ], +) + +cc_library( + name = "parameters_utils", + srcs = ["parameters_utils.cc"], + hdrs = ["parameters_utils.h"], + deps = [ + ":heuristic_parameters_cc_proto", + ":parameters_cc_proto", + "//ortools/util:optional_boolean_cc_proto", + "@abseil-cpp//absl/types:span", + ], +) + +cc_library( + name = "types", + hdrs = ["types.h"], + deps = [ + "//ortools/util:piecewise_linear_function", + "//ortools/util:strong_integers", + ], +) + +cc_library( + name = "utils", + srcs = ["utils.cc"], + hdrs = ["utils.h"], + deps = [ + "//ortools/util:saturated_arithmetic", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/types:span", + ], +) + +cc_library( + name = "neighborhoods", + srcs = ["neighborhoods.cc"], + hdrs = ["neighborhoods.h"], + deps = [ + ":types", + ":utils", + "//ortools/base:types", + "//ortools/constraint_solver:cp", + "//ortools/util:bitset", + "//ortools/util:saturated_arithmetic", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/types:span", + ], +) + +cc_library( + name = "index_manager", + srcs = ["index_manager.cc"], + hdrs = ["index_manager.h"], + deps = [ + ":types", + "//ortools/base", + "//ortools/base:base_export", + "//ortools/base:strong_vector", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/types:span", + ], +) + +cc_library( + name = "breaks", + srcs = ["breaks.cc"], + hdrs = ["breaks.h"], + deps = [ + ":filter_committables", + "//ortools/algorithms:binary_search", + "//ortools/util:saturated_arithmetic", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/types:span", + ], +) + +cc_library( + name = "routing", + srcs = [ + "constraints.cc", + "decision_builders.cc", + "filters.cc", + "flow.cc", + "ils.cc", + "insertion_lns.cc", + "lp_scheduling.cc", + "routing.cc", + "sat.cc", + "search.cc", + ], + hdrs = [ + "constraints.h", + "decision_builders.h", + "filters.h", + "ils.h", + "insertion_lns.h", + "lp_scheduling.h", + "routing.h", + "search.h", + ], + copts = select({ + "on_linux": [], + "on_macos": [], + "on_windows": ["/Zc:preprocessor"], + "//conditions:default": [], + }), + deps = [ + ":breaks", + ":enums_cc_proto", + ":filter_committables", + ":heuristic_parameters_cc_proto", + ":ils_cc_proto", + ":index_manager", + ":neighborhoods", + ":parameters", + ":parameters_cc_proto", + ":parameters_utils", + ":types", + ":utils", + "//ortools/base", + "//ortools/base:dump_vars", + "//ortools/base:map_util", + "//ortools/base:mathutil", + "//ortools/base:protoutil", + "//ortools/base:stl_util", + "//ortools/base:strong_vector", + "//ortools/base:types", + "//ortools/constraint_solver:cp", + "//ortools/constraint_solver:solver_parameters_cc_proto", + "//ortools/glop:lp_solver", + "//ortools/glop:parameters_cc_proto", + "//ortools/graph", + "//ortools/graph:christofides", + "//ortools/graph:connected_components", + "//ortools/graph:linear_assignment", + "//ortools/graph:min_cost_flow", + "//ortools/lp_data", + "//ortools/lp_data:base", + "//ortools/port:proto_utils", + "//ortools/sat:cp_model_cc_proto", + "//ortools/sat:cp_model_solver", + "//ortools/sat:integer_base", + "//ortools/sat:lp_utils", + "//ortools/sat:model", + "//ortools/sat:sat_parameters_cc_proto", + "//ortools/util:bitset", + "//ortools/util:flat_matrix", + "//ortools/util:optional_boolean_cc_proto", + "//ortools/util:piecewise_linear_function", + "//ortools/util:range_minimum_query", + "//ortools/util:range_query_function", + "//ortools/util:saturated_arithmetic", + "//ortools/util:sorted_interval_list", + "//ortools/util:time_limit", + "@abseil-cpp//absl/algorithm:container", + "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/container:btree", + "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/container:flat_hash_set", + "@abseil-cpp//absl/container:inlined_vector", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/functional:any_invocable", + "@abseil-cpp//absl/functional:bind_front", + "@abseil-cpp//absl/hash", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/log:die_if_null", + "@abseil-cpp//absl/memory", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/time", + "@abseil-cpp//absl/types:span", + "@protobuf", + ], +) + +cc_library( + name = "filter_committables", + srcs = ["filter_committables.cc"], + hdrs = ["filter_committables.h"], + deps = [ + "//ortools/util:bitset", + "//ortools/util:saturated_arithmetic", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/types:span", + ], +) + +proto_library( + name = "heuristic_parameters_proto", + srcs = ["heuristic_parameters.proto"], +) + +java_proto_library( + name = "heuristic_parameters_java_proto", + deps = [":heuristic_parameters_proto"], +) + +cc_proto_library( + name = "heuristic_parameters_cc_proto", + deps = [":heuristic_parameters_proto"], +) + +py_proto_library( + name = "heuristic_parameters_py_pb2", + deps = [":heuristic_parameters_proto"], +) diff --git a/ortools/routing/CMakeLists.txt b/ortools/routing/CMakeLists.txt new file mode 100644 index 00000000000..78a3b0fa835 --- /dev/null +++ b/ortools/routing/CMakeLists.txt @@ -0,0 +1,39 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +file(GLOB _SRCS "*.h" "*.cc") +set(NAME ${PROJECT_NAME}_routing) + +# Will be merge in libortools.so +#add_library(${NAME} STATIC ${_SRCS}) +add_library(${NAME} OBJECT ${_SRCS}) +set_target_properties(${NAME} PROPERTIES + POSITION_INDEPENDENT_CODE ON + ) +if(MSVC AND BUILD_SHARED_LIBS) + target_compile_definitions(${NAME} PUBLIC "OR_BUILD_DLL") + target_compile_definitions(${NAME} PRIVATE "OR_EXPORT") +endif() +target_include_directories(${NAME} PRIVATE + ${PROJECT_SOURCE_DIR} + ${PROJECT_BINARY_DIR}) +target_link_libraries(${NAME} PRIVATE + ZLIB::ZLIB + absl::base + absl::memory + absl::strings + absl::str_format + protobuf::libprotobuf + ${PROJECT_NAMESPACE}::ortools_proto + ${PROJECT_NAMESPACE}::routing_proto) +#add_library(${PROJECT_NAMESPACE}::routing ALIAS ${NAME}) diff --git a/ortools/routing/README.md b/ortools/routing/README.md index 8a3b5cf641f..c307753022a 100644 --- a/ortools/routing/README.md +++ b/ortools/routing/README.md @@ -7,21 +7,21 @@ extension that is implemented on top of the CP solver library. To begin, skim: -* [../constraint_solver/routing.h](../constraint_solver/routing.h): The +* [routing.h](../routing/routing.h): The vehicle routing library lets one model and solve generic vehicle routing problems ranging from the Traveling Salesman Problem to more complex problems such as the Capacitated Vehicle Routing Problem with Time Windows. ### Parameters -* [../constraint_solver/routing_parameters.proto](../constraint_solver/routing_parameters.proto): +* [parameters.proto](../routing/parameters.proto): The Vehicle Routing solver parameters. -* [../constraint_solver/routing_enums.proto](../constraint_solver/routing_enums.proto): +* [enums.proto](../routing/enums.proto): Enums used to define routing parameters. ### Solution -* [../constraint_solver/assignment.proto](../constraint_solver/assignment.proto): +* [assignment.proto](../constraint_solver/assignment.proto): Holds the solution of a Routing problem (as a special case of a CS problem). ## Parsers diff --git a/ortools/routing/breaks.cc b/ortools/routing/breaks.cc new file mode 100644 index 00000000000..85ba622b73f --- /dev/null +++ b/ortools/routing/breaks.cc @@ -0,0 +1,297 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/breaks.h" + +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/types/span.h" +#include "ortools/algorithms/binary_search.h" +#include "ortools/routing/filter_committables.h" +#include "ortools/util/saturated_arithmetic.h" + +namespace operations_research::routing { + +// break_duration_on_transition_ is initialized with an upper bound on the +// number of transitions. +BreakPropagator::BreakPropagator(int num_nodes) + : break_duration_on_transition_(num_nodes, 0) {} + +BreakPropagator::PropagationResult BreakPropagator::FastPropagations( + int path, DimensionValues& dimension_values, + const PrePostVisitValues& visits) { + using VehicleBreak = DimensionValues::VehicleBreak; + const absl::Span vehicle_breaks = + absl::MakeSpan(dimension_values.MutableVehicleBreaks(path)); + if (vehicle_breaks.empty()) return kUnchanged; + const absl::Span cumuls = dimension_values.MutableCumuls(path); + const int num_cumuls = cumuls.size(); + const absl::Span travels = dimension_values.Travels(path); + const absl::Span travel_sums = + dimension_values.TravelSums(path); + const absl::Span pre_visits = visits.PreVisits(path); + const absl::Span post_visits = visits.PostVisits(path); + const auto visit_start_max = [cumuls, pre_visits](int c) -> int64_t { + return CapSub(cumuls[c].max, pre_visits[c]); + }; + const auto visit_end_min = [cumuls, post_visits](int c) -> int64_t { + return CapAdd(cumuls[c].min, post_visits[c]); + }; + const auto travel_slack = [cumuls, travels](int c) -> int64_t { + const int64_t slack = + CapSub(CapSub(cumuls[c + 1].max, cumuls[c].min), travels[c]); + DCHECK_GE(slack, 0); + return slack; + }; + PropagationResult result = kUnchanged; + // Propagations on cumuls are delayed to not break BinarySearch. + delayed_propagations_.clear(); + // When breaks must be performed inside the route, accumulate location as a + // min/max window and their total duration. + int breaks_window_min = num_cumuls; + int breaks_window_max = -1; + int64_t breaks_total_duration = 0; + break_duration_on_transition_.Revert(); + const int num_breaks = vehicle_breaks.size(); + for (VehicleBreak& br : vehicle_breaks) { + if (br.is_performed.min == 0) continue; + if (!IncreaseMin(CapSub(br.end.min, br.start.max), &br.duration, &result)) { + return result; + } + // Find largest c_min such that time windows prevent break br to end + // before cumul c_min. In that case, all visits up to and including c_min + // must be performed before the break. + int c_min = BinarySearch( + -1, num_cumuls, [&visit_start_max, break_end_min = br.end.min](int c) { + return visit_start_max(c) < break_end_min; + }); + if (c_min >= 0) { + while (c_min < num_cumuls - 1 && travel_slack(c_min) < br.duration.min) { + ++c_min; + } + if (!IncreaseMin(visit_end_min(c_min), &br.start, &result) || + !IncreaseMin(CapAdd(br.start.min, br.duration.min), &br.end, + &result)) { + return kInfeasible; + } + // The min break duration should fit, but in some cases (interbreaks) we + // may have br.start.min + br.duration.min < br.end.min. + // If br.end.min - br.start.min > travel_slack: + // - either the break is during the travel c_min -> c_min+1, + // in this case its duration is at most travel_slack, + // so that br.end.min - br.start <= travel_slack. + // Some of the travel c_min -> c_min+1 may be performed after the break. + // - or the break is after c_min+1, and all of the travel c_min -> c_min+1 + // is performed before the break: br.start >= cumul[c_min] + travel. + // We have to take the weaker of the two alternatives, the first one. + if (c_min < num_cumuls - 1 && + !IncreaseMin(CapSub(br.end.min, travel_slack(c_min)), &br.start, + &result)) { + return kInfeasible; + } + // Visit c_min must be before the break. + const int64_t cumul_ub = CapSub(br.start.max, post_visits[c_min]); + if (cumuls[c_min].min > cumul_ub) return kInfeasible; + delayed_propagations_.push_back( + {.value = cumul_ub, .index = c_min, .is_min = false}); + } + // Find smallest c_max such that time windows prevent break br to start + // after cumul c_max. In that case, all visits including and after c_max + // must be performed after the break. + int c_max = BinarySearch( + num_cumuls, -1, + [&visit_end_min, break_start_max = br.start.max](int c) { + return break_start_max < visit_end_min(c); + }); + if (c_max < num_cumuls) { + while (c_max > 0 && travel_slack(c_max - 1) < br.duration.min) --c_max; + if (!DecreaseMax(visit_start_max(c_max), &br.end, &result) || + !DecreaseMax(CapSub(br.end.max, br.duration.min), &br.start, + &result)) { + return kInfeasible; + } + // See the comment on the symmetric situation above. + if (c_max > 0 && + !DecreaseMax(CapAdd(br.start.max, travel_slack(c_max - 1)), &br.end, + &result)) { + return kInfeasible; + } + // Visit c_max must be after the break, delay to not break BinarySearch. + const int64_t cumul_lb = CapAdd(br.end.min, pre_visits[c_max]); + if (cumuls[c_max].max < cumul_lb) return kInfeasible; + delayed_propagations_.push_back( + {.value = cumul_lb, .index = c_max, .is_min = true}); + } + // If the break must be inside the route, it must be inside some cumul + // window, here [c_min, c_max]. + // Overload checking: if transit + break duration do not fit, the break is + // infeasible. + // Edge finding: if it fits, push cumuls c_min/c_max to leave enough room. + if (0 <= c_min && c_max < num_cumuls) { + const int64_t transit = CapAdd( + br.duration.min, CapSub(travel_sums[c_max], travel_sums[c_min])); + if (CapAdd(cumuls[c_min].min, transit) > cumuls[c_max].max) { + return kInfeasible; + } + delayed_propagations_.push_back( + {.value = CapAdd(cumuls[c_min].min, transit), + .index = c_max, + .is_min = true}); + delayed_propagations_.push_back( + {.value = CapSub(cumuls[c_max].max, transit), + .index = c_min, + .is_min = false}); + breaks_window_min = std::min(breaks_window_min, c_min); + breaks_window_max = std::max(breaks_window_max, c_max); + CapAddTo(br.duration.min, &breaks_total_duration); + // If this break is forced on the transition c_min -> c_min + 1, + // accumulate its duration to this transition. + if (num_breaks > 1 && c_min + 1 == c_max) { + const int64_t total_duration = break_duration_on_transition_.Get(c_min); + break_duration_on_transition_.Set( + c_min, CapAdd(total_duration, br.duration.min)); + } + } + } + // After the previous loop, there are no BinarySearch() calls, so there is + // no need to delay propagations. + + // Per-transition reasoning: total break duration + travel must fit. + for (const int t : break_duration_on_transition_.ChangedIndices()) { + const int64_t total = + CapAdd(travels[t], break_duration_on_transition_.Get(t)); + if (!IncreaseMin(CapAdd(cumuls[t].min, total), &cumuls[t + 1], &result) || + !DecreaseMax(CapSub(cumuls[t + 1].max, total), &cumuls[t], &result)) { + return kInfeasible; + } + } + // Overload checker reasoning on overall break window. + if (breaks_total_duration > 0) { + const int64_t window_transit = CapAdd( + breaks_total_duration, + CapSub(travel_sums[breaks_window_max], travel_sums[breaks_window_min])); + if (!IncreaseMin(CapAdd(cumuls[breaks_window_min].min, window_transit), + &cumuls[breaks_window_max], &result) || + !DecreaseMax(CapSub(cumuls[breaks_window_max].max, window_transit), + &cumuls[breaks_window_min], &result)) { + return kInfeasible; + } + } + for (const auto& [value, index, is_min] : delayed_propagations_) { + if (is_min && !IncreaseMin(value, &cumuls[index], &result)) { + return kInfeasible; + } + if (!is_min && !DecreaseMax(value, &cumuls[index], &result)) { + return kInfeasible; + } + } + return result; +} + +// Add interbreak reasoning. +BreakPropagator::PropagationResult BreakPropagator::PropagateInterbreak( + int path, DimensionValues& dimension, + absl::Span> interbreaks) { + PropagationResult result = kUnchanged; + absl::Span cumuls = dimension.MutableCumuls(path); + std::vector& vehicle_breaks = + dimension.MutableVehicleBreaks(path); + // We use fake breaks for start/end of path: + // - start break: [kint64min, cumul[0]) + // - end break: [cumul[n-1], kint64max). + const int64_t kint64min = std::numeric_limits::min(); + const int64_t kint64max = std::numeric_limits::max(); + vehicle_breaks.push_back({.start = {kint64min, kint64min}, + .end = cumuls.front(), + .duration = {0, kint64max}, + .is_performed = {1, 1}}); + vehicle_breaks.push_back({.start = cumuls.back(), + .end = {kint64max, kint64max}, + .duration = {0, kint64max}, + .is_performed = {1, 1}}); + const int num_breaks = vehicle_breaks.size(); + for (const auto [limit, min_break_duration] : interbreaks) { + // Generate and sort events by increasing time. Events have to be + // regenerated for each interbreak, because end events depend on the limit. + usage_events_.clear(); + for (int i = 0; i < num_breaks; ++i) { + const auto& br = vehicle_breaks[i]; + if (br.is_performed.max == 0 || br.duration.max < min_break_duration) { + continue; + } + usage_events_.push_back( + {.time = br.start.min, .index = i, .is_start = true}); + usage_events_.push_back( + {.time = CapAdd(br.end.max, limit), .index = i, .is_start = false}); + } + std::sort(usage_events_.begin(), usage_events_.end()); + // Main loop: sweep over events, maintain max profile height. + // When sweeping over time, we cross some time intervals of duration > 0: + // - if profile height is 0, no break can cover the interval. Infeasible. + // - if profile height is 1, the only active break must cover the interval. + // When num_active_breaks == 1, the xor of all active breaks is the only + // active break. + int num_active_breaks = 0; + int xor_active_breaks = 0; + int64_t previous_time = kint64min; + for (const UsageEvent& event : usage_events_) { + if (event.time != previous_time) { + DCHECK_GT(event.time, previous_time); + // Time changed: check covering condition. + if (num_active_breaks == 0) return kInfeasible; + if (num_active_breaks == 1) { + VehicleBreak& br = vehicle_breaks[xor_active_breaks]; + const int64_t new_start_max = + std::min(previous_time, CapSub(br.end.max, min_break_duration)); + const int64_t new_end_min = + std::max(CapSub(event.time, limit), + CapAdd(br.start.min, min_break_duration)); + if (!DecreaseMax(new_start_max, &br.start, &result) || + !IncreaseMin(new_end_min, &br.end, &result)) { + return kInfeasible; + } + if (xor_active_breaks < num_breaks - 2) { + const int64_t new_duration_min = std::max( + min_break_duration, CapSub(new_end_min, new_start_max)); + if (!IncreaseMin(1, &br.is_performed, &result) || + !IncreaseMin(new_duration_min, &br.duration, &result)) { + return kInfeasible; + } + } + } + } + // Update the set of active intervals. + num_active_breaks += event.is_start ? 1 : -1; + xor_active_breaks ^= event.index; + previous_time = event.time; + } + // Propagate fake start/end information to actual start/end. + const Interval& new_start = vehicle_breaks[num_breaks - 2].end; + const Interval& new_end = vehicle_breaks[num_breaks - 1].start; + if (!IntersectWith(new_start, &cumuls.front(), &result) || + !IntersectWith(new_end, &cumuls.back(), &result)) { + vehicle_breaks.resize(num_breaks - 2); + return kInfeasible; + } + } + // Remove fake path start/end breaks. + vehicle_breaks.resize(num_breaks - 2); + return result; +} + +} // namespace operations_research::routing diff --git a/ortools/routing/breaks.h b/ortools/routing/breaks.h new file mode 100644 index 00000000000..8331572c1fc --- /dev/null +++ b/ortools/routing/breaks.h @@ -0,0 +1,110 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef OR_TOOLS_ROUTING_BREAKS_H_ +#define OR_TOOLS_ROUTING_BREAKS_H_ + +#include +#include +#include + +#include "absl/types/span.h" +#include "ortools/routing/filter_committables.h" + +namespace operations_research::routing { + +class BreakPropagator { + public: + explicit BreakPropagator(int num_nodes); + + // Result of a propagation: kInfeasible means some infeasibility was found, + // kChanged means that the propagation tightened the bounds of some intervals, + // kUnchanged means that the propagation did not change anything. + enum class PropagationResult { kInfeasible, kChanged, kUnchanged }; + // TODO(user): when the OSS version is at C++20, replace this by + // using enum PropagationResult; + static constexpr PropagationResult kInfeasible = + PropagationResult::kInfeasible; + static constexpr PropagationResult kChanged = PropagationResult::kChanged; + static constexpr PropagationResult kUnchanged = PropagationResult::kUnchanged; + + // Applies fast propagations, O(log |path|) per break, to the given path. + PropagationResult FastPropagations(int path, + DimensionValues& dimension_values, + const PrePostVisitValues& visits); + // Propagates interbreak rules on a given path, with a covering reasoning. + // Each interbreak is a pair (interbreak_limit, min_break_duration). + PropagationResult PropagateInterbreak( + int path, DimensionValues& dimension, + absl::Span> interbreaks); + + private: + using Interval = DimensionValues::Interval; + using VehicleBreak = DimensionValues::VehicleBreak; + + static bool IncreaseMin(int64_t new_min, Interval* interval, + PropagationResult* propagation_result) { + if (interval->min >= new_min) return true; + if (!interval->IncreaseMin(new_min)) { + *propagation_result = kInfeasible; + return false; + } + *propagation_result = kChanged; + return true; + } + static bool DecreaseMax(int64_t new_max, Interval* interval, + PropagationResult* propagation_result) { + if (interval->max <= new_max) return true; + if (!interval->DecreaseMax(new_max)) { + *propagation_result = kInfeasible; + return false; + } + *propagation_result = kChanged; + return true; + } + static bool IntersectWith(Interval source, Interval* target, + PropagationResult* propagation_result) { + if (!source.IntersectWith(*target)) { + *propagation_result = kInfeasible; + } else if (source != *target) { + *propagation_result = kChanged; + } + *target = source; + return *propagation_result != kInfeasible; + } + // In cases where propagators expect some property of variables to hold, + // for instance "cumuls[i].min should be weakly increasing in i", + // it is necessary to delay modification of the variables until after all + // propagations are done. + // This struct can be used to store such delayed propagations. + struct DelayedPropagation { + int64_t value; // New bound of the variable. + int index; // Some information on which variable to modify. + bool is_min; // The bound is a min if this is true, otherwise a max. + }; + std::vector delayed_propagations_; + // Events used in PropagateInterbreak(). + struct UsageEvent { + int64_t time; + int index; + bool is_start; + bool operator<(const UsageEvent& other) const { return time < other.time; } + }; + std::vector usage_events_; + // Per-transition reasoning. + CommittableArray break_duration_on_transition_; +}; + +} // namespace operations_research::routing + +#endif // OR_TOOLS_ROUTING_BREAKS_H_ diff --git a/ortools/constraint_solver/routing_constraints.cc b/ortools/routing/constraints.cc similarity index 77% rename from ortools/constraint_solver/routing_constraints.cc rename to ortools/routing/constraints.cc index 656732a38d8..da13afb0b45 100644 --- a/ortools/constraint_solver/routing_constraints.cc +++ b/ortools/routing/constraints.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_constraints.h" +#include "ortools/routing/constraints.h" #include #include @@ -23,16 +23,21 @@ #include #include "absl/container/flat_hash_set.h" +#include "absl/functional/any_invocable.h" #include "absl/log/check.h" +#include "absl/types/span.h" #include "ortools/base/strong_vector.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_lp_scheduling.h" -#include "ortools/constraint_solver/routing_search.h" +#include "ortools/routing/breaks.h" +#include "ortools/routing/filter_committables.h" +#include "ortools/routing/filters.h" +#include "ortools/routing/lp_scheduling.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/search.h" #include "ortools/util/saturated_arithmetic.h" -namespace operations_research { +namespace operations_research::routing { namespace { // Constraint which ensures that var != values. @@ -795,4 +800,236 @@ Constraint* MakeRouteConstraint( model, std::move(route_cost_vars), std::move(route_evaluator))); } -} // namespace operations_research +namespace { + +/// GlobalVehicleBreaksConstraint ensures breaks constraints are enforced on +/// all vehicles in the dimension passed to its constructor. +/// It is intended to be used for dimensions representing time. +/// A break constraint ensures break intervals fit on the route of a vehicle. +/// For a given vehicle, it forces break intervals to be disjoint from visit +/// intervals, where visit intervals start at CumulVar(node) and last for +/// node_visit_transit[node]. Moreover, it ensures that there is enough time +/// between two consecutive nodes of a route to do transit and vehicle breaks, +/// i.e. if Next(nodeA) = nodeB, CumulVar(nodeA) = tA and CumulVar(nodeB) = tB, +/// then SlackVar(nodeA) >= sum_{breaks \subseteq [tA, tB)} duration(break). +class GlobalVehicleBreaksConstraint : public Constraint { + public: + explicit GlobalVehicleBreaksConstraint(const RoutingDimension* dimension); + std::string DebugString() const override { + return "GlobalVehicleBreaksConstraint"; + } + + void Post() override; + void InitialPropagate() override; + + private: + void PropagateNode(int node); + void PropagateVehicle(int vehicle); + + const RoutingModel* model_; + const RoutingDimension* const dimension_; + std::vector vehicle_demons_; + + DimensionValues dimension_values_; + PrePostVisitValues visits_; + std::vector cumul_intervals_; + std::vector slack_intervals_; + BreakPropagator break_propagator_; +}; + +GlobalVehicleBreaksConstraint::GlobalVehicleBreaksConstraint( + const RoutingDimension* dimension) + : Constraint(dimension->model()->solver()), + model_(dimension->model()), + dimension_(dimension), + dimension_values_(dimension->model()->vehicles(), + dimension->cumuls().size()), + visits_(dimension->model()->vehicles(), dimension->cumuls().size()), + cumul_intervals_(dimension->cumuls().size()), + slack_intervals_(dimension->cumuls().size()), + break_propagator_(dimension->cumuls().size()) { + vehicle_demons_.resize(model_->vehicles()); +} + +void GlobalVehicleBreaksConstraint::Post() { + for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { + if (dimension_->GetBreakIntervalsOfVehicle(vehicle).empty() && + dimension_->GetBreakDistanceDurationOfVehicle(vehicle).empty()) { + continue; + } + vehicle_demons_[vehicle] = MakeDelayedConstraintDemon1( + solver(), this, &GlobalVehicleBreaksConstraint::PropagateVehicle, + "PropagateVehicle", vehicle); + for (IntervalVar* interval : + dimension_->GetBreakIntervalsOfVehicle(vehicle)) { + interval->WhenAnything(vehicle_demons_[vehicle]); + } + } + const int num_cumuls = dimension_->cumuls().size(); + const int num_nexts = model_->Nexts().size(); + for (int node = 0; node < num_cumuls; node++) { + Demon* dimension_demon = MakeConstraintDemon1( + solver(), this, &GlobalVehicleBreaksConstraint::PropagateNode, + "PropagateNode", node); + if (node < num_nexts) { + model_->NextVar(node)->WhenBound(dimension_demon); + dimension_->SlackVar(node)->WhenRange(dimension_demon); + } + model_->VehicleVar(node)->WhenBound(dimension_demon); + dimension_->CumulVar(node)->WhenRange(dimension_demon); + } +} + +void GlobalVehicleBreaksConstraint::InitialPropagate() { + for (int vehicle = 0; vehicle < model_->vehicles(); vehicle++) { + if (!dimension_->GetBreakIntervalsOfVehicle(vehicle).empty() || + !dimension_->GetBreakDistanceDurationOfVehicle(vehicle).empty()) { + PropagateVehicle(vehicle); + } + } +} + +// This dispatches node events to the right vehicle propagator. +// It also filters out a part of uninteresting events, on which the vehicle +// propagator will not find anything new. +void GlobalVehicleBreaksConstraint::PropagateNode(int node) { + if (!model_->VehicleVar(node)->Bound()) return; + const int vehicle = model_->VehicleVar(node)->Min(); + if (vehicle < 0 || vehicle_demons_[vehicle] == nullptr) return; + EnqueueDelayedDemon(vehicle_demons_[vehicle]); +} + +// First, perform energy-based reasoning on intervals and cumul variables. +// Then, perform reasoning on slack variables. +void GlobalVehicleBreaksConstraint::PropagateVehicle(int vehicle) { + dimension_values_.Revert(); + visits_.Revert(); + + // Fill dimension_values_ from the path. + // If the path is not a complete start -> end, return. + // This leverages travel caching in FillDimensionValuesFromRoutingDimension(). + int node = model_->Start(vehicle); + while (!model_->IsEnd(node)) { + dimension_values_.PushNode(node); + if (model_->NextVar(node)->Bound()) { + node = model_->NextVar(node)->Min(); + } else { + return; + } + } + dimension_values_.PushNode(node); + dimension_values_.MakePathFromNewNodes(vehicle); + // Translate CP variables to Intervals, and fill dimension_values_. + const auto& cp_cumuls = dimension_->cumuls(); + const auto& cp_slacks = dimension_->slacks(); + for (const int node : dimension_values_.Nodes(vehicle)) { + cumul_intervals_[node] = {.min = cp_cumuls[node]->Min(), + .max = cp_cumuls[node]->Max()}; + if (dimension_->model()->IsEnd(node)) { + slack_intervals_[node] = {.min = 0, .max = 0}; + } else { + slack_intervals_[node] = {.min = cp_slacks[node]->Min(), + .max = cp_slacks[node]->Max()}; + } + } + if (!FillDimensionValuesFromRoutingDimension( + vehicle, dimension_->vehicle_capacities()[vehicle], + dimension_->vehicle_span_upper_bounds()[vehicle], cumul_intervals_, + slack_intervals_, dimension_->transit_evaluator(vehicle), + dimension_values_)) { + solver()->Fail(); + } + if (!PropagateTransitAndSpan(vehicle, dimension_values_)) { + solver()->Fail(); + } + // Extract pre/post visit data. + auto any_invocable = [this](int evaluator_index) + -> std::optional> { + const auto& evaluator = + evaluator_index == -1 + ? nullptr + : dimension_->model()->TransitCallback(evaluator_index); + if (evaluator == nullptr) return std::nullopt; + return evaluator; + }; + FillPrePostVisitValues( + vehicle, dimension_values_, + any_invocable(dimension_->GetPreTravelEvaluatorOfVehicle(vehicle)), + any_invocable(dimension_->GetPostTravelEvaluatorOfVehicle(vehicle)), + visits_); + // Copy break data into dimension_values_. + using VehicleBreak = DimensionValues::VehicleBreak; + const std::vector& cp_breaks = + dimension_->GetBreakIntervalsOfVehicle(vehicle); + std::vector& dv_breaks = + dimension_values_.MutableVehicleBreaks(vehicle); + dv_breaks.clear(); + for (const IntervalVar* cp_break : cp_breaks) { + if (cp_break->MayBePerformed()) { + dv_breaks.push_back( + {.start = {.min = cp_break->StartMin(), .max = cp_break->StartMax()}, + .end = {.min = cp_break->EndMin(), .max = cp_break->EndMax()}, + .duration = {.min = cp_break->DurationMin(), + .max = cp_break->DurationMax()}, + .is_performed = {.min = cp_break->MustBePerformed(), .max = 1}}); + } else { + dv_breaks.push_back({.start = {.min = 0, .max = 0}, + .end = {.min = 0, .max = 0}, + .duration = {.min = 0, .max = 0}, + .is_performed = {.min = 0, .max = 0}}); + } + } + // Propagate inside dimension_values_, fail if infeasible. + if (break_propagator_.FastPropagations(vehicle, dimension_values_, visits_) == + BreakPropagator::kInfeasible) { + solver()->Fail(); + } + const auto& interbreaks = + dimension_->GetBreakDistanceDurationOfVehicle(vehicle); + if (break_propagator_.PropagateInterbreak(vehicle, dimension_values_, + interbreaks) == + BreakPropagator::kInfeasible) { + solver()->Fail(); + } + if (!PropagateTransitAndSpan(vehicle, dimension_values_)) { + solver()->Fail(); + } + // Copy changes back to CP variables. + using Interval = DimensionValues::Interval; + const int num_nodes = dimension_values_.NumNodes(vehicle); + const absl::Span nodes = dimension_values_.Nodes(vehicle); + const absl::Span dv_cumuls = + dimension_values_.Cumuls(vehicle); + for (int r = 0; r < num_nodes; ++r) { + const int node = nodes[r]; + cp_cumuls[node]->SetRange(dv_cumuls[r].min, dv_cumuls[r].max); + } + const int num_breaks = cp_breaks.size(); + for (int b = 0; b < num_breaks; ++b) { + IntervalVar* cp_break = cp_breaks[b]; + if (!cp_break->MayBePerformed()) continue; + const VehicleBreak& dv_break = dv_breaks[b]; + cp_break->SetStartRange(dv_break.start.min, dv_break.start.max); + cp_break->SetEndRange(dv_break.end.min, dv_break.end.max); + cp_break->SetDurationRange(dv_break.duration.min, dv_break.duration.max); + if (dv_break.is_performed.min == 1) { + cp_break->SetPerformed(true); + } else if (dv_break.is_performed.max == 0) { + cp_break->SetPerformed(false); + } + } + // If everything went fine, we can save dimension state. + // Saving is only done for caching reasons, this allows subsequent calls to + // FillDimensionValuesFromRoutingDimension() to re-use travel evaluations. + dimension_values_.Commit(); + visits_.Commit(); +} + +} // namespace + +Constraint* MakeGlobalVehicleBreaksConstraint( + Solver* solver, const RoutingDimension* dimension) { + return solver->RevAlloc(new GlobalVehicleBreaksConstraint(dimension)); +} + +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_constraints.h b/ortools/routing/constraints.h similarity index 84% rename from ortools/constraint_solver/routing_constraints.h rename to ortools/routing/constraints.h index 8258dbda81d..4d7e1759b8f 100644 --- a/ortools/constraint_solver/routing_constraints.h +++ b/ortools/routing/constraints.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_CONSTRAINTS_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_CONSTRAINTS_H_ +#ifndef OR_TOOLS_ROUTING_CONSTRAINTS_H_ +#define OR_TOOLS_ROUTING_CONSTRAINTS_H_ #include #include @@ -20,9 +20,9 @@ #include #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" +#include "ortools/routing/routing.h" -namespace operations_research { +namespace operations_research::routing { Constraint* MakeDifferentFromValues(Solver* solver, IntVar* var, std::vector values); @@ -49,6 +49,9 @@ Constraint* MakeRouteConstraint( std::function(const std::vector&)> route_evaluator); -} // namespace operations_research +Constraint* MakeGlobalVehicleBreaksConstraint( + Solver* solver, const RoutingDimension* dimension); -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_CONSTRAINTS_H_ +} // namespace operations_research::routing + +#endif // OR_TOOLS_ROUTING_CONSTRAINTS_H_ diff --git a/ortools/routing/csharp/CMakeLists.txt b/ortools/routing/csharp/CMakeLists.txt new file mode 100644 index 00000000000..17f28345098 --- /dev/null +++ b/ortools/routing/csharp/CMakeLists.txt @@ -0,0 +1,38 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set_property(SOURCE routing.i PROPERTY CPLUSPLUS ON) +set_property(SOURCE routing.i PROPERTY SWIG_MODULE_NAME RoutingGlobals) +set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE routing.i PROPERTY COMPILE_OPTIONS + -namespace ${DOTNET_PROJECT}.Routing + -dllimport google-ortools-native) +swig_add_library(dotnet_routing + TYPE OBJECT + LANGUAGE csharp + OUTPUT_DIR ${DOTNET_PROJECT_DIR}/ortools/routing + SOURCES routing.i) + +#target_include_directories(dotnet_routing PRIVATE ${DOTNET_INCLUDE_DIRS}) +set_target_properties(dotnet_routing PROPERTIES + SWIG_USE_TARGET_INCLUDE_DIRECTORIES ON + POSITION_INDEPENDENT_CODE ON) +target_link_libraries(dotnet_routing PRIVATE ortools::ortools) + +if(BUILD_TESTING) + file(GLOB DOTNET_SRCS "*Tests.cs") + foreach(FILE_NAME IN LISTS DOTNET_SRCS) + add_dotnet_test(FILE_NAME ${FILE_NAME}) + endforeach() +endif() diff --git a/ortools/constraint_solver/csharp/RoutingSolverTests.cs b/ortools/routing/csharp/RoutingSolverTests.cs similarity index 94% rename from ortools/constraint_solver/csharp/RoutingSolverTests.cs rename to ortools/routing/csharp/RoutingSolverTests.cs index 35f22991ad7..6311c4f83af 100644 --- a/ortools/constraint_solver/csharp/RoutingSolverTests.cs +++ b/ortools/routing/csharp/RoutingSolverTests.cs @@ -15,6 +15,7 @@ using System.Linq; using Xunit; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; namespace Google.OrTools.Tests { @@ -47,8 +48,7 @@ public void SimpleLambdaCallback(bool callGC) GC.Collect(); } // Setting first solution heuristic. - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; Assignment solution = routing.SolveWithParameters(searchParameters); // 0 --(+1)-> 1 --(+1)-> 2 --(+1)-> 3 --(+1)-> 4 --(+4)-> 0 := +8 @@ -73,8 +73,7 @@ public void TestTransitMatrix() // Define cost of each arc. routing.SetArcCostEvaluatorOfAllVehicles(transitCallbackIndex); // Setting first solution heuristic. - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; Assignment solution = routing.SolveWithParameters(searchParameters); // 0 --(+1)-> 1 --(+1)-> 2 --(+1)-> 3 --(+1)-> 4 --(+1)-> 0 := +5 @@ -102,8 +101,7 @@ public void TestTransitCallback() // Define cost of each arc. routing.SetArcCostEvaluatorOfAllVehicles(transitCallbackIndex); // Setting first solution heuristic. - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; Assignment solution = routing.SolveWithParameters(searchParameters); Assert.Equal(8, solution.ObjectiveValue()); @@ -129,8 +127,7 @@ public void TestMatrixDimension() // Define cost of each arc. routing.SetArcCostEvaluatorOfAllVehicles(result.first); // Setting first solution heuristic. - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; Assignment solution = routing.SolveWithParameters(searchParameters); // 0 --(+1)-> 1 --(+1)-> 2 --(+1)-> 3 --(+1)-> 4 --(+1)-> 0 := +5 @@ -152,8 +149,7 @@ public void TestUnaryTransitVector() // Define cost of each arc. routing.SetArcCostEvaluatorOfAllVehicles(transitCallbackIndex); // Setting first solution heuristic. - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; Assignment solution = routing.SolveWithParameters(searchParameters); // 0 --(+1)-> 1 --(+1)-> 2 --(+1)-> 3 --(+1)-> 4 --(+1)-> 0 := +5 @@ -181,8 +177,7 @@ public void TestUnaryTransitCallback() // Define cost of each arc. routing.SetArcCostEvaluatorOfAllVehicles(transitCallbackIndex); // Setting first solution heuristic. - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; Assignment solution = routing.SolveWithParameters(searchParameters); // 0 --(+1)-> 1 --(+2)-> 2 --(+3)-> 3 --(+4)-> 4 --(+5)-> 0 := +15 @@ -206,8 +201,7 @@ public void TestVectorDimension() // Define cost of each arc. routing.SetArcCostEvaluatorOfAllVehicles(result.first); // Setting first solution heuristic. - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; Assignment solution = routing.SolveWithParameters(searchParameters); // 0 --(+1)-> 1 --(+1)-> 2 --(+1)-> 3 --(+1)-> 4 --(+1)-> 0 := +5 diff --git a/ortools/constraint_solver/csharp/routing_index_manager.i b/ortools/routing/csharp/index_manager.i similarity index 76% rename from ortools/constraint_solver/csharp/routing_index_manager.i rename to ortools/routing/csharp/index_manager.i index d844e00b137..00b72b2c99f 100644 --- a/ortools/constraint_solver/csharp/routing_index_manager.i +++ b/ortools/routing/csharp/index_manager.i @@ -13,20 +13,19 @@ // Wrapper for RoutingIndexManager. -%include "ortools/constraint_solver/csharp/routing_types.i" +%include "ortools/routing/csharp/types.i" %{ -#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/routing/index_manager.h" %} -DEFINE_INDEX_TYPE_TYPEDEF(operations_research::RoutingNodeIndex, - operations_research::RoutingIndexManager::NodeIndex); +DEFINE_INDEX_TYPE_TYPEDEF(operations_research::routing::RoutingNodeIndex, + operations_research::routing::RoutingIndexManager::NodeIndex); %ignoreall -%unignore operations_research; - -namespace operations_research { +%unignore operations_research::routing; +namespace operations_research::routing { %unignore RoutingIndexManager; %unignore RoutingIndexManager::GetStartIndex; @@ -46,8 +45,8 @@ namespace operations_research { %rename (GetNumberOfIndices) RoutingIndexManager::num_indices; %unignore RoutingIndexManager::~RoutingIndexManager; -} // namespace operations_research +} // namespace operations_research::routing -%include "ortools/constraint_solver/routing_index_manager.h" +%include "ortools/routing/index_manager.h" %unignoreall diff --git a/ortools/constraint_solver/csharp/routing.i b/ortools/routing/csharp/routing.i similarity index 66% rename from ortools/constraint_solver/csharp/routing.i rename to ortools/routing/csharp/routing.i index cc46159dfc2..bcbcb606ea6 100644 --- a/ortools/constraint_solver/csharp/routing.i +++ b/ortools/routing/csharp/routing.i @@ -22,54 +22,73 @@ using System.Collections.Generic; %include "ortools/base/base.i" %template(IntBoolPair) std::pair; -%include "ortools/constraint_solver/csharp/constraint_solver.i" +%include "enumsimple.swg" +%import "ortools/util/csharp/absl_string_view.i" +%import "ortools/util/csharp/vector.i" + +%{ +#include +%} + +%template(IntVector) std::vector; +%template(IntVectorVector) std::vector >; +VECTOR_AS_CSHARP_ARRAY(int, int, int, IntVector); +JAGGED_MATRIX_AS_CSHARP_ARRAY(int, int, int, IntVectorVector); + +%template(Int64Vector) std::vector; +%template(Int64VectorVector) std::vector >; +VECTOR_AS_CSHARP_ARRAY(int64_t, int64_t, long, Int64Vector); +JAGGED_MATRIX_AS_CSHARP_ARRAY(int64_t, int64_t, long, Int64VectorVector); + +%import "ortools/constraint_solver/csharp/constraint_solver.i" %import "ortools/util/csharp/sorted_interval_list.i" // Domain -%include "ortools/constraint_solver/csharp/routing_index_manager.i" +%include "ortools/routing/csharp/index_manager.i" // We need to forward-declare the proto here, so that PROTO_INPUT involving it // works correctly. The order matters very much: this declaration needs to be // before the %{ #include ".../routing.h" %}. -namespace operations_research { +namespace operations_research::routing { class RoutingModelParameters; class RoutingSearchParameters; class RoutingSearchStatus; -} // namespace operations_research +} // namespace operations_research::routing + +%module(directors="1") RoutingGlobals; // Include the file we want to wrap a first time. %{ -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_types.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" %} -%module(directors="1") operations_research; - // RoutingModel methods. DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingCostClassIndex, - operations_research::RoutingModel::CostClassIndex); + operations_research::routing::RoutingCostClassIndex, + operations_research::routing::RoutingModel::CostClassIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingDimensionIndex, - operations_research::RoutingModel::DimensionIndex); + operations_research::routing::RoutingDimensionIndex, + operations_research::routing::RoutingModel::DimensionIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingDisjunctionIndex, - operations_research::RoutingModel::DisjunctionIndex); + operations_research::routing::RoutingDisjunctionIndex, + operations_research::routing::RoutingModel::DisjunctionIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingVehicleClassIndex, - operations_research::RoutingModel::VehicleClassIndex); + operations_research::routing::RoutingVehicleClassIndex, + operations_research::routing::RoutingModel::VehicleClassIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingResourceClassIndex, - operations_research::RoutingModel::ResourceClassIndex); + operations_research::routing::RoutingResourceClassIndex, + operations_research::routing::RoutingModel::ResourceClassIndex); -namespace operations_research { +namespace operations_research::routing { // GlobalVehicleBreaksConstraint %unignore GlobalVehicleBreaksConstraint; %typemap(csimports) GlobalVehicleBreaksConstraint %{ +using Google.OrTools.ConstraintSolver; %} // PathsMetadata @@ -80,6 +99,7 @@ namespace operations_research { %typemap(csimports) RoutingDimension %{ using System; using System.Collections.Generic; +using Google.OrTools.ConstraintSolver; %} %typemap(cscode) RoutingDimension %{ // Keep reference to delegate to avoid GC to collect them early. @@ -106,6 +126,7 @@ using System.Collections.Generic; %typemap(csimports) RoutingModel %{ using System; using System.Collections.Generic; +using Google.OrTools.ConstraintSolver; using Domain = Google.OrTools.Util.Domain; %} %typemap(cscode) RoutingModel %{ @@ -179,6 +200,7 @@ using Domain = Google.OrTools.Util.Domain; // RoutingModelVisitor %unignore RoutingModelVisitor; %typemap(csimports) RoutingModelVisitor %{ +using Google.OrTools.ConstraintSolver; %} // SimpleBoundCosts @@ -190,32 +212,45 @@ using Domain = Google.OrTools.Util.Domain; // TypeRegulationsConstraint %unignore TypeRegulationsConstraint; %typemap(csimports) TypeRegulationsConstraint %{ +using Google.OrTools.ConstraintSolver; %} // TypeRegulationsChecker %unignore TypeRegulationsChecker; %ignore TypeRegulationsChecker::CheckVehicle; -} // namespace operations_research +} // namespace operations_research::routing %rename("%(camelcase)s", %$isfunction) ""; +// Add needed import to RoutingGlobalsPINVOKE.cs +%pragma(csharp) imclassimports=%{ +// Types from ConstraintSolver +using Google.OrTools.ConstraintSolver; +%} + // Protobuf support -PROTO_INPUT(operations_research::RoutingSearchParameters, - Google.OrTools.ConstraintSolver.RoutingSearchParameters, +PROTO_INPUT(operations_research::routing::RoutingSearchParameters, + Google.OrTools.Routing.RoutingSearchParameters, search_parameters) -PROTO_INPUT(operations_research::RoutingModelParameters, - Google.OrTools.ConstraintSolver.RoutingModelParameters, +PROTO_INPUT(operations_research::routing::RoutingModelParameters, + Google.OrTools.Routing.RoutingModelParameters, parameters) -PROTO2_RETURN(operations_research::RoutingSearchParameters, - Google.OrTools.ConstraintSolver.RoutingSearchParameters) -PROTO2_RETURN(operations_research::RoutingModelParameters, - Google.OrTools.ConstraintSolver.RoutingModelParameters) -PROTO_ENUM_RETURN(operations_research::RoutingSearchStatus::Value, - Google.OrTools.ConstraintSolver.RoutingSearchStatus.Types.Value) +PROTO2_RETURN(operations_research::routing::RoutingSearchParameters, + Google.OrTools.Routing.RoutingSearchParameters) +PROTO2_RETURN(operations_research::routing::RoutingModelParameters, + Google.OrTools.Routing.RoutingModelParameters) +PROTO_ENUM_RETURN(operations_research::routing::RoutingSearchStatus::Value, + Google.OrTools.Routing.RoutingSearchStatus.Types.Value) + +// Add needed import to RoutingGlobals.cs +%pragma(csharp) moduleimports=%{ +// Types from ConstraintSolver +using Google.OrTools.ConstraintSolver; +%} // Wrap routing includes // TODO(user): Replace with %ignoreall/%unignoreall //swiglint: disable include-h-allglobals -%include "ortools/constraint_solver/routing_parameters.h" -%include "ortools/constraint_solver/routing.h" +%include "ortools/routing/parameters.h" +%include "ortools/routing/routing.h" diff --git a/ortools/constraint_solver/csharp/routing_types.i b/ortools/routing/csharp/types.i similarity index 77% rename from ortools/constraint_solver/csharp/routing_types.i rename to ortools/routing/csharp/types.i index 4964d288753..b2cf7b7f310 100644 --- a/ortools/constraint_solver/csharp/routing_types.i +++ b/ortools/routing/csharp/types.i @@ -17,11 +17,10 @@ // int). // This file is to be %included when wrapped objects need to use these typemaps. -%include "ortools/base/base.i" %import "ortools/util/csharp/vector.i" %{ -#include "ortools/constraint_solver/routing_types.h" +#include "ortools/routing/types.h" %} // This macro defines typemaps for IndexT, std::vector and @@ -71,13 +70,23 @@ JAGGED_MATRIX_AS_CSHARP_ARRAY(IndexT, int, int, IntVectorVector); %apply const std::vector >& { const std::vector >& }; %enddef // DEFINE_INDEX_TYPE_TYPEDEF -DEFINE_INDEX_TYPE(operations_research::RoutingNodeIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingCostClassIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingDimensionIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingDisjunctionIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingVehicleClassIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingResourceClassIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingNodeIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingCostClassIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingDimensionIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingDisjunctionIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingVehicleClassIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingResourceClassIndex); -// TODO(user): Replace with %ignoreall/%unignoreall -//swiglint: disable include-h-allglobals -%include "ortools/constraint_solver/routing_types.h" +%ignoreall + +%unignore operations_research::routing; +namespace operations_research::routing { + +// PickupDeliveryPair +%unignore PickupDeliveryPair; + +} // namespace operations_research::routing + +%include "ortools/routing/types.h" + +%unignoreall diff --git a/ortools/constraint_solver/routing_decision_builders.cc b/ortools/routing/decision_builders.cc similarity index 97% rename from ortools/constraint_solver/routing_decision_builders.cc rename to ortools/routing/decision_builders.cc index c83e614176a..65d2a170134 100644 --- a/ortools/constraint_solver/routing_decision_builders.cc +++ b/ortools/routing/decision_builders.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_decision_builders.h" +#include "ortools/routing/decision_builders.h" #include #include @@ -29,11 +29,11 @@ #include "ortools/base/map_util.h" #include "ortools/base/strong_vector.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_lp_scheduling.h" +#include "ortools/routing/lp_scheduling.h" +#include "ortools/routing/routing.h" #include "ortools/util/saturated_arithmetic.h" -namespace operations_research { +namespace operations_research::routing { namespace { @@ -913,16 +913,6 @@ void FinalizerVariables::AddWeightedVariableTarget(IntVar* var, int64_t target, } } -void FinalizerVariables::AddWeightedVariableToMinimize(IntVar* var, - int64_t cost) { - AddWeightedVariableTarget(var, std::numeric_limits::min(), cost); -} - -void FinalizerVariables::AddWeightedVariableToMaximize(IntVar* var, - int64_t cost) { - AddWeightedVariableTarget(var, std::numeric_limits::max(), cost); -} - void FinalizerVariables::AddVariableTarget(IntVar* var, int64_t target) { CHECK(var != nullptr); if (finalizer_variable_target_set_.contains(var)) return; @@ -930,14 +920,6 @@ void FinalizerVariables::AddVariableTarget(IntVar* var, int64_t target) { finalizer_variable_targets_.push_back({var, target}); } -void FinalizerVariables::AddVariableToMaximize(IntVar* var) { - AddVariableTarget(var, std::numeric_limits::max()); -} - -void FinalizerVariables::AddVariableToMinimize(IntVar* var) { - AddVariableTarget(var, std::numeric_limits::min()); -} - DecisionBuilder* FinalizerVariables::CreateFinalizer() { std::stable_sort(weighted_finalizer_variable_targets_.begin(), weighted_finalizer_variable_targets_.end(), @@ -963,4 +945,4 @@ DecisionBuilder* FinalizerVariables::CreateFinalizer() { std::move(targets)); } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_decision_builders.h b/ortools/routing/decision_builders.h similarity index 75% rename from ortools/constraint_solver/routing_decision_builders.h rename to ortools/routing/decision_builders.h index 00d84b4594b..8cefd903931 100644 --- a/ortools/constraint_solver/routing_decision_builders.h +++ b/ortools/routing/decision_builders.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_DECISION_BUILDERS_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_DECISION_BUILDERS_H_ +#ifndef OR_TOOLS_ROUTING_DECISION_BUILDERS_H_ +#define OR_TOOLS_ROUTING_DECISION_BUILDERS_H_ #include #include @@ -21,10 +21,10 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_lp_scheduling.h" +#include "ortools/routing/lp_scheduling.h" +#include "ortools/routing/routing.h" -namespace operations_research { +namespace operations_research::routing { /// A decision builder which tries to assign values to variables as close as /// possible to target values first. @@ -70,27 +70,18 @@ DecisionBuilder* MakeRestoreDimensionValuesForUnchangedRoutes( class FinalizerVariables { public: explicit FinalizerVariables(Solver* solver) : solver_(solver) {} - /// Adds a variable to minimize in the solution finalizer. The solution - /// finalizer is called each time a solution is found during the search and - /// allows to instantiate secondary variables (such as dimension cumul - /// variables). - void AddVariableToMinimize(IntVar* var); - /// Adds a variable to maximize in the solution finalizer (see above for - /// information on the solution finalizer). - void AddVariableToMaximize(IntVar* var); - /// Adds a variable to minimize in the solution finalizer, with a weighted - /// priority: the higher the more priority it has. - void AddWeightedVariableToMinimize(IntVar* var, int64_t cost); - /// Adds a variable to maximize in the solution finalizer, with a weighted - /// priority: the higher the more priority it has. - void AddWeightedVariableToMaximize(IntVar* var, int64_t cost); /// Add a variable to set the closest possible to the target value in the - /// solution finalizer. + /// solution finalizer. The solution finalizer is called each time a solution + /// is found during the search and allows to instantiate secondary variables + /// (such as dimension cumul variables). void AddVariableTarget(IntVar* var, int64_t target); /// Same as above with a weighted priority: the higher the cost, the more /// priority it has to be set close to the target value. void AddWeightedVariableTarget(IntVar* var, int64_t target, int64_t cost); - /// + /// Returns a DecisionBuilder* that sets the variables passed through + /// AddVariableTarget and AddWeightedVariableTarget towards their target, + /// setting weigthed variables by decreasing weight first, then unweighted + /// variables in the order they were added. DecisionBuilder* CreateFinalizer(); private: @@ -108,5 +99,5 @@ class FinalizerVariables { #endif }; -} // namespace operations_research -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_DECISION_BUILDERS_H_ +} // namespace operations_research::routing +#endif // OR_TOOLS_ROUTING_DECISION_BUILDERS_H_ diff --git a/ortools/constraint_solver/docs/PDP.md b/ortools/routing/docs/PDP.md similarity index 100% rename from ortools/constraint_solver/docs/PDP.md rename to ortools/routing/docs/PDP.md diff --git a/ortools/routing/docs/README.md b/ortools/routing/docs/README.md new file mode 100644 index 00000000000..7a870fc9409 --- /dev/null +++ b/ortools/routing/docs/README.md @@ -0,0 +1,10 @@ +# Overview + +You can find here the documentation for the following OR-Tools component. + +* [Routing Solver](ROUTING.md) + + A specialized library for identifying best vehicle routes given constraints. + + This library extension is implemented on top of the + [CP Solver library](../../constraint_solver/docs/README.md). diff --git a/ortools/constraint_solver/docs/ROUTING.md b/ortools/routing/docs/ROUTING.md similarity index 70% rename from ortools/constraint_solver/docs/ROUTING.md rename to ortools/routing/docs/ROUTING.md index 3517b169f19..4f517df89f7 100644 --- a/ortools/constraint_solver/docs/ROUTING.md +++ b/ortools/routing/docs/ROUTING.md @@ -19,17 +19,23 @@ and .Net. Each language have different requirements for the code samples. ### C++ code samples ```cpp +// Snippet from ortools/routing/samples/simple_routing_program.cc #include #include #include #include -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/base/init_google.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" -namespace operations_research { +namespace operations_research::routing { void SimpleRoutingProgram() { // Instantiate the data problem. @@ -79,10 +85,12 @@ void SimpleRoutingProgram() { LOG(INFO) << "Distance of the route: " << route_distance << "m"; } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::SimpleRoutingProgram(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::SimpleRoutingProgram(); return EXIT_SUCCESS; } ``` @@ -91,79 +99,83 @@ int main(int /*argc*/, char* /*argv*/[]) { ```python #!/usr/bin/env python3 +# Snippet from ortools/routing/samples/simple_routing_program.py """Vehicle Routing example.""" -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting def main(): - """Entry point of the program.""" - # Instantiate the data problem. - num_locations = 5 - num_vehicles = 1 - depot = 0 - - # Create the routing index manager. - manager = pywrapcp.RoutingIndexManager(num_locations, num_vehicles, depot) - - # Create Routing Model. - routing = pywrapcp.RoutingModel(manager) - - # Create and register a transit callback. - def distance_callback(from_index, to_index): - """Returns the absolute difference between the two nodes.""" - # Convert from routing variable Index to user NodeIndex. - from_node = int(manager.IndexToNode(from_index)) - to_node = int(manager.IndexToNode(to_index)) - return abs(to_node - from_node) - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - - # Define cost of each arc. - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - - # Setting first solution heuristic. - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) # pylint: disable=no-member - - # Solve the problem. - assignment = routing.SolveWithParameters(search_parameters) - - # Print solution on console. - print(f"Objective: {assignment.ObjectiveValue()}") - index = routing.Start(0) - plan_output = "Route for vehicle 0:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f"{manager.IndexToNode(index)} -> " - previous_index = index - index = assignment.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += f"{manager.IndexToNode(index)}\n" - plan_output += f"Distance of the route: {route_distance}m\n" - print(plan_output) + """Entry point of the program.""" + # Instantiate the data problem. + num_locations = 5 + num_vehicles = 1 + depot = 0 + + # Create the routing index manager. + manager = pywraprouting.RoutingIndexManager( + num_locations, num_vehicles, depot + ) + + # Create Routing Model. + routing = pywraprouting.RoutingModel(manager) + + # Create and register a transit callback. + def distance_callback(from_index, to_index): + """Returns the absolute difference between the two nodes.""" + # Convert from routing variable Index to user NodeIndex. + from_node = int(manager.IndexToNode(from_index)) + to_node = int(manager.IndexToNode(to_index)) + return abs(to_node - from_node) + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + + # Define cost of each arc. + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + + # Setting first solution heuristic. + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) # pylint: disable=no-member + + # Solve the problem. + assignment = routing.SolveWithParameters(search_parameters) + + # Print solution on console. + print(f"Objective: {assignment.ObjectiveValue()}") + index = routing.Start(0) + plan_output = "Route for vehicle 0:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f"{manager.IndexToNode(index)} -> " + previous_index = index + index = assignment.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) + plan_output += f"{manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) if __name__ == "__main__": - main() + main() ``` ### Java code samples ```java -package com.google.ortools.constraintsolver.samples; +// Snippet from ortools/routing/samples/SimpleRoutingProgram.java +package com.google.ortools.routing.samples; import static java.lang.Math.abs; import com.google.ortools.Loader; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.RoutingSearchParameters; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; import java.util.logging.Logger; /** Minimal Routing example to showcase calling the solver.*/ @@ -197,7 +209,7 @@ public class SimpleRoutingProgram { // Setting first solution heuristic. RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); @@ -227,9 +239,11 @@ public class SimpleRoutingProgram { ### .Net code samples -```cs +```csharp +// Snippet from ortools/routing/samples/SimpleRoutingProgram.cs using System; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; ///

/// This is a sample using the routing library .Net wrapper. @@ -263,8 +277,7 @@ public class SimpleRoutingProgram routing.SetArcCostEvaluatorOfAllVehicles(transitCallbackIndex); // Setting first solution heuristic. - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // Solve the problem. diff --git a/ortools/constraint_solver/docs/TSP.md b/ortools/routing/docs/TSP.md similarity index 100% rename from ortools/constraint_solver/docs/TSP.md rename to ortools/routing/docs/TSP.md diff --git a/ortools/constraint_solver/docs/VRP.md b/ortools/routing/docs/VRP.md similarity index 100% rename from ortools/constraint_solver/docs/VRP.md rename to ortools/routing/docs/VRP.md diff --git a/ortools/constraint_solver/docs/generate_svg.sh b/ortools/routing/docs/generate_svg.sh similarity index 100% rename from ortools/constraint_solver/docs/generate_svg.sh rename to ortools/routing/docs/generate_svg.sh diff --git a/ortools/routing/docs/routing_svg.py b/ortools/routing/docs/routing_svg.py new file mode 100755 index 00000000000..064a109f8e2 --- /dev/null +++ b/ortools/routing/docs/routing_svg.py @@ -0,0 +1,1253 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Generate SVG for a Routing problem.""" + +# [START import] +import argparse +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy +RoutingSearchStatus = enums_pb2.RoutingSearchStatus +# [END import] + + +# [START data_model] +class DataModel: # pylint: disable=too-many-instance-attributes + """Stores the data for the problem.""" + + def __init__(self, args): + # Locations in block units + locations = [ + (4, 4), # depot + (2, 0), + (8, 0), # locations to visit + (0, 1), + (1, 1), + (5, 2), + (7, 2), + (3, 3), + (6, 3), + (5, 5), + (8, 5), + (1, 6), + (2, 6), + (3, 7), + (6, 7), + (0, 8), + (7, 8), + ] + # Convert locations in meters using a city block dimension of 114m x 80m. + self._locations = [(l[0] * 114, l[1] * 80) for l in locations] + self._distance_matrix = [ + [ + 0, + 548, + 776, + 696, + 582, + 274, + 502, + 194, + 308, + 194, + 536, + 502, + 388, + 354, + 468, + 776, + 662, + ], + [ + 548, + 0, + 684, + 308, + 194, + 502, + 730, + 354, + 696, + 742, + 1084, + 594, + 480, + 674, + 1016, + 868, + 1210, + ], + [ + 776, + 684, + 0, + 992, + 878, + 502, + 274, + 810, + 468, + 742, + 400, + 1278, + 1164, + 1130, + 788, + 1552, + 754, + ], + [ + 696, + 308, + 992, + 0, + 114, + 650, + 878, + 502, + 844, + 890, + 1232, + 514, + 628, + 822, + 1164, + 560, + 1358, + ], + [ + 582, + 194, + 878, + 114, + 0, + 536, + 764, + 388, + 730, + 776, + 1118, + 400, + 514, + 708, + 1050, + 674, + 1244, + ], + [ + 274, + 502, + 502, + 650, + 536, + 0, + 228, + 308, + 194, + 240, + 582, + 776, + 662, + 628, + 514, + 1050, + 708, + ], + [ + 502, + 730, + 274, + 878, + 764, + 228, + 0, + 536, + 194, + 468, + 354, + 1004, + 890, + 856, + 514, + 1278, + 480, + ], + [ + 194, + 354, + 810, + 502, + 388, + 308, + 536, + 0, + 342, + 388, + 730, + 468, + 354, + 320, + 662, + 742, + 856, + ], + [ + 308, + 696, + 468, + 844, + 730, + 194, + 194, + 342, + 0, + 274, + 388, + 810, + 696, + 662, + 320, + 1084, + 514, + ], + [ + 194, + 742, + 742, + 890, + 776, + 240, + 468, + 388, + 274, + 0, + 342, + 536, + 422, + 388, + 274, + 810, + 468, + ], + [ + 536, + 1084, + 400, + 1232, + 1118, + 582, + 354, + 730, + 388, + 342, + 0, + 878, + 764, + 730, + 388, + 1152, + 354, + ], + [ + 502, + 594, + 1278, + 514, + 400, + 776, + 1004, + 468, + 810, + 536, + 878, + 0, + 114, + 308, + 650, + 274, + 844, + ], + [ + 388, + 480, + 1164, + 628, + 514, + 662, + 890, + 354, + 696, + 422, + 764, + 114, + 0, + 194, + 536, + 388, + 730, + ], + [ + 354, + 674, + 1130, + 822, + 708, + 628, + 856, + 320, + 662, + 388, + 730, + 308, + 194, + 0, + 342, + 422, + 536, + ], + [ + 468, + 1016, + 788, + 1164, + 1050, + 514, + 514, + 662, + 320, + 274, + 388, + 650, + 536, + 342, + 0, + 764, + 194, + ], + [ + 776, + 868, + 1552, + 560, + 674, + 1050, + 1278, + 742, + 1084, + 810, + 1152, + 274, + 388, + 422, + 764, + 0, + 798, + ], + [ + 662, + 1210, + 754, + 1358, + 1244, + 708, + 480, + 856, + 514, + 468, + 354, + 844, + 730, + 536, + 194, + 798, + 0, + ], + ] + self._time_matrix = [ + [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], + [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], + [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], + [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], + [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], + [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], + [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], + [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], + [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], + [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], + [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], + [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], + [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], + [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], + [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], + [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], + [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], + ] + self._time_windows = [ + (0, 5), # depot + (7, 12), # 1 + (10, 15), # 2 + (5, 14), # 3 + (5, 13), # 4 + (0, 5), # 5 + (5, 10), # 6 + (0, 10), # 7 + (5, 10), # 8 + (0, 5), # 9 + (10, 16), # 10 + (10, 15), # 11 + (0, 5), # 12 + (5, 10), # 13 + (7, 12), # 14 + (10, 15), # 15 + (5, 15), # 16 + ] + if args["drop_nodes"]: + self._demands = [0, 1, 1, 3, 6, 3, 6, 8, 8, 1, 2, 1, 2, 6, 6, 8, 8] + else: + self._demands = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8] + self._pickups_deliveries = [ + [1, 6], + [2, 10], + [4, 3], + [5, 9], + [7, 8], + [15, 11], + [13, 12], + [16, 14], + ] + + if args["tsp"]: + self._num_vehicles = 1 + else: + self._num_vehicles = 4 + self._vehicle_capacities = [15, 15, 15, 15] + + if args["resources"]: + self._vehicle_load_time = 5 + self._vehicle_unload_time = 5 + + self._depot = 0 + self._depot_capacity = 2 + self._starts = [1, 2, 15, 16] + self._ends = [0, 0, 0, 0] + + @property + def locations(self): + """Gets the locations.""" + return self._locations + + @property + def distance_matrix(self): + """Gets the distance matrix.""" + return self._distance_matrix + + @property + def time_matrix(self): + """Gets the time matrix.""" + return self._time_matrix + + @property + def time_windows(self): + """Gets the time windows.""" + return self._time_windows + + @property + def demands(self): + """Gets the locations demands.""" + return self._demands + + @property + def pickups_deliveries(self): + """Gets the pickups deliveries.""" + return self._pickups_deliveries + + @property + def num_vehicles(self): + """Gets the number of vehicles.""" + return self._num_vehicles + + @property + def vehicle_capacities(self): + """Gets the capacity of each vehicles.""" + return self._vehicle_capacities + + @property + def vehicle_load_time(self): + """Gets the load time of each vehicles.""" + return self._vehicle_load_time + + @property + def vehicle_unload_time(self): + """Gets the unload time of each vehicles.""" + return self._vehicle_unload_time + + @property + def depot_capacity(self): + """Gets the depot capacity.""" + return self._depot_capacity + + @property + def depot(self): + """Gets the depot node index.""" + return self._depot + + @property + def starts(self): + """Gets the start nodes indices.""" + return self._starts + + @property + def ends(self): + """Gets the end nodes indices.""" + return self._ends + + # [END data_model] + + +########### +# Printer # +########### +class GoogleColorPalette: + """Google color codes palette.""" + + def __init__(self): + """Initialize Google ColorPalette.""" + self._colors = [ + ("blue", r"#4285F4"), + ("red", r"#EA4335"), + ("yellow", r"#FBBC05"), + ("green", r"#34A853"), + ("black", r"#101010"), + ("white", r"#FFFFFF"), + ] + + def __getitem__(self, key): + """Gets color name from idx.""" + return self._colors[key][0] + + def __len__(self): + """Gets the number of colors.""" + return len(self._colors) + + @property + def colors(self): + """Gets the colors list.""" + return self._colors + + def name(self, idx): + """Return color name from idx.""" + return self._colors[idx][0] + + def value(self, idx): + """Return color value from idx.""" + return self._colors[idx][1] + + def value_from_name(self, name): + """Return color value from name.""" + return dict(self._colors)[name] + + +class SVG: + """SVG draw primitives.""" + + @staticmethod + def header(size, margin): + """Writes header.""" + print( + r''.format( + width=size[0] + 2 * margin, + height=size[1] + 2 * margin, + margin=margin, + ) + ) + + @staticmethod + def definitions(colors): + """Writes definitions.""" + print( + r"" + ) + print(r"") + for color in colors: + print( + r' '.format(colorname=color[0]) + ) + print( + r' ' + .format(color=color[1]) + ) + print(r" ") + print(r"") + + @staticmethod + def footer(): + """Writes svg footer.""" + print(r"") + + @staticmethod + def draw_line(position_1, position_2, size, fg_color): + """Draws a line.""" + line_style = (r'style="stroke-width:{sz};stroke:{fg};fill:none"').format( + sz=size, fg=fg_color + ) + print( + r''.format( + x1=position_1[0], + y1=position_1[1], + x2=position_2[0], + y2=position_2[1], + style=line_style, + ) + ) + + @staticmethod + def draw_polyline(position_1, position_2, size, fg_color, colorname): + """Draws a line with arrow maker in the middle.""" + polyline_style = ( + r'style="stroke-width:{sz};stroke:{fg};fill:none;' + 'marker-mid:url(#arrow_{colorname})"' + ).format(sz=size, fg=fg_color, colorname=colorname) + print( + r''.format( + x1=position_1[0], + y1=position_1[1], + x2=(position_1[0] + position_2[0]) / 2, + y2=(position_1[1] + position_2[1]) / 2, + x3=position_2[0], + y3=position_2[1], + style=polyline_style, + ) + ) + + @staticmethod + def draw_circle(position, radius, size, fg_color, bg_color="white"): + """Print a circle.""" + circle_style = (r'style="stroke-width:{sz};stroke:{fg};fill:{bg}"').format( + sz=size, fg=fg_color, bg=bg_color + ) + print( + r''.format( + cx=position[0], cy=position[1], r=radius, style=circle_style + ) + ) + + @staticmethod + def draw_text(text, position, size, fg_color="none", bg_color="black"): + """Print a middle centred text.""" + text_style = ( + r'style="text-anchor:middle;font-weight:bold;' + 'font-size:{sz};stroke:{fg};fill:{bg}"' + ).format(sz=size, fg=fg_color, bg=bg_color) + print( + r'{txt}'.format( + x=position[0], + y=position[1], + dy=size / 3, + style=text_style, + txt=text, + ) + ) + + +class SVGPrinter: # pylint: disable=too-many-instance-attributes + """Generate Problem as svg file to stdout.""" + + # pylint: disable=too-many-arguments + def __init__(self, args, data, manager=None, routing=None, assignment=None): + """Initializes the printer.""" + self._args = args + self._data = data + self._manager = manager + self._routing = routing + self._assignment = assignment + # Design variables + self._color_palette = GoogleColorPalette() + self._svg = SVG() + # City block size 114mx80m + self._radius = min(114, 80) / 3 + self._stroke_width = self._radius / 4 + + @property + def data(self): + """Gets the Data Model.""" + return self._data + + @property + def manager(self): + """Gets the RoutingIndexManager.""" + return self._manager + + @property + def routing(self): + """Gets the Routing solver.""" + return self._routing + + @property + def assignment(self): + """Gets the assignment.""" + return self._assignment + + @property + def color_palette(self): + """Gets the color palette.""" + return self._color_palette + + @property + def svg(self): + """Gets the svg.""" + return self._svg + + def draw_grid(self): + """Draws the city grid.""" + print(r"") + color = "#969696" + # Horizontal streets + for i in range(9): + p_1 = [0, i * 80] + p_2 = [8 * 114, p_1[1]] + self._svg.draw_line(p_1, p_2, 2, color) + # Vertical streets + for i in range(9): + p_1 = [i * 114, 0] + p_2 = [p_1[0], 8 * 80] + self._svg.draw_line(p_1, p_2, 2, color) + + def draw_depot(self): + """Draws the depot.""" + print(r"") + color = self._color_palette.value_from_name("black") + loc = self._data.locations[self._data.depot] + self._svg.draw_circle(loc, self._radius, self._stroke_width, color, "white") + self._svg.draw_text(self._data.depot, loc, self._radius, "none", color) + + def draw_depots(self): + """Draws the depot.""" + print(r"") + # print starts + for vehicle_idx, start in enumerate(self._data.starts): + del vehicle_idx + color = self._color_palette.value_from_name("black") + # color = self._color_palette.value(vehicle_idx) + loc = self._data.locations[start] + self._svg.draw_circle( + loc, self._radius, self._stroke_width, color, "white" + ) + self._svg.draw_text(start, loc, self._radius, "none", color) + # print end + color = self._color_palette.value_from_name("black") + loc = self._data.locations[0] + self._svg.draw_circle(loc, self._radius, self._stroke_width, color, "white") + self._svg.draw_text(0, loc, self._radius, "none", color) + + def draw_locations(self): + """Draws all the locations but the depot.""" + print(r"") + color = self._color_palette.value_from_name("blue") + if not self._args["starts_ends"]: + for idx, loc in enumerate(self._data.locations): + if idx == self._data.depot: + continue + self._svg.draw_circle( + loc, self._radius, self._stroke_width, color, "white" + ) + self._svg.draw_text(idx, loc, self._radius, "none", color) + else: + for idx, loc in enumerate(self._data.locations): + if idx in self._data.starts + self._data.ends: + continue + self._svg.draw_circle( + loc, self._radius, self._stroke_width, color, "white" + ) + self._svg.draw_text(idx, loc, self._radius, "none", color) + + def draw_demands(self): + """Draws all the demands.""" + print(r"") + for idx, loc in enumerate(self._data.locations): + if idx == self._data.depot: + continue + demand = self._data.demands[idx] + position = [ + x + y for x, y in zip(loc, [self._radius * 1.2, self._radius * 1.1]) + ] + color = self._color_palette.value_from_name("red") + # color = self._color_palette.value(int(math.log(demand, 2))) + self._svg.draw_text(demand, position, self._radius, "none", color) + + def draw_pickups_deliveries(self): + """Draws all pickups deliveries.""" + print(r"") + colorname = "red" + color = self._color_palette.value_from_name(colorname) + for pickup_delivery in self._data.pickups_deliveries: + self._svg.draw_polyline( + self._data.locations[pickup_delivery[0]], + self._data.locations[pickup_delivery[1]], + self._stroke_width, + color, + colorname, + ) + + def draw_time_windows(self): + """Draws all the time windows.""" + print(r"") + for idx, loc in enumerate(self._data.locations): + if idx == self._data.depot: + continue + time_window = self._data.time_windows[idx] + position = [ + x + y for x, y in zip(loc, [self._radius * 0, -self._radius * 1.6]) + ] + color = self._color_palette.value_from_name("red") + self._svg.draw_text( + "[{t1},{t2}]".format(t1=time_window[0], t2=time_window[1]), + position, + self._radius * 0.75, + "white", + color, + ) + + ############## + ## ROUTES ## + ############## + + def draw_drop_nodes(self): + """Draws the dropped nodes.""" + print(r"") + if self._assignment is None: + print("") + # Display dropped nodes. + dropped_nodes = [] + for node in range(self._routing.Size()): + if self._routing.IsStart(node) or self._routing.IsEnd(node): + continue + if self._assignment.Value(self._routing.NextVar(node)) == node: + dropped_nodes.append(self._manager.IndexToNode(node)) + color = self._color_palette.value_from_name("black") + for node_idx in dropped_nodes: + loc = self._data.locations[node_idx] + self._svg.draw_circle( + loc, self._radius, self._stroke_width, color, "white" + ) + self._svg.draw_text(node_idx, loc, self._radius, "none", color) + + def routes(self): + """Creates the route list from the assignment.""" + if self._assignment is None: + print("") + return [] + routes = [] + for vehicle_id in range(self._data.num_vehicles): + index = self._routing.Start(vehicle_id) + route = [] + while not self._routing.IsEnd(index): + node_index = self._manager.IndexToNode(index) + route.append(node_index) + index = self._assignment.Value(self._routing.NextVar(index)) + node_index = self._manager.IndexToNode(index) + route.append(node_index) + routes.append(route) + return routes + + def draw_route(self, route, color, colorname): + """Draws a Route.""" + # First print route + previous_loc_idx = None + for loc_idx in route: + if previous_loc_idx is not None and previous_loc_idx != loc_idx: + self._svg.draw_polyline( + self._data.locations[previous_loc_idx], + self._data.locations[loc_idx], + self._stroke_width, + color, + colorname, + ) + previous_loc_idx = loc_idx + # Then print location along the route + for loc_idx in route: + if loc_idx != self._data.depot: + loc = self._data.locations[loc_idx] + self._svg.draw_circle( + loc, self._radius, self._stroke_width, color, "white" + ) + self._svg.draw_text(loc_idx, loc, self._radius, "none", color) + + def draw_routes(self): + """Draws the routes.""" + print(r"") + for route_idx, route in enumerate(self.routes()): + print(r"".format(idx=route_idx)) + color = self._color_palette.value(route_idx) + colorname = self._color_palette.name(route_idx) + self.draw_route(route, color, colorname) + + def tw_routes(self): + """Creates the route time window list from the assignment.""" + if self._assignment is None: + print("") + return [] + time_dimension = self._routing.GetDimensionOrDie("Time") + loc_routes = [] + tw_routes = [] + for vehicle_id in range(self._data.num_vehicles): + index = self._routing.Start(vehicle_id) + # index = self._assignment.Value(self._routing.NextVar(index)) + loc_route = [] + tw_route = [] + while True: + node_index = self._manager.IndexToNode(index) + loc_route.append(node_index) + time_var = time_dimension.CumulVar(index) + t_min = self._assignment.Min(time_var) + t_max = self._assignment.Max(time_var) + tw_route.append((t_min, t_max)) + if self._routing.IsEnd(index): + break + index = self._assignment.Value(self._routing.NextVar(index)) + loc_routes.append(loc_route) + tw_routes.append(tw_route) + return zip(loc_routes, tw_routes) + + def draw_tw_route(self, route_idx, locations, tw_route, color): + """Draws the time windows for a Route.""" + is_start = -1 + for loc_idx, time_window in zip(locations, tw_route): + loc = self._data.locations[loc_idx] + if loc_idx == 0: # special case for depot + position = [ + x + y + for x, y in zip( + loc, [self._radius * is_start, self._radius * (1.8 + route_idx)] + ) + ] + is_start = 1 + else: + position = [ + x + y for x, y in zip(loc, [self._radius * 0, self._radius * 1.8]) + ] + self._svg.draw_text( + "[{t_min}]".format(t_min=time_window[0]), + position, + self._radius * 0.75, + "white", + color, + ) + + def draw_tw_routes(self): + """Draws the time window routes.""" + print(r"") + for route_idx, loc_tw in enumerate(self.tw_routes()): + print(r"".format(route_idx)) + color = self._color_palette.value(route_idx) + self.draw_tw_route(route_idx, loc_tw[0], loc_tw[1], color) + + def print_to_console(self): + """Prints a full svg document on stdout.""" + margin = self._radius * 2 + 2 + size = [8 * 114, 8 * 80] + self._svg.header(size, margin) + self._svg.definitions(self._color_palette.colors) + self.draw_grid() + if not self._args["solution"]: + if self._args["pickup_delivery"]: + self.draw_pickups_deliveries() + self.draw_locations() + else: + self.draw_routes() + self.draw_drop_nodes() + if self._args["starts_ends"]: + self.draw_depots() + else: + self.draw_depot() + if self._args["capacity"]: + self.draw_demands() + if self._args["drop_nodes"]: + self.draw_demands() + if self._args["time_windows"] or self._args["resources"]: + self.draw_time_windows() + if (self._args["time_windows"] or self._args["resources"]) and self._args[ + "solution" + ]: + self.draw_tw_routes() + self._svg.footer() + + +######## +# Main # +######## +def main(): # pylint: disable=too-many-locals,too-many-branches + """Entry point of the program.""" + parser = argparse.ArgumentParser(description="Output VRP as svg image.") + parser.add_argument( + "-tsp", "--tsp", action="store_true", help="use 1 vehicle" + ) + parser.add_argument( + "-vrp", "--vrp", action="store_true", help="use 4 vehicle" + ) + parser.add_argument( + "-gs", + "--global-span", + action="store_true", + help="use global span constraints", + ) + parser.add_argument( + "-c", "--capacity", action="store_true", help="use capacity constraints" + ) + parser.add_argument( + "-r", "--resources", action="store_true", help="use resources constraints" + ) + parser.add_argument( + "-dn", + "--drop-nodes", + action="store_true", + help="allow drop nodes (disjuntion constraints)", + ) + parser.add_argument( + "-tw", + "--time-windows", + action="store_true", + help="use time-window constraints", + ) + parser.add_argument( + "-se", + "--starts-ends", + action="store_true", + help="use multiple starts & ends", + ) + parser.add_argument( + "-pd", + "--pickup-delivery", + action="store_true", + help="use pickup & delivery constraints", + ) + parser.add_argument( + "-fifo", + "--fifo", + action="store_true", + help="use pickup & delivery FIFO Policy", + ) + parser.add_argument( + "-lifo", + "--lifo", + action="store_true", + help="use pickup & delivery LIFO Policy", + ) + parser.add_argument( + "-s", "--solution", action="store_true", help="print solution" + ) + args = vars(parser.parse_args()) + + # Instantiate the data problem. + # [START data] + data = DataModel(args) + # [END data] + + if not args["solution"]: + # Print svg on cout + printer = SVGPrinter(args, data) + printer.print_to_console() + return 0 + + # Create the routing index manager. + # [START index_manager] + if args["starts_ends"]: + manager = pywraprouting.RoutingIndexManager( + len(data.locations), data.num_vehicles, data.starts, data.ends + ) + else: + manager = pywraprouting.RoutingIndexManager( + len(data.locations), data.num_vehicles, data.depot + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + + # [END routing_model] + + # Register distance callback + def distance_callback(from_index, to_index): + """Returns the manhattan distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data.distance_matrix[from_node][to_node] + + distance_callback_index = routing.RegisterTransitCallback(distance_callback) + + # Register time callback + def time_callback(from_index, to_index): + """Returns the manhattan distance travel time between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data.time_matrix[from_node][to_node] + + time_callback_index = routing.RegisterTransitCallback(time_callback) + + # Register demands callback + def demand_callback(from_index): + """Returns the demand of the node.""" + # Convert from routing variable Index to demands NodeIndex. + from_node = manager.IndexToNode(from_index) + return data.demands[from_node] + + demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback) + + if args["time_windows"] or args["resources"]: + routing.SetArcCostEvaluatorOfAllVehicles(time_callback_index) + else: + routing.SetArcCostEvaluatorOfAllVehicles(distance_callback_index) + + if args["global_span"] or args["pickup_delivery"]: + dimension_name = "Distance" + routing.AddDimension(distance_callback_index, 0, 3000, True, dimension_name) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + + if args["capacity"] or args["drop_nodes"]: + routing.AddDimensionWithVehicleCapacity( + demand_callback_index, 0, data.vehicle_capacities, True, "Capacity" + ) + + if args["drop_nodes"]: + # Allow to drop nodes. + penalty = 1000 + for node in range(1, len(data.locations)): + routing.AddDisjunction([manager.NodeToIndex(node)], penalty) + + if args["pickup_delivery"]: + dimension_name = "Distance" + routing.AddDimension(distance_callback_index, 0, 3000, True, dimension_name) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + for request in data.pickups_deliveries: + pickup_index = manager.NodeToIndex(request[0]) + delivery_index = manager.NodeToIndex(request[1]) + routing.AddPickupAndDelivery(pickup_index, delivery_index) + routing.solver().Add( + routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index) + ) + routing.solver().Add( + distance_dimension.CumulVar(pickup_index) + <= distance_dimension.CumulVar(delivery_index) + ) + if args["fifo"]: + routing.SetPickupAndDeliveryPolicyOfAllVehicles( + pywraprouting.RoutingModel.PICKUP_AND_DELIVERY_FIFO + ) + if args["lifo"]: + routing.SetPickupAndDeliveryPolicyOfAllVehicles( + pywraprouting.RoutingModel.PICKUP_AND_DELIVERY_LIFO + ) + + if args["starts_ends"]: + dimension_name = "Distance" + routing.AddDimension(distance_callback_index, 0, 2000, True, dimension_name) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + + time = "Time" + if args["time_windows"] or args["resources"]: + routing.AddDimension(time_callback_index, 30, 30, False, time) + time_dimension = routing.GetDimensionOrDie(time) + # Add time window constraints for each location except depot and 'copy' the + # slack var in the solution object (aka Assignment) to print it. + for location_idx, time_window in enumerate(data.time_windows): + if location_idx == 0: + continue + index = manager.NodeToIndex(location_idx) + time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) + routing.AddToAssignment(time_dimension.SlackVar(index)) + # Add time window constraints for each vehicle start node and 'copy' the + # slack var in the solution object (aka Assignment) to print it. + for vehicle_id in range(data.num_vehicles): + index = routing.Start(vehicle_id) + time_window = data.time_windows[0] + time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) + routing.AddToAssignment(time_dimension.SlackVar(index)) + + # Instantiate route start and end times to produce feasible times. + for vehicle_id in range(data.num_vehicles): + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.End(vehicle_id)) + ) + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.Start(vehicle_id)) + ) + + if args["resources"]: + # Add resource constraints at the depot. + time_dimension = routing.GetDimensionOrDie(time) + solver = routing.solver() + intervals = [] + for i in range(data.num_vehicles): + # Add loading time at start of routes + intervals.append( + solver.FixedDurationIntervalVar( + time_dimension.CumulVar(routing.Start(i)), + data.vehicle_load_time, + "depot_interval", + ) + ) + # Add unloading time at end of routes. + intervals.append( + solver.FixedDurationIntervalVar( + time_dimension.CumulVar(routing.End(i)), + data.vehicle_unload_time, + "depot_interval ", + ) + ) + + depot_usage = [1 for i in range(data.num_vehicles * 2)] + solver.AddConstraint( + solver.Cumulative(intervals, depot_usage, data.depot_capacity, "depot") + ) + + # Setting first solution heuristic (cheapest addition). + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + # pylint: disable=no-member + if not args["pickup_delivery"]: + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + else: + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION + ) + + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(2) + + # Solve the problem. + assignment = routing.SolveWithParameters(search_parameters) + # Print the solution. + printer = SVGPrinter(args, data, manager, routing, assignment) + printer.print_to_console() + return 0 + + +if __name__ == "__main__": + main() diff --git a/ortools/constraint_solver/docs/tsp.svg b/ortools/routing/docs/tsp.svg similarity index 100% rename from ortools/constraint_solver/docs/tsp.svg rename to ortools/routing/docs/tsp.svg diff --git a/ortools/constraint_solver/docs/tsp_distance_matrix.svg b/ortools/routing/docs/tsp_distance_matrix.svg similarity index 100% rename from ortools/constraint_solver/docs/tsp_distance_matrix.svg rename to ortools/routing/docs/tsp_distance_matrix.svg diff --git a/ortools/constraint_solver/docs/tsp_distance_matrix_solution.svg b/ortools/routing/docs/tsp_distance_matrix_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/tsp_distance_matrix_solution.svg rename to ortools/routing/docs/tsp_distance_matrix_solution.svg diff --git a/ortools/constraint_solver/docs/tsp_solution.svg b/ortools/routing/docs/tsp_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/tsp_solution.svg rename to ortools/routing/docs/tsp_solution.svg diff --git a/ortools/constraint_solver/docs/vrp.svg b/ortools/routing/docs/vrp.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp.svg rename to ortools/routing/docs/vrp.svg diff --git a/ortools/constraint_solver/docs/vrp_capacity.svg b/ortools/routing/docs/vrp_capacity.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_capacity.svg rename to ortools/routing/docs/vrp_capacity.svg diff --git a/ortools/constraint_solver/docs/vrp_capacity_solution.svg b/ortools/routing/docs/vrp_capacity_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_capacity_solution.svg rename to ortools/routing/docs/vrp_capacity_solution.svg diff --git a/ortools/constraint_solver/docs/vrp_drop_nodes.svg b/ortools/routing/docs/vrp_drop_nodes.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_drop_nodes.svg rename to ortools/routing/docs/vrp_drop_nodes.svg diff --git a/ortools/constraint_solver/docs/vrp_drop_nodes_solution.svg b/ortools/routing/docs/vrp_drop_nodes_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_drop_nodes_solution.svg rename to ortools/routing/docs/vrp_drop_nodes_solution.svg diff --git a/ortools/constraint_solver/docs/vrp_global_span.svg b/ortools/routing/docs/vrp_global_span.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_global_span.svg rename to ortools/routing/docs/vrp_global_span.svg diff --git a/ortools/constraint_solver/docs/vrp_global_span_solution.svg b/ortools/routing/docs/vrp_global_span_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_global_span_solution.svg rename to ortools/routing/docs/vrp_global_span_solution.svg diff --git a/ortools/constraint_solver/docs/vrp_pickup_delivery.svg b/ortools/routing/docs/vrp_pickup_delivery.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_pickup_delivery.svg rename to ortools/routing/docs/vrp_pickup_delivery.svg diff --git a/ortools/constraint_solver/docs/vrp_pickup_delivery_fifo_solution.svg b/ortools/routing/docs/vrp_pickup_delivery_fifo_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_pickup_delivery_fifo_solution.svg rename to ortools/routing/docs/vrp_pickup_delivery_fifo_solution.svg diff --git a/ortools/constraint_solver/docs/vrp_pickup_delivery_lifo_solution.svg b/ortools/routing/docs/vrp_pickup_delivery_lifo_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_pickup_delivery_lifo_solution.svg rename to ortools/routing/docs/vrp_pickup_delivery_lifo_solution.svg diff --git a/ortools/constraint_solver/docs/vrp_pickup_delivery_solution.svg b/ortools/routing/docs/vrp_pickup_delivery_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_pickup_delivery_solution.svg rename to ortools/routing/docs/vrp_pickup_delivery_solution.svg diff --git a/ortools/constraint_solver/docs/vrp_resources.svg b/ortools/routing/docs/vrp_resources.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_resources.svg rename to ortools/routing/docs/vrp_resources.svg diff --git a/ortools/constraint_solver/docs/vrp_resources_solution.svg b/ortools/routing/docs/vrp_resources_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_resources_solution.svg rename to ortools/routing/docs/vrp_resources_solution.svg diff --git a/ortools/constraint_solver/docs/vrp_solution.svg b/ortools/routing/docs/vrp_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_solution.svg rename to ortools/routing/docs/vrp_solution.svg diff --git a/ortools/constraint_solver/docs/vrp_starts_ends.svg b/ortools/routing/docs/vrp_starts_ends.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_starts_ends.svg rename to ortools/routing/docs/vrp_starts_ends.svg diff --git a/ortools/constraint_solver/docs/vrp_starts_ends_solution.svg b/ortools/routing/docs/vrp_starts_ends_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_starts_ends_solution.svg rename to ortools/routing/docs/vrp_starts_ends_solution.svg diff --git a/ortools/constraint_solver/docs/vrp_time_windows.svg b/ortools/routing/docs/vrp_time_windows.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_time_windows.svg rename to ortools/routing/docs/vrp_time_windows.svg diff --git a/ortools/constraint_solver/docs/vrp_time_windows_solution.svg b/ortools/routing/docs/vrp_time_windows_solution.svg similarity index 100% rename from ortools/constraint_solver/docs/vrp_time_windows_solution.svg rename to ortools/routing/docs/vrp_time_windows_solution.svg diff --git a/ortools/constraint_solver/routing_enums.proto b/ortools/routing/enums.proto similarity index 98% rename from ortools/constraint_solver/routing_enums.proto rename to ortools/routing/enums.proto index 2d67b4f8027..fcea3c85a01 100644 --- a/ortools/constraint_solver/routing_enums.proto +++ b/ortools/routing/enums.proto @@ -15,11 +15,11 @@ syntax = "proto3"; -option java_package = "com.google.ortools.constraintsolver"; +option java_package = "com.google.ortools.routing"; option java_multiple_files = true; -option csharp_namespace = "Google.OrTools.ConstraintSolver"; +option csharp_namespace = "Google.OrTools.Routing"; -package operations_research; +package operations_research.routing; // First solution strategies, used as starting point of local search. message FirstSolutionStrategy { diff --git a/ortools/routing/filter_committables.cc b/ortools/routing/filter_committables.cc new file mode 100644 index 00000000000..dee45e17876 --- /dev/null +++ b/ortools/routing/filter_committables.cc @@ -0,0 +1,73 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ortools/routing/filter_committables.h" + +#include "absl/log/check.h" +#include "absl/types/span.h" + +namespace operations_research::routing { + +bool PropagateTransitAndSpan(int path, DimensionValues& dimension_values) { + DCHECK_GT(dimension_values.NumNodes(path), 0); + using Interval = DimensionValues::Interval; + const absl::Span cumuls = dimension_values.MutableCumuls(path); + const absl::Span transits = dimension_values.Transits(path); + const int num_nodes = dimension_values.NumNodes(path); + const Interval span = dimension_values.Span(path); + // Span -> cumul front/back. + if (!cumuls.back().IntersectWith(cumuls[0] + span)) return false; + if (!cumuls[0].IntersectWith(cumuls.back() - span)) return false; + // Propagate from start to end. + Interval cumul = cumuls[0]; + for (int t = 0; t < num_nodes - 1; ++t) { + cumul.Add(transits[t]); + if (!cumul.IntersectWith(cumuls[t + 1])) return false; + cumuls[t + 1] = cumul; + } + // Propagate span to cumul front, then re-propagate from start to end + // as long as there are changes. + cumul = cumuls.back() - span; + for (int t = 0; t < num_nodes; ++t) { + if (!cumul.IntersectWith(cumuls[t])) return false; + if (cumul == cumuls[t]) break; + cumuls[t] = cumul; + if (t == num_nodes - 1) break; + cumul.Add(transits[t]); + } + // Propagate from end to start. + cumul = cumuls.back(); + for (int t = num_nodes - 2; t >= 0; --t) { + cumul.Subtract(transits[t]); + if (!cumul.IntersectWith(cumuls[t])) return false; + cumuls[t] = cumul; + } + // Propagate span to cumul back, then re-propagate from end to start + // as long as there are changes. + cumul = cumuls[0] + span; + for (int t = num_nodes - 1; t >= 0; --t) { + if (!cumul.IntersectWith(cumuls[t])) return false; + if (cumul == cumuls[t]) break; + cumuls[t] = cumul; + if (t == 0) break; + cumul.Subtract(transits[t - 1]); + } + // Cumul front/back -> span. + if (!dimension_values.MutableSpan(path).IntersectWith(cumuls.back() - + cumuls[0])) { + return false; + } + return true; +} + +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_filter_committables.h b/ortools/routing/filter_committables.h similarity index 95% rename from ortools/constraint_solver/routing_filter_committables.h rename to ortools/routing/filter_committables.h index 52490648089..fa193fcbd5f 100644 --- a/ortools/constraint_solver/routing_filter_committables.h +++ b/ortools/routing/filter_committables.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTER_COMMITTABLES_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTER_COMMITTABLES_H_ +#ifndef OR_TOOLS_ROUTING_FILTER_COMMITTABLES_H_ +#define OR_TOOLS_ROUTING_FILTER_COMMITTABLES_H_ #include #include @@ -25,7 +25,7 @@ #include "ortools/util/bitset.h" #include "ortools/util/saturated_arithmetic.h" -namespace operations_research { +namespace operations_research::routing { // A vector that allows to revert back to a previously committed state, // get the set of changed indices, and get current and committed values. @@ -254,22 +254,26 @@ class DimensionValues { max = std::min(max, other.max); return min <= max; } - // A set addition, with intervals: adds other.min to the min, other.max to - // the max, with CapAdd(). - void Add(const Interval& other) { + // Set addition of intervals: adds other.min to the min, other.max to the + // max, with CapAdd(). + Interval operator+(const Interval& other) const { DCHECK(!IsEmpty()); DCHECK(!other.IsEmpty()); - min = CapAdd(min, other.min); - max = CapAdd(max, other.max); + return {.min = CapAdd(min, other.min), .max = CapAdd(max, other.max)}; } - // A set subtraction, with intervals: subtracts other.max from the min, + // Set addition, with intervals: adds other.min to the min, other.max to + // the max, with CapAdd(). + void Add(const Interval& other) { *this = *this + other; } + // Set subtraction, with intervals: subtracts other.max from the min, // other.min from the max, with CapSub(). - void Subtract(const Interval& other) { + Interval operator-(const Interval& other) const { DCHECK(!IsEmpty()); DCHECK(!other.IsEmpty()); - min = CapSub(min, other.max); - max = CapSub(max, other.min); + return {.min = CapSub(min, other.max), .max = CapSub(max, other.min)}; } + // Set subtraction, with intervals: subtracts other.max from the min, + // other.min from the max, with CapSub(). + void Subtract(const Interval& other) { *this = *this - other; } // Returns an interval containing all integers: {kint64min, kint64max}. static Interval AllIntegers() { return {.min = std::numeric_limits::min(), @@ -505,6 +509,8 @@ class DimensionValues { CommittableValue num_elements_; }; +bool PropagateTransitAndSpan(int path, DimensionValues& dimension_values); + class PrePostVisitValues { public: PrePostVisitValues(int num_paths, int num_nodes) @@ -634,6 +640,6 @@ class PrePostVisitValues { CommittableValue num_elements_; }; -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTER_COMMITTABLES_H_ +#endif // OR_TOOLS_ROUTING_FILTER_COMMITTABLES_H_ diff --git a/ortools/constraint_solver/routing_filters.cc b/ortools/routing/filters.cc similarity index 94% rename from ortools/constraint_solver/routing_filters.cc rename to ortools/routing/filters.cc index 4338f1fe29d..90e9be3f438 100644 --- a/ortools/constraint_solver/routing_filters.cc +++ b/ortools/routing/filters.cc @@ -13,7 +13,7 @@ // Implementation of local search filters for routing models. -#include "ortools/constraint_solver/routing_filters.h" +#include "ortools/routing/filters.h" #include @@ -34,20 +34,23 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/flags/flag.h" +#include "absl/functional/any_invocable.h" #include "absl/log/check.h" +#include "absl/log/log.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" -#include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/strong_vector.h" #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_lp_scheduling.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_types.h" +#include "ortools/routing/breaks.h" +#include "ortools/routing/filter_committables.h" +#include "ortools/routing/lp_scheduling.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" #include "ortools/util/bitset.h" #include "ortools/util/piecewise_linear_function.h" #include "ortools/util/saturated_arithmetic.h" @@ -57,7 +60,7 @@ ABSL_FLAG(bool, routing_strong_debug_checks, false, "Run stronger checks in debug; these stronger tests might change " "the complexity of the code in particular."); -namespace operations_research { +namespace operations_research::routing { namespace { // Route constraint filter @@ -837,6 +840,135 @@ IntVarLocalSearchFilter* MakeVehicleAmortizedCostFilter( namespace { +// TODO(user): Make this filter use PathStates. +// TODO(user): Optimize the case where same vehicle groups are disjoint and +// deltas are not "splitting" the groups. +class SameVehicleCostFilter : public BasePathFilter { + public: + explicit SameVehicleCostFilter(const RoutingModel& model) + : BasePathFilter(model.Nexts(), model.Size() + model.vehicles(), + model.GetPathsMetadata()), + model_(model), + same_vehicle_costs_per_node_(model.Size()), + nodes_per_vehicle_(model.GetNumberOfSoftSameVehicleConstraints()), + new_nodes_per_vehicle_(model.GetNumberOfSoftSameVehicleConstraints()), + current_vehicle_per_node_(model.Size()), + current_cost_(0) { + for (int i = 0; i < model.GetNumberOfSoftSameVehicleConstraints(); ++i) { + for (int node : model.GetSoftSameVehicleIndices(i)) { + same_vehicle_costs_per_node_[node].push_back(i); + } + } + start_to_vehicle_.resize(Size(), -1); + for (int v = 0; v < model.vehicles(); v++) { + const int64_t start = model.Start(v); + start_to_vehicle_[start] = v; + } + } + int64_t GetSynchronizedObjectiveValue() const override { + return current_cost_; + } + int64_t GetAcceptedObjectiveValue() const override { + return lns_detected() ? 0 : delta_cost_; + } + std::string DebugString() const override { return "SameVehicleCostFilter"; } + + private: + bool InitializeAcceptPath() override { + delta_cost_ = current_cost_; + for (int same_vehicle_cost_index : touched_) { + new_nodes_per_vehicle_[same_vehicle_cost_index] = + nodes_per_vehicle_[same_vehicle_cost_index]; + } + touched_.clear(); + return true; + } + bool AcceptPath(int64_t path_start, int64_t chain_start, + int64_t chain_end) override { + const int64_t vehicle = start_to_vehicle_[path_start]; + DCHECK_GE(vehicle, 0); + if (chain_start == chain_end) return true; + for (int64_t node = GetNext(chain_start); node != chain_end; + node = GetNext(node)) { + if (vehicle == current_vehicle_per_node_[node]) continue; + for (int same_vehicle_cost_index : same_vehicle_costs_per_node_[node]) { + auto& new_nodes = new_nodes_per_vehicle_[same_vehicle_cost_index]; + new_nodes[vehicle]++; + new_nodes[current_vehicle_per_node_[node]]--; + if (new_nodes[current_vehicle_per_node_[node]] == 0) { + new_nodes.erase(current_vehicle_per_node_[node]); + } + touched_.insert(same_vehicle_cost_index); + } + } + return true; + } + bool FinalizeAcceptPath(int64_t /*objective_min*/, + int64_t objective_max) override { + for (int same_vehicle_cost_index : touched_) { + CapAddTo(CapSub(GetCost(same_vehicle_cost_index, new_nodes_per_vehicle_), + GetCost(same_vehicle_cost_index, nodes_per_vehicle_)), + &delta_cost_); + } + return delta_cost_ <= objective_max; + } + + void OnAfterSynchronizePaths() override { + current_cost_ = 0; + touched_.clear(); + for (int same_vehicle_cost_index = 0; + same_vehicle_cost_index < nodes_per_vehicle_.size(); + ++same_vehicle_cost_index) { + nodes_per_vehicle_[same_vehicle_cost_index].clear(); + touched_.insert(same_vehicle_cost_index); + } + current_vehicle_per_node_.assign(model_.Size(), -1); + for (int v = 0; v < model_.vehicles(); ++v) { + int64_t node = GetNext(model_.Start(v)); + DCHECK(model_.IsEnd(node) || IsVarSynced(node)); + while (!model_.IsEnd(node)) { + for (int same_vehicle_cost_index : same_vehicle_costs_per_node_[node]) { + nodes_per_vehicle_[same_vehicle_cost_index][v]++; + } + current_vehicle_per_node_[node] = v; + node = GetNext(node); + } + } + for (int same_vehicle_cost_index = 0; + same_vehicle_cost_index < nodes_per_vehicle_.size(); + ++same_vehicle_cost_index) { + CapAddTo(GetCost(same_vehicle_cost_index, nodes_per_vehicle_), + ¤t_cost_); + } + } + int64_t GetCost(int index, const std::vector>& + nodes_per_vehicle) const { + const int num_vehicles_used = nodes_per_vehicle[index].size(); + if (num_vehicles_used <= 1) return 0; + return CapProd(num_vehicles_used - 1, model_.GetSoftSameVehicleCost(index)); + } + + const RoutingModel& model_; + std::vector start_to_vehicle_; + std::vector> same_vehicle_costs_per_node_; + std::vector> nodes_per_vehicle_; + std::vector> new_nodes_per_vehicle_; + absl::flat_hash_set touched_; + std::vector current_vehicle_per_node_; + int64_t current_cost_; + int64_t delta_cost_; +}; + +} // namespace + +IntVarLocalSearchFilter* MakeSameVehicleCostFilter( + const RoutingModel& routing_model) { + return routing_model.solver()->RevAlloc( + new SameVehicleCostFilter(routing_model)); +} + +namespace { + class TypeRegulationsFilter : public BasePathFilter { public: explicit TypeRegulationsFilter(const RoutingModel& model); @@ -1187,29 +1319,31 @@ bool FillDimensionValuesFromRoutingDimension( // TODO(user): use committed values as a cache to avoid calling evaluators. void FillPrePostVisitValues( int path, const DimensionValues& dimension_values, - absl::AnyInvocable pre_travel_evaluator, - absl::AnyInvocable post_travel_evaluator, + std::optional> + pre_travel_evaluator, + std::optional> + post_travel_evaluator, PrePostVisitValues& visit_values) { const int num_nodes = dimension_values.NumNodes(path); visit_values.ChangePathSize(path, num_nodes); absl::Span pre_visits = visit_values.MutablePreVisits(path); absl::Span post_visits = visit_values.MutablePostVisits(path); absl::Span nodes = dimension_values.Nodes(path); - if (pre_travel_evaluator == nullptr) { - absl::c_fill(post_visits, 0); - } else { + if (pre_travel_evaluator.has_value()) { for (int i = 0; i < num_nodes - 1; ++i) { - post_visits[i] = pre_travel_evaluator(nodes[i], nodes[i + 1]); + post_visits[i] = (*pre_travel_evaluator)(nodes[i], nodes[i + 1]); } post_visits.back() = 0; - } - if (post_travel_evaluator == nullptr) { - absl::c_fill(pre_visits, 0); } else { + absl::c_fill(post_visits, 0); + } + if (post_travel_evaluator.has_value()) { pre_visits[0] = 0; for (int i = 1; i < num_nodes; ++i) { - pre_visits[i] = post_travel_evaluator(nodes[i - 1], nodes[i]); + pre_visits[i] = (*post_travel_evaluator)(nodes[i - 1], nodes[i]); } + } else { + absl::c_fill(pre_visits, 0); } } @@ -1364,10 +1498,7 @@ class PathCumulFilter : public BasePathFilter { bool FillDimensionValues(int path); // Propagates the transit constraint, cumul[r] + transit[r] == cumul[r+1], // in dimension_values_'s current path data. - bool PropagateTransits(int path); - // Propagates the transit constraint supposing that there are no forbidden - // intervals for cumuls. This is faster than considering the intervals. - bool PropagateTransitsWithoutForbiddenIntervals(int path); + bool PropagateTransitsAndSpans(int path); // Propagates both the transit constraint and cumul forbidden intervals. bool PropagateTransitsWithForbiddenIntervals(int path); // Propagates the span constraint, cumul[start] + span == cumul[end]. @@ -1498,6 +1629,9 @@ class PathCumulFilter : public BasePathFilter { // Data reflecting information on path variables for the committed and the // current solution. DimensionValues dimension_values_; + PrePostVisitValues visits_; + BreakPropagator break_propagator_; + // Maps each path to the sum of its path-only costs: span/slack cost, // soft cumul costs, soft span limits. CommittableArray cost_of_path_; @@ -1519,6 +1653,7 @@ class PathCumulFilter : public BasePathFilter { // This vector is empty if there are no precedences on the dimension_. const std::vector> node_index_to_precedences_; + absl::flat_hash_map, int64_t> precedence_offsets_; struct PathAndRank { int path = -1; int rank = -1; @@ -1697,6 +1832,8 @@ PathCumulFilter::PathCumulFilter(const RoutingModel& routing_model, })), dimension_values_(routing_model.vehicles(), dimension.cumuls().size()), + visits_(routing_model.vehicles(), dimension.cumuls().size()), + break_propagator_(dimension.cumuls().size()), cost_of_path_(NumPaths(), 0), synchronized_objective_value_(0), accepted_objective_value_(0), @@ -1711,6 +1848,15 @@ PathCumulFilter::PathCumulFilter(const RoutingModel& routing_model, filter_objective_cost_(filter_objective_cost), may_use_optimizers_(may_use_optimizers), propagate_own_objective_value_(propagate_own_objective_value) { + for (int node = 0; node < node_index_to_precedences_.size(); ++node) { + for (const auto [first_node, second_node, offset, + unused_performed_constraint] : + node_index_to_precedences_[node]) { + int64_t& current_offset = gtl::LookupOrInsert( + &precedence_offsets_, {first_node, second_node}, offset); + current_offset = std::max(current_offset, offset); + } + } #ifndef NDEBUG for (int vehicle = 0; vehicle < routing_model.vehicles(); vehicle++) { if (FilterWithDimensionCumulOptimizerForVehicle(vehicle)) { @@ -1721,11 +1867,12 @@ PathCumulFilter::PathCumulFilter(const RoutingModel& routing_model, #endif // NDEBUG } -bool PathCumulFilter::PropagateTransits(int path) { +bool PathCumulFilter::PropagateTransitsAndSpans(int path) { if (has_forbidden_intervals_) { - return PropagateTransitsWithForbiddenIntervals(path); + return PropagateSpan(path) && + PropagateTransitsWithForbiddenIntervals(path) && PropagateSpan(path); } else { - return PropagateTransitsWithoutForbiddenIntervals(path); + return PropagateTransitAndSpan(path, dimension_values_); } } @@ -1764,27 +1911,6 @@ bool PathCumulFilter::PropagateTransitsWithForbiddenIntervals(int path) { return true; } -bool PathCumulFilter::PropagateTransitsWithoutForbiddenIntervals(int path) { - DCHECK_LT(0, dimension_values_.NumNodes(path)); - absl::Span cumuls = dimension_values_.MutableCumuls(path); - absl::Span transits = dimension_values_.Transits(path); - const int num_nodes = dimension_values_.NumNodes(path); - // Propagate from start to end. - Interval cumul = cumuls.front(); - for (int r = 1; r < num_nodes; ++r) { - cumul.Add(transits[r - 1]); - if (!cumul.IntersectWith(cumuls[r])) return false; - cumuls[r] = cumul; - } - // Propagate from end to start. - for (int r = num_nodes - 2; r >= 0; --r) { - cumul.Subtract(transits[r]); - if (!cumul.IntersectWith(cumuls[r])) return false; - cumuls[r] = cumul; - } - return true; -} - bool PathCumulFilter::PropagateSpan(int path) { absl::Span travel_sums = dimension_values_.TravelSums(path); absl::Span cumuls = dimension_values_.MutableCumuls(path); @@ -1902,19 +2028,58 @@ bool PathCumulFilter::AcceptPath(int64_t path_start, int64_t /*chain_start*/, // Filter feasibility: cumul windows, transit, span, breaks. // The first PropagateSpan() is mostly used to check feasibility of total // travel within span max, the second can tighten all start/end/span bounds. - if (!PropagateSpan(path) || !PropagateTransits(path) || - !PropagateSpan(path)) { - return false; - } + if (!PropagateTransitsAndSpans(path)) return false; if (dimension_.HasPickupToDeliveryLimits()) { if (!PropagatePickupToDeliveryLimits(path)) return false; // Re-propagate span and transits. - if (!PropagateSpan(path) || !PropagateTransits(path) || - !PropagateSpan(path)) { + if (!PropagateTransitsAndSpans(path)) return false; + } + if (FilterVehicleBreaks(path)) { + // TODO(user) using enum BreakPropagator::PropagationResult once C++20 + // support is available in OSS. + const auto& interbreaks = + dimension_.GetBreakDistanceDurationOfVehicle(path); + if (!PropagateVehicleBreaks(path) || + break_propagator_.PropagateInterbreak(path, dimension_values_, + interbreaks) == + BreakPropagator::PropagationResult::kInfeasible || + !PropagateTransitsAndSpans(path)) { return false; } + // Fill pre/post travel data. + visits_.Revert(); + auto any_invocable = [this](int evaluator_index) + -> std::optional> { + const auto& evaluator = + evaluator_index == -1 + ? nullptr + : dimension_.model()->TransitCallback(evaluator_index); + if (evaluator == nullptr) return std::nullopt; + return evaluator; + }; + FillPrePostVisitValues( + path, dimension_values_, + any_invocable(dimension_.GetPreTravelEvaluatorOfVehicle(path)), + any_invocable(dimension_.GetPostTravelEvaluatorOfVehicle(path)), + visits_); + // Loop until there are no changes, capped at a small number of iterations. + BreakPropagator::PropagationResult result = BreakPropagator::kChanged; + int num_iterations = 2; + while (result == BreakPropagator::kChanged && num_iterations-- > 0) { + result = + break_propagator_.FastPropagations(path, dimension_values_, visits_); + if (result == BreakPropagator::kChanged) { + if (!PropagateVehicleBreaks(path) || + break_propagator_.PropagateInterbreak(path, dimension_values_, + interbreaks) == + BreakPropagator::PropagationResult::kInfeasible || + !PropagateTransitsAndSpans(path)) { + return false; + } + } + } + if (result == BreakPropagator::kInfeasible) return false; } - if (FilterVehicleBreaks(path) && !PropagateVehicleBreaks(path)) return false; // Filter costs: span (linear/quadratic/piecewise), // soft cumul windows (linear/piecewise). @@ -1989,6 +2154,28 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, int64_t objective_max) { if (lns_detected()) return true; if (FilterPrecedences()) { + // Fast pass on consecutive nodes of changed paths, useful when the number + // of precedences is much larger than the number of nodes. + // TODO(user): Remove this when we have a better way to express + // precedence chains, which does not require a quadratic number of + // precedences. + for (const int path : dimension_values_.ChangedPaths()) { + const absl::Span travel_sums = + dimension_values_.TravelSums(path); + int prev = -1; + int rank = -1; + for (const int node : dimension_values_.Nodes(path)) { + int64_t offset = std::numeric_limits::min(); + // Check the "opposite" precedence constraint. + if (gtl::FindCopy(precedence_offsets_, std::pair{node, prev}, + &offset) && + CapSub(travel_sums[rank], travel_sums[rank + 1]) < offset) { + return false; + } + prev = node; + ++rank; + } + } // Find location of all nodes: remove committed nodes of changed paths, // then add nodes of changed paths. This removes nodes that became loops. for (const int path : dimension_values_.ChangedPaths()) { @@ -2028,6 +2215,17 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, DCHECK(node == first_node || node == second_node); DCHECK_EQ(first_node, dimension_values_.Nodes(path1)[rank1]); DCHECK_EQ(second_node, dimension_values_.Nodes(path2)[rank2]); + // Check the compatibility between the precedence and the implicit + // precedence induced from the route sequence. + if (path1 == path2 && rank2 < rank1) { + absl::Span travel_sums = + dimension_values_.TravelSums(path); + // Check that travel(second_node, first_node) <= -offset, + // (equivalent to -travel(second_node, first_node) >= offset). + if (CapSub(travel_sums[rank2], travel_sums[rank1]) < offset) { + return false; + } + } // Check that cumul1 + offset <= cumul2 is feasible. if (CapAdd(dimension_values_.Cumuls(path1)[rank1].min, offset) > dimension_values_.Cumuls(path2)[rank2].max) @@ -2098,9 +2296,7 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, path_accessor_, /*resource=*/nullptr, filter_objective_cost_ ? &path_cost_with_lp : nullptr); solve_duration_shares--; - if (status == DimensionSchedulingStatus::INFEASIBLE) { - return false; - } + if (status == DimensionSchedulingStatus::INFEASIBLE) return false; // Replace previous path cost with the LP optimizer cost. if (filter_objective_cost_ && (status == DimensionSchedulingStatus::OPTIMAL || @@ -2664,7 +2860,7 @@ bool PickupDeliveryFilter::AcceptPathOrdered(int path) { LocalSearchFilter* MakePickupDeliveryFilter( const RoutingModel& routing_model, const PathState* path_state, - const std::vector& pairs, + absl::Span pairs, const std::vector& vehicle_policies) { return routing_model.solver()->RevAlloc( @@ -4815,4 +5011,4 @@ LocalSearchFilter* MakePathEnergyCostFilter( // TODO(user): Implement same-vehicle filter. Could be merged with node // precedence filter. -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_filters.h b/ortools/routing/filters.h similarity index 97% rename from ortools/constraint_solver/routing_filters.h rename to ortools/routing/filters.h index dad6520347c..7d27414feae 100644 --- a/ortools/constraint_solver/routing_filters.h +++ b/ortools/routing/filters.h @@ -11,30 +11,32 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTERS_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTERS_H_ +#ifndef OR_TOOLS_ROUTING_FILTERS_H_ +#define OR_TOOLS_ROUTING_FILTERS_H_ #include #include #include #include +#include #include #include +#include "absl/functional/any_invocable.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_filter_committables.h" -#include "ortools/constraint_solver/routing_lp_scheduling.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_types.h" +#include "ortools/routing/filter_committables.h" +#include "ortools/routing/lp_scheduling.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" #include "ortools/util/bitset.h" #include "ortools/util/range_minimum_query.h" -namespace operations_research { +namespace operations_research::routing { // Given a DimensionValues whose path has changed nodes, fills the travels, // travel_sums, transits, cumuls, and span of the new path. @@ -51,8 +53,10 @@ bool FillDimensionValuesFromRoutingDimension( void FillPrePostVisitValues( int path, const DimensionValues& dimension_values, - absl::AnyInvocable pre_travel_evaluator, - absl::AnyInvocable post_travel_evaluator, + std::optional> + pre_travel_evaluator, + std::optional> + post_travel_evaluator, PrePostVisitValues& visit_values); // Propagates vehicle break constraints in dimension_values. @@ -85,6 +89,10 @@ IntVarLocalSearchFilter* MakeNodeDisjunctionFilter( IntVarLocalSearchFilter* MakeVehicleAmortizedCostFilter( const RoutingModel& routing_model); +/// Returns a filter computing same vehicle costs. +IntVarLocalSearchFilter* MakeSameVehicleCostFilter( + const RoutingModel& routing_model); + /// Returns a filter ensuring type regulation constraints are enforced. IntVarLocalSearchFilter* MakeTypeRegulationsFilter( const RoutingModel& routing_model); @@ -477,7 +485,7 @@ LocalSearchFilter* MakeVehicleVarFilter(const RoutingModel& routing_model, /// pair of nodes and given policies. LocalSearchFilter* MakePickupDeliveryFilter( const RoutingModel& routing_model, const PathState* path_state, - const std::vector& pairs, + absl::Span pairs, const std::vector& vehicle_policies); // This checker enforces dimension requirements. @@ -1019,6 +1027,6 @@ class BasePathFilter : public IntVarLocalSearchFilter { bool lns_detected_; }; -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTERS_H_ +#endif // OR_TOOLS_ROUTING_FILTERS_H_ diff --git a/ortools/constraint_solver/routing_flow.cc b/ortools/routing/flow.cc similarity index 97% rename from ortools/constraint_solver/routing_flow.cc rename to ortools/routing/flow.cc index a8219780657..110f5b70bf5 100644 --- a/ortools/constraint_solver/routing_flow.cc +++ b/ortools/routing/flow.cc @@ -24,17 +24,16 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/types/span.h" -#include "ortools/base/int_type.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_lp_scheduling.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/graph/min_cost_flow.h" +#include "ortools/routing/lp_scheduling.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" #include "ortools/util/saturated_arithmetic.h" -namespace operations_research { +namespace operations_research::routing { namespace { // Compute set of disjunctions involved in a pickup and delivery pair. @@ -120,7 +119,7 @@ bool RoutingModel::IsMatchingModel() const { return false; } -// Solve matching model using a min-cost flow. Here is the underlyihg flow: +// Solve matching model using a min-cost flow. Here is the underlying flow: // // ---------- Source ------------- // | (1,0) | (N,0) @@ -167,8 +166,8 @@ bool RoutingModel::SolveMatchingModel( std::vector optimizers; optimizers.reserve(dimensions.size()); for (RoutingDimension* dimension : dimensions) { - optimizers.emplace_back(dimension, - parameters.continuous_scheduling_solver()); + optimizers.emplace_back( + dimension, parameters.continuous_scheduling_solver(), &search_stats_); } int num_flow_nodes = 0; @@ -389,6 +388,7 @@ bool RoutingModel::SolveMatchingModel( flow.SetNodeSupply(sink, -flow_supply); // TODO(user): Take time limit into account. + search_stats_.num_min_cost_flow_calls++; if (flow.Solve() != SimpleMinCostFlow::OPTIMAL) { return false; } @@ -452,4 +452,4 @@ bool RoutingModel::SolveMatchingModel( return true; } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/routing/heuristic_parameters.proto b/ortools/routing/heuristic_parameters.proto new file mode 100644 index 00000000000..ed950db6cc8 --- /dev/null +++ b/ortools/routing/heuristic_parameters.proto @@ -0,0 +1,101 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Protocol buffer used to parametrize the local cheapest insertion heuristics. + +syntax = "proto3"; + +package operations_research.routing; + +// Parameters used to configure local cheapest insertion heuristics. +message LocalCheapestInsertionParameters { + // In insertion-based heuristics, describes what positions must be considered + // when inserting a pickup/delivery pair, and in what order they are + // considered. + enum PairInsertionStrategy { + // Let the solver decide the set of positions and its ordering. + AUTOMATIC = 0; + // Consider all positions, by increasing (cost(pickup), cost(delivery)). + BEST_PICKUP_THEN_BEST_DELIVERY = 1; + // Consider all positions, by increasing by cost(pickup) + cost(delivery). + BEST_PICKUP_DELIVERY_PAIR = 2; + // Only consider insertion positions that are compatible with the multitour + // property, meaning a series of pickups may only start when the vehicle + // is not carrying any delivery. This setting is designed to explore much + // less possibilities than the full BEST_PICKUP_DELIVERY_PAIR. + // Order by increasing by cost(pickup) + cost(delivery). + BEST_PICKUP_DELIVERY_PAIR_MULTITOUR = 3; + } + + // Choice of insertion strategy for pickup/delivery pairs, used in local + // cheapest insertion, both first solution heuristic and LNS. + PairInsertionStrategy pickup_delivery_strategy = 1; + + // Properties used to select in which order nodes or node pairs are considered + // in insertion heuristics. + enum InsertionSortingProperty { + // Invalid property. + SORTING_PROPERTY_UNSPECIFIED = 0; + // Selects nodes with the least number of allowed vehicles. + SORTING_PROPERTY_ALLOWED_VEHICLES = 1; + // Selects nodes with the highest penalty. + SORTING_PROPERTY_PENALTY = 2; + // Selects nodes with the highest penalty / number of allowed vehicles + // ratio. + SORTING_PROPERTY_PENALTY_OVER_ALLOWED_VEHICLES_RATIO = 3; + // Selects nodes that are on average the farthest from vehicles. + SORTING_PROPERTY_HIGHEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS = 4; + // Selects nodes that are on average the closest to vehicles. + SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS = 5; + // Select nodes with the smallest distance to the closest vehicle. + SORTING_PROPERTY_LOWEST_MIN_ARC_COST_TO_VEHICLE_START_ENDS = 6; + // Selects nodes that have a higher dimension usage on average, where the + // usage is determined as the ratio of node demand over vehicle capacity. + // Currently, this property only supports unary dimensions. + SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE = 7; + // Selects nodes in random order. + // This property cannot be used in conjunction with other properties. + SORTING_PROPERTY_RANDOM = 8; + } + + // The properties used to sort insertion entries in the local cheapest + // insertion heuristic, in *decreasing* order of priority. The properties + // listed here are applied hierarchically, from highest to lowest priority. + // When no properties are provided + // (SORTING_PROPERTY_ALLOWED_VEHICLES, SORTING_PROPERTY_PENALTY) + // is used by default. + repeated InsertionSortingProperty insertion_sorting_properties = 2; +} + +// Parameters used to configure savings heuristics. +message SavingsParameters { + // Ratio (in ]0, 1]) of neighbors to consider for each node when constructing + // the savings. If unspecified, its value is considered to be 1.0. + double neighbors_ratio = 1; + // The number of neighbors considered for each node in the Savings heuristic + // is chosen so that the space used to store the savings doesn't exceed + // max_memory_usage_bytes, which must be in ]0, 1e10]. + // NOTE: If both neighbors_ratio and max_memory_usage_bytes + // are specified, the number of neighbors considered for each node will be the + // minimum of the two numbers determined by these parameters. + double max_memory_usage_bytes = 2; + // Add savings related to reverse arcs when finding the nearest neighbors + // of the nodes. + bool add_reverse_arcs = 3; + // Coefficient of the cost of the arc for which the saving value is being + // computed: + // Saving(a-->b) = Cost(a-->end) + Cost(start-->b) + // - arc_coefficient * Cost(a-->b) + // This parameter must be greater than 0, and its default value is 1. + double arc_coefficient = 4; +} diff --git a/ortools/constraint_solver/routing_ils.cc b/ortools/routing/ils.cc similarity index 95% rename from ortools/constraint_solver/routing_ils.cc rename to ortools/routing/ils.cc index b07aafea105..d8dae1e2c14 100644 --- a/ortools/constraint_solver/routing_ils.cc +++ b/ortools/routing/ils.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_ils.h" +#include "ortools/routing/ils.h" #include #include @@ -26,20 +26,19 @@ #include "absl/functional/bind_front.h" #include "absl/log/check.h" +#include "absl/log/log.h" #include "absl/time/time.h" #include "absl/types/span.h" #include "google/protobuf/repeated_ptr_field.h" -#include "ortools/base/logging.h" #include "ortools/base/protoutil.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_ils.pb.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_parameters_utils.h" -#include "ortools/constraint_solver/routing_search.h" -#include "ortools/constraint_solver/routing_types.h" - -namespace operations_research { +#include "ortools/routing/ils.pb.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/search.h" +#include "ortools/routing/types.h" + +namespace operations_research::routing { namespace { // Returns global cheapest insertion parameters based on the given search @@ -65,20 +64,15 @@ MakeGlobalCheapestInsertionParameters( return gci_parameters; } -// Returns savings parameters based on the given search parameters. -// TODO(user): consider having an ILS specific set of parameters. -SavingsFilteredHeuristic::SavingsParameters MakeSavingsParameters( - const RoutingSearchParameters& search_parameters) { - SavingsFilteredHeuristic::SavingsParameters savings_parameters; - savings_parameters.neighbors_ratio = - search_parameters.savings_neighbors_ratio(); - savings_parameters.max_memory_usage_bytes = - search_parameters.savings_max_memory_usage_bytes(); - savings_parameters.add_reverse_arcs = - search_parameters.savings_add_reverse_arcs(); - savings_parameters.arc_coefficient = - search_parameters.savings_arc_coefficient(); - return savings_parameters; +// Returns local cheapest insertion parameters based on the given recreate +// strategy if available. Returns default parameters otherwise. +LocalCheapestInsertionParameters GetLocalCheapestInsertionParameters( + const RecreateStrategy& recreate_strategy, + const LocalCheapestInsertionParameters& default_parameters) { + return recreate_strategy.has_parameters() && + recreate_strategy.parameters().has_local_cheapest_insertion() + ? recreate_strategy.parameters().local_cheapest_insertion() + : default_parameters; } // Returns a ruin procedure based on the given ruin strategy. @@ -110,7 +104,7 @@ std::unique_ptr MakeRuinProcedure( std::vector> MakeRuinProcedures( RoutingModel* model, std::mt19937* rnd, const google::protobuf::RepeatedPtrField< - ::operations_research::RuinStrategy>& ruin_strategies, + ::operations_research::routing::RuinStrategy>& ruin_strategies, int num_neighbors_for_route_selection) { std::vector> ruin_procedures; for (const RuinStrategy& ruin : ruin_strategies) { @@ -226,25 +220,29 @@ std::unique_ptr MakeRecreateProcedure( const RoutingSearchParameters& parameters, RoutingModel* model, std::function stop_search, LocalSearchFilterManager* filter_manager) { - switch (parameters.iterated_local_search_parameters() - .ruin_recreate_parameters() - .recreate_strategy()) { - case FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION: + const RecreateStrategy& recreate_strategy = + parameters.iterated_local_search_parameters() + .ruin_recreate_parameters() + .recreate_strategy(); + switch (recreate_strategy.heuristic()) { + case FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION: { return std::make_unique( model, std::move(stop_search), absl::bind_front(&RoutingModel::GetArcCostForVehicle, model), - parameters.local_cheapest_cost_insertion_pickup_delivery_strategy(), - GetLocalCheapestInsertionSortingProperties( - parameters.local_cheapest_insertion_sorting_properties()), + GetLocalCheapestInsertionParameters( + recreate_strategy, + parameters.local_cheapest_insertion_parameters()), filter_manager, model->GetBinCapacities()); - case FirstSolutionStrategy::LOCAL_CHEAPEST_COST_INSERTION: + } + case FirstSolutionStrategy::LOCAL_CHEAPEST_COST_INSERTION: { return std::make_unique( model, std::move(stop_search), /*evaluator=*/nullptr, - parameters.local_cheapest_cost_insertion_pickup_delivery_strategy(), - GetLocalCheapestInsertionSortingProperties( - parameters.local_cheapest_insertion_sorting_properties()), + GetLocalCheapestInsertionParameters( + recreate_strategy, + parameters.local_cheapest_cost_insertion_parameters()), filter_manager, model->GetBinCapacities()); + } case FirstSolutionStrategy::SEQUENTIAL_CHEAPEST_INSERTION: { GlobalCheapestInsertionFilteredHeuristic:: GlobalCheapestInsertionParameters gci_parameters = @@ -268,13 +266,15 @@ std::unique_ptr MakeRecreateProcedure( filter_manager, gci_parameters); } case FirstSolutionStrategy::SAVINGS: { + // TODO(user): support ILS-specific savings parameters. return std::make_unique( - model, std::move(stop_search), MakeSavingsParameters(parameters), + model, std::move(stop_search), parameters.savings_parameters(), filter_manager); } case FirstSolutionStrategy::PARALLEL_SAVINGS: { + // TODO(user): support ILS-specific savings parameters. return std::make_unique( - model, std::move(stop_search), MakeSavingsParameters(parameters), + model, std::move(stop_search), parameters.savings_parameters(), filter_manager); } default: @@ -1181,4 +1181,4 @@ std::pair GetSimulatedAnnealingTemperatures( return {reference_temperature * 0.1, reference_temperature * 0.001}; } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_ils.h b/ortools/routing/ils.h similarity index 96% rename from ortools/constraint_solver/routing_ils.h rename to ortools/routing/ils.h index 0115586356c..7a1eda9de5d 100644 --- a/ortools/constraint_solver/routing_ils.h +++ b/ortools/routing/ils.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_ILS_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_ILS_H_ +#ifndef OR_TOOLS_ROUTING_ILS_H_ +#define OR_TOOLS_ROUTING_ILS_H_ #include #include @@ -24,12 +24,12 @@ #include "absl/time/time.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_ils.pb.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/ils.pb.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" #include "ortools/util/bitset.h" -namespace operations_research { +namespace operations_research::routing { // Wraps a routing assignment providing extra features. class RoutingSolution { @@ -278,6 +278,6 @@ std::pair GetSimulatedAnnealingTemperatures( const RoutingModel& model, const SimulatedAnnealingParameters& sa_params, std::mt19937* rnd); -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_ILS_H_ +#endif // OR_TOOLS_ROUTING_ILS_H_ diff --git a/ortools/constraint_solver/routing_ils.proto b/ortools/routing/ils.proto similarity index 94% rename from ortools/constraint_solver/routing_ils.proto rename to ortools/routing/ils.proto index f88b4d6ba97..33dc98dcbc2 100644 --- a/ortools/constraint_solver/routing_ils.proto +++ b/ortools/routing/ils.proto @@ -21,13 +21,14 @@ syntax = "proto3"; -option java_package = "com.google.ortools.constraintsolver"; +option java_package = "com.google.ortools.routing"; option java_multiple_files = true; -option csharp_namespace = "Google.OrTools.ConstraintSolver"; +option csharp_namespace = "Google.OrTools.Routing"; -import "ortools/constraint_solver/routing_enums.proto"; +import "ortools/routing/enums.proto"; +import "ortools/routing/heuristic_parameters.proto"; -package operations_research; +package operations_research.routing; // Ruin strategy that removes a number of spatially close routes. message SpatiallyCloseRoutesRuinStrategy { @@ -146,6 +147,21 @@ message RuinStrategy { } } +// Parameters to customize a recreate strategy. +message RecreateParameters { + oneof parameters { + LocalCheapestInsertionParameters local_cheapest_insertion = 1; + } +} + +// Strategy defining how a solution is recreated. +message RecreateStrategy { + optional FirstSolutionStrategy.Value heuristic = 1; + // The selected parameters should match the chosen recreate heuristic. + // If not set, the default parameters from the RoutingModel are used. + optional RecreateParameters parameters = 2; +} + // The ruin composition strategies specifies how ruin are selected at every ILS // iteration. message RuinCompositionStrategy { @@ -175,7 +191,7 @@ message RuinRecreateParameters { RuinCompositionStrategy.Value ruin_composition_strategy = 2; // Strategy defining how a reference solution is recreated. - FirstSolutionStrategy.Value recreate_strategy = 3; + RecreateStrategy recreate_strategy = 3; // Ratio in [0, 1] of non start/end nodes to consider as neighbors for the // identification of routes spatially close to a non start/end seed node. diff --git a/ortools/constraint_solver/routing_index_manager.cc b/ortools/routing/index_manager.cc similarity index 97% rename from ortools/constraint_solver/routing_index_manager.cc rename to ortools/routing/index_manager.cc index 64ab0e7d3d7..64bf481a7ed 100644 --- a/ortools/constraint_solver/routing_index_manager.cc +++ b/ortools/routing/index_manager.cc @@ -11,10 +11,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/routing/index_manager.h" #include -#include #include #include @@ -23,7 +22,7 @@ #include "absl/types/span.h" #include "ortools/base/logging.h" -namespace operations_research { +namespace operations_research::routing { const int64_t RoutingIndexManager::kUnassigned = -1; @@ -146,4 +145,4 @@ std::vector RoutingIndexManager::IndicesToNodes( return nodes; } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_index_manager.h b/ortools/routing/index_manager.h similarity index 94% rename from ortools/constraint_solver/routing_index_manager.h rename to ortools/routing/index_manager.h index 4c5341b3d51..ce9e020650a 100644 --- a/ortools/constraint_solver/routing_index_manager.h +++ b/ortools/routing/index_manager.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_INDEX_MANAGER_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_INDEX_MANAGER_H_ +#ifndef OR_TOOLS_ROUTING_INDEX_MANAGER_H_ +#define OR_TOOLS_ROUTING_INDEX_MANAGER_H_ #include #include @@ -20,10 +20,10 @@ #include "absl/log/check.h" #include "absl/types/span.h" +#include "ortools/base/base_export.h" #include "ortools/base/strong_vector.h" -#include "ortools/constraint_solver/routing_types.h" - -namespace operations_research { +#include "ortools/routing/types.h" +namespace operations_research::routing { /// Manager for any NodeIndex <-> variable index conversion. The routing solver /// uses variable indices internally and through its API. These variable indices @@ -117,6 +117,6 @@ class OR_DLL RoutingIndexManager { int num_unique_depots_; }; -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_INDEX_MANAGER_H_ +#endif // OR_TOOLS_ROUTING_INDEX_MANAGER_H_ diff --git a/ortools/constraint_solver/routing_insertion_lns.cc b/ortools/routing/insertion_lns.cc similarity index 98% rename from ortools/constraint_solver/routing_insertion_lns.cc rename to ortools/routing/insertion_lns.cc index 4e5ef4c025e..bbafc0dba01 100644 --- a/ortools/constraint_solver/routing_insertion_lns.cc +++ b/ortools/routing/insertion_lns.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_insertion_lns.h" +#include "ortools/routing/insertion_lns.h" #include #include @@ -22,15 +22,14 @@ #include #include "absl/log/check.h" -#include "ortools/base/int_type.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_search.h" -#include "ortools/constraint_solver/routing_utils.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/search.h" +#include "ortools/routing/utils.h" #include "ortools/util/bitset.h" -namespace operations_research { +namespace operations_research::routing { // FilteredHeuristicLocalSearchOperator @@ -529,4 +528,4 @@ bool FilteredHeuristicExpensiveChainLNSOperator:: return false; } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_insertion_lns.h b/ortools/routing/insertion_lns.h similarity index 95% rename from ortools/constraint_solver/routing_insertion_lns.h rename to ortools/routing/insertion_lns.h index 00b90e93a6b..28e383c8b72 100644 --- a/ortools/constraint_solver/routing_insertion_lns.h +++ b/ortools/routing/insertion_lns.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_INSERTION_LNS_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_INSERTION_LNS_H_ +#ifndef OR_TOOLS_ROUTING_INSERTION_LNS_H_ +#define OR_TOOLS_ROUTING_INSERTION_LNS_H_ #include #include @@ -25,12 +25,12 @@ #include "absl/strings/str_cat.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_search.h" -#include "ortools/constraint_solver/routing_types.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/search.h" +#include "ortools/routing/types.h" #include "ortools/util/bitset.h" -namespace operations_research { +namespace operations_research::routing { /// Class of operators using a RoutingFilteredHeuristic to insert unperformed /// nodes after changes have been made to the current solution. @@ -247,6 +247,6 @@ class FilteredHeuristicCloseNodesLNSOperator SparseBitset<> changed_prevs_; }; -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_INSERTION_LNS_H_ +#endif // OR_TOOLS_ROUTING_INSERTION_LNS_H_ diff --git a/ortools/routing/java/CMakeLists.txt b/ortools/routing/java/CMakeLists.txt new file mode 100644 index 00000000000..90aaca3e430 --- /dev/null +++ b/ortools/routing/java/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set_property(SOURCE routing.i PROPERTY CPLUSPLUS ON) +set_property(SOURCE routing.i PROPERTY SWIG_MODULE_NAME Globals) +set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE routing.i PROPERTY COMPILE_OPTIONS + -package ${JAVA_PACKAGE}.routing) +swig_add_library(jnirouting + TYPE OBJECT + LANGUAGE java + OUTPUT_DIR ${JAVA_PROJECT_DIR}/${JAVA_SRC_PATH}/routing + SOURCES routing.i) + +target_include_directories(jnirouting PRIVATE ${JNI_INCLUDE_DIRS}) +set_target_properties(jnirouting PROPERTIES + SWIG_USE_TARGET_INCLUDE_DIRECTORIES ON + POSITION_INDEPENDENT_CODE ON) +target_link_libraries(jnirouting PRIVATE ortools::ortools) + +if(BUILD_TESTING) + file(GLOB JAVA_SRCS "*Test.java") + foreach(FILE_NAME IN LISTS JAVA_SRCS) + add_java_test(FILE_NAME ${FILE_NAME}) + endforeach() +endif() diff --git a/ortools/constraint_solver/java/RoutingSolverTest.java b/ortools/routing/java/RoutingSolverTest.java similarity index 97% rename from ortools/constraint_solver/java/RoutingSolverTest.java rename to ortools/routing/java/RoutingSolverTest.java index 9b0b5988acd..edb0deb8352 100644 --- a/ortools/constraint_solver/java/RoutingSolverTest.java +++ b/ortools/routing/java/RoutingSolverTest.java @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.ortools.constraintsolver; +package com.google.ortools.routing; import static com.google.common.truth.Truth.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -21,9 +21,11 @@ import com.google.auto.value.AutoValue; import com.google.ortools.Loader; -import com.google.ortools.constraintsolver.RoutingModelParameters; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.RoutingSearchStatus; +import com.google.ortools.constraintsolver.Assignment; +import com.google.ortools.constraintsolver.IntVar; +import com.google.ortools.routing.RoutingModelParameters; +import com.google.ortools.routing.RoutingSearchParameters; +import com.google.ortools.routing.RoutingSearchStatus; import com.google.protobuf.Duration; import java.util.ArrayList; import java.util.function.LongBinaryOperator; @@ -153,7 +155,7 @@ public void testRoutingModel() { @Test public void testRoutingModelParameters() { - final RoutingModelParameters parameters = main.defaultRoutingModelParameters(); + final RoutingModelParameters parameters = Globals.defaultRoutingModelParameters(); final RoutingIndexManager manager = new RoutingIndexManager(coordinates.size(), 1, 0); assertNotNull(manager); final RoutingModel model = new RoutingModel(manager, parameters); @@ -167,7 +169,7 @@ public void testRoutingModel_solveWithParameters() { assertNotNull(manager); final RoutingModel model = new RoutingModel(manager); assertNotNull(model); - final RoutingSearchParameters parameters = main.defaultRoutingSearchParameters(); + final RoutingSearchParameters parameters = Globals.defaultRoutingSearchParameters(); final LongBinaryOperator callback = createManhattanCostCallback(manager); final int cost = model.registerTransitCallback(callback); System.gc(); @@ -385,7 +387,7 @@ public void testRoutingModel_addConstantDimension() { dimension.setSpanCostCoefficientForAllVehicles(2); RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setTimeLimit(Duration.newBuilder().setSeconds(10)) .build(); @@ -414,7 +416,7 @@ public void testRoutingModel_addVectorDimension() { model.setArcCostEvaluatorOfAllVehicles(pair.getFirst()); RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setTimeLimit(Duration.newBuilder().setSeconds(10)) .build(); @@ -449,7 +451,7 @@ public void testRoutingModel_addMatrixDimension() { model.setArcCostEvaluatorOfAllVehicles(pair.getFirst()); final RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setTimeLimit(Duration.newBuilder().setSeconds(10)) .build(); @@ -498,7 +500,7 @@ public void testRoutingModel_addDimension() { } RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setTimeLimit(Duration.newBuilder().setSeconds(10)) .build(); diff --git a/ortools/constraint_solver/java/routing_index_manager.i b/ortools/routing/java/index_manager.i similarity index 77% rename from ortools/constraint_solver/java/routing_index_manager.i rename to ortools/routing/java/index_manager.i index b35085a10b7..4e1670c0c73 100644 --- a/ortools/constraint_solver/java/routing_index_manager.i +++ b/ortools/routing/java/index_manager.i @@ -14,19 +14,19 @@ // Wrapper for RoutingIndexManager. %include "ortools/base/base.i" -%include "ortools/constraint_solver/java/routing_types.i" +%include "ortools/routing/java/types.i" %{ -#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/routing/index_manager.h" %} -DEFINE_INDEX_TYPE_TYPEDEF(operations_research::RoutingNodeIndex, - operations_research::RoutingIndexManager::NodeIndex); +DEFINE_INDEX_TYPE_TYPEDEF(operations_research::routing::RoutingNodeIndex, + operations_research::routing::RoutingIndexManager::NodeIndex); %ignoreall -%unignore operations_research; -namespace operations_research { +%unignore operations_research::routing; +namespace operations_research::routing { %unignore RoutingIndexManager; %unignore RoutingIndexManager::~RoutingIndexManager; @@ -46,8 +46,8 @@ namespace operations_research { %rename (nodeToIndex) RoutingIndexManager::NodeToIndex; %rename (nodesToIndices) RoutingIndexManager::NodesToIndices; -} // namespace operations_research +} // namespace operations_research::routing -%include "ortools/constraint_solver/routing_index_manager.h" +%include "ortools/routing/index_manager.h" %unignoreall diff --git a/ortools/constraint_solver/java/routing.i b/ortools/routing/java/routing.i similarity index 89% rename from ortools/constraint_solver/java/routing.i rename to ortools/routing/java/routing.i index 761cf8bd74d..fcb6f6c2a3c 100644 --- a/ortools/constraint_solver/java/routing.i +++ b/ortools/routing/java/routing.i @@ -13,7 +13,7 @@ // TODO(user): Refactor this file to adhere to the SWIG style guide. -// %module main +%module Globals %include "std_pair.i" %template(IntBoolPair) std::pair; @@ -21,50 +21,49 @@ %include "ortools/base/base.i" %include "ortools/util/java/vector.i" %include "ortools/util/java/proto.i" -%include "ortools/constraint_solver/java/constraint_solver.i" +%import "ortools/constraint_solver/java/constraint_solver.i" %import "ortools/util/java/sorted_interval_list.i" // Domain -%include "ortools/constraint_solver/java/routing_index_manager.i" +%include "ortools/routing/java/index_manager.i" // We need to forward-declare the proto here, so that PROTO_INPUT involving it // works correctly. The order matters very much: this declaration needs to be // before the %{ #include ".../routing.h" %}. -namespace operations_research { +namespace operations_research::routing { class RoutingModelParameters; class RoutingSearchParameters; class RoutingSearchStatus; -} // namespace operations_research +} // namespace operations_research::routing // Include the files we want to wrap a first time. %{ -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_types.h" -#include "ortools/constraint_solver/routing.h" -#include +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" %} // RoutingModel methods. DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingCostClassIndex, - operations_research::RoutingModel::CostClassIndex); + operations_research::routing::RoutingCostClassIndex, + operations_research::routing::RoutingModel::CostClassIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingDimensionIndex, - operations_research::RoutingModel::DimensionIndex); + operations_research::routing::RoutingDimensionIndex, + operations_research::routing::RoutingModel::DimensionIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingDisjunctionIndex, - operations_research::RoutingModel::DisjunctionIndex); + operations_research::routing::RoutingDisjunctionIndex, + operations_research::routing::RoutingModel::DisjunctionIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingVehicleClassIndex, - operations_research::RoutingModel::VehicleClassIndex); + operations_research::routing::RoutingVehicleClassIndex, + operations_research::routing::RoutingModel::VehicleClassIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingResourceClassIndex, - operations_research::RoutingModel::ResourceClassIndex); + operations_research::routing::RoutingResourceClassIndex, + operations_research::routing::RoutingModel::ResourceClassIndex); -namespace operations_research { +namespace operations_research::routing { // GlobalVehicleBreaksConstraint %typemap(javaimports) GlobalVehicleBreaksConstraint %{ @@ -342,12 +341,12 @@ import com.google.ortools.constraintsolver.Constraint; %rename (getBoundCost) SimpleBoundCosts::bound_cost; %rename (getSize) SimpleBoundCosts::Size; -} // namespace operations_research +} // namespace operations_research::routing // Generic rename rules. %rename (buildSolution) *::BuildSolution; -// Add needed import to mainJNI.java +// Add needed import to GlobalsJNI.java %pragma(java) jniclassimports=%{ // Types from ConstraintSolver import com.google.ortools.constraintsolver.Assignment; @@ -410,40 +409,47 @@ import java.lang.Runnable; %} // Protobuf support -PROTO_INPUT(operations_research::RoutingSearchParameters, - com.google.ortools.constraintsolver.RoutingSearchParameters, +PROTO_INPUT(operations_research::routing::RoutingSearchParameters, + com.google.ortools.routing.RoutingSearchParameters, search_parameters) -PROTO_INPUT(operations_research::RoutingModelParameters, - com.google.ortools.constraintsolver.RoutingModelParameters, +PROTO_INPUT(operations_research::routing::RoutingModelParameters, + com.google.ortools.routing.RoutingModelParameters, parameters) -PROTO2_RETURN(operations_research::RoutingSearchParameters, - com.google.ortools.constraintsolver.RoutingSearchParameters) -PROTO2_RETURN(operations_research::RoutingModelParameters, - com.google.ortools.constraintsolver.RoutingModelParameters) -PROTO_ENUM_RETURN(operations_research::RoutingSearchStatus::Value, - com.google.ortools.constraintsolver.RoutingSearchStatus.Value) - -// Wrap routing_types.h, routing_parameters.h according to the SWIG style guide. +PROTO2_RETURN(operations_research::routing::RoutingSearchParameters, + com.google.ortools.routing.RoutingSearchParameters) +PROTO2_RETURN(operations_research::routing::RoutingModelParameters, + com.google.ortools.routing.RoutingModelParameters) +PROTO_ENUM_RETURN(operations_research::routing::RoutingSearchStatus::Value, + com.google.ortools.routing.RoutingSearchStatus.Value) + +// Wrap types.h, parameters.h according to the SWIG style guide. %ignoreall %unignore RoutingTransitCallback1; %unignore RoutingTransitCallback2; %unignore RoutingIndexPair; %unignore RoutingIndexPairs; -namespace operations_research { +// Add needed import to Globals.java +%pragma(java) moduleimports=%{ +// Types from ConstraintSolver +import com.google.ortools.constraintsolver.Assignment; +import com.google.ortools.constraintsolver.IntVar; +%} + +namespace operations_research::routing { // Globals -// IMPORTANT(user): These functions from routing_parameters.h are global, so in Java -// they are in the main.java (import com.[...].constraintsolver.main). +// IMPORTANT(user): These functions from parameters.h are global, so in Java +// they are in the Globals.java (import com.[...].routing.Globals). %rename (defaultRoutingSearchParameters) DefaultRoutingSearchParameters; %rename (defaultRoutingModelParameters) DefaultRoutingModelParameters; %rename (findErrorInRoutingSearchParameters) FindErrorInRoutingSearchParameters; %rename (makeSetValuesFromTargets) MakeSetValuesFromTargets; -} // namespace operations_research +} // namespace operations_research::routing -%include "ortools/constraint_solver/routing_types.h" -%include "ortools/constraint_solver/routing_parameters.h" +%include "ortools/routing/types.h" +%include "ortools/routing/parameters.h" %unignoreall // TODO(user): Use ignoreall/unignoreall for this one. A lot of work. //swiglint: disable include-h-allglobals -%include "ortools/constraint_solver/routing.h" +%include "ortools/routing/routing.h" diff --git a/ortools/constraint_solver/java/routing_types.i b/ortools/routing/java/types.i similarity index 68% rename from ortools/constraint_solver/java/routing_types.i rename to ortools/routing/java/types.i index b10d8c3bf5a..45e27fbdfae 100644 --- a/ortools/constraint_solver/java/routing_types.i +++ b/ortools/routing/java/types.i @@ -21,7 +21,28 @@ %import "ortools/util/java/vector.i" %{ -#include "ortools/constraint_solver/routing_types.h" +#include "ortools/routing/types.h" + +/* Global JNI reference deleter */ +class GlobalRefGuard { + JavaVM *jvm_; + jobject jref_; + // non-copyable + GlobalRefGuard(const GlobalRefGuard &) = delete; + GlobalRefGuard &operator=(const GlobalRefGuard &) = delete; + public: + GlobalRefGuard(JavaVM *jvm, jobject jref): jvm_(jvm), jref_(jref) {} + ~GlobalRefGuard() { + JNIEnv *jenv = NULL; + JavaVMAttachArgs args; + args.version = JNI_VERSION_1_2; + args.name = NULL; + args.group = NULL; + jvm_->AttachCurrentThread((void**)&jenv, &args); + jenv->DeleteGlobalRef(jref_); + jvm_->DetachCurrentThread(); + } +}; %} // This macro defines typemaps for IndexT, std::vector and @@ -60,9 +81,9 @@ MATRIX_AS_JAVA_ARRAY(IndexT, int, Int); %apply const std::vector >& { const std::vector >& }; %enddef // DEFINE_INDEX_TYPE_TYPEDEF -DEFINE_INDEX_TYPE(operations_research::RoutingNodeIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingCostClassIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingDimensionIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingDisjunctionIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingVehicleClassIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingResourceClassIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingNodeIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingCostClassIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingDimensionIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingDisjunctionIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingVehicleClassIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingResourceClassIndex); diff --git a/ortools/constraint_solver/routing_lp_scheduling.cc b/ortools/routing/lp_scheduling.cc similarity index 89% rename from ortools/constraint_solver/routing_lp_scheduling.cc rename to ortools/routing/lp_scheduling.cc index b183eeb172a..bc61dc3becd 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.cc +++ b/ortools/routing/lp_scheduling.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_lp_scheduling.h" +#include "ortools/routing/lp_scheduling.h" #include #include @@ -39,13 +39,12 @@ #include "ortools/base/map_util.h" #include "ortools/base/mathutil.h" #include "ortools/base/strong_vector.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/glop/parameters.pb.h" #include "ortools/graph/min_cost_flow.h" #include "ortools/port/proto_utils.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/lp_utils.h" #include "ortools/util/flat_matrix.h" @@ -53,10 +52,13 @@ #include "ortools/util/saturated_arithmetic.h" #include "ortools/util/sorted_interval_list.h" -namespace operations_research { +namespace operations_research::routing { namespace { +constexpr int64_t kint64min = std::numeric_limits::min(); +constexpr int64_t kint64max = std::numeric_limits::max(); + // The following sets of parameters give the fastest response time without // impacting solutions found negatively. glop::GlopParameters GetGlopParametersForLocalLP() { @@ -88,11 +90,11 @@ bool GetCumulBoundsWithOffset(const RoutingDimension& dimension, std::max(dimension.GetFirstPossibleGreaterOrEqualValueForNode( node_index, cumul_offset), cumul_var.Min()); - DCHECK_LT(first_after_offset, std::numeric_limits::max()); + DCHECK_LT(first_after_offset, kint64max); *lower_bound = CapSub(first_after_offset, cumul_offset); DCHECK_GE(*lower_bound, 0); - if (*upper_bound == std::numeric_limits::max()) { + if (*upper_bound == kint64max) { return true; } *upper_bound = CapSub(*upper_bound, cumul_offset); @@ -182,7 +184,8 @@ void StoreVisitedPickupDeliveryPairsOnRoute( LocalDimensionCumulOptimizer::LocalDimensionCumulOptimizer( const RoutingDimension* dimension, - RoutingSearchParameters::SchedulingSolver solver_type) + RoutingSearchParameters::SchedulingSolver solver_type, + RoutingSearchStats* search_stats) : optimizer_core_(dimension, /*use_precedence_propagator=*/false) { // Using one solver per vehicle in the hope that if routes don't change this // will be faster. @@ -194,14 +197,14 @@ LocalDimensionCumulOptimizer::LocalDimensionCumulOptimizer( for (int vehicle = 0; vehicle < vehicles; ++vehicle) { // TODO(user): Instead of passing false, detect if the relaxation // will always violate the MIPL constraints. - solver_[vehicle] = - std::make_unique(false, parameters); + solver_[vehicle] = std::make_unique( + false, parameters, search_stats); } break; } case RoutingSearchParameters::SCHEDULING_CP_SAT: { for (int vehicle = 0; vehicle < vehicles; ++vehicle) { - solver_[vehicle] = std::make_unique(); + solver_[vehicle] = std::make_unique(search_stats); } break; } @@ -332,6 +335,23 @@ LocalDimensionCumulOptimizer::ComputePackedRouteCumuls( resource, solver_[vehicle].get(), packed_cumuls, packed_breaks); } +DimensionSchedulingStatus +LocalDimensionCumulOptimizer::ComputeRouteCumulsWithTransitTargets( + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, + absl::Span transit_targets, + DimensionCumulOptimizerCore::TransitTargetCost transit_target_cost, + std::vector* optimal_transits, + std::vector* optimal_cumuls, + std::vector* optimal_breaks) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); + return optimizer_core_.OptimizeSingleRouteWithTransitTargets( + vehicle, solve_duration_ratio, next_accessor, transit_targets, + transit_target_cost, solver_[vehicle].get(), optimal_transits, + optimal_cumuls, optimal_breaks); +} + const int CumulBoundsPropagator::kNoParent = -2; const int CumulBoundsPropagator::kParentToBePropagated = -1; @@ -361,7 +381,7 @@ bool CumulBoundsPropagator::InitializeArcsAndBounds( const std::function& next_accessor, int64_t cumul_offset, const std::vector* dimension_travel_info_per_route) { - propagated_bounds_.assign(num_nodes_, std::numeric_limits::min()); + propagated_bounds_.assign(num_nodes_, kint64min); for (std::vector& arcs : outgoing_arcs_) { arcs.clear(); @@ -383,7 +403,7 @@ bool CumulBoundsPropagator::InitializeArcsAndBounds( return false; } lower_bounds[PositiveNode(node)] = cumul_lb; - if (cumul_ub < std::numeric_limits::max()) { + if (cumul_ub < kint64max) { lower_bounds[NegativeNode(node)] = -cumul_ub; } @@ -407,7 +427,7 @@ bool CumulBoundsPropagator::InitializeArcsAndBounds( // node + transit + slack_var == next // Add arcs for node + transit + slack_min <= next AddArcs(node, next, CapAdd(transit, slack_var.Min())); - if (slack_var.Max() < std::numeric_limits::max()) { + if (slack_var.Max() < kint64max) { // Add arcs for node + transit + slack_max >= next. AddArcs(next, node, CapSub(-slack_var.Max(), transit)); } @@ -417,7 +437,7 @@ bool CumulBoundsPropagator::InitializeArcsAndBounds( // Add vehicle span upper bound: end - span_ub <= start. const int64_t span_ub = dimension_.GetSpanUpperBoundForVehicle(vehicle); - if (span_ub < std::numeric_limits::max()) { + if (span_ub < kint64max) { AddArcs(model->End(vehicle), model->Start(vehicle), -span_ub); } @@ -442,7 +462,7 @@ bool CumulBoundsPropagator::InitializeArcsAndBounds( const int64_t limit = dimension_.GetPickupToDeliveryLimitForPair( pair_index, model->GetPickupPosition(pickup_index)->alternative_index, model->GetDeliveryPosition(delivery_index)->alternative_index); - if (limit < std::numeric_limits::max()) { + if (limit < kint64max) { // delivery_cumul - limit <= pickup_cumul. AddArcs(delivery_index, pickup_index, -limit); } @@ -455,11 +475,9 @@ bool CumulBoundsPropagator::InitializeArcsAndBounds( continue; } const bool first_node_unperformed = - lower_bounds[PositiveNode(first_node)] == - std::numeric_limits::min(); + lower_bounds[PositiveNode(first_node)] == kint64min; const bool second_node_unperformed = - lower_bounds[PositiveNode(second_node)] == - std::numeric_limits::min(); + lower_bounds[PositiveNode(second_node)] == kint64min; switch (RoutingDimension::GetPrecedenceStatus(first_node_unperformed, second_node_unperformed, performed_constraint)) { @@ -552,10 +570,9 @@ bool CumulBoundsPropagator::PropagateCumulBounds( for (const ArcInfo& arc : outgoing_arcs_[node]) { // NOTE: kint64min as a lower bound means no lower bound at all, so we // don't use this value to propagate. - const int64_t induced_lb = - (lower_bound == std::numeric_limits::min()) - ? std::numeric_limits::min() - : CapAdd(lower_bound, arc.offset); + const int64_t induced_lb = (lower_bound == kint64min) + ? kint64min + : CapAdd(lower_bound, arc.offset); const int head_node = arc.head; if (induced_lb <= current_lb[head_node]) { @@ -632,7 +649,7 @@ DimensionCumulOptimizerCore::ComputeSingleRouteSolutionCostWithoutFixedTransits( !model->IsEnd(next_accessor(model->Start(vehicle))) || model->IsVehicleUsedWhenEmpty(vehicle); if (!SetRouteCumulConstraints( - vehicle, next_accessor, dimension_->transit_evaluator(vehicle), + vehicle, next_accessor, dimension_->transit_evaluator(vehicle), {}, dimension_travel_info, dimension_->GetLocalOptimizerOffsetForVehicle(vehicle), optimize_vehicle_costs, solver, nullptr, &cost_offset_value)) { @@ -777,9 +794,7 @@ bool GetDomainOffsetBounds(const Domain& domain, int64_t offset, const int64_t lower_bound = std::max(CapSub(domain.Min(), offset), 0); const int64_t upper_bound = - domain.Max() == std::numeric_limits::max() - ? std::numeric_limits::max() - : CapSub(domain.Max(), offset); + domain.Max() == kint64max ? kint64max : CapSub(domain.Max(), offset); if (lower_bound > upper_bound) return false; *interval = ClosedInterval(lower_bound, upper_bound); @@ -892,7 +907,7 @@ DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( const int64_t cumul_offset = dimension_->GetLocalOptimizerOffsetForVehicle(vehicle); int64_t cost_offset = 0; - if (!SetRouteCumulConstraints(vehicle, next_accessor, transit_accessor, + if (!SetRouteCumulConstraints(vehicle, next_accessor, transit_accessor, {}, dimension_travel_info, cumul_offset, optimize_vehicle_costs, solver, transit_cost, &cost_offset)) { @@ -951,12 +966,12 @@ DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( } if (cumul_values != nullptr) { - SetValuesFromLP(current_route_cumul_variables_, cumul_offset, solver, - &cumul_values->at(i)); + SetValuesFromLP(current_route_cumul_variables_, cumul_offset, kint64min, + solver, &cumul_values->at(i)); } if (break_values != nullptr) { - SetValuesFromLP(current_route_break_variables_, cumul_offset, solver, - &break_values->at(i)); + SetValuesFromLP(current_route_break_variables_, cumul_offset, kint64min, + solver, &break_values->at(i)); } } @@ -1009,7 +1024,7 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::Optimize( ? nullptr : &dimension_travel_info_per_route[vehicle]; if (!SetRouteCumulConstraints( - vehicle, next_accessor, dimension_->transit_evaluator(vehicle), + vehicle, next_accessor, dimension_->transit_evaluator(vehicle), {}, dimension_travel_info, cumul_offset, optimize_vehicle_costs, solver, &route_transit_cost, &route_cost_offset)) { return DimensionSchedulingStatus::INFEASIBLE; @@ -1046,8 +1061,10 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::Optimize( // TODO(user): In case the status is RELAXED_OPTIMAL_ONLY, check we can // safely avoid filling variable and cost values. - SetValuesFromLP(index_to_cumul_variable_, cumul_offset, solver, cumul_values); - SetValuesFromLP(all_break_variables_, cumul_offset, solver, break_values); + SetValuesFromLP(index_to_cumul_variable_, cumul_offset, kint64min, solver, + cumul_values); + SetValuesFromLP(all_break_variables_, cumul_offset, kint64min, solver, + break_values); SetResourceIndices(solver, resource_indices_per_group); if (cost_without_transits != nullptr) { @@ -1101,9 +1118,10 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPack( // TODO(user): In case the status is RELAXED_OPTIMAL_ONLY, check we can // safely avoid filling variable values. const int64_t global_offset = dimension_->GetGlobalOptimizerOffset(); - SetValuesFromLP(index_to_cumul_variable_, global_offset, solver, + SetValuesFromLP(index_to_cumul_variable_, global_offset, kint64min, solver, cumul_values); - SetValuesFromLP(all_break_variables_, global_offset, solver, break_values); + SetValuesFromLP(all_break_variables_, global_offset, kint64min, solver, + break_values); solver->Clear(); return status; } @@ -1148,10 +1166,10 @@ DimensionCumulOptimizerCore::OptimizeAndPackSingleRoute( } const int64_t local_offset = dimension_->GetLocalOptimizerOffsetForVehicle(vehicle); - SetValuesFromLP(current_route_cumul_variables_, local_offset, solver, - cumul_values); - SetValuesFromLP(current_route_break_variables_, local_offset, solver, - break_values); + SetValuesFromLP(current_route_cumul_variables_, local_offset, kint64min, + solver, cumul_values); + SetValuesFromLP(current_route_break_variables_, local_offset, kint64min, + solver, break_values); solver->Clear(); return status; } @@ -1208,10 +1226,10 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes( solver->ClearObjective(); for (int vehicle : vehicles) { const int end_cumul_var = index_to_cumul_variable_[model->End(vehicle)]; - // end_cumul_var <= solver.GetValue(end_cumul_var) - solver->SetVariableBounds( - end_cumul_var, solver->GetVariableLowerBound(end_cumul_var), - MathUtil::FastInt64Round(solver->GetValue(end_cumul_var))); + // end_cumul_var <= solver.GetVariableValue(end_cumul_var) + solver->SetVariableBounds(end_cumul_var, + solver->GetVariableLowerBound(end_cumul_var), + solver->GetVariableValue(end_cumul_var)); // Maximize the starts of the routes. solver->SetObjectiveCoefficient( @@ -1227,6 +1245,168 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes( return status; } +DimensionSchedulingStatus +DimensionCumulOptimizerCore::OptimizeSingleRouteWithTransitTargets( + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, + absl::Span transit_targets, + TransitTargetCost transit_target_cost, RoutingLinearSolverWrapper* solver, + std::vector* optimal_transits, + std::vector* optimal_cumuls, + std::vector* optimal_breaks) { + ClearIfNonNull(optimal_transits); + ClearIfNonNull(optimal_cumuls); + ClearIfNonNull(optimal_breaks); + InitOptimizer(solver); + const int64_t cumul_offset = + dimension_->GetLocalOptimizerOffsetForVehicle(vehicle); + const auto& transit_evaluator = dimension_->transit_evaluator(vehicle); + // Setup the regular route cumul constraints. + if (!SetRouteCumulConstraints( + vehicle, next_accessor, transit_evaluator, transit_targets, {}, + cumul_offset, /*optimize_costs=*/false, solver, nullptr, nullptr)) { + return DimensionSchedulingStatus::INFEASIBLE; + } + DCHECK_GE(current_route_cumul_variables_.size(), 2); + DCHECK_EQ(transit_targets.size(), current_route_cumul_variables_.size() - 1); + + const auto [threshold_ratio, cost_coefficient_below_threshold, + cost_coefficient_above_threshold] = transit_target_cost; + DCHECK_GT(threshold_ratio, 0); + DCHECK_LT(threshold_ratio, 1); + DCHECK_GT(cost_coefficient_above_threshold, 0); + DCHECK_GT(cost_coefficient_below_threshold, cost_coefficient_above_threshold); + + // Add transit target costs, to try and be as close as possible to the transit + // targets. + const std::vector& variable_transits = + current_route_variable_transit_variables_; + DCHECK_EQ(variable_transits.size(), transit_targets.size()); + for (int pos = 0; pos < variable_transits.size(); ++pos) { + int variable_transit = variable_transits[pos]; + if (variable_transit < 0) { + DCHECK_LE(transit_targets[pos], + transit_evaluator(current_route_nodes_[pos], + current_route_nodes_[pos + 1])); + continue; + } + + // NOTE: In the following, constants are identified in upper-case and + // variables are in lower-case. + // We want the variable_transit to be as close as possible to its upper + // bound, UB = TRANSIT_TARGET - FIXED_TRANSIT. + // We therefore try to maximize each variable_transit, but by adding convex + // costs so that potential violations from the transit targets are spread + // along the path. + // + // We use a more "aggressive" cost (hence a more aggressive cost slope) + // when the variable_transit is below a given threshold, determined as + // THRESHOLD = + // (TRANSIT_TARGET_THRESHOLD_RATIO * TRANSIT_TARGET) - FIXED_TRANSIT. + // + // We use violation_above_threshold and violation_below_threshold variables + // to represent how much the variable_transit differs from its upper bound, + // by representing the overall violation as a sum of the violation above the + // THRESHOLD and below it. We have: + // variable_transit + violation_above_threshold + violation_below_threshold + // == UB, with + // violation_above_threshold ∈ [0, UB - THRESHOLD] and + // violation_below_threshold ∈ [0, THRESHOLD]. + // The goal is then to minimize the overall violation, by + // adding to the objective function: + // Cost += violation_above_threshold * COST_COEFFICIENT_ABOVE_THRESHOLD + + // violation_below_threshold * COST_COEFFICIENT_BELOW_THRESHOLD. + // + // Since the cost coefficients are such that the cost function is convex wrt + // the variable_transit + // (COST_COEFFICIENT_BELOW_THRESHOLD > COST_COEFFICIENT_ABOVE_THRESHOLD), + // The solver will use up all the violation_above_threshold before starting + // to use violation_below_threshold in order to minimize the overall cost, + // with a preference for using up as little of violation_below as possible + // along the route. + const int64_t variable_transit_ub = + solver->GetVariableUpperBound(variable_transit); + + const int64_t transit_target = transit_targets[pos]; + const int64_t fixed_transit = CapSub(transit_target, variable_transit_ub); + DCHECK_GT(transit_target, fixed_transit); + DCHECK_GE(fixed_transit, 0); + const int64_t threshold = std::max( + CapSub(threshold_ratio * transit_target, fixed_transit), 0L); + + DCHECK_GT(variable_transit_ub, threshold); + const int violation_above_threshold = + solver->AddVariable(0, CapSub(variable_transit_ub, threshold)); + const int violation_below_threshold = solver->AddVariable(0, threshold); + solver->AddLinearConstraint(variable_transit_ub, variable_transit_ub, + {{variable_transit, 1}, + {violation_above_threshold, 1}, + {violation_below_threshold, 1}}); + solver->SetObjectiveCoefficient(violation_above_threshold, + cost_coefficient_above_threshold); + solver->SetObjectiveCoefficient(violation_below_threshold, + cost_coefficient_below_threshold); + } + + // TODO(user): Adapt the solve duration ratio here and below. Divide by 2? + const RoutingModel& model = *dimension_->model(); + DimensionSchedulingStatus status = + solver->Solve(model.RemainingTime() * solve_duration_ratio); + if (status == DimensionSchedulingStatus::INFEASIBLE) { + solver->Clear(); + return status; + } + + // Now force the values of the variable transits to their optimal ones wrt. + // the previous model, then setup the cumul costs in the objective and solve + // again. + // NOTE(user): Given our constraint matrix, our problem *should* always + // have an integer optimal solution, in which case we can round to the nearest + // integer for the variable_transits. + // If this DCHECK ever fails, it can be removed but the code below should be + // adapted to have a 2-phase approach, solving once with the rounded values as + // bound and if this fails, solve again using std::floor. + DCHECK(solver->SolutionIsInteger()); + for (int pos = 0; pos < variable_transits.size(); ++pos) { + const int variable_transit = variable_transits[pos]; + if (variable_transit < 0) { + continue; + } + const int64_t variable_transit_value = + solver->GetVariableValue(variable_transit); + DCHECK_GE(variable_transit_value, 0); + solver->SetVariableBounds(variable_transit, variable_transit_value, + variable_transit_value); + } + solver->ClearObjective(); + SetRouteCumulCosts(vehicle, cumul_offset, /*total_fixed_transit=*/0, solver, + nullptr, nullptr); + status = solver->Solve(model.RemainingTime() * solve_duration_ratio); + if (status == DimensionSchedulingStatus::INFEASIBLE) { + solver->Clear(); + return status; + } + SetValuesFromLP(current_route_cumul_variables_, cumul_offset, kint64min, + solver, optimal_cumuls); + SetValuesFromLP(current_route_break_variables_, cumul_offset, kint64min, + solver, optimal_breaks); + SetValuesFromLP(current_route_variable_transit_variables_, 0, 0, solver, + optimal_transits); + if (optimal_transits != nullptr) { + DCHECK_EQ(optimal_transits->size(), current_route_nodes_.size() - 1); + // Add the fixed transit on each arc to optimal_transits. + for (int pos = 0; pos < optimal_transits->size(); ++pos) { + const int64_t fixed_transit = + std::min(transit_targets[pos], + transit_evaluator(current_route_nodes_[pos], + current_route_nodes_[pos + 1])); + CapAddTo(fixed_transit, &(*optimal_transits)[pos]); + } + } + solver->Clear(); + return status; +} + #define SET_DEBUG_VARIABLE_NAME(solver, var, name) \ do { \ if (DEBUG_MODE) { \ @@ -1295,8 +1475,7 @@ bool DimensionCumulOptimizerCore::TightenRouteCumulBounds( for (int pos = route_size - 2; pos >= 0; --pos) { // If cumul_max[pos+1] is kint64max, it will be translated to // double +infinity, so it must not constrain cumul_max[pos]. - if (current_route_max_cumuls_[pos + 1] < - std::numeric_limits::max()) { + if (current_route_max_cumuls_[pos + 1] < kint64max) { const int64_t slack_min = dimension_->SlackVar(route[pos])->Min(); current_route_max_cumuls_[pos] = std::min( current_route_max_cumuls_[pos], @@ -1365,23 +1544,36 @@ double FindBestScaling(absl::Span coefficients, bool DimensionCumulOptimizerCore::SetRouteTravelConstraints( const RouteDimensionTravelInfo* dimension_travel_info, - absl::Span lp_slacks, absl::Span fixed_transit, + absl::Span lp_slacks, absl::Span fixed_transits, + absl::Span transit_targets, RoutingLinearSolverWrapper* solver) { const std::vector& lp_cumuls = current_route_cumul_variables_; const int path_size = lp_cumuls.size(); + std::vector& variable_transits = + current_route_variable_transit_variables_; if (dimension_travel_info == nullptr || dimension_travel_info->transition_info.empty()) { - // Travel is not travel-start dependent. + variable_transits.assign(path_size - 1, -1); // Add all path constraints to LP: - // cumul[i] + fixed_transit[i] + slack[i] == cumul[i+1] - // <=> fixed_transit[i] == cumul[i+1] - cumul[i] - slack[i]. + // cumul[i+1] == + // cumul[i] + fixed_transit[i] + variable_transit[i] + slack[i] + // <=> fixed_transit[i] == + // cumul[i+1] - cumul[i] - slack[i] - variable_transit[i]. for (int pos = 0; pos < path_size - 1; ++pos) { - const int ct = - solver->CreateNewConstraint(fixed_transit[pos], fixed_transit[pos]); + const int64_t fixed_transit = fixed_transits[pos]; + const int ct = solver->CreateNewConstraint(fixed_transit, fixed_transit); solver->SetCoefficient(ct, lp_cumuls[pos + 1], 1); solver->SetCoefficient(ct, lp_cumuls[pos], -1); solver->SetCoefficient(ct, lp_slacks[pos], -1); + if (!transit_targets.empty()) { + const int64_t max_variable_transit = + CapSub(transit_targets[pos], fixed_transit); + if (max_variable_transit > 0) { + variable_transits[pos] = solver->AddVariable(0, max_variable_transit); + solver->SetCoefficient(ct, variable_transits[pos], -1); + } + } } return true; } @@ -1543,10 +1735,9 @@ bool DimensionCumulOptimizerCore::SetRouteTravelConstraints( if (factor <= 0) return false; const int linearization_ct = solver->AddLinearConstraint( - MathUtil::FastInt64Round(factor * (y_intercept - 0.5)), - std::numeric_limits::max(), - {{travel_value, MathUtil::FastInt64Round(factor)}, - {travel_start, MathUtil::FastInt64Round(-factor * slope)}}); + MathUtil::Round(factor * (y_intercept - 0.5)), kint64max, + {{travel_value, MathUtil::Round(factor)}, + {travel_start, MathUtil::Round(-factor * slope)}}); if (need_bins) { solver->SetEnforcementLiteral(linearization_ct, belongs_to_this_segment_var); @@ -1561,8 +1752,8 @@ bool DimensionCumulOptimizerCore::SetRouteTravelConstraints( // const int64_t Tm = (transit_function.y_anchors[seg] + // transit_function.y_anchors[seg + 1]) / 2; The constraint is // implemented as: cost_scaled * Tm >= cost const int cost_ct = - // solver->AddLinearConstraint(0, std::numeric_limits::max(), - // {{cost_scaled, Tm}, {cost, -1}}); + // solver->AddLinearConstraint(0, kint64max, + // {{cost_scaled, Tm}, {cost, -1}}); // solver->SetEnforcementLiteral(cost_ct, belongs_to_this_segment_var); } @@ -1641,11 +1832,10 @@ bool DimensionCumulOptimizerCore::SetRouteTravelConstraints( if (factor <= 0) return false; solver->AddLinearConstraint( - MathUtil::FastInt64Round(factor * y_intercept), - std::numeric_limits::max(), + MathUtil::Round(factor * y_intercept), kint64max, {{compression_cost, std::round(factor)}, {travel_compression_absolute, - MathUtil::FastInt64Round(-factor * slope)}}); + MathUtil::Round(-factor * slope)}}); } // ====== UNCOMMENT TO USE PRODUCT TO COMPUTE THE EXACT ERROR ===== // // Normally cost_scaled = C₂×(Tᵣ - T)²/Tᵣ @@ -1654,8 +1844,7 @@ bool DimensionCumulOptimizerCore::SetRouteTravelConstraints( // const int prod = solver->CreateNewPositiveVariable(); // solver->AddProductConstraint(prod, {cost_scaled, travel_value}); // The constraint is implemented as: cost_scaled * Tᵣ >= cost - // solver->AddLinearConstraint(0, std::numeric_limits::max(), - // {{prod, 1}, {cost, -1}}); + // solver->AddLinearConstraint(0, kint64max, {{prod, 1}, {cost, -1}}); // ====== UNCOMMENT TO USE AVERAGE ERROR APPROXIMATION ===== // // Normally cost_scaled = C₂×(Tᵣ - T)²/Tᵣ @@ -1665,8 +1854,7 @@ bool DimensionCumulOptimizerCore::SetRouteTravelConstraints( // cost_scaled = cost. So the cost_function must be defined as cost = // C₂×(Tᵣ - T)²/Tₐ The constraint is implemented as: cost_scaled >= cost solver->AddLinearConstraint( - 0, std::numeric_limits::max(), - {{relative_compression_cost, 1}, {compression_cost, -1}}); + 0, kint64max, {{relative_compression_cost, 1}, {compression_cost, -1}}); solver->SetObjectiveCoefficient(relative_compression_cost, 1.0); } @@ -1691,12 +1879,14 @@ bool RouteIsValid(const RoutingModel& model, int vehicle, bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( int vehicle, const std::function& next_accessor, const std::function& transit_accessor, + absl::Span transit_targets, const RouteDimensionTravelInfo* dimension_travel_info, int64_t cumul_offset, bool optimize_costs, RoutingLinearSolverWrapper* solver, int64_t* route_transit_cost, int64_t* route_cost_offset) { RoutingModel* const model = dimension_->model(); // Extract the vehicle's path from next_accessor. - std::vector path; + std::vector& path = current_route_nodes_; + path.clear(); { DCHECK(RouteIsValid(*model, vehicle, next_accessor)); int node = model->Start(vehicle); @@ -1715,6 +1905,9 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( for (int pos = 1; pos < path_size; ++pos) { int64_t& transit = fixed_transit[pos - 1]; transit = transit_accessor(path[pos - 1], path[pos]); + if (!transit_targets.empty()) { + transit = std::min(transit, transit_targets[pos - 1]); + } total_fixed_transit = CapAdd(total_fixed_transit, transit); } } @@ -1785,63 +1978,11 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( } if (!SetRouteTravelConstraints(dimension_travel_info, lp_slacks, - fixed_transit, solver)) { + fixed_transit, transit_targets, solver)) { return false; } if (route_cost_offset != nullptr) *route_cost_offset = 0; - if (optimize_costs) { - // Add soft upper bounds. - for (int pos = 0; pos < path_size; ++pos) { - if (!dimension_->HasCumulVarSoftUpperBound(path[pos])) continue; - const int64_t coef = - dimension_->GetCumulVarSoftUpperBoundCoefficient(path[pos]); - if (coef == 0) continue; - int64_t bound = dimension_->GetCumulVarSoftUpperBound(path[pos]); - if (bound < cumul_offset && route_cost_offset != nullptr) { - // Add coef * (cumul_offset - bound) to the cost offset. - *route_cost_offset = CapAdd(*route_cost_offset, - CapProd(CapSub(cumul_offset, bound), coef)); - } - bound = std::max(0, CapSub(bound, cumul_offset)); - if (current_route_max_cumuls_[pos] <= bound) { - // constraint is never violated. - continue; - } - const int soft_ub_diff = solver->CreateNewPositiveVariable(); - SET_DEBUG_VARIABLE_NAME(solver, soft_ub_diff, - absl::StrFormat("soft_ub_diff(%ld)", pos)); - solver->SetObjectiveCoefficient(soft_ub_diff, coef); - // cumul - soft_ub_diff <= bound. - const int ct = solver->CreateNewConstraint( - std::numeric_limits::min(), bound); - solver->SetCoefficient(ct, lp_cumuls[pos], 1); - solver->SetCoefficient(ct, soft_ub_diff, -1); - } - // Add soft lower bounds. - for (int pos = 0; pos < path_size; ++pos) { - if (!dimension_->HasCumulVarSoftLowerBound(path[pos])) continue; - const int64_t coef = - dimension_->GetCumulVarSoftLowerBoundCoefficient(path[pos]); - if (coef == 0) continue; - const int64_t bound = std::max( - 0, CapSub(dimension_->GetCumulVarSoftLowerBound(path[pos]), - cumul_offset)); - if (current_route_min_cumuls_[pos] >= bound) { - // constraint is never violated. - continue; - } - const int soft_lb_diff = solver->CreateNewPositiveVariable(); - SET_DEBUG_VARIABLE_NAME(solver, soft_lb_diff, - absl::StrFormat("soft_lb_diff(%ld)", pos)); - solver->SetObjectiveCoefficient(soft_lb_diff, coef); - // bound - cumul <= soft_lb_diff - const int ct = solver->CreateNewConstraint( - bound, std::numeric_limits::max()); - solver->SetCoefficient(ct, lp_cumuls[pos], 1); - solver->SetCoefficient(ct, soft_lb_diff, 1); - } - } // Add pickup and delivery limits. std::vector visited_pairs; StoreVisitedPickupDeliveryPairsOnRoute( @@ -1863,10 +2004,9 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( const int64_t limit = dimension_->GetPickupToDeliveryLimitForPair( pair_index, model->GetPickupPosition(pickup_index)->alternative_index, model->GetDeliveryPosition(delivery_index)->alternative_index); - if (limit < std::numeric_limits::max()) { + if (limit < kint64max) { // delivery_cumul - pickup_cumul <= limit. - const int ct = solver->CreateNewConstraint( - std::numeric_limits::min(), limit); + const int ct = solver->CreateNewConstraint(kint64min, limit); solver->SetCoefficient(ct, index_to_cumul_variable_[delivery_index], 1); solver->SetCoefficient(ct, index_to_cumul_variable_[pickup_index], -1); } @@ -1874,107 +2014,26 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( // Add span bound constraint. const int64_t span_bound = dimension_->GetSpanUpperBoundForVehicle(vehicle); - if (span_bound < std::numeric_limits::max()) { + if (span_bound < kint64max) { // end_cumul - start_cumul <= bound - const int ct = solver->CreateNewConstraint( - std::numeric_limits::min(), span_bound); + const int ct = solver->CreateNewConstraint(kint64min, span_bound); solver->SetCoefficient(ct, lp_cumuls.back(), 1); solver->SetCoefficient(ct, lp_cumuls.front(), -1); } - // Add span and slack costs. - // NOTE: The fixed transit is removed from the span cost since it doesn't - // affect the optimization of the scheduling of the route. - const int64_t span_cost_coef = - dimension_->GetSpanCostCoefficientForVehicle(vehicle); - const int64_t slack_cost_coef = CapAdd( - span_cost_coef, dimension_->GetSlackCostCoefficientForVehicle(vehicle)); - if (optimize_costs && slack_cost_coef > 0) { - // span_without_fixed_transit_var = - // end_cumul - start_cumul - total_fixed_transit - const int span_without_fixed_transit_var = - solver->CreateNewPositiveVariable(); - SET_DEBUG_VARIABLE_NAME(solver, span_without_fixed_transit_var, - "span_without_fixed_transit_var"); - solver->AddLinearConstraint(total_fixed_transit, total_fixed_transit, - {{lp_cumuls.back(), 1}, - {lp_cumuls.front(), -1}, - {span_without_fixed_transit_var, -1}}); - solver->SetObjectiveCoefficient(span_without_fixed_transit_var, - slack_cost_coef); - } - // Add soft span cost. - if (optimize_costs && dimension_->HasSoftSpanUpperBounds()) { - const BoundCost bound_cost = - dimension_->GetSoftSpanUpperBoundForVehicle(vehicle); - if (bound_cost.bound < std::numeric_limits::max() && - bound_cost.cost > 0) { - const int span_violation = solver->CreateNewPositiveVariable(); - SET_DEBUG_VARIABLE_NAME(solver, span_violation, "span_violation"); - // end - start <= bound + span_violation - const int violation = solver->CreateNewConstraint( - std::numeric_limits::min(), bound_cost.bound); - solver->SetCoefficient(violation, lp_cumuls.back(), 1.0); - solver->SetCoefficient(violation, lp_cumuls.front(), -1.0); - solver->SetCoefficient(violation, span_violation, -1.0); - // Add span_violation * cost to objective. - solver->SetObjectiveCoefficient(span_violation, bound_cost.cost); - } - } - if (optimize_costs && solver->IsCPSATSolver() && - dimension_->HasQuadraticCostSoftSpanUpperBounds()) { - // NOTE: the quadratic soft bound might be different from the one above. - const BoundCost bound_cost = - dimension_->GetQuadraticCostSoftSpanUpperBoundForVehicle(vehicle); - if (bound_cost.bound < std::numeric_limits::max() && - bound_cost.cost > 0) { - const int span_violation = solver->CreateNewPositiveVariable(); - SET_DEBUG_VARIABLE_NAME( - solver, span_violation, - absl::StrFormat("quadratic_span_violation(%ld)", vehicle)); - // end - start <= bound + span_violation - const int violation = solver->CreateNewConstraint( - std::numeric_limits::min(), bound_cost.bound); - solver->SetCoefficient(violation, lp_cumuls.back(), 1.0); - solver->SetCoefficient(violation, lp_cumuls.front(), -1.0); - solver->SetCoefficient(violation, span_violation, -1.0); - // Add variable squared_span_violation, equal to span_violation². - const int squared_span_violation = solver->CreateNewPositiveVariable(); - SET_DEBUG_VARIABLE_NAME( - solver, squared_span_violation, - absl::StrFormat("squared_span_violation(%ld)", vehicle)); - solver->AddProductConstraint(squared_span_violation, - {span_violation, span_violation}); - // Add squared_span_violation * cost to objective. - solver->SetObjectiveCoefficient(squared_span_violation, bound_cost.cost); - } - } - // Add global span constraint. - if (optimize_costs && dimension_->global_span_cost_coefficient() > 0) { - // min_start_cumul_ <= cumuls[start] - int ct = - solver->CreateNewConstraint(std::numeric_limits::min(), 0); - solver->SetCoefficient(ct, min_start_cumul_, 1); - solver->SetCoefficient(ct, lp_cumuls.front(), -1); - // max_end_cumul_ >= cumuls[end] - ct = solver->CreateNewConstraint(0, std::numeric_limits::max()); - solver->SetCoefficient(ct, max_end_cumul_, 1); - solver->SetCoefficient(ct, lp_cumuls.back(), -1); - } - // Fill transit cost if specified. - if (route_transit_cost != nullptr) { - if (optimize_costs && span_cost_coef > 0) { - *route_transit_cost = CapProd(total_fixed_transit, span_cost_coef); - } else { - *route_transit_cost = 0; - } + + if (optimize_costs) { + SetRouteCumulCosts(vehicle, cumul_offset, total_fixed_transit, solver, + route_transit_cost, route_cost_offset); } + + current_route_break_variables_.clear(); + if (!dimension_->HasBreakConstraints()) return true; + // For every break that must be inside the route, the duration of that break // must be flowed in the slacks of arcs that can intersect the break. // This LP modelization is correct but not complete: // can miss some cases where the breaks cannot fit. // TODO(user): remove the need for returns in the code below. - current_route_break_variables_.clear(); - if (!dimension_->HasBreakConstraints()) return true; const std::vector& breaks = dimension_->GetBreakIntervalsOfVehicle(vehicle); const int num_breaks = breaks.size(); @@ -1982,15 +2041,14 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( // and it reduces to a span maximum. // TODO(user): Also add the case where no breaks can intersect the route. if (num_breaks == 0) { - int64_t maximum_route_span = std::numeric_limits::max(); + int64_t maximum_route_span = kint64max; for (const auto& distance_duration : dimension_->GetBreakDistanceDurationOfVehicle(vehicle)) { maximum_route_span = std::min(maximum_route_span, distance_duration.first); } - if (maximum_route_span < std::numeric_limits::max()) { - const int ct = solver->CreateNewConstraint( - std::numeric_limits::min(), maximum_route_span); + if (maximum_route_span < kint64max) { + const int ct = solver->CreateNewConstraint(kint64min, maximum_route_span); solver->SetCoefficient(ct, lp_cumuls.back(), 1); solver->SetCoefficient(ct, lp_cumuls.front(), -1); } @@ -2098,8 +2156,7 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( // Break can be before route. if (break_end_min <= vehicle_start_max) { const int ct = solver->AddLinearConstraint( - 0, std::numeric_limits::max(), - {{lp_cumuls.front(), 1}, {lp_breaks[br].end, -1}}); + 0, kint64max, {{lp_cumuls.front(), 1}, {lp_breaks[br].end, -1}}); const int break_is_before_route = solver->AddVariable(0, 1); SET_DEBUG_VARIABLE_NAME( solver, break_is_before_route, @@ -2110,8 +2167,7 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( // Break can be after route. if (vehicle_end_min <= break_start_max) { const int ct = solver->AddLinearConstraint( - 0, std::numeric_limits::max(), - {{lp_breaks[br].start, 1}, {lp_cumuls.back(), -1}}); + 0, kint64max, {{lp_breaks[br].start, 1}, {lp_cumuls.back(), -1}}); const int break_is_after_route = solver->AddVariable(0, 1); SET_DEBUG_VARIABLE_NAME( solver, break_is_after_route, @@ -2148,8 +2204,8 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( solver, break_in_slack, absl::StrFormat("break_in_slack(%ld, %ld)", br, pos)); if (slack_linear_lower_bound_ct[pos] == -1) { - slack_linear_lower_bound_ct[pos] = solver->AddLinearConstraint( - std::numeric_limits::min(), 0, {{lp_slacks[pos], -1}}); + slack_linear_lower_bound_ct[pos] = + solver->AddLinearConstraint(kint64min, 0, {{lp_slacks[pos], -1}}); } // To keep the model clean // (cf. glop::LinearProgram::NotifyThatColumnsAreClean), constraints on @@ -2177,21 +2233,21 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( solver->AddProductConstraint(break_duration_in_slack, {break_in_slack, lp_breaks[br].duration}); if (slack_exact_lower_bound_ct[pos] == -1) { - slack_exact_lower_bound_ct[pos] = solver->AddLinearConstraint( - std::numeric_limits::min(), 0, {{lp_slacks[pos], -1}}); + slack_exact_lower_bound_ct[pos] = + solver->AddLinearConstraint(kint64min, 0, {{lp_slacks[pos], -1}}); } solver->SetCoefficient(slack_exact_lower_bound_ct[pos], break_duration_in_slack, 1); // If break_in_slack_i == 1, then // 1) break_start >= cumul[pos] + pre_travel[pos] const int break_start_after_current_ct = solver->AddLinearConstraint( - pre_travel[pos], std::numeric_limits::max(), + pre_travel[pos], kint64max, {{lp_breaks[br].start, 1}, {lp_cumuls[pos], -1}}); solver->SetEnforcementLiteral(break_start_after_current_ct, break_in_slack); // 2) break_end <= cumul[pos+1] - post_travel[pos] const int break_ends_before_next_ct = solver->AddLinearConstraint( - post_travel[pos], std::numeric_limits::max(), + post_travel[pos], kint64max, {{lp_cumuls[pos + 1], 1}, {lp_breaks[br].end, -1}}); solver->SetEnforcementLiteral(break_ends_before_next_ct, break_in_slack); @@ -2341,7 +2397,7 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( for (int br = 1; br < num_breaks; ++br) { if (lp_breaks[br].start == -1 || lp_breaks[br - 1].start == -1) continue; solver->AddLinearConstraint( - 0, std::numeric_limits::max(), + 0, kint64max, {{lp_breaks[br - 1].end, -1}, {lp_breaks[br].start, 1}}); } } @@ -2399,11 +2455,11 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( solver->AddLinearConstraint( 1, 1, {{break_is_eligible, 1}, {break_is_not_eligible, 1}}); const int positive_ct = solver->AddLinearConstraint( - min_break_duration, std::numeric_limits::max(), + min_break_duration, kint64max, {{lp_break.end, 1}, {lp_break.start, -1}}); solver->SetEnforcementLiteral(positive_ct, break_is_eligible); const int negative_ct = solver->AddLinearConstraint( - std::numeric_limits::min(), min_break_duration - 1, + kint64min, min_break_duration - 1, {{lp_break.end, 1}, {lp_break.start, -1}}); solver->SetEnforcementLiteral(negative_ct, break_is_not_eligible); } @@ -2426,30 +2482,168 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( solver->SetEnforcementLiteral(empty_cover_ct, break_is_not_eligible); const int cover = - solver->AddVariable(CapAdd(vehicle_start_min, limit), - std::numeric_limits::max()); + solver->AddVariable(CapAdd(vehicle_start_min, limit), kint64max); SET_DEBUG_VARIABLE_NAME(solver, cover, absl::StrFormat("cover(%ld)", br)); solver->AddMaximumConstraint(cover, {previous_cover, break_cover}); // Cover chaining. If route end is not covered, break start must be: // cover_{i-1} < route_end => s_i <= cover_{i-1} const int route_end_is_not_covered = solver->AddReifiedLinearConstraint( - 1, std::numeric_limits::max(), - {{lp_cumuls.back(), 1}, {previous_cover, -1}}); + 1, kint64max, {{lp_cumuls.back(), 1}, {previous_cover, -1}}); const int break_start_cover_ct = solver->AddLinearConstraint( - 0, std::numeric_limits::max(), - {{previous_cover, 1}, {lp_break.start, -1}}); + 0, kint64max, {{previous_cover, 1}, {lp_break.start, -1}}); solver->SetEnforcementLiteral(break_start_cover_ct, route_end_is_not_covered); previous_cover = cover; } - solver->AddLinearConstraint(0, std::numeric_limits::max(), + solver->AddLinearConstraint(0, kint64max, {{previous_cover, 1}, {lp_cumuls.back(), -1}}); } return true; } // NOLINT(readability/fn_size) +void DimensionCumulOptimizerCore::SetRouteCumulCosts( + int vehicle, int64_t cumul_offset, int64_t total_fixed_transit, + RoutingLinearSolverWrapper* solver, int64_t* route_transit_cost, + int64_t* route_cost_offset) { + const std::vector& lp_cumuls = current_route_cumul_variables_; + const std::vector& path = current_route_nodes_; + const int path_size = path.size(); + // Add soft upper bounds. + for (int pos = 0; pos < path_size; ++pos) { + const int64_t node = path[pos]; + if (!dimension_->HasCumulVarSoftUpperBound(node)) continue; + const int64_t coef = dimension_->GetCumulVarSoftUpperBoundCoefficient(node); + if (coef == 0) continue; + int64_t bound = dimension_->GetCumulVarSoftUpperBound(node); + if (bound < cumul_offset && route_cost_offset != nullptr) { + // Add coef * (cumul_offset - bound) to the cost offset. + *route_cost_offset = CapAdd(*route_cost_offset, + CapProd(CapSub(cumul_offset, bound), coef)); + } + bound = std::max(0, CapSub(bound, cumul_offset)); + if (current_route_max_cumuls_[pos] <= bound) { + // constraint is never violated. + continue; + } + const int soft_ub_diff = solver->CreateNewPositiveVariable(); + SET_DEBUG_VARIABLE_NAME(solver, soft_ub_diff, + absl::StrFormat("soft_ub_diff(%ld)", pos)); + solver->SetObjectiveCoefficient(soft_ub_diff, coef); + // cumul - soft_ub_diff <= bound. + const int ct = solver->CreateNewConstraint(kint64min, bound); + solver->SetCoefficient(ct, lp_cumuls[pos], 1); + solver->SetCoefficient(ct, soft_ub_diff, -1); + } + // Add soft lower bounds. + for (int pos = 0; pos < path_size; ++pos) { + const int64_t node = path[pos]; + if (!dimension_->HasCumulVarSoftLowerBound(node)) continue; + const int64_t coef = dimension_->GetCumulVarSoftLowerBoundCoefficient(node); + if (coef == 0) continue; + const int64_t bound = std::max( + 0, CapSub(dimension_->GetCumulVarSoftLowerBound(node), cumul_offset)); + if (current_route_min_cumuls_[pos] >= bound) { + // constraint is never violated. + continue; + } + const int soft_lb_diff = solver->CreateNewPositiveVariable(); + SET_DEBUG_VARIABLE_NAME(solver, soft_lb_diff, + absl::StrFormat("soft_lb_diff(%ld)", pos)); + solver->SetObjectiveCoefficient(soft_lb_diff, coef); + // bound - cumul <= soft_lb_diff + const int ct = solver->CreateNewConstraint(bound, kint64max); + solver->SetCoefficient(ct, lp_cumuls[pos], 1); + solver->SetCoefficient(ct, soft_lb_diff, 1); + } + + // Add span and slack costs. + // NOTE: The fixed transit is removed from the span cost since it doesn't + // affect the optimization of the scheduling of the route. + const int64_t span_cost_coef = + dimension_->GetSpanCostCoefficientForVehicle(vehicle); + const int64_t slack_cost_coef = CapAdd( + span_cost_coef, dimension_->GetSlackCostCoefficientForVehicle(vehicle)); + if (slack_cost_coef > 0) { + // span_without_fixed_transit_var = + // end_cumul - start_cumul - total_fixed_transit + const int span_without_fixed_transit_var = + solver->CreateNewPositiveVariable(); + SET_DEBUG_VARIABLE_NAME(solver, span_without_fixed_transit_var, + "span_without_fixed_transit_var"); + solver->AddLinearConstraint(total_fixed_transit, total_fixed_transit, + {{lp_cumuls.back(), 1}, + {lp_cumuls.front(), -1}, + {span_without_fixed_transit_var, -1}}); + solver->SetObjectiveCoefficient(span_without_fixed_transit_var, + slack_cost_coef); + } + // Add soft span cost. + if (dimension_->HasSoftSpanUpperBounds()) { + const BoundCost bound_cost = + dimension_->GetSoftSpanUpperBoundForVehicle(vehicle); + if (bound_cost.bound < kint64max && bound_cost.cost > 0) { + const int span_violation = solver->CreateNewPositiveVariable(); + SET_DEBUG_VARIABLE_NAME(solver, span_violation, "span_violation"); + // end - start <= bound + span_violation + const int violation = + solver->CreateNewConstraint(kint64min, bound_cost.bound); + solver->SetCoefficient(violation, lp_cumuls.back(), 1.0); + solver->SetCoefficient(violation, lp_cumuls.front(), -1.0); + solver->SetCoefficient(violation, span_violation, -1.0); + // Add span_violation * cost to objective. + solver->SetObjectiveCoefficient(span_violation, bound_cost.cost); + } + } + if (solver->IsCPSATSolver() && + dimension_->HasQuadraticCostSoftSpanUpperBounds()) { + // NOTE: the quadratic soft bound might be different from the one above. + const BoundCost bound_cost = + dimension_->GetQuadraticCostSoftSpanUpperBoundForVehicle(vehicle); + if (bound_cost.bound < kint64max && bound_cost.cost > 0) { + const int span_violation = solver->CreateNewPositiveVariable(); + SET_DEBUG_VARIABLE_NAME( + solver, span_violation, + absl::StrFormat("quadratic_span_violation(%ld)", vehicle)); + // end - start <= bound + span_violation + const int violation = + solver->CreateNewConstraint(kint64min, bound_cost.bound); + solver->SetCoefficient(violation, lp_cumuls.back(), 1.0); + solver->SetCoefficient(violation, lp_cumuls.front(), -1.0); + solver->SetCoefficient(violation, span_violation, -1.0); + // Add variable squared_span_violation, equal to span_violation². + const int squared_span_violation = solver->CreateNewPositiveVariable(); + SET_DEBUG_VARIABLE_NAME( + solver, squared_span_violation, + absl::StrFormat("squared_span_violation(%ld)", vehicle)); + solver->AddProductConstraint(squared_span_violation, + {span_violation, span_violation}); + // Add squared_span_violation * cost to objective. + solver->SetObjectiveCoefficient(squared_span_violation, bound_cost.cost); + } + } + // Add global span constraint. + if (dimension_->global_span_cost_coefficient() > 0) { + // min_start_cumul_ <= cumuls[start] + int ct = solver->CreateNewConstraint(kint64min, 0); + solver->SetCoefficient(ct, min_start_cumul_, 1); + solver->SetCoefficient(ct, lp_cumuls.front(), -1); + // max_end_cumul_ >= cumuls[end] + ct = solver->CreateNewConstraint(0, kint64max); + solver->SetCoefficient(ct, max_end_cumul_, 1); + solver->SetCoefficient(ct, lp_cumuls.back(), -1); + } + // Fill transit cost if specified. + if (route_transit_cost != nullptr) { + if (span_cost_coef > 0) { + *route_transit_cost = CapProd(total_fixed_transit, span_cost_coef); + } else { + *route_transit_cost = 0; + } + } +} + namespace { bool AllValuesContainedExcept(const IntVar& var, absl::Span values, const absl::flat_hash_set& ignored_values) { @@ -2503,8 +2697,7 @@ bool DimensionCumulOptimizerCore::SetGlobalConstraints( << " has a self-precedence on node " << first_node << "."; // cumul[second_node] - cumul[first_node] >= offset. - const int ct = solver->CreateNewConstraint( - offset, std::numeric_limits::max()); + const int ct = solver->CreateNewConstraint(offset, kint64max); solver->SetCoefficient(ct, second_cumul_var, 1); solver->SetCoefficient(ct, first_cumul_var, -1); } @@ -2704,19 +2897,14 @@ bool DimensionCumulOptimizerCore::SetGlobalConstraintsForResourceAssignment( #undef SET_DEBUG_VARIABLE_NAME void DimensionCumulOptimizerCore::SetValuesFromLP( - absl::Span lp_variables, int64_t offset, + absl::Span lp_variables, int64_t offset, int64_t default_value, RoutingLinearSolverWrapper* solver, std::vector* lp_values) const { if (lp_values == nullptr) return; - lp_values->assign(lp_variables.size(), std::numeric_limits::min()); + lp_values->assign(lp_variables.size(), default_value); for (int i = 0; i < lp_variables.size(); i++) { const int lp_var = lp_variables[i]; - if (lp_var < 0) continue; // Keep default value, kint64min. - const double lp_value_double = solver->GetValue(lp_var); - const int64_t lp_value_int64 = - (lp_value_double >= std::numeric_limits::max()) - ? std::numeric_limits::max() - : MathUtil::FastInt64Round(lp_value_double); - (*lp_values)[i] = CapAdd(lp_value_int64, offset); + if (lp_var < 0) continue; // Keep default value. + (*lp_values)[i] = CapAdd(solver->GetVariableValue(lp_var), offset); } } @@ -2762,7 +2950,8 @@ void DimensionCumulOptimizerCore::SetResourceIndices( for (int rc = 0; rc < num_resource_classes; rc++) { const int assignment_var = resource_class_to_vehicle_assignment_vars[rc * num_vehicles + v]; - if (assignment_var >= 0 && solver->GetValue(assignment_var) == 1) { + if (assignment_var >= 0 && + solver->GetVariableValue(assignment_var) == 1) { // This resource class is assigned to this vehicle. const std::vector& class_resource_indices = resource_indices_per_class[RCIndex(rc)]; @@ -2784,7 +2973,8 @@ void DimensionCumulOptimizerCore::SetResourceIndices( GlobalDimensionCumulOptimizer::GlobalDimensionCumulOptimizer( const RoutingDimension* dimension, - RoutingSearchParameters::SchedulingSolver solver_type) + RoutingSearchParameters::SchedulingSolver solver_type, + RoutingSearchStats* search_stats) : optimizer_core_(dimension, /*use_precedence_propagator=*/ !dimension->GetNodePrecedences().empty()) { @@ -2794,11 +2984,11 @@ GlobalDimensionCumulOptimizer::GlobalDimensionCumulOptimizer( /*is_relaxation=*/!dimension->model() ->GetDimensionResourceGroupIndices(dimension) .empty(), - GetGlopParametersForGlobalLP()); + GetGlopParametersForGlobalLP(), search_stats); break; } case RoutingSearchParameters::SCHEDULING_CP_SAT: { - solver_ = std::make_unique(); + solver_ = std::make_unique(search_stats); break; } default: @@ -3213,12 +3403,11 @@ std::string DomainToString( return absl::StrFormat("= %s", Int64ToStr(domain->Get(0))); } else if (domain->Get(0) == 0 && domain->Get(1) == 1) { return "∈ Binary"; - } else if (domain->Get(0) == std::numeric_limits::min() && - domain->Get(1) == std::numeric_limits::max()) { + } else if (domain->Get(0) == kint64min && domain->Get(1) == kint64max) { return "∈ ℝ"; - } else if (domain->Get(0) == std::numeric_limits::min()) { + } else if (domain->Get(0) == kint64min) { return absl::StrFormat("≤ %s", Int64ToStr(domain->Get(1))); - } else if (domain->Get(1) == std::numeric_limits::max()) { + } else if (domain->Get(1) == kint64max) { return absl::StrFormat("≥ %s", Int64ToStr(domain->Get(0))); } return absl::StrFormat("∈ [%ls, %s]", Int64ToStr(domain->Get(0)), @@ -3239,12 +3428,7 @@ std::string VariableToString( if (response_.IsInitialized() && variable.IsInitialized() && (response_.status() == sat::CpSolverStatus::OPTIMAL || response_.status() == sat::CpSolverStatus::FEASIBLE)) { - const double lp_value_double = response_.solution(index); - const int64_t lp_value_int64 = - (lp_value_double >= std::numeric_limits::max()) - ? std::numeric_limits::max() - : MathUtil::FastInt64Round(lp_value_double); - s += Int64ToStr(lp_value_int64) + " "; + s += Int64ToStr(response_.solution(index)) + " "; } else { s += "? "; } @@ -3534,4 +3718,4 @@ std::string RoutingCPSatWrapper::PrintModel() const { return s; } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_lp_scheduling.h b/ortools/routing/lp_scheduling.h similarity index 92% rename from ortools/constraint_solver/routing_lp_scheduling.h rename to ortools/routing/lp_scheduling.h index 118c6ab4d2f..ca526f41f80 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.h +++ b/ortools/routing/lp_scheduling.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_LP_SCHEDULING_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_LP_SCHEDULING_H_ +#ifndef OR_TOOLS_ROUTING_LP_SCHEDULING_H_ +#define OR_TOOLS_ROUTING_LP_SCHEDULING_H_ #include #include @@ -34,13 +34,13 @@ #include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/base/mathutil.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/glop/lp_solver.h" #include "ortools/glop/parameters.pb.h" #include "ortools/lp_data/lp_data.h" #include "ortools/lp_data/lp_types.h" #include "ortools/port/proto_utils.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_solver.h" #include "ortools/sat/model.h" @@ -48,7 +48,7 @@ #include "ortools/util/piecewise_linear_function.h" #include "ortools/util/sorted_interval_list.h" -namespace operations_research { +namespace operations_research::routing { // Classes to solve dimension cumul placement (aka scheduling) problems using // linear programming. @@ -171,6 +171,8 @@ class RoutingLinearSolverWrapper { public: static const int kNoConstraint = -1; + explicit RoutingLinearSolverWrapper(RoutingSearchStats* search_stats) + : search_stats_(search_stats) {} virtual ~RoutingLinearSolverWrapper() = default; virtual void Clear() = 0; virtual int CreateNewPositiveVariable() = 0; @@ -195,7 +197,7 @@ class RoutingLinearSolverWrapper { virtual void SetEnforcementLiteral(int ct, int condition) = 0; virtual DimensionSchedulingStatus Solve(absl::Duration duration_limit) = 0; virtual int64_t GetObjectiveValue() const = 0; - virtual double GetValue(int index) const = 0; + virtual int64_t GetVariableValue(int index) const = 0; virtual bool SolutionIsInteger() const = 0; // This function is meant to override the parameters of the solver. @@ -273,12 +275,17 @@ class RoutingLinearSolverWrapper { SetEnforcementLiteral(within_bounds_ct, within_bounds); return within_bounds; } + + protected: + RoutingSearchStats* const search_stats_; }; class RoutingGlopWrapper : public RoutingLinearSolverWrapper { public: - RoutingGlopWrapper(bool is_relaxation, const glop::GlopParameters& parameters) - : is_relaxation_(is_relaxation) { + RoutingGlopWrapper(bool is_relaxation, const glop::GlopParameters& parameters, + RoutingSearchStats* search_stats) + : RoutingLinearSolverWrapper(search_stats), + is_relaxation_(is_relaxation) { lp_solver_.SetParameters(parameters); linear_program_.SetMaximizationProblem(false); } @@ -380,7 +387,8 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper { if (coefficient != 0) { const double normalized_coeff = coefficient / max_coefficient; SetCoefficient(ct.value(), variable, normalized_coeff); - normalized_objective_value += normalized_coeff * GetValue(variable); + normalized_objective_value += + normalized_coeff * GetValueDouble(glop::ColIndex(variable)); } } normalized_objective_value = std::max( @@ -406,6 +414,7 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper { linear_program_.NotifyThatColumnsAreClean(); VLOG(2) << linear_program_.Dump(); const glop::ProblemStatus status = lp_solver_.Solve(linear_program_); + if (search_stats_) search_stats_->num_glop_calls_in_lp_scheduling++; const bool feasible_only = status == glop::ProblemStatus::PRIMAL_FEASIBLE; if (status != glop::ProblemStatus::OPTIMAL && status != glop::ProblemStatus::IMPRECISE && !feasible_only) { @@ -415,11 +424,7 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper { return DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY; } for (const auto& allowed_interval : allowed_intervals_) { - const double value_double = GetValue(allowed_interval.first); - const int64_t value = - (value_double >= std::numeric_limits::max()) - ? std::numeric_limits::max() - : MathUtil::FastInt64Round(value_double); + const int64_t value = GetVariableValue(allowed_interval.first); const SortedDisjointIntervalList* const interval_list = allowed_interval.second.get(); const auto it = interval_list->FirstIntervalGreaterOrEqual(value); @@ -433,10 +438,13 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper { return DimensionSchedulingStatus::OPTIMAL; } int64_t GetObjectiveValue() const override { - return MathUtil::FastInt64Round(lp_solver_.GetObjectiveValue()); + return MathUtil::Round(lp_solver_.GetObjectiveValue()); } - double GetValue(int index) const override { - return lp_solver_.variable_values()[glop::ColIndex(index)]; + int64_t GetVariableValue(int index) const override { + const double value_double = GetValueDouble(glop::ColIndex(index)); + return (value_double >= std::numeric_limits::max()) + ? std::numeric_limits::max() + : MathUtil::Round(value_double); } bool SolutionIsInteger() const override { return linear_program_.SolutionIsInteger(lp_solver_.variable_values(), @@ -455,6 +463,10 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper { std::string PrintModel() const override { return linear_program_.Dump(); } private: + double GetValueDouble(glop::ColIndex index) const { + return lp_solver_.variable_values()[index]; + } + const bool is_relaxation_; glop::LinearProgram linear_program_; glop::LPSolver lp_solver_; @@ -464,8 +476,9 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper { class RoutingCPSatWrapper : public RoutingLinearSolverWrapper { public: - RoutingCPSatWrapper() { - parameters_.set_num_search_workers(1); + explicit RoutingCPSatWrapper(RoutingSearchStats* const search_stats) + : RoutingLinearSolverWrapper(search_stats) { + parameters_.set_num_workers(1); // Keeping presolve but with 1 iteration; as of 10/2023 it is // significantly faster than both full presolve and no presolve. parameters_.set_cp_model_presolve(true); @@ -480,6 +493,7 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper { parameters_.set_cut_level(0); parameters_.set_add_lp_constraints_lazily(false); parameters_.set_use_absl_random(false); + parameters_.set_alternative_pool_size(0); } ~RoutingCPSatWrapper() override {} void Clear() override { @@ -646,6 +660,7 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper { sat::Model model; model.Add(sat::NewSatParameters(parameters_)); response_ = sat::SolveCpModel(model_, &model); + if (search_stats_) search_stats_->num_cp_sat_calls_in_lp_scheduling++; VLOG(2) << response_; DCHECK_NE(response_.status(), sat::CpSolverStatus::MODEL_INVALID); if (response_.status() == sat::CpSolverStatus::OPTIMAL || @@ -661,9 +676,9 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper { return DimensionSchedulingStatus::INFEASIBLE; } int64_t GetObjectiveValue() const override { - return MathUtil::FastInt64Round(response_.objective_value()); + return MathUtil::Round(response_.objective_value()); } - double GetValue(int index) const override { + int64_t GetVariableValue(int index) const override { return response_.solution(index); } bool SolutionIsInteger() const override { return true; } @@ -776,6 +791,20 @@ class DimensionCumulOptimizerCore { const Resource* resource, RoutingLinearSolverWrapper* solver, std::vector* cumul_values, std::vector* break_values); + struct TransitTargetCost { + double threshold_ratio; + int64_t cost_coefficient_below_threshold; + int64_t cost_coefficient_above_threshold; + }; + DimensionSchedulingStatus OptimizeSingleRouteWithTransitTargets( + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, + absl::Span transit_targets, + TransitTargetCost transit_target_cost, RoutingLinearSolverWrapper* solver, + std::vector* optimal_transits, + std::vector* optimal_cumuls, + std::vector* optimal_breaks); + const RoutingDimension* dimension() const { return dimension_; } private: @@ -802,17 +831,29 @@ class DimensionCumulOptimizerCore { bool SetRouteCumulConstraints( int vehicle, const std::function& next_accessor, const std::function& transit_accessor, + absl::Span transit_targets, const RouteDimensionTravelInfo* dimension_travel_info, int64_t cumul_offset, bool optimize_costs, RoutingLinearSolverWrapper* solver, int64_t* route_transit_cost, int64_t* route_cost_offset); + // Sets the objective coefficients related to the cumuls and transits of the + // route in the solver. Supposes that the current_route_cumul_variables_ and + // current_route_nodes_ have correctly been initialized prior to calling this + // method. + void SetRouteCumulCosts(int vehicle, int64_t cumul_offset, + int64_t total_fixed_transit, + RoutingLinearSolverWrapper* solver, + int64_t* route_transit_cost, + int64_t* route_cost_offset); + // Sets the constraints for all variables related to travel. Handles // static or time-dependent travel values. // Returns false if some infeasibility was detected, true otherwise. bool SetRouteTravelConstraints( const RouteDimensionTravelInfo* dimension_travel_info, - absl::Span lp_slacks, absl::Span fixed_transit, + absl::Span lp_slacks, absl::Span fixed_transits, + absl::Span transit_targets, RoutingLinearSolverWrapper* solver); // Sets the global constraints on the dimension, and adds global objective @@ -832,6 +873,7 @@ class DimensionCumulOptimizerCore { int64_t cumul_offset, RoutingLinearSolverWrapper* solver); void SetValuesFromLP(absl::Span lp_variables, int64_t offset, + int64_t default_value, RoutingLinearSolverWrapper* solver, std::vector* lp_values) const; @@ -853,10 +895,14 @@ class DimensionCumulOptimizerCore { std::unique_ptr propagator_; std::vector current_route_min_cumuls_; std::vector current_route_max_cumuls_; + // Stores the nodes on the current route. + std::vector current_route_nodes_; const RoutingDimension* const dimension_; // Scheduler variables for current route cumuls and for all nodes cumuls. std::vector current_route_cumul_variables_; std::vector index_to_cumul_variable_; + // Scheduler variables for current route transits. + std::vector current_route_variable_transit_variables_; // Scheduler variables for current route breaks and all vehicle breaks. // There are two variables for each break: start and end. // current_route_break_variables_ has variables corresponding to @@ -896,7 +942,8 @@ class LocalDimensionCumulOptimizer { public: LocalDimensionCumulOptimizer( const RoutingDimension* dimension, - RoutingSearchParameters::SchedulingSolver solver_type); + RoutingSearchParameters::SchedulingSolver solver_type, + RoutingSearchStats* search_stats); // If feasible, computes the optimal cost of the route performed by a vehicle, // minimizing cumul soft lower and upper bound costs and vehicle span costs, @@ -973,6 +1020,18 @@ class LocalDimensionCumulOptimizer { const RoutingModel::ResourceGroup::Resource* resource, std::vector* packed_cumuls, std::vector* packed_breaks); + // TODO(user): Add a "resource" to the method. + // TODO(user): Also pack the route at the end of the optimization. + // --> Merge with the "packing" method ? + DimensionSchedulingStatus ComputeRouteCumulsWithTransitTargets( + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, + absl::Span transit_targets, + DimensionCumulOptimizerCore::TransitTargetCost transit_target_cost, + std::vector* optimal_transits, + std::vector* optimal_cumuls, + std::vector* optimal_breaks); + const RoutingDimension* dimension() const { return optimizer_core_.dimension(); } @@ -986,7 +1045,8 @@ class GlobalDimensionCumulOptimizer { public: GlobalDimensionCumulOptimizer( const RoutingDimension* dimension, - RoutingSearchParameters::SchedulingSolver solver_type); + RoutingSearchParameters::SchedulingSolver solver_type, + RoutingSearchStats* search_stats); // If feasible, computes the optimal cost of the entire model with regards to // the optimizer_core_'s dimension costs, minimizing cumul soft lower/upper // bound costs and vehicle/global span costs, and stores it in "optimal_cost" @@ -1113,6 +1173,6 @@ std::vector PiecewiseLinearFunctionToSlopeAndYIntercept( std::vector SlopeAndYInterceptToConvexityRegions( absl::Span slope_and_y_intercept); -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_LP_SCHEDULING_H_ +#endif // OR_TOOLS_ROUTING_LP_SCHEDULING_H_ diff --git a/ortools/constraint_solver/routing_neighborhoods.cc b/ortools/routing/neighborhoods.cc similarity index 99% rename from ortools/constraint_solver/routing_neighborhoods.cc rename to ortools/routing/neighborhoods.cc index 04e66361117..1bcd2b05869 100644 --- a/ortools/constraint_solver/routing_neighborhoods.cc +++ b/ortools/routing/neighborhoods.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_neighborhoods.h" +#include "ortools/routing/neighborhoods.h" #include #include @@ -24,11 +24,11 @@ #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing_types.h" -#include "ortools/constraint_solver/routing_utils.h" +#include "ortools/routing/types.h" +#include "ortools/routing/utils.h" #include "ortools/util/saturated_arithmetic.h" -namespace operations_research { +namespace operations_research::routing { using NeighborAccessor = std::function&(/*node=*/int, /*start_node=*/int)>; @@ -1848,4 +1848,4 @@ LocalSearchOperator* MakeExchangeSubtrip( nullptr, pairs); } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_neighborhoods.h b/ortools/routing/neighborhoods.h similarity index 99% rename from ortools/constraint_solver/routing_neighborhoods.h rename to ortools/routing/neighborhoods.h index bd31c5d588a..bcd837e1847 100644 --- a/ortools/constraint_solver/routing_neighborhoods.h +++ b/ortools/routing/neighborhoods.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_NEIGHBORHOODS_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_NEIGHBORHOODS_H_ +#ifndef OR_TOOLS_ROUTING_NEIGHBORHOODS_H_ +#define OR_TOOLS_ROUTING_NEIGHBORHOODS_H_ #include @@ -26,10 +26,10 @@ #include "absl/types/span.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing_types.h" +#include "ortools/routing/types.h" #include "ortools/util/bitset.h" -namespace operations_research { +namespace operations_research::routing { /// Relocate neighborhood which moves chains of neighbors. /// The operator starts by relocating a node n after a node m, then continues @@ -949,6 +949,6 @@ LocalSearchOperator* MakeExchangeSubtrip( std::function start_empty_path_class, absl::Span pairs); -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_NEIGHBORHOODS_H_ +#endif // OR_TOOLS_ROUTING_NEIGHBORHOODS_H_ diff --git a/ortools/constraint_solver/routing_parameters.cc b/ortools/routing/parameters.cc similarity index 81% rename from ortools/constraint_solver/routing_parameters.cc rename to ortools/routing/parameters.cc index efdb0c74043..1143540fcd0 100644 --- a/ortools/constraint_solver/routing_parameters.cc +++ b/ortools/routing/parameters.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/parameters.h" #include #include @@ -19,27 +19,31 @@ #include #include "absl/container/flat_hash_map.h" +#include "absl/log/log.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" +#include "absl/strings/string_view.h" #include "absl/time/time.h" #include "google/protobuf/descriptor.h" #include "google/protobuf/duration.pb.h" +#include "google/protobuf/extension_set.h" #include "google/protobuf/message.h" #include "ortools/base/logging.h" #include "ortools/base/proto_enum_utils.h" #include "ortools/base/protoutil.h" #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_ils.pb.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/constraint_solver/solver_parameters.pb.h" #include "ortools/port/proto_utils.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/heuristic_parameters.pb.h" +#include "ortools/routing/ils.pb.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/sat/sat_parameters.pb.h" #include "ortools/util/optional_boolean.pb.h" #include "ortools/util/testing_utils.h" -namespace operations_research { +namespace operations_research::routing { RoutingModelParameters DefaultRoutingModelParameters() { RoutingModelParameters parameters; @@ -67,7 +71,8 @@ IteratedLocalSearchParameters CreateDefaultIteratedLocalSearchParameters() { // ->mutable_spatially_close_routes() // ->set_num_ruined_routes(2); rr->set_ruin_composition_strategy(RuinCompositionStrategy::UNSET); - rr->set_recreate_strategy(FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION); + rr->mutable_recreate_strategy()->set_heuristic( + FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION); rr->set_route_selection_neighbors_ratio(1.0); rr->set_route_selection_min_neighbors(10); rr->set_route_selection_max_neighbors(100); @@ -86,10 +91,10 @@ RoutingSearchParameters CreateDefaultRoutingSearchParameters() { RoutingSearchParameters p; p.set_first_solution_strategy(FirstSolutionStrategy::AUTOMATIC); p.set_use_unfiltered_first_solution_strategy(false); - p.set_savings_neighbors_ratio(1); - p.set_savings_max_memory_usage_bytes(6e9); - p.set_savings_add_reverse_arcs(false); - p.set_savings_arc_coefficient(1); + p.mutable_savings_parameters()->set_neighbors_ratio(1); + p.mutable_savings_parameters()->set_max_memory_usage_bytes(6e9); + p.mutable_savings_parameters()->set_add_reverse_arcs(false); + p.mutable_savings_parameters()->set_arc_coefficient(1); p.set_cheapest_insertion_farthest_seeds_ratio(0); p.set_cheapest_insertion_first_solution_neighbors_ratio(1); p.set_cheapest_insertion_first_solution_min_neighbors(1); @@ -98,10 +103,11 @@ RoutingSearchParameters CreateDefaultRoutingSearchParameters() { p.set_cheapest_insertion_first_solution_use_neighbors_ratio_for_initialization( // NOLINT false); p.set_cheapest_insertion_add_unperformed_entries(false); - p.set_local_cheapest_insertion_pickup_delivery_strategy( - RoutingSearchParameters::BEST_PICKUP_THEN_BEST_DELIVERY); - p.set_local_cheapest_cost_insertion_pickup_delivery_strategy( - RoutingSearchParameters::BEST_PICKUP_DELIVERY_PAIR); + p.mutable_local_cheapest_insertion_parameters()->set_pickup_delivery_strategy( + LocalCheapestInsertionParameters::BEST_PICKUP_THEN_BEST_DELIVERY); + p.mutable_local_cheapest_cost_insertion_parameters() + ->set_pickup_delivery_strategy( + LocalCheapestInsertionParameters::BEST_PICKUP_DELIVERY_PAIR); RoutingSearchParameters::LocalSearchNeighborhoodOperators* o = p.mutable_local_search_operators(); o->set_use_relocate(BOOL_TRUE); @@ -161,7 +167,7 @@ RoutingSearchParameters CreateDefaultRoutingSearchParameters() { p.set_use_cp_sat(BOOL_FALSE); p.set_use_generalized_cp_sat(BOOL_FALSE); p.mutable_sat_parameters()->set_linearization_level(2); - p.mutable_sat_parameters()->set_num_search_workers(1); + p.mutable_sat_parameters()->set_num_workers(1); p.set_report_intermediate_cp_sat_solutions(false); p.set_fallback_to_cp_sat_size_threshold(20); p.set_continuous_scheduling_solver(RoutingSearchParameters::SCHEDULING_GLOP); @@ -258,6 +264,72 @@ bool IsValidNonNegativeDuration(const google::protobuf::Duration& d) { status_or_duration.value() >= absl::ZeroDuration(); } +// Searches for errors in LocalCheapestInsertionParameters and appends them to +// the given `errors` vector. +void FindErrorsInLocalCheapestInsertionParameters( + absl::string_view prefix, + const LocalCheapestInsertionParameters& parameters, + std::vector& errors) { + using absl::StrCat; + + absl::flat_hash_map< + LocalCheapestInsertionParameters::InsertionSortingProperty, int> + sorting_properties_map; + for (const LocalCheapestInsertionParameters::InsertionSortingProperty + property : + REPEATED_ENUM_ADAPTER(parameters, insertion_sorting_properties)) { + if (property == + LocalCheapestInsertionParameters::SORTING_PROPERTY_UNSPECIFIED) { + errors.emplace_back(StrCat( + prefix, " - Invalid insertion sorting property: ", + LocalCheapestInsertionParameters::InsertionSortingProperty_Name( + LocalCheapestInsertionParameters::SORTING_PROPERTY_UNSPECIFIED))); + } + const int occurrences = sorting_properties_map[property]++; + if (occurrences == 2) { + errors.emplace_back(StrCat( + prefix, " - Duplicate insertion sorting property: ", + LocalCheapestInsertionParameters::InsertionSortingProperty_Name( + property))); + } + if (property == LocalCheapestInsertionParameters::SORTING_PROPERTY_RANDOM && + parameters.insertion_sorting_properties().size() > 1) { + errors.emplace_back( + StrCat(prefix, + " - SORTING_PROPERTY_RANDOM cannot be used in conjunction " + "with other properties.")); + } + } +} + +void FindErrorsInRecreateParameters( + const FirstSolutionStrategy::Value heuristic, + const RecreateParameters& parameters, std::vector& errors) { + switch (parameters.parameters_case()) { + case RecreateParameters::kLocalCheapestInsertion: { + const std::string prefix = + heuristic == FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION + ? "Local cheapest insertion (recreate heuristic)" + : "Local cheapest cost insertion (recreate heuristic)"; + FindErrorsInLocalCheapestInsertionParameters( + prefix, parameters.local_cheapest_insertion(), errors); + break; + } + default: + LOG(DFATAL) << "Unsupported unset recreate parameters."; + break; + } +} + +std::string GetRecreateParametersName(const RecreateParameters& parameters) { + switch (parameters.parameters_case()) { + case RecreateParameters::kLocalCheapestInsertion: + return "local_cheapest_insertion"; + case RecreateParameters::PARAMETERS_NOT_SET: + return "PARAMETERS_NOT_SET"; + } +} + // Searches for errors in ILS parameters and appends them to the given `errors` // vector. void FindErrorsInIteratedLocalSearchParameters( @@ -373,12 +445,52 @@ void FindErrorsInIteratedLocalSearchParameters( "route_selection_max_neighbors")); } - if (rr.recreate_strategy() == FirstSolutionStrategy::UNSET) { + const FirstSolutionStrategy::Value recreate_heuristic = + rr.recreate_strategy().heuristic(); + if (recreate_heuristic == FirstSolutionStrategy::UNSET) { errors.emplace_back( StrCat("Invalid value for " "iterated_local_search_parameters.ruin_recreate_parameters." - "recreate_strategy: ", - rr.recreate_strategy())); + "recreate_strategy.heuristic: ", + FirstSolutionStrategy::Value_Name(recreate_heuristic))); + } + + if (rr.recreate_strategy().has_parameters()) { + const RecreateParameters& recreate_params = + rr.recreate_strategy().parameters(); + if (recreate_params.parameters_case() == + RecreateParameters::PARAMETERS_NOT_SET) { + errors.emplace_back( + StrCat("Invalid value for " + "iterated_local_search_parameters.ruin_recreate_parameters." + "recreate_strategy.parameters: ", + GetRecreateParametersName(recreate_params))); + } else { + const absl::flat_hash_map + strategy_to_parameters_case_map = { + {FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION, + RecreateParameters::kLocalCheapestInsertion}, + {FirstSolutionStrategy::LOCAL_CHEAPEST_COST_INSERTION, + RecreateParameters::kLocalCheapestInsertion}}; + + const RecreateParameters& recreate_params = + rr.recreate_strategy().parameters(); + + if (const auto params = + strategy_to_parameters_case_map.find(recreate_heuristic); + params == strategy_to_parameters_case_map.end() || + recreate_params.parameters_case() != params->second) { + errors.emplace_back( + StrCat("recreate_strategy.heuristic is set to ", + FirstSolutionStrategy::Value_Name(recreate_heuristic), + " but recreate_strategy.parameters define ", + GetRecreateParametersName(recreate_params))); + } else { + FindErrorsInRecreateParameters(recreate_heuristic, recreate_params, + errors); + } + } } } @@ -484,20 +596,23 @@ std::vector FindErrorsInRoutingSearchParameters( } } #endif // !__ANDROID__ && !__wasm__ - if (const double ratio = search_parameters.savings_neighbors_ratio(); + if (const double ratio = + search_parameters.savings_parameters().neighbors_ratio(); std::isnan(ratio) || ratio <= 0 || ratio > 1) { - errors.emplace_back(StrCat("Invalid savings_neighbors_ratio: ", ratio)); + errors.emplace_back( + StrCat("Invalid savings_parameters.neighbors_ratio: ", ratio)); } if (const double max_memory = - search_parameters.savings_max_memory_usage_bytes(); + search_parameters.savings_parameters().max_memory_usage_bytes(); std::isnan(max_memory) || max_memory <= 0 || max_memory > 1e10) { - errors.emplace_back( - StrCat("Invalid savings_max_memory_usage_bytes: ", max_memory)); + errors.emplace_back(StrCat( + "Invalid savings_parameters.max_memory_usage_bytes: ", max_memory)); } - if (const double coefficient = search_parameters.savings_arc_coefficient(); + if (const double coefficient = + search_parameters.savings_parameters().arc_coefficient(); std::isnan(coefficient) || coefficient <= 0 || std::isinf(coefficient)) { errors.emplace_back( - StrCat("Invalid savings_arc_coefficient: ", coefficient)); + StrCat("Invalid savings_parameters.arc_coefficient: ", coefficient)); } if (const double ratio = search_parameters.cheapest_insertion_farthest_seeds_ratio(); @@ -531,33 +646,14 @@ std::vector FindErrorsInRoutingSearchParameters( "Invalid cheapest_insertion_ls_operator_min_neighbors: ", min_neighbors, ". Must be greater or equal to 1.")); } - { - absl::flat_hash_map - sorting_properties_map; - for (const RoutingSearchParameters::InsertionSortingProperty property : - REPEATED_ENUM_ADAPTER(search_parameters, - local_cheapest_insertion_sorting_properties)) { - if (property == RoutingSearchParameters::SORTING_PROPERTY_UNSPECIFIED) { - errors.emplace_back( - StrCat("Invalid local cheapest insertion sorting property: ", - RoutingSearchParameters::InsertionSortingProperty_Name( - RoutingSearchParameters::SORTING_PROPERTY_UNSPECIFIED))); - } - const int occurrences = sorting_properties_map[property]++; - if (occurrences == 2) { - errors.emplace_back(StrCat( - "Duplicate local cheapest insertion sorting property: ", - RoutingSearchParameters::InsertionSortingProperty_Name(property))); - } - if (property == RoutingSearchParameters::SORTING_PROPERTY_RANDOM && - search_parameters.local_cheapest_insertion_sorting_properties() - .size() > 1) { - errors.emplace_back( - StrCat("SORTING_PROPERTY_RANDOM cannot be used in conjunction " - "with other properties.")); - } - } - } + + FindErrorsInLocalCheapestInsertionParameters( + "Local cheapest insertion (first solution heuristic)", + search_parameters.local_cheapest_insertion_parameters(), errors); + FindErrorsInLocalCheapestInsertionParameters( + "Local cheapest cost insertion (first solution heuristic)", + search_parameters.local_cheapest_cost_insertion_parameters(), errors); + if (const double ratio = search_parameters.ls_operator_neighbors_ratio(); std::isnan(ratio) || ratio <= 0 || ratio > 1) { errors.emplace_back(StrCat("Invalid ls_operator_neighbors_ratio: ", ratio)); @@ -738,7 +834,7 @@ std::vector FindErrorsInRoutingSearchParameters( if (const sat::SatParameters& sat_parameters = search_parameters.sat_parameters(); sat_parameters.enumerate_all_solutions() && - (sat_parameters.num_search_workers() > 1 || + (sat_parameters.num_workers() > 1 || sat_parameters.interleave_search())) { errors.emplace_back( "sat_parameters.enumerate_all_solutions cannot be true in parallel" @@ -758,4 +854,4 @@ std::vector FindErrorsInRoutingSearchParameters( return errors; } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_parameters.h b/ortools/routing/parameters.h similarity index 81% rename from ortools/constraint_solver/routing_parameters.h rename to ortools/routing/parameters.h index 4afad16b72d..a355e46557d 100644 --- a/ortools/constraint_solver/routing_parameters.h +++ b/ortools/routing/parameters.h @@ -11,15 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_PARAMETERS_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_PARAMETERS_H_ +#ifndef OR_TOOLS_ROUTING_PARAMETERS_H_ +#define OR_TOOLS_ROUTING_PARAMETERS_H_ #include #include -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/parameters.pb.h" -namespace operations_research { +namespace operations_research::routing { RoutingModelParameters DefaultRoutingModelParameters(); RoutingSearchParameters DefaultRoutingSearchParameters(); @@ -35,6 +35,6 @@ std::string FindErrorInRoutingSearchParameters( std::vector FindErrorsInRoutingSearchParameters( const RoutingSearchParameters& search_parameters); -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_PARAMETERS_H_ +#endif // OR_TOOLS_ROUTING_PARAMETERS_H_ diff --git a/ortools/constraint_solver/routing_parameters.proto b/ortools/routing/parameters.proto similarity index 86% rename from ortools/constraint_solver/routing_parameters.proto rename to ortools/routing/parameters.proto index 2b1069c17cc..34b839e092e 100644 --- a/ortools/constraint_solver/routing_parameters.proto +++ b/ortools/routing/parameters.proto @@ -17,18 +17,19 @@ syntax = "proto3"; -option java_package = "com.google.ortools.constraintsolver"; +option java_package = "com.google.ortools.routing"; option java_multiple_files = true; -option csharp_namespace = "Google.OrTools.ConstraintSolver"; +option csharp_namespace = "Google.OrTools.Routing"; import "google/protobuf/duration.proto"; -import "ortools/constraint_solver/routing_enums.proto"; -import "ortools/constraint_solver/routing_ils.proto"; import "ortools/constraint_solver/solver_parameters.proto"; +import "ortools/routing/enums.proto"; +import "ortools/routing/heuristic_parameters.proto"; +import "ortools/routing/ils.proto"; import "ortools/sat/sat_parameters.proto"; import "ortools/util/optional_boolean.proto"; -package operations_research; +package operations_research.routing; // Parameters defining the search used to solve vehicle routing problems. // @@ -36,9 +37,9 @@ package operations_research; // then the routing library will pick its preferred value for that parameter // automatically: this should be the case for most parameters. // To see those "default" parameters, call GetDefaultRoutingSearchParameters(). -// Next ID: 68 +// Next ID: 71 message RoutingSearchParameters { - reserved 19, 65; + reserved 14, 15, 18, 19, 23, 49, 55, 65, 67; // First solution strategies, used as starting point of local search. FirstSolutionStrategy.Value first_solution_strategy = 1; @@ -48,26 +49,9 @@ message RoutingSearchParameters { // // Use filtered version of first solution strategy if available. bool use_unfiltered_first_solution_strategy = 2; - // Parameters specific to the Savings first solution heuristic. - // Ratio (in ]0, 1]) of neighbors to consider for each node when constructing - // the savings. If unspecified, its value is considered to be 1.0. - double savings_neighbors_ratio = 14; - // The number of neighbors considered for each node in the Savings heuristic - // is chosen so that the space used to store the savings doesn't exceed - // savings_max_memory_usage_bytes, which must be in ]0, 1e10]. - // NOTE: If both savings_neighbors_ratio and savings_max_memory_usage_bytes - // are specified, the number of neighbors considered for each node will be the - // minimum of the two numbers determined by these parameters. - double savings_max_memory_usage_bytes = 23; - // Add savings related to reverse arcs when finding the nearest neighbors - // of the nodes. - bool savings_add_reverse_arcs = 15; - // Coefficient of the cost of the arc for which the saving value is being - // computed: - // Saving(a-->b) = Cost(a-->end) + Cost(start-->b) - // - savings_arc_coefficient * Cost(a-->b) - // This parameter must be greater than 0, and its default value is 1. - double savings_arc_coefficient = 18; + + // Parameters for the Savings heuristic. + SavingsParameters savings_parameters = 70; // Ratio (between 0 and 1) of available vehicles in the model on which // farthest nodes of the model are inserted as seeds in the @@ -107,66 +91,12 @@ message RoutingSearchParameters { // the GlobalCheapestInsertion heuristic. bool cheapest_insertion_add_unperformed_entries = 40; - // In insertion-based heuristics, describes what positions must be considered - // when inserting a pickup/delivery pair, and in what order they are - // considered. - enum PairInsertionStrategy { - // Let the solver decide the set of positions and its ordering. - AUTOMATIC = 0; - // Consider all positions, by increasing (cost(pickup), cost(delivery)). - BEST_PICKUP_THEN_BEST_DELIVERY = 1; - // Consider all positions, by increasing by cost(pickup) + cost(delivery). - BEST_PICKUP_DELIVERY_PAIR = 2; - // Only consider insertion positions that are compatible with the multitour - // property, meaning a series of pickups may only start when the vehicle - // is not carrying any delivery. This setting is designed to explore much - // less possibilities than the full BEST_PICKUP_DELIVERY_PAIR. - // Order by increasing by cost(pickup) + cost(delivery). - BEST_PICKUP_DELIVERY_PAIR_MULTITOUR = 3; - } - // Choice of insertion strategy for pickup/delivery pairs, used in local - // cheapest insertion, both first solution heuristic and LNS. - PairInsertionStrategy local_cheapest_insertion_pickup_delivery_strategy = 49; - // Choice of insertion strategy for pickup/delivery pairs, used in local - // cheapest cost insertion, both first solution heuristic and LNS. - PairInsertionStrategy local_cheapest_cost_insertion_pickup_delivery_strategy = - 55; - - // Properties used to select in which order nodes or node pairs are considered - // in insertion heuristics. - enum InsertionSortingProperty { - // Invalid property. - SORTING_PROPERTY_UNSPECIFIED = 0; - // Selects nodes with the least number of allowed vehicles. - SORTING_PROPERTY_ALLOWED_VEHICLES = 1; - // Selects nodes with the highest penalty. - SORTING_PROPERTY_PENALTY = 2; - // Selects nodes with the highest penalty / number of allowed vehicles - // ratio. - SORTING_PROPERTY_PENALTY_OVER_ALLOWED_VEHICLES_RATIO = 3; - // Selects nodes that are on average the farthest from vehicles. - SORTING_PROPERTY_HIGHEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS = 4; - // Selects nodes that are on average the closest to vehicles. - SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS = 5; - // Select nodes with the smallest distance to the closest vehicle. - SORTING_PROPERTY_LOWEST_MIN_ARC_COST_TO_VEHICLE_START_ENDS = 6; - // Selects nodes that have a higher dimension usage on average, where the - // usage is determined as the ratio of node demand over vehicle capacity. - // Currently, this property only supports unary dimensions. - SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE = 7; - // Selects nodes in random order. - // This property cannot be used in conjunction with other properties. - SORTING_PROPERTY_RANDOM = 8; - } + // Parameters for the local cheapest insertion heuristic. + LocalCheapestInsertionParameters local_cheapest_insertion_parameters = 68; - // The properties used to sort insertion entries in the local cheapest - // insertion heuristic, in *decreasing* order of priority. The properties - // listed here are applied hierarchically, from highest to lowest priority. - // When no properties are provided - // (SORTING_PROPERTY_ALLOWED_VEHICLES, SORTING_PROPERTY_PENALTY) - // is used by default. - repeated InsertionSortingProperty - local_cheapest_insertion_sorting_properties = 67; + // Parameters for the local cheapest cost insertion heuristic. + LocalCheapestInsertionParameters local_cheapest_cost_insertion_parameters = + 69; // If true use minimum matching instead of minimal matching in the // Christofides algorithm. diff --git a/ortools/constraint_solver/routing_parameters_utils.cc b/ortools/routing/parameters_utils.cc similarity index 78% rename from ortools/constraint_solver/routing_parameters_utils.cc rename to ortools/routing/parameters_utils.cc index e2934d1c60b..43671d72b27 100644 --- a/ortools/constraint_solver/routing_parameters_utils.cc +++ b/ortools/routing/parameters_utils.cc @@ -11,24 +11,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_parameters_utils.h" +#include "ortools/routing/parameters_utils.h" #include #include "absl/types/span.h" #include "ortools/util/optional_boolean.pb.h" -namespace operations_research { +namespace operations_research::routing { -std::vector +std::vector GetLocalCheapestInsertionSortingProperties( absl::Span lci_insertion_sorting_properties) { - std::vector + std::vector sorting_properties; for (const int property : lci_insertion_sorting_properties) { sorting_properties.push_back( - static_cast( + static_cast( property)); } @@ -37,9 +37,9 @@ GetLocalCheapestInsertionSortingProperties( // ones with the highest penalty. if (sorting_properties.empty()) { sorting_properties.push_back( - RoutingSearchParameters::SORTING_PROPERTY_ALLOWED_VEHICLES); + LocalCheapestInsertionParameters::SORTING_PROPERTY_ALLOWED_VEHICLES); sorting_properties.push_back( - RoutingSearchParameters::SORTING_PROPERTY_PENALTY); + LocalCheapestInsertionParameters::SORTING_PROPERTY_PENALTY); } return sorting_properties; } @@ -57,4 +57,4 @@ void DisableAllLocalSearchOperators( } } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_parameters_utils.h b/ortools/routing/parameters_utils.h similarity index 58% rename from ortools/constraint_solver/routing_parameters_utils.h rename to ortools/routing/parameters_utils.h index e9dc893b33a..45b0603a399 100644 --- a/ortools/constraint_solver/routing_parameters_utils.h +++ b/ortools/routing/parameters_utils.h @@ -11,26 +11,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_PARAMETERS_UTILS_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_PARAMETERS_UTILS_H_ +#ifndef OR_TOOLS_ROUTING_PARAMETERS_UTILS_H_ +#define OR_TOOLS_ROUTING_PARAMETERS_UTILS_H_ #include #include "absl/types/span.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/heuristic_parameters.pb.h" +#include "ortools/routing/parameters.pb.h" -namespace operations_research { +namespace operations_research::routing { -// Takes RoutingSearchParameters::local_cheapest_insertion_sorting_properties in -// input and returns the ordered list of properties that is used to sort nodes -// when performing a local cheapest insertion first heuristic. -std::vector +// Takes LocalCheapestInsertionParameters::insertion_sorting_properties +// in input and returns the ordered list of properties that is used to sort +// nodes when performing a local cheapest insertion first heuristic. +std::vector GetLocalCheapestInsertionSortingProperties( absl::Span lci_insertion_sorting_properties); void DisableAllLocalSearchOperators( RoutingSearchParameters::LocalSearchNeighborhoodOperators* operators); -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_PARAMETERS_UTILS_H_ +#endif // OR_TOOLS_ROUTING_PARAMETERS_UTILS_H_ diff --git a/ortools/routing/parsers/BUILD.bazel b/ortools/routing/parsers/BUILD.bazel index 0ba5fb68895..883228d58ee 100644 --- a/ortools/routing/parsers/BUILD.bazel +++ b/ortools/routing/parsers/BUILD.bazel @@ -340,7 +340,7 @@ cc_library( hdrs = ["cvrptw_lib.h"], deps = [ "//ortools/base", - "//ortools/constraint_solver:routing", + "//ortools/routing", "//ortools/util:random_engine", ], ) @@ -352,7 +352,7 @@ cc_library( deps = [ ":capacity_planning_cc_proto", "//ortools/base", - "//ortools/constraint_solver:routing", + "//ortools/routing", "//ortools/util:random_engine", ], ) diff --git a/ortools/routing/parsers/CMakeLists.txt b/ortools/routing/parsers/CMakeLists.txt index c2eb296602c..a3114773c96 100644 --- a/ortools/routing/parsers/CMakeLists.txt +++ b/ortools/routing/parsers/CMakeLists.txt @@ -26,11 +26,14 @@ target_include_directories(${NAME} PUBLIC $ $) target_link_libraries(${NAME} PRIVATE + ZLIB::ZLIB absl::memory absl::strings absl::status absl::str_format protobuf::libprotobuf ${RE2_DEPS} - ${PROJECT_NAMESPACE}::ortools_proto) + ${PROJECT_NAMESPACE}::ortools_proto + ${PROJECT_NAMESPACE}::routing_proto +) #add_library(${PROJECT_NAMESPACE}::routing_parsers ALIAS ${NAME}) diff --git a/ortools/routing/parsers/cvrptw_lib.cc b/ortools/routing/parsers/cvrptw_lib.cc index 7997807b604..eb7d4f8e943 100644 --- a/ortools/routing/parsers/cvrptw_lib.cc +++ b/ortools/routing/parsers/cvrptw_lib.cc @@ -30,10 +30,10 @@ #include "absl/types/span.h" #include "ortools/base/logging.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/routing.h" -namespace operations_research { +namespace operations_research::routing { using NodeIndex = RoutingIndexManager::NodeIndex; @@ -168,7 +168,7 @@ void DisplayPlan(const RoutingIndexManager& manager, bool use_same_vehicle_costs, int64_t max_nodes_per_group, int64_t same_vehicle_cost, absl::Span dimension_names) { - std::vector dimensions; + std::vector dimensions; for (const std::string& dimension_name : dimension_names) { dimensions.push_back(&routing.GetDimensionOrDie(dimension_name)); } @@ -239,7 +239,7 @@ void DisplayPlan(const RoutingIndexManager& manager, while (true) { absl::StrAppendFormat(&plan_output, "%d ", manager.IndexToNode(order).value()); - for (const operations_research::RoutingDimension* dimension : dimensions) { + for (const RoutingDimension* dimension : dimensions) { str_append_variable(dimension->CumulVar(order), dimension->name()); operations_research::IntVar* const slack_var = routing.IsEnd(order) ? nullptr : dimension->SlackVar(order); @@ -254,4 +254,4 @@ void DisplayPlan(const RoutingIndexManager& manager, } LOG(INFO) << plan_output; } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/routing/parsers/cvrptw_lib.h b/ortools/routing/parsers/cvrptw_lib.h index ca75e9adbbf..7983bc3fa78 100644 --- a/ortools/routing/parsers/cvrptw_lib.h +++ b/ortools/routing/parsers/cvrptw_lib.h @@ -25,9 +25,9 @@ #include "absl/types/span.h" #include "ortools/base/strong_vector.h" -#include "ortools/constraint_solver/routing.h" +#include "ortools/routing/routing.h" -namespace operations_research { +namespace operations_research::routing { typedef std::function RoutingNodeEvaluator2; @@ -73,7 +73,8 @@ class LocationContainer { std::mt19937 randomizer_; const int64_t speed_; - util_intops::StrongVector locations_; + util_intops::StrongVector + locations_; }; // Random demand. @@ -95,44 +96,43 @@ class RandomDemand { // Service time (proportional to demand) + transition time callback. class ServiceTimePlusTransition { public: - ServiceTimePlusTransition( - int64_t time_per_demand_unit, - operations_research::RoutingNodeEvaluator2 demand, - operations_research::RoutingNodeEvaluator2 transition_time); + ServiceTimePlusTransition(int64_t time_per_demand_unit, + RoutingNodeEvaluator2 demand, + RoutingNodeEvaluator2 transition_time); int64_t Compute(RoutingIndexManager::NodeIndex from, RoutingIndexManager::NodeIndex to) const; private: const int64_t time_per_demand_unit_; - operations_research::RoutingNodeEvaluator2 demand_; - operations_research::RoutingNodeEvaluator2 transition_time_; + RoutingNodeEvaluator2 demand_; + RoutingNodeEvaluator2 transition_time_; }; // Stop service time + transition time callback. class StopServiceTimePlusTransition { public: - StopServiceTimePlusTransition( - int64_t stop_time, const LocationContainer& location_container, - operations_research::RoutingNodeEvaluator2 transition_time); + StopServiceTimePlusTransition(int64_t stop_time, + const LocationContainer& location_container, + RoutingNodeEvaluator2 transition_time); int64_t Compute(RoutingIndexManager::NodeIndex from, RoutingIndexManager::NodeIndex to) const; private: const int64_t stop_time_; const LocationContainer& location_container_; - operations_research::RoutingNodeEvaluator2 demand_; - operations_research::RoutingNodeEvaluator2 transition_time_; + RoutingNodeEvaluator2 demand_; + RoutingNodeEvaluator2 transition_time_; }; // Route plan displayer. // TODO(user): Move the display code to the routing library. -void DisplayPlan( - const operations_research::RoutingIndexManager& manager, - const operations_research::RoutingModel& routing, - const operations_research::Assignment& plan, bool use_same_vehicle_costs, - int64_t max_nodes_per_group, int64_t same_vehicle_cost, - absl::Span dimension_names); - -} // namespace operations_research +void DisplayPlan(const RoutingIndexManager& manager, + const RoutingModel& routing, + const operations_research::Assignment& plan, + bool use_same_vehicle_costs, int64_t max_nodes_per_group, + int64_t same_vehicle_cost, + absl::Span dimension_names); + +} // namespace operations_research::routing #endif // OR_TOOLS_ROUTING_PARSERS_CVRPTW_LIB_H_ diff --git a/ortools/routing/python/CMakeLists.txt b/ortools/routing/python/CMakeLists.txt new file mode 100644 index 00000000000..9fe346f31bf --- /dev/null +++ b/ortools/routing/python/CMakeLists.txt @@ -0,0 +1,77 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# routing +pybind11_add_module(routing_pybind11 MODULE model.cc) +set_target_properties(routing_pybind11 PROPERTIES + LIBRARY_OUTPUT_NAME "model") + +# note: macOS is APPLE and also UNIX ! +if(APPLE) + set_target_properties(routing_pybind11 PROPERTIES + SUFFIX ".so" + INSTALL_RPATH "@loader_path;@loader_path/../../../${PYTHON_PROJECT}/.libs") +elseif(UNIX) + set_target_properties(routing_pybind11 PROPERTIES + INSTALL_RPATH "$ORIGIN:$ORIGIN/../../../${PYTHON_PROJECT}/.libs") +endif() + +target_link_libraries(routing_pybind11 PRIVATE + ${PROJECT_NAMESPACE}::ortools + pybind11_native_proto_caster +) +add_library(${PROJECT_NAMESPACE}::routing_pybind11 ALIAS routing_pybind11) + +# legacy pywraprouting +set_property(SOURCE routing.i PROPERTY CPLUSPLUS ON) +set_property(SOURCE routing.i PROPERTY SWIG_MODULE_NAME pywraprouting) +set_property(SOURCE routing.i PROPERTY COMPILE_DEFINITIONS + ${OR_TOOLS_COMPILE_DEFINITIONS} ABSL_MUST_USE_RESULT=) +set_property(SOURCE routing.i PROPERTY COMPILE_OPTIONS -nofastunpack) +swig_add_library(pywraprouting + TYPE MODULE + LANGUAGE python + OUTPUT_DIR ${PYTHON_PROJECT_DIR}/routing + SOURCES routing.i) + +target_include_directories(pywraprouting PRIVATE ${Python3_INCLUDE_DIRS}) +set_property(TARGET pywraprouting PROPERTY SWIG_USE_TARGET_INCLUDE_DIRECTORIES ON) +target_compile_definitions(pywraprouting PUBLIC "PY3") + +# note: macOS is APPLE and also UNIX ! +if(APPLE) + set_target_properties(pywraprouting PROPERTIES + SUFFIX ".so" + INSTALL_RPATH "@loader_path;@loader_path/../../${PROJECT_NAME}/.libs") + target_link_options(pywraprouting PRIVATE "LINKER:-undefined,dynamic_lookup") +elseif(UNIX) + set_target_properties(pywraprouting PROPERTIES + INSTALL_RPATH "$ORIGIN:$ORIGIN/../../${PROJECT_NAME}/.libs") +endif() +target_link_libraries(pywraprouting PRIVATE ortools::ortools) + +# Variable PYTHON_LIBRARIES can contains keyword `optimized` +# which won't be interpreted inside a generator expression. +# i.e. we can't use: $<$:${PYTHON_LIBRARIES}> +# see: https://cmake.org/cmake/help/git-stage/command/target_link_libraries.html#command:target_link_libraries +if(MSVC) + target_link_libraries(pywraprouting PRIVATE ${Python3_LIBRARIES}) +endif() + +# Test +if(BUILD_TESTING) + file(GLOB PYTHON_SRCS "*_test.py") + foreach(FILE_NAME IN LISTS PYTHON_SRCS) + add_python_test(FILE_NAME ${FILE_NAME}) + endforeach() +endif() diff --git a/ortools/routing/python/doc.h b/ortools/routing/python/doc.h new file mode 100644 index 00000000000..d2e45bd2cce --- /dev/null +++ b/ortools/routing/python/doc.h @@ -0,0 +1,3990 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + This file contains docstrings for use in the Python bindings. + Do not edit! They were automatically extracted by pybind11_mkdoc. + */ + +#define __EXPAND(x) x +#define __COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT +#define __VA_SIZE(...) __EXPAND(__COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1)) +#define __CAT1(a, b) a##b +#define __CAT2(a, b) __CAT1(a, b) +#define __DOC1(n1) __doc_##n1 +#define __DOC2(n1, n2) __doc_##n1##_##n2 +#define __DOC3(n1, n2, n3) __doc_##n1##_##n2##_##n3 +#define __DOC4(n1, n2, n3, n4) __doc_##n1##_##n2##_##n3##_##n4 +#define __DOC5(n1, n2, n3, n4, n5) __doc_##n1##_##n2##_##n3##_##n4##_##n5 +#define __DOC6(n1, n2, n3, n4, n5, n6) \ + __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6 +#define __DOC7(n1, n2, n3, n4, n5, n6, n7) \ + __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 +#define DOC(...) \ + __EXPAND(__EXPAND(__CAT2(__DOC, __VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) + +#if defined(__GNUG__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#endif + +static const char* __doc_operations_research_AppendTasksFromIntervals = + R"doc()doc"; + +static const char* __doc_operations_research_BoundCost = + R"doc(A structure meant to store soft bounds and associated violation +constants. It is 'Simple' because it has one BoundCost per element, in +contrast to 'Multiple'. Design notes: - it is meant to store model +information to be shared through pointers, so it disallows copy and +assign to avoid accidental duplication. - it keeps soft bounds as an +array of structs to help cache, because code that uses such bounds +typically use both bound and cost. - soft bounds are named pairs, +prevents some mistakes. - using operator[] to access elements is not +interesting, because the structure will be accessed through pointers, +moreover having to type bound_cost reminds the user of the order if +they do a copy assignment of the element.)doc"; + +static const char* __doc_operations_research_BoundCost_BoundCost = R"doc()doc"; + +static const char* __doc_operations_research_BoundCost_BoundCost_2 = + R"doc()doc"; + +static const char* __doc_operations_research_BoundCost_bound = R"doc()doc"; + +static const char* __doc_operations_research_BoundCost_cost = R"doc()doc"; + +static const char* __doc_operations_research_DisjunctivePropagator = + R"doc(This class acts like a CP propagator: it takes a set of tasks given by +their start/duration/end features, and reduces the range of possible +values.)doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_ChainSpanMin = + R"doc(Propagates a lower bound of the chain span, end[num_chain_tasks] - +start[0], to span_min.)doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_ChainSpanMinDynamic = + R"doc(Computes a lower bound of the span of the chain, taking into account +only the first nonchain task. For more accurate results, this should +be called after Precedences(), otherwise the lower bound might be +lower than feasible.)doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_DetectablePrecedencesWithChain = + R"doc(Does detectable precedences deductions on tasks in the chain +precedence, taking the time windows of nonchain tasks into account.)doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_DistanceDuration = + R"doc(Propagates distance_duration constraints, if any.)doc"; + +static const char* __doc_operations_research_DisjunctivePropagator_EdgeFinding = + R"doc(Does edge-finding deductions on all tasks.)doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_ForbiddenIntervals = + R"doc(Tasks might have holes in their domain, this enforces such holes.)doc"; + +static const char* __doc_operations_research_DisjunctivePropagator_MirrorTasks = + R"doc(Transforms the problem with a time symmetry centered in 0. Returns +true for convenience.)doc"; + +static const char* __doc_operations_research_DisjunctivePropagator_Precedences = + R"doc(Propagates the deductions from the chain of precedences, if there is +one.)doc"; + +static const char* __doc_operations_research_DisjunctivePropagator_Propagate = + R"doc(Computes new bounds for all tasks, returns false if infeasible. This +does not compute a fixed point, so recalling it may filter more.)doc"; + +static const char* __doc_operations_research_DisjunctivePropagator_Tasks = + R"doc(A structure to hold tasks described by their features. The first +num_chain_tasks are considered linked by a chain of precedences, i.e. +if i < j < num_chain_tasks, then end(i) <= start(j). This occurs +frequently in routing, and can be leveraged by some variants of +classic propagators.)doc"; + +static const char* __doc_operations_research_DisjunctivePropagator_Tasks_Clear = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_distance_duration = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_duration_max = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_duration_min = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_end_max = R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_end_min = R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_forbidden_intervals = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_is_preemptible = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_num_chain_tasks = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_span_max = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_span_min = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_start_max = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_Tasks_start_min = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_event_of_task = R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_nonchain_tasks_by_start_max = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_tasks_by_end_max = + R"doc()doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_tasks_by_start_min = + R"doc(Mappings between events and tasks.)doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_theta_lambda_tree = + R"doc(The main algorithm uses Vilim's theta tree data structure. See Petr +Vilim's PhD thesis "Global Constraints in Scheduling".)doc"; + +static const char* + __doc_operations_research_DisjunctivePropagator_total_duration_before = + R"doc(Maps chain elements to the sum of chain task durations before them.)doc"; + +static const char* __doc_operations_research_FillPathEvaluation = R"doc()doc"; + +static const char* __doc_operations_research_FillTravelBoundsOfVehicle = + R"doc()doc"; + +static const char* __doc_operations_research_FinalizerVariables = R"doc()doc"; + +static const char* __doc_operations_research_GlobalDimensionCumulOptimizer = + R"doc()doc"; + +static const char* __doc_operations_research_GlobalVehicleBreaksConstraint = + R"doc(GlobalVehicleBreaksConstraint ensures breaks constraints are enforced +on all vehicles in the dimension passed to its constructor. It is +intended to be used for dimensions representing time. A break +constraint ensures break intervals fit on the route of a vehicle. For +a given vehicle, it forces break intervals to be disjoint from visit +intervals, where visit intervals start at CumulVar(node) and last for +node_visit_transit[node]. Moreover, it ensures that there is enough +time between two consecutive nodes of a route to do transit and +vehicle breaks, i.e. if Next(nodeA) = nodeB, CumulVar(nodeA) = tA and +CumulVar(nodeB) = tB, then SlackVar(nodeA) >= sum_{breaks \subseteq +[tA, tB)} duration(break).)doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_DebugString = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_FillPartialPathOfVehicle = + R"doc(Sets path_ to be the longest sequence such that _ path_[0] is the +start of the vehicle _ Next(path_[i-1]) is Bound() and has value +path_[i], followed by the end of the vehicle if the last node was not +an end.)doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_FillPathTravels = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_GlobalVehicleBreaksConstraint = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_InitialPropagate = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_Post = R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_PropagateNode = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_PropagateVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator = + R"doc(This translates pruning information to solver variables. If +constructed with an IntervalVar*, it follows the usual semantics of +IntervalVars. If constructed with an IntVar*, before_start and +after_start, operations are translated to simulate an interval that +starts at start - before_start and ends and start + after_start. If +constructed with nothing, the TaskTranslator will do nothing. This +class should have been an interface + subclasses, but that would force +pointers in the user's task vector, which means dynamic allocation. +With this union-like structure, a vector's reserved size will adjust +to usage and eventually no more dynamic allocation will be made.)doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_SetDurationMin = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_SetEndMax = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_SetEndMin = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_SetStartMax = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_SetStartMin = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_TaskTranslator = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_TaskTranslator_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_TaskTranslator_3 = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_after_start = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_before_start = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_interval = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_TaskTranslator_start = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_dimension = + R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_disjunctive_propagator = + R"doc(This is used to restrict bounds of tasks.)doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_model = R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_path = R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_task_translators = + R"doc(Route and interval variables are normalized to the following values.)doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_tasks = R"doc()doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_travel_bounds = + R"doc(Used to help filling tasks_ at each propagation.)doc"; + +static const char* + __doc_operations_research_GlobalVehicleBreaksConstraint_vehicle_demons = + R"doc()doc"; + +static const char* __doc_operations_research_IndexNeighborFinder = + R"doc(Class to find index neighbors. Used by various parts of the vehicle +routing framework (heuristics, local search) which rely on finding +index neighbors (essentially to speed up search). It relies on having +coordinates.)doc"; + +static const char* __doc_operations_research_IndexNeighborFinder_2 = + R"doc(Class to find index neighbors. Used by various parts of the vehicle +routing framework (heuristics, local search) which rely on finding +index neighbors (essentially to speed up search). It relies on having +coordinates.)doc"; + +static const char* + __doc_operations_research_IndexNeighborFinder_FindIndexNeighbors = + R"doc()doc"; + +static const char* + __doc_operations_research_IndexNeighborFinder_IndexNeighborFinder = + R"doc()doc"; + +static const char* + __doc_operations_research_IndexNeighborFinder_IndexNeighborFinder_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_IndexNeighborFinder_IndexNeighborFinder_3 = + R"doc()doc"; + +static const char* __doc_operations_research_IndexNeighborFinder_manager = + R"doc(When the IndexNeighborFinder is created using NodeIndex as index, the +manager_ is used to translate from variable index to NodeIndex and +back when calling FindIndexNeighbors().)doc"; + +static const char* + __doc_operations_research_IndexNeighborFinder_operator_assign = R"doc()doc"; + +static const char* __doc_operations_research_IndexNeighborFinder_points = + R"doc()doc"; + +static const char* __doc_operations_research_IntVarFilteredDecisionBuilder = + R"doc()doc"; + +static const char* __doc_operations_research_LocalDimensionCumulOptimizer = + R"doc()doc"; + +static const char* __doc_operations_research_LocalSearchPhaseParameters = + R"doc()doc"; + +static const char* __doc_operations_research_MakeBinCapacities = R"doc()doc"; + +static const char* __doc_operations_research_MakeVehicleBreaksFilter = + R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata = R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_End = R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_Ends = R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_GetPath = + R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_IsEnd = R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_IsStart = + R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_NumPaths = + R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_Paths = R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_PathsMetadata = + R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_Start = R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_Starts = R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_end_of_path = + R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_is_end = R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_is_start = + R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_path_of_node = + R"doc()doc"; + +static const char* __doc_operations_research_PathsMetadata_start_of_path = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension = + R"doc(for a given vehicle, it is passed as an external vector, it would be +better to have this information here.)doc"; + +static const char* __doc_operations_research_RoutingDimension_2 = + R"doc(for a given vehicle, it is passed as an external vector, it would be +better to have this information here.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_AddNodePrecedence = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_AddNodePrecedence_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_AllTransitEvaluatorSignsAreUnknown = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_AreVehicleTransitsPositive = + R"doc(Returns true iff the transit evaluator of 'vehicle' is positive for +all arcs.)doc"; + +static const char* __doc_operations_research_RoutingDimension_CloseModel = + R"doc(Finalize the model of the dimension.)doc"; + +static const char* __doc_operations_research_RoutingDimension_CumulVar = + R"doc(Get the cumul, transit and slack variables for the given node (given +as int64_t var index).)doc"; + +static const char* __doc_operations_research_RoutingDimension_FixedTransitVar = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetAllowedIntervalsInRange = + R"doc(Returns allowed intervals for a given node in a given interval.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetBinaryTransitEvaluator = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetBreakDistanceDurationOfVehicle = + R"doc(Returns the pairs (distance, duration) specified by break distance +constraints.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetBreakIntervalsOfVehicle = + R"doc(Returns the break intervals set by SetBreakIntervalsOfVehicle().)doc"; + +static const char* __doc_operations_research_RoutingDimension_GetCumulVarMax = + R"doc(Gets the current maximum of the cumul variable associated to index.)doc"; + +static const char* __doc_operations_research_RoutingDimension_GetCumulVarMin = + R"doc(Gets the current minimum of the cumul variable associated to index.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetCumulVarPiecewiseLinearCost = + R"doc(Returns the piecewise linear cost of a cumul variable for a given +variable index. The returned pointer has the same validity as this +class.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetCumulVarSoftLowerBound = + R"doc(Returns the soft lower bound of a cumul variable for a given variable +index. The "hard" lower bound of the variable is returned if no soft +lower bound has been set.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetCumulVarSoftLowerBoundCoefficient = + R"doc(Returns the cost coefficient of the soft lower bound of a cumul +variable for a given variable index. If no soft lower bound has been +set, 0 is returned.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetCumulVarSoftUpperBound = + R"doc(Returns the soft upper bound of a cumul variable for a given variable +index. The "hard" upper bound of the variable is returned if no soft +upper bound has been set.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetCumulVarSoftUpperBoundCoefficient = + R"doc(Returns the cost coefficient of the soft upper bound of a cumul +variable for a given variable index. If no soft upper bound has been +set, 0 is returned.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetFirstPossibleGreaterOrEqualValueForNode = + R"doc(Returns the smallest value outside the forbidden intervals of node +'index' that is greater than or equal to a given 'min_value'.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetGlobalOptimizerOffset = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetLastPossibleLessOrEqualValueForNode = + R"doc(Returns the largest value outside the forbidden intervals of node +'index' that is less than or equal to a given 'max_value'. NOTE: If +this method is called with a max_value lower than the node's cumul +min, it will return -1.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetLocalOptimizerOffsetForVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetNodePrecedences = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetPathPrecedenceGraph = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetPickupToDeliveryLimitForPair = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetPostTravelEvaluatorOfVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetPreTravelEvaluatorOfVehicle = + R"doc(!defined(SWIGPYTHON))doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetQuadraticCostSoftSpanUpperBoundForVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetSlackCostCoefficientForVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetSlackCostCoefficientForVehicleClass = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetSoftSpanUpperBoundForVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetSpanCostCoefficientForVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetSpanCostCoefficientForVehicleClass = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetSpanUpperBoundForVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetTransitEvaluatorSign = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_GetTransitValue = + R"doc(Returns the transition value for a given pair of nodes (as var index); +this value is the one taken by the corresponding transit variable when +the 'next' variable for 'from_index' is bound to 'to_index'.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetTransitValueFromClass = + R"doc(Same as above but taking a vehicle class of the dimension instead of a +vehicle (the class of a vehicle can be obtained with +vehicle_to_class()).)doc"; + +static const char* + __doc_operations_research_RoutingDimension_GetUnaryTransitEvaluator = + R"doc(Returns the unary callback evaluating the transit value between two +node indices for a given vehicle. If the corresponding callback is not +unary, returns a null callback.)doc"; + +static const char* __doc_operations_research_RoutingDimension_HasBreakConstraints = + R"doc(Returns true if any break interval or break distance was defined.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_HasCumulVarPiecewiseLinearCost = + R"doc(Returns true if a piecewise linear cost has been set for a given +variable index.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_HasCumulVarSoftLowerBound = + R"doc(Returns true if a soft lower bound has been set for a given variable +index.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_HasCumulVarSoftUpperBound = + R"doc(Returns true if a soft upper bound has been set for a given variable +index.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_HasPickupToDeliveryLimits = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_HasQuadraticCostSoftSpanUpperBounds = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_HasSoftSpanUpperBounds = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_Initialize = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_InitializeBreaks = + R"doc(Sets up vehicle_break_intervals_, vehicle_break_distance_duration_, +pre_travel_evaluators and post_travel_evaluators.)doc"; + +static const char* __doc_operations_research_RoutingDimension_InitializeCumuls = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_InitializeTransitVariables = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_InitializeTransits = R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_IsUnary = + R"doc(Returns true iff all transit evaluators for this dimension are unary.)doc"; + +static const char* __doc_operations_research_RoutingDimension_NodePrecedence = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_NodePrecedence_first_node = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_NodePrecedence_offset = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_NodePrecedence_second_node = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_PiecewiseLinearCost = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_PiecewiseLinearCost_PiecewiseLinearCost = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_PiecewiseLinearCost_cost = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_PiecewiseLinearCost_var = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_RoutingDimension = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_RoutingDimension_2 = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_RoutingDimension_3 = R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_SelfBased = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetBreakDistanceDurationOfVehicle = + R"doc(With breaks supposed to be consecutive, this forces the distance +between breaks of size at least minimum_break_duration to be at most +distance. This supposes that the time until route start and after +route end are infinite breaks.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetBreakIntervalsOfVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetBreakIntervalsOfVehicle_2 = + R"doc(Deprecated, sets pre_travel(i, j) = node_visit_transit[i].)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetBreakIntervalsOfVehicle_3 = + R"doc(Deprecated, sets pre_travel(i, j) = node_visit_transit[i] and +post_travel(i, j) = delays(i, j).)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetCumulVarPiecewiseLinearCost = + R"doc(Sets a piecewise linear cost on the cumul variable of a given variable +index. If f is a piecewise linear function, the resulting cost at +'index' will be f(CumulVar(index)). As of 3/2017, only non-decreasing +positive cost functions are supported.)doc"; + +static const char* __doc_operations_research_RoutingDimension_SetCumulVarRange = + R"doc(Restricts the range of the cumul variable associated to index.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetCumulVarSoftLowerBound = + R"doc(Sets a soft lower bound to the cumul variable of a given variable +index. If the value of the cumul variable is less than the bound, a +cost proportional to the difference between this value and the bound +is added to the cost function of the model: cumulVar > lower_bound -> +cost = 0 cumulVar <= lower_bound -> cost = coefficient * (lower_bound +- cumulVar). This is also handy to model earliness costs when the +dimension represents time.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetCumulVarSoftUpperBound = + R"doc(Sets a soft upper bound to the cumul variable of a given variable +index. If the value of the cumul variable is greater than the bound, a +cost proportional to the difference between this value and the bound +is added to the cost function of the model: cumulVar <= upper_bound -> +cost = 0 cumulVar > upper_bound -> cost = coefficient * (cumulVar - +upper_bound) This is also handy to model tardiness costs when the +dimension represents time.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetGlobalSpanCostCoefficient = + R"doc(Sets a cost proportional to the *global* dimension span, that is the +difference between the largest value of route end cumul variables and +the smallest value of route start cumul variables. In other words: +global_span_cost = coefficient * (Max(dimension end value) - +Min(dimension start value)).)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetOffsetForGlobalOptimizer = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetPickupToDeliveryLimitFunctionForPair = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetQuadraticCostSoftSpanUpperBoundForVehicle = + R"doc(If the span of vehicle on this dimension is larger than bound, the +cost will be increased by cost * (span - bound)^2.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetSlackCostCoefficientForAllVehicles = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetSlackCostCoefficientForVehicle = + R"doc(Sets a cost proportional to the dimension total slack on a given +vehicle, or on all vehicles at once. "coefficient" must be +nonnegative. This is handy to model costs only proportional to idle +time when the dimension represents time. The cost for a vehicle is +slack_cost = coefficient * (dimension end value - dimension start +value - total_transit).)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetSoftSpanUpperBoundForVehicle = + R"doc(If the span of vehicle on this dimension is larger than bound, the +cost will be increased by cost * (span - bound).)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetSpanCostCoefficientForAllVehicles = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetSpanCostCoefficientForVehicle = + R"doc(Sets a cost proportional to the dimension span on a given vehicle, or +on all vehicles at once. "coefficient" must be nonnegative. This is +handy to model costs proportional to idle time when the dimension +represents time. The cost for a vehicle is span_cost = coefficient * +(dimension end value - dimension start value).)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetSpanUpperBoundForVehicle = + R"doc(!defined(SWIGPYTHON) Sets an upper bound on the dimension span on a +given vehicle. This is the preferred way to limit the "length" of the +route of a vehicle according to a dimension.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetVehicleOffsetsForLocalOptimizer = + R"doc(Moves elements of "offsets" into vehicle_offsets_for_local_optimizer_.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetupCumulVarPiecewiseLinearCosts = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetupCumulVarSoftLowerBoundCosts = + R"doc(Sets up the cost variables related to cumul soft lower bounds.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetupCumulVarSoftUpperBoundCosts = + R"doc(Sets up the cost variables related to cumul soft upper bounds.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetupGlobalSpanCost = + R"doc(Sets up the cost variables related to the global span and per-vehicle +span costs (only for the "slack" part of the latter).)doc"; + +static const char* + __doc_operations_research_RoutingDimension_SetupSlackAndDependentTransitCosts = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_ShortestTransitionSlack = + R"doc(It makes sense to use the function only for self-dependent dimension. +For such dimensions the value of the slack of a node determines the +transition cost of the next transit. Provided that 1. cumul[node] is +fixed, 2. next[node] and next[next[node]] (if exists) are fixed, the +value of slack[node] for which cumul[next[node]] + transit[next[node]] +is minimized can be found in O(1) using this function.)doc"; + +static const char* __doc_operations_research_RoutingDimension_SlackVar = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_SoftBound = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_SoftBound_bound = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_SoftBound_coefficient = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_SoftBound_var = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_TransitVar = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_base_dimension = + R"doc(Returns the parent in the dependency tree if any or nullptr otherwise.)doc"; + +static const char* __doc_operations_research_RoutingDimension_base_dimension_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_break_constraints_are_initialized = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_capacity_vars = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_class_evaluators = + R"doc(Values in class_evaluators_ correspond to the evaluators in +RoutingModel::transit_evaluators_ for each vehicle class.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_class_transit_evaluator = + R"doc(Returns the callback evaluating the transit value between two node +indices for a given vehicle class.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_cumul_var_piecewise_linear_cost = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_cumul_var_soft_lower_bound = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_cumul_var_soft_upper_bound = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_cumuls = + R"doc(Like CumulVar(), TransitVar(), SlackVar() but return the whole +variable vectors instead (indexed by int64_t var index).)doc"; + +static const char* __doc_operations_research_RoutingDimension_cumuls_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_dependent_transits = R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_fixed_transits = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_fixed_transits_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_forbidden_intervals = + R"doc(Returns forbidden intervals for each node.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_forbidden_intervals_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_global_optimizer_offset = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_global_span_cost_coefficient = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_global_span_cost_coefficient_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_local_optimizer_offset_for_vehicle = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_model = + R"doc(Returns the model on which the dimension was created.)doc"; + +static const char* __doc_operations_research_RoutingDimension_model_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_name = + R"doc(Returns the name of the dimension.)doc"; + +static const char* __doc_operations_research_RoutingDimension_name_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_node_precedences = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_operator_assign = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_path_precedence_graph = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_pickup_to_delivery_limits_per_pair_index = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_slacks = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_slacks_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_state_dependent_class_evaluators = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_state_dependent_vehicle_to_class = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_transit_evaluator = + R"doc(Returns the callback evaluating the transit value between two node +indices for a given vehicle.)doc"; + +static const char* __doc_operations_research_RoutingDimension_transits = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_transits_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_break_distance_duration = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_break_intervals = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_capacities = + R"doc(Returns the capacities for all vehicles.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_capacities_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_post_travel_evaluators = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_pre_travel_evaluators = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_quadratic_cost_soft_span_upper_bound = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_slack_cost_coefficients = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_slack_cost_coefficients_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_soft_span_upper_bound = + R"doc(nullptr if not defined.)doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_span_cost_coefficients = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_span_cost_coefficients_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_span_upper_bounds = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_span_upper_bounds_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingDimension_vehicle_to_class = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingDimension_vehicle_to_class_2 = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModelVisitor = + R"doc(Routing model visitor.)doc"; + +static const char* __doc_operations_research_RoutingModel_ActiveVar = + R"doc(Returns the active variable of the node corresponding to index.)doc"; + +static const char* __doc_operations_research_RoutingModel_ActiveVehicleVar = + R"doc(Returns the active variable of the vehicle. It will be equal to 1 iff +the route of the vehicle is not empty, 0 otherwise.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddAtSolutionCallback = + R"doc(Adds a callback called each time a solution is found during the +search. This is a shortcut to creating a monitor to call the callback +on AtSolution() and adding it with AddSearchMonitor. If +track_unchecked_neighbors is true, the callback will also be called on +AcceptUncheckedNeighbor() events, which is useful to grab solutions +obtained when solver_parameters.check_solution_period > 1 (aka +fastLS).)doc"; + +static const char* __doc_operations_research_RoutingModel_AddConstantDimension = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_AddConstantDimensionWithSlack = + R"doc(Creates a dimension where the transit variable is constrained to be +equal to 'value'; 'capacity' is the upper bound of the cumul +variables. 'name' is the name used to reference the dimension; this +name is used to get cumul and transit variables from the routing +model. Returns a pair consisting of an index to the registered unary +transit callback and a bool denoting whether the dimension has been +created. It is false if a dimension with the same name has already +been created (and doesn't create the new dimension but still register +a new callback).)doc"; + +static const char* __doc_operations_research_RoutingModel_AddDimension = + R"doc(Creates a dimension where the transit variable is constrained to be +equal to evaluator(i, next(i)); 'slack_max' is the upper bound of the +slack variable and 'capacity' is the upper bound of the cumul +variables. 'name' is the name used to reference the dimension; this +name is used to get cumul and transit variables from the routing +model. Returns false if a dimension with the same name has already +been created (and doesn't create the new dimension). Takes ownership +of the callback 'evaluator'.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddDimensionDependentDimensionWithVehicleCapacity = + R"doc(Creates a dimension with transits depending on the cumuls of another +dimension. 'pure_transits' are the per-vehicle fixed transits as +above. 'dependent_transits' is a vector containing for each vehicle an +index to a registered state dependent transit callback. +'base_dimension' indicates the dimension from which the cumul variable +is taken. If 'base_dimension' is nullptr, then the newly created +dimension is self-based.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddDimensionDependentDimensionWithVehicleCapacity_2 = + R"doc(As above, but pure_transits are taken to be zero evaluators.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddDimensionDependentDimensionWithVehicleCapacity_3 = + R"doc(Homogeneous versions of the functions above.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddDimensionDependentDimensionWithVehicleCapacity_4 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_AddDimensionDependentDimensionWithVehicleCapacityInternal = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_AddDimensionWithCapacityInternal = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_AddDimensionWithVehicleCapacity = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_AddDimensionWithVehicleTransitAndCapacity = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_AddDimensionWithVehicleTransits = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_AddDisjunction = + R"doc(Adds a disjunction constraint on the indices: exactly +'max_cardinality' of the indices are active. Start and end indices of +any vehicle cannot be part of a disjunction. + +If a penalty is given, at most 'max_cardinality' of the indices can be +active, and if less are active, 'penalty' is payed per inactive index. +This is equivalent to adding the constraint: p + Sum(i)active[i] == +max_cardinality where p is an integer variable, and the following cost +to the cost function: p * penalty. 'penalty' must be positive to make +the disjunction optional; a negative penalty will force +'max_cardinality' indices of the disjunction to be performed, and +therefore p == 0. Note: passing a vector with a single index will +model an optional index with a penalty cost if it is not visited.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddHardTypeIncompatibility = + R"doc(Incompatibilities: Two nodes with "hard" incompatible types cannot +share the same route at all, while with a "temporal" incompatibility +they can't be on the same route at the same time.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddIntervalToAssignment = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_AddLocalSearchFilter = + R"doc(Adds a custom local search filter to the list of filters used to speed +up local search by pruning unfeasible variable assignments. Calling +this method after the routing model has been closed (CloseModel() or +Solve() has been called) has no effect. The routing model does not +take ownership of the filter.)doc"; + +static const char* __doc_operations_research_RoutingModel_AddLocalSearchOperator = + R"doc(Adds a local search operator to the set of operators used to solve the +vehicle routing problem.)doc"; + +static const char* __doc_operations_research_RoutingModel_AddMatrixDimension = + R"doc(Creates a dimension where the transit variable is constrained to be +equal to 'values[i][next(i)]' for node i; 'capacity' is the upper +bound of the cumul variables. 'name' is the name used to reference the +dimension; this name is used to get cumul and transit variables from +the routing model. Returns a pair consisting of an index to the +registered transit callback and a bool denoting whether the dimension +has been created. It is false if a dimension with the same name has +already been created (and doesn't create the new dimension but still +register a new callback).)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddNoCycleConstraintInternal = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_AddPickupAndDelivery = + R"doc(Notifies that index1 and index2 form a pair of nodes which should +belong to the same route. This methods helps the search find better +solutions, especially in the local search phase. It should be called +each time you have an equality constraint linking the vehicle +variables of two node (including for instance pickup and delivery +problems): Solver* const solver = routing.solver(); int64_t index1 = +manager.NodeToIndex(node1); int64_t index2 = +manager.NodeToIndex(node2); +solver->AddConstraint(solver->MakeEquality( +routing.VehicleVar(index1), routing.VehicleVar(index2))); +routing.AddPickupAndDelivery(index1, index2);)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddPickupAndDeliverySets = + R"doc(Same as AddPickupAndDelivery but notifying that the performed node +from the disjunction of index 'pickup_disjunction' is on the same +route as the performed node from the disjunction of index +'delivery_disjunction'.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddPickupAndDeliverySetsInternal = + R"doc(Sets up pickup and delivery sets.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddRequiredTypeAlternativesWhenAddingType = + R"doc(If type_D depends on type_R when adding type_D, any node_D of type_D +and VisitTypePolicy TYPE_ADDED_TO_VEHICLE or +TYPE_SIMULTANEOUSLY_ADDED_AND_REMOVED requires at least one type_R on +its vehicle at the time node_D is visited.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddRequiredTypeAlternativesWhenRemovingType = + R"doc(The following requirements apply when visiting dependent nodes that +remove their type from the route, i.e. type_R must be on the vehicle +when type_D of VisitTypePolicy ADDED_TYPE_REMOVED_FROM_VEHICLE, +TYPE_ON_VEHICLE_UP_TO_VISIT or TYPE_SIMULTANEOUSLY_ADDED_AND_REMOVED +is visited.)doc"; + +static const char* __doc_operations_research_RoutingModel_AddResourceGroup = + R"doc(Adds a resource group to the routing model and returns a pointer to +it.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddSameVehicleRequiredTypeAlternatives = + R"doc(Requirements: NOTE: As of 2019-04, cycles in the requirement graph are +not supported, and lead to the dependent nodes being skipped if +possible (otherwise the model is considered infeasible). The following +functions specify that "dependent_type" requires at least one of the +types in "required_type_alternatives". + +For same-vehicle requirements, a node of dependent type type_D +requires at least one node of type type_R among the required +alternatives on the same route.)doc"; + +static const char* __doc_operations_research_RoutingModel_AddSearchMonitor = + R"doc(Adds a search monitor to the search used to solve the routing model.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddSoftSameVehicleConstraint = + R"doc(Adds a soft constraint to force a set of variable indices to be on the +same vehicle. If all nodes are not on the same vehicle, each extra +vehicle used adds 'cost' to the cost function.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddTemporalTypeIncompatibility = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_AddToAssignment = + R"doc(Adds an extra variable to the vehicle routing assignment.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddVariableMaximizedByFinalizer = + R"doc(Adds a variable to maximize in the solution finalizer (see above for +information on the solution finalizer).)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddVariableMinimizedByFinalizer = + R"doc(Adds a variable to minimize in the solution finalizer. The solution +finalizer is called each time a solution is found during the search +and allows to instantiate secondary variables (such as dimension cumul +variables).)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddVariableTargetToFinalizer = + R"doc(Add a variable to set the closest possible to the target value in the +solution finalizer.)doc"; + +static const char* __doc_operations_research_RoutingModel_AddVectorDimension = + R"doc(Creates a dimension where the transit variable is constrained to be +equal to 'values[i]' for node i; 'capacity' is the upper bound of the +cumul variables. 'name' is the name used to reference the dimension; +this name is used to get cumul and transit variables from the routing +model. Returns a pair consisting of an index to the registered unary +transit callback and a bool denoting whether the dimension has been +created. It is false if a dimension with the same name has already +been created (and doesn't create the new dimension but still register +a new callback).)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddWeightedVariableMaximizedByFinalizer = + R"doc(Adds a variable to maximize in the solution finalizer, with a weighted +priority: the higher the more priority it has.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddWeightedVariableMinimizedByFinalizer = + R"doc(Adds a variable to minimize in the solution finalizer, with a weighted +priority: the higher the more priority it has.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AddWeightedVariableTargetToFinalizer = + R"doc(Same as above with a weighted priority: the higher the cost, the more +priority it has to be set close to the target value.)doc"; + +static const char* __doc_operations_research_RoutingModel_AppendArcCosts = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_AppendAssignmentIfFeasible = + R"doc(Append an assignment to a vector of assignments if it is feasible.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AppendHomogeneousArcCosts = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ApplyLocks = + R"doc(Applies a lock chain to the next search. 'locks' represents an ordered +vector of nodes representing a partial route which will be fixed +during the next search; it will constrain next variables such that: +next[locks[i]] == locks[i+1]. + +Returns the next variable at the end of the locked chain; this +variable is not locked. An assignment containing the locks can be +obtained by calling PreAssignment().)doc"; + +static const char* + __doc_operations_research_RoutingModel_ApplyLocksToAllVehicles = + R"doc(Applies lock chains to all vehicles to the next search, such that +locks[p] is the lock chain for route p. Returns false if the locks do +not contain valid routes; expects that the routes do not contain the +depots, i.e. there are empty vectors in place of empty routes. If +close_routes is set to true, adds the end nodes to the route of each +vehicle and deactivates other nodes. An assignment containing the +locks can be obtained by calling PreAssignment().)doc"; + +static const char* + __doc_operations_research_RoutingModel_ArcIsMoreConstrainedThanArc = + R"doc(Returns whether the arc from->to1 is more constrained than from->to2, +taking into account, in order: - whether the destination node isn't an +end node - whether the destination node is mandatory - whether the +destination node is bound to the same vehicle as the source - the +"primary constrained" dimension (see SetPrimaryConstrainedDimension) +It then breaks ties using, in order: - the arc cost (taking +unperformed penalties into account) - the size of the vehicle vars of +"to1" and "to2" (lowest size wins) - the value: the lowest value of +the indices to1 and to2 wins. See the .cc for details. The more +constrained arc is typically preferable when building a first +solution. This method is intended to be used as a callback for the +BestValueByComparisonSelector value selector. Args: from: the variable +index of the source node to1: the variable index of the first +candidate destination node. to2: the variable index of the second +candidate destination node.)doc"; + +static const char* + __doc_operations_research_RoutingModel_AreRoutesInterdependent = + R"doc(Returns true if routes are interdependent. This means that any +modification to a route might impact another.)doc"; + +static const char* __doc_operations_research_RoutingModel_AssignmentToRoutes = + R"doc(Converts the solution in the given assignment to routes for all +vehicles. Expects that assignment contains a valid solution (i.e. +routes for all vehicles end with an end index for that vehicle).)doc"; + +static const char* __doc_operations_research_RoutingModel_CancelSearch = + R"doc(Cancels the current search.)doc"; + +static const char* + __doc_operations_research_RoutingModel_CheckIfAssignmentIsFeasible = + R"doc(Checks if an assignment is feasible.)doc"; + +static const char* __doc_operations_research_RoutingModel_CheckLimit = + R"doc(Returns true if the search limit has been crossed with the given time +offset.)doc"; + +static const char* __doc_operations_research_RoutingModel_CloseModel = + R"doc(Closes the current routing model; after this method is called, no +modification to the model can be done, but RoutesToAssignment becomes +available. Note that CloseModel() is automatically called by Solve() +and other methods that produce solution. This is equivalent to calling +CloseModelWithParameters(DefaultRoutingSearchParameters()).)doc"; + +static const char* + __doc_operations_research_RoutingModel_CloseModelWithParameters = + R"doc(Same as above taking search parameters (as of 10/2015 some the +parameters have to be set when closing the model).)doc"; + +static const char* __doc_operations_research_RoutingModel_CloseVisitTypes = + R"doc("close" types.)doc"; + +static const char* + __doc_operations_research_RoutingModel_CompactAndCheckAssignment = + R"doc(Same as CompactAssignment() but also checks the validity of the final +compact solution; if it is not valid, no attempts to repair it are +made (instead, the method returns nullptr).)doc"; + +static const char* __doc_operations_research_RoutingModel_CompactAssignment = + R"doc(Returns a compacted version of the given assignment, in which all +vehicles with id lower or equal to some N have non-empty routes, and +all vehicles with id greater than N have empty routes. Does not take +ownership of the returned object. If found, the cost of the compact +assignment is the same as in the original assignment and it preserves +the values of 'active' variables. Returns nullptr if a compact +assignment was not found. This method only works in homogenous mode, +and it only swaps equivalent vehicles (vehicles with the same start +and end nodes). When creating the compact assignment, the empty plan +is replaced by the route assigned to the compatible vehicle with the +highest id. Note that with more complex constraints on vehicle +variables, this method might fail even if a compact solution exists. +This method changes the vehicle and dimension variables as necessary. +While compacting the solution, only basic checks on vehicle variables +are performed; if one of these checks fails no attempts to repair it +are made (instead, the method returns nullptr).)doc"; + +static const char* + __doc_operations_research_RoutingModel_CompactAssignmentInternal = + R"doc(See CompactAssignment. Checks the final solution if +check_compact_assignment is true.)doc"; + +static const char* __doc_operations_research_RoutingModel_ComputeCostClasses = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ComputeLowerBound = + R"doc(Computes a lower bound to the routing problem solving a linear +assignment problem. The routing model must be closed before calling +this method. Note that problems with node disjunction constraints +(including optional nodes) and non-homogenous costs are not supported +(the method returns 0 in these cases).)doc"; + +static const char* __doc_operations_research_RoutingModel_ComputeResourceClasses = + R"doc(Computes resource classes for all resource groups in the model.)doc"; + +static const char* + __doc_operations_research_RoutingModel_ComputeVehicleClasses = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ComputeVehicleTypes = + R"doc(The following method initializes the vehicle_type_container_: - +Computes the vehicle types of vehicles and stores it in +type_index_of_vehicle. - The vehicle classes corresponding to each +vehicle type index are stored and sorted by fixed cost in +sorted_vehicle_classes_per_type. - The vehicles for each vehicle class +are stored in vehicles_per_vehicle_class.)doc"; + +static const char* __doc_operations_research_RoutingModel_ConcatenateOperators = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CostCacheElement = + R"doc(Storage of a cost cache element corresponding to a cost arc ending at +node 'index' and on the cost class 'cost_class'.)doc"; + +static const char* + __doc_operations_research_RoutingModel_CostCacheElement_cost = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CostCacheElement_cost_class_index = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CostCacheElement_index = + R"doc(This is usually an int64_t, but using an int here decreases the RAM +usage, and should be fine since in practice we never have more than +1<<31 vars. Note(user): on 2013-11, microbenchmarks on the arc costs +callbacks also showed a 2% speed-up thanks to using int rather than +int64_t.)doc"; + +static const char* __doc_operations_research_RoutingModel_CostClass = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CostClass_CostClass = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CostClass_DimensionCost = + R"doc(Only dimensions that have non-zero cost evaluator and a non-zero cost +coefficient (in this cost class) are listed here. Since we only need +their transit evaluator (the raw version that takes var index, not +Node Index) and their span cost coefficient, we just store those. This +is sorted by the natural operator < (and *not* by DimensionIndex).)doc"; + +static const char* + __doc_operations_research_RoutingModel_CostClass_DimensionCost_dimension = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CostClass_DimensionCost_operator_lt = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CostClass_DimensionCost_slack_cost_coefficient = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CostClass_DimensionCost_span_cost_coefficient = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CostClass_DimensionCost_transit_evaluator_class = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CostClass_dimension_transit_evaluator_class_and_cost_coefficient = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CostClass_evaluator_index = + R"doc(Index of the arc cost evaluator, registered in the RoutingModel class.)doc"; + +static const char* __doc_operations_research_RoutingModel_CostVar = + R"doc(Returns the global cost variable which is being minimized.)doc"; + +static const char* + __doc_operations_research_RoutingModel_CostsAreHomogeneousAcrossVehicles = + R"doc(Whether costs are homogeneous across all vehicles.)doc"; + +static const char* __doc_operations_research_RoutingModel_CreateCPOperator = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CreateCPOperator_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateCPOperatorWithNeighbors = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateCPOperatorWithNeighbors_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CreateDisjunction = + R"doc(Returns nullptr if no penalty cost, otherwise returns penalty +variable.)doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateFirstSolutionDecisionBuilders = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateInsertionOperator = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateIntVarFilteredDecisionBuilder = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateLocalSearchFilters = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateLocalSearchParameters = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateMakeInactiveOperator = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateNeighborhoodOperators = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CreateOperator = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CreateOperator_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateOperatorWithNeighbors = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateOperatorWithNeighborsRatio = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateOperatorWithNeighborsRatio_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateOperatorWithNeighborsRatio_3 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CreatePairOperator = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CreatePairOperator_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_CreatePrimaryLocalSearchDecisionBuilder = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_CreateSameVehicleCost = + R"doc(Returns the cost variable related to the soft same vehicle constraint +of index 'vehicle_index'.)doc"; + +static const char* + __doc_operations_research_RoutingModel_CreateSolutionFinalizer = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_DebugOutputAssignment = + R"doc(Print some debugging information about an assignment, including the +feasible intervals of the CumulVar for dimension "dimension_to_print" +at each step of the routes. If "dimension_to_print" is omitted, all +dimensions will be printed.)doc"; + +static const char* + __doc_operations_research_RoutingModel_DetectImplicitPickupAndDeliveries = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_DimensionCumulOptimizers = + R"doc(Internal struct used to store the lp/mp versions of the local and +global cumul optimizers for a given dimension.)doc"; + +static const char* + __doc_operations_research_RoutingModel_DimensionCumulOptimizers_lp_optimizer = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_DimensionCumulOptimizers_mp_optimizer = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_DisjunctionValues = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_DisjunctionValues_max_cardinality = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_DisjunctionValues_penalty = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_DoRestoreAssignment = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_End = + R"doc(Returns the variable index of the ending node of a vehicle route.)doc"; + +static const char* + __doc_operations_research_RoutingModel_FastSolveFromAssignmentWithParameters = + R"doc(Improves a given assignment using unchecked local search. If +check_solution_in_cp is true the final solution will be checked with +the CP solver. As of 11/2023, only works with greedy descent.)doc"; + +static const char* __doc_operations_research_RoutingModel_FilterOptions = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_FilterOptions_filter_objective = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_FilterOptions_filter_with_cp_solver = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_FilterOptions_operator_eq = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_FinalizeAllowedVehicles = + R"doc(The following method looks at node-to-vehicle assignment feasibility +based on node transit values on unary dimensions: If the (absolute) +value of transit for a node is greater than the vehicle capacity on +any dimension, this node cannot be served by this vehicle and the +latter is thus removed from allowed_vehicles_[node].)doc"; + +static const char* __doc_operations_research_RoutingModel_FinalizeVisitTypes = + R"doc(This method scans the visit types and sets up the following members: - +single_nodes_of_type_[type] contains indices of nodes of visit type +"type" which are not part of any pickup/delivery pair. - +pair_indices_of_type_[type] is the set of "pair_index" such that +pickup_delivery_pairs_[pair_index] has at least one pickup or delivery +with visit type "type". - topologically_sorted_visit_types_ contains +the visit types in topological order based on required-->dependent +arcs from the visit type requirements.)doc"; + +static const char* + __doc_operations_research_RoutingModel_FindErrorInSearchParametersForModel = + R"doc(Checks that the current search parameters are valid for the current +model's specific settings.)doc"; + +static const char* __doc_operations_research_RoutingModel_FindNextActive = + R"doc(Returns the first active variable index in 'indices' starting from +index + 1.)doc"; + +static const char* + __doc_operations_research_RoutingModel_ForEachNodeInDisjunctionWithMaxCardinalityFromIndex = + R"doc(Calls f for each variable index of indices in the same disjunctions as +the node corresponding to the variable index 'index'; only +disjunctions of cardinality 'cardinality' are considered.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetAllDimensionNames = + R"doc(Outputs the names of all dimensions added to the routing engine.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetAmortizedLinearCostFactorOfVehicles = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetAmortizedQuadraticCostFactorOfVehicles = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetArcCostForClass = + R"doc(Returns the cost of the segment between two nodes for a given cost +class. Input are variable indices of nodes and the cost class. Unlike +GetArcCostForVehicle(), if cost_class is kNoCost, then the returned +cost won't necessarily be zero: only some of the components of the +cost that depend on the cost class will be omited. See the code for +details.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetArcCostForClassInternal = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetArcCostForFirstSolution = + R"doc(Returns the cost of the arc in the context of the first solution +strategy. This is typically a simplification of the actual cost; see +the .cc.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetArcCostForVehicle = + R"doc(Returns the cost of the transit arc between two nodes for a given +vehicle. Input are variable indices of node. This returns 0 if vehicle +< 0.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetAutomaticFirstSolutionStrategy = + R"doc(Returns the automatic first solution strategy selected.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetBinCapacities = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetCostClassIndexOfVehicle = + R"doc(Get the cost class index of the given vehicle.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetCostClassesCount = + R"doc(Returns the number of different cost classes in the model.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetCumulBounds = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetDeliveryPositions = + R"doc(Returns the pickup and delivery positions where the node is a +delivery.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetDepot = + R"doc(Returns the variable index of the first starting or ending node of all +routes. If all routes start and end at the same node (single depot), +this is the node returned.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetDimensionIndex = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetDimensionOrDie = + R"doc(Returns a dimension from its name. Dies if the dimension does not +exist.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetDimensionResourceGroupIndex = + R"doc(Returns the index of the resource group attached to the dimension. +DCHECKS that there's exactly one resource group for this dimension.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetDimensionResourceGroupIndices = + R"doc(Returns the indices of resource groups for this dimension. This method +can only be called after the model has been closed.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetDimensionTransitCostSum = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetDimensions = + R"doc(Returns all dimensions of the model.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetDimensionsWithGlobalCumulOptimizers = + R"doc(Returns the dimensions which have +[global|local]_dimension_optimizers_.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetDimensionsWithLocalCumulOptimizers = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetDimensionsWithSoftOrSpanCosts = + R"doc(Returns dimensions with soft or vehicle span costs.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetDisjunctionIndices = + R"doc(Returns the indices of the disjunctions to which an index belongs.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetDisjunctionMaxCardinality = + R"doc(Returns the maximum number of possible active nodes of the node +disjunction of index 'index'.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetDisjunctionNodeIndices = + R"doc(Returns the variable indices of the nodes in the disjunction of index +'index'.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetDisjunctionPenalty = + R"doc(Returns the penalty of the node disjunction of index 'index'.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetFilteredFirstSolutionDecisionBuilderOrNull = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetFirstSolutionDecisionBuilder = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetFixedCostOfVehicle = + R"doc(Returns the route fixed cost taken into account if the route of the +vehicle is not empty, aka there's at least one node on the route other +than the first and last nodes.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetGlobalCumulOptimizerIndex = + R"doc(Returns the internal global/local optimizer index for the given +dimension if any, and -1 otherwise.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetHardTypeIncompatibilitiesOfType = + R"doc(Returns visit types incompatible with a given type.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetHomogeneousCost = + R"doc(Returns the cost of the segment between two nodes supposing all +vehicle costs are the same (returns the cost for the first vehicle +otherwise).)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetImplicitUniquePickupAndDeliveryPairs = + R"doc(Returns implicit pickup and delivery pairs currently in the model. +Pairs are implicit if they are not linked by a pickup and delivery +constraint but that for a given unary dimension, the first element of +the pair has a positive demand d, and the second element has a demand +of -d.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetLocalCumulOptimizerIndex = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetMaximumNumberOfActiveVehicles = + R"doc(Returns the maximum number of active vehicles.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetMutableCPInterrupt = + R"doc(Returns the atomic to stop the CP solver.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetMutableCPSatInterrupt = + R"doc(Returns the atomic to stop the CP-SAT solver.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetMutableDimension = + R"doc(Returns a dimension from its name. Returns nullptr if the dimension +does not exist.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetMutableGlobalCumulLPOptimizer = + R"doc(Returns the global/local dimension cumul optimizer for a given +dimension, or nullptr if there is none.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetMutableGlobalCumulMPOptimizer = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetMutableLocalCumulLPOptimizer = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetMutableLocalCumulMPOptimizer = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetNeighborhoodOperators = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetNextVarMax = + R"doc(Get the current maximum of the next variable associated to index.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetNextVarMin = + R"doc(Get the current minimum of the next variable associated to index.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetNonZeroCostClassesCount = + R"doc(Ditto, minus the 'always zero', built-in cost class.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetNumOfSingletonNodes = + R"doc(Returns the number of non-start/end nodes which do not appear in a +pickup/delivery pair.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetNumberOfDecisionsInFirstSolution = + R"doc(Returns statistics on first solution search, number of decisions sent +to filters, number of decisions rejected by filters.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetNumberOfDisjunctions = + R"doc(Returns the number of node disjunctions in the model.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetNumberOfRejectsInFirstSolution = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetNumberOfVisitTypes = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetOrCreateAssignment = + R"doc(Set of auxiliary methods used to setup the search.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetOrCreateCumulativeLimit = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetOrCreateFirstSolutionLargeNeighborhoodSearchLimit = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetOrCreateLargeNeighborhoodSearchLimit = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetOrCreateLimit = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetOrCreateLocalSearchFilterManager = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetOrCreateLocalSearchLimit = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetOrCreateNodeNeighborsByCostClass = + R"doc(Returns neighbors of all nodes for every cost class. The result is +cached and is computed once. The number of neighbors considered is +based on a ratio of non-vehicle nodes, specified by neighbors_ratio, +with a minimum of min-neighbors node considered.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetOrCreateNodeNeighborsByCostClass_2 = + R"doc(Returns parameters.num_neighbors neighbors of all nodes for every cost +class. The result is cached and is computed once.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetOrCreateTmpAssignment = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetPairIndicesOfType = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetPathsMetadata = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetPerfectBinaryDisjunctions = + R"doc(Returns the list of all perfect binary disjunctions, as pairs of +variable indices: a disjunction is "perfect" when its variables do not +appear in any other disjunction. Each pair is sorted (lowest variable +index first), and the output vector is also sorted (lowest pairs +first).)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetPickupAndDeliveryDisjunctions = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetPickupAndDeliveryPairs = + R"doc(Returns pickup and delivery pairs currently in the model.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetPickupAndDeliveryPolicyOfVehicle = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetPickupPositions = + R"doc(Returns the pickup and delivery positions where the node is a pickup.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetPrimaryConstrainedDimension = + R"doc(Get the primary constrained dimension, or an empty string if it is +unset.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetResourceGroup = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetResourceGroups = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetRoutesFromAssignment = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetSameVehicleIndicesOfIndex = + R"doc(Returns variable indices of nodes constrained to be on the same route.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetSearchMonitors = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetSingleNodesOfType = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetTemporalTypeIncompatibilitiesOfType = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetTopologicallySortedVisitTypes = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetUnaryDimensions = + R"doc(Returns dimensions for which all transit evaluators are unary.)doc"; + +static const char* + __doc_operations_research_RoutingModel_GetVehicleClassIndexOfVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetVehicleClassesCount = + R"doc(Returns the number of different vehicle classes in the model.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetVehicleOfClass = + R"doc(Returns a vehicle of the given vehicle class, and -1 if there are no +vehicles for this class.)doc"; + +static const char* __doc_operations_research_RoutingModel_GetVehicleStartClass = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_GetVehicleTypeContainer = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetVisitType = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_GetVisitTypePolicy = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_HasDimension = + R"doc(Returns true if a dimension exists for a given dimension name.)doc"; + +static const char* __doc_operations_research_RoutingModel_HasGlobalCumulOptimizer = + R"doc(Returns whether the given dimension has global/local cumul optimizers.)doc"; + +static const char* + __doc_operations_research_RoutingModel_HasHardTypeIncompatibilities = + R"doc(Returns true iff any hard (resp. temporal) type incompatibilities have +been added to the model.)doc"; + +static const char* + __doc_operations_research_RoutingModel_HasLocalCumulOptimizer = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_HasMandatoryDisjunctions = + R"doc(Returns true if the model contains mandatory disjunctions (ones with +kNoPenalty as penalty).)doc"; + +static const char* + __doc_operations_research_RoutingModel_HasMaxCardinalityConstrainedDisjunctions = + R"doc(Returns true if the model contains at least one disjunction which is +constrained by its max_cardinality.)doc"; + +static const char* + __doc_operations_research_RoutingModel_HasSameVehicleTypeRequirements = + R"doc(Returns true iff any same-route (resp. temporal) type requirements +have been added to the model.)doc"; + +static const char* + __doc_operations_research_RoutingModel_HasTemporalTypeIncompatibilities = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_HasTemporalTypeRequirements = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_HasTypeRegulations = + R"doc(Returns true iff the model has any incompatibilities or requirements +set on node types.)doc"; + +static const char* + __doc_operations_research_RoutingModel_HasVehicleWithCostClassIndex = + R"doc(Returns true iff the model contains a vehicle with the given +cost_class_index.)doc"; + +static const char* + __doc_operations_research_RoutingModel_IgnoreDisjunctionsAlreadyForcedToZero = + R"doc(SPECIAL: Makes the solver ignore all the disjunctions whose active +variables are all trivially zero (i.e. Max() == 0), by setting their +max_cardinality to 0. This can be useful when using the +BaseBinaryDisjunctionNeighborhood operators, in the context of arc- +based routing.)doc"; + +static const char* + __doc_operations_research_RoutingModel_InitSameVehicleGroups = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_Initialize = + R"doc(Internal methods.)doc"; + +static const char* + __doc_operations_research_RoutingModel_InitializeDimensionInternal = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_IsDelivery = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_IsEnd = + R"doc(Returns true if 'index' represents the last node of a route.)doc"; + +static const char* __doc_operations_research_RoutingModel_IsMatchingModel = + R"doc(Returns true if a vehicle/node matching problem is detected.)doc"; + +static const char* __doc_operations_research_RoutingModel_IsPickup = + R"doc(Returns whether the node is a pickup (resp. delivery).)doc"; + +static const char* __doc_operations_research_RoutingModel_IsStart = + R"doc(Returns true if 'index' represents the first node of a route.)doc"; + +static const char* + __doc_operations_research_RoutingModel_IsVehicleAllowedForIndex = + R"doc(Returns true if a vehicle is allowed to visit a given node.)doc"; + +static const char* __doc_operations_research_RoutingModel_IsVehicleUsed = + R"doc(Returns true if the route of 'vehicle' is non empty in 'assignment'.)doc"; + +static const char* + __doc_operations_research_RoutingModel_IsVehicleUsedWhenEmpty = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_LogSolution = + R"doc(Log a solution.)doc"; + +static const char* + __doc_operations_research_RoutingModel_MakeGreedyDescentLSOperator = + R"doc(Perhaps move it to constraint_solver.h. MakeGreedyDescentLSOperator +creates a local search operator that tries to improve the initial +assignment by moving a logarithmically decreasing step away in each +possible dimension.)doc"; + +static const char* + __doc_operations_research_RoutingModel_MakeGuidedSlackFinalizer = + R"doc(MakeGuidedSlackFinalizer creates a DecisionBuilder for the slacks of a +dimension using a callback to choose which values to start with. The +finalizer works only when all next variables in the model have been +fixed. It has the following two characteristics: 1. It follows the +routes defined by the nexts variables when choosing a variable to make +a decision on. 2. When it comes to choose a value for the slack of +node i, the decision builder first calls the callback with argument i, +and supposingly the returned value is x it creates decisions slack[i] += x, slack[i] = x + 1, slack[i] = x - 1, slack[i] = x + 2, etc.)doc"; + +static const char* + __doc_operations_research_RoutingModel_MakeSelfDependentDimensionFinalizer = + R"doc(__SWIG__ MakeSelfDependentDimensionFinalizer is a finalizer for the +slacks of a self-dependent dimension. It makes an extensive use of the +caches of the state dependent transits. In detail, +MakeSelfDependentDimensionFinalizer returns a composition of a local +search decision builder with a greedy descent operator for the cumul +of the start of each route and a guided slack finalizer. Provided +there are no time windows and the maximum slacks are large enough, +once the cumul of the start of route is fixed, the guided finalizer +can find optimal values of the slacks for the rest of the route in +time proportional to the length of the route. Therefore the composed +finalizer generally works in time O(log(t)*n*m), where t is the latest +possible departute time, n is the number of nodes in the network and m +is the number of vehicles.)doc"; + +static const char* + __doc_operations_research_RoutingModel_MakeStateDependentTransit = + R"doc(Creates a cached StateDependentTransit from an std::function.)doc"; + +static const char* __doc_operations_research_RoutingModel_MutablePreAssignment = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_Next = + R"doc(Assignment inspection Returns the variable index of the node directly +after the node corresponding to 'index' in 'assignment'.)doc"; + +static const char* __doc_operations_research_RoutingModel_NextVar = + R"doc(!defined(SWIGPYTHON) Returns the next variable of the node +corresponding to index. Note that NextVar(index) == index is +equivalent to ActiveVar(index) == 0.)doc"; + +static const char* __doc_operations_research_RoutingModel_Nexts = + R"doc(Returns all next variables of the model, such that Nexts(i) is the +next variable of the node corresponding to i.)doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsByCostClass = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsByCostClass_ComputeNeighbors = + R"doc(Computes num_neighbors neighbors of all nodes for every cost class in +routing_model.)doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsByCostClass_GetNeighborsOfNodeForCostClass = + R"doc(Returns the neighbors of the given node for the given cost_class.)doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsByCostClass_NodeNeighborsByCostClass = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsByCostClass_all_nodes = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsByCostClass_node_index_to_neighbors_by_cost_class = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsParameters = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsParameters_add_vehicle_starts_to_neighbors = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsParameters_num_neighbors = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_NodeNeighborsParameters_operator_eq = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_OptimizeCumulsOfDimensionFromAssignmentWithCumulDependentTransits = + R"doc(This method optimizes the cumuls of the 'original_assignment' for the +given 'dimension'. The goal of the optimizer is to find an optimal +scheduling w.r.t the original costs in the model, while also +minimizing the "error" in the transit value considered between +consecutive nodes compared to the given piecewise linear formulations.)doc"; + +static const char* + __doc_operations_research_RoutingModel_PackCumulsOfOptimizerDimensionsFromAssignment = + R"doc(For every dimension in the model with an optimizer in +local/global_dimension_optimizers_, this method tries to pack the +cumul values of the dimension, such that: - The cumul costs (span +costs, soft lower and upper bound costs, etc) are minimized. - The +cumuls of the ends of the routes are minimized for this given minimal +cumul cost. - Given these minimal end cumuls, the route start cumuls +are maximized. Returns the assignment resulting from allocating these +packed cumuls with the solver, and nullptr if these cumuls could not +be set by the solver.)doc"; + +static const char* __doc_operations_research_RoutingModel_PickupAndDeliveryPolicy = + R"doc(Types of precedence policy applied to pickup and delivery pairs.)doc"; + +static const char* + __doc_operations_research_RoutingModel_PickupAndDeliveryPolicy_PICKUP_AND_DELIVERY_FIFO = + R"doc(Deliveries must be performed in the same order as pickups.)doc"; + +static const char* + __doc_operations_research_RoutingModel_PickupAndDeliveryPolicy_PICKUP_AND_DELIVERY_LIFO = + R"doc(Deliveries must be performed in reverse order of pickups.)doc"; + +static const char* + __doc_operations_research_RoutingModel_PickupAndDeliveryPolicy_PICKUP_AND_DELIVERY_NO_ORDER = + R"doc(Any precedence is accepted.)doc"; + +static const char* __doc_operations_research_RoutingModel_PickupDeliveryPosition = + R"doc(The position of a node in the set of pickup and delivery pairs.)doc"; + +static const char* + __doc_operations_research_RoutingModel_PickupDeliveryPosition_alternative_index = + R"doc(The index of the node in the vector of pickup (resp. delivery) +alternatives of the pair.)doc"; + +static const char* + __doc_operations_research_RoutingModel_PickupDeliveryPosition_pd_pair_index = + R"doc(The index of the pickup and delivery pair within which the node +appears.)doc"; + +static const char* __doc_operations_research_RoutingModel_PreAssignment = + R"doc(Returns an assignment used to fix some of the variables of the +problem. In practice, this assignment locks partial routes of the +problem. This can be used in the context of locking the parts of the +routes which have already been driven in online routing problems.)doc"; + +static const char* __doc_operations_research_RoutingModel_QuietCloseModel = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_QuietCloseModelWithParameters = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ReadAssignment = + R"doc(Reads an assignment from a file and returns the current solution. +Returns nullptr if the file cannot be opened or if the assignment is +not valid.)doc"; + +static const char* + __doc_operations_research_RoutingModel_ReadAssignmentFromRoutes = + R"doc(Restores the routes as the current solution. Returns nullptr if the +solution cannot be restored (routes do not contain a valid solution). +Note that calling this method will run the solver to assign values to +the dimension variables; this may take considerable amount of time, +especially when using dimensions with slack.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RegisterStateDependentTransitCallback = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RegisterTransitCallback = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RegisterTransitMatrix = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RegisterUnaryTransitCallback = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RegisterUnaryTransitVector = + R"doc(Registers 'callback' and returns its index. The sign parameter allows +to notify the solver that the callback only return values of the given +sign. This can help the solver, but passing an incorrect sign may +crash in non-opt compilation mode, and yield incorrect results in opt.)doc"; + +static const char* __doc_operations_research_RoutingModel_RemainingTime = + R"doc(Returns the time left in the search limit.)doc"; + +static const char* __doc_operations_research_RoutingModel_ReplaceUnusedVehicle = + R"doc(Replaces the route of unused_vehicle with the route of active_vehicle +in compact_assignment. Expects that unused_vehicle is a vehicle with +an empty route and that the route of active_vehicle is non-empty. Also +expects that 'assignment' contains the original assignment, from which +compact_assignment was created. Returns true if the vehicles were +successfully swapped; otherwise, returns false.)doc"; + +static const char* __doc_operations_research_RoutingModel_ResourceGroup = + R"doc(A ResourceGroup defines a set of available Resources with attributes +on one or multiple dimensions. For every ResourceGroup in the model, +each (used) vehicle in the solution which requires a resource (see +NotifyVehicleRequiresResource()) from this group must be assigned to +exactly 1 resource, and each resource can in turn be assigned to at +most 1 vehicle requiring it. This vehicle-to-resource assignment will +apply the corresponding Attributes to the dimensions affected by the +resource group. NOTE: As of 2021/07, each ResourceGroup can only +affect a single RoutingDimension at a time, i.e. all Resources in a +group must apply attributes to the same single dimension.)doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_AddResource = + R"doc(Adds a Resource with the given attributes for the corresponding +dimension. Returns the index of the added resource in resources_.)doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Attributes = + R"doc(Attributes for a dimension.)doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Attributes_Attributes = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Attributes_Attributes_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Attributes_end_domain = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Attributes_end_domain_2 = + R"doc(end_domain_.Min() <= cumul[End(v)] <= end_domain_.Max())doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Attributes_start_domain = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Attributes_start_domain_2 = + R"doc(The following domains constrain the dimension start/end cumul of the +vehicle assigned to this resource: start_domain_.Min() <= +cumul[Start(v)] <= start_domain_.Max())doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_ClearAllowedResourcesForVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_ComputeResourceClasses = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetAffectedDimensionIndices = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetDimensionAttributesForClass = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetResource = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetResourceClassIndex = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetResourceClassesCount = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetResourceIndicesInClass = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetResourceIndicesPerClass = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetResources = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetResourcesMarkedAllowedForVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_GetVehiclesRequiringAResource = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ResourceGroup_Index = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_IsResourceAllowedForVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_NotifyVehicleRequiresAResource = + R"doc(Notifies that the given vehicle index requires a resource from this +group if the vehicle is used (i.e. if its route is non-empty or +vehicle_used_when_empty_[vehicle] is true).)doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Resource = + R"doc(A Resource sets attributes (costs/constraints) for a set of +dimensions.)doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_ResourceGroup = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Resource_GetDefaultAttributes = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Resource_GetDimensionAttributes = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Resource_Resource = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Resource_SetDimensionAttributes = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Resource_dimension_attributes = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_Resource_model = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_SetAllowedResourcesForVehicle = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ResourceGroup_Size = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_VehicleRequiresAResource = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_affected_dimension_indices = + R"doc(All indices of dimensions affected by this resource group.)doc"; + +static const char* __doc_operations_research_RoutingModel_ResourceGroup_index = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ResourceGroup_model = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_resource_class_indices = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_resource_indices_per_class = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_resources = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_vehicle_requires_resource = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_ResourceGroup_vehicles_requiring_resource = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ResourceVar = + R"doc(Returns the resource variable for the given vehicle index in the given +resource group. If a vehicle doesn't require a resource from the +corresponding resource group, then ResourceVar(v, r_g) == -1.)doc"; + +static const char* __doc_operations_research_RoutingModel_ResourceVars = + R"doc(Returns vehicle resource variables for a given resource group, such +that ResourceVars(r_g)[v] is the resource variable for vehicle 'v' in +resource group 'r_g'.)doc"; + +static const char* __doc_operations_research_RoutingModel_RestoreAssignment = + R"doc(Restores an assignment as a solution in the routing model and returns +the new solution. Returns nullptr if the assignment is not valid.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteCanBeUsedByVehicle = + R"doc(Checks that all nodes on the route starting at start_index (using the +solution stored in assignment) can be visited by the given vehicle.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo = + R"doc(Contains the information needed by the solver to optimize a +dimension's cumuls with travel-start dependent transit values.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_DebugString = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo = + R"doc(Contains the information for a single transition on the route.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_DebugString = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_PiecewiseLinearFormulation = + R"doc(The following struct defines a piecewise linear formulation, with +int64_t values for the "anchor" x and y values, and potential double +values for the slope of each linear function.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_PiecewiseLinearFormulation_DebugString = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_PiecewiseLinearFormulation_x_anchors = + R"doc(The set of *increasing* anchor cumul values for the interpolation.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_PiecewiseLinearFormulation_y_anchors = + R"doc(The y values used for the interpolation: For any x anchor value, let i +be an index such that x_anchors[i] ≤ x < x_anchors[i+1], then the y +value for x is y_anchors[i] * (1-λ) + y_anchors[i+1] * λ, with λ = (x +- x_anchors[i]) / (x_anchors[i+1] - x_anchors[i]).)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_compressed_travel_value_lower_bound = + R"doc(The hard lower bound of the compressed travel value that will be +enforced by the scheduling module.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_post_travel_transit_value = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_pre_travel_transit_value = + R"doc(The parts of the transit which occur pre/post travel between the +nodes. The total transit between the two nodes i and j is = +pre_travel_transit_value + travel(i, j) + post_travel_transit_value.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_travel_compression_cost = + R"doc(travel_compression_cost models the cost of the difference between the +(real) travel value Tᵣ given by travel_start_dependent_travel and the +compressed travel value considered in the scheduling.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_travel_start_dependent_travel = + R"doc(Models the (real) travel value Tᵣ, for this transition based on the +departure value of the travel.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_TransitionInfo_travel_value_upper_bound = + R"doc(The hard upper bound of the (real) travel value Tᵣ (see above). This +value should be chosen so as to prevent the overall cost of the model +(dimension costs + travel_compression_cost) to overflow.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_transition_info = + R"doc(For each node #i on the route, transition_info[i] contains the +relevant information for the travel between nodes #i and #(i + 1) on +the route.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RouteDimensionTravelInfo_travel_cost_coefficient = + R"doc(The cost per unit of travel for this vehicle.)doc"; + +static const char* __doc_operations_research_RoutingModel_RoutesToAssignment = + R"doc(Fills an assignment from a specification of the routes of the +vehicles. The routes are specified as lists of variable indices that +appear on the routes of the vehicles. The indices of the outer vector +in 'routes' correspond to vehicles IDs, the inner vector contains the +variable indices on the routes for the given vehicle. The inner +vectors must not contain the start and end indices, as these are +determined by the routing model. Sets the value of NextVars in the +assignment, adding the variables to the assignment if necessary. The +method does not touch other variables in the assignment. The method +can only be called after the model is closed. With +ignore_inactive_indices set to false, this method will fail (return +nullptr) in case some of the route contain indices that are +deactivated in the model; when set to true, these indices will be +skipped. Returns true if routes were successfully loaded. However, +such assignment still might not be a valid solution to the routing +problem due to more complex constraints; it is advisible to call +solver()->CheckSolution() afterwards.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator = + R"doc(Local search move operator usable in routing.)doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_CROSS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_CROSS_EXCHANGE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_EXCHANGE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_EXCHANGE_PAIR = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_EXCHANGE_RELOCATE_PAIR = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_EXCHANGE_SUBTRIP = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_EXTENDED_SWAP_ACTIVE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_FULL_PATH_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_GLOBAL_CHEAPEST_INSERTION_CLOSE_NODES_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_GLOBAL_CHEAPEST_INSERTION_EXPENSIVE_CHAIN_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_GLOBAL_CHEAPEST_INSERTION_PATH_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_INACTIVE_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_LIGHT_RELOCATE_PAIR = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_LIN_KERNIGHAN = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_LOCAL_CHEAPEST_INSERTION_CLOSE_NODES_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_LOCAL_CHEAPEST_INSERTION_EXPENSIVE_CHAIN_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_LOCAL_CHEAPEST_INSERTION_PATH_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_LOCAL_SEARCH_OPERATOR_COUNTER = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_MAKE_ACTIVE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_MAKE_ACTIVE_AND_RELOCATE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_MAKE_CHAIN_INACTIVE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_MAKE_INACTIVE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_NODE_PAIR_SWAP = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_OR_OPT = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_PATH_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_RELOCATE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_RELOCATE_AND_MAKE_ACTIVE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_RELOCATE_EXPENSIVE_CHAIN = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_RELOCATE_NEIGHBORS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_RELOCATE_PAIR = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_RELOCATE_PATH_GLOBAL_CHEAPEST_INSERTION_INSERT_UNPERFORMED = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_RELOCATE_SUBTRIP = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_SHORTEST_PATH_SWAP_ACTIVE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_SWAP_ACTIVE = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_TSP_LNS = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_TSP_OPT = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_RoutingLocalSearchOperator_TWO_OPT = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_RoutingModel = + R"doc(Constructor taking an index manager. The version which does not take +RoutingModelParameters is equivalent to passing +DefaultRoutingModelParameters().)doc"; + +static const char* __doc_operations_research_RoutingModel_RoutingModel_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_RoutingModel_3 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SafeGetCostClassInt64OfVehicle = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_SecondaryOptimizer = + R"doc(Class used to solve a secondary model within a first solution +strategy.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SecondaryOptimizer_SecondaryOptimizer = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SecondaryOptimizer_Solve = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SecondaryOptimizer_call_count = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SecondaryOptimizer_model = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SecondaryOptimizer_search_parameters = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SecondaryOptimizer_solve_period = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SecondaryOptimizer_state = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SecondaryOptimizer_var_to_index = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SetAllowedVehiclesForIndex = + R"doc(Sets the vehicles which can visit a given node. If the node is in a +disjunction, this will not prevent it from being unperformed. +Specifying an empty vector of vehicles has no effect (all vehicles +will be allowed to visit the node).)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetAmortizedCostFactorsOfAllVehicles = + R"doc(The following methods set the linear and quadratic cost factors of +vehicles (must be positive values). The default value of these +parameters is zero for all vehicles. + +When set, the cost_ of the model will contain terms aiming at reducing +the number of vehicles used in the model, by adding the following to +the objective for every vehicle v: INDICATOR(v used in the model) * +[linear_cost_factor_of_vehicle_[v] - +quadratic_cost_factor_of_vehicle_[v]*(square of length of route v)] +i.e. for every used vehicle, we add the linear factor as fixed cost, +and subtract the square of the route length multiplied by the +quadratic factor. This second term aims at making the routes as dense +as possible. + +Sets the linear and quadratic cost factor of all vehicles.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetAmortizedCostFactorsOfVehicle = + R"doc(Sets the linear and quadratic cost factor of the given vehicle.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetArcCostEvaluatorOfAllVehicles = + R"doc(Sets the cost function of the model such that the cost of a segment of +a route between node 'from' and 'to' is evaluator(from, to), whatever +the route or vehicle performing the route.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetArcCostEvaluatorOfVehicle = + R"doc(Sets the cost function for a given vehicle route.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetAssignmentFromOtherModelAssignment = + R"doc(Given a "source_model" and its "source_assignment", resets +"target_assignment" with the IntVar variables (nexts_, and +vehicle_vars_ if costs aren't homogeneous across vehicles) of "this" +model, with the values set according to those in "other_assignment". +The objective_element of target_assignment is set to this->cost_.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetFirstSolutionEvaluator = + R"doc(Takes ownership of evaluator.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetFixedCostOfAllVehicles = + R"doc(Sets the fixed cost of all vehicle routes. It is equivalent to calling +SetFixedCostOfVehicle on all vehicle routes.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetFixedCostOfVehicle = + R"doc(Sets the fixed cost of one vehicle route.)doc"; + +static const char* __doc_operations_research_RoutingModel_SetIndexNeighborFinder = + R"doc(Sets the sweep arranger to be used by routing heuristics; ownership of +the arranger is taken.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetMaximumNumberOfActiveVehicles = + R"doc(Constrains the maximum number of active vehicles, aka the number of +vehicles which do not have an empty route. For instance, this can be +used to limit the number of routes in the case where there are fewer +drivers than vehicles and that the fleet of vehicle is heterogeneous.)doc"; + +static const char* __doc_operations_research_RoutingModel_SetNextVarRange = + R"doc(e.g. intersection with a set of values, removing a set of values, +membership test... on a per-need basis. Restricts the range of the +next variable associated to index.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetOptimizedWithCumulDependentTransits = + R"doc(Notifies that the given dimension's cumuls can be optimized with +cumul-dependent transits.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetPathEnergyCostOfVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SetPathEnergyCostsOfVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SetPickupAndDeliveryPolicyOfAllVehicles = + R"doc(Sets the Pickup and delivery policy of all vehicles. It is equivalent +to calling SetPickupAndDeliveryPolicyOfVehicle on all vehicles.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SetPickupAndDeliveryPolicyOfVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SetPrimaryConstrainedDimension = + R"doc(Set the given dimension as "primary constrained". As of August 2013, +this is only used by ArcIsMoreConstrainedThanArc(). "dimension" must +be the name of an existing dimension, or be empty, in which case there +will not be a primary dimension after this call.)doc"; + +static const char* __doc_operations_research_RoutingModel_SetSameVehicleGroup = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_SetSecondaryModel = + R"doc(Sets a secondary solver (routing model + parameters) which can be used +to run sub-solves while building a first solution.)doc"; + +static const char* __doc_operations_research_RoutingModel_SetSweepArranger = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_SetTabuVarsCallback = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SetVehicleUsedWhenEmpty = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_SetVisitType = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SetupAssignmentCollector = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SetupDecisionBuilders = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_SetupImprovementLimit = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_SetupMetaheuristics = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_SetupSearch = + R"doc(Sets up search objects, such as decision builders and monitors.)doc"; + +static const char* __doc_operations_research_RoutingModel_SetupSearchMonitors = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_SetupTrace = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_Size = + R"doc(Returns the number of next variables in the model.)doc"; + +static const char* __doc_operations_research_RoutingModel_Solve = + R"doc(Solves the current routing model; closes the current model. This is +equivalent to calling +SolveWithParameters(DefaultRoutingSearchParameters()) or +SolveFromAssignmentWithParameters(assignment, +DefaultRoutingSearchParameters()).)doc"; + +static const char* + __doc_operations_research_RoutingModel_SolveFromAssignmentWithParameters = + R"doc(Same as above, except that if assignment is not null, it will be used +as the initial solution.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SolveFromAssignmentsWithParameters = + R"doc(Same as above but will try all assignments in order as first solutions +until one succeeds.)doc"; + +static const char* __doc_operations_research_RoutingModel_SolveMatchingModel = + R"doc(Solve matching problem with min-cost flow and store result in +assignment.)doc"; + +static const char* + __doc_operations_research_RoutingModel_SolveWithIteratedLocalSearch = + R"doc(Solves the current routing model by using an Iterated Local Search +approach.)doc"; + +static const char* __doc_operations_research_RoutingModel_SolveWithParameters = + R"doc(Solves the current routing model with the given parameters. If +'solutions' is specified, it will contain the k best solutions found +during the search (from worst to best, including the one returned by +this method), where k corresponds to the +'number_of_solutions_to_collect' in 'search_parameters'. Note that the +Assignment returned by the method and the ones in solutions are owned +by the underlying solver and should not be deleted.)doc"; + +static const char* __doc_operations_research_RoutingModel_Start = + R"doc(Model inspection. Returns the variable index of the starting node of a +vehicle route.)doc"; + +static const char* + __doc_operations_research_RoutingModel_StateDependentTransit = + R"doc(What follows is relevant for models with time/state dependent +transits. Such transits, say from node A to node B, are functions f: +int64_t->int64_t of the cumuls of a dimension. The user is free to +implement the abstract RangeIntToIntFunction interface, but it is +expected that the implementation of each method is quite fast. For +performance-related reasons, StateDependentTransit keeps an additional +pointer to a RangeMinMaxIndexFunction, with similar functionality to +RangeIntToIntFunction, for g(x) = f(x)+x, where f is the transit from +A to B. In most situations the best solutions are problem-specific, +but in case of doubt the user may use the MakeStateDependentTransit +function from the routing library, which works out-of-the-box, with +very good running time, but memory inefficient in some situations.)doc"; + +static const char* + __doc_operations_research_RoutingModel_StateDependentTransitCallback = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_StateDependentTransit_transit = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_StateDependentTransit_transit_plus_identity = + R"doc(f(x))doc"; + +static const char* + __doc_operations_research_RoutingModel_StoreDimensionCumulOptimizers = + R"doc(Creates global and local cumul optimizers for the dimensions needing +them, and stores them in the corresponding +[local|global]_dimension_optimizers_ vectors. This function also +computes and stores the "offsets" for these dimensions, used in the +local/global optimizers to simplify LP computations. + +Note on the offsets computation: The global/local cumul offsets are +used by the respective optimizers to have smaller numbers, and +therefore better numerical behavior in the LP. These offsets are used +as a minimum value for the cumuls over the route (or globally), i.e. a +value we consider all cumuls to be greater or equal to. When transits +are all positive, the cumuls of every node on a route is necessarily +greater than the cumul of its start. Therefore, the local offset for a +vehicle can be set to the minimum of its start node's cumul, and for +the global optimizers, to the min start cumul over all vehicles. +However, to be able to distinguish between infeasible nodes (i.e. +nodes for which the cumul upper bound is less than the min cumul of +the vehicle's start), we set the offset to "min_start_cumul" - 1. By +doing so, all infeasible nodes described above will have bounds of [0, +0]. Example: Start cumul bounds: [11, 20] --> offset = 11 - 1 = 10. +Two nodes with cumul bounds. Node1: [5, 10], Node2: [7, 20] After +applying the offset to the above windows, they become: Vehicle: [1, +10]. Node1: [0, 0] (infeasible). Node2: [0, 10]. + +On the other hand, when transits on a route can be negative, no +assumption can be made on the cumuls of nodes wrt the start cumuls, +and the offset is therefore set to 0.)doc"; + +static const char* __doc_operations_research_RoutingModel_TimeBuffer = + R"doc(Returns the time buffer to safely return a solution.)doc"; + +static const char* + __doc_operations_research_RoutingModel_TopologicallySortVisitTypes = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_TransitCallback = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_TransitEvaluatorSign = + R"doc(Represents the sign of values returned by a transit evaluator.)doc"; + +static const char* + __doc_operations_research_RoutingModel_TransitEvaluatorSign_kTransitEvaluatorSignNegativeOrZero = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_TransitEvaluatorSign_kTransitEvaluatorSignPositiveOrZero = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_TransitEvaluatorSign_kTransitEvaluatorSignUnknown = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_UnaryTransitCallbackOrNull = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_UnperformedPenalty = + R"doc(Get the "unperformed" penalty of a node. This is only well defined if +the node is only part of a single Disjunction, and that disjunction +has a penalty. For forced active nodes returns max int64_t. In all other +cases, this returns 0.)doc"; + +static const char* + __doc_operations_research_RoutingModel_UnperformedPenaltyOrValue = + R"doc(Same as above except that it returns default_value instead of 0 when +penalty is not well defined (default value is passed as first argument +to simplify the usage of the method in a callback).)doc"; + +static const char* + __doc_operations_research_RoutingModel_UpdateSearchFromParametersIfNeeded = + R"doc(Updates search objects if parameters have changed.)doc"; + +static const char* __doc_operations_research_RoutingModel_UpdateTimeLimit = + R"doc(Updates the time limit of the search limit.)doc"; + +static const char* __doc_operations_research_RoutingModel_UsesLightPropagation = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ValuedNodes = + R"doc(Structure storing a value for a set of variable indices. Is used to +store data for index disjunctions (variable indices, max_cardinality +and penalty when unperformed).)doc"; + +static const char* __doc_operations_research_RoutingModel_ValuedNodes_indices = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ValuedNodes_value = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_VariableValuePair = + R"doc(Struct used to store a variable value.)doc"; + +static const char* + __doc_operations_research_RoutingModel_VariableValuePair_value = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_VariableValuePair_var_index = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_VehicleIndex = + R"doc(Returns the vehicle of the given start/end index, and -1 if the given +index is not a vehicle start/end.)doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleRouteConsideredVar = + R"doc(Returns the variable specifying whether or not the given vehicle route +is considered for costs and constraints. It will be equal to 1 iff the +route of the vehicle is not empty OR vehicle_used_when_empty_[vehicle] +is true.)doc"; + +static const char* __doc_operations_research_RoutingModel_VehicleTypeContainer = + R"doc(Struct used to sort and store vehicles by their type. Two vehicles +have the same "vehicle type" iff they have the same cost class and +start/end nodes.)doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleTypeContainer_NumTypes = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleTypeContainer_Type = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleTypeContainer_VehicleClassEntry = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleTypeContainer_VehicleClassEntry_fixed_cost = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleTypeContainer_VehicleClassEntry_operator_lt = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleTypeContainer_VehicleClassEntry_vehicle_class = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleTypeContainer_sorted_vehicle_classes_per_type = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleTypeContainer_type_index_of_vehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_VehicleTypeContainer_vehicles_per_vehicle_class = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_VehicleVar = + R"doc(Returns the vehicle variable of the node corresponding to index. Note +that VehicleVar(index) == -1 is equivalent to ActiveVar(index) == 0.)doc"; + +static const char* __doc_operations_research_RoutingModel_VehicleVars = + R"doc(Returns all vehicle variables of the model, such that VehicleVars(i) +is the vehicle variable of the node corresponding to i.)doc"; + +static const char* __doc_operations_research_RoutingModel_VisitTypePolicy = + R"doc(Set the node visit types and incompatibilities/requirements between +the types (see below). + +NOTE: Before adding any incompatibilities and/or requirements on +types: 1) All corresponding node types must have been set. 2) +CloseVisitTypes() must be called so all containers are resized +accordingly. + +The following enum is used to describe how a node with a given type +'T' impacts the number of types 'T' on the route when visited, and +thus determines how temporal incompatibilities and requirements take +effect.)doc"; + +static const char* + __doc_operations_research_RoutingModel_VisitTypePolicy_ADDED_TYPE_REMOVED_FROM_VEHICLE = + R"doc(When visited, one instance of type 'T' previously added to the route +(TYPE_ADDED_TO_VEHICLE), if any, is removed from the vehicle. If the +type was not previously added to the route or all added instances have +already been removed, this visit has no effect on the types.)doc"; + +static const char* + __doc_operations_research_RoutingModel_VisitTypePolicy_TYPE_ADDED_TO_VEHICLE = + R"doc(When visited, the number of types 'T' on the vehicle increases by one.)doc"; + +static const char* + __doc_operations_research_RoutingModel_VisitTypePolicy_TYPE_ON_VEHICLE_UP_TO_VISIT = + R"doc(With the following policy, the visit enforces that type 'T' is +considered on the route from its start until this node is visited.)doc"; + +static const char* + __doc_operations_research_RoutingModel_VisitTypePolicy_TYPE_SIMULTANEOUSLY_ADDED_AND_REMOVED = + R"doc(The visit doesn't have an impact on the number of types 'T' on the +route, as it's (virtually) added and removed directly. This policy can +be used for visits which are part of an incompatibility or requirement +set without affecting the type count on the route.)doc"; + +static const char* __doc_operations_research_RoutingModel_WriteAssignment = + R"doc(Writes the current solution to a file containing an AssignmentProto. +Returns false if the file cannot be opened or if there is no current +solution.)doc"; + +static const char* __doc_operations_research_RoutingModel_active = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_assignment = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_at_solution_monitors = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_automatic_first_solution_strategy = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_bin_capacities = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_cache_callbacks = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_closed = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_collect_assignments = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_collect_one_assignment = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_collect_secondary_ls_assignments = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_cost = + R"doc(Costs)doc"; + +static const char* __doc_operations_research_RoutingModel_cost_cache = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_cost_class_index_of_vehicle = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_cost_classes = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_costs_are_homogeneous_across_vehicles = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_cumulative_limit = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_dimension_cumuls_optimized_with_cumul_dependent_transits = + R"doc(Whether or not a given dimension's cumuls could be optimized with +cumul-dependent transits. This is false by default for all dimensions, +and can be set to true specifically for a dimension through +SetOptimizedWithCumulDependentTransits().)doc"; + +static const char* + __doc_operations_research_RoutingModel_dimension_local_optimizer_for_cumul_dependent_transits = + R"doc(When dimension_cumuls_optimized_with_cumul_dependent_transits_[d] = +true for a dimension which doesn't require an LP/MP optimizer based on +the constraints, we create and store a local MP optimizer required for +optimizing with cumul-dependent transits.)doc"; + +static const char* + __doc_operations_research_RoutingModel_dimension_name_to_index = + R"doc(Dimensions)doc"; + +static const char* + __doc_operations_research_RoutingModel_dimension_resource_group_indices = + R"doc(Stores the set of resource groups related to each dimension.)doc"; + +static const char* __doc_operations_research_RoutingModel_dimensions = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_disjunctions = + R"doc(Disjunctions)doc"; + +static const char* + __doc_operations_research_RoutingModel_enable_deep_serialization = + R"doc(Returns the value of the internal enable_deep_serialization_ +parameter.)doc"; + +static const char* + __doc_operations_research_RoutingModel_enable_deep_serialization_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_extra_filters = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_extra_intervals = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_extra_operators = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_extra_vars = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_finalizer_variables = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_first_solution_decision_builders = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_first_solution_evaluator = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_first_solution_evaluator_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_first_solution_filtered_decision_builders = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_first_solution_lns_limit = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_fixed_cost_of_vehicle = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_global_dimension_optimizers = + R"doc(TODO(user): Define a new Dimension[Global|Local]OptimizerIndex +type and use it to define ITIVectors and for the dimension to +optimizer index mappings below.)doc"; + +static const char* + __doc_operations_research_RoutingModel_global_optimizer_index = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_has_hard_type_incompatibilities = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_has_same_vehicle_type_requirements = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_has_temporal_type_incompatibilities = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_has_temporal_type_requirements = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_has_vehicle_with_zero_cost_class = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_implicit_pickup_delivery_pairs_without_alternatives = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_improve_db = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_index_neighbor_finder = + R"doc(Returns the index neighbor finder to be used by routing heuristics.)doc"; + +static const char* + __doc_operations_research_RoutingModel_index_neighbor_finder_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_index_to_delivery_positions = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_index_to_disjunctions = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_index_to_equivalence_class = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_index_to_pickup_positions = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_index_to_type_policy = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_index_to_visit_type = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_interrupt_cp = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_interrupt_cp_sat = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_is_bound_to_end = + R"doc(is_bound_to_end_[i] will be true iff the path starting at var #i is +fully bound and reaches the end of a route, i.e. either: - IsEnd(i) is +true - or nexts_[i] is bound and is_bound_to_end_[nexts_[i].Value()] +is true.)doc"; + +static const char* + __doc_operations_research_RoutingModel_is_bound_to_end_ct_added = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_limit = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_linear_cost_factor_of_vehicle = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_lns_limit = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_local_dimension_optimizers = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_local_optimizer_index = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_local_optimum_reached = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_local_search_filter_managers = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_local_search_operators = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_ls_limit = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_manager = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_max_active_vehicles = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_metaheuristic = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_monitors = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_monitors_after_setup = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_monitors_before_setup = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_nexts = + R"doc(Decision variables: indexed by int64_t var index.)doc"; + +static const char* __doc_operations_research_RoutingModel_no_cycle_constraint = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_node_neighbors_by_cost_class_per_size = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_nodes = + R"doc(Sizes and indices Returns the number of nodes in the model.)doc"; + +static const char* __doc_operations_research_RoutingModel_nodes_2 = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_num_vehicle_classes = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_num_visit_types = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_objective_lower_bound = + R"doc(Returns the current lower bound found by internal solvers during the +search.)doc"; + +static const char* + __doc_operations_research_RoutingModel_objective_lower_bound_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_operator_assign = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_optimized_dimensions_assignment_collector = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_pair_indices_of_type = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_paths_metadata = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_pickup_delivery_disjunctions = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_pickup_delivery_pairs = + R"doc(Pickup and delivery)doc"; + +static const char* __doc_operations_research_RoutingModel_preassignment = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_primary_constrained_dimension = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_primary_ls_operator = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_quadratic_cost_factor_of_vehicle = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_resource_groups = + R"doc(Resource Groups. If resource_groups_ is not empty, then for each group +of resources, each (used) vehicle must be assigned to exactly 1 +resource, and each resource can in turn be assigned to at most 1 +vehicle.)doc"; + +static const char* __doc_operations_research_RoutingModel_resource_vars = + R"doc(Resource variables, indexed first by resource group index and then by +vehicle index. A resource variable can have a negative value of -1, +iff the corresponding vehicle doesn't require a resource from this +resource group, OR if the vehicle is unused (i.e. no visits on its +route and vehicle_used_when_empty_[v] is false).)doc"; + +static const char* __doc_operations_research_RoutingModel_restore_assignment = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_restore_tmp_assignment = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_same_vehicle_costs = + R"doc(Same vehicle costs)doc"; + +static const char* __doc_operations_research_RoutingModel_same_vehicle_group = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_same_vehicle_groups = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_search_log = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_search_parameters = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_secondary_ls_db = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_secondary_ls_monitors = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_secondary_ls_operator = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_secondary_model = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_secondary_optimizer = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_secondary_parameters = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_single_nodes_of_type = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_solve_db = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_solver = + R"doc(Returns the underlying constraint solver. Can be used to add extra +constraints and/or modify search algorithms.)doc"; + +static const char* __doc_operations_research_RoutingModel_solver_2 = + R"doc(Model)doc"; + +static const char* __doc_operations_research_RoutingModel_start_end_count = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_state_dependent_transit_evaluators = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_state_dependent_transit_evaluators_cache = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_status = + R"doc(Returns the current status of the routing model.)doc"; + +static const char* __doc_operations_research_RoutingModel_status_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_sweep_arranger = + R"doc(Returns the sweep arranger to be used by routing heuristics.)doc"; + +static const char* __doc_operations_research_RoutingModel_sweep_arranger_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_tabu_var_callback = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_time_buffer = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_tmp_assignment = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_topologically_sorted_visit_types = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_transit_evaluator_sign = R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_transit_evaluators = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_unary_transit_evaluators = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_vehicle_active = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_vehicle_amortized_cost_factors_set = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_vehicle_class_index_of_vehicle = + R"doc(Index by source index.)doc"; + +static const char* + __doc_operations_research_RoutingModel_vehicle_pickup_delivery_policy = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_vehicle_route_considered = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_vehicle_start_class_callback = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_vehicle_to_transit_cost = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_vehicle_type_container = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingModel_vehicle_used_when_empty = + R"doc(vehicle_used_when_empty_[vehicle] determines if "vehicle" should be +taken into account for costs (arc costs, span costs, etc.) and +constraints (eg. resources) even when the route of the vehicle is +empty (i.e. goes straight from its start to its end). + +NOTE1: A vehicle's fixed cost is added iff the vehicle serves nodes on +its route, regardless of this variable's value. + +NOTE2: The default value for this boolean is 'false' for all vehicles, +i.e. by default empty routes will not contribute to the cost nor be +considered for constraints.)doc"; + +static const char* __doc_operations_research_RoutingModel_vehicle_vars = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingModel_vehicles = + R"doc(Returns the number of vehicle routes in the model.)doc"; + +static const char* __doc_operations_research_RoutingModel_vehicles_2 = + R"doc()doc"; + +static const char* __doc_operations_research_SimpleBoundCosts = R"doc()doc"; + +static const char* __doc_operations_research_SimpleBoundCosts_SimpleBoundCosts = + R"doc()doc"; + +static const char* + __doc_operations_research_SimpleBoundCosts_SimpleBoundCosts_2 = R"doc()doc"; + +static const char* __doc_operations_research_SimpleBoundCosts_Size = + R"doc()doc"; + +static const char* __doc_operations_research_SimpleBoundCosts_bound_cost = + R"doc()doc"; + +static const char* __doc_operations_research_SimpleBoundCosts_bound_cost_2 = + R"doc()doc"; + +static const char* __doc_operations_research_SimpleBoundCosts_bound_costs = + R"doc()doc"; + +static const char* __doc_operations_research_SimpleBoundCosts_operator_assign = + R"doc()doc"; + +static const char* __doc_operations_research_SolveModelWithSat = + R"doc(Attempts to solve the model using the cp-sat solver. As of 5/2019, +will solve the TSP corresponding to the model if it has a single +vehicle. Therefore the resulting solution might not actually be +feasible. Will return false if a solution could not be found.)doc"; + +static const char* __doc_operations_research_SweepArranger = R"doc()doc"; + +static const char* __doc_operations_research_TravelBounds = R"doc()doc"; + +static const char* __doc_operations_research_TravelBounds_max_travels = + R"doc()doc"; + +static const char* __doc_operations_research_TravelBounds_min_travels = + R"doc()doc"; + +static const char* __doc_operations_research_TravelBounds_post_travels = + R"doc()doc"; + +static const char* __doc_operations_research_TravelBounds_pre_travels = + R"doc()doc"; + +static const char* __doc_operations_research_TypeIncompatibilityChecker = + R"doc(Checker for type incompatibilities.)doc"; + +static const char* + __doc_operations_research_TypeIncompatibilityChecker_CheckTypeRegulations = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeIncompatibilityChecker_HasRegulationsToCheck = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeIncompatibilityChecker_TypeIncompatibilityChecker = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeIncompatibilityChecker_check_hard_incompatibilities = + R"doc(NOTE(user): As temporal incompatibilities are always verified with +this checker, we only store 1 boolean indicating whether or not hard +incompatibilities are also verified.)doc"; + +static const char* __doc_operations_research_TypeRegulationsChecker = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_CheckTypeRegulations = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_CheckVehicle = R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_FinalizeCheck = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_HasRegulationsToCheck = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_InitializeCheck = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_OnInitializeCheck = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_TypeCurrentlyOnRoute = + R"doc(Returns true iff there's at least one instance of the given type on +the route when scanning the route at the given position 'pos'. This is +the case iff we have at least one added but non-removed instance of +the type, or if +occurrences_of_type_[type].last_type_on_vehicle_up_to_visit is greater +than 'pos'.)doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_TypeOccursOnRoute = + R"doc(Returns true iff any occurrence of the given type was seen on the +route, i.e. iff the added count for this type is positive, or if a +node of this type and policy TYPE_ON_VEHICLE_UP_TO_VISIT is visited on +the route (see TypePolicyOccurrence.last_type_on_vehicle_up_to_visit).)doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_TypePolicyOccurrence = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_TypePolicyOccurrence_num_type_added_to_vehicle = + R"doc(Number of TYPE_ADDED_TO_VEHICLE and +TYPE_SIMULTANEOUSLY_ADDED_AND_REMOVED node type policies seen on the +route.)doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_TypePolicyOccurrence_num_type_removed_from_vehicle = + R"doc(Number of ADDED_TYPE_REMOVED_FROM_VEHICLE (effectively removing a type +from the route) and TYPE_SIMULTANEOUSLY_ADDED_AND_REMOVED node type +policies seen on the route. This number is always <= +num_type_added_to_vehicle, as a type is only actually removed if it +was on the route before.)doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_TypePolicyOccurrence_position_of_last_type_on_vehicle_up_to_visit = + R"doc(Position of the last node of policy TYPE_ON_VEHICLE_UP_TO_VISIT +visited on the route. If positive, the type is considered on the +vehicle from the start of the route until this position.)doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_TypeRegulationsChecker = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_current_route_visits = + R"doc()doc"; + +static const char* __doc_operations_research_TypeRegulationsChecker_model = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsChecker_occurrences_of_type = + R"doc()doc"; + +static const char* __doc_operations_research_TypeRegulationsConstraint = + R"doc(The following constraint ensures that incompatibilities and +requirements between types are respected. + +It verifies both "hard" and "temporal" incompatibilities. Two nodes +with hard incompatible types cannot be served by the same vehicle at +all, while with a temporal incompatibility they can't be on the same +route at the same time. The VisitTypePolicy of a node determines how +visiting it impacts the type count on the route. + +For example, for - three temporally incompatible types T1 T2 and T3 - +2 pairs of nodes a1/r1 and a2/r2 of type T1 and T2 respectively, with +- a1 and a2 of VisitTypePolicy TYPE_ADDED_TO_VEHICLE - r1 and r2 of +policy ADDED_TYPE_REMOVED_FROM_VEHICLE - 3 nodes A, UV and AR of type +T3, respectively with type policies TYPE_ADDED_TO_VEHICLE, +TYPE_ON_VEHICLE_UP_TO_VISIT and TYPE_SIMULTANEOUSLY_ADDED_AND_REMOVED +the configurations UV --> a1 --> r1 --> a2 --> r2, a1 --> r1 --> a2 +--> r2 --> A and a1 --> r1 --> AR --> a2 --> r2 are acceptable, +whereas the configurations a1 --> a2 --> r1 --> ..., or A --> a1 --> +r1 --> ..., or a1 --> r1 --> UV --> ... are not feasible. + +It also verifies same-vehicle and temporal type requirements. A node +of type T_d with a same-vehicle requirement for type T_r needs to be +served by the same vehicle as a node of type T_r. Temporal +requirements, on the other hand, can take effect either when the +dependent type is being added to the route or when it's removed from +it, which is determined by the dependent node's VisitTypePolicy. In +the above example: - If T3 is required on the same vehicle as T1, A, +AR or UV must be on the same vehicle as a1. - If T2 is required when +adding T1, a2 must be visited *before* a1, and if r2 is also visited +on the route, it must be *after* a1, i.e. T2 must be on the vehicle +when a1 is visited: ... --> a2 --> ... --> a1 --> ... --> r2 --> ... - +If T3 is required when removing T1, T3 needs to be on the vehicle when +r1 is visited: ... --> A --> ... --> r1 --> ... OR ... --> r1 --> ... +--> UV --> ...)doc"; + +static const char* + __doc_operations_research_TypeRegulationsConstraint_CheckRegulationsOnVehicle = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsConstraint_InitialPropagate = + R"doc()doc"; + +static const char* __doc_operations_research_TypeRegulationsConstraint_Post = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsConstraint_PropagateNodeRegulations = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsConstraint_TypeRegulationsConstraint = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsConstraint_incompatibility_checker = + R"doc()doc"; + +static const char* __doc_operations_research_TypeRegulationsConstraint_model = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsConstraint_requirement_checker = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRegulationsConstraint_vehicle_demons = + R"doc()doc"; + +static const char* __doc_operations_research_TypeRequirementChecker = + R"doc(Checker for type requirements.)doc"; + +static const char* + __doc_operations_research_TypeRequirementChecker_CheckRequiredTypesCurrentlyOnRoute = + R"doc(Verifies that for each set in required_type_alternatives, at least one +of the required types is on the route at position 'pos'.)doc"; + +static const char* + __doc_operations_research_TypeRequirementChecker_CheckTypeRegulations = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRequirementChecker_FinalizeCheck = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRequirementChecker_HasRegulationsToCheck = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRequirementChecker_OnInitializeCheck = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRequirementChecker_TypeRequirementChecker = + R"doc()doc"; + +static const char* + __doc_operations_research_TypeRequirementChecker_types_with_same_vehicle_requirements_on_route = + R"doc()doc"; + +#if defined(__GNUG__) +#pragma GCC diagnostic pop +#endif diff --git a/ortools/constraint_solver/python/routing_index_manager.i b/ortools/routing/python/index_manager.i similarity index 76% rename from ortools/constraint_solver/python/routing_index_manager.i rename to ortools/routing/python/index_manager.i index aaa0d4a6a13..02dc547d8bf 100644 --- a/ortools/constraint_solver/python/routing_index_manager.i +++ b/ortools/routing/python/index_manager.i @@ -14,21 +14,21 @@ // Wrapper for RoutingIndexManager. %include "ortools/base/base.i" -%include "ortools/constraint_solver/python/routing_types.i" +%include "ortools/routing/python/types.i" %import "ortools/util/python/vector.i" %{ -#include "ortools/constraint_solver/routing_index_manager.h" +#include "ortools/routing/index_manager.h" %} -DEFINE_INDEX_TYPE_TYPEDEF(operations_research::RoutingNodeIndex, - operations_research::RoutingIndexManager::NodeIndex); +DEFINE_INDEX_TYPE_TYPEDEF(operations_research::routing::RoutingNodeIndex, + operations_research::routing::RoutingIndexManager::NodeIndex); %ignoreall -%unignore operations_research; +%unignore operations_research::routing; -namespace operations_research { +namespace operations_research::routing { %unignore RoutingIndexManager; %unignore RoutingIndexManager::GetStartIndex; @@ -47,8 +47,8 @@ namespace operations_research { %rename (GetNumberOfIndices) RoutingIndexManager::num_indices; %unignore RoutingIndexManager::~RoutingIndexManager; -} // namespace operations_research +} // namespace operations_research::routing -%include "ortools/constraint_solver/routing_index_manager.h" +%include "ortools/routing/index_manager.h" %unignoreall diff --git a/ortools/routing/python/index_manager_doc.h b/ortools/routing/python/index_manager_doc.h new file mode 100644 index 00000000000..0ca647262dd --- /dev/null +++ b/ortools/routing/python/index_manager_doc.h @@ -0,0 +1,146 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + This file contains docstrings for use in the Python bindings. + Do not edit! They were automatically extracted by pybind11_mkdoc. + */ + +#define __EXPAND(x) x +#define __COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT +#define __VA_SIZE(...) __EXPAND(__COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1)) +#define __CAT1(a, b) a##b +#define __CAT2(a, b) __CAT1(a, b) +#define __DOC1(n1) __doc_##n1 +#define __DOC2(n1, n2) __doc_##n1##_##n2 +#define __DOC3(n1, n2, n3) __doc_##n1##_##n2##_##n3 +#define __DOC4(n1, n2, n3, n4) __doc_##n1##_##n2##_##n3##_##n4 +#define __DOC5(n1, n2, n3, n4, n5) __doc_##n1##_##n2##_##n3##_##n4##_##n5 +#define __DOC6(n1, n2, n3, n4, n5, n6) \ + __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6 +#define __DOC7(n1, n2, n3, n4, n5, n6, n7) \ + __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 +#define DOC(...) \ + __EXPAND(__EXPAND(__CAT2(__DOC, __VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) + +#if defined(__GNUG__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#endif + +static const char* __doc_operations_research_RoutingIndexManager = + R"doc(Manager for any NodeIndex <-> variable index conversion. The routing +solver uses variable indices internally and through its API. These +variable indices are tricky to manage directly because one Node can +correspond to a multitude of variables, depending on the number of +times they appear in the model, and if they're used as start and/or +end points. This class aims to simplify variable index usage, allowing +users to use NodeIndex instead. + +Usage: + +``` +{.cpp} + auto starts_ends = ...; /// These are NodeIndex. + RoutingIndexManager manager(10, 4, starts_ends); // 10 nodes, 4 vehicles. + RoutingModel model(manager); +``` + +Then, use 'manager.NodeToIndex(node)' whenever model requires a +variable index. + +Note: the mapping between node indices and variables indices is +subject to change so no assumption should be made on it. The only +guarantee is that indices range between 0 and n-1, where n = number of +vehicles * 2 (for start and end nodes) + number of non-start or end +nodes.)doc"; + +static const char* __doc_operations_research_RoutingIndexManager_GetEndIndex = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_GetIndexToNodeMap = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_GetStartIndex = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_IndexToNode = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_IndicesToNodes = R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_Initialize = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_NodeToIndex = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_NodesToIndices = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_RoutingIndexManager = + R"doc(Creates a NodeIndex to variable index mapping for a problem containing +'num_nodes', 'num_vehicles' and the given starts and ends for each +vehicle. If used, any start/end arrays have to have exactly +'num_vehicles' elements.)doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_RoutingIndexManager_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_RoutingIndexManager_3 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_index_to_node = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_node_to_index = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_num_indices = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_num_nodes = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_num_nodes_2 = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_num_unique_depots = + R"doc(complete.)doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_num_unique_depots_2 = + R"doc()doc"; + +static const char* __doc_operations_research_RoutingIndexManager_num_vehicles = + R"doc()doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_num_vehicles_2 = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_vehicle_to_end = R"doc()doc"; + +static const char* + __doc_operations_research_RoutingIndexManager_vehicle_to_start = + R"doc()doc"; + +#if defined(__GNUG__) +#pragma GCC diagnostic pop +#endif diff --git a/ortools/routing/python/model.cc b/ortools/routing/python/model.cc new file mode 100644 index 00000000000..424356b3356 --- /dev/null +++ b/ortools/routing/python/model.cc @@ -0,0 +1,236 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include + +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/constraint_solver/solver_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/python/doc.h" +#include "ortools/routing/python/index_manager_doc.h" +#include "ortools/routing/python/parameters_doc.h" +#include "ortools/routing/routing.h" +#include "pybind11/cast.h" +#include "pybind11/functional.h" +#include "pybind11/gil.h" +#include "pybind11/pybind11.h" +#include "pybind11/stl.h" +#include "pybind11_protobuf/native_proto_caster.h" + +using ::operations_research::Assignment; +using ::operations_research::routing::DefaultRoutingModelParameters; +using ::operations_research::routing::DefaultRoutingSearchParameters; +using ::operations_research::routing::RoutingDimension; +using ::operations_research::routing::RoutingIndexManager; +using ::operations_research::routing::RoutingModel; +using ::operations_research::routing::RoutingModelParameters; +using ::operations_research::routing::RoutingSearchParameters; +using ::pybind11::arg; + +PYBIND11_MODULE(model, m) { + pybind11_protobuf::ImportNativeProtoCasters(); + + pybind11::module::import( + "ortools.constraint_solver.python.constraint_solver"); + + m.def("default_routing_model_parameters", &DefaultRoutingModelParameters, + DOC(operations_research, DefaultRoutingModelParameters)); + + m.def("default_routing_search_parameters", &DefaultRoutingSearchParameters, + DOC(operations_research, DefaultRoutingSearchParameters)); + + pybind11::class_( + m, "RoutingIndexManager", DOC(operations_research, RoutingIndexManager)) + .def(pybind11::init([](int num_nodes, int num_vehicles, int depot) { + return new RoutingIndexManager( + num_nodes, num_vehicles, + RoutingIndexManager::NodeIndex(depot)); + }), + DOC(operations_research, RoutingIndexManager, RoutingIndexManager)) + .def(pybind11::init([](int num_nodes, int num_vehicles, + const std::vector starts, + const std::vector ends) { + std::vector start_nodes; + start_nodes.reserve(starts.size()); + std::transform(starts.cbegin(), starts.cend(), + std::back_inserter(start_nodes), [](int node) { + return RoutingIndexManager::NodeIndex(node); + }); + + std::vector end_nodes; + end_nodes.reserve(ends.size()); + std::transform( + ends.cbegin(), ends.cend(), std::back_inserter(end_nodes), + [](int node) { return RoutingIndexManager::NodeIndex(node); }); + + return new RoutingIndexManager(num_nodes, num_vehicles, + start_nodes, end_nodes); + }), + DOC(operations_research, RoutingIndexManager, RoutingIndexManager)) + .def("num_nodes", &RoutingIndexManager::num_nodes, + DOC(operations_research, RoutingIndexManager, num_nodes)) + .def("num_vehicles", &RoutingIndexManager::num_vehicles, + DOC(operations_research, RoutingIndexManager, num_vehicles)) + .def("num_indices", &RoutingIndexManager::num_indices, + DOC(operations_research, RoutingIndexManager, num_indices)) + .def( + "index_to_node", + [](RoutingIndexManager* routing_manager, int64_t index) { + return routing_manager->IndexToNode(index).value(); + }, + DOC(operations_research, RoutingIndexManager, IndexToNode)) + .def( + "node_to_index", + [](RoutingIndexManager* routing_manager, int node) { + return routing_manager->NodeToIndex( + RoutingIndexManager::NodeIndex(node)); + }, + DOC(operations_research, RoutingIndexManager, NodeToIndex)) + .def("get_start_index", &RoutingIndexManager::GetStartIndex, + DOC(operations_research, RoutingIndexManager, GetStartIndex)) + .def("get_end_index", &RoutingIndexManager::GetEndIndex, + DOC(operations_research, RoutingIndexManager, GetEndIndex)); + + pybind11::class_(m, "RoutingDimension") + .def("model", &RoutingDimension::model, + pybind11::return_value_policy::reference_internal) + .def("get_transit_value", &RoutingDimension::GetTransitValue, + arg("from_index"), arg("to_index"), arg("vehicle")) + .def("cumul_var", &RoutingDimension::CumulVar, + pybind11::return_value_policy::reference_internal, arg("index")); + + pybind11::class_ rm(m, "RoutingModel"); + rm.def(pybind11::init([](const RoutingIndexManager& routing_index_manager) { + return new RoutingModel(routing_index_manager); + })); + rm.def(pybind11::init([](const RoutingIndexManager& routing_index_manager, + const RoutingModelParameters& parameters) { + return new RoutingModel(routing_index_manager, parameters); + })); + rm.def( + "register_transit_matrix", + [](RoutingModel* routing_model, + std::vector> transit_matrix) { + return routing_model->RegisterTransitMatrix(std::move(transit_matrix)); + }); + rm.def("register_unary_transit_vector", + [](RoutingModel* routing_model, std::vector transit_vector) { + return routing_model->RegisterUnaryTransitVector( + std::move(transit_vector)); + }); + rm.def("register_unary_transit_callback", + [](RoutingModel* routing_model, + std::function transit_callback) { + return routing_model->RegisterUnaryTransitCallback( + std::move(transit_callback)); + }); + rm.def("register_transit_callback", + [](RoutingModel* routing_model, + std::function transit_callback) { + return routing_model->RegisterTransitCallback( + std::move(transit_callback)); + }); + rm.def("set_arc_cost_evaluator_of_all_vehicles", + &RoutingModel::SetArcCostEvaluatorOfAllVehicles, + arg("transit_callback_index")); + rm.def("add_dimension", &RoutingModel::AddDimension, arg("evaluator_index"), + arg("slack_max"), arg("capacity"), arg("fix_start_cumul_to_zero"), + arg("name")); + rm.def("add_dimension_with_vehicle_capacity", + &RoutingModel::AddDimensionWithVehicleCapacity, arg("evaluator_index"), + arg("slack_max"), arg("vehicle_capacities"), + arg("fix_start_cumul_to_zero"), arg("name")); + rm.def("add_dimension_with_vehicle_transits", + &RoutingModel::AddDimensionWithVehicleTransits, + arg("evaluator_indices"), arg("slack_max"), arg("capacity"), + arg("fix_start_cumul_to_zero"), arg("name")); + rm.def("add_dimension_with_vehicle_transit_and_capacity", + &RoutingModel::AddDimensionWithVehicleTransitAndCapacity, + arg("evaluator_indices"), arg("slack_max"), arg("vehicle_capacities"), + arg("fix_start_cumul_to_zero"), arg("name")); + rm.def("add_constant_dimension", &RoutingModel::AddConstantDimension, + arg("value"), arg("capacity"), arg("fix_start_cumul_to_zero"), + arg("name")); + rm.def("add_vector_dimension", &RoutingModel::AddVectorDimension, + arg("values"), arg("capacity"), arg("fix_start_cumul_to_zero"), + arg("name")); + rm.def("add_matrix_dimension", &RoutingModel::AddMatrixDimension, + arg("values"), arg("capacity"), arg("fix_start_cumul_to_zero"), + arg("name")); + rm.def("get_dimension_or_die", &RoutingModel::GetDimensionOrDie, + pybind11::return_value_policy::reference_internal, + arg("dimension_name")); + rm.def("close_model", &RoutingModel::CloseModel); + rm.def("close_model_with_parameters", &RoutingModel::CloseModelWithParameters, + arg("search_parameters")); + rm.def("solve", &RoutingModel::Solve, + pybind11::return_value_policy::reference_internal, + arg("assignment") = nullptr); + // TODO(user) Add support for solutions parameters too. + rm.def( + "solve_with_parameters", + [](RoutingModel* routing_model, + const RoutingSearchParameters& search_parameters + /*,std::vector* solutions = nullptr*/) + -> const Assignment* { + return routing_model->SolveWithParameters(search_parameters, nullptr); + }, + pybind11::return_value_policy::reference_internal, + arg("search_parameters") + //, arg("solutions") = nullptr + ); + rm.def("status", &RoutingModel::status); + rm.def("nodes", &RoutingModel::nodes); + rm.def("vehicles", &RoutingModel::vehicles); + rm.def("size", &RoutingModel::Size); + rm.def("start", &RoutingModel::Start, arg("vehicle")); + rm.def("end", &RoutingModel::End, arg("vehicle")); + rm.def("is_start", &RoutingModel::IsStart, arg("index")); + rm.def("is_end", &RoutingModel::IsEnd, arg("index")); + rm.def("next", &RoutingModel::Next, arg("assignment"), arg("index")); + rm.def("next_var", &RoutingModel::NextVar, + pybind11::return_value_policy::reference_internal, arg("index")); + rm.def("get_arc_cost_for_vehicle", &RoutingModel::GetArcCostForVehicle, + arg("from_index"), arg("to_index"), arg("vehicle")); + rm.def("solver", &RoutingModel::solver, + pybind11::return_value_policy::reference_internal); + + pybind11::enum_(rm, "PenaltyCostBehavior") + .value("PENALIZE_ONCE", RoutingModel::PenaltyCostBehavior::PENALIZE_ONCE) + .value("PENALIZE_PER_INACTIVE", + RoutingModel::PenaltyCostBehavior::PENALIZE_PER_INACTIVE) + .export_values(); + + rm.def( + "add_disjunction", + [](RoutingModel* routing_model, const std::vector& indices, + int64_t penalty, int64_t max_cardinality, + RoutingModel::PenaltyCostBehavior penalty_cost_behavior) -> int { + return static_cast(routing_model + ->AddDisjunction(indices, penalty, + max_cardinality, + penalty_cost_behavior) + .value()); + }, + // &RoutingModel::AddDisjunction, + arg("indices"), arg("penalty") = RoutingModel::kNoPenalty, + arg("max_cardinality") = 1, + arg("penalty_cost_behavior") = + RoutingModel::PenaltyCostBehavior::PENALIZE_ONCE); +} diff --git a/ortools/routing/python/model_test.py b/ortools/routing/python/model_test.py new file mode 100644 index 00000000000..9eac9247886 --- /dev/null +++ b/ortools/routing/python/model_test.py @@ -0,0 +1,730 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test for routing pybind11 layer.""" + +import functools + +from absl.testing import absltest + +from ortools.constraint_solver.python import constraint_solver +from ortools.routing import enums_pb2 +from ortools.routing import parameters_pb2 +from ortools.routing.python import model + +FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy +RoutingSearchStatus = enums_pb2.RoutingSearchStatus +RoutingSearchParameters = parameters_pb2.RoutingSearchParameters + + +def Distance(node_i, node_j): + return node_i + node_j + + +def TransitDistance(manager, i, j): + return Distance(manager.index_to_node(i), manager.index_to_node(j)) + + +def UnaryTransitDistance(manager, i): + return Distance(manager.index_to_node(i), 0) + + +def One(unused_i, unused_j): + return 1 + + +def Two(unused_i, unused_j): + return 1 + + +def Three(unused_i, unused_j): + return 1 + + +class TestRoutingIndexManager(absltest.TestCase): + + def testCtor(self): + manager = model.RoutingIndexManager(42, 3, 7) + self.assertIsNotNone(manager) + print(manager) + self.assertEqual(42, manager.num_nodes()) + self.assertEqual(3, manager.num_vehicles()) + self.assertEqual(42 + 3 * 2 - 1, manager.num_indices()) + for i in range(manager.num_vehicles()): + self.assertEqual(7, manager.index_to_node(manager.get_start_index(i))) + self.assertEqual(7, manager.index_to_node(manager.get_end_index(i))) + + def testCtorMultiDepotSame(self): + manager = model.RoutingIndexManager(42, 3, [0, 0, 0], [0, 0, 0]) + self.assertIsNotNone(manager) + print(manager) + self.assertEqual(42, manager.num_nodes()) + self.assertEqual(3, manager.num_vehicles()) + self.assertEqual(42 + 3 * 2 - 1, manager.num_indices()) + for i in range(manager.num_vehicles()): + self.assertEqual(0, manager.index_to_node(manager.get_start_index(i))) + self.assertEqual(0, manager.index_to_node(manager.get_end_index(i))) + + def testCtorMultiDepotAllDiff(self): + manager = model.RoutingIndexManager(42, 3, [1, 2, 3], [4, 5, 6]) + self.assertIsNotNone(manager) + print(manager) + self.assertEqual(42, manager.num_nodes()) + self.assertEqual(3, manager.num_vehicles()) + self.assertEqual(42, manager.num_indices()) + for i in range(manager.num_vehicles()): + self.assertEqual(i + 1, manager.index_to_node(manager.get_start_index(i))) + self.assertEqual(i + 4, manager.index_to_node(manager.get_end_index(i))) + + +class ModelTest(absltest.TestCase): + + def testCtor(self): + manager = model.RoutingIndexManager(42, 3, 7) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + print(routing_model) + for i in range(manager.num_vehicles()): + self.assertEqual(7, manager.index_to_node(routing_model.start(i))) + self.assertEqual(7, manager.index_to_node(routing_model.end(i))) + + def testSolve(self): + manager = model.RoutingIndexManager(42, 3, 7) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve() + self.assertEqual( + RoutingSearchStatus.ROUTING_OPTIMAL, routing_model.status() + ) + self.assertIsNotNone(assignment) + self.assertEqual(0, assignment.objective_value()) + + def testSolveMultiDepot(self): + manager = model.RoutingIndexManager(42, 3, [1, 2, 3], [4, 5, 6]) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve() + self.assertEqual( + RoutingSearchStatus.ROUTING_OPTIMAL, routing_model.status() + ) + self.assertIsNotNone(assignment) + self.assertEqual(0, assignment.objective_value()) + + def testTransitCallback(self): + manager = model.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + self.assertEqual(1, transit_idx) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve() + self.assertTrue(assignment) + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertEqual(20, assignment.objective_value()) + + def testTransitLambda(self): + manager = model.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + transit_id = routing_model.register_transit_callback( + lambda from_index, to_index: 1 + ) + self.assertEqual(1, transit_id) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_id) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve() + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertIsNotNone(assignment) + self.assertEqual(5, assignment.objective_value()) + + def testTransitMatrix(self): + manager = model.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + matrix = [[i + 1 for i in range(5)] for _ in range(5)] + transit_idx = routing_model.register_transit_matrix(matrix) + self.assertEqual(1, transit_idx) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve() + self.assertTrue(assignment) + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertEqual(15, assignment.objective_value()) + + def testUnaryTransitCallback(self): + manager = model.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + transit_idx = routing_model.register_unary_transit_callback( + functools.partial(UnaryTransitDistance, manager) + ) + self.assertEqual(1, transit_idx) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve() + self.assertTrue(assignment) + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertEqual(10, assignment.objective_value()) + + def testUnaryTransitLambda(self): + manager = model.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + transit_id = routing_model.register_unary_transit_callback( + lambda from_index: 1 + ) + self.assertEqual(1, transit_id) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_id) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve() + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertIsNotNone(assignment) + self.assertEqual(5, assignment.objective_value()) + + def testUnaryTransitVector(self): + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + vector = list(range(10)) + transit_idx = routing_model.register_unary_transit_vector(vector) + self.assertEqual(1, transit_idx) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve() + self.assertTrue(assignment) + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertEqual(45, assignment.objective_value()) + + def testTSP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertEqual(90, assignment.objective_value()) + # Inspect solution + index = routing_model.start(0) + visited_nodes = [] + expected_visited_nodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] + while not routing_model.is_end(index): + index = assignment.value(routing_model.next_var(index)) + visited_nodes.append(manager.index_to_node(index)) + self.assertEqual(expected_visited_nodes, visited_nodes) + + def testVRP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 2, [0, 1], [1, 0]) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertEqual(89, assignment.objective_value()) + # Inspect solution + index = routing_model.start(1) + visited_nodes = [] + expected_visited_nodes = [2, 4, 6, 8, 3, 5, 7, 9, 0] + while not routing_model.is_end(index): + index = assignment.value(routing_model.next_var(index)) + visited_nodes.append(manager.index_to_node(index)) + self.assertEqual(expected_visited_nodes, visited_nodes) + self.assertTrue( + routing_model.is_end( + assignment.value(routing_model.next_var(routing_model.start(0))) + ) + ) + + def testDimensionTSP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Add generic dimension + routing_model.add_dimension(transit_idx, 90, 90, True, "distance") + distance_dimension = routing_model.get_dimension_or_die("distance") + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertEqual(90, assignment.objective_value()) + # Inspect solution + node = routing_model.start(0) + cumul = 0 + while not routing_model.is_end(node): + self.assertEqual( + cumul, assignment.value(distance_dimension.cumul_var(node)) + ) + next_node = assignment.value(routing_model.next_var(node)) + cumul += Distance(node, next_node) + node = next_node + + def testDimensionWithVehicleCapacitiesTSP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Add generic dimension + routing_model.add_dimension_with_vehicle_capacity( + transit_idx, 90, [90], True, "distance" + ) + distance_dimension = routing_model.get_dimension_or_die("distance") + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertEqual(90, assignment.objective_value()) + # Inspect solution + node = routing_model.start(0) + cumul = 0 + while not routing_model.is_end(node): + self.assertEqual( + cumul, assignment.value(distance_dimension.cumul_var(node)) + ) + next_node = assignment.value(routing_model.next_var(node)) + cumul += Distance(node, next_node) + node = next_node + + def testDimensionWithVehicleTransitsTSP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Add generic dimension + routing_model.add_dimension_with_vehicle_transits( + [transit_idx], 90, 90, True, "distance" + ) + distance_dimension = routing_model.get_dimension_or_die("distance") + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertEqual(90, assignment.objective_value()) + # Inspect solution + node = routing_model.start(0) + cumul = 0 + while not routing_model.is_end(node): + self.assertEqual( + cumul, assignment.value(distance_dimension.cumul_var(node)) + ) + next_node = assignment.value(routing_model.next_var(node)) + cumul += Distance(node, next_node) + node = next_node + + def testDimensionWithVehicleTransitsVRP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 3, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Add generic dimension + distances = [ + routing_model.register_transit_callback(One), + routing_model.register_transit_callback(Two), + routing_model.register_transit_callback(Three), + ] + routing_model.add_dimension_with_vehicle_transits( + distances, 90, 90, True, "distance" + ) + distance_dimension = routing_model.get_dimension_or_die("distance") + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertEqual(90, assignment.objective_value()) + # Inspect solution + for vehicle in range(0, routing_model.vehicles()): + node = routing_model.start(vehicle) + cumul = 0 + while not routing_model.is_end(node): + self.assertEqual( + cumul, assignment.min(distance_dimension.cumul_var(node)) + ) + next_node = assignment.value(routing_model.next_var(node)) + # Increment cumul by the vehicle distance which is equal to the vehicle + # index + 1, cf. distances. + cumul += vehicle + 1 + node = next_node + + def testConstantDimensionTSP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 3, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Add constant dimension + constant_id, success = routing_model.add_constant_dimension( + 1, 100, True, "count" + ) + self.assertTrue(success) + self.assertEqual(transit_idx + 1, constant_id) + count_dimension = routing_model.get_dimension_or_die("count") + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertEqual(90, assignment.objective_value()) + # Inspect solution + node = routing_model.start(0) + count = 0 + while not routing_model.is_end(node): + self.assertEqual(count, assignment.value(count_dimension.cumul_var(node))) + count += 1 + node = assignment.value(routing_model.next_var(node)) + self.assertEqual(10, count) + + def testVectorDimensionTSP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Add vector dimension + values = list(range(10)) + unary_transit_id, success = routing_model.add_vector_dimension( + values, 100, True, "vector" + ) + self.assertTrue(success) + self.assertEqual(transit_idx + 1, unary_transit_id) + vector_dimension = routing_model.get_dimension_or_die("vector") + # Solve + search_parameters: RoutingSearchParameters = ( + model.default_routing_search_parameters() + ) + self.assertIsNotNone(search_parameters) + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertIsNotNone(assignment) + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertEqual(90, assignment.objective_value()) + # Inspect solution + node = routing_model.start(0) + cumul = 0 + while not routing_model.is_end(node): + self.assertEqual( + cumul, assignment.value(vector_dimension.cumul_var(node)) + ) + cumul += values[node] + node = assignment.value(routing_model.next_var(node)) + + def testMatrixDimensionTSP(self): + # Create routing model + manager = model.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + cost = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(cost) + # Add matrix dimension + values = [[j for _ in range(5)] for j in range(5)] + transit_id, success = routing_model.add_matrix_dimension( + values, 100, True, "matrix" + ) + self.assertTrue(success) + self.assertEqual(cost + 1, transit_id) + dimension = routing_model.get_dimension_or_die("matrix") + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertIsNotNone(assignment) + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertEqual(20, assignment.objective_value()) + # Inspect solution + index = routing_model.start(0) + cumul = 0 + while not routing_model.is_end(index): + self.assertEqual(cumul, assignment.value(dimension.cumul_var(index))) + cumul += values[manager.index_to_node(index)][ + manager.index_to_node(index) + ] + index = assignment.value(routing_model.next_var(index)) + + def testMatrixDimensionVRP(self): + manager = model.RoutingIndexManager(5, 2, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + matrix = [[i + j for i in range(5)] for j in range(5)] + transit_idx = routing_model.register_transit_matrix(matrix) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Add matrix dimension + matrix_transit_idx, success = routing_model.add_matrix_dimension( + matrix, 10, True, "matrix" # capacity # fix_start_cumul_to_zero + ) + self.assertTrue(success) + self.assertEqual(transit_idx + 1, matrix_transit_idx) + dimension = routing_model.get_dimension_or_die("matrix") + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + self.assertEqual( + RoutingSearchStatus.ROUTING_NOT_SOLVED, routing_model.status() + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertIsNotNone(assignment) + self.assertEqual( + RoutingSearchStatus.ROUTING_SUCCESS, routing_model.status() + ) + self.assertEqual(20, assignment.objective_value()) + # Inspect solution + for v in range(manager.num_vehicles()): + index = routing_model.start(v) + cumul = 0 + while not routing_model.is_end(index): + self.assertEqual(cumul, assignment.value(dimension.cumul_var(index))) + prev_index = index + index = assignment.value(routing_model.next_var(index)) + cumul += matrix[manager.index_to_node(prev_index)][ + manager.index_to_node(index) + ] + + def testDisjunctionTSP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Add disjunctions + disjunctions = [ + [manager.node_to_index(1), manager.node_to_index(2)], + [manager.node_to_index(3)], + [manager.node_to_index(4)], + [manager.node_to_index(5)], + [manager.node_to_index(6)], + [manager.node_to_index(7)], + [manager.node_to_index(8)], + [manager.node_to_index(9)], + ] + for disjunction in disjunctions: + routing_model.add_disjunction(disjunction) + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertEqual(86, assignment.objective_value()) + # Inspect solution + node = routing_model.start(0) + count = 0 + while not routing_model.is_end(node): + count += 1 + node = assignment.value(routing_model.next_var(node)) + self.assertEqual(9, count) + + def testDisjunctionPenaltyTSP(self): + # Create routing model + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager) + self.assertIsNotNone(routing_model) + # Add cost function + transit_idx = routing_model.register_transit_callback( + functools.partial(TransitDistance, manager) + ) + routing_model.set_arc_cost_evaluator_of_all_vehicles(transit_idx) + # Add disjunctions + disjunctions = [ + ([manager.node_to_index(1), manager.node_to_index(2)], 1000), + ([manager.node_to_index(3)], 1000), + ([manager.node_to_index(4)], 1000), + ([manager.node_to_index(5)], 1000), + ([manager.node_to_index(6)], 1000), + ([manager.node_to_index(7)], 1000), + ([manager.node_to_index(8)], 1000), + ([manager.node_to_index(9)], 0), + ] + for disjunction, penalty in disjunctions: + routing_model.add_disjunction(disjunction, penalty) + # Solve + search_parameters = model.default_routing_search_parameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = routing_model.solve_with_parameters(search_parameters) + self.assertEqual(68, assignment.objective_value()) + # Inspect solution + node = routing_model.start(0) + count = 0 + while not routing_model.is_end(node): + count += 1 + node = assignment.value(routing_model.next_var(node)) + self.assertEqual(8, count) + + def testRoutingModelParameters(self): + # Create routing model with parameters + parameters = model.default_routing_model_parameters() + parameters.solver_parameters.CopyFrom( + constraint_solver.Solver.default_solver_parameters() + ) + parameters.solver_parameters.trace_propagation = True + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager, parameters) + self.assertIsNotNone(routing_model) + self.assertEqual(1, routing_model.vehicles()) + self.assertTrue(routing_model.solver().parameters().trace_propagation) + + def testRoutingLocalSearchFiltering(self): + parameters = model.default_routing_model_parameters() + parameters.solver_parameters.profile_local_search = True + manager = model.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + routing_model = model.RoutingModel(manager, parameters) + self.assertIsNotNone(routing_model) + routing_model.solve() + profile = routing_model.solver().local_search_profile() + print(profile) + self.assertIsInstance(profile, str) + self.assertTrue(profile) # Verify it's not empty. + + +if __name__ == "__main__": + absltest.main() diff --git a/ortools/routing/python/parameters_doc.h b/ortools/routing/python/parameters_doc.h new file mode 100644 index 00000000000..77ebd6dec67 --- /dev/null +++ b/ortools/routing/python/parameters_doc.h @@ -0,0 +1,65 @@ +// Copyright 2010-2025 Google LLC +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* + This file contains docstrings for use in the Python bindings. + Do not edit! They were automatically extracted by pybind11_mkdoc. + */ + +#define __EXPAND(x) x +#define __COUNT(_1, _2, _3, _4, _5, _6, _7, COUNT, ...) COUNT +#define __VA_SIZE(...) __EXPAND(__COUNT(__VA_ARGS__, 7, 6, 5, 4, 3, 2, 1)) +#define __CAT1(a, b) a##b +#define __CAT2(a, b) __CAT1(a, b) +#define __DOC1(n1) __doc_##n1 +#define __DOC2(n1, n2) __doc_##n1##_##n2 +#define __DOC3(n1, n2, n3) __doc_##n1##_##n2##_##n3 +#define __DOC4(n1, n2, n3, n4) __doc_##n1##_##n2##_##n3##_##n4 +#define __DOC5(n1, n2, n3, n4, n5) __doc_##n1##_##n2##_##n3##_##n4##_##n5 +#define __DOC6(n1, n2, n3, n4, n5, n6) \ + __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6 +#define __DOC7(n1, n2, n3, n4, n5, n6, n7) \ + __doc_##n1##_##n2##_##n3##_##n4##_##n5##_##n6##_##n7 +#define DOC(...) \ + __EXPAND(__EXPAND(__CAT2(__DOC, __VA_SIZE(__VA_ARGS__)))(__VA_ARGS__)) + +#if defined(__GNUG__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-variable" +#endif + +static const char* __doc_operations_research_DefaultRoutingModelParameters = + R"doc()doc"; + +static const char* __doc_operations_research_DefaultRoutingSearchParameters = + R"doc()doc"; + +static const char* + __doc_operations_research_DefaultSecondaryRoutingSearchParameters = + R"doc()doc"; + +static const char* + __doc_operations_research_FindErrorInRoutingSearchParameters = + R"doc(Returns an empty std::string if the routing search parameters are +valid, and a non-empty, human readable error description if they're +not.)doc"; + +static const char* + __doc_operations_research_FindErrorsInRoutingSearchParameters = + R"doc(Returns a list of std::string describing the errors in the routing +search parameters. Returns an empty vector if the parameters are +valid.)doc"; + +#if defined(__GNUG__) +#pragma GCC diagnostic pop +#endif diff --git a/ortools/routing/python/pywraprouting_test.py b/ortools/routing/python/pywraprouting_test.py new file mode 100755 index 00000000000..f2c0939d407 --- /dev/null +++ b/ortools/routing/python/pywraprouting_test.py @@ -0,0 +1,941 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Test Routing API.""" + +import functools + +from absl.testing import absltest +from ortools.constraint_solver import pywrapcp +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + + +def Distance(node_i, node_j): + return node_i + node_j + + +def TransitDistance(manager, i, j): + return Distance(manager.IndexToNode(i), manager.IndexToNode(j)) + + +def UnaryTransitDistance(manager, i): + return Distance(manager.IndexToNode(i), 0) + + +def One(unused_i, unused_j): + return 1 + + +def Two(unused_i, unused_j): + return 1 + + +def Three(unused_i, unused_j): + return 1 + + +class Callback: + + def __init__(self, model): + self.model = model + self.costs = [] + + def __call__(self): + self.costs.append(self.model.CostVar().Max()) + + +class TestPyWrapRoutingIndexManager(absltest.TestCase): + + def testCtor(self): + manager = pywraprouting.RoutingIndexManager(42, 3, 7) + self.assertIsNotNone(manager) + self.assertEqual(42, manager.GetNumberOfNodes()) + self.assertEqual(3, manager.GetNumberOfVehicles()) + self.assertEqual(42 + 3 * 2 - 1, manager.GetNumberOfIndices()) + for i in range(manager.GetNumberOfVehicles()): + self.assertEqual(7, manager.IndexToNode(manager.GetStartIndex(i))) + self.assertEqual(7, manager.IndexToNode(manager.GetEndIndex(i))) + + def testCtorMultiDepotSame(self): + manager = pywraprouting.RoutingIndexManager(42, 3, [0, 0, 0], [0, 0, 0]) + self.assertIsNotNone(manager) + self.assertEqual(42, manager.GetNumberOfNodes()) + self.assertEqual(3, manager.GetNumberOfVehicles()) + self.assertEqual(42 + 3 * 2 - 1, manager.GetNumberOfIndices()) + for i in range(manager.GetNumberOfVehicles()): + self.assertEqual(0, manager.IndexToNode(manager.GetStartIndex(i))) + self.assertEqual(0, manager.IndexToNode(manager.GetEndIndex(i))) + + def testCtorMultiDepotAllDiff(self): + manager = pywraprouting.RoutingIndexManager(42, 3, [1, 2, 3], [4, 5, 6]) + self.assertIsNotNone(manager) + self.assertEqual(42, manager.GetNumberOfNodes()) + self.assertEqual(3, manager.GetNumberOfVehicles()) + self.assertEqual(42, manager.GetNumberOfIndices()) + for i in range(manager.GetNumberOfVehicles()): + self.assertEqual(i + 1, manager.IndexToNode(manager.GetStartIndex(i))) + self.assertEqual(i + 4, manager.IndexToNode(manager.GetEndIndex(i))) + + +class TestPyWrapRoutingModel(absltest.TestCase): + + def testCtor(self): + manager = pywraprouting.RoutingIndexManager(42, 3, 7) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + for i in range(manager.GetNumberOfVehicles()): + self.assertEqual(7, manager.IndexToNode(model.Start(i))) + self.assertEqual(7, manager.IndexToNode(model.End(i))) + + def testSolve(self): + manager = pywraprouting.RoutingIndexManager(42, 3, 7) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.Solve() + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_OPTIMAL, model.status() + ) + self.assertIsNotNone(assignment) + self.assertEqual(0, assignment.ObjectiveValue()) + + def testSolveMultiDepot(self): + manager = pywraprouting.RoutingIndexManager(42, 3, [1, 2, 3], [4, 5, 6]) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.Solve() + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_OPTIMAL, model.status() + ) + self.assertIsNotNone(assignment) + self.assertEqual(0, assignment.ObjectiveValue()) + + def testTransitCallback(self): + manager = pywraprouting.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + self.assertEqual(1, transit_idx) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.Solve() + self.assertTrue(assignment) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertEqual(20, assignment.ObjectiveValue()) + + def testTransitLambda(self): + manager = pywraprouting.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + transit_id = model.RegisterTransitCallback(lambda from_index, to_index: 1) + self.assertEqual(1, transit_id) + model.SetArcCostEvaluatorOfAllVehicles(transit_id) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.Solve() + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertIsNotNone(assignment) + self.assertEqual(5, assignment.ObjectiveValue()) + + def testTransitMatrix(self): + manager = pywraprouting.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + matrix = [[i + 1 for i in range(5)] for _ in range(5)] + transit_idx = model.RegisterTransitMatrix(matrix) + self.assertEqual(1, transit_idx) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.Solve() + self.assertTrue(assignment) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertEqual(15, assignment.ObjectiveValue()) + + def testUnaryTransitCallback(self): + manager = pywraprouting.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + transit_idx = model.RegisterUnaryTransitCallback( + functools.partial(UnaryTransitDistance, manager) + ) + self.assertEqual(1, transit_idx) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.Solve() + self.assertTrue(assignment) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertEqual(10, assignment.ObjectiveValue()) + + def testUnaryTransitLambda(self): + manager = pywraprouting.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + transit_id = model.RegisterUnaryTransitCallback(lambda from_index: 1) + self.assertEqual(1, transit_id) + model.SetArcCostEvaluatorOfAllVehicles(transit_id) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.Solve() + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertIsNotNone(assignment) + self.assertEqual(5, assignment.ObjectiveValue()) + + def testUnaryTransitVector(self): + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + vector = list(range(10)) + transit_idx = model.RegisterUnaryTransitVector(vector) + self.assertEqual(1, transit_idx) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.Solve() + self.assertTrue(assignment) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertEqual(45, assignment.ObjectiveValue()) + + def testTSP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertEqual(90, assignment.ObjectiveValue()) + # Inspect solution + index = model.Start(0) + visited_nodes = [] + expected_visited_nodes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] + while not model.IsEnd(index): + index = assignment.Value(model.NextVar(index)) + visited_nodes.append(manager.IndexToNode(index)) + self.assertEqual(expected_visited_nodes, visited_nodes) + + def testVRP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 2, [0, 1], [1, 0]) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual(89, assignment.ObjectiveValue()) + # Inspect solution + index = model.Start(1) + visited_nodes = [] + expected_visited_nodes = [2, 4, 6, 8, 3, 5, 7, 9, 0] + while not model.IsEnd(index): + index = assignment.Value(model.NextVar(index)) + visited_nodes.append(manager.IndexToNode(index)) + self.assertEqual(expected_visited_nodes, visited_nodes) + self.assertTrue( + model.IsEnd(assignment.Value(model.NextVar(model.Start(0)))) + ) + + def testDimensionTSP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Add generic dimension + model.AddDimension(transit_idx, 90, 90, True, "distance") + distance_dimension = model.GetDimensionOrDie("distance") + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual(90, assignment.ObjectiveValue()) + # Inspect solution + node = model.Start(0) + cumul = 0 + while not model.IsEnd(node): + self.assertEqual( + cumul, assignment.Value(distance_dimension.CumulVar(node)) + ) + next_node = assignment.Value(model.NextVar(node)) + cumul += Distance(node, next_node) + node = next_node + + def testDimensionWithVehicleCapacitiesTSP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Add generic dimension + model.AddDimensionWithVehicleCapacity( + transit_idx, 90, [90], True, "distance" + ) + distance_dimension = model.GetDimensionOrDie("distance") + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual(90, assignment.ObjectiveValue()) + # Inspect solution + node = model.Start(0) + cumul = 0 + while not model.IsEnd(node): + self.assertEqual( + cumul, assignment.Value(distance_dimension.CumulVar(node)) + ) + next_node = assignment.Value(model.NextVar(node)) + cumul += Distance(node, next_node) + node = next_node + + def testDimensionWithVehicleTransitsTSP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Add generic dimension + model.AddDimensionWithVehicleTransits( + [transit_idx], 90, 90, True, "distance" + ) + distance_dimension = model.GetDimensionOrDie("distance") + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual(90, assignment.ObjectiveValue()) + # Inspect solution + node = model.Start(0) + cumul = 0 + while not model.IsEnd(node): + self.assertEqual( + cumul, assignment.Value(distance_dimension.CumulVar(node)) + ) + next_node = assignment.Value(model.NextVar(node)) + cumul += Distance(node, next_node) + node = next_node + + def testDimensionWithVehicleTransitsVRP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 3, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Add generic dimension + distances = [ + model.RegisterTransitCallback(One), + model.RegisterTransitCallback(Two), + model.RegisterTransitCallback(Three), + ] + model.AddDimensionWithVehicleTransits(distances, 90, 90, True, "distance") + distance_dimension = model.GetDimensionOrDie("distance") + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual(90, assignment.ObjectiveValue()) + # Inspect solution + for vehicle in range(0, model.vehicles()): + node = model.Start(vehicle) + cumul = 0 + while not model.IsEnd(node): + self.assertEqual( + cumul, assignment.Min(distance_dimension.CumulVar(node)) + ) + next_node = assignment.Value(model.NextVar(node)) + # Increment cumul by the vehicle distance which is equal to the vehicle + # index + 1, cf. distances. + cumul += vehicle + 1 + node = next_node + + def testConstantDimensionTSP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 3, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Add constant dimension + constant_id, success = model.AddConstantDimension(1, 100, True, "count") + self.assertTrue(success) + self.assertEqual(transit_idx + 1, constant_id) + count_dimension = model.GetDimensionOrDie("count") + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual(90, assignment.ObjectiveValue()) + # Inspect solution + node = model.Start(0) + count = 0 + while not model.IsEnd(node): + self.assertEqual(count, assignment.Value(count_dimension.CumulVar(node))) + count += 1 + node = assignment.Value(model.NextVar(node)) + self.assertEqual(10, count) + + def testVectorDimensionTSP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Add vector dimension + values = list(range(10)) + unary_transit_id, success = model.AddVectorDimension( + values, 100, True, "vector" + ) + self.assertTrue(success) + self.assertEqual(transit_idx + 1, unary_transit_id) + vector_dimension = model.GetDimensionOrDie("vector") + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertIsNotNone(assignment) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertEqual(90, assignment.ObjectiveValue()) + # Inspect solution + node = model.Start(0) + cumul = 0 + while not model.IsEnd(node): + self.assertEqual(cumul, assignment.Value(vector_dimension.CumulVar(node))) + cumul += values[node] + node = assignment.Value(model.NextVar(node)) + + def testMatrixDimensionTSP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(5, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + cost = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(cost) + # Add matrix dimension + values = [[j for _ in range(5)] for j in range(5)] + transit_id, success = model.AddMatrixDimension(values, 100, True, "matrix") + self.assertTrue(success) + self.assertEqual(cost + 1, transit_id) + dimension = model.GetDimensionOrDie("matrix") + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertIsNotNone(assignment) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertEqual(20, assignment.ObjectiveValue()) + # Inspect solution + index = model.Start(0) + cumul = 0 + while not model.IsEnd(index): + self.assertEqual(cumul, assignment.Value(dimension.CumulVar(index))) + cumul += values[manager.IndexToNode(index)][manager.IndexToNode(index)] + index = assignment.Value(model.NextVar(index)) + + def testMatrixDimensionVRP(self): + manager = pywraprouting.RoutingIndexManager(5, 2, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + matrix = [[i + j for i in range(5)] for j in range(5)] + transit_idx = model.RegisterTransitMatrix(matrix) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Add matrix dimension + matrix_transit_idx, success = model.AddMatrixDimension( + matrix, 10, True, "matrix" # capacity # fix_start_cumul_to_zero + ) + self.assertTrue(success) + self.assertEqual(transit_idx + 1, matrix_transit_idx) + dimension = model.GetDimensionOrDie("matrix") + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_NOT_SOLVED, model.status() + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertIsNotNone(assignment) + self.assertEqual( + enums_pb2.RoutingSearchStatus.ROUTING_SUCCESS, model.status() + ) + self.assertEqual(20, assignment.ObjectiveValue()) + # Inspect solution + for v in range(manager.GetNumberOfVehicles()): + index = model.Start(v) + cumul = 0 + while not model.IsEnd(index): + self.assertEqual(cumul, assignment.Value(dimension.CumulVar(index))) + prev_index = index + index = assignment.Value(model.NextVar(index)) + cumul += matrix[manager.IndexToNode(prev_index)][ + manager.IndexToNode(index) + ] + + def testDisjunctionTSP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Add disjunctions + disjunctions = [ + [manager.NodeToIndex(1), manager.NodeToIndex(2)], + [manager.NodeToIndex(3)], + [manager.NodeToIndex(4)], + [manager.NodeToIndex(5)], + [manager.NodeToIndex(6)], + [manager.NodeToIndex(7)], + [manager.NodeToIndex(8)], + [manager.NodeToIndex(9)], + ] + for disjunction in disjunctions: + model.AddDisjunction(disjunction) + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual(86, assignment.ObjectiveValue()) + # Inspect solution + node = model.Start(0) + count = 0 + while not model.IsEnd(node): + count += 1 + node = assignment.Value(model.NextVar(node)) + self.assertEqual(9, count) + + def testDisjunctionPenaltyTSP(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Add disjunctions + disjunctions = [ + ([manager.NodeToIndex(1), manager.NodeToIndex(2)], 1000), + ([manager.NodeToIndex(3)], 1000), + ([manager.NodeToIndex(4)], 1000), + ([manager.NodeToIndex(5)], 1000), + ([manager.NodeToIndex(6)], 1000), + ([manager.NodeToIndex(7)], 1000), + ([manager.NodeToIndex(8)], 1000), + ([manager.NodeToIndex(9)], 0), + ] + for disjunction, penalty in disjunctions: + model.AddDisjunction(disjunction, penalty) + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.FIRST_UNBOUND_MIN_VALUE + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual(68, assignment.ObjectiveValue()) + # Inspect solution + node = model.Start(0) + count = 0 + while not model.IsEnd(node): + count += 1 + node = assignment.Value(model.NextVar(node)) + self.assertEqual(8, count) + + def testRoutingModelParameters(self): + # Create routing model with parameters + parameters = pywraprouting.DefaultRoutingModelParameters() + parameters.solver_parameters.CopyFrom( + pywrapcp.Solver.DefaultSolverParameters() + ) + parameters.solver_parameters.trace_propagation = True + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager, parameters) + self.assertIsNotNone(model) + self.assertEqual(1, model.vehicles()) + self.assertTrue(model.solver().Parameters().trace_propagation) + + def testRoutingLocalSearchFiltering(self): + parameters = pywraprouting.DefaultRoutingModelParameters() + parameters.solver_parameters.profile_local_search = True + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager, parameters) + self.assertIsNotNone(model) + model.Solve() + profile = model.solver().LocalSearchProfile() + print(profile) + self.assertIsInstance(profile, str) + self.assertTrue(profile) # Verify it's not empty. + + def testRoutingSearchParameters(self): + # Create routing model + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Close with parameters + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.SAVINGS + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.local_search_operators.use_two_opt = ( + pywraprouting.BOOL_FALSE + ) + search_parameters.solution_limit = 20 + model.CloseModelWithParameters(search_parameters) + # Solve with parameters + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual( + 11, model.GetNumberOfDecisionsInFirstSolution(search_parameters) + ) + self.assertEqual( + 0, model.GetNumberOfRejectsInFirstSolution(search_parameters) + ) + self.assertEqual(90, assignment.ObjectiveValue()) + assignment = model.SolveFromAssignmentWithParameters( + assignment, search_parameters + ) + self.assertEqual(90, assignment.ObjectiveValue()) + + def testFindErrorInRoutingSearchParameters(self): + params = pywraprouting.DefaultRoutingSearchParameters() + params.local_search_operators.use_cross = pywraprouting.BOOL_UNSPECIFIED + self.assertIn( + "cross", pywraprouting.FindErrorInRoutingSearchParameters(params) + ) + + def testCallback(self): + manager = pywraprouting.RoutingIndexManager(10, 1, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + callback = Callback(model) + model.AddAtSolutionCallback(callback) + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + assignment = model.SolveWithParameters(search_parameters) + self.assertEqual(90, assignment.ObjectiveValue()) + self.assertEqual(len(callback.costs), 1) + self.assertEqual(90, callback.costs[0]) + + def testReadAssignment(self): + manager = pywraprouting.RoutingIndexManager(10, 2, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # TODO(user): porting this segfaults the tests. + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + routes = [ + [ + manager.NodeToIndex(1), + manager.NodeToIndex(3), + manager.NodeToIndex(5), + manager.NodeToIndex(4), + manager.NodeToIndex(2), + manager.NodeToIndex(6), + ], + [ + manager.NodeToIndex(7), + manager.NodeToIndex(9), + manager.NodeToIndex(8), + ], + ] + assignment = model.ReadAssignmentFromRoutes(routes, False) + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.solution_limit = 1 + solution = model.SolveFromAssignmentWithParameters( + assignment, search_parameters + ) + self.assertEqual(90, solution.ObjectiveValue()) + for vehicle in range(0, model.vehicles()): + node = model.Start(vehicle) + count = 0 + while not model.IsEnd(node): + node = solution.Value(model.NextVar(node)) + if not model.IsEnd(node): + self.assertEqual(routes[vehicle][count], manager.IndexToNode(node)) + count += 1 + + def testAutomaticFirstSolutionStrategy_simple(self): + manager = pywraprouting.RoutingIndexManager(31, 7, 3) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + self.assertIsNotNone(model.SolveWithParameters(search_parameters)) + self.assertEqual( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC, + model.GetAutomaticFirstSolutionStrategy(), + ) + + def testAutomaticFirstSolutionStrategy_pd(self): + manager = pywraprouting.RoutingIndexManager(31, 7, 0) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + # Add cost function + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + model.SetArcCostEvaluatorOfAllVehicles(transit_idx) + self.assertTrue(model.AddDimension(transit_idx, 0, 1000, True, "distance")) + dst_dimension = model.GetDimensionOrDie("distance") + # Add few Pickup and Delivery + for request in [[2 * i, 2 * i + 1] for i in range(1, 15)]: + pickup_index = manager.NodeToIndex(request[0]) + delivery_index = manager.NodeToIndex(request[1]) + model.AddPickupAndDelivery(pickup_index, delivery_index) + model.solver().Add( + model.VehicleVar(pickup_index) == model.VehicleVar(delivery_index) + ) + model.solver().Add( + dst_dimension.CumulVar(pickup_index) + <= dst_dimension.CumulVar(delivery_index) + ) + # Solve + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + self.assertIsNotNone(model.SolveWithParameters(search_parameters)) + self.assertEqual( + enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION, + model.GetAutomaticFirstSolutionStrategy(), + ) + + +class TestBoundCost(absltest.TestCase): + + def testCtor(self): + bound_cost = pywraprouting.BoundCost() + self.assertIsNotNone(bound_cost) + self.assertEqual(0, bound_cost.bound) + self.assertEqual(0, bound_cost.cost) + + bound_cost = pywraprouting.BoundCost(97, 43) + self.assertIsNotNone(bound_cost) + self.assertEqual(97, bound_cost.bound) + self.assertEqual(43, bound_cost.cost) + + +class TestRoutingDimension(absltest.TestCase): + + def testCtor(self): + manager = pywraprouting.RoutingIndexManager(31, 7, 3) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + self.assertTrue(model.AddDimension(transit_idx, 90, 90, True, "distance")) + model.GetDimensionOrDie("distance") + + def testSoftSpanUpperBound(self): + manager = pywraprouting.RoutingIndexManager(31, 7, 3) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + self.assertTrue(model.AddDimension(transit_idx, 100, 100, True, "distance")) + dimension = model.GetDimensionOrDie("distance") + + bound_cost = pywraprouting.BoundCost(97, 43) + self.assertIsNotNone(bound_cost) + self.assertFalse(dimension.HasSoftSpanUpperBounds()) + for v in range(manager.GetNumberOfVehicles()): + dimension.SetSoftSpanUpperBoundForVehicle(bound_cost, v) + bc = dimension.GetSoftSpanUpperBoundForVehicle(v) + self.assertIsNotNone(bc) + self.assertEqual(97, bc.bound) + self.assertEqual(43, bc.cost) + self.assertTrue(dimension.HasSoftSpanUpperBounds()) + + def testQuadraticCostSoftSpanUpperBound(self): + manager = pywraprouting.RoutingIndexManager(31, 7, 3) + self.assertIsNotNone(manager) + model = pywraprouting.RoutingModel(manager) + self.assertIsNotNone(model) + transit_idx = model.RegisterTransitCallback( + functools.partial(TransitDistance, manager) + ) + self.assertTrue(model.AddDimension(transit_idx, 100, 100, True, "distance")) + dimension = model.GetDimensionOrDie("distance") + + bound_cost = pywraprouting.BoundCost(97, 43) + self.assertIsNotNone(bound_cost) + self.assertFalse(dimension.HasQuadraticCostSoftSpanUpperBounds()) + for v in range(manager.GetNumberOfVehicles()): + dimension.SetQuadraticCostSoftSpanUpperBoundForVehicle(bound_cost, v) + bc = dimension.GetQuadraticCostSoftSpanUpperBoundForVehicle(v) + self.assertIsNotNone(bc) + self.assertEqual(97, bc.bound) + self.assertEqual(43, bc.cost) + self.assertTrue(dimension.HasQuadraticCostSoftSpanUpperBounds()) + + +# TODO(user): Add tests for Routing[Cost|Vehicle|Resource]ClassIndex + +if __name__ == "__main__": + absltest.main() diff --git a/ortools/constraint_solver/python/routing.i b/ortools/routing/python/routing.i similarity index 50% rename from ortools/constraint_solver/python/routing.i rename to ortools/routing/python/routing.i index ff7bb9006a8..4b49f36ab9f 100644 --- a/ortools/constraint_solver/python/routing.i +++ b/ortools/routing/python/routing.i @@ -24,62 +24,73 @@ %include "ortools/util/python/pair.i" %include "ortools/util/python/vector.i" -%include "ortools/constraint_solver/python/constraint_solver.i" -%include "ortools/constraint_solver/python/routing_types.i" -%include "ortools/constraint_solver/python/routing_index_manager.i" +// While the module name will be overridden by the one specified on the cmd line, +// without this, derived classes (e.g. TypeRequirementChecker) will import base +// class from the module specified in the following %import. +%module pywraprouting +%import(module="ortools.constraint_solver.pywrapcp") "ortools/constraint_solver/python/constraint_solver.i" +%include "ortools/routing/python/types.i" +%include "ortools/routing/python/index_manager.i" // We need to forward-declare the proto here, so that PROTO_INPUT involving it // works correctly. The order matters very much: this declaration needs to be // before the %{ #include ".../routing.h" %}. -namespace operations_research { +namespace operations_research::routing { class RoutingModelParameters; class RoutingSearchParameters; class RoutingSearchStatus; -} // namespace operations_research +} // namespace operations_research::routing // Include the files we want to wrap a first time. %{ -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_types.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/types.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" #include "ortools/util/optional_boolean.pb.h" %} DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingCostClassIndex, - operations_research::RoutingModel::CostClassIndex); + operations_research::routing::RoutingCostClassIndex, + operations_research::routing::RoutingModel::CostClassIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingDimensionIndex, - operations_research::RoutingModel::DimensionIndex); + operations_research::routing::RoutingDimensionIndex, + operations_research::routing::RoutingModel::DimensionIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingDisjunctionIndex, - operations_research::RoutingModel::DisjunctionIndex); + operations_research::routing::RoutingDisjunctionIndex, + operations_research::routing::RoutingModel::DisjunctionIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingVehicleClassIndex, - operations_research::RoutingModel::VehicleClassIndex); + operations_research::routing::RoutingVehicleClassIndex, + operations_research::routing::RoutingModel::VehicleClassIndex); DEFINE_INDEX_TYPE_TYPEDEF( - operations_research::RoutingResourceClassIndex, - operations_research::RoutingModel::ResourceClassIndex); + operations_research::routing::RoutingResourceClassIndex, + operations_research::routing::RoutingModel::ResourceClassIndex); // ============= Type conversions ============== -%ignore operations_research::RoutingModel::RegisterStateDependentTransitCallback; -%ignore operations_research::RoutingModel::StateDependentTransitCallback; -%ignore operations_research::RoutingModel::MakeStateDependentTransit; -%ignore operations_research::RoutingModel::AddDimensionDependentDimensionWithVehicleCapacity; -%ignore operations_research::RoutingModel::AddResourceGroup; -%ignore operations_research::RoutingModel::GetResourceGroups; - -PY_PROTO_TYPEMAP(ortools.constraint_solver.routing_parameters_pb2, +// See ./constraint_solver_helpers.i. +PY_CONVERT_HELPER_INTEXPR_AND_INTVAR(); +PY_CONVERT_HELPER_PTR(IntervalVar); +PY_CONVERT_HELPER_PTR(LocalSearchFilter); +PY_CONVERT_HELPER_PTR(LocalSearchOperator); +PY_CONVERT_HELPER_PTR(SearchMonitor); + +%ignore operations_research::routing::RoutingModel::RegisterStateDependentTransitCallback; +%ignore operations_research::routing::RoutingModel::StateDependentTransitCallback; +%ignore operations_research::routing::RoutingModel::MakeStateDependentTransit; +%ignore operations_research::routing::RoutingModel::AddDimensionDependentDimensionWithVehicleCapacity; +%ignore operations_research::routing::RoutingModel::AddResourceGroup; +%ignore operations_research::routing::RoutingModel::GetResourceGroups; + +PY_PROTO_TYPEMAP(ortools.routing.parameters_pb2, RoutingModelParameters, - operations_research::RoutingModelParameters) -PY_PROTO_TYPEMAP(ortools.constraint_solver.routing_parameters_pb2, + operations_research::routing::RoutingModelParameters) +PY_PROTO_TYPEMAP(ortools.routing.parameters_pb2, RoutingSearchParameters, - operations_research::RoutingSearchParameters) + operations_research::routing::RoutingSearchParameters) -// Wrap routing_types.h, routing_parameters.h according to the SWIG style guide. +// Wrap types.h, parameters.h according to the SWIG style guide. %ignoreall %unignore RoutingTransitCallback1; %unignore RoutingTransitCallback2; @@ -90,8 +101,8 @@ PY_PROTO_TYPEMAP(ortools.constraint_solver.routing_parameters_pb2, %unignore DefaultRoutingModelParameters; %unignore FindErrorInRoutingSearchParameters; -%include "ortools/constraint_solver/routing_types.h" -%include "ortools/constraint_solver/routing_parameters.h" +%include "ortools/routing/types.h" +%include "ortools/routing/parameters.h" %unignoreall %unignore operations_research; @@ -104,6 +115,10 @@ enum OptionalBoolean { BOOL_FALSE = 2, BOOL_TRUE = 3, }; +} // namespace operations_research + +%unignore operations_research::routing; +namespace operations_research::routing { struct FirstSolutionStrategy { enum Value {}; @@ -124,8 +139,8 @@ struct RoutingSearchStatus { %unignore SimpleBoundCosts::bound_cost; %rename("size") SimpleBoundCosts::Size; -} // namespace operations_research +} // namespace operations_research::routing // TODO(user): Use ignoreall/unignoreall for this one. A lot of work. //swiglint: disable include-h-allglobals -%include "ortools/constraint_solver/routing.h" +%include "ortools/routing/routing.h" diff --git a/ortools/constraint_solver/python/routing_types.i b/ortools/routing/python/types.i similarity index 83% rename from ortools/constraint_solver/python/routing_types.i rename to ortools/routing/python/types.i index 04d6a789e6f..c7949de427c 100644 --- a/ortools/constraint_solver/python/routing_types.i +++ b/ortools/routing/python/types.i @@ -21,7 +21,7 @@ %import "ortools/util/python/vector.i" %{ -#include "ortools/constraint_solver/routing_types.h" +#include "ortools/routing/types.h" template inline PyObject* PyInt_FromIndexT(const IndexT i) { @@ -68,9 +68,9 @@ PY_LIST_LIST_INPUT_TYPEMAP(IndexT, PyInt_Check); %apply const std::vector& { std::vector& }; %enddef // DEFINE_INDEX_TYPE_TYPEDEF -DEFINE_INDEX_TYPE(operations_research::RoutingNodeIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingCostClassIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingDimensionIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingDisjunctionIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingVehicleClassIndex); -DEFINE_INDEX_TYPE(operations_research::RoutingResourceClassIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingNodeIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingCostClassIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingDimensionIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingDisjunctionIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingVehicleClassIndex); +DEFINE_INDEX_TYPE(operations_research::routing::RoutingResourceClassIndex); diff --git a/ortools/constraint_solver/routing.cc b/ortools/routing/routing.cc similarity index 96% rename from ortools/constraint_solver/routing.cc rename to ortools/routing/routing.cc index ebacbcc1280..a7dc30470aa 100644 --- a/ortools/constraint_solver/routing.cc +++ b/ortools/routing/routing.cc @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing.h" +#include "ortools/routing/routing.h" #include @@ -39,6 +39,7 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/flags/flag.h" +#include "absl/functional/any_invocable.h" #include "absl/functional/bind_front.h" #include "absl/hash/hash.h" #include "absl/log/check.h" @@ -51,7 +52,6 @@ #include "absl/time/time.h" #include "absl/types/span.h" #include "google/protobuf/util/message_differencer.h" -#include "ortools/base/int_type.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/mathutil.h" @@ -61,26 +61,25 @@ #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing_constraints.h" -#include "ortools/constraint_solver/routing_decision_builders.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_filters.h" -#include "ortools/constraint_solver/routing_ils.h" -#include "ortools/constraint_solver/routing_ils.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_insertion_lns.h" -#include "ortools/constraint_solver/routing_lp_scheduling.h" -#include "ortools/constraint_solver/routing_neighborhoods.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_parameters_utils.h" -#include "ortools/constraint_solver/routing_search.h" -#include "ortools/constraint_solver/routing_types.h" -#include "ortools/constraint_solver/routing_utils.h" #include "ortools/constraint_solver/solver_parameters.pb.h" #include "ortools/graph/connected_components.h" #include "ortools/graph/graph.h" #include "ortools/graph/linear_assignment.h" +#include "ortools/routing/constraints.h" +#include "ortools/routing/decision_builders.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/filters.h" +#include "ortools/routing/ils.h" +#include "ortools/routing/ils.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/insertion_lns.h" +#include "ortools/routing/lp_scheduling.h" +#include "ortools/routing/neighborhoods.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/search.h" +#include "ortools/routing/types.h" +#include "ortools/routing/utils.h" #include "ortools/util/bitset.h" #include "ortools/util/optional_boolean.pb.h" #include "ortools/util/piecewise_linear_function.h" @@ -98,7 +97,7 @@ using CostValue = int64_t; // Trace settings -namespace operations_research { +namespace operations_research::routing { std::string RoutingModel::RouteDimensionTravelInfo::DebugString( std::string line_prefix) const { @@ -237,8 +236,8 @@ void RoutingModel::NodeNeighborsByCostClass::ComputeNeighbors( node_index_to_outgoing_neighbor_indicator_by_cost_class_.clear(); all_incoming_nodes_.clear(); all_outgoing_nodes_.clear(); - if (num_neighbors == max_num_neighbors && - only_sort_neighbors_for_partial_neighborhoods) { + full_neighborhood_ = num_neighbors == max_num_neighbors; + if (full_neighborhood_ && only_sort_neighbors_for_partial_neighborhoods) { all_incoming_nodes_.reserve(size); all_outgoing_nodes_.reserve(size); for (int node = 0; node < size_with_vehicle_nodes; node++) { @@ -1086,7 +1085,7 @@ bool RoutingModel::HasDimension(absl::string_view dimension_name) const { } RoutingModel::DimensionIndex RoutingModel::GetDimensionIndex( - const std::string& dimension_name) const { + absl::string_view dimension_name) const { return gtl::FindWithDefault(dimension_name_to_index_, dimension_name, kNoDimension); } @@ -1391,7 +1390,7 @@ void RoutingModel::SetAmortizedCostFactorsOfVehicle( } void RoutingModel::AddRouteConstraint( - std::function(const std::vector&)> + absl::AnyInvocable(const std::vector&)> route_evaluator, bool costs_are_homogeneous_across_vehicles) { costs_are_homogeneous_across_vehicles_ &= @@ -1755,6 +1754,52 @@ void RoutingModel::FinalizeVisitTypes() { TopologicallySortVisitTypes(); } +namespace { +template +std::vector> GetTopologicallySortedNodes( + const SparseBitset<>& active_nodes, std::vector node_in_degree, + const std::vector>& children, + const C& comparator) { + std::vector current_nodes_with_zero_indegree; + for (int node : active_nodes.PositionsSetAtLeastOnce()) { + if (node_in_degree[node] == 0) { + current_nodes_with_zero_indegree.push_back(node); + } + } + std::vector> topologically_sorted_nodes; + int num_nodes_added = 0; + while (!current_nodes_with_zero_indegree.empty()) { + // Add all zero-degree nodes to the same topological order group, while + // also marking their dependent nodes that become part of the next group. + topologically_sorted_nodes.push_back({}); + std::vector& topological_group = topologically_sorted_nodes.back(); + std::vector next_nodes_with_zero_indegree; + for (int node : current_nodes_with_zero_indegree) { + topological_group.push_back(node); + num_nodes_added++; + for (int dependent_node : children[node]) { + DCHECK_GT(node_in_degree[dependent_node], 0); + if (--node_in_degree[dependent_node] == 0) { + next_nodes_with_zero_indegree.push_back(dependent_node); + } + } + } + absl::c_sort(topological_group, comparator); + // Swap the current nodes with zero in-degree with the next ones. + current_nodes_with_zero_indegree.swap(next_nodes_with_zero_indegree); + } + + const int num_active_nodes = + active_nodes.NumberOfSetCallsWithDifferentArguments(); + DCHECK_LE(num_nodes_added, num_active_nodes); + if (num_nodes_added < num_active_nodes) { + // Graph is cyclic, no topological order. + topologically_sorted_nodes.clear(); + } + return topologically_sorted_nodes; +} +} // namespace + void RoutingModel::TopologicallySortVisitTypes() { if (!HasSameVehicleTypeRequirements() && !HasTemporalTypeRequirements()) { return; @@ -1795,59 +1840,56 @@ void RoutingModel::TopologicallySortVisitTypes() { } } - // Compute topological order of visit types. - topologically_sorted_visit_types_.clear(); - std::vector current_types_with_zero_indegree; - for (int type : types_in_requirement_graph.PositionsSetAtLeastOnce()) { - DCHECK(type_requirement_tightness[type].first > 0 || - type_requirement_tightness[type].second > 0); - if (in_degree[type] == 0) { - current_types_with_zero_indegree.push_back(type); - } - } + topologically_sorted_visit_types_ = GetTopologicallySortedNodes( + types_in_requirement_graph, std::move(in_degree), type_to_dependent_types, + // Sort the types in the current topological group based on their + // requirement tightness. + // NOTE: For a deterministic order, types with equal tightness are sorted + // by increasing type. + // TODO(user): Put types of the same topological order and same + // requirement tightness in a single group (so that they all get inserted + // simultaneously by the GlobalCheapestInsertion heuristic, for instance). + [&type_requirement_tightness](int type1, int type2) { + const auto& tightness1 = type_requirement_tightness[type1]; + const auto& tightness2 = type_requirement_tightness[type2]; + return tightness1 > tightness2 || + (tightness1 == tightness2 && type1 < type2); + }); +} - int num_types_added = 0; - while (!current_types_with_zero_indegree.empty()) { - // Add all zero-degree nodes to the same topological order group, while - // also marking their dependent types that become part of the next group. - topologically_sorted_visit_types_.push_back({}); - std::vector& topological_group = - topologically_sorted_visit_types_.back(); - std::vector next_types_with_zero_indegree; - for (int type : current_types_with_zero_indegree) { - topological_group.push_back(type); - num_types_added++; - for (int dependent_type : type_to_dependent_types[type]) { - DCHECK_GT(in_degree[dependent_type], 0); - if (--in_degree[dependent_type] == 0) { - next_types_with_zero_indegree.push_back(dependent_type); - } - } - } - // Sort the types in the current topological group based on their - // requirement tightness. - // NOTE: For a deterministic order, types with equal tightness are sorted by - // increasing type. - // TODO(user): Put types of the same topological order and same - // requirement tightness in a single group (so that they all get inserted - // simultaneously by the GlobalCheapestInsertion heuristic, for instance). - std::sort(topological_group.begin(), topological_group.end(), - [&type_requirement_tightness](int type1, int type2) { - const auto& tightness1 = type_requirement_tightness[type1]; - const auto& tightness2 = type_requirement_tightness[type2]; - return tightness1 > tightness2 || - (tightness1 == tightness2 && type1 < type2); - }); - // Swap the current types with zero in-degree with the next ones. - current_types_with_zero_indegree.swap(next_types_with_zero_indegree); - } - - const int num_types_in_requirement_graph = - types_in_requirement_graph.NumberOfSetCallsWithDifferentArguments(); - DCHECK_LE(num_types_added, num_types_in_requirement_graph); - if (num_types_added < num_types_in_requirement_graph) { - // Requirement graph is cyclic, no topological order. - topologically_sorted_visit_types_.clear(); +void RoutingModel::FinalizePrecedences() { + for (const RoutingDimension* dimension : dimensions_) { + if (dimension->GetNodePrecedences().empty()) continue; + std::vector in_degree(Size(), 0); + SparseBitset<> nodes_in_precedences(Size()); + std::vector> successors(Size()); + std::vector node_max_offset(Size(), + std::numeric_limits::min()); + // Note: A precedence constraint between first_node and second_node with an + // offset enforces cumuls(second_node) >= cumuls(first_node) + offset. + for (const auto [first_node, second_node, offset, unused] : + dimension->GetNodePrecedences()) { + in_degree[second_node]++; + nodes_in_precedences.Set(first_node); + nodes_in_precedences.Set(second_node); + successors[first_node].insert(second_node); + node_max_offset[first_node] = + std::max(node_max_offset[first_node], offset); + node_max_offset[second_node] = + std::max(node_max_offset[second_node], offset); + } + topologically_sorted_node_precedences_.push_back( + GetTopologicallySortedNodes( + nodes_in_precedences, std::move(in_degree), successors, + // Sort the nodes in the current topological group based on their + // precedence offset. + // NOTE: For a deterministic order, nodes with equal offset are + // sorted by increasing node. + [&node_max_offset](int node1, int node2) { + const int64_t offset1 = node_max_offset[node1]; + const int64_t offset2 = node_max_offset[node2]; + return offset1 > offset2 || (offset1 == offset2 && node1 < node2); + })); } } @@ -1966,7 +2008,7 @@ void RoutingModel::AddSoftSameVehicleConstraint(std::vector indices, } } -void RoutingModel::SetAllowedVehiclesForIndex(const std::vector& vehicles, +void RoutingModel::SetAllowedVehiclesForIndex(absl::Span vehicles, int64_t index) { DCHECK(!closed_); auto& allowed_vehicles = allowed_vehicles_[index]; @@ -2513,6 +2555,7 @@ void RoutingModel::CloseModelWithParameters( ComputeVehicleTypes(); ComputeResourceClasses(); FinalizeVisitTypes(); + FinalizePrecedences(); vehicle_start_class_callback_ = [this](int64_t start) { return GetVehicleStartClass(start); }; @@ -2937,6 +2980,10 @@ void RoutingModel::CloseModelWithParameters( solver_->AddConstraint(solver_->MakeGreaterOrEqual( active_[first_node], active_[second_node])); break; + case PerformedConstraint::kFirstImpliesSecond: + solver_->AddConstraint(solver_->MakeGreaterOrEqual( + active_[second_node], active_[first_node])); + break; case PerformedConstraint::kFirstAndSecondEqual: solver_->AddConstraint( solver_->MakeEquality(active_[first_node], active_[second_node])); @@ -3554,6 +3601,16 @@ void RoutingModel::SetAssignmentFromOtherModelAssignment( target_assignment->AddObjective(cost_); } +SubSolverStatistics RoutingModel::GetSubSolverStatistics() const { + SubSolverStatistics stats; + stats.set_num_glop_calls_in_lp_scheduling( + search_stats_.num_glop_calls_in_lp_scheduling); + stats.set_num_cp_sat_calls_in_lp_scheduling( + search_stats_.num_cp_sat_calls_in_lp_scheduling); + stats.set_num_min_cost_flow_calls(search_stats_.num_min_cost_flow_calls); + return stats; +} + // Computing a lower bound to the cost of a vehicle routing problem solving a // a linear assignment problem (minimum-cost perfect bipartite matching). // A bipartite graph is created with left nodes representing the nodes of the @@ -4679,7 +4736,7 @@ LocalSearchOperator* RoutingModel::CreateInsertionOperator() { insertion_operator = solver_->ConcatenateOperators( {MakePairActive(solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, - implicit_pickup_delivery_pairs_without_alternatives_), + implicit_pickup_delivery_pairs_without_alternatives_), insertion_operator}); } return insertion_operator; @@ -4723,17 +4780,17 @@ void RoutingModel::CreateNeighborhoodOperators( get_incoming_neighbors = [neighbors_by_cost_class, this]( int64_t node, int64_t start) -> const std::vector& { - DCHECK(!IsStart(node)); - return neighbors_by_cost_class->GetIncomingNeighborsOfNodeForCostClass( - GetCostClassIndexOfVehicle(VehicleIndex(start)).value(), node); - }; + DCHECK(!IsStart(node)); + return neighbors_by_cost_class->GetIncomingNeighborsOfNodeForCostClass( + GetCostClassIndexOfVehicle(VehicleIndex(start)).value(), node); + }; get_outgoing_neighbors = [neighbors_by_cost_class, this]( int64_t node, int64_t start) -> const std::vector& { - DCHECK(!IsEnd(node)); - return neighbors_by_cost_class->GetOutgoingNeighborsOfNodeForCostClass( - GetCostClassIndexOfVehicle(VehicleIndex(start)).value(), node); - }; + DCHECK(!IsEnd(node)); + return neighbors_by_cost_class->GetOutgoingNeighborsOfNodeForCostClass( + GetCostClassIndexOfVehicle(VehicleIndex(start)).value(), node); + }; } local_search_operators_.clear(); @@ -4804,7 +4861,7 @@ void RoutingModel::CreateNeighborhoodOperators( solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); local_search_operators_[SWAP_ACTIVE_CHAIN] = MakeSwapActiveChain( solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, - parameters.max_swap_active_chain_size()); + parameters.max_swap_active_chain_size()); local_search_operators_[EXTENDED_SWAP_ACTIVE] = MakeExtendedSwapActive( solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); std::vector> alternative_sets(disjunctions_.size()); @@ -4838,9 +4895,9 @@ void RoutingModel::CreateNeighborhoodOperators( solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, get_incoming_neighbors, get_outgoing_neighbors, pickup_delivery_pairs_, [this](int64_t start) { - return vehicle_pickup_delivery_policy_[VehicleIndex(start)] == - RoutingModel::PICKUP_AND_DELIVERY_LIFO; - })); + return vehicle_pickup_delivery_policy_[VehicleIndex(start)] == + RoutingModel::PICKUP_AND_DELIVERY_LIFO; + })); light_relocate_pair_operators.push_back(MakeGroupPairAndRelocate( solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, get_incoming_neighbors, get_outgoing_neighbors, pickup_delivery_pairs_)); @@ -4851,14 +4908,14 @@ void RoutingModel::CreateNeighborhoodOperators( vehicle_start_class_callback_, get_incoming_neighbors, get_outgoing_neighbors, pickup_delivery_pairs_), solver_->RevAlloc(new SwapIndexPairOperator(nexts_, get_vehicle_vars(), - pickup_delivery_pairs_))}); + pickup_delivery_pairs_))}); local_search_operators_[EXCHANGE_RELOCATE_PAIR] = MakePairExchangeRelocate( solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, pickup_delivery_pairs_); local_search_operators_[RELOCATE_NEIGHBORS] = MakeRelocateNeighbors( solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, get_incoming_neighbors, get_outgoing_neighbors, - GetLocalSearchHomogeneousArcCostCallback(parameters)); + GetLocalSearchHomogeneousArcCostCallback(parameters)); local_search_operators_[NODE_PAIR_SWAP] = solver_->ConcatenateOperators( {MakeIndexPairSwapActive(solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, @@ -4918,12 +4975,11 @@ void RoutingModel::CreateNeighborhoodOperators( }; const auto make_local_cheapest_insertion_filtered_heuristic = [this, ¶meters]() { + const LocalCheapestInsertionParameters& lci_params = + parameters.local_cheapest_insertion_parameters(); return std::make_unique( this, [this]() { return CheckLimit(time_buffer_); }, - GetLocalSearchArcCostCallback(parameters), - parameters.local_cheapest_insertion_pickup_delivery_strategy(), - GetLocalCheapestInsertionSortingProperties( - parameters.local_cheapest_insertion_sorting_properties()), + GetLocalSearchArcCostCallback(parameters), lci_params, GetOrCreateLocalSearchFilterManager( parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/false}), @@ -5247,6 +5303,12 @@ RoutingModel::CreateLocalSearchFilters( kAccept, priority}); } } + if (!same_vehicle_costs_.empty()) { + if (options.filter_objective) { + filter_events.push_back( + {MakeSameVehicleCostFilter(*this), kAccept, priority}); + } + } // If vehicle costs are not homogeneous, vehicle variables will be added to // local search deltas and their domain will be checked by @@ -5372,15 +5434,6 @@ RoutingModel::CreateLocalSearchFilters( {MakeRouteConstraintFilter(*this), kAccept, priority}); } - { - ++priority; - for (const RoutingDimension* dimension : dimensions_) { - if (!dimension->HasBreakConstraints()) continue; - filter_events.push_back( - {MakeVehicleBreaksFilter(*this, *dimension), kAccept, priority}); - } - } - if (!extra_filters_.empty()) { ++priority; for (const auto& event : extra_filters_) { @@ -5497,9 +5550,11 @@ void RoutingModel::StoreDimensionCumulOptimizers( global_optimizer_index_[dim] = global_dimension_optimizers_.size(); global_dimension_optimizers_.push_back( {std::make_unique( - dimension, parameters.continuous_scheduling_solver()), + dimension, parameters.continuous_scheduling_solver(), + &search_stats_), std::make_unique( - dimension, parameters.mixed_integer_scheduling_solver())}); + dimension, parameters.mixed_integer_scheduling_solver(), + &search_stats_)}); if (!AllTransitsPositive(*dimension)) { dimension->SetOffsetForGlobalOptimizer(0); } else { @@ -5573,9 +5628,11 @@ void RoutingModel::StoreDimensionCumulOptimizers( local_optimizer_index_[dim] = local_dimension_optimizers_.size(); local_dimension_optimizers_.push_back( {std::make_unique( - dimension, parameters.continuous_scheduling_solver()), + dimension, parameters.continuous_scheduling_solver(), + &search_stats_), std::make_unique( - dimension, parameters.mixed_integer_scheduling_solver())}); + dimension, parameters.mixed_integer_scheduling_solver(), + &search_stats_)}); } if (needs_optimizer) { optimized_dimensions_collector_assignment->Add(dimension->cumuls()); @@ -5891,32 +5948,28 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( optimize_on_insertion = absl::bind_front(&SecondaryOptimizer::Solve, secondary_optimizer_.get()); } - const RoutingSearchParameters::PairInsertionStrategy lci_pair_strategy = - search_parameters.local_cheapest_insertion_pickup_delivery_strategy(); - first_solution_filtered_decision_builders_[FirstSolutionStrategy:: - LOCAL_CHEAPEST_INSERTION] = - CreateIntVarFilteredDecisionBuilder< - LocalCheapestInsertionFilteredHeuristic>( - [this](int64_t i, int64_t j, int64_t vehicle) { - return GetArcCostForVehicle(i, j, vehicle); - }, - lci_pair_strategy, - GetLocalCheapestInsertionSortingProperties( - search_parameters.local_cheapest_insertion_sorting_properties()), - GetOrCreateLocalSearchFilterManager( - search_parameters, {/*filter_objective=*/false, - /*filter_with_cp_solver=*/false}), - /*use_first_solution_hint=*/true, bin_capacities_.get(), - optimize_on_insertion); + const LocalCheapestInsertionParameters& lci_params = + search_parameters.local_cheapest_insertion_parameters(); + first_solution_filtered_decision_builders_ + [FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION] = + CreateIntVarFilteredDecisionBuilder< + LocalCheapestInsertionFilteredHeuristic>( + [this](int64_t i, int64_t j, int64_t vehicle) { + return GetArcCostForVehicle(i, j, vehicle); + }, + lci_params, + GetOrCreateLocalSearchFilterManager( + search_parameters, {/*filter_objective=*/false, + /*filter_with_cp_solver=*/false}), + /*use_first_solution_hint=*/true, bin_capacities_.get(), + optimize_on_insertion); IntVarFilteredDecisionBuilder* const strong_lci = CreateIntVarFilteredDecisionBuilder< LocalCheapestInsertionFilteredHeuristic>( [this](int64_t i, int64_t j, int64_t vehicle) { return GetArcCostForVehicle(i, j, vehicle); }, - lci_pair_strategy, - GetLocalCheapestInsertionSortingProperties( - search_parameters.local_cheapest_insertion_sorting_properties()), + lci_params, GetOrCreateLocalSearchFilterManager(search_parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/true}), @@ -5931,17 +5984,13 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( [FirstSolutionStrategy::BEST_INSERTION])); // Local cheapest cost insertion - const RoutingSearchParameters::PairInsertionStrategy lcci_pair_strategy = - search_parameters - .local_cheapest_cost_insertion_pickup_delivery_strategy(); + const LocalCheapestInsertionParameters& lcci_params = + search_parameters.local_cheapest_cost_insertion_parameters(); first_solution_filtered_decision_builders_ [FirstSolutionStrategy::LOCAL_CHEAPEST_COST_INSERTION] = CreateIntVarFilteredDecisionBuilder< LocalCheapestInsertionFilteredHeuristic>( - /*evaluator=*/nullptr, lcci_pair_strategy, - GetLocalCheapestInsertionSortingProperties( - search_parameters - .local_cheapest_insertion_sorting_properties()), + /*evaluator=*/nullptr, lcci_params, GetOrCreateLocalSearchFilterManager( search_parameters, {/*filter_objective=*/true, /*filter_with_cp_solver=*/false}), @@ -5950,9 +5999,7 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( IntVarFilteredDecisionBuilder* const strong_lcci = CreateIntVarFilteredDecisionBuilder< LocalCheapestInsertionFilteredHeuristic>( - /*evaluator=*/nullptr, lcci_pair_strategy, - GetLocalCheapestInsertionSortingProperties( - search_parameters.local_cheapest_insertion_sorting_properties()), + /*evaluator=*/nullptr, lcci_params, GetOrCreateLocalSearchFilterManager(search_parameters, {/*filter_objective=*/true, /*filter_with_cp_solver=*/true}), @@ -5967,15 +6014,6 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( [FirstSolutionStrategy::BEST_INSERTION])); // Savings - SavingsFilteredHeuristic::SavingsParameters savings_parameters; - savings_parameters.neighbors_ratio = - search_parameters.savings_neighbors_ratio(); - savings_parameters.max_memory_usage_bytes = - search_parameters.savings_max_memory_usage_bytes(); - savings_parameters.add_reverse_arcs = - search_parameters.savings_add_reverse_arcs(); - savings_parameters.arc_coefficient = - search_parameters.savings_arc_coefficient(); LocalSearchFilterManager* filter_manager = nullptr; if (!search_parameters.use_unfiltered_first_solution_strategy()) { filter_manager = GetOrCreateLocalSearchFilterManager( @@ -5985,7 +6023,7 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( IntVarFilteredDecisionBuilder* parallel_savings_db = CreateIntVarFilteredDecisionBuilder( - savings_parameters, filter_manager); + search_parameters.savings_parameters(), filter_manager); if (!search_parameters.use_unfiltered_first_solution_strategy()) { first_solution_filtered_decision_builders_ [FirstSolutionStrategy::PARALLEL_SAVINGS] = parallel_savings_db; @@ -5995,14 +6033,14 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( solver_->Try( parallel_savings_db, CreateIntVarFilteredDecisionBuilder( - savings_parameters, + search_parameters.savings_parameters(), GetOrCreateLocalSearchFilterManager( search_parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/true}))); IntVarFilteredDecisionBuilder* sequential_savings_db = CreateIntVarFilteredDecisionBuilder( - savings_parameters, filter_manager); + search_parameters.savings_parameters(), filter_manager); if (!search_parameters.use_unfiltered_first_solution_strategy()) { first_solution_filtered_decision_builders_[FirstSolutionStrategy::SAVINGS] = sequential_savings_db; @@ -6013,7 +6051,7 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( sequential_savings_db, CreateIntVarFilteredDecisionBuilder< SequentialSavingsFilteredHeuristic>( - savings_parameters, + search_parameters.savings_parameters(), GetOrCreateLocalSearchFilterManager( search_parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/true}))); @@ -6397,9 +6435,9 @@ class LocalOptimumWatcher : public SearchMonitor { ListenToEvent(Solver::MonitorEvent::kLocalOptimum); } void EndInitialPropagation() override { end_initial_propagation_callback_(); } - bool LocalOptimum() override { + bool AtLocalOptimum() override { local_optimum_callback_(); - return SearchMonitor::LocalOptimum(); + return SearchMonitor::AtLocalOptimum(); } private: @@ -6467,12 +6505,12 @@ void RoutingModel::AddWeightedVariableTargetToFinalizer(IntVar* var, void RoutingModel::AddWeightedVariableMinimizedByFinalizer(IntVar* var, int64_t cost) { - finalizer_variables_->AddWeightedVariableToMinimize(var, cost); + finalizer_variables_->AddWeightedVariableTarget(var, kint64min, cost); } void RoutingModel::AddWeightedVariableMaximizedByFinalizer(IntVar* var, int64_t cost) { - finalizer_variables_->AddWeightedVariableToMaximize(var, cost); + finalizer_variables_->AddWeightedVariableTarget(var, kint64max, cost); } void RoutingModel::AddVariableTargetToFinalizer(IntVar* var, int64_t target) { @@ -6480,11 +6518,11 @@ void RoutingModel::AddVariableTargetToFinalizer(IntVar* var, int64_t target) { } void RoutingModel::AddVariableMaximizedByFinalizer(IntVar* var) { - finalizer_variables_->AddVariableToMaximize(var); + finalizer_variables_->AddVariableTarget(var, kint64max); } void RoutingModel::AddVariableMinimizedByFinalizer(IntVar* var) { - finalizer_variables_->AddVariableToMinimize(var); + finalizer_variables_->AddVariableTarget(var, kint64min); } void RoutingModel::SetupSearch( @@ -6558,9 +6596,9 @@ RoutingDimension::~RoutingDimension() { } void RoutingDimension::Initialize( - const std::vector& transit_evaluators, - const std::vector& cumul_dependent_transit_evaluators, - const std::vector& state_dependent_transit_evaluators, + absl::Span transit_evaluators, + absl::Span cumul_dependent_transit_evaluators, + absl::Span state_dependent_transit_evaluators, int64_t slack_max) { InitializeCumuls(); InitializeTransits(transit_evaluators, cumul_dependent_transit_evaluators, @@ -6891,7 +6929,7 @@ bool TypeRequirementChecker::HasRegulationsToCheck() const { } bool TypeRequirementChecker::CheckRequiredTypesCurrentlyOnRoute( - const std::vector>& required_type_alternatives, + absl::Span> required_type_alternatives, int pos) { for (const absl::flat_hash_set& requirement_alternatives : required_type_alternatives) { @@ -7080,9 +7118,26 @@ void RoutingDimension::CloseModel(bool use_light_propagation) { } } if (HasBreakConstraints()) { - GlobalVehicleBreaksConstraint* constraint = - model()->solver()->RevAlloc(new GlobalVehicleBreaksConstraint(this)); - solver->AddConstraint(constraint); + solver->AddConstraint( + MakeGlobalVehicleBreaksConstraint(model_->solver(), this)); + // If a vehicle has a duration-distance (max interbreak) constraint, + // its breaks must be ordered. + for (int v = 0; v < model_->vehicles(); ++v) { + const std::vector& breaks = GetBreakIntervalsOfVehicle(v); + const int num_breaks = breaks.size(); + if (num_breaks <= 1 || GetBreakDistanceDurationOfVehicle(v).empty()) { + continue; + } + for (int b = 1; b < num_breaks; ++b) { + Constraint* precedence = solver->MakeIntervalVarRelation( + breaks[b], Solver::STARTS_AFTER_END, breaks[b - 1]); + solver->AddConstraint(precedence); + } + } + // Add all cumuls to the finalizer. + for (IntVar* cumul : cumuls_) { + model_->AddVariableMinimizedByFinalizer(cumul); + } } } @@ -7578,4 +7633,4 @@ void RoutingDimension::SetupSlackAndDependentTransitCosts() const { } } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing.h b/ortools/routing/routing.h similarity index 94% rename from ortools/constraint_solver/routing.h rename to ortools/routing/routing.h index 9abc62b863e..c52403b3013 100644 --- a/ortools/constraint_solver/routing.h +++ b/ortools/routing/routing.h @@ -153,8 +153,8 @@ /// Keywords: Vehicle Routing, Traveling Salesman Problem, TSP, VRP, CVRPTW, /// PDP. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_H_ +#ifndef OR_TOOLS_ROUTING_ROUTING_H_ +#define OR_TOOLS_ROUTING_ROUTING_H_ #include #include @@ -172,30 +172,30 @@ #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" +#include "absl/functional/any_invocable.h" #include "absl/hash/hash.h" #include "absl/log/check.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" #include "absl/types/span.h" -#include "ortools/base/int_type.h" #include "ortools/base/logging.h" #include "ortools/base/strong_vector.h" #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_types.h" -#include "ortools/constraint_solver/routing_utils.h" #include "ortools/graph/graph.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/heuristic_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/types.h" +#include "ortools/routing/utils.h" #include "ortools/util/piecewise_linear_function.h" #include "ortools/util/range_query_function.h" #include "ortools/util/saturated_arithmetic.h" -#include "ortools/util/scheduling.h" #include "ortools/util/sorted_interval_list.h" -namespace operations_research { +namespace operations_research::routing { class FinalizerVariables; class GlobalDimensionCumulOptimizer; @@ -251,6 +251,12 @@ class PathsMetadata { std::vector path_of_node_; }; +struct RoutingSearchStats { + int64_t num_cp_sat_calls_in_lp_scheduling = 0; + int64_t num_glop_calls_in_lp_scheduling = 0; + int64_t num_min_cost_flow_calls = 0; +}; + class OR_DLL RoutingModel { public: /// Types of precedence policy applied to pickup and delivery pairs. @@ -969,13 +975,28 @@ class OR_DLL RoutingModel { /// Adds a soft constraint to force a set of variable indices to be on the /// same vehicle. If all nodes are not on the same vehicle, each extra vehicle /// used adds 'cost' to the cost function. + /// TODO(user): Extend this to allow nodes/indices to be on the same given + /// set of vehicle. void AddSoftSameVehicleConstraint(std::vector indices, int64_t cost); + /// Returns the number of soft same vehicle constraints in the model. + int GetNumberOfSoftSameVehicleConstraints() const { + return same_vehicle_costs_.size(); + } + /// Returns the indices of the nodes in the soft same vehicle constraint of + /// index 'index'. + const std::vector& GetSoftSameVehicleIndices(int index) const { + return same_vehicle_costs_[index].indices; + } + /// Returns the cost of the soft same vehicle constraint of index 'index'. + int64_t GetSoftSameVehicleCost(int index) const { + return same_vehicle_costs_[index].value; + } /// Sets the vehicles which can visit a given node. If the node is in a /// disjunction, this will not prevent it from being unperformed. /// Specifying an empty vector of vehicles has no effect (all vehicles /// will be allowed to visit the node). - void SetAllowedVehiclesForIndex(const std::vector& vehicles, + void SetAllowedVehiclesForIndex(absl::Span vehicles, int64_t index); /// Returns true if a vehicle is allowed to visit a given node. @@ -1108,6 +1129,11 @@ class OR_DLL RoutingModel { DCHECK(closed_); return topologically_sorted_visit_types_; } + const std::vector>>& + GetTopologicallySortedNodePrecedences() const { + DCHECK(closed_); + return topologically_sorted_node_precedences_; + } #endif // SWIG /// Incompatibilities: /// Two nodes with "hard" incompatible types cannot share the same route at @@ -1285,13 +1311,15 @@ class OR_DLL RoutingModel { // callback must not return a value if the route vector is invalid, and // returns the value of the route otherwise. // The callback must always return the same value for a given route. +#ifndef SWIG void AddRouteConstraint( - std::function(const std::vector&)> + absl::AnyInvocable(const std::vector&)> route_evaluator, bool costs_are_homogeneous_across_vehicles = false); +#endif std::optional GetRouteCost(const std::vector& route) const { int64_t route_cost = 0; - for (const auto& evaluator : route_evaluators_) { + for (auto& evaluator : route_evaluators_) { std::optional cost = evaluator(route); if (!cost.has_value()) return std::nullopt; CapAddTo(cost.value(), &route_cost); @@ -1427,6 +1455,8 @@ class OR_DLL RoutingModel { void SetAssignmentFromOtherModelAssignment( Assignment* target_assignment, const RoutingModel* source_model, const Assignment* source_assignment); + /// Returns detailed search statistics. + operations_research::SubSolverStatistics GetSubSolverStatistics() const; /// Computes a lower bound to the routing problem solving a linear assignment /// problem. The routing model must be closed before calling this method. /// Note that problems with node disjunction constraints (including optional @@ -1651,6 +1681,7 @@ class OR_DLL RoutingModel { if (routing_model_.IsStart(node_index)) return empty_neighbors_; if (node_index_to_incoming_neighbors_by_cost_class_.empty()) { + DCHECK(IsFullNeighborhood()); return all_incoming_nodes_; } const std::vector>& node_index_to_incoming_neighbors = @@ -1669,6 +1700,7 @@ class OR_DLL RoutingModel { if (routing_model_.IsEnd(node_index)) return empty_neighbors_; if (node_index_to_outgoing_neighbors_by_cost_class_.empty()) { + DCHECK(IsFullNeighborhood()); return all_outgoing_nodes_; } const std::vector>& node_index_to_outgoing_neighbors = @@ -1684,6 +1716,7 @@ class OR_DLL RoutingModel { bool IsNeighborhoodArcForCostClass(int cost_class, int64_t from, int64_t to) const { if (node_index_to_outgoing_neighbor_indicator_by_cost_class_.empty()) { + DCHECK(full_neighborhood_); return true; } if (routing_model_.IsEnd(from)) { @@ -1693,6 +1726,8 @@ class OR_DLL RoutingModel { [cost_class][from][to]; } + bool IsFullNeighborhood() const { return full_neighborhood_; } + private: const RoutingModel& routing_model_; #if __cplusplus >= 202002L @@ -1710,6 +1745,8 @@ class OR_DLL RoutingModel { std::vector all_outgoing_nodes_; std::vector all_incoming_nodes_; + + bool full_neighborhood_ = false; }; /// Returns neighbors of all nodes for every cost class. The result is cached @@ -1995,7 +2032,7 @@ class OR_DLL RoutingModel { int64_t GetNumberOfRejectsInFirstSolution( const RoutingSearchParameters& search_parameters) const; /// Returns the automatic first solution strategy selected. - operations_research::FirstSolutionStrategy::Value + operations_research::routing::FirstSolutionStrategy::Value GetAutomaticFirstSolutionStrategy() const { return automatic_first_solution_strategy_; } @@ -2189,7 +2226,7 @@ class OR_DLL RoutingModel { const std::vector& state_dependent_evaluator_indices, int64_t slack_max, bool fix_start_cumul_to_zero, RoutingDimension* dimension); - DimensionIndex GetDimensionIndex(const std::string& dimension_name) const; + DimensionIndex GetDimensionIndex(absl::string_view dimension_name) const; /// Creates global and local cumul optimizers for the dimensions needing them, /// and stores them in the corresponding [local|global]_dimension_optimizers_ @@ -2251,6 +2288,10 @@ class OR_DLL RoutingModel { void FinalizeVisitTypes(); // Called by FinalizeVisitTypes() to setup topologically_sorted_visit_types_. void TopologicallySortVisitTypes(); + // This method updates topologically_sorted_node_precedences_ which contains + // nodes in topological order based on precedence constraints for + // dimensions of the model. + void FinalizePrecedences(); int64_t GetArcCostForClassInternal(int64_t from_index, int64_t to_index, CostClassIndex cost_class_index) const; int64_t GetArcCostWithGuidedLocalSearchPenalties(int64_t from_index, @@ -2504,8 +2545,8 @@ class OR_DLL RoutingModel { std::vector linear_cost_factor_of_vehicle_; std::vector quadratic_cost_factor_of_vehicle_; bool vehicle_amortized_cost_factors_set_; - std::vector< - std::function(const std::vector&)>> + mutable std::vector< + absl::AnyInvocable(const std::vector&)>> route_evaluators_; /// vehicle_used_when_empty_[vehicle] determines if "vehicle" should be /// taken into account for costs (arc costs, span costs, etc.) and constraints @@ -2611,6 +2652,8 @@ class OR_DLL RoutingModel { std::vector > topologically_sorted_visit_types_; // clang-format on int num_visit_types_; + std::vector>> + topologically_sorted_node_precedences_; // Two indices are equivalent if they correspond to the same node (as given // to the constructors taking a RoutingIndexManager). std::vector index_to_equivalence_class_; @@ -2685,6 +2728,8 @@ class OR_DLL RoutingModel { RegularLimit* first_solution_lns_limit_ = nullptr; absl::Duration time_buffer_; + RoutingSearchStats search_stats_; + std::atomic interrupt_cp_sat_; std::atomic interrupt_cp_; @@ -2730,213 +2775,11 @@ class OR_DLL RoutingModelVisitor : public BaseObject { }; #if !defined(SWIG) -/// This class acts like a CP propagator: it takes a set of tasks given by -/// their start/duration/end features, and reduces the range of possible values. -class DisjunctivePropagator { - public: - /// A structure to hold tasks described by their features. - /// The first num_chain_tasks are considered linked by a chain of precedences, - /// i.e. if i < j < num_chain_tasks, then end(i) <= start(j). - /// This occurs frequently in routing, and can be leveraged by - /// some variants of classic propagators. - struct Tasks { - int num_chain_tasks = 0; - std::vector start_min; - std::vector start_max; - std::vector duration_min; - std::vector duration_max; - std::vector end_min; - std::vector end_max; - std::vector is_preemptible; - std::vector forbidden_intervals; - std::vector> distance_duration; - int64_t span_min = 0; - int64_t span_max = kint64max; - - void Clear() { - start_min.clear(); - start_max.clear(); - duration_min.clear(); - duration_max.clear(); - end_min.clear(); - end_max.clear(); - is_preemptible.clear(); - forbidden_intervals.clear(); - distance_duration.clear(); - span_min = 0; - span_max = kint64max; - num_chain_tasks = 0; - } - }; - - /// Computes new bounds for all tasks, returns false if infeasible. - /// This does not compute a fixed point, so recalling it may filter more. - bool Propagate(Tasks* tasks); - - /// Propagates the deductions from the chain of precedences, if there is one. - bool Precedences(Tasks* tasks); - /// Transforms the problem with a time symmetry centered in 0. Returns true - /// for convenience. - bool MirrorTasks(Tasks* tasks); - /// Does edge-finding deductions on all tasks. - bool EdgeFinding(Tasks* tasks); - /// Does detectable precedences deductions on tasks in the chain precedence, - /// taking the time windows of nonchain tasks into account. - bool DetectablePrecedencesWithChain(Tasks* tasks); - /// Tasks might have holes in their domain, this enforces such holes. - bool ForbiddenIntervals(Tasks* tasks); - /// Propagates distance_duration constraints, if any. - bool DistanceDuration(Tasks* tasks); - /// Propagates a lower bound of the chain span, - /// end[num_chain_tasks] - start[0], to span_min. - bool ChainSpanMin(Tasks* tasks); - /// Computes a lower bound of the span of the chain, taking into account only - /// the first nonchain task. - /// For more accurate results, this should be called after Precedences(), - /// otherwise the lower bound might be lower than feasible. - bool ChainSpanMinDynamic(Tasks* tasks); - - private: - /// The main algorithm uses Vilim's theta tree data structure. - /// See Petr Vilim's PhD thesis "Global Constraints in Scheduling". - ThetaLambdaTree theta_lambda_tree_; - /// Mappings between events and tasks. - std::vector tasks_by_start_min_; - std::vector tasks_by_end_max_; - std::vector event_of_task_; - std::vector nonchain_tasks_by_start_max_; - /// Maps chain elements to the sum of chain task durations before them. - std::vector total_duration_before_; -}; - -struct TravelBounds { - std::vector min_travels; - std::vector max_travels; - std::vector pre_travels; - std::vector post_travels; -}; - -void AppendTasksFromPath(absl::Span path, - const TravelBounds& travel_bounds, - const RoutingDimension& dimension, - DisjunctivePropagator::Tasks* tasks); -void AppendTasksFromIntervals(const std::vector& intervals, - DisjunctivePropagator::Tasks* tasks); void FillPathEvaluation(absl::Span path, const RoutingModel::TransitCallback2& evaluator, std::vector* values); -void FillTravelBoundsOfVehicle(int vehicle, absl::Span path, - const RoutingDimension& dimension, - TravelBounds* travel_bounds); #endif // !defined(SWIG) -/// GlobalVehicleBreaksConstraint ensures breaks constraints are enforced on -/// all vehicles in the dimension passed to its constructor. -/// It is intended to be used for dimensions representing time. -/// A break constraint ensures break intervals fit on the route of a vehicle. -/// For a given vehicle, it forces break intervals to be disjoint from visit -/// intervals, where visit intervals start at CumulVar(node) and last for -/// node_visit_transit[node]. Moreover, it ensures that there is enough time -/// between two consecutive nodes of a route to do transit and vehicle breaks, -/// i.e. if Next(nodeA) = nodeB, CumulVar(nodeA) = tA and CumulVar(nodeB) = tB, -/// then SlackVar(nodeA) >= sum_{breaks \subseteq [tA, tB)} duration(break). -class GlobalVehicleBreaksConstraint : public Constraint { - public: - explicit GlobalVehicleBreaksConstraint(const RoutingDimension* dimension); - std::string DebugString() const override { - return "GlobalVehicleBreaksConstraint"; - } - - void Post() override; - void InitialPropagate() override; - - private: - void PropagateNode(int node); - void PropagateVehicle(int vehicle); - - const RoutingModel* model_; - const RoutingDimension* const dimension_; - std::vector vehicle_demons_; - std::vector path_; - - /// Sets path_ to be the longest sequence such that - /// _ path_[0] is the start of the vehicle - /// _ Next(path_[i-1]) is Bound() and has value path_[i], - /// followed by the end of the vehicle if the last node was not an end. - void FillPartialPathOfVehicle(int vehicle); - void FillPathTravels(absl::Span path); - - /// This translates pruning information to solver variables. - /// If constructed with an IntervalVar*, it follows the usual semantics of - /// IntervalVars. If constructed with an IntVar*, before_start and - /// after_start, operations are translated to simulate an interval that starts - /// at start - before_start and ends and start + after_start. If constructed - /// with nothing, the TaskTranslator will do nothing. This class should have - /// been an interface + subclasses, but that would force pointers in the - /// user's task vector, which means dynamic allocation. With this union-like - /// structure, a vector's reserved size will adjust to usage and eventually no - /// more dynamic allocation will be made. - class TaskTranslator { - public: - TaskTranslator(IntVar* start, int64_t before_start, int64_t after_start) - : start_(start), - before_start_(before_start), - after_start_(after_start) {} - explicit TaskTranslator(IntervalVar* interval) : interval_(interval) {} - TaskTranslator() = default; - - void SetStartMin(int64_t value) { - if (start_ != nullptr) { - start_->SetMin(CapAdd(before_start_, value)); - } else if (interval_ != nullptr) { - interval_->SetStartMin(value); - } - } - void SetStartMax(int64_t value) { - if (start_ != nullptr) { - start_->SetMax(CapAdd(before_start_, value)); - } else if (interval_ != nullptr) { - interval_->SetStartMax(value); - } - } - void SetDurationMin(int64_t value) { - if (interval_ != nullptr) { - interval_->SetDurationMin(value); - } - } - void SetEndMin(int64_t value) { - if (start_ != nullptr) { - start_->SetMin(CapSub(value, after_start_)); - } else if (interval_ != nullptr) { - interval_->SetEndMin(value); - } - } - void SetEndMax(int64_t value) { - if (start_ != nullptr) { - start_->SetMax(CapSub(value, after_start_)); - } else if (interval_ != nullptr) { - interval_->SetEndMax(value); - } - } - - private: - IntVar* start_ = nullptr; - int64_t before_start_; - int64_t after_start_; - IntervalVar* interval_ = nullptr; - }; - - /// Route and interval variables are normalized to the following values. - std::vector task_translators_; - - /// This is used to restrict bounds of tasks. - DisjunctivePropagator disjunctive_propagator_; - DisjunctivePropagator::Tasks tasks_; - - /// Used to help filling tasks_ at each propagation. - TravelBounds travel_bounds_; -}; - class TypeRegulationsChecker { public: explicit TypeRegulationsChecker(const RoutingModel& model); @@ -3028,7 +2871,7 @@ class TypeRequirementChecker : public TypeRegulationsChecker { /// Verifies that for each set in required_type_alternatives, at least one of /// the required types is on the route at position 'pos'. bool CheckRequiredTypesCurrentlyOnRoute( - const std::vector >& required_type_alternatives, + absl::Span> required_type_alternatives, int pos); // clang-format on bool CheckTypeRegulations(int type, VisitTypePolicy policy, int pos) override; @@ -3506,6 +3349,8 @@ class RoutingDimension { kFirstAndSecondIndependent, // if second_node is performed, first_node must be performed. kSecondImpliesFirst, + // if first_node is performed, second_node must be performed. + kFirstImpliesSecond, // first_node is performed iff second_node is performed. kFirstAndSecondEqual, }; @@ -3541,6 +3386,13 @@ class RoutingDimension { } if (second_unperformed) return PrecedenceStatus::kInactive; break; + case NodePrecedence::PerformedConstraint::kFirstImpliesSecond: + if (second_unperformed) { + if (!first_unperformed) return PrecedenceStatus::kInvalid; + return PrecedenceStatus::kInactive; + } + if (first_unperformed) return PrecedenceStatus::kInactive; + break; case NodePrecedence::PerformedConstraint::kFirstAndSecondEqual: if (first_unperformed != second_unperformed) { return PrecedenceStatus::kInvalid; @@ -3676,9 +3528,9 @@ class RoutingDimension { const RoutingDimension* base_dimension); RoutingDimension(RoutingModel* model, std::vector vehicle_capacities, const std::string& name, SelfBased); - void Initialize(const std::vector& transit_evaluators, - const std::vector& cumul_dependent_transit_evaluators, - const std::vector& state_dependent_transit_evaluators, + void Initialize(absl::Span transit_evaluators, + absl::Span cumul_dependent_transit_evaluators, + absl::Span state_dependent_transit_evaluators, int64_t slack_max); void InitializeCumuls(); void InitializeTransits( @@ -3797,10 +3649,5 @@ bool SolveModelWithSat(RoutingModel* model, const Assignment* initial_solution, Assignment* solution); -#if !defined(SWIG) -IntVarLocalSearchFilter* MakeVehicleBreaksFilter( - const RoutingModel& routing_model, const RoutingDimension& dimension); -#endif - -} // namespace operations_research -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_H_ +} // namespace operations_research::routing +#endif // OR_TOOLS_ROUTING_ROUTING_H_ diff --git a/ortools/routing/samples/BUILD.bazel b/ortools/routing/samples/BUILD.bazel index a32d5c89ed2..c30dd772be3 100644 --- a/ortools/routing/samples/BUILD.bazel +++ b/ortools/routing/samples/BUILD.bazel @@ -11,12 +11,51 @@ # See the License for the specific language governing permissions and # limitations under the License. +load(":code_samples.bzl", "code_sample_cc") + +code_sample_cc(name = "simple_routing_program") + +code_sample_cc(name = "tsp") + +code_sample_cc(name = "tsp_circuit_board") + +code_sample_cc(name = "tsp_cities") + +code_sample_cc(name = "tsp_distance_matrix") + +code_sample_cc(name = "vrp") + +code_sample_cc(name = "vrp_breaks") + +code_sample_cc(name = "vrp_capacity") + +code_sample_cc(name = "vrp_drop_nodes") + +code_sample_cc(name = "vrp_global_span") + +code_sample_cc(name = "vrp_initial_routes") + +code_sample_cc(name = "vrp_pickup_delivery") + +code_sample_cc(name = "vrp_pickup_delivery_fifo") + +code_sample_cc(name = "vrp_pickup_delivery_lifo") + +code_sample_cc(name = "vrp_resources") + +code_sample_cc(name = "vrp_starts_ends") + +code_sample_cc(name = "vrp_time_windows") + +code_sample_cc(name = "vrp_with_time_limit") + +# cvrptw samples cc_binary( name = "cvrptw", srcs = ["cvrptw.cc"], deps = [ "//ortools/base", - "//ortools/constraint_solver:routing", + "//ortools/routing", "//ortools/routing/parsers:cvrptw_lib", ], ) @@ -26,7 +65,7 @@ cc_binary( srcs = ["cvrp_disjoint_tw.cc"], deps = [ "//ortools/base", - "//ortools/constraint_solver:routing", + "//ortools/routing", "//ortools/routing/parsers:cvrptw_lib", ], ) @@ -36,8 +75,8 @@ cc_binary( srcs = ["cvrptw_with_breaks.cc"], deps = [ "//ortools/base", - "//ortools/constraint_solver:routing", - "//ortools/constraint_solver:routing_enums_cc_proto", + "//ortools/routing", + "//ortools/routing:enums_cc_proto", "//ortools/routing/parsers:cvrptw_lib", "@abseil-cpp//absl/strings", ], @@ -48,7 +87,7 @@ cc_binary( srcs = ["cvrptw_with_resources.cc"], deps = [ "//ortools/base", - "//ortools/constraint_solver:routing", + "//ortools/routing", "//ortools/routing/parsers:cvrptw_lib", ], ) @@ -58,7 +97,7 @@ cc_binary( srcs = ["cvrptw_with_stop_times_and_resources.cc"], deps = [ "//ortools/base", - "//ortools/constraint_solver:routing", + "//ortools/routing", "//ortools/routing/parsers:cvrptw_lib", "@abseil-cpp//absl/strings", ], @@ -69,7 +108,7 @@ cc_binary( srcs = ["cvrptw_with_refueling.cc"], deps = [ "//ortools/base", - "//ortools/constraint_solver:routing", + "//ortools/routing", "//ortools/routing/parsers:cvrptw_lib", ], ) diff --git a/ortools/routing/samples/CMakeLists.txt b/ortools/routing/samples/CMakeLists.txt new file mode 100644 index 00000000000..ee48e3d3dae --- /dev/null +++ b/ortools/routing/samples/CMakeLists.txt @@ -0,0 +1,47 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +if(NOT BUILD_SAMPLES) + return() +endif() + +if(BUILD_CXX_SAMPLES) + file(GLOB CXX_SRCS "*.cc") + list(FILTER CXX_SRCS EXCLUDE REGEX "/cvrp_disjoint_tw") + list(FILTER CXX_SRCS EXCLUDE REGEX "/cvrptw\.cc") + list(FILTER CXX_SRCS EXCLUDE REGEX "/cvrptw_") + foreach(SAMPLE IN LISTS CXX_SRCS) + add_cxx_sample(FILE_NAME ${SAMPLE}) + endforeach() +endif() + +if(BUILD_PYTHON_SAMPLES) + file(GLOB PYTHON_SRCS "*.py") + foreach(SAMPLE IN LISTS PYTHON_SRCS) + add_python_sample(FILE_NAME ${SAMPLE}) + endforeach() +endif() + +if(BUILD_JAVA_SAMPLES) + file(GLOB JAVA_SRCS "*.java") + foreach(SAMPLE IN LISTS JAVA_SRCS) + add_java_sample(FILE_NAME ${SAMPLE}) + endforeach() +endif() + +if(BUILD_DOTNET_SAMPLES) + file(GLOB DOTNET_SRCS "*.cs") + foreach(SAMPLE IN LISTS DOTNET_SRCS) + add_dotnet_sample(FILE_NAME ${SAMPLE}) + endforeach() +endif() diff --git a/ortools/constraint_solver/samples/SimpleRoutingProgram.cs b/ortools/routing/samples/SimpleRoutingProgram.cs similarity index 96% rename from ortools/constraint_solver/samples/SimpleRoutingProgram.cs rename to ortools/routing/samples/SimpleRoutingProgram.cs index 23b85ff25c5..cffe8a56116 100644 --- a/ortools/constraint_solver/samples/SimpleRoutingProgram.cs +++ b/ortools/routing/samples/SimpleRoutingProgram.cs @@ -15,6 +15,7 @@ // [START import] using System; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -60,8 +61,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/SimpleRoutingProgram.java b/ortools/routing/samples/SimpleRoutingProgram.java similarity index 89% rename from ortools/constraint_solver/samples/SimpleRoutingProgram.java rename to ortools/routing/samples/SimpleRoutingProgram.java index c09ec11823c..55fd5d08c72 100644 --- a/ortools/constraint_solver/samples/SimpleRoutingProgram.java +++ b/ortools/routing/samples/SimpleRoutingProgram.java @@ -12,17 +12,17 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import static java.lang.Math.abs; import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -68,7 +68,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/Tsp.cs b/ortools/routing/samples/Tsp.cs similarity index 92% rename from ortools/constraint_solver/samples/Tsp.cs rename to ortools/routing/samples/Tsp.cs index b2b49925b4d..ce370ac8330 100644 --- a/ortools/constraint_solver/samples/Tsp.cs +++ b/ortools/routing/samples/Tsp.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -95,6 +96,14 @@ public long Call(long fromIndex, long toIndex) /// static void PrintSolution(in RoutingModel routing, in RoutingIndexManager manager, in Assignment solution) { + RoutingSearchStatus.Types.Value status = routing.GetStatus(); + Console.WriteLine("Status: {0}", status); + if (status != RoutingSearchStatus.Types.Value.RoutingOptimal && + status != RoutingSearchStatus.Types.Value.RoutingSuccess) + { + Console.WriteLine("No solution found!"); + return; + } Console.WriteLine("Objective: {0}", solution.ObjectiveValue()); // Inspect solution. Console.WriteLine("Route for Vehicle 0:"); @@ -143,8 +152,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/Tsp.java b/ortools/routing/samples/Tsp.java similarity index 92% rename from ortools/constraint_solver/samples/Tsp.java rename to ortools/routing/samples/Tsp.java index 1ab24872eb8..41eb3befc16 100644 --- a/ortools/constraint_solver/samples/Tsp.java +++ b/ortools/routing/samples/Tsp.java @@ -12,19 +12,19 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import static java.lang.Math.abs; import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.RoutingSearchStatus; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; +import com.google.ortools.routing.RoutingSearchStatus; import java.util.function.LongBinaryOperator; import java.util.logging.Logger; @@ -169,7 +169,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/TspCircuitBoard.cs b/ortools/routing/samples/TspCircuitBoard.cs similarity index 97% rename from ortools/constraint_solver/samples/TspCircuitBoard.cs rename to ortools/routing/samples/TspCircuitBoard.cs index c0f4d7d416b..fba1304eb62 100644 --- a/ortools/constraint_solver/samples/TspCircuitBoard.cs +++ b/ortools/routing/samples/TspCircuitBoard.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -121,7 +122,7 @@ static void PrintSolution(in RoutingModel routing, in RoutingIndexManager manage routeDistance += routing.GetArcCostForVehicle(previousIndex, index, 0); } Console.WriteLine("{0}", manager.IndexToNode((int)index)); - Console.WriteLine("Route distance: {0}m", routeDistance); + Console.WriteLine("Route distance: {0}mm", routeDistance); } // [END solution_printer] @@ -162,8 +163,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/TspCircuitBoard.java b/ortools/routing/samples/TspCircuitBoard.java similarity index 95% rename from ortools/constraint_solver/samples/TspCircuitBoard.java rename to ortools/routing/samples/TspCircuitBoard.java index a03ad1e8ea9..3a0af36974d 100644 --- a/ortools/constraint_solver/samples/TspCircuitBoard.java +++ b/ortools/routing/samples/TspCircuitBoard.java @@ -12,16 +12,16 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -156,7 +156,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/TspCities.cs b/ortools/routing/samples/TspCities.cs similarity index 97% rename from ortools/constraint_solver/samples/TspCities.cs rename to ortools/routing/samples/TspCities.cs index 669df97f870..9120c5ddc4d 100644 --- a/ortools/constraint_solver/samples/TspCities.cs +++ b/ortools/routing/samples/TspCities.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -105,8 +106,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/TspCities.java b/ortools/routing/samples/TspCities.java similarity index 92% rename from ortools/constraint_solver/samples/TspCities.java rename to ortools/routing/samples/TspCities.java index ea4f4ae91d9..0ceeb723026 100644 --- a/ortools/constraint_solver/samples/TspCities.java +++ b/ortools/routing/samples/TspCities.java @@ -12,16 +12,16 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -112,7 +112,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/TspDistanceMatrix.cs b/ortools/routing/samples/TspDistanceMatrix.cs similarity index 97% rename from ortools/constraint_solver/samples/TspDistanceMatrix.cs rename to ortools/routing/samples/TspDistanceMatrix.cs index 08e935b292c..c978ce9f405 100644 --- a/ortools/constraint_solver/samples/TspDistanceMatrix.cs +++ b/ortools/routing/samples/TspDistanceMatrix.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -110,8 +111,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/TspDistanceMatrix.java b/ortools/routing/samples/TspDistanceMatrix.java similarity index 93% rename from ortools/constraint_solver/samples/TspDistanceMatrix.java rename to ortools/routing/samples/TspDistanceMatrix.java index e3ec6f3b548..f609fd059f3 100644 --- a/ortools/constraint_solver/samples/TspDistanceMatrix.java +++ b/ortools/routing/samples/TspDistanceMatrix.java @@ -12,16 +12,16 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -115,7 +115,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/Vrp.cs b/ortools/routing/samples/Vrp.cs similarity index 89% rename from ortools/constraint_solver/samples/Vrp.cs rename to ortools/routing/samples/Vrp.cs index bb87bdae487..c7970d4016c 100644 --- a/ortools/constraint_solver/samples/Vrp.cs +++ b/ortools/routing/samples/Vrp.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -54,13 +55,20 @@ class DataModel /// /// Print the solution. /// - static void PrintSolution(in DataModel data, in RoutingModel routing, in RoutingIndexManager manager, - in Assignment solution) + static void PrintSolution(in RoutingModel routing, in RoutingIndexManager manager, in Assignment solution) { + RoutingSearchStatus.Types.Value status = routing.GetStatus(); + Console.WriteLine("Status: {0}", status); + if (status != RoutingSearchStatus.Types.Value.RoutingOptimal && + status != RoutingSearchStatus.Types.Value.RoutingSuccess) + { + Console.WriteLine("No solution found!"); + return; + } Console.WriteLine("Objective: {0}", solution.ObjectiveValue()); // Inspect solution. long totalDistance = 0; - for (int i = 0; i < data.VehicleNumber; ++i) + for (int i = 0; i < manager.GetNumberOfVehicles(); ++i) { if (!routing.IsVehicleUsed(solution, i)) { @@ -121,8 +129,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] @@ -133,7 +140,7 @@ public static void Main(String[] args) // Print solution on console. // [START print_solution] - PrintSolution(data, routing, manager, solution); + PrintSolution(routing, manager, solution); // [END print_solution] } } diff --git a/ortools/constraint_solver/samples/Vrp.java b/ortools/routing/samples/Vrp.java similarity index 92% rename from ortools/constraint_solver/samples/Vrp.java rename to ortools/routing/samples/Vrp.java index 0a2c5c3c697..f3bd405ee0a 100644 --- a/ortools/constraint_solver/samples/Vrp.java +++ b/ortools/routing/samples/Vrp.java @@ -12,17 +12,17 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.RoutingSearchStatus; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; +import com.google.ortools.routing.RoutingSearchStatus; import java.util.logging.Logger; // [END import] @@ -132,7 +132,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/VrpBreaks.cs b/ortools/routing/samples/VrpBreaks.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpBreaks.cs rename to ortools/routing/samples/VrpBreaks.cs index 13a577af865..addd27bd4e7 100644 --- a/ortools/constraint_solver/samples/VrpBreaks.cs +++ b/ortools/routing/samples/VrpBreaks.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -172,8 +173,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/VrpBreaks.java b/ortools/routing/samples/VrpBreaks.java similarity index 94% rename from ortools/constraint_solver/samples/VrpBreaks.java rename to ortools/routing/samples/VrpBreaks.java index efd32e34cda..03db40a7efe 100644 --- a/ortools/constraint_solver/samples/VrpBreaks.java +++ b/ortools/routing/samples/VrpBreaks.java @@ -12,22 +12,22 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; import com.google.ortools.constraintsolver.AssignmentIntervalContainer; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; import com.google.ortools.constraintsolver.IntVar; import com.google.ortools.constraintsolver.IntervalVar; import com.google.ortools.constraintsolver.IntervalVarElement; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; import com.google.ortools.constraintsolver.Solver; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -187,7 +187,7 @@ public static void main(String[] args) { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/VrpCapacity.cs b/ortools/routing/samples/VrpCapacity.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpCapacity.cs rename to ortools/routing/samples/VrpCapacity.cs index 7f231fb35a6..b882348ee16 100644 --- a/ortools/constraint_solver/samples/VrpCapacity.cs +++ b/ortools/routing/samples/VrpCapacity.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; using Google.Protobuf.WellKnownTypes; // Duration // [END import] @@ -149,8 +150,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; searchParameters.LocalSearchMetaheuristic = LocalSearchMetaheuristic.Types.Value.GuidedLocalSearch; searchParameters.TimeLimit = new Duration { Seconds = 1 }; diff --git a/ortools/constraint_solver/samples/VrpCapacity.java b/ortools/routing/samples/VrpCapacity.java similarity index 92% rename from ortools/constraint_solver/samples/VrpCapacity.java rename to ortools/routing/samples/VrpCapacity.java index da98fd9cea3..9415bfc3fd5 100644 --- a/ortools/constraint_solver/samples/VrpCapacity.java +++ b/ortools/routing/samples/VrpCapacity.java @@ -12,17 +12,17 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.LocalSearchMetaheuristic; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.LocalSearchMetaheuristic; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import com.google.protobuf.Duration; import java.util.logging.Logger; // [END import] @@ -59,10 +59,11 @@ static class DataModel { public final int vehicleNumber = 4; public final int depot = 0; } + // [END data_model] // [START solution_printer] - /// @brief Print the solution. + // Print the solution. static void printSolution( DataModel data, RoutingModel routing, RoutingIndexManager manager, Assignment solution) { // Solution cost. @@ -96,9 +97,10 @@ static void printSolution( logger.info("Total distance of all routes: " + totalDistance + "m"); logger.info("Total load of all routes: " + totalLoad); } + // [END solution_printer] - public static void main(String[] args) throws Exception { + public static void main(String[] args) { Loader.loadNativeLibraries(); // Instantiate the data problem. // [START data] @@ -149,7 +151,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .setLocalSearchMetaheuristic(LocalSearchMetaheuristic.Value.GUIDED_LOCAL_SEARCH) diff --git a/ortools/constraint_solver/samples/VrpDropNodes.cs b/ortools/routing/samples/VrpDropNodes.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpDropNodes.cs rename to ortools/routing/samples/VrpDropNodes.cs index e3f2a5a187e..0438203668a 100644 --- a/ortools/constraint_solver/samples/VrpDropNodes.cs +++ b/ortools/routing/samples/VrpDropNodes.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; using Google.Protobuf.WellKnownTypes; // Duration // [END import] @@ -170,8 +171,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; searchParameters.LocalSearchMetaheuristic = LocalSearchMetaheuristic.Types.Value.GuidedLocalSearch; searchParameters.TimeLimit = new Duration { Seconds = 1 }; diff --git a/ortools/constraint_solver/samples/VrpDropNodes.java b/ortools/routing/samples/VrpDropNodes.java similarity index 94% rename from ortools/constraint_solver/samples/VrpDropNodes.java rename to ortools/routing/samples/VrpDropNodes.java index e311be9d2d6..ba1b798eb82 100644 --- a/ortools/constraint_solver/samples/VrpDropNodes.java +++ b/ortools/routing/samples/VrpDropNodes.java @@ -12,17 +12,17 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.LocalSearchMetaheuristic; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.LocalSearchMetaheuristic; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import com.google.protobuf.Duration; import java.util.logging.Logger; // [END import] @@ -166,7 +166,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .setLocalSearchMetaheuristic(LocalSearchMetaheuristic.Value.GUIDED_LOCAL_SEARCH) diff --git a/ortools/constraint_solver/samples/VrpGlobalSpan.cs b/ortools/routing/samples/VrpGlobalSpan.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpGlobalSpan.cs rename to ortools/routing/samples/VrpGlobalSpan.cs index c12be6b2403..ff553ca1e39 100644 --- a/ortools/constraint_solver/samples/VrpGlobalSpan.cs +++ b/ortools/routing/samples/VrpGlobalSpan.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -132,8 +133,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/VrpGlobalSpan.java b/ortools/routing/samples/VrpGlobalSpan.java similarity index 92% rename from ortools/constraint_solver/samples/VrpGlobalSpan.java rename to ortools/routing/samples/VrpGlobalSpan.java index 33b941627eb..047e5be8f59 100644 --- a/ortools/constraint_solver/samples/VrpGlobalSpan.java +++ b/ortools/routing/samples/VrpGlobalSpan.java @@ -12,17 +12,17 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -132,7 +132,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/VrpInitialRoutes.cs b/ortools/routing/samples/VrpInitialRoutes.cs similarity index 97% rename from ortools/constraint_solver/samples/VrpInitialRoutes.cs rename to ortools/routing/samples/VrpInitialRoutes.cs index 260f4600dbb..a879be6aadd 100644 --- a/ortools/constraint_solver/samples/VrpInitialRoutes.cs +++ b/ortools/routing/samples/VrpInitialRoutes.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -147,14 +148,12 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); // [END parameters] // Solve the problem. // [START solve] - Assignment solution = routing.SolveFromAssignmentWithParameters( - initialSolution, searchParameters); + Assignment solution = routing.SolveFromAssignmentWithParameters(initialSolution, searchParameters); // [END solve] // Print solution on console. diff --git a/ortools/constraint_solver/samples/VrpInitialRoutes.java b/ortools/routing/samples/VrpInitialRoutes.java similarity index 93% rename from ortools/constraint_solver/samples/VrpInitialRoutes.java rename to ortools/routing/samples/VrpInitialRoutes.java index b78abce984f..0a3bd8c8585 100644 --- a/ortools/constraint_solver/samples/VrpInitialRoutes.java +++ b/ortools/routing/samples/VrpInitialRoutes.java @@ -12,16 +12,16 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -144,7 +144,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = main.defaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = Globals.defaultRoutingSearchParameters(); // [END parameters] // Solve the problem. diff --git a/ortools/constraint_solver/samples/VrpPickupDelivery.cs b/ortools/routing/samples/VrpPickupDelivery.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpPickupDelivery.cs rename to ortools/routing/samples/VrpPickupDelivery.cs index a7fe59e37a2..a756777840c 100644 --- a/ortools/constraint_solver/samples/VrpPickupDelivery.cs +++ b/ortools/routing/samples/VrpPickupDelivery.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -152,8 +153,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/VrpPickupDelivery.java b/ortools/routing/samples/VrpPickupDelivery.java similarity index 93% rename from ortools/constraint_solver/samples/VrpPickupDelivery.java rename to ortools/routing/samples/VrpPickupDelivery.java index bdf855a6dc3..807c8284d5f 100644 --- a/ortools/constraint_solver/samples/VrpPickupDelivery.java +++ b/ortools/routing/samples/VrpPickupDelivery.java @@ -12,18 +12,18 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; import com.google.ortools.constraintsolver.Solver; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -161,7 +161,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PARALLEL_CHEAPEST_INSERTION) .build(); diff --git a/ortools/constraint_solver/samples/VrpPickupDeliveryFifo.cs b/ortools/routing/samples/VrpPickupDeliveryFifo.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpPickupDeliveryFifo.cs rename to ortools/routing/samples/VrpPickupDeliveryFifo.cs index bf2e9d33f01..a4fc7f202d4 100644 --- a/ortools/constraint_solver/samples/VrpPickupDeliveryFifo.cs +++ b/ortools/routing/samples/VrpPickupDeliveryFifo.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -153,8 +154,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/VrpPickupDeliveryFifo.java b/ortools/routing/samples/VrpPickupDeliveryFifo.java similarity index 93% rename from ortools/constraint_solver/samples/VrpPickupDeliveryFifo.java rename to ortools/routing/samples/VrpPickupDeliveryFifo.java index ce1f354903e..e9d3bd8a05f 100644 --- a/ortools/constraint_solver/samples/VrpPickupDeliveryFifo.java +++ b/ortools/routing/samples/VrpPickupDeliveryFifo.java @@ -12,18 +12,18 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; import com.google.ortools.constraintsolver.Solver; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -162,7 +162,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PARALLEL_CHEAPEST_INSERTION) .build(); diff --git a/ortools/constraint_solver/samples/VrpPickupDeliveryLifo.cs b/ortools/routing/samples/VrpPickupDeliveryLifo.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpPickupDeliveryLifo.cs rename to ortools/routing/samples/VrpPickupDeliveryLifo.cs index 90ff335807f..56c16d11349 100644 --- a/ortools/constraint_solver/samples/VrpPickupDeliveryLifo.cs +++ b/ortools/routing/samples/VrpPickupDeliveryLifo.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -152,8 +153,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/VrpPickupDeliveryLifo.java b/ortools/routing/samples/VrpPickupDeliveryLifo.java similarity index 93% rename from ortools/constraint_solver/samples/VrpPickupDeliveryLifo.java rename to ortools/routing/samples/VrpPickupDeliveryLifo.java index b1afce76979..6b80b3ea91b 100644 --- a/ortools/constraint_solver/samples/VrpPickupDeliveryLifo.java +++ b/ortools/routing/samples/VrpPickupDeliveryLifo.java @@ -12,18 +12,18 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; import com.google.ortools.constraintsolver.Solver; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -162,7 +162,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PARALLEL_CHEAPEST_INSERTION) .build(); diff --git a/ortools/constraint_solver/samples/VrpResources.cs b/ortools/routing/samples/VrpResources.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpResources.cs rename to ortools/routing/samples/VrpResources.cs index 70d8b295533..791341ce7b6 100644 --- a/ortools/constraint_solver/samples/VrpResources.cs +++ b/ortools/routing/samples/VrpResources.cs @@ -17,6 +17,7 @@ using System.Linq; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -200,8 +201,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/VrpResources.java b/ortools/routing/samples/VrpResources.java similarity index 94% rename from ortools/constraint_solver/samples/VrpResources.java rename to ortools/routing/samples/VrpResources.java index fe725e6c045..0eaf7a53f52 100644 --- a/ortools/constraint_solver/samples/VrpResources.java +++ b/ortools/routing/samples/VrpResources.java @@ -12,20 +12,20 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; import com.google.ortools.constraintsolver.IntVar; import com.google.ortools.constraintsolver.IntervalVar; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; import com.google.ortools.constraintsolver.Solver; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.Arrays; import java.util.logging.Logger; // [END import] @@ -202,7 +202,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/VrpSolutionCallback.cs b/ortools/routing/samples/VrpSolutionCallback.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpSolutionCallback.cs rename to ortools/routing/samples/VrpSolutionCallback.cs index ae6b37be40a..735021cf634 100644 --- a/ortools/constraint_solver/samples/VrpSolutionCallback.cs +++ b/ortools/routing/samples/VrpSolutionCallback.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; using Google.Protobuf.WellKnownTypes; // Duration // [END import] @@ -178,8 +179,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; searchParameters.LocalSearchMetaheuristic = LocalSearchMetaheuristic.Types.Value.GuidedLocalSearch; searchParameters.TimeLimit = new Duration { Seconds = 5 }; diff --git a/ortools/constraint_solver/samples/VrpSolutionCallback.java b/ortools/routing/samples/VrpSolutionCallback.java similarity index 93% rename from ortools/constraint_solver/samples/VrpSolutionCallback.java rename to ortools/routing/samples/VrpSolutionCallback.java index 6e9b57071df..2425484a2f1 100644 --- a/ortools/constraint_solver/samples/VrpSolutionCallback.java +++ b/ortools/routing/samples/VrpSolutionCallback.java @@ -12,18 +12,18 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.LocalSearchMetaheuristic; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.LocalSearchMetaheuristic; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import com.google.protobuf.Duration; import java.util.logging.Logger; // [END import] @@ -176,7 +176,7 @@ public static void main(String[] args) { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .setLocalSearchMetaheuristic(LocalSearchMetaheuristic.Value.GUIDED_LOCAL_SEARCH) diff --git a/ortools/constraint_solver/samples/VrpStartsEnds.cs b/ortools/routing/samples/VrpStartsEnds.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpStartsEnds.cs rename to ortools/routing/samples/VrpStartsEnds.cs index 512710e1e16..913d1520666 100644 --- a/ortools/constraint_solver/samples/VrpStartsEnds.cs +++ b/ortools/routing/samples/VrpStartsEnds.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] /// @@ -134,8 +135,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/VrpStartsEnds.java b/ortools/routing/samples/VrpStartsEnds.java similarity index 92% rename from ortools/constraint_solver/samples/VrpStartsEnds.java rename to ortools/routing/samples/VrpStartsEnds.java index 5380ff475c9..db7434662d1 100644 --- a/ortools/constraint_solver/samples/VrpStartsEnds.java +++ b/ortools/routing/samples/VrpStartsEnds.java @@ -12,17 +12,17 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -135,7 +135,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/VrpTimeWindows.cs b/ortools/routing/samples/VrpTimeWindows.cs similarity index 98% rename from ortools/constraint_solver/samples/VrpTimeWindows.cs rename to ortools/routing/samples/VrpTimeWindows.cs index 061d95fe36c..beff4ff4369 100644 --- a/ortools/constraint_solver/samples/VrpTimeWindows.cs +++ b/ortools/routing/samples/VrpTimeWindows.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; // [END import] // [START program_part1] @@ -175,8 +176,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; // [END parameters] diff --git a/ortools/constraint_solver/samples/VrpTimeWindows.java b/ortools/routing/samples/VrpTimeWindows.java similarity index 93% rename from ortools/constraint_solver/samples/VrpTimeWindows.java rename to ortools/routing/samples/VrpTimeWindows.java index 5f9751e9815..198ed313925 100644 --- a/ortools/constraint_solver/samples/VrpTimeWindows.java +++ b/ortools/routing/samples/VrpTimeWindows.java @@ -12,18 +12,18 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; import com.google.ortools.constraintsolver.IntVar; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import java.util.logging.Logger; // [END import] @@ -174,7 +174,7 @@ public static void main(String[] args) throws Exception { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .build(); diff --git a/ortools/constraint_solver/samples/VrpWithTimeLimit.cs b/ortools/routing/samples/VrpWithTimeLimit.cs similarity index 97% rename from ortools/constraint_solver/samples/VrpWithTimeLimit.cs rename to ortools/routing/samples/VrpWithTimeLimit.cs index 2dbc079256a..381f7ff5db1 100644 --- a/ortools/constraint_solver/samples/VrpWithTimeLimit.cs +++ b/ortools/routing/samples/VrpWithTimeLimit.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; using Google.OrTools.ConstraintSolver; +using Google.OrTools.Routing; using Google.Protobuf.WellKnownTypes; // Duration // [END import] @@ -107,8 +108,7 @@ public static void Main(String[] args) // Setting first solution heuristic. // [START parameters] - RoutingSearchParameters searchParameters = - operations_research_constraint_solver.DefaultRoutingSearchParameters(); + RoutingSearchParameters searchParameters = RoutingGlobals.DefaultRoutingSearchParameters(); searchParameters.FirstSolutionStrategy = FirstSolutionStrategy.Types.Value.PathCheapestArc; searchParameters.LocalSearchMetaheuristic = LocalSearchMetaheuristic.Types.Value.GuidedLocalSearch; searchParameters.LogSearch = true; diff --git a/ortools/constraint_solver/samples/VrpWithTimeLimit.java b/ortools/routing/samples/VrpWithTimeLimit.java similarity index 89% rename from ortools/constraint_solver/samples/VrpWithTimeLimit.java rename to ortools/routing/samples/VrpWithTimeLimit.java index 13e2d40201f..2f3a974032a 100644 --- a/ortools/constraint_solver/samples/VrpWithTimeLimit.java +++ b/ortools/routing/samples/VrpWithTimeLimit.java @@ -12,20 +12,20 @@ // limitations under the License. // [START program] -package com.google.ortools.constraintsolver.samples; +package com.google.ortools.routing.samples; // [START import] import static java.lang.Math.max; import com.google.ortools.Loader; import com.google.ortools.constraintsolver.Assignment; -import com.google.ortools.constraintsolver.FirstSolutionStrategy; -import com.google.ortools.constraintsolver.LocalSearchMetaheuristic; -import com.google.ortools.constraintsolver.RoutingDimension; -import com.google.ortools.constraintsolver.RoutingIndexManager; -import com.google.ortools.constraintsolver.RoutingModel; -import com.google.ortools.constraintsolver.RoutingSearchParameters; -import com.google.ortools.constraintsolver.main; +import com.google.ortools.routing.FirstSolutionStrategy; +import com.google.ortools.routing.Globals; +import com.google.ortools.routing.LocalSearchMetaheuristic; +import com.google.ortools.routing.RoutingDimension; +import com.google.ortools.routing.RoutingIndexManager; +import com.google.ortools.routing.RoutingModel; +import com.google.ortools.routing.RoutingSearchParameters; import com.google.protobuf.Duration; import java.util.logging.Logger; // [END import] @@ -107,7 +107,7 @@ public static void main(String[] args) { // Setting first solution heuristic. // [START parameters] RoutingSearchParameters searchParameters = - main.defaultRoutingSearchParameters() + Globals.defaultRoutingSearchParameters() .toBuilder() .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC) .setLocalSearchMetaheuristic(LocalSearchMetaheuristic.Value.GUIDED_LOCAL_SEARCH) diff --git a/ortools/routing/samples/code_samples.bzl b/ortools/routing/samples/code_samples.bzl new file mode 100644 index 00000000000..35967c1cc00 --- /dev/null +++ b/ortools/routing/samples/code_samples.bzl @@ -0,0 +1,39 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Helper macro to compile and test code samples.""" + +load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_test") + +def code_sample_cc(name): + cc_binary( + name = name + "_cc", + srcs = [name + ".cc"], + deps = [ + "//ortools/base", + "//ortools/routing:routing", + "//ortools/routing:enums_cc_proto", + ], + ) + + cc_test( + name = name + "_cc_test", + size = "small", + srcs = [name + ".cc"], + deps = [ + ":" + name + "_cc", + "//ortools/base", + "//ortools/routing:routing", + "//ortools/routing:enums_cc_proto", + ], + ) diff --git a/ortools/routing/samples/cvrp_disjoint_tw.cc b/ortools/routing/samples/cvrp_disjoint_tw.cc index 297fe9d8c01..18e51bd1ece 100644 --- a/ortools/routing/samples/cvrp_disjoint_tw.cc +++ b/ortools/routing/samples/cvrp_disjoint_tw.cc @@ -30,31 +30,33 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" +#include "absl/log/globals.h" #include "absl/random/random.h" #include "google/protobuf/text_format.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/cvrptw_lib.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" using operations_research::Assignment; -using operations_research::DefaultRoutingSearchParameters; -using operations_research::GetSeed; -using operations_research::LocationContainer; -using operations_research::RandomDemand; -using operations_research::RoutingDimension; -using operations_research::RoutingIndexManager; -using operations_research::RoutingModel; -using operations_research::RoutingNodeIndex; -using operations_research::RoutingSearchParameters; -using operations_research::ServiceTimePlusTransition; using operations_research::Solver; +using operations_research::routing::DefaultRoutingSearchParameters; +using operations_research::routing::GetSeed; +using operations_research::routing::LocationContainer; +using operations_research::routing::RandomDemand; +using operations_research::routing::RoutingDimension; +using operations_research::routing::RoutingIndexManager; +using operations_research::routing::RoutingModel; +using operations_research::routing::RoutingNodeIndex; +using operations_research::routing::RoutingSearchParameters; +using operations_research::routing::ServiceTimePlusTransition; ABSL_FLAG(int, vrp_orders, 100, "Number of nodes in the problem."); ABSL_FLAG(int, vrp_vehicles, 20, "Number of vehicles in the problem."); @@ -74,6 +76,7 @@ const int64_t kSameVehicleCost = 1000; int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) << "Specify an instance size greater than 0."; CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) diff --git a/ortools/routing/samples/cvrp_reload.py b/ortools/routing/samples/cvrp_reload.py new file mode 100755 index 00000000000..fb31ebb1a2c --- /dev/null +++ b/ortools/routing/samples/cvrp_reload.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python3 +# This Python file uses the following encoding: utf-8 +# Copyright 2015 Tin Arm Engineering AB +# Copyright 2018 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Capacitated Vehicle Routing Problem (CVRP). + +This is a sample using the routing library python wrapper to solve a CVRP +problem while allowing multiple trips, i.e., vehicles can return to a depot +to reset their load ("reload"). + +A description of the CVRP problem can be found here: +http://en.wikipedia.org/wiki/Vehicle_routing_problem. + +Distances are in meters. + +In order to implement multiple trips, new nodes are introduced at the same +locations of the original depots. These additional nodes can be dropped +from the schedule at 0 cost. + +The max_slack parameter associated to the capacity constraints of all nodes +can be set to be the maximum of the vehicles' capacities, rather than 0 like +in a traditional CVRP. Slack is required since before a solution is found, +it is not known how much capacity will be transferred at the new nodes. For +all the other (original) nodes, the slack is then re-set to 0. + +The above two considerations are implemented in `add_capacity_constraints()`. + +Last, it is useful to set a large distance between the initial depot and the +new nodes introduced, to avoid schedules having spurious transits through +those new nodes unless it's necessary to reload. This consideration is taken +into account in `create_distance_evaluator()`. +""" + +from functools import partial + +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + + +########################### +# Problem Data Definition # +########################### +def create_data_model(): + """Stores the data for the problem""" + data = {} + _capacity = 15 + # Locations in block unit + _locations = [ + (4, 4), # depot + (4, 4), # unload depot_first + (4, 4), # unload depot_second + (4, 4), # unload depot_third + (4, 4), # unload depot_fourth + (4, 4), # unload depot_fifth + (2, 0), + (8, 0), # locations to visit + (0, 1), + (1, 1), + (5, 2), + (7, 2), + (3, 3), + (6, 3), + (5, 5), + (8, 5), + (1, 6), + (2, 6), + (3, 7), + (6, 7), + (0, 8), + (7, 8), + ] + # Compute locations in meters using the block dimension defined as follow + # Manhattan average block: 750ft x 264ft -> 228m x 80m + # here we use: 114m x 80m city block + # src: https://nyti.ms/2GDoRIe 'NY Times: Know Your distance' + data["locations"] = [(l[0] * 114, l[1] * 80) for l in _locations] + data["num_locations"] = len(data["locations"]) + data["demands"] = [ + 0, # depot + -_capacity, # unload depot_first + -_capacity, # unload depot_second + -_capacity, # unload depot_third + -_capacity, # unload depot_fourth + -_capacity, # unload depot_fifth + 3, + 3, # 1, 2 + 3, + 4, # 3, 4 + 3, + 4, # 5, 6 + 8, + 8, # 7, 8 + 3, + 3, # 9,10 + 3, + 3, # 11,12 + 4, + 4, # 13, 14 + 8, + 8, + ] # 15, 16 + data["time_per_demand_unit"] = 5 # 5 minutes/unit + data["time_windows"] = [ + (0, 0), # depot + (0, 1000), # unload depot_first + (0, 1000), # unload depot_second + (0, 1000), # unload depot_third + (0, 1000), # unload depot_fourth + (0, 1000), # unload depot_fifth + (75, 850), + (75, 850), # 1, 2 + (60, 700), + (45, 550), # 3, 4 + (0, 800), + (50, 600), # 5, 6 + (0, 1000), + (10, 200), # 7, 8 + (0, 1000), + (75, 850), # 9, 10 + (85, 950), + (5, 150), # 11, 12 + (15, 250), + (10, 200), # 13, 14 + (45, 550), + (30, 400), + ] # 15, 16 + data["num_vehicles"] = 3 + data["vehicle_capacity"] = _capacity + data["vehicle_max_distance"] = 10_000 + data["vehicle_max_time"] = 1_500 + data["vehicle_speed"] = ( + 5 * 60 / 3.6 + ) # Travel speed: 5km/h to convert in m/min + data["depot"] = 0 + return data + + +####################### +# Problem Constraints # +####################### +def manhattan_distance(position_1, position_2): + """Computes the Manhattan distance between two points""" + return abs(position_1[0] - position_2[0]) + abs(position_1[1] - position_2[1]) + + +def create_distance_evaluator(data): + """Creates callback to return distance between points.""" + _distances = {} + # precompute distance between location to have distance callback in O(1) + for from_node in range(data["num_locations"]): + _distances[from_node] = {} + for to_node in range(data["num_locations"]): + if from_node == to_node: + _distances[from_node][to_node] = 0 + # Forbid start/end/reload node to be consecutive. + elif from_node in range(6) and to_node in range(6): + _distances[from_node][to_node] = data["vehicle_max_distance"] + else: + _distances[from_node][to_node] = manhattan_distance( + data["locations"][from_node], data["locations"][to_node] + ) + + def distance_evaluator(manager, from_node, to_node): + """Returns the manhattan distance between the two nodes""" + return _distances[manager.IndexToNode(from_node)][ + manager.IndexToNode(to_node) + ] + + return distance_evaluator + + +def add_distance_dimension(routing, manager, data, distance_evaluator_index): + """Add Global Span constraint""" + del manager + distance = "Distance" + routing.AddDimension( + distance_evaluator_index, + 0, # null slack + data["vehicle_max_distance"], # maximum distance per vehicle + True, # start cumul to zero + distance, + ) + distance_dimension = routing.GetDimensionOrDie(distance) + # Try to minimize the max distance among vehicles. + # /!\ It doesn't mean the standard deviation is minimized + distance_dimension.SetGlobalSpanCostCoefficient(100) + + +def create_demand_evaluator(data): + """Creates callback to get demands at each location.""" + _demands = data["demands"] + + def demand_evaluator(manager, from_node): + """Returns the demand of the current node""" + return _demands[manager.IndexToNode(from_node)] + + return demand_evaluator + + +def add_capacity_constraints(routing, manager, data, demand_evaluator_index): + """Adds capacity constraint""" + vehicle_capacity = data["vehicle_capacity"] + capacity = "Capacity" + routing.AddDimension( + demand_evaluator_index, + vehicle_capacity, + vehicle_capacity, + True, # start cumul to zero + capacity, + ) + + # Add Slack for reseting to zero unload depot nodes. + # e.g. vehicle with load 10/15 arrives at node 1 (depot unload) + # so we have CumulVar = 10(current load) + -15(unload) + 5(slack) = 0. + capacity_dimension = routing.GetDimensionOrDie(capacity) + # Allow to drop reloading nodes with zero cost. + for node in [1, 2, 3, 4, 5]: + node_index = manager.NodeToIndex(node) + routing.AddDisjunction([node_index], 0) + + # Allow to drop regular node with a cost. + for node in range(6, len(data["demands"])): + node_index = manager.NodeToIndex(node) + capacity_dimension.SlackVar(node_index).SetValue(0) + routing.AddDisjunction([node_index], 100_000) + + +def create_time_evaluator(data): + """Creates callback to get total times between locations.""" + + def service_time(data, node): + """Gets the service time for the specified location.""" + return abs(data["demands"][node]) * data["time_per_demand_unit"] + + def travel_time(data, from_node, to_node): + """Gets the travel times between two locations.""" + if from_node == to_node: + travel_time = 0 + else: + travel_time = ( + manhattan_distance( + data["locations"][from_node], data["locations"][to_node] + ) + / data["vehicle_speed"] + ) + return travel_time + + _total_time = {} + # precompute total time to have time callback in O(1) + for from_node in range(data["num_locations"]): + _total_time[from_node] = {} + for to_node in range(data["num_locations"]): + if from_node == to_node: + _total_time[from_node][to_node] = 0 + else: + _total_time[from_node][to_node] = int( + service_time(data, from_node) + + travel_time(data, from_node, to_node) + ) + + def time_evaluator(manager, from_node, to_node): + """Returns the total time between the two nodes""" + return _total_time[manager.IndexToNode(from_node)][ + manager.IndexToNode(to_node) + ] + + return time_evaluator + + +def add_time_window_constraints(routing, manager, data, time_evaluator): + """Add Time windows constraint""" + time = "Time" + max_time = data["vehicle_max_time"] + routing.AddDimension( + time_evaluator, + max_time, # allow waiting time + max_time, # maximum time per vehicle + False, # don't force start cumul to zero since we are giving TW to start nodes + time, + ) + time_dimension = routing.GetDimensionOrDie(time) + # Add time window constraints for each location except depot + # and 'copy' the slack var in the solution object (aka Assignment) to print it + for location_idx, time_window in enumerate(data["time_windows"]): + if location_idx == 0: + continue + index = manager.NodeToIndex(location_idx) + time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) + routing.AddToAssignment(time_dimension.SlackVar(index)) + # Add time window constraints for each vehicle start node + # and 'copy' the slack var in the solution object (aka Assignment) to print it + for vehicle_id in range(data["num_vehicles"]): + index = routing.Start(vehicle_id) + time_dimension.CumulVar(index).SetRange( + data["time_windows"][0][0], data["time_windows"][0][1] + ) + routing.AddToAssignment(time_dimension.SlackVar(index)) + # Warning: Slack var is not defined for vehicle's end node + # routing.AddToAssignment(time_dimension.SlackVar(self.routing.End(vehicle_id))) + + +########### +# Printer # +########### +def print_solution( + data, manager, routing, assignment +): # pylint:disable=too-many-locals + """Prints assignment on console""" + print(f"Objective: {assignment.ObjectiveValue()}") + total_distance = 0 + total_load = 0 + total_time = 0 + capacity_dimension = routing.GetDimensionOrDie("Capacity") + time_dimension = routing.GetDimensionOrDie("Time") + distance_dimension = routing.GetDimensionOrDie("Distance") + dropped = [] + for order in range(6, routing.nodes()): + index = manager.NodeToIndex(order) + if assignment.Value(routing.NextVar(index)) == index: + dropped.append(order) + print(f"dropped orders: {dropped}") + dropped = [] + for reload in range(1, 6): + index = manager.NodeToIndex(reload) + if assignment.Value(routing.NextVar(index)) == index: + dropped.append(reload) + print(f"dropped reload stations: {dropped}") + + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(assignment, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + load_value = 0 + distance = 0 + while not routing.IsEnd(index): + time_var = time_dimension.CumulVar(index) + plan_output += ( + f" {manager.IndexToNode(index)} " + f"Load({assignment.Min(capacity_dimension.CumulVar(index))}) " + f"Time({assignment.Min(time_var)},{assignment.Max(time_var)}) ->" + ) + previous_index = index + index = assignment.Value(routing.NextVar(index)) + distance += distance_dimension.GetTransitValue( + previous_index, index, vehicle_id + ) + # capacity dimension TransitVar is negative at reload stations during replenishment + # don't want to consider those values when calculating the total load of the route + # hence only considering the positive values + load_value += max( + 0, + capacity_dimension.GetTransitValue(previous_index, index, vehicle_id), + ) + time_var = time_dimension.CumulVar(index) + plan_output += ( + f" {manager.IndexToNode(index)} " + f"Load({assignment.Min(capacity_dimension.CumulVar(index))}) " + f"Time({assignment.Min(time_var)},{assignment.Max(time_var)})\n" + ) + plan_output += f"Distance of the route: {distance}m\n" + plan_output += f"Load of the route: {load_value}\n" + plan_output += f"Time of the route: {assignment.Min(time_var)}min\n" + print(plan_output) + total_distance += distance + total_load += load_value + total_time += assignment.Min(time_var) + print(f"Total Distance of all routes: {total_distance}m") + print(f"Total Load of all routes: {total_load}") + print(f"Total Time of all routes: {total_time}min") + + +######## +# Main # +######## +def main(): + """Entry point of the program""" + # Instantiate the data problem. + data = create_data_model() + + # Create the routing index manager + manager = pywraprouting.RoutingIndexManager( + data["num_locations"], data["num_vehicles"], data["depot"] + ) + + # Create Routing Model + routing = pywraprouting.RoutingModel(manager) + + # Define weight of each edge + distance_evaluator_index = routing.RegisterTransitCallback( + partial(create_distance_evaluator(data), manager) + ) + routing.SetArcCostEvaluatorOfAllVehicles(distance_evaluator_index) + + # Add Distance constraint to minimize the longuest route + add_distance_dimension(routing, manager, data, distance_evaluator_index) + + # Add Capacity constraint + demand_evaluator_index = routing.RegisterUnaryTransitCallback( + partial(create_demand_evaluator(data), manager) + ) + add_capacity_constraints(routing, manager, data, demand_evaluator_index) + + # Add Time Window constraint + time_evaluator_index = routing.RegisterTransitCallback( + partial(create_time_evaluator(data), manager) + ) + add_time_window_constraints(routing, manager, data, time_evaluator_index) + + # Setting first solution heuristic (cheapest addition). + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) # pylint: disable=no-member + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(3) + + # Solve the problem. + solution = routing.SolveWithParameters(search_parameters) + if solution: + print_solution(data, manager, routing, solution) + else: + print("No solution found !") + + +if __name__ == "__main__": + main() diff --git a/ortools/routing/samples/cvrptw.cc b/ortools/routing/samples/cvrptw.cc index 95efaf8f038..b1ab211b2e3 100644 --- a/ortools/routing/samples/cvrptw.cc +++ b/ortools/routing/samples/cvrptw.cc @@ -28,30 +28,32 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" +#include "absl/log/globals.h" #include "absl/random/random.h" #include "google/protobuf/text_format.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/cvrptw_lib.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" using operations_research::Assignment; -using operations_research::DefaultRoutingSearchParameters; -using operations_research::GetSeed; -using operations_research::LocationContainer; -using operations_research::RandomDemand; -using operations_research::RoutingDimension; -using operations_research::RoutingIndexManager; -using operations_research::RoutingModel; -using operations_research::RoutingNodeIndex; -using operations_research::RoutingSearchParameters; -using operations_research::ServiceTimePlusTransition; +using operations_research::routing::DefaultRoutingSearchParameters; +using operations_research::routing::GetSeed; +using operations_research::routing::LocationContainer; +using operations_research::routing::RandomDemand; +using operations_research::routing::RoutingDimension; +using operations_research::routing::RoutingIndexManager; +using operations_research::routing::RoutingModel; +using operations_research::routing::RoutingNodeIndex; +using operations_research::routing::RoutingSearchParameters; +using operations_research::routing::ServiceTimePlusTransition; ABSL_FLAG(int, vrp_orders, 100, "Number of nodes in the problem"); ABSL_FLAG(int, vrp_vehicles, 20, "Number of vehicles in the problem"); @@ -70,6 +72,7 @@ const int64_t kSameVehicleCost = 1000; int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) << "Specify an instance size greater than 0."; CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) diff --git a/ortools/routing/samples/cvrptw.py b/ortools/routing/samples/cvrptw.py new file mode 100644 index 00000000000..97f4fde8e12 --- /dev/null +++ b/ortools/routing/samples/cvrptw.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Capacited Vehicles Routing Problem with Time Windows (CVRPTW). + +This is a sample using the routing library python wrapper to solve a VRP +problem. +A description of the problem can be found here: +http://en.wikipedia.org/wiki/Vehicle_routing_problem. + +Distances are in meters. +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy +LocalSearchMetaheuristic = enums_pb2.LocalSearchMetaheuristic +RoutingSearchStatus = enums_pb2.RoutingSearchStatus +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + # [START time_windows] + data["time_matrix"] = [ + [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], + [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], + [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], + [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], + [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], + [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], + [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], + [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], + [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], + [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], + [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], + [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], + [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], + [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], + [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], + [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], + [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], + ] + data["time_windows"] = [ + (0, 30), # depot + (7, 12), # 1 + (10, 15), # 2 + (16, 18), # 3 + (10, 13), # 4 + (0, 5), # 5 + (5, 10), # 6 + (0, 4), # 7 + (5, 10), # 8 + (0, 3), # 9 + (10, 16), # 10 + (10, 15), # 11 + (0, 5), # 12 + (5, 10), # 13 + (7, 8), # 14 + (10, 15), # 15 + (11, 15), # 16 + ] + assert len(data["distance_matrix"]) == len(data["time_matrix"]) + assert len(data["time_matrix"]) == len(data["time_windows"]) + # [END time_windows] + # [START demands_capacities] + data["demands"] = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8] + assert len(data["distance_matrix"]) == len(data["demands"]) + data["vehicle_capacities"] = [15, 15, 15, 15] + # [END demands_capacities] + data["num_vehicles"] = len(data["vehicle_capacities"]) + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(manager, routing, solution): + """Prints solution on console.""" + status = routing.status() + print(f"Status: {RoutingSearchStatus.Value.Name(status)}") + if ( + status != RoutingSearchStatus.ROUTING_OPTIMAL + and status != RoutingSearchStatus.ROUTING_SUCCESS + ): + print("No solution found!") + return + print(f"Objective: {solution.ObjectiveValue()}") + time_dimension = routing.GetDimensionOrDie("Time") + capacity_dimension = routing.GetDimensionOrDie("Capacity") + total_distance = 0 + total_time = 0 + total_load = 0 + for vehicle_id in range(manager.GetNumberOfVehicles()): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing.IsEnd(index): + time_var = time_dimension.CumulVar(index) + capacity_var = capacity_dimension.CumulVar(index) + plan_output += ( + f"Node_{manager.IndexToNode(index)}" + f" TW:[{time_var.Min()},{time_var.Max()}]" + f" Time({solution.Min(time_var)},{solution.Max(time_var)})" + f" Load({solution.Value(capacity_var)}/{capacity_var.Max()})" + " -> " + ) + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + time_var = time_dimension.CumulVar(index) + capacity_var = capacity_dimension.CumulVar(index) + plan_output += ( + f"Node_{manager.IndexToNode(index)}" + f" Time({solution.Min(time_var)},{solution.Max(time_var)})" + f" Load({solution.Value(capacity_var)}/{capacity_var.Max()})" + "\n" + ) + plan_output += f"Distance of the route: {route_distance}m\n" + plan_output += f"Time of the route: {solution.Min(time_var)}min\n" + plan_output += f"Load of the route: {solution.Value(capacity_var)}\n" + print(plan_output) + total_distance += route_distance + total_time += solution.Min(time_var) + total_load += solution.Value(capacity_var) + print(f"Total distance of all routes: {total_distance}m") + print(f"Total time of all routes: {total_time}min") + print(f"Total load of all routes: {total_load}") + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a distance transit callback. + # [START distance_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + distance_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END distance_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(distance_callback_index) + # [END arc_cost] + + # Add Time Windows constraint. + # [START time_windows_constraint] + def time_callback(from_index, to_index): + """Returns the travel time between the two nodes.""" + # Convert from routing variable Index to time matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["time_matrix"][from_node][to_node] + + time_callback_index = routing.RegisterTransitCallback(time_callback) + routing.AddDimension( + time_callback_index, + 30, # allow waiting time + 30, # maximum time per vehicle + False, # Don't force start cumul to zero. + "Time", + ) + time_dimension = routing.GetDimensionOrDie("Time") + # Add time window constraints for each location except depot. + for location_idx, time_window in enumerate(data["time_windows"]): + if location_idx == data["depot"]: + continue + index = manager.NodeToIndex(location_idx) + time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) + # Add time window constraints for each vehicle start node. + depot_idx = data["depot"] + for vehicle_id in range(data["num_vehicles"]): + index = routing.Start(vehicle_id) + time_dimension.CumulVar(index).SetRange( + data["time_windows"][depot_idx][0], data["time_windows"][depot_idx][1] + ) + # [END time_windows_constraint] + + # Instantiate route start and end times to produce feasible times. + # [START depot_start_end_times] + for i in range(data["num_vehicles"]): + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.Start(i)) + ) + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.End(i)) + ) + # [END depot_start_end_times] + + # Add Capacity constraint. + # [START capacity_constraint] + def demand_callback(from_index): + """Returns the demand of the node.""" + # Convert from routing variable Index to demands NodeIndex. + from_node = manager.IndexToNode(from_index) + return data["demands"][from_node] + + demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback) + routing.AddDimensionWithVehicleCapacity( + demand_callback_index, + 0, # null capacity slack + data["vehicle_capacities"], # vehicle maximum capacities + True, # start cumul to zero + "Capacity", + ) + # [END capacity_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION + ) + search_parameters.local_search_metaheuristic = ( + LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(3) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + print_solution(manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/routing/samples/cvrptw_break.py b/ortools/routing/samples/cvrptw_break.py new file mode 100755 index 00000000000..32f571edbfe --- /dev/null +++ b/ortools/routing/samples/cvrptw_break.py @@ -0,0 +1,373 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Capacitated Vehicle Routing Problem with Time Windows (CVRPTW). + +This is a sample using the routing library python wrapper to solve a CVRPTW +problem. +A description of the problem can be found here: +http://en.wikipedia.org/wiki/Vehicle_routing_problem. + +Distances are in meters and time in minutes. +""" + +# [START import] +import functools +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + # Locations in block unit + locations_ = [ + # fmt: off + (4, 4), # depot + (2, 0), (8, 0), # locations to visit + (0, 1), (1, 1), + (5, 2), (7, 2), + (3, 3), (6, 3), + (5, 5), (8, 5), + (1, 6), (2, 6), + (3, 7), (6, 7), + (0, 8), (7, 8), + # fmt: on + ] + # Compute locations in meters using the block dimension defined as follow + # Manhattan average block: 750ft x 264ft -> 228m x 80m + # here we use: 114m x 80m city block + # src: https://nyti.ms/2GDoRIe "NY Times: Know Your distance" + data["locations"] = [(l[0] * 114, l[1] * 80) for l in locations_] + data["numlocations_"] = len(data["locations"]) + data["time_windows"] = [ + # fmt: off + (0, 0), # depot + (75, 85), (75, 85), # 1, 2 + (60, 70), (45, 55), # 3, 4 + (0, 8), (50, 60), # 5, 6 + (0, 10), (10, 20), # 7, 8 + (0, 10), (75, 85), # 9, 10 + (85, 95), (5, 15), # 11, 12 + (15, 25), (10, 20), # 13, 14 + (45, 55), (30, 40), + # 15, 16 + # fmt: on + ] + data["demands"] = [ + # fmt: off + 0, # depot + 1, 1, # 1, 2 + 2, 4, # 3, 4 + 2, 4, # 5, 6 + 8, 8, # 7, 8 + 1, 2, # 9, 10 + 1, 2, # 11, 12 + 4, 4, # 13, 14 + 8, 8, + # 15, 16 + # fmt: on + ] + data["time_per_demand_unit"] = 5 # 5 minutes/unit + data["num_vehicles"] = 4 + data["breaks"] = [(2, False), (2, False), (2, False), (2, False)] + data["vehicle_capacity"] = 15 + data["vehicle_speed"] = 83 # Travel speed: 5km/h converted in m/min + data["depot"] = 0 + return data + # [END data_model] + + +def manhattan_distance(position_1, position_2): + """Computes the Manhattan distance between two points.""" + return abs(position_1[0] - position_2[0]) + abs(position_1[1] - position_2[1]) + + +def create_distance_evaluator(data): + """Creates callback to return distance between points.""" + distances_ = {} + # precompute distance between location to have distance callback in O(1) + for from_node in range(data["numlocations_"]): + distances_[from_node] = {} + for to_node in range(data["numlocations_"]): + if from_node == to_node: + distances_[from_node][to_node] = 0 + else: + distances_[from_node][to_node] = manhattan_distance( + data["locations"][from_node], data["locations"][to_node] + ) + + def distance_evaluator(manager, from_node, to_node): + """Returns the manhattan distance between the two nodes.""" + return distances_[manager.IndexToNode(from_node)][ + manager.IndexToNode(to_node) + ] + + return distance_evaluator + + +def create_demand_evaluator(data): + """Creates callback to get demands at each location.""" + demands_ = data["demands"] + + def demand_evaluator(manager, node): + """Returns the demand of the current node.""" + return demands_[manager.IndexToNode(node)] + + return demand_evaluator + + +def add_capacity_constraints(routing, data, demand_evaluator_index): + """Adds capacity constraint.""" + capacity = "Capacity" + routing.AddDimension( + demand_evaluator_index, + 0, # null capacity slack + data["vehicle_capacity"], + True, # start cumul to zero + capacity, + ) + + +def create_time_evaluator(data): + """Creates callback to get total times between locations.""" + + def service_time(data, node): + """Gets the service time for the specified location.""" + return data["demands"][node] * data["time_per_demand_unit"] + + def travel_time(data, from_node, to_node): + """Gets the travel times between two locations.""" + if from_node == to_node: + travel_time = 0 + else: + travel_time = ( + manhattan_distance( + data["locations"][from_node], data["locations"][to_node] + ) + / data["vehicle_speed"] + ) + return travel_time + + total_time_ = {} + # precompute total time to have time callback in O(1) + for from_node in range(data["numlocations_"]): + total_time_[from_node] = {} + for to_node in range(data["numlocations_"]): + if from_node == to_node: + total_time_[from_node][to_node] = 0 + else: + total_time_[from_node][to_node] = int( + service_time(data, from_node) + + travel_time(data, from_node, to_node) + ) + + def time_evaluator(manager, from_node, to_node): + """Returns the total time between the two nodes.""" + return total_time_[manager.IndexToNode(from_node)][ + manager.IndexToNode(to_node) + ] + + return time_evaluator + + +def add_time_window_constraints(routing, manager, data, time_evaluator_index): + """Add Global Span constraint.""" + time = "Time" + horizon = 120 + routing.AddDimension( + time_evaluator_index, + horizon, # allow waiting time + horizon, # maximum time per vehicle + False, # don't force start cumul to zero + time, + ) + time_dimension = routing.GetDimensionOrDie(time) + # Add time window constraints for each location except depot + # and 'copy' the slack var in the solution object (aka Assignment) to print it + for location_idx, time_window in enumerate(data["time_windows"]): + if location_idx == data["depot"]: + continue + index = manager.NodeToIndex(location_idx) + time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) + routing.AddToAssignment(time_dimension.SlackVar(index)) + # Add time window constraints for each vehicle start node + # and 'copy' the slack var in the solution object (aka Assignment) to print it + for vehicle_id in range(data["num_vehicles"]): + index = routing.Start(vehicle_id) + time_dimension.CumulVar(index).SetRange( + data["time_windows"][0][0], data["time_windows"][0][1] + ) + routing.AddToAssignment(time_dimension.SlackVar(index)) + # The time window at the end node was impliclty set in the time dimension + # definition to be [0, horizon]. + # Warning: Slack var is not defined for vehicle end nodes and should not + # be added to the assignment. + + +# [START solution_printer] +def print_solution( + data, manager, routing, assignment +): # pylint:disable=too-many-locals + """Prints assignment on console.""" + print(f"Objective: {assignment.ObjectiveValue()}") + + print("Breaks:") + intervals = assignment.IntervalVarContainer() + for i in range(intervals.Size()): + brk = intervals.Element(i) + if brk.PerformedValue() == 1: + print( + f"{brk.Var().Name()}:" + f" Start({brk.StartValue()}) Duration({brk.DurationValue()})" + ) + else: + print(f"{brk.Var().Name()}: Unperformed") + + total_distance = 0 + total_load = 0 + total_time = 0 + capacity_dimension = routing.GetDimensionOrDie("Capacity") + time_dimension = routing.GetDimensionOrDie("Time") + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(assignment, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + distance = 0 + while not routing.IsEnd(index): + load_var = capacity_dimension.CumulVar(index) + time_var = time_dimension.CumulVar(index) + slack_var = time_dimension.SlackVar(index) + node = manager.IndexToNode(index) + plan_output += ( + f" {node}" + f" Load({assignment.Value(load_var)})" + f" Time({assignment.Min(time_var)}, {assignment.Max(time_var)})" + f" Slack({assignment.Min(slack_var)}, {assignment.Max(slack_var)})" + " ->" + ) + previous_index = index + index = assignment.Value(routing.NextVar(index)) + distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + load_var = capacity_dimension.CumulVar(index) + time_var = time_dimension.CumulVar(index) + node = manager.IndexToNode(index) + plan_output += ( + f" {node}" + f" Load({assignment.Value(load_var)})" + f" Time({assignment.Min(time_var)}, {assignment.Max(time_var)})\n" + ) + plan_output += f"Distance of the route: {distance}m\n" + plan_output += f"Load of the route: {assignment.Value(load_var)}\n" + plan_output += f"Time of the route: {assignment.Value(time_var)}\n" + print(plan_output) + total_distance += distance + total_load += assignment.Value(load_var) + total_time += assignment.Value(time_var) + print(f"Total Distance of all routes: {total_distance}m") + print(f"Total Load of all routes: {total_load}") + print(f"Total Time of all routes: {total_time}min") + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager + manager = pywraprouting.RoutingIndexManager( + data["numlocations_"], data["num_vehicles"], data["depot"] + ) + + # Create Routing Model + routing = pywraprouting.RoutingModel(manager) + + # Define weight of each edge + distance_evaluator_index = routing.RegisterTransitCallback( + functools.partial(create_distance_evaluator(data), manager) + ) + routing.SetArcCostEvaluatorOfAllVehicles(distance_evaluator_index) + + # Add Capacity constraint + demand_evaluator_index = routing.RegisterUnaryTransitCallback( + functools.partial(create_demand_evaluator(data), manager) + ) + add_capacity_constraints(routing, data, demand_evaluator_index) + + # Add Time Window constraint + time_evaluator_index = routing.RegisterTransitCallback( + functools.partial(create_time_evaluator(data), manager) + ) + add_time_window_constraints(routing, manager, data, time_evaluator_index) + + # Add breaks + time_dimension = routing.GetDimensionOrDie("Time") + node_visit_transit = {} + for index in range(routing.Size()): + node = manager.IndexToNode(index) + node_visit_transit[index] = int( + data["demands"][node] * data["time_per_demand_unit"] + ) + + break_intervals = {} + for v in range(data["num_vehicles"]): + vehicle_break = data["breaks"][v] + break_intervals[v] = [ + routing.solver().FixedDurationIntervalVar( + 15, + 100, + vehicle_break[0], + vehicle_break[1], + f"Break for vehicle {v}", + ) + ] + time_dimension.SetBreakIntervalsOfVehicle( + break_intervals[v], v, node_visit_transit.values() + ) + + # Setting first solution heuristic (cheapest addition). + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) # pylint: disable=no-member + # [END parameters] + + # Solve the problem. + # [START solve] + assignment = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if assignment: + print_solution(data, manager, routing, assignment) + else: + print("No solution found!") + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/routing/samples/cvrptw_soft_capacity.cc b/ortools/routing/samples/cvrptw_soft_capacity.cc index d6e3d9c5565..26db8df7406 100644 --- a/ortools/routing/samples/cvrptw_soft_capacity.cc +++ b/ortools/routing/samples/cvrptw_soft_capacity.cc @@ -27,30 +27,32 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" +#include "absl/log/globals.h" #include "absl/random/random.h" #include "google/protobuf/text_format.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/cvrptw_lib.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" using operations_research::Assignment; -using operations_research::DefaultRoutingSearchParameters; -using operations_research::GetSeed; -using operations_research::LocationContainer; -using operations_research::RandomDemand; -using operations_research::RoutingDimension; -using operations_research::RoutingIndexManager; -using operations_research::RoutingModel; -using operations_research::RoutingNodeIndex; -using operations_research::RoutingSearchParameters; -using operations_research::ServiceTimePlusTransition; +using operations_research::routing::DefaultRoutingSearchParameters; +using operations_research::routing::GetSeed; +using operations_research::routing::LocationContainer; +using operations_research::routing::RandomDemand; +using operations_research::routing::RoutingDimension; +using operations_research::routing::RoutingIndexManager; +using operations_research::routing::RoutingModel; +using operations_research::routing::RoutingNodeIndex; +using operations_research::routing::RoutingSearchParameters; +using operations_research::routing::ServiceTimePlusTransition; ABSL_FLAG(int, vrp_orders, 100, "Number of nodes in the problem."); ABSL_FLAG(int, vrp_vehicles, 20, "Number of vehicles in the problem."); @@ -78,6 +80,7 @@ const int64_t kSameVehicleCost = 1000; int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) << "Specify an instance size greater than 0."; CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) diff --git a/ortools/routing/samples/cvrptw_with_breaks.cc b/ortools/routing/samples/cvrptw_with_breaks.cc index c6b7e6a73f9..791be7a2009 100644 --- a/ortools/routing/samples/cvrptw_with_breaks.cc +++ b/ortools/routing/samples/cvrptw_with_breaks.cc @@ -32,35 +32,37 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" +#include "absl/log/globals.h" #include "absl/random/random.h" #include "absl/strings/str_cat.h" #include "google/protobuf/text_format.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/cvrptw_lib.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" using operations_research::Assignment; -using operations_research::DefaultRoutingSearchParameters; -using operations_research::FirstSolutionStrategy; -using operations_research::GetSeed; using operations_research::IntervalVar; -using operations_research::LocationContainer; -using operations_research::RandomDemand; -using operations_research::RoutingDimension; -using operations_research::RoutingIndexManager; -using operations_research::RoutingModel; -using operations_research::RoutingNodeIndex; -using operations_research::RoutingSearchParameters; -using operations_research::ServiceTimePlusTransition; using operations_research::Solver; +using operations_research::routing::DefaultRoutingSearchParameters; +using operations_research::routing::FirstSolutionStrategy; +using operations_research::routing::GetSeed; +using operations_research::routing::LocationContainer; +using operations_research::routing::RandomDemand; +using operations_research::routing::RoutingDimension; +using operations_research::routing::RoutingIndexManager; +using operations_research::routing::RoutingModel; +using operations_research::routing::RoutingNodeIndex; +using operations_research::routing::RoutingSearchParameters; +using operations_research::routing::ServiceTimePlusTransition; ABSL_FLAG(int, vrp_orders, 100, "Nodes in the problem."); ABSL_FLAG(int, vrp_vehicles, 20, @@ -76,6 +78,7 @@ const char* kCapacity = "Capacity"; int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) << "Specify an instance size greater than 0."; CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) diff --git a/ortools/routing/samples/cvrptw_with_precedences.cc b/ortools/routing/samples/cvrptw_with_precedences.cc index a4bbcc3904e..bbf3f40d560 100644 --- a/ortools/routing/samples/cvrptw_with_precedences.cc +++ b/ortools/routing/samples/cvrptw_with_precedences.cc @@ -27,31 +27,33 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" +#include "absl/log/globals.h" #include "absl/random/random.h" #include "google/protobuf/text_format.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/graph/graph_builder.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/cvrptw_lib.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" using operations_research::Assignment; -using operations_research::DefaultRoutingSearchParameters; -using operations_research::GetSeed; -using operations_research::LocationContainer; -using operations_research::RandomDemand; -using operations_research::RoutingDimension; -using operations_research::RoutingIndexManager; -using operations_research::RoutingModel; -using operations_research::RoutingNodeIndex; -using operations_research::RoutingSearchParameters; -using operations_research::ServiceTimePlusTransition; +using operations_research::routing::DefaultRoutingSearchParameters; +using operations_research::routing::GetSeed; +using operations_research::routing::LocationContainer; +using operations_research::routing::RandomDemand; +using operations_research::routing::RoutingDimension; +using operations_research::routing::RoutingIndexManager; +using operations_research::routing::RoutingModel; +using operations_research::routing::RoutingNodeIndex; +using operations_research::routing::RoutingSearchParameters; +using operations_research::routing::ServiceTimePlusTransition; ABSL_FLAG(int, vrp_orders, 100, "Nodes in the problem."); ABSL_FLAG(int, vrp_vehicles, 20, @@ -78,6 +80,7 @@ const int64_t kSameVehicleCost = 1000; int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) << "Specify an instance size greater than 0."; CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) diff --git a/ortools/routing/samples/cvrptw_with_refueling.cc b/ortools/routing/samples/cvrptw_with_refueling.cc index b019679dccf..0a21a3e1d12 100644 --- a/ortools/routing/samples/cvrptw_with_refueling.cc +++ b/ortools/routing/samples/cvrptw_with_refueling.cc @@ -25,31 +25,33 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" +#include "absl/log/globals.h" #include "absl/random/random.h" #include "google/protobuf/text_format.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/cvrptw_lib.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" using operations_research::Assignment; -using operations_research::DefaultRoutingSearchParameters; -using operations_research::GetSeed; -using operations_research::LocationContainer; -using operations_research::RandomDemand; -using operations_research::RoutingDimension; -using operations_research::RoutingIndexManager; -using operations_research::RoutingModel; -using operations_research::RoutingNodeIndex; -using operations_research::RoutingSearchParameters; -using operations_research::ServiceTimePlusTransition; using operations_research::Solver; +using operations_research::routing::DefaultRoutingSearchParameters; +using operations_research::routing::GetSeed; +using operations_research::routing::LocationContainer; +using operations_research::routing::RandomDemand; +using operations_research::routing::RoutingDimension; +using operations_research::routing::RoutingIndexManager; +using operations_research::routing::RoutingModel; +using operations_research::routing::RoutingNodeIndex; +using operations_research::routing::RoutingSearchParameters; +using operations_research::routing::ServiceTimePlusTransition; ABSL_FLAG(int, vrp_orders, 20, "Nodes in the problem."); ABSL_FLAG(int, vrp_vehicles, 4, @@ -72,6 +74,7 @@ bool IsRefuelNode(int64_t node) { int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) << "Specify an instance size greater than 0."; CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) diff --git a/ortools/routing/samples/cvrptw_with_resources.cc b/ortools/routing/samples/cvrptw_with_resources.cc index ec62249c669..49312320d9b 100644 --- a/ortools/routing/samples/cvrptw_with_resources.cc +++ b/ortools/routing/samples/cvrptw_with_resources.cc @@ -27,33 +27,35 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" +#include "absl/log/globals.h" #include "absl/random/random.h" #include "google/protobuf/text_format.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/cvrptw_lib.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" using operations_research::Assignment; -using operations_research::DefaultRoutingSearchParameters; -using operations_research::GetSeed; using operations_research::IntervalVar; using operations_research::IntVar; -using operations_research::LocationContainer; -using operations_research::RandomDemand; -using operations_research::RoutingDimension; -using operations_research::RoutingIndexManager; -using operations_research::RoutingModel; -using operations_research::RoutingNodeIndex; -using operations_research::RoutingSearchParameters; -using operations_research::ServiceTimePlusTransition; using operations_research::Solver; +using operations_research::routing::DefaultRoutingSearchParameters; +using operations_research::routing::GetSeed; +using operations_research::routing::LocationContainer; +using operations_research::routing::RandomDemand; +using operations_research::routing::RoutingDimension; +using operations_research::routing::RoutingIndexManager; +using operations_research::routing::RoutingModel; +using operations_research::routing::RoutingNodeIndex; +using operations_research::routing::RoutingSearchParameters; +using operations_research::routing::ServiceTimePlusTransition; ABSL_FLAG(int, vrp_orders, 100, "Nodes in the problem."); ABSL_FLAG(int, vrp_vehicles, 20, @@ -69,6 +71,7 @@ const char* kCapacity = "Capacity"; int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) << "Specify an instance size greater than 0."; CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) diff --git a/ortools/routing/samples/cvrptw_with_stop_times_and_resources.cc b/ortools/routing/samples/cvrptw_with_stop_times_and_resources.cc index 2e9cdab550f..3d65ab79838 100644 --- a/ortools/routing/samples/cvrptw_with_stop_times_and_resources.cc +++ b/ortools/routing/samples/cvrptw_with_stop_times_and_resources.cc @@ -25,34 +25,36 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" +#include "absl/log/globals.h" #include "absl/random/random.h" #include "absl/strings/str_cat.h" #include "google/protobuf/text_format.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/cvrptw_lib.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" using operations_research::Assignment; -using operations_research::DefaultRoutingSearchParameters; -using operations_research::GetSeed; using operations_research::IntervalVar; using operations_research::IntVar; -using operations_research::LocationContainer; -using operations_research::RandomDemand; -using operations_research::RoutingDimension; -using operations_research::RoutingIndexManager; -using operations_research::RoutingModel; -using operations_research::RoutingNodeIndex; -using operations_research::RoutingSearchParameters; using operations_research::Solver; -using operations_research::StopServiceTimePlusTransition; +using operations_research::routing::DefaultRoutingSearchParameters; +using operations_research::routing::GetSeed; +using operations_research::routing::LocationContainer; +using operations_research::routing::RandomDemand; +using operations_research::routing::RoutingDimension; +using operations_research::routing::RoutingIndexManager; +using operations_research::routing::RoutingModel; +using operations_research::routing::RoutingNodeIndex; +using operations_research::routing::RoutingSearchParameters; +using operations_research::routing::StopServiceTimePlusTransition; ABSL_FLAG(int, vrp_stops, 25, "Stop locations in the problem."); ABSL_FLAG(int, vrp_orders_per_stop, 5, "Nodes for each stop."); @@ -69,6 +71,7 @@ const char* kCapacity = "Capacity"; int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); CHECK_LT(0, absl::GetFlag(FLAGS_vrp_stops)) << "Specify an instance size greater than 0."; CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders_per_stop)) diff --git a/ortools/routing/samples/cvrptw_with_time_dependent_costs.cc b/ortools/routing/samples/cvrptw_with_time_dependent_costs.cc index e812e08f1f7..6c37c4e2b8f 100644 --- a/ortools/routing/samples/cvrptw_with_time_dependent_costs.cc +++ b/ortools/routing/samples/cvrptw_with_time_dependent_costs.cc @@ -21,34 +21,35 @@ #include #include +#include "absl/base/log_severity.h" #include "absl/flags/flag.h" #include "absl/functional/bind_front.h" +#include "absl/log/globals.h" #include "absl/random/random.h" #include "google/protobuf/text_format.h" #include "ortools/base/init_google.h" #include "ortools/base/logging.h" -#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/parameters.pb.h" #include "ortools/routing/parsers/cvrptw_lib.h" -#include "ortools/util/range_query_function.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" #include "ortools/util/step_function.h" using operations_research::Assignment; -using operations_research::DefaultRoutingSearchParameters; -using operations_research::GetSeed; -using operations_research::LocationContainer; -using operations_research::RandomDemand; -using operations_research::RoutingDimension; -using operations_research::RoutingIndexManager; -using operations_research::RoutingModel; -using operations_research::RoutingNodeIndex; -using operations_research::RoutingSearchParameters; -using operations_research::ServiceTimePlusTransition; using operations_research::StepFunction; +using operations_research::routing::DefaultRoutingSearchParameters; +using operations_research::routing::GetSeed; +using operations_research::routing::LocationContainer; +using operations_research::routing::RandomDemand; +using operations_research::routing::RoutingDimension; +using operations_research::routing::RoutingIndexManager; +using operations_research::routing::RoutingModel; +using operations_research::routing::RoutingNodeIndex; +using operations_research::routing::RoutingSearchParameters; +using operations_research::routing::ServiceTimePlusTransition; ABSL_FLAG(int, vrp_orders, 25, "Nodes in the problem."); ABSL_FLAG(int, vrp_vehicles, 10, @@ -146,6 +147,7 @@ class TrafficTransitionEvaluator { int main(int argc, char** argv) { InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); CHECK_LT(0, absl::GetFlag(FLAGS_vrp_orders)) << "Specify an instance size greater than 0."; CHECK_LT(0, absl::GetFlag(FLAGS_vrp_vehicles)) diff --git a/ortools/constraint_solver/samples/simple_routing_program.cc b/ortools/routing/samples/simple_routing_program.cc similarity index 82% rename from ortools/constraint_solver/samples/simple_routing_program.cc rename to ortools/routing/samples/simple_routing_program.cc index 482fdf9d3ac..69b966ec5b7 100644 --- a/ortools/constraint_solver/samples/simple_routing_program.cc +++ b/ortools/routing/samples/simple_routing_program.cc @@ -18,13 +18,18 @@ #include #include -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" +#include "ortools/constraint_solver/constraint_solver.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { void SimpleRoutingProgram() { // Instantiate the data problem. @@ -88,10 +93,12 @@ void SimpleRoutingProgram() { // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::SimpleRoutingProgram(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::SimpleRoutingProgram(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/simple_routing_program.py b/ortools/routing/samples/simple_routing_program.py new file mode 100644 index 00000000000..818dca36e6f --- /dev/null +++ b/ortools/routing/samples/simple_routing_program.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Vehicle Routing example.""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + num_locations = 5 + num_vehicles = 1 + depot = 0 + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + num_locations, num_vehicles, depot + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the absolute difference between the two nodes.""" + # Convert from routing variable Index to user NodeIndex. + from_node = int(manager.IndexToNode(from_index)) + to_node = int(manager.IndexToNode(to_index)) + return abs(to_node - from_node) + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) # pylint: disable=no-member + # [END parameters] + + # Solve the problem. + # [START solve] + assignment = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + print(f"Objective: {assignment.ObjectiveValue()}") + index = routing.Start(0) + plan_output = "Route for vehicle 0:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f"{manager.IndexToNode(index)} -> " + previous_index = index + index = assignment.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) + plan_output += f"{manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/tsp.cc b/ortools/routing/samples/tsp.cc similarity index 90% rename from ortools/constraint_solver/samples/tsp.cc rename to ortools/routing/samples/tsp.cc index 3ed91c45cdb..739762e76db 100644 --- a/ortools/constraint_solver/samples/tsp.cc +++ b/ortools/routing/samples/tsp.cc @@ -18,15 +18,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> locations{ @@ -152,10 +155,12 @@ void Tsp() { PrintSolution(manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::Tsp(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::Tsp(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/tsp.py b/ortools/routing/samples/tsp.py new file mode 100644 index 00000000000..3fdaeab732e --- /dev/null +++ b/ortools/routing/samples/tsp.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Travelling Salesman Problem. + +A description of the problem can be found here: +http://en.wikipedia.org/wiki/Travelling_salesperson_problem. +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import parameters_pb2 +from ortools.routing.python import model + +FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy +RoutingSearchStatus = enums_pb2.RoutingSearchStatus +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + # Locations in block units + locations = [ + # fmt:off + (4, 4), # depot + (2, 0), (8, 0), # locations to visit + (0, 1), (1, 1), + (5, 2), (7, 2), + (3, 3), (6, 3), + (5, 5), (8, 5), + (1, 6), (2, 6), + (3, 7), (6, 7), + (0, 8), (7, 8) + # fmt:on + ] + # Convert locations in meters using a city block dimension of 114m x 80m. + data["locations"] = [(l[0] * 114, l[1] * 80) for l in locations] + data["num_vehicles"] = 1 + data["depot"] = 0 + return data + # [END data_model] + + +# [START distance_callback] +def create_distance_callback(data, manager): + """Creates callback to return distance between points.""" + distances_ = {} + index_manager_ = manager + # precompute distance between location to have distance callback in O(1) + for from_counter, from_node in enumerate(data["locations"]): + distances_[from_counter] = {} + for to_counter, to_node in enumerate(data["locations"]): + if from_counter == to_counter: + distances_[from_counter][to_counter] = 0 + else: + distances_[from_counter][to_counter] = abs( + from_node[0] - to_node[0] + ) + abs(from_node[1] - to_node[1]) + + def distance_callback(from_index, to_index): + """Returns the manhattan distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = index_manager_.index_to_node(from_index) + to_node = index_manager_.index_to_node(to_index) + return distances_[from_node][to_node] + + return distance_callback + # [END distance_callback] + + +# [START solution_printer] +def print_solution(manager, routing, solution): + """Prints assignment on console.""" + status = routing.status() + print(f"Status: {RoutingSearchStatus.Value.Name(status)}") + if ( + status != RoutingSearchStatus.ROUTING_OPTIMAL + and status != RoutingSearchStatus.ROUTING_SUCCESS + ): + print("No solution found!") + return + print(f"Objective: {solution.objective_value()}") + index = routing.start(0) + plan_output = "Route for vehicle 0:\n" + route_distance = 0 + while not routing.is_end(index): + plan_output += f" {manager.index_to_node(index)} ->" + previous_index = index + index = solution.value(routing.next_var(index)) + route_distance += routing.get_arc_cost_for_vehicle(previous_index, index, 0) + plan_output += f" {manager.index_to_node(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = model.RoutingIndexManager( + len(data["locations"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START model] + routing = model.RoutingModel(manager) + # [END model] + + # Create and register a transit callback. + # [START transit_callback] + distance_callback = create_distance_callback(data, manager) + transit_callback_index = routing.register_transit_callback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.set_arc_cost_evaluator_of_all_vehicles(transit_callback_index) + # [END arc_cost] + + # Setting first solution heuristic. + # [START parameters] + search_parameters: parameters_pb2.RoutingSearchParameters = ( + model.default_routing_search_parameters() + ) + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.solve() + # solution = routing.solve_with_parameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + print_solution(manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/tsp_circuit_board.cc b/ortools/routing/samples/tsp_circuit_board.cc similarity index 93% rename from ortools/constraint_solver/samples/tsp_circuit_board.cc rename to ortools/routing/samples/tsp_circuit_board.cc index 9d6e98de415..2fc55e5ccd1 100644 --- a/ortools/constraint_solver/samples/tsp_circuit_board.cc +++ b/ortools/routing/samples/tsp_circuit_board.cc @@ -19,15 +19,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> locations{ @@ -181,10 +184,12 @@ void Tsp() { PrintSolution(manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::Tsp(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::Tsp(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/constraint_solver/samples/tsp_circuit_board.py b/ortools/routing/samples/tsp_circuit_board.py old mode 100755 new mode 100644 similarity index 57% rename from ortools/constraint_solver/samples/tsp_circuit_board.py rename to ortools/routing/samples/tsp_circuit_board.py index 76f62601ee4..67298f345da --- a/ortools/constraint_solver/samples/tsp_circuit_board.py +++ b/ortools/routing/samples/tsp_circuit_board.py @@ -17,19 +17,19 @@ # [START import] import math -from ortools.constraint_solver import routing_enums_pb2 -from ortools.constraint_solver import pywrapcp +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting # [END import] # [START data_model] def create_data_model(): - """Stores the data for the problem.""" - data = {} - # Locations in block units - data["locations"] = [ - # fmt: off + """Stores the data for the problem.""" + data = {} + # Locations in block units + data["locations"] = [ + # fmt: off (288, 149), (288, 129), (270, 133), (256, 141), (256, 157), (246, 157), (236, 169), (228, 169), (228, 161), (220, 169), (212, 169), (204, 169), (196, 169), (188, 169), (196, 161), (188, 145), (172, 145), (164, 145), @@ -77,107 +77,107 @@ def create_data_model(): (188, 93), (180, 93), (180, 101), (180, 109), (180, 117), (180, 125), (196, 145), (204, 145), (212, 145), (220, 145), (228, 145), (236, 145), (246, 141), (252, 125), (260, 129), (280, 133) - # fmt: on - ] - data["num_vehicles"] = 1 - data["depot"] = 0 - return data - # [END data_model] + # fmt: on + ] + data["num_vehicles"] = 1 + data["depot"] = 0 + return data + # [END data_model] # [START distance_callback] def compute_euclidean_distance_matrix(locations): - """Creates callback to return distance between points.""" - distances = {} - for from_counter, from_node in enumerate(locations): - distances[from_counter] = {} - for to_counter, to_node in enumerate(locations): - if from_counter == to_counter: - distances[from_counter][to_counter] = 0 - else: - # Euclidean distance - distances[from_counter][to_counter] = int( - math.hypot((from_node[0] - to_node[0]), (from_node[1] - to_node[1])) - ) - return distances - # [END distance_callback] + """Creates callback to return distance between points.""" + distances = {} + for from_counter, from_node in enumerate(locations): + distances[from_counter] = {} + for to_counter, to_node in enumerate(locations): + if from_counter == to_counter: + distances[from_counter][to_counter] = 0 + else: + # Euclidean distance + distances[from_counter][to_counter] = int( + math.hypot((from_node[0] - to_node[0]), (from_node[1] - to_node[1])) + ) + return distances + # [END distance_callback] # [START solution_printer] def print_solution(manager, routing, solution): - """Prints solution on console.""" - print(f"Objective: {solution.ObjectiveValue()}") - index = routing.Start(0) - plan_output = "Route:\n" - route_distance = 0 - while not routing.IsEnd(index): - plan_output += f" {manager.IndexToNode(index)} ->" - previous_index = index - index = solution.Value(routing.NextVar(index)) - route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) - plan_output += f" {manager.IndexToNode(index)}\n" - print(plan_output) - plan_output += f"Objective: {route_distance}m\n" - # [END solution_printer] + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + index = routing.Start(0) + plan_output = "Route:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} ->" + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) + plan_output += f" {manager.IndexToNode(index)}\n" + plan_output += f"Route distance: {route_distance}mm\n" + print(plan_output) + # [END solution_printer] def main(): - """Entry point of the program.""" - # Instantiate the data problem. - # [START data] - data = create_data_model() - # [END data] - - # Create the routing index manager. - # [START index_manager] - manager = pywrapcp.RoutingIndexManager( - len(data["locations"]), data["num_vehicles"], data["depot"] - ) - # [END index_manager] - - # Create Routing Model. - # [START routing_model] - routing = pywrapcp.RoutingModel(manager) - # [END routing_model] - - # [START transit_callback] - distance_matrix = compute_euclidean_distance_matrix(data["locations"]) - - def distance_callback(from_index, to_index): - """Returns the distance between the two nodes.""" - # Convert from routing variable Index to distance matrix NodeIndex. - from_node = manager.IndexToNode(from_index) - to_node = manager.IndexToNode(to_index) - return distance_matrix[from_node][to_node] - - transit_callback_index = routing.RegisterTransitCallback(distance_callback) - # [END transit_callback] - - # Define cost of each arc. - # [START arc_cost] - routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) - # [END arc_cost] - - # Setting first solution heuristic. - # [START parameters] - search_parameters = pywrapcp.DefaultRoutingSearchParameters() - search_parameters.first_solution_strategy = ( - routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC - ) - # [END parameters] - - # Solve the problem. - # [START solve] - solution = routing.SolveWithParameters(search_parameters) - # [END solve] - - # Print solution on console. - # [START print_solution] - if solution: - print_solution(manager, routing, solution) - # [END print_solution] + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["locations"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # [START transit_callback] + distance_matrix = compute_euclidean_distance_matrix(data["locations"]) + + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return distance_matrix[from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(manager, routing, solution) + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/constraint_solver/samples/tsp_cities.cc b/ortools/routing/samples/tsp_cities.cc similarity index 89% rename from ortools/constraint_solver/samples/tsp_cities.cc rename to ortools/routing/samples/tsp_cities.cc index b3c8006a316..72818dafae5 100644 --- a/ortools/constraint_solver/samples/tsp_cities.cc +++ b/ortools/routing/samples/tsp_cities.cc @@ -18,15 +18,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -127,10 +130,12 @@ void Tsp() { // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::Tsp(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::Tsp(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/tsp_cities.py b/ortools/routing/samples/tsp_cities.py new file mode 100644 index 00000000000..62f97632577 --- /dev/null +++ b/ortools/routing/samples/tsp_cities.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Travelling Salesperson Problem (TSP) between cities.""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + [0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972], + [2451, 0, 1745, 1524, 831, 1240, 959, 2596, 403, 1589, 1374, 357, 579], + [713, 1745, 0, 355, 920, 803, 1737, 851, 1858, 262, 940, 1453, 1260], + [1018, 1524, 355, 0, 700, 862, 1395, 1123, 1584, 466, 1056, 1280, 987], + [1631, 831, 920, 700, 0, 663, 1021, 1769, 949, 796, 879, 586, 371], + [1374, 1240, 803, 862, 663, 0, 1681, 1551, 1765, 547, 225, 887, 999], + [2408, 959, 1737, 1395, 1021, 1681, 0, 2493, 678, 1724, 1891, 1114, 701], + [213, 2596, 851, 1123, 1769, 1551, 2493, 0, 2699, 1038, 1605, 2300, 2099], + [2571, 403, 1858, 1584, 949, 1765, 678, 2699, 0, 1744, 1645, 653, 600], + [875, 1589, 262, 466, 796, 547, 1724, 1038, 1744, 0, 679, 1272, 1162], + [1420, 1374, 940, 1056, 879, 225, 1891, 1605, 1645, 679, 0, 1017, 1200], + [2145, 357, 1453, 1280, 586, 887, 1114, 2300, 653, 1272, 1017, 0, 504], + [1972, 579, 1260, 987, 371, 999, 701, 2099, 600, 1162, 1200, 504, 0], + ] + data["num_vehicles"] = 1 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()} miles") + index = routing.Start(0) + plan_output = "Route for vehicle 0:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} ->" + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) + plan_output += f" {manager.IndexToNode(index)}\n" + plan_output += f"Route distance: {route_distance}miles\n" + print(plan_output) + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + + # [END routing_model] + + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/tsp_cities_routes.cc b/ortools/routing/samples/tsp_cities_routes.cc similarity index 89% rename from ortools/constraint_solver/samples/tsp_cities_routes.cc rename to ortools/routing/samples/tsp_cities_routes.cc index b1977b8d780..c734004d5fe 100644 --- a/ortools/constraint_solver/samples/tsp_cities_routes.cc +++ b/ortools/routing/samples/tsp_cities_routes.cc @@ -18,15 +18,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -130,10 +133,12 @@ void Tsp() { // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::Tsp(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::Tsp(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/constraint_solver/samples/tsp_distance_matrix.cc b/ortools/routing/samples/tsp_distance_matrix.cc similarity index 90% rename from ortools/constraint_solver/samples/tsp_distance_matrix.cc rename to ortools/routing/samples/tsp_distance_matrix.cc index b879e5b141e..a31e8f71b85 100644 --- a/ortools/constraint_solver/samples/tsp_distance_matrix.cc +++ b/ortools/routing/samples/tsp_distance_matrix.cc @@ -18,15 +18,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -149,10 +152,12 @@ void Tsp() { // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::Tsp(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::Tsp(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/tsp_distance_matrix.py b/ortools/routing/samples/tsp_distance_matrix.py new file mode 100644 index 00000000000..c8f4d02c1e3 --- /dev/null +++ b/ortools/routing/samples/tsp_distance_matrix.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Travelling Salesman Problem.""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + data["num_vehicles"] = 1 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + index = routing.Start(0) + plan_output = "Route for vehicle 0:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} ->" + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) + plan_output += f" {manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/routing/samples/tsp_legacy.py b/ortools/routing/samples/tsp_legacy.py new file mode 100644 index 00000000000..48b79d80742 --- /dev/null +++ b/ortools/routing/samples/tsp_legacy.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Travelling Salesman Problem. + +A description of the problem can be found here: +http://en.wikipedia.org/wiki/Travelling_salesperson_problem. +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy +RoutingSearchStatus = enums_pb2.RoutingSearchStatus +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + # Locations in block units + locations = [ + # fmt:off + (4, 4), # depot + (2, 0), (8, 0), # locations to visit + (0, 1), (1, 1), + (5, 2), (7, 2), + (3, 3), (6, 3), + (5, 5), (8, 5), + (1, 6), (2, 6), + (3, 7), (6, 7), + (0, 8), (7, 8) + # fmt:on + ] + # Convert locations in meters using a city block dimension of 114m x 80m. + data["locations"] = [(l[0] * 114, l[1] * 80) for l in locations] + data["num_vehicles"] = 1 + data["depot"] = 0 + return data + # [END data_model] + + +# [START distance_callback] +def create_distance_callback(data, manager): + """Creates callback to return distance between points.""" + distances_ = {} + index_manager_ = manager + # precompute distance between location to have distance callback in O(1) + for from_counter, from_node in enumerate(data["locations"]): + distances_[from_counter] = {} + for to_counter, to_node in enumerate(data["locations"]): + if from_counter == to_counter: + distances_[from_counter][to_counter] = 0 + else: + distances_[from_counter][to_counter] = abs( + from_node[0] - to_node[0] + ) + abs(from_node[1] - to_node[1]) + + def distance_callback(from_index, to_index): + """Returns the manhattan distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = index_manager_.IndexToNode(from_index) + to_node = index_manager_.IndexToNode(to_index) + return distances_[from_node][to_node] + + return distance_callback + # [END distance_callback] + + +# [START solution_printer] +def print_solution(manager, routing, solution): + """Prints assignment on console.""" + status = routing.status() + print(f"Status: {RoutingSearchStatus.Value.Name(status)}") + if ( + status != RoutingSearchStatus.ROUTING_OPTIMAL + and status != RoutingSearchStatus.ROUTING_SUCCESS + ): + print("No solution found!") + return + print(f"Objective: {solution.ObjectiveValue()}") + index = routing.Start(0) + plan_output = "Route for vehicle 0:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} ->" + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle(previous_index, index, 0) + plan_output += f" {manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["locations"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + distance_callback = create_distance_callback(data, manager) + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + print_solution(manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp.cc b/ortools/routing/samples/vrp.cc similarity index 91% rename from ortools/constraint_solver/samples/vrp.cc rename to ortools/routing/samples/vrp.cc index afc3701986f..21f598970f3 100644 --- a/ortools/constraint_solver/samples/vrp.cc +++ b/ortools/routing/samples/vrp.cc @@ -18,15 +18,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -164,10 +167,12 @@ void Vrp() { PrintSolution(manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::Vrp(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::Vrp(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp.py b/ortools/routing/samples/vrp.py new file mode 100644 index 00000000000..9da649cf17a --- /dev/null +++ b/ortools/routing/samples/vrp.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Vehicles Routing Problem (VRP). + +This is a sample using the routing library python wrapper to solve a VRP +problem. +A description of the problem can be found here: +http://en.wikipedia.org/wiki/Vehicle_routing_problem. + +Distances are in meters. +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +FirstSolutionStrategy = enums_pb2.FirstSolutionStrategy +RoutingSearchStatus = enums_pb2.RoutingSearchStatus +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(manager, routing, solution): + """Prints assignment on console.""" + status = routing.status() + print(f"Status: {RoutingSearchStatus.Value.Name(status)}") + if ( + status != RoutingSearchStatus.ROUTING_OPTIMAL + and status != RoutingSearchStatus.ROUTING_SUCCESS + ): + print("No solution found!") + return + print(f"Objective: {solution.ObjectiveValue()}") + total_distance = 0 + for vehicle_index in range(manager.GetNumberOfVehicles()): + if not routing.IsVehicleUsed(solution, vehicle_index): + continue + index = routing.Start(vehicle_index) + plan_output = f"Route for vehicle {vehicle_index}:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} ->" + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_index + ) + plan_output += f" {manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + total_distance += route_distance + print(f"Total Distance of all routes: {total_distance}m") + + +# [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + print_solution(manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_breaks.cc b/ortools/routing/samples/vrp_breaks.cc similarity index 93% rename from ortools/constraint_solver/samples/vrp_breaks.cc rename to ortools/routing/samples/vrp_breaks.cc index f317628c5c3..4337f0d12dc 100644 --- a/ortools/constraint_solver/samples/vrp_breaks.cc +++ b/ortools/routing/samples/vrp_breaks.cc @@ -25,16 +25,19 @@ #include #include +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" #include "absl/strings/str_cat.h" -#include "ortools/base/logging.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> time_matrix{ @@ -203,10 +206,12 @@ void VrpBreaks() { } // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpBreaks(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpBreaks(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_breaks.py b/ortools/routing/samples/vrp_breaks.py new file mode 100755 index 00000000000..71fe2ff6b9d --- /dev/null +++ b/ortools/routing/samples/vrp_breaks.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Vehicle Routing Problem (VRP) with breaks. + +This is a sample using the routing library python wrapper to solve a VRP +problem. +A description of the problem can be found here: +http://en.wikipedia.org/wiki/Vehicle_routing_problem. + +Durations are in minutes. +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["num_vehicles"] = 4 + data["depot"] = 0 + data["time_matrix"] = [ + [0, 27, 38, 34, 29, 13, 25, 9, 15, 9, 26, 25, 19, 17, 23, 38, 33], + [27, 0, 34, 15, 9, 25, 36, 17, 34, 37, 54, 29, 24, 33, 50, 43, 60], + [38, 34, 0, 49, 43, 25, 13, 40, 23, 37, 20, 63, 58, 56, 39, 77, 37], + [34, 15, 49, 0, 5, 32, 43, 25, 42, 44, 61, 25, 31, 41, 58, 28, 67], + [29, 9, 43, 5, 0, 26, 38, 19, 36, 38, 55, 20, 25, 35, 52, 33, 62], + [13, 25, 25, 32, 26, 0, 11, 15, 9, 12, 29, 38, 33, 31, 25, 52, 35], + [25, 36, 13, 43, 38, 11, 0, 26, 9, 23, 17, 50, 44, 42, 25, 63, 24], + [9, 17, 40, 25, 19, 15, 26, 0, 17, 19, 36, 23, 17, 16, 33, 37, 42], + [15, 34, 23, 42, 36, 9, 9, 17, 0, 13, 19, 40, 34, 33, 16, 54, 25], + [9, 37, 37, 44, 38, 12, 23, 19, 13, 0, 17, 26, 21, 19, 13, 40, 23], + [26, 54, 20, 61, 55, 29, 17, 36, 19, 17, 0, 43, 38, 36, 19, 57, 17], + [25, 29, 63, 25, 20, 38, 50, 23, 40, 26, 43, 0, 5, 15, 32, 13, 42], + [19, 24, 58, 31, 25, 33, 44, 17, 34, 21, 38, 5, 0, 9, 26, 19, 36], + [17, 33, 56, 41, 35, 31, 42, 16, 33, 19, 36, 15, 9, 0, 17, 21, 26], + [23, 50, 39, 58, 52, 25, 25, 33, 16, 13, 19, 32, 26, 17, 0, 38, 9], + [38, 43, 77, 28, 33, 52, 63, 37, 54, 40, 57, 13, 19, 21, 38, 0, 39], + [33, 60, 37, 67, 62, 35, 24, 42, 25, 23, 17, 42, 36, 26, 9, 39, 0], + ] + # 15 min of service time + data["service_time"] = [15] * len(data["time_matrix"]) + data["service_time"][data["depot"]] = 0 + assert len(data["time_matrix"]) == len(data["service_time"]) + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + + print("Breaks:") + intervals = solution.IntervalVarContainer() + for i in range(intervals.Size()): + brk = intervals.Element(i) + if brk.PerformedValue(): + print( + f"{brk.Var().Name()}: " + + f"Start({brk.StartValue()}) Duration({brk.DurationValue()})" + ) + else: + print(f"{brk.Var().Name()}: Unperformed") + + time_dimension = routing.GetDimensionOrDie("Time") + total_time = 0 + for vehicle_id in range(manager.GetNumberOfVehicles()): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + while not routing.IsEnd(index): + time_var = time_dimension.CumulVar(index) + plan_output += f"{manager.IndexToNode(index)} " + plan_output += f"Time({solution.Value(time_var)}) -> " + index = solution.Value(routing.NextVar(index)) + time_var = time_dimension.CumulVar(index) + plan_output += f"{manager.IndexToNode(index)} " + plan_output += f"Time({solution.Value(time_var)})\n" + plan_output += f"Time of the route: {solution.Value(time_var)}min\n" + print(plan_output) + total_time += solution.Value(time_var) + print(f"Total time of all routes: {total_time}min") + # [END solution_printer] + + +def main(): + """Solve the VRP with time windows.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["time_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def time_callback(from_index, to_index): + """Returns the travel time + service time between the two nodes.""" + # Convert from routing variable Index to time matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return ( + data["time_matrix"][from_node][to_node] + + data["service_time"][from_node] + ) + + transit_callback_index = routing.RegisterTransitCallback(time_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Time Windows constraint. + time = "Time" + routing.AddDimension( + transit_callback_index, + 10, # needed optional waiting time to place break + 180, # maximum time per vehicle + True, # Force start cumul to zero. + time, + ) + time_dimension = routing.GetDimensionOrDie(time) + time_dimension.SetGlobalSpanCostCoefficient(10) + + # Breaks + # [START break_constraint] + # warning: Need a pre-travel array using the solver's index order. + node_visit_transit = [0] * routing.Size() + for index in range(routing.Size()): + node = manager.IndexToNode(index) + node_visit_transit[index] = data["service_time"][node] + + break_intervals = {} + for v in range(manager.GetNumberOfVehicles()): + break_intervals[v] = [ + routing.solver().FixedDurationIntervalVar( + 50, # start min + 60, # start max + 10, # duration: 10 min + False, # optional: no + f"Break for vehicle {v}", + ) + ] + time_dimension.SetBreakIntervalsOfVehicle( + break_intervals[v], v, node_visit_transit # breaks # vehicle index + ) + # [END break_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + # search_parameters.log_search = True + search_parameters.time_limit.FromSeconds(2) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(manager, routing, solution) + else: + print("No solution found !") + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/routing/samples/vrp_breaks_from_start.py b/ortools/routing/samples/vrp_breaks_from_start.py new file mode 100755 index 00000000000..ddb7c7c5615 --- /dev/null +++ b/ortools/routing/samples/vrp_breaks_from_start.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# [START program] +"""Vehicles Routing Problem (VRP) with breaks relative to the vehicle start time. + +Each vehicles start at T:15min, T:30min, T:45min and T:60min respectively. + +Each vehicle must perform a break lasting 5 minutes, +starting between 25 and 45 minutes after route start. +e.g. vehicle 2 starting a T:45min must start a 5min breaks +between [45+25,45+45] i.e. in the range [70, 90]. + +Durations are in minutes. +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["num_vehicles"] = 4 + data["depot"] = 0 + data["time_matrix"] = [ + [0, 27, 38, 34, 29, 13, 25, 9, 15, 9, 26, 25, 19, 17, 23, 38, 33], + [27, 0, 34, 15, 9, 25, 36, 17, 34, 37, 54, 29, 24, 33, 50, 43, 60], + [38, 34, 0, 49, 43, 25, 13, 40, 23, 37, 20, 63, 58, 56, 39, 77, 37], + [34, 15, 49, 0, 5, 32, 43, 25, 42, 44, 61, 25, 31, 41, 58, 28, 67], + [29, 9, 43, 5, 0, 26, 38, 19, 36, 38, 55, 20, 25, 35, 52, 33, 62], + [13, 25, 25, 32, 26, 0, 11, 15, 9, 12, 29, 38, 33, 31, 25, 52, 35], + [25, 36, 13, 43, 38, 11, 0, 26, 9, 23, 17, 50, 44, 42, 25, 63, 24], + [9, 17, 40, 25, 19, 15, 26, 0, 17, 19, 36, 23, 17, 16, 33, 37, 42], + [15, 34, 23, 42, 36, 9, 9, 17, 0, 13, 19, 40, 34, 33, 16, 54, 25], + [9, 37, 37, 44, 38, 12, 23, 19, 13, 0, 17, 26, 21, 19, 13, 40, 23], + [26, 54, 20, 61, 55, 29, 17, 36, 19, 17, 0, 43, 38, 36, 19, 57, 17], + [25, 29, 63, 25, 20, 38, 50, 23, 40, 26, 43, 0, 5, 15, 32, 13, 42], + [19, 24, 58, 31, 25, 33, 44, 17, 34, 21, 38, 5, 0, 9, 26, 19, 36], + [17, 33, 56, 41, 35, 31, 42, 16, 33, 19, 36, 15, 9, 0, 17, 21, 26], + [23, 50, 39, 58, 52, 25, 25, 33, 16, 13, 19, 32, 26, 17, 0, 38, 9], + [38, 43, 77, 28, 33, 52, 63, 37, 54, 40, 57, 13, 19, 21, 38, 0, 39], + [33, 60, 37, 67, 62, 35, 24, 42, 25, 23, 17, 42, 36, 26, 9, 39, 0], + ] + # 15 min of service time + data["service_time"] = [15] * len(data["time_matrix"]) + data["service_time"][data["depot"]] = 0 + assert len(data["time_matrix"]) == len(data["service_time"]) + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + + print("Breaks:") + intervals = solution.IntervalVarContainer() + for i in range(intervals.Size()): + brk = intervals.Element(i) + if brk.PerformedValue() == 1: + print( + f"{brk.Var().Name()}: " + + f"Start({brk.StartValue()}) Duration({brk.DurationValue()})" + ) + else: + print(f"{brk.Var().Name()}: Unperformed") + + time_dimension = routing.GetDimensionOrDie("Time") + total_time = 0 + for vehicle_id in range(manager.GetNumberOfVehicles()): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + while not routing.IsEnd(index): + time_var = time_dimension.CumulVar(index) + if routing.IsStart(index): + start_time = solution.Value(time_var) + plan_output += f"{manager.IndexToNode(index)} " + plan_output += f"Time({solution.Value(time_var)}) -> " + index = solution.Value(routing.NextVar(index)) + time_var = time_dimension.CumulVar(index) + plan_output += f"{manager.IndexToNode(index)} " + plan_output += f"Time({solution.Value(time_var)})" + print(plan_output) + route_time = solution.Value(time_var) - start_time + print(f"Time of the route: {route_time}min\n") + total_time += route_time + print(f"Total time of all routes: {total_time}min") + # [END solution_printer] + + +def main(): + """Solve the VRP with time windows.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["time_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def time_callback(from_index, to_index): + """Returns the travel time between the two nodes.""" + # Convert from routing variable Index to time matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["time_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(time_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Time Windows constraint. + time = "Time" + routing.AddDimension( + transit_callback_index, + 10, # need optional waiting time to place break + 180, # maximum time per vehicle + False, # Don't force start cumul to zero. + time, + ) + time_dimension = routing.GetDimensionOrDie(time) + time_dimension.SetGlobalSpanCostCoefficient(10) + + # Each vehicle start with a 15min delay + for vehicle_id in range(manager.GetNumberOfVehicles()): + index = routing.Start(vehicle_id) + time_dimension.CumulVar(index).SetValue((vehicle_id + 1) * 15) + + # Add breaks + # [START break_constraint] + # warning: Need a pre-travel array using the solver's index order. + node_visit_transit = [0] * routing.Size() + for index in range(routing.Size()): + node = manager.IndexToNode(index) + node_visit_transit[index] = data["service_time"][node] + + # Add a break lasting 5 minutes, start between 25 and 45 minutes after route start + for v in range(manager.GetNumberOfVehicles()): + start_var = time_dimension.CumulVar(routing.Start(v)) + break_start = routing.solver().Sum( + [routing.solver().IntVar(25, 45), start_var] + ) + + break_intervals = [ + routing.solver().FixedDurationIntervalVar( + break_start, 5, f"Break for vehicle {v}" + ) + ] + time_dimension.SetBreakIntervalsOfVehicle( + break_intervals, v, node_visit_transit + ) + # [END break_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + # search_parameters.log_search = True + search_parameters.time_limit.FromSeconds(2) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(manager, routing, solution) + else: + print("No solution found !") + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_capacity.cc b/ortools/routing/samples/vrp_capacity.cc similarity index 92% rename from ortools/constraint_solver/samples/vrp_capacity.cc rename to ortools/routing/samples/vrp_capacity.cc index 8a17479d265..08aef67f04f 100644 --- a/ortools/constraint_solver/samples/vrp_capacity.cc +++ b/ortools/routing/samples/vrp_capacity.cc @@ -18,16 +18,19 @@ #include #include +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" #include "google/protobuf/duration.pb.h" -#include "ortools/base/logging.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -189,10 +192,12 @@ void VrpCapacity() { PrintSolution(data, manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpCapacity(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpCapacity(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_capacity.py b/ortools/routing/samples/vrp_capacity.py new file mode 100644 index 00000000000..be6224139cf --- /dev/null +++ b/ortools/routing/samples/vrp_capacity.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Capacited Vehicles Routing Problem (CVRP).""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + # [START demands_capacities] + data["demands"] = [0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8] + data["vehicle_capacities"] = [15, 15, 15, 15] + # [END demands_capacities] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + total_distance = 0 + total_load = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + route_load = 0 + while not routing.IsEnd(index): + node_index = manager.IndexToNode(index) + route_load += data["demands"][node_index] + plan_output += f" {node_index} Load({route_load}) -> " + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f" {manager.IndexToNode(index)} Load({route_load})\n" + plan_output += f"Distance of the route: {route_distance}m\n" + plan_output += f"Load of the route: {route_load}\n" + print(plan_output) + total_distance += route_distance + total_load += route_load + print(f"Total distance of all routes: {total_distance}m") + print(f"Total load of all routes: {total_load}") + # [END solution_printer] + + +def main(): + """Solve the CVRP problem.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Capacity constraint. + # [START capacity_constraint] + def demand_callback(from_index): + """Returns the demand of the node.""" + # Convert from routing variable Index to demands NodeIndex. + from_node = manager.IndexToNode(from_index) + return data["demands"][from_node] + + demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback) + routing.AddDimensionWithVehicleCapacity( + demand_callback_index, + 0, # null capacity slack + data["vehicle_capacities"], # vehicle maximum capacities + True, # start cumul to zero + "Capacity", + ) + # [END capacity_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(1) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(data, manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_drop_nodes.cc b/ortools/routing/samples/vrp_drop_nodes.cc similarity index 93% rename from ortools/constraint_solver/samples/vrp_drop_nodes.cc rename to ortools/routing/samples/vrp_drop_nodes.cc index bef0308b1ec..8d00899255e 100644 --- a/ortools/constraint_solver/samples/vrp_drop_nodes.cc +++ b/ortools/routing/samples/vrp_drop_nodes.cc @@ -18,16 +18,19 @@ #include #include +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" #include "google/protobuf/duration.pb.h" -#include "ortools/base/logging.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -205,10 +208,12 @@ void VrpDropNodes() { PrintSolution(data, manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpDropNodes(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpDropNodes(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_drop_nodes.py b/ortools/routing/samples/vrp_drop_nodes.py new file mode 100644 index 00000000000..c104d7eb615 --- /dev/null +++ b/ortools/routing/samples/vrp_drop_nodes.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Capacited Vehicles Routing Problem (CVRP).""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + # [START demands_capacities] + data["demands"] = [0, 1, 1, 3, 6, 3, 6, 8, 8, 1, 2, 1, 2, 6, 6, 8, 8] + data["vehicle_capacities"] = [15, 15, 15, 15] + # [END demands_capacities] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, assignment): + """Prints assignment on console.""" + print(f"Objective: {assignment.ObjectiveValue()}") + # Display dropped nodes. + dropped_nodes = "Dropped nodes:" + for node in range(routing.Size()): + if routing.IsStart(node) or routing.IsEnd(node): + continue + if assignment.Value(routing.NextVar(node)) == node: + dropped_nodes += f" {manager.IndexToNode(node)}" + print(dropped_nodes) + # Display routes + total_distance = 0 + total_load = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(assignment, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + route_load = 0 + while not routing.IsEnd(index): + node_index = manager.IndexToNode(index) + route_load += data["demands"][node_index] + plan_output += f" {node_index} Load({route_load}) -> " + previous_index = index + index = assignment.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f" {manager.IndexToNode(index)} Load({route_load})\n" + plan_output += f"Distance of the route: {route_distance}m\n" + plan_output += f"Load of the route: {route_load}\n" + print(plan_output) + total_distance += route_distance + total_load += route_load + print(f"Total Distance of all routes: {total_distance}m") + print(f"Total Load of all routes: {total_load}") + # [END solution_printer] + + +def main(): + """Solve the CVRP problem.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Capacity constraint. + # [START capacity_constraint] + def demand_callback(from_index): + """Returns the demand of the node.""" + # Convert from routing variable Index to demands NodeIndex. + from_node = manager.IndexToNode(from_index) + return data["demands"][from_node] + + demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback) + routing.AddDimensionWithVehicleCapacity( + demand_callback_index, + 0, # null capacity slack + data["vehicle_capacities"], # vehicle maximum capacities + True, # start cumul to zero + "Capacity", + ) + # Allow to drop nodes. + penalty = 1000 + for node in range(1, len(data["distance_matrix"])): + routing.AddDisjunction([manager.NodeToIndex(node)], penalty) + # [END capacity_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(1) + # [END parameters] + + # Solve the problem. + # [START solve] + assignment = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if assignment: + print_solution(data, manager, routing, assignment) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_global_span.cc b/ortools/routing/samples/vrp_global_span.cc similarity index 91% rename from ortools/constraint_solver/samples/vrp_global_span.cc rename to ortools/routing/samples/vrp_global_span.cc index d0db8c0f757..c4c2f8ba21a 100644 --- a/ortools/constraint_solver/samples/vrp_global_span.cc +++ b/ortools/routing/samples/vrp_global_span.cc @@ -19,15 +19,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -168,10 +171,12 @@ void VrpGlobalSpan() { } // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpGlobalSpan(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpGlobalSpan(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_global_span.py b/ortools/routing/samples/vrp_global_span.py new file mode 100644 index 00000000000..087d28268b1 --- /dev/null +++ b/ortools/routing/samples/vrp_global_span.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Vehicles Routing Problem (VRP). + +This is a sample using the routing library python wrapper to solve a VRP +problem. +A description of the problem can be found here: +http://en.wikipedia.org/wiki/Vehicle_routing_problem. + +Distances are in meters. +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + max_route_distance = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} -> " + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f"{manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + max_route_distance = max(route_distance, max_route_distance) + print(f"Maximum of the route distances: {max_route_distance}m") + + +# [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing.AddDimension( + transit_callback_index, + 0, # no slack + 3000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + # [END distance_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(data, manager, routing, solution) + else: + print("No solution found !") + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_initial_routes.cc b/ortools/routing/samples/vrp_initial_routes.cc similarity index 92% rename from ortools/constraint_solver/samples/vrp_initial_routes.cc rename to ortools/routing/samples/vrp_initial_routes.cc index 8f85c8da2c5..08c7cc990ea 100644 --- a/ortools/constraint_solver/samples/vrp_initial_routes.cc +++ b/ortools/routing/samples/vrp_initial_routes.cc @@ -19,16 +19,19 @@ #include #include +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" #include "google/protobuf/duration.pb.h" -#include "ortools/base/logging.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -193,9 +196,11 @@ void VrpInitialRoutes() { PrintSolution(data, manager, routing, *solution); // [START print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpInitialRoutes(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpInitialRoutes(); return EXIT_SUCCESS; } diff --git a/ortools/routing/samples/vrp_initial_routes.py b/ortools/routing/samples/vrp_initial_routes.py new file mode 100644 index 00000000000..e47d46c6d8f --- /dev/null +++ b/ortools/routing/samples/vrp_initial_routes.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Vehicles Routing Problem (VRP).""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + # [START initial_routes] + data["initial_routes"] = [ + # fmt: off + [8, 16, 14, 13, 12, 11], + [3, 4, 9, 10], + [15, 1], + [7, 5, 2, 6], + # fmt: on + ] + # [END initial_routes] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + max_route_distance = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} -> " + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f"{manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + max_route_distance = max(route_distance, max_route_distance) + print(f"Maximum of the route distances: {max_route_distance}m") + + +# [END solution_printer] + + +def main(): + """Solve the CVRP problem.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing.AddDimension( + transit_callback_index, + 0, # no slack + 3000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + # [END distance_constraint] + + # Close model with the custom search parameters. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(5) + # When an initial solution is given for search, the model will be closed with + # the default search parameters unless it is explicitly closed with the custom + # search parameters. + routing.CloseModelWithParameters(search_parameters) + # [END parameters] + + # Get initial solution from routes after closing the model. + # [START print_initial_solution] + initial_solution = routing.ReadAssignmentFromRoutes( + data["initial_routes"], True + ) + print("Initial solution:") + print_solution(data, manager, routing, initial_solution) + # [END print_initial_solution] + + # Solve the problem. + # [START solve] + solution = routing.SolveFromAssignmentWithParameters( + initial_solution, search_parameters + ) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print("Solution after search:") + print_solution(data, manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/routing/samples/vrp_items_to_deliver.py b/ortools/routing/samples/vrp_items_to_deliver.py new file mode 100755 index 00000000000..22db156a852 --- /dev/null +++ b/ortools/routing/samples/vrp_items_to_deliver.py @@ -0,0 +1,608 @@ +#!/usr/bin/env python3 +# [START program] +"""Vehicles Routing Problem (VRP) for delivering items from any suppliers. + +Description: Need to deliver some item X and Y at end nodes (at least 11 X and +13 Y). Several locations provide them and even few provide both. + +fleet: + * vehicles: 2 + * x capacity: 15 + * y capacity: 15 + * start node: 0 + * end node: 1 +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["num_vehicles"] = 2 + # [START starts_ends] + data["starts"] = [0] * data["num_vehicles"] + data["ends"] = [1] * data["num_vehicles"] + assert len(data["starts"]) == data["num_vehicles"] + assert len(data["ends"]) == data["num_vehicles"] + # [END starts_ends] + + # [START demands_capacities] + # Need 11 X and 13 Y + data["providers_x"] = [ + 0, # start + -11, # end + 2, # X supply 1 + 2, # X supply 2 + 4, # X supply 3 + 4, # X supply 4 + 4, # X supply 5 + 5, # X supply 6 + 1, # X/Y supply 1 + 2, # X/Y supply 2 + 2, # X/Y supply 3 + 0, # Y supply 1 + 0, # Y supply 2 + 0, # Y supply 3 + 0, # Y supply 4 + 0, # Y supply 5 + 0, # Y supply 6 + ] + data["providers_y"] = [ + 0, # start + -13, # ends + 0, # X supply 1 + 0, # X supply 2 + 0, # X supply 3 + 0, # X supply 4 + 0, # X supply 5 + 0, # X supply 6 + 3, # X/Y supply 1 + 2, # X/Y supply 2 + 1, # X/Y supply 3 + 3, # Y supply 1 + 3, # Y supply 2 + 3, # Y supply 3 + 3, # Y supply 4 + 3, # Y supply 5 + 5, # Y supply 6 + ] + data["vehicle_capacities_x"] = [15] * data["num_vehicles"] + data["vehicle_capacities_y"] = [15] * data["num_vehicles"] + assert len(data["vehicle_capacities_x"]) == data["num_vehicles"] + assert len(data["vehicle_capacities_y"]) == data["num_vehicles"] + # [END demands_capacities] + data["distance_matrix"] = [ + [ + 0, + 548, + 776, + 696, + 582, + 274, + 502, + 194, + 308, + 194, + 536, + 502, + 388, + 354, + 468, + 776, + 662, + ], + [ + 548, + 0, + 684, + 308, + 194, + 502, + 730, + 354, + 696, + 742, + 1084, + 594, + 480, + 674, + 1016, + 868, + 1210, + ], + [ + 776, + 684, + 0, + 992, + 878, + 502, + 274, + 810, + 468, + 742, + 400, + 1278, + 1164, + 1130, + 788, + 1552, + 754, + ], + [ + 696, + 308, + 992, + 0, + 114, + 650, + 878, + 502, + 844, + 890, + 1232, + 514, + 628, + 822, + 1164, + 560, + 1358, + ], + [ + 582, + 194, + 878, + 114, + 0, + 536, + 764, + 388, + 730, + 776, + 1118, + 400, + 514, + 708, + 1050, + 674, + 1244, + ], + [ + 274, + 502, + 502, + 650, + 536, + 0, + 228, + 308, + 194, + 240, + 582, + 776, + 662, + 628, + 514, + 1050, + 708, + ], + [ + 502, + 730, + 274, + 878, + 764, + 228, + 0, + 536, + 194, + 468, + 354, + 1004, + 890, + 856, + 514, + 1278, + 480, + ], + [ + 194, + 354, + 810, + 502, + 388, + 308, + 536, + 0, + 342, + 388, + 730, + 468, + 354, + 320, + 662, + 742, + 856, + ], + [ + 308, + 696, + 468, + 844, + 730, + 194, + 194, + 342, + 0, + 274, + 388, + 810, + 696, + 662, + 320, + 1084, + 514, + ], + [ + 194, + 742, + 742, + 890, + 776, + 240, + 468, + 388, + 274, + 0, + 342, + 536, + 422, + 388, + 274, + 810, + 468, + ], + [ + 536, + 1084, + 400, + 1232, + 1118, + 582, + 354, + 730, + 388, + 342, + 0, + 878, + 764, + 730, + 388, + 1152, + 354, + ], + [ + 502, + 594, + 1278, + 514, + 400, + 776, + 1004, + 468, + 810, + 536, + 878, + 0, + 114, + 308, + 650, + 274, + 844, + ], + [ + 388, + 480, + 1164, + 628, + 514, + 662, + 890, + 354, + 696, + 422, + 764, + 114, + 0, + 194, + 536, + 388, + 730, + ], + [ + 354, + 674, + 1130, + 822, + 708, + 628, + 856, + 320, + 662, + 388, + 730, + 308, + 194, + 0, + 342, + 422, + 536, + ], + [ + 468, + 1016, + 788, + 1164, + 1050, + 514, + 514, + 662, + 320, + 274, + 388, + 650, + 536, + 342, + 0, + 764, + 194, + ], + [ + 776, + 868, + 1552, + 560, + 674, + 1050, + 1278, + 742, + 1084, + 810, + 1152, + 274, + 388, + 422, + 764, + 0, + 798, + ], + [ + 662, + 1210, + 754, + 1358, + 1244, + 708, + 480, + 856, + 514, + 468, + 354, + 844, + 730, + 536, + 194, + 798, + 0, + ], + ] + assert len(data["providers_x"]) == len(data["distance_matrix"]) + assert len(data["providers_y"]) == len(data["distance_matrix"]) + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, assignment): + """Prints assignment on console.""" + print(f"Objective: {assignment.ObjectiveValue()}") + # Display dropped nodes. + dropped_nodes = "Dropped nodes:" + for node in range(routing.Size()): + if routing.IsStart(node) or routing.IsEnd(node): + continue + if assignment.Value(routing.NextVar(node)) == node: + dropped_nodes += f" {manager.IndexToNode(node)}" + print(dropped_nodes) + # Display routes + total_distance = 0 + total_load_x = 0 + total_load_y = 0 + for vehicle_id in range(manager.GetNumberOfVehicles()): + if not routing.IsVehicleUsed(assignment, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + route_load_x = 0 + route_load_y = 0 + while not routing.IsEnd(index): + node_index = manager.IndexToNode(index) + route_load_x += data["providers_x"][node_index] + route_load_y += data["providers_y"][node_index] + plan_output += ( + f" {node_index} Load(X:{route_load_x}, Y:{route_load_y}) -> " + ) + previous_index = index + previous_node_index = node_index + index = assignment.Value(routing.NextVar(index)) + node_index = manager.IndexToNode(index) + # route_distance += routing.GetArcCostForVehicle(previous_index, index, vehicle_id) + route_distance += data["distance_matrix"][previous_node_index][node_index] + node_index = manager.IndexToNode(index) + plan_output += f" {node_index} Load({route_load_x}, {route_load_y})\n" + plan_output += f"Distance of the route: {route_distance}m\n" + plan_output += f"Load of the route: X:{route_load_x}, Y:{route_load_y}\n" + print(plan_output) + total_distance += route_distance + total_load_x += route_load_x + total_load_y += route_load_y + print(f"Total Distance of all routes: {total_distance}m") + print(f"Total load of all routes: X:{total_load_x}, Y:{total_load_y}") + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), + data["num_vehicles"], + data["starts"], + data["ends"], + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing.AddDimension( + transit_callback_index, + 0, # no slack + 2000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + # Minimize the longest road + distance_dimension.SetGlobalSpanCostCoefficient(100) + + # [END distance_constraint] + + # Add Capacity constraint. + # [START capacity_constraint] + def demand_callback_x(from_index): + """Returns the demand of the node.""" + # Convert from routing variable Index to demands NodeIndex. + from_node = manager.IndexToNode(from_index) + return data["providers_x"][from_node] + + demand_callback_x_index = routing.RegisterUnaryTransitCallback( + demand_callback_x + ) + routing.AddDimensionWithVehicleCapacity( + demand_callback_x_index, + 0, # null capacity slack + data["vehicle_capacities_x"], # vehicle maximum capacities + True, # start cumul to zero + "Load_x", + ) + + def demand_callback_y(from_index): + """Returns the demand of the node.""" + # Convert from routing variable Index to demands NodeIndex. + from_node = manager.IndexToNode(from_index) + return data["providers_y"][from_node] + + demand_callback_y_index = routing.RegisterUnaryTransitCallback( + demand_callback_y + ) + routing.AddDimensionWithVehicleCapacity( + demand_callback_y_index, + 0, # null capacity slack + data["vehicle_capacities_y"], # vehicle maximum capacities + True, # start cumul to zero + "Load_y", + ) + # [END capacity_constraint] + + # Add constraint at end + solver = routing.solver() + load_x_dim = routing.GetDimensionOrDie("Load_x") + load_y_dim = routing.GetDimensionOrDie("Load_y") + ends = [] + for v in range(manager.GetNumberOfVehicles()): + ends.append(routing.End(v)) + + node_end = data["ends"][0] + solver.Add( + solver.Sum([load_x_dim.CumulVar(l) for l in ends]) + >= -data["providers_x"][node_end] + ) + solver.Add( + solver.Sum([load_y_dim.CumulVar(l) for l in ends]) + >= -data["providers_y"][node_end] + ) + # solver.Add(load_y_dim.CumulVar(end) >= -data['providers_y'][node_end]) + + # Allow to freely drop any nodes. + penalty = 0 + for node in range(0, len(data["distance_matrix"])): + if node not in data["starts"] and node not in data["ends"]: + routing.AddDisjunction([manager.NodeToIndex(node)], penalty) + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + # Sets a time limit; default is 100 milliseconds. + # search_parameters.log_search = True + search_parameters.time_limit.FromSeconds(1) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(data, manager, routing, solution) + else: + print("no solution found !") + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/routing/samples/vrp_node_max.py b/ortools/routing/samples/vrp_node_max.py new file mode 100755 index 00000000000..3fca18f9a8c --- /dev/null +++ b/ortools/routing/samples/vrp_node_max.py @@ -0,0 +1,275 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Vehicles Routing Problem (VRP). + +Each route as an associated objective cost equal to the max node value along the +road multiply by a constant factor (4200) +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + data["value"] = [ + 0, # depot + 42, # 1 + 42, # 2 + 8, # 3 + 8, # 4 + 8, # 5 + 8, # 6 + 8, # 7 + 8, # 8 + 8, # 9 + 8, # 10 + 8, # 11 + 8, # 12 + 8, # 13 + 8, # 14 + 42, # 15 + 42, # 16 + ] + assert len(data["distance_matrix"]) == len(data["value"]) + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + + +# [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + max_route_distance = 0 + dim_one = routing.GetDimensionOrDie("One") + dim_two = routing.GetDimensionOrDie("Two") + + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing.IsEnd(index): + one_var = dim_one.CumulVar(index) + one_slack_var = dim_one.SlackVar(index) + two_var = dim_two.CumulVar(index) + two_slack_var = dim_two.SlackVar(index) + plan_output += ( + f" N:{manager.IndexToNode(index)}" + f" one:({solution.Value(one_var)}, {solution.Value(one_slack_var)})" + f" two:({solution.Value(two_var)}, {solution.Value(two_slack_var)})" + " -> " + ) + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + one_var = dim_one.CumulVar(index) + two_var = dim_two.CumulVar(index) + plan_output += ( + f"N:{manager.IndexToNode(index)}" + f" one:{solution.Value(one_var)}" + f" two:{solution.Value(two_var)}\n" + ) + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + max_route_distance = max(route_distance, max_route_distance) + print(f"Maximum of the route distances: {max_route_distance}m") + + +# [END solution_printer] + + +def main(): + """Solve the CVRP problem.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing.AddDimension( + transit_callback_index, + 0, # no slack + 3_000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(10) + # [END distance_constraint] + + # Max Node value Constraint. + # Dimension One will be used to compute the max node value up to the node in + # the route and store the result in the SlackVar of the node. + routing.AddConstantDimensionWithSlack( + 0, # transit 0 + 42 * 16, # capacity: be able to store PEAK*ROUTE_LENGTH in worst case + 42, # slack_max: to be able to store peak in slack + True, # Fix StartCumulToZero not really matter here + "One", + ) + dim_one = routing.GetDimensionOrDie("One") + + # Dimension Two will be used to store the max node value in the route end node + # CumulVar so we can use it as an objective cost. + routing.AddConstantDimensionWithSlack( + 0, # transit 0 + 42 * 16, # capacity: be able to have PEAK value in CumulVar(End) + 42, # slack_max: to be able to store peak in slack + True, # Fix StartCumulToZero YES here + "Two", + ) + dim_two = routing.GetDimensionOrDie("Two") + + # force depot Slack to be value since we don't have any predecessor... + for v in range(manager.GetNumberOfVehicles()): + start = routing.Start(v) + dim_one.SlackVar(start).SetValue(data["value"][0]) + routing.AddToAssignment(dim_one.SlackVar(start)) + + dim_two.SlackVar(start).SetValue(data["value"][0]) + routing.AddToAssignment(dim_two.SlackVar(start)) + + # Step by step relation + # Slack(N) = max( Slack(N-1) , value(N) ) + solver = routing.solver() + for node in range(1, 17): + index = manager.NodeToIndex(node) + routing.AddToAssignment(dim_one.SlackVar(index)) + routing.AddToAssignment(dim_two.SlackVar(index)) + test = [] + for v in range(manager.GetNumberOfVehicles()): + previous_index = routing.Start(v) + cond = routing.NextVar(previous_index) == index + value = solver.Max(dim_one.SlackVar(previous_index), data["value"][node]) + test.append((cond * value).Var()) + for previous in range(1, 17): + previous_index = manager.NodeToIndex(previous) + cond = routing.NextVar(previous_index) == index + value = solver.Max(dim_one.SlackVar(previous_index), data["value"][node]) + test.append((cond * value).Var()) + solver.Add(solver.Sum(test) == dim_one.SlackVar(index)) + + # relation between dimensions, copy last node Slack from dim ONE to dim TWO + for node in range(1, 17): + index = manager.NodeToIndex(node) + values = [] + for v in range(manager.GetNumberOfVehicles()): + next_index = routing.End(v) + cond = routing.NextVar(index) == next_index + value = dim_one.SlackVar(index) + values.append((cond * value).Var()) + solver.Add(solver.Sum(values) == dim_two.SlackVar(index)) + + # Should force all others dim_two slack var to zero... + for v in range(manager.GetNumberOfVehicles()): + end = routing.End(v) + dim_two.SetCumulVarSoftUpperBound(end, 0, 4200) + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + # search_parameters.log_search = True + search_parameters.time_limit.FromSeconds(5) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(data, manager, routing, solution) + else: + print("No solution found !") + # [END print_solution] + + +if __name__ == "__main__": + main() + # [END program] diff --git a/ortools/routing/samples/vrp_nodes_indices.py b/ortools/routing/samples/vrp_nodes_indices.py new file mode 100755 index 00000000000..42bb751336e --- /dev/null +++ b/ortools/routing/samples/vrp_nodes_indices.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# [START program] +"""Sample to better understand Node/Index relation. + +This script generate few markdown tables to better understand +the relation between nodes and indices. + +Things to notice: +* Since we have two duplicates (node 5 and node 4) solver need 2 extra indices +to have an unique index for each vehicle start/stop and locations. +* Solver needs to "create" an index for a vehicle 1 start since solver need an +unique start index per vehicle. +* All end nodes are moved to the end of the index list aka [15, 16, 17, 18]. +* routing.Size() return the number of node which are not end nodes (here 15 aka +[0-14]) +note: using the two properties above, we know that any index in +range(routing.Size()) is not a vehicle end node. + +* Since end nodes are moved to the end, their respective "empty" node index are +reused so all locations indices are "shifted" +e.g. node 9 is mapped to index 6 +* Same for start nodes which are moved to "empty" space +e.g. start node 7 mapped to index 4 + +Takeaway: +* Allways use routing.Start(), routing.End(), manager.IndexToNode() or +manager.NodeToIndex(). +* Location node is not necessarily equal to its index. +* To loop through ALL indices use manager.GetNumberOfIndices() (Python) or +manager::num_indices() (C++) +""" + +from ortools.routing import pywraprouting + + +def main(): + """Entry point of the program.""" + locations = 17 + starts = [5, 5, 7, 8] + ends = [1, 2, 4, 4] + vehicles = len(starts) + assert len(starts) == len(ends) + + manager = pywraprouting.RoutingIndexManager(locations, vehicles, starts, ends) + routing = pywraprouting.RoutingModel(manager) + + print("Starts/Ends:") + header = "| |" + separator = "|---|" + v_starts = "| start |" + v_ends = "| end |" + for v in range(manager.GetNumberOfVehicles()): + header += f" vehicle {v} |" + separator += "---|" + v_starts += f" {starts[v]} |" + v_ends += f" {ends[v]} |" + print(header) + print(separator) + print(v_starts) + print(v_ends) + + print("\nNodes:") + print( + "| locations | manager.GetNumberOfNodes | manager.GetNumberOfIndices |" + " routing.nodes | routing.Size |" + ) + print("|---|---|---|---|---|") + print( + f"| {locations} | {manager.GetNumberOfNodes()} |" + f" {manager.GetNumberOfIndices()} | {routing.nodes()} |" + f" {routing.Size()} |" + ) + + print("\nLocations:") + print("| node | index | routing.IsStart | routing.IsEnd |") + print("|---|---|---|---|") + for node in range(manager.GetNumberOfNodes()): + if node in starts or node in ends: + continue + index = manager.NodeToIndex(node) + print( + f"| {node} | {index} | {routing.IsStart(index)} |" + f" {routing.IsEnd(index)} |" + ) + + print("\nStart/End:") + print( + "| vehicle | Start/end | node | index | routing.IsStart | routing.IsEnd |" + ) + print("|---|---|---|---|---|---|") + for v in range(manager.GetNumberOfVehicles()): + start_index = routing.Start(v) + start_node = manager.IndexToNode(start_index) + print( + f"| {v} | start | {start_node} | {start_index} |" + f" {routing.IsStart(start_index)} | {routing.IsEnd(start_index)} |" + ) + for v in range(manager.GetNumberOfVehicles()): + end_index = routing.End(v) + end_node = manager.IndexToNode(end_index) + print( + f"| {v} | end | {end_node} | {end_index} |" + f" {routing.IsStart(end_index)} | {routing.IsEnd(end_index)} |" + ) + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_pickup_delivery.cc b/ortools/routing/samples/vrp_pickup_delivery.cc similarity index 93% rename from ortools/constraint_solver/samples/vrp_pickup_delivery.cc rename to ortools/routing/samples/vrp_pickup_delivery.cc index ab35f5bb6f9..257163690ee 100644 --- a/ortools/constraint_solver/samples/vrp_pickup_delivery.cc +++ b/ortools/routing/samples/vrp_pickup_delivery.cc @@ -18,15 +18,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -200,10 +203,12 @@ void VrpGlobalSpan() { PrintSolution(data, manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpGlobalSpan(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpGlobalSpan(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_pickup_delivery.py b/ortools/routing/samples/vrp_pickup_delivery.py new file mode 100755 index 00000000000..3fd44d85bf0 --- /dev/null +++ b/ortools/routing/samples/vrp_pickup_delivery.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Pickup Delivery Problem (PDP).""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + # [START pickups_deliveries] + data["pickups_deliveries"] = [ + [1, 6], + [2, 10], + [4, 3], + [5, 9], + [7, 8], + [15, 11], + [13, 12], + [16, 14], + ] + # [END pickups_deliveries] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + total_distance = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} -> " + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f"{manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + total_distance += route_distance + print(f"Total Distance of all routes: {total_distance}m") + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + + # [END routing_model] + + # Define cost of each arc. + # [START arc_cost] + def distance_callback(from_index, to_index): + """Returns the manhattan distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing.AddDimension( + transit_callback_index, + 0, # no slack + 3000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + # [END distance_constraint] + + # Define Transportation Requests. + # [START pickup_delivery_constraint] + for request in data["pickups_deliveries"]: + pickup_index = manager.NodeToIndex(request[0]) + delivery_index = manager.NodeToIndex(request[1]) + routing.AddPickupAndDelivery(pickup_index, delivery_index) + routing.solver().Add( + routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index) + ) + routing.solver().Add( + distance_dimension.CumulVar(pickup_index) + <= distance_dimension.CumulVar(delivery_index) + ) + # [END pickup_delivery_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(data, manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_pickup_delivery_fifo.cc b/ortools/routing/samples/vrp_pickup_delivery_fifo.cc similarity index 93% rename from ortools/constraint_solver/samples/vrp_pickup_delivery_fifo.cc rename to ortools/routing/samples/vrp_pickup_delivery_fifo.cc index 2dde94113b2..6ba76c2d9e1 100644 --- a/ortools/constraint_solver/samples/vrp_pickup_delivery_fifo.cc +++ b/ortools/routing/samples/vrp_pickup_delivery_fifo.cc @@ -18,15 +18,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -202,10 +205,12 @@ void VrpGlobalSpan() { PrintSolution(data, manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpGlobalSpan(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpGlobalSpan(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_pickup_delivery_fifo.py b/ortools/routing/samples/vrp_pickup_delivery_fifo.py new file mode 100755 index 00000000000..aea7dff8718 --- /dev/null +++ b/ortools/routing/samples/vrp_pickup_delivery_fifo.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Pickup Delivery Problem (PDP).""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + # [START pickups_deliveries] + data["pickups_deliveries"] = [ + [1, 6], + [2, 10], + [4, 3], + [5, 9], + [7, 8], + [15, 11], + [13, 12], + [16, 14], + ] + # [END pickups_deliveries] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, assignment): + """Prints assignment on console.""" + print(f"Objective: {assignment.ObjectiveValue()}") + total_distance = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(assignment, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} -> " + previous_index = index + index = assignment.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f"{manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + total_distance += route_distance + print(f"Total Distance of all routes: {total_distance}m") + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + + # [END routing_model] + + # Define cost of each arc. + # [START arc_cost] + def distance_callback(from_index, to_index): + """Returns the manhattan distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing.AddDimension( + transit_callback_index, + 0, # no slack + 3000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + # [END distance_constraint] + + # Define Transportation Requests. + # [START pickup_delivery_constraint] + for request in data["pickups_deliveries"]: + pickup_index = manager.NodeToIndex(request[0]) + delivery_index = manager.NodeToIndex(request[1]) + routing.AddPickupAndDelivery(pickup_index, delivery_index) + routing.solver().Add( + routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index) + ) + routing.solver().Add( + distance_dimension.CumulVar(pickup_index) + <= distance_dimension.CumulVar(delivery_index) + ) + routing.SetPickupAndDeliveryPolicyOfAllVehicles( + pywraprouting.RoutingModel.PICKUP_AND_DELIVERY_FIFO + ) + # [END pickup_delivery_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION + ) + # [END parameters] + + # Solve the problem. + # [START solve] + assignment = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if assignment: + print_solution(data, manager, routing, assignment) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_pickup_delivery_lifo.cc b/ortools/routing/samples/vrp_pickup_delivery_lifo.cc similarity index 93% rename from ortools/constraint_solver/samples/vrp_pickup_delivery_lifo.cc rename to ortools/routing/samples/vrp_pickup_delivery_lifo.cc index 4c0ea85ab66..9a02fda0629 100644 --- a/ortools/constraint_solver/samples/vrp_pickup_delivery_lifo.cc +++ b/ortools/routing/samples/vrp_pickup_delivery_lifo.cc @@ -18,15 +18,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -202,10 +205,12 @@ void VrpGlobalSpan() { PrintSolution(data, manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpGlobalSpan(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpGlobalSpan(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_pickup_delivery_lifo.py b/ortools/routing/samples/vrp_pickup_delivery_lifo.py new file mode 100755 index 00000000000..c70cd404462 --- /dev/null +++ b/ortools/routing/samples/vrp_pickup_delivery_lifo.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Pickup Delivery Problem (PDP).""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + # [START pickups_deliveries] + data["pickups_deliveries"] = [ + [1, 6], + [2, 10], + [4, 3], + [5, 9], + [7, 8], + [15, 11], + [13, 12], + [16, 14], + ] + # [END pickups_deliveries] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, assignment): + """Prints assignment on console.""" + print(f"Objective: {assignment.ObjectiveValue()}") + total_distance = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(assignment, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} -> " + previous_index = index + index = assignment.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f"{manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + total_distance += route_distance + print(f"Total Distance of all routes: {total_distance}m") + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + + # [END routing_model] + + # Define cost of each arc. + # [START arc_cost] + def distance_callback(from_index, to_index): + """Returns the manhattan distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing.AddDimension( + transit_callback_index, + 0, # no slack + 3000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + # [END distance_constraint] + + # Define Transportation Requests. + # [START pickup_delivery_constraint] + for request in data["pickups_deliveries"]: + pickup_index = manager.NodeToIndex(request[0]) + delivery_index = manager.NodeToIndex(request[1]) + routing.AddPickupAndDelivery(pickup_index, delivery_index) + routing.solver().Add( + routing.VehicleVar(pickup_index) == routing.VehicleVar(delivery_index) + ) + routing.solver().Add( + distance_dimension.CumulVar(pickup_index) + <= distance_dimension.CumulVar(delivery_index) + ) + routing.SetPickupAndDeliveryPolicyOfAllVehicles( + pywraprouting.RoutingModel.PICKUP_AND_DELIVERY_LIFO + ) + # [END pickup_delivery_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION + ) + # [END parameters] + + # Solve the problem. + # [START solve] + assignment = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if assignment: + print_solution(data, manager, routing, assignment) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_resources.cc b/ortools/routing/samples/vrp_resources.cc similarity index 93% rename from ortools/constraint_solver/samples/vrp_resources.cc rename to ortools/routing/samples/vrp_resources.cc index 09c47a457b0..2e5497d16d0 100644 --- a/ortools/constraint_solver/samples/vrp_resources.cc +++ b/ortools/routing/samples/vrp_resources.cc @@ -20,15 +20,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> time_matrix{ @@ -224,10 +227,12 @@ void VrpTimeWindows() { PrintSolution(data, manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpTimeWindows(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpTimeWindows(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_resources.py b/ortools/routing/samples/vrp_resources.py new file mode 100644 index 00000000000..6d8c76283b4 --- /dev/null +++ b/ortools/routing/samples/vrp_resources.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Vehicles Routing Problem (VRP) with Resource Constraints.""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["time_matrix"] = [ + [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], + [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], + [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], + [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], + [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], + [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], + [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], + [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], + [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], + [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], + [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], + [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], + [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], + [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], + [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], + [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], + [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], + ] + data["time_windows"] = [ + (0, 5), # depot + (7, 12), # 1 + (10, 15), # 2 + (5, 14), # 3 + (5, 13), # 4 + (0, 5), # 5 + (5, 10), # 6 + (0, 10), # 7 + (5, 10), # 8 + (0, 5), # 9 + (10, 16), # 10 + (10, 15), # 11 + (0, 5), # 12 + (5, 10), # 13 + (7, 12), # 14 + (10, 15), # 15 + (5, 15), # 16 + ] + data["num_vehicles"] = 4 + # [START resources_data] + data["vehicle_load_time"] = 5 + data["vehicle_unload_time"] = 5 + data["depot_capacity"] = 2 + # [END resources_data] + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + time_dimension = routing.GetDimensionOrDie("Time") + total_time = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + while not routing.IsEnd(index): + time_var = time_dimension.CumulVar(index) + plan_output += ( + f"{manager.IndexToNode(index)}" + f" Time({solution.Min(time_var)}, {solution.Max(time_var)})" + " -> " + ) + index = solution.Value(routing.NextVar(index)) + time_var = time_dimension.CumulVar(index) + plan_output += ( + f"{manager.IndexToNode(index)}" + f" Time({solution.Min(time_var)},{solution.Max(time_var)})\n" + ) + plan_output += f"Time of the route: {solution.Min(time_var)}min\n" + print(plan_output) + total_time += solution.Min(time_var) + print(f"Total time of all routes: {total_time}min") + # [END solution_printer] + + +def main(): + """Solve the VRP with time windows.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["time_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def time_callback(from_index, to_index): + """Returns the travel time between the two nodes.""" + # Convert from routing variable Index to time matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["time_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(time_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Time Windows constraint. + # [START time_windows_constraint] + time = "Time" + routing.AddDimension( + transit_callback_index, + 60, # allow waiting time + 60, # maximum time per vehicle + False, # Don't force start cumul to zero. + time, + ) + time_dimension = routing.GetDimensionOrDie(time) + # Add time window constraints for each location except depot. + for location_idx, time_window in enumerate(data["time_windows"]): + if location_idx == 0: + continue + index = manager.NodeToIndex(location_idx) + time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) + # Add time window constraints for each vehicle start node. + for vehicle_id in range(data["num_vehicles"]): + index = routing.Start(vehicle_id) + time_dimension.CumulVar(index).SetRange( + data["time_windows"][0][0], data["time_windows"][0][1] + ) + # [END time_windows_constraint] + + # Add resource constraints at the depot. + # [START depot_load_time] + solver = routing.solver() + intervals = [] + for i in range(data["num_vehicles"]): + # Add time windows at start of routes + intervals.append( + solver.FixedDurationIntervalVar( + time_dimension.CumulVar(routing.Start(i)), + data["vehicle_load_time"], + "depot_interval", + ) + ) + # Add time windows at end of routes. + intervals.append( + solver.FixedDurationIntervalVar( + time_dimension.CumulVar(routing.End(i)), + data["vehicle_unload_time"], + "depot_interval", + ) + ) + # [END depot_load_time] + + # [START depot_capacity] + depot_usage = [1 for _ in range(len(intervals))] + solver.Add( + solver.Cumulative(intervals, depot_usage, data["depot_capacity"], "depot") + ) + # [END depot_capacity] + + # Instantiate route start and end times to produce feasible times. + # [START depot_start_end_times] + for i in range(data["num_vehicles"]): + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.Start(i)) + ) + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.End(i)) + ) + # [END depot_start_end_times] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(data, manager, routing, solution) + # [END print_solution] + else: + print("No solution found !") + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_routes.cc b/ortools/routing/samples/vrp_routes.cc similarity index 90% rename from ortools/constraint_solver/samples/vrp_routes.cc rename to ortools/routing/samples/vrp_routes.cc index 1d0a3516e06..bf477de3f13 100644 --- a/ortools/constraint_solver/samples/vrp_routes.cc +++ b/ortools/routing/samples/vrp_routes.cc @@ -18,15 +18,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -154,10 +157,12 @@ void Vrp() { PrintSolution(routes); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::Vrp(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::Vrp(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/constraint_solver/samples/vrp_solution_callback.cc b/ortools/routing/samples/vrp_solution_callback.cc similarity index 93% rename from ortools/constraint_solver/samples/vrp_solution_callback.cc rename to ortools/routing/samples/vrp_solution_callback.cc index fd9349c56af..b5571b10375 100644 --- a/ortools/constraint_solver/samples/vrp_solution_callback.cc +++ b/ortools/routing/samples/vrp_solution_callback.cc @@ -20,16 +20,19 @@ #include #include +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" #include "google/protobuf/duration.pb.h" -#include "ortools/base/logging.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -219,10 +222,12 @@ void VrpSolutionCallback() { } // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpSolutionCallback(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpSolutionCallback(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_solution_callback.py b/ortools/routing/samples/vrp_solution_callback.py new file mode 100755 index 00000000000..ca136753f1f --- /dev/null +++ b/ortools/routing/samples/vrp_solution_callback.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Vehicles Routing Problem (VRP). + +This is a sample using the routing library python wrapper to solve a VRP +problem. + +The solver stop after improving its solution 15 times or after 5 seconds. + +Distances are in meters. +""" + +# [START import] +import weakref + +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_callback_printer] +def print_solution( + routing_manager: pywraprouting.RoutingIndexManager, + routing_model: pywraprouting.RoutingModel, +): + """Prints solution on console.""" + print("################") + print(f"Solution objective: {routing_model.CostVar().Value()}") + total_distance = 0 + for vehicle_id in range(routing_manager.GetNumberOfVehicles()): + index = routing_model.Start(vehicle_id) + if routing_model.IsEnd(routing_model.NextVar(index).Value()): + continue + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing_model.IsEnd(index): + plan_output += f" {routing_manager.IndexToNode(index)} ->" + previous_index = index + index = routing_model.NextVar(index).Value() + route_distance += routing_model.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f" {routing_manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + total_distance += route_distance + print(f"Total Distance of all routes: {total_distance}m") + + +# [END solution_callback_printer] + + +# [START solution_callback] +class SolutionCallback: + """Create a solution callback.""" + + def __init__( + self, + manager: pywraprouting.RoutingIndexManager, + model: pywraprouting.RoutingModel, + limit: int, + ): + # We need a weak ref on the routing model to avoid a cycle. + self._routing_manager_ref = weakref.ref(manager) + self._routing_model_ref = weakref.ref(model) + self._counter = 0 + self._counter_limit = limit + self.objectives = [] + + def __call__(self): + objective = int( + self._routing_model_ref().CostVar().Value() + ) # pytype: disable=attribute-error + if not self.objectives or objective < self.objectives[-1]: + self.objectives.append(objective) + print_solution( + self._routing_manager_ref(), self._routing_model_ref() + ) # pytype: disable=attribute-error + self._counter += 1 + if self._counter > self._counter_limit: + self._routing_model_ref().solver().FinishCurrentSearch() # pytype: disable=attribute-error + + +# [END solution_callback] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + routing_manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing_model = pywraprouting.RoutingModel(routing_manager) + + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = routing_manager.IndexToNode(from_index) + to_node = routing_manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing_model.RegisterTransitCallback( + distance_callback + ) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing_model.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing_model.AddDimension( + transit_callback_index, + 0, # no slack + 3000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing_model.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + # [END distance_constraint] + + # Attach a solution callback. + # [START attach_callback] + solution_callback = SolutionCallback(routing_manager, routing_model, 15) + routing_model.AddAtSolutionCallback(solution_callback) + # [END attach_callback] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(5) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing_model.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print(f"Best objective: {solution_callback.objectives[-1]}") + else: + print("No solution found !") + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrp_starts_ends.cc b/ortools/routing/samples/vrp_starts_ends.cc similarity index 92% rename from ortools/constraint_solver/samples/vrp_starts_ends.cc rename to ortools/routing/samples/vrp_starts_ends.cc index d4b581c2f07..93f02e316e8 100644 --- a/ortools/constraint_solver/samples/vrp_starts_ends.cc +++ b/ortools/routing/samples/vrp_starts_ends.cc @@ -19,15 +19,18 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> distance_matrix{ @@ -176,10 +179,12 @@ void VrpStartsEnds() { PrintSolution(data, manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpStartsEnds(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpStartsEnds(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_starts_ends.py b/ortools/routing/samples/vrp_starts_ends.py new file mode 100644 index 00000000000..4b72cd1c7e2 --- /dev/null +++ b/ortools/routing/samples/vrp_starts_ends.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Simple Vehicles Routing Problem.""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["distance_matrix"] = [ + # fmt: off + [0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662], + [548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210], + [776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754], + [696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358], + [582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244], + [274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708], + [502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480], + [194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856], + [308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514], + [194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468], + [536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354], + [502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844], + [388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730], + [354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536], + [468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194], + [776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798], + [662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0], + # fmt: on + ] + data["num_vehicles"] = 4 + # [START starts_ends] + data["starts"] = [1, 2, 15, 16] + data["ends"] = [0, 0, 0, 0] + # [END starts_ends] + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + max_route_distance = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} -> " + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f"{manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + max_route_distance = max(route_distance, max_route_distance) + print(f"Maximum of the route distances: {max_route_distance}m") + # [END solution_printer] + + +def main(): + """Entry point of the program.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["distance_matrix"]), + data["num_vehicles"], + data["starts"], + data["ends"], + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + # Convert from routing variable Index to distance matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["distance_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing.AddDimension( + transit_callback_index, + 0, # no slack + 2000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + # [END distance_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(data, manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() + # [END program] diff --git a/ortools/constraint_solver/samples/vrp_time_windows.cc b/ortools/routing/samples/vrp_time_windows.cc similarity index 93% rename from ortools/constraint_solver/samples/vrp_time_windows.cc rename to ortools/routing/samples/vrp_time_windows.cc index a3440d12ca3..df4a7d8ab0e 100644 --- a/ortools/constraint_solver/samples/vrp_time_windows.cc +++ b/ortools/routing/samples/vrp_time_windows.cc @@ -20,16 +20,19 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] // [START program_part1] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> time_matrix{ @@ -198,10 +201,12 @@ void VrpTimeWindows() { PrintSolution(data, manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpTimeWindows(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpTimeWindows(); return EXIT_SUCCESS; } // [END program_part1] diff --git a/ortools/routing/samples/vrp_time_windows.py b/ortools/routing/samples/vrp_time_windows.py new file mode 100644 index 00000000000..c66f6dc4646 --- /dev/null +++ b/ortools/routing/samples/vrp_time_windows.py @@ -0,0 +1,199 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Vehicles Routing Problem (VRP) with Time Windows.""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["time_matrix"] = [ + [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], + [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], + [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], + [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], + [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], + [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], + [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], + [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], + [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], + [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], + [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], + [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], + [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], + [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], + [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], + [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], + [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], + ] + data["time_windows"] = [ + (0, 5), # depot + (7, 12), # 1 + (10, 15), # 2 + (16, 18), # 3 + (10, 13), # 4 + (0, 5), # 5 + (5, 10), # 6 + (0, 4), # 7 + (5, 10), # 8 + (0, 3), # 9 + (10, 16), # 10 + (10, 15), # 11 + (0, 5), # 12 + (5, 10), # 13 + (7, 8), # 14 + (10, 15), # 15 + (11, 15), # 16 + ] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(data, manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + time_dimension = routing.GetDimensionOrDie("Time") + total_time = 0 + for vehicle_id in range(data["num_vehicles"]): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + while not routing.IsEnd(index): + time_var = time_dimension.CumulVar(index) + plan_output += ( + f"{manager.IndexToNode(index)}" + f" Time({solution.Min(time_var)},{solution.Max(time_var)})" + " -> " + ) + index = solution.Value(routing.NextVar(index)) + time_var = time_dimension.CumulVar(index) + plan_output += ( + f"{manager.IndexToNode(index)}" + f" Time({solution.Min(time_var)},{solution.Max(time_var)})\n" + ) + plan_output += f"Time of the route: {solution.Min(time_var)}min\n" + print(plan_output) + total_time += solution.Min(time_var) + print(f"Total time of all routes: {total_time}min") + # [END solution_printer] + + +def main(): + """Solve the VRP with time windows.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["time_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def time_callback(from_index, to_index): + """Returns the travel time between the two nodes.""" + # Convert from routing variable Index to time matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["time_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(time_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Time Windows constraint. + # [START time_windows_constraint] + time = "Time" + routing.AddDimension( + transit_callback_index, + 30, # allow waiting time + 30, # maximum time per vehicle + False, # Don't force start cumul to zero. + time, + ) + time_dimension = routing.GetDimensionOrDie(time) + # Add time window constraints for each location except depot. + for location_idx, time_window in enumerate(data["time_windows"]): + if location_idx == data["depot"]: + continue + index = manager.NodeToIndex(location_idx) + time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) + # Add time window constraints for each vehicle start node. + depot_idx = data["depot"] + for vehicle_id in range(data["num_vehicles"]): + index = routing.Start(vehicle_id) + time_dimension.CumulVar(index).SetRange( + data["time_windows"][depot_idx][0], data["time_windows"][depot_idx][1] + ) + # [END time_windows_constraint] + + # Instantiate route start and end times to produce feasible times. + # [START depot_start_end_times] + for i in range(data["num_vehicles"]): + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.Start(i)) + ) + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.End(i)) + ) + # [END depot_start_end_times] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(data, manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/routing/samples/vrp_time_windows_per_vehicles.py b/ortools/routing/samples/vrp_time_windows_per_vehicles.py new file mode 100755 index 00000000000..dac4e9abdf6 --- /dev/null +++ b/ortools/routing/samples/vrp_time_windows_per_vehicles.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# [START program] +"""Vehicles Routing Problem (VRP) with Time Window (TW) per vehicle. + +All time are in minutes using 0am as origin +e.g. 8am = 480, 11am = 660, 1pm = 780 ... + +We have 1 depot (0) and 16 locations (1-16). +We have a fleet of 4 vehicles (0-3) whose working time is [480, 1020] (8am-5pm) +We have the distance matrix between these locations and depot. +We have a service time of 25min at each location. + +Locations are duplicated so we can simulate a TW per vehicle. +location: [01-16] vehicle: 0 TW: [540, 660] (9am-11am) +location: [17-32] vehicle: 1 TW: [660, 780] (11am-1pm) +location: [33-48] vehicle: 2 TW: [780, 900] (1pm-3pm) +location: [49-64] vehicle: 3 TW: [900, 1020] (3pm-5pm) +""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting +# [END import] + + +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["time_matrix"] = [ + [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], + [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], + [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], + [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], + [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], + [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], + [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], + [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], + [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], + [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], + [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], + [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], + [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], + [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], + [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], + [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], + [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], + ] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + # [END data_model] + + +# [START solution_printer] +def print_solution(manager, routing, assignment): + """Prints solution on console.""" + print(f"Objective: {assignment.ObjectiveValue()}") + # Display dropped nodes. + dropped_nodes = "Dropped nodes:" + for index in range(routing.Size()): + if routing.IsStart(index) or routing.IsEnd(index): + continue + if assignment.Value(routing.NextVar(index)) == index: + node = manager.IndexToNode(index) + if node > 16: + original = node + while original > 16: + original = original - 16 + dropped_nodes += f" {node}({original})" + else: + dropped_nodes += f" {node}" + print(dropped_nodes) + # Display routes + time_dimension = routing.GetDimensionOrDie("Time") + total_time = 0 + for vehicle_id in range(manager.GetNumberOfVehicles()): + if not routing.IsVehicleUsed(assignment, vehicle_id): + continue + plan_output = f"Route for vehicle {vehicle_id}:\n" + index = routing.Start(vehicle_id) + start_time = 0 + while not routing.IsEnd(index): + time_var = time_dimension.CumulVar(index) + node = manager.IndexToNode(index) + if node > 16: + original = node + while original > 16: + original = original - 16 + plan_output += f"{node}({original})" + else: + plan_output += f"{node}" + plan_output += f" Time:{assignment.Value(time_var)} -> " + if start_time == 0: + start_time = assignment.Value(time_var) + index = assignment.Value(routing.NextVar(index)) + time_var = time_dimension.CumulVar(index) + node = manager.IndexToNode(index) + plan_output += f"{node} Time:{assignment.Value(time_var)}\n" + end_time = assignment.Value(time_var) + duration = end_time - start_time + plan_output += f"Duration of the route:{duration}min\n" + print(plan_output) + total_time += duration + print(f"Total duration of all routes: {total_time}min") + # [END solution_printer] + + +def main(): + """Solve the VRP with time windows.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + 1 + 16 * 4, data["num_vehicles"], data["depot"] # number of locations + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def time_callback(from_index, to_index): + """Returns the travel time between the two nodes.""" + # Convert from routing variable Index to time matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + # since our matrix is 17x17 map duplicated node to original one to + # retrieve the travel time + while from_node > 16: + from_node = from_node - 16 + while to_node > 16: + to_node = to_node - 16 + # add service of 25min for each location (except depot) + service_time = 0 + if from_node != data["depot"]: + service_time = 25 + return data["time_matrix"][from_node][to_node] + service_time + + transit_callback_index = routing.RegisterTransitCallback(time_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Time Windows constraint. + # [START time_windows_constraint] + time = "Time" + routing.AddDimension( + transit_callback_index, + 0, # allow waiting time (0 min) + 1020, # maximum time per vehicle (9 hours) + False, # Don't force start cumul to zero. + time, + ) + time_dimension = routing.GetDimensionOrDie(time) + # Add time window constraints for each location except depot. + for location_idx in range(17): + if location_idx == data["depot"]: + continue + # Vehicle 0 location TW: [9am, 11am] + index_0 = manager.NodeToIndex(location_idx) + time_dimension.CumulVar(index_0).SetRange(540, 660) + routing.VehicleVar(index_0).SetValues([-1, 0]) + + # Vehicle 1 location TW: [11am, 1pm] + index_1 = manager.NodeToIndex(location_idx + 16 * 1) + time_dimension.CumulVar(index_1).SetRange(660, 780) + routing.VehicleVar(index_1).SetValues([-1, 1]) + + # Vehicle 2 location TW: [1pm, 3pm] + index_2 = manager.NodeToIndex(location_idx + 16 * 2) + time_dimension.CumulVar(index_2).SetRange(780, 900) + routing.VehicleVar(index_2).SetValues([-1, 2]) + + # Vehicle 3 location TW: [3pm, 5pm] + index_3 = manager.NodeToIndex(location_idx + 16 * 3) + time_dimension.CumulVar(index_3).SetRange(900, 1020) + routing.VehicleVar(index_3).SetValues([-1, 3]) + + # Add Disjunction so only one node among duplicate is visited + penalty = 100_000 # Give solver strong incentive to visit one node + routing.AddDisjunction([index_0, index_1, index_2, index_3], penalty, 1) + + # Add time window constraints for each vehicle start node. + depot_idx = data["depot"] + for vehicle_id in range(data["num_vehicles"]): + index = routing.Start(vehicle_id) + time_dimension.CumulVar(index).SetRange(480, 1020) # (8am, 5pm) + + # Add time window constraints for each vehicle end node. + depot_idx = data["depot"] + for vehicle_id in range(data["num_vehicles"]): + index = routing.End(vehicle_id) + time_dimension.CumulVar(index).SetRange(480, 1020) # (8am, 5pm) + # [END time_windows_constraint] + + # Instantiate route start and end times to produce feasible times. + # [START depot_start_end_times] + for i in range(data["num_vehicles"]): + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.Start(i)) + ) + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.End(i)) + ) + # [END depot_start_end_times] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(1) + # [END parameters] + + # Solve the problem. + # [START solve] + assignment = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if assignment: + print_solution(manager, routing, assignment) + else: + print("no solution found !") + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/routing/samples/vrp_tokens.py b/ortools/routing/samples/vrp_tokens.py new file mode 100755 index 00000000000..00325bd66e7 --- /dev/null +++ b/ortools/routing/samples/vrp_tokens.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Simple VRP with special locations which need to be visited at end of the route.""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +def create_data_model(): + """Stores the data for the problem.""" + data = {} + # Special location don't consume token, while regular one consume one + data["tokens"] = [ + 0, # 0 depot + 0, # 1 special node + 0, # 2 special node + 0, # 3 special node + 0, # 4 special node + 0, # 5 special node + -1, # 6 + -1, # 7 + -1, # 8 + -1, # 9 + -1, # 10 + -1, # 11 + -1, # 12 + -1, # 13 + -1, # 14 + -1, # 15 + -1, # 16 + -1, # 17 + -1, # 18 + ] + # just need to be big enough, not a limiting factor + data["vehicle_tokens"] = [20, 20, 20, 20] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + + +def print_solution(manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + token_dimension = routing.GetDimensionOrDie("Token") + total_distance = 0 + total_token = 0 + for vehicle_id in range(manager.GetNumberOfVehicles()): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + plan_output = f"Route for vehicle {vehicle_id}:\n" + index = routing.Start(vehicle_id) + total_token += solution.Value(token_dimension.CumulVar(index)) + route_distance = 0 + route_token = 0 + while not routing.IsEnd(index): + node_index = manager.IndexToNode(index) + token_var = token_dimension.CumulVar(index) + route_token = solution.Value(token_var) + plan_output += f" {node_index} Token({route_token}) -> " + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + node_index = manager.IndexToNode(index) + token_var = token_dimension.CumulVar(index) + route_token = solution.Value(token_var) + plan_output += f" {node_index} Token({route_token})\n" + plan_output += f"Distance of the route: {route_distance}m\n" + total_distance += route_distance + print(plan_output) + print(f"Total distance of all routes: {total_distance}m") + print(f"Total token of all routes: {total_token}") + + +def main(): + """Solve the CVRP problem.""" + # Instantiate the data problem. + data = create_data_model() + + # Create the routing index manager. + manager = pywraprouting.RoutingIndexManager( + len(data["tokens"]), data["num_vehicles"], data["depot"] + ) + + # Create Routing Model. + routing = pywraprouting.RoutingModel(manager) + + # Create and register a transit callback. + def distance_callback(from_index, to_index): + """Returns the distance between the two nodes.""" + del from_index + del to_index + return 10 + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + + routing.AddDimension( + transit_callback_index, + 0, # null slack + 3000, # maximum distance per vehicle + True, # start cumul to zero + "distance", + ) + distance_dimension = routing.GetDimensionOrDie("distance") + distance_dimension.SetGlobalSpanCostCoefficient(100) + + # Define cost of each arc. + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + + # Add Token constraint. + def token_callback(from_index): + """Returns the number of token consumed by the node.""" + # Convert from routing variable Index to tokens NodeIndex. + from_node = manager.IndexToNode(from_index) + return data["tokens"][from_node] + + token_callback_index = routing.RegisterUnaryTransitCallback(token_callback) + routing.AddDimensionWithVehicleCapacity( + token_callback_index, + 0, # null capacity slack + data["vehicle_tokens"], # vehicle maximum tokens + False, # start cumul to zero + "Token", + ) + # Add constraint: special node can only be visited if token remaining is zero + token_dimension = routing.GetDimensionOrDie("Token") + for node in range(1, 6): + index = manager.NodeToIndex(node) + routing.solver().Add(token_dimension.CumulVar(index) == 0) + + # Instantiate route start and end times to produce feasible times. + # [START depot_start_end_times] + for i in range(manager.GetNumberOfVehicles()): + routing.AddVariableMinimizedByFinalizer( + token_dimension.CumulVar(routing.Start(i)) + ) + routing.AddVariableMinimizedByFinalizer( + token_dimension.CumulVar(routing.End(i)) + ) + # [END depot_start_end_times] + + # Setting first solution heuristic. + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.time_limit.FromSeconds(1) + + # Solve the problem. + solution = routing.SolveWithParameters(search_parameters) + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(manager, routing, solution) + else: + print("No solution found !") + # [END print_solution] + + +if __name__ == "__main__": + main() diff --git a/ortools/constraint_solver/samples/vrp_with_time_limit.cc b/ortools/routing/samples/vrp_with_time_limit.cc similarity index 88% rename from ortools/constraint_solver/samples/vrp_with_time_limit.cc rename to ortools/routing/samples/vrp_with_time_limit.cc index ab8bfa772ca..06e1700b21a 100644 --- a/ortools/constraint_solver/samples/vrp_with_time_limit.cc +++ b/ortools/routing/samples/vrp_with_time_limit.cc @@ -18,16 +18,19 @@ #include #include +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" #include "google/protobuf/duration.pb.h" -#include "ortools/base/logging.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] -namespace operations_research { +namespace operations_research::routing { //! @brief Print the solution. //! @param[in] manager Index manager used. //! @param[in] routing Routing solver used. @@ -127,10 +130,12 @@ void VrpGlobalSpan() { PrintSolution(manager, routing, *solution); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpGlobalSpan(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpGlobalSpan(); return EXIT_SUCCESS; } // [END program] diff --git a/ortools/routing/samples/vrp_with_time_limit.py b/ortools/routing/samples/vrp_with_time_limit.py new file mode 100644 index 00000000000..a7385650682 --- /dev/null +++ b/ortools/routing/samples/vrp_with_time_limit.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""Vehicles Routing Problem (VRP).""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START solution_printer] +def print_solution(manager, routing, solution): + """Prints solution on console.""" + print(f"Objective: {solution.ObjectiveValue()}") + max_route_distance = 0 + for vehicle_id in range(manager.GetNumberOfVehicles()): + if not routing.IsVehicleUsed(solution, vehicle_id): + continue + index = routing.Start(vehicle_id) + plan_output = f"Route for vehicle {vehicle_id}:\n" + route_distance = 0 + while not routing.IsEnd(index): + plan_output += f" {manager.IndexToNode(index)} -> " + previous_index = index + index = solution.Value(routing.NextVar(index)) + route_distance += routing.GetArcCostForVehicle( + previous_index, index, vehicle_id + ) + plan_output += f"{manager.IndexToNode(index)}\n" + plan_output += f"Distance of the route: {route_distance}m\n" + print(plan_output) + max_route_distance = max(route_distance, max_route_distance) + print(f"Maximum of the route distances: {max_route_distance}m") + # [END solution_printer] + + +def main(): + """Solve the CVRP problem.""" + # Instantiate the data problem. + # [START data] + num_locations = 20 + num_vehicles = 5 + depot = 0 + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + num_locations, num_vehicles, depot + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def distance_callback(from_index, to_index): + # pylint: disable=unused-argument + """Returns the distance between the two nodes.""" + return 1 + + transit_callback_index = routing.RegisterTransitCallback(distance_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Distance constraint. + # [START distance_constraint] + dimension_name = "Distance" + routing.AddDimension( + transit_callback_index, + 0, # no slack + 3000, # vehicle maximum travel distance + True, # start cumul to zero + dimension_name, + ) + distance_dimension = routing.GetDimensionOrDie(dimension_name) + distance_dimension.SetGlobalSpanCostCoefficient(100) + # [END distance_constraint] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + search_parameters.local_search_metaheuristic = ( + enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH + ) + search_parameters.log_search = True + search_parameters.time_limit.FromSeconds(5) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution on console. + # [START print_solution] + if solution: + print_solution(manager, routing, solution) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program] diff --git a/ortools/constraint_solver/samples/vrptw_store_solution_data.cc b/ortools/routing/samples/vrptw_store_solution_data.cc similarity index 94% rename from ortools/constraint_solver/samples/vrptw_store_solution_data.cc rename to ortools/routing/samples/vrptw_store_solution_data.cc index f8f15dfc158..70d15c0bb8f 100644 --- a/ortools/constraint_solver/samples/vrptw_store_solution_data.cc +++ b/ortools/routing/samples/vrptw_store_solution_data.cc @@ -20,16 +20,19 @@ #include #include -#include "ortools/base/logging.h" +#include "absl/base/log_severity.h" +#include "absl/log/globals.h" +#include "absl/log/log.h" +#include "ortools/base/init_google.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_index_manager.h" -#include "ortools/constraint_solver/routing_parameters.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/index_manager.h" +#include "ortools/routing/parameters.h" +#include "ortools/routing/routing.h" // [END import] // [START program_part1] -namespace operations_research { +namespace operations_research::routing { // [START data_model] struct DataModel { const std::vector> time_matrix{ @@ -238,10 +241,12 @@ void VrpTimeWindows() { GetCumulData(*solution, routing, time_dimension)); // [END print_solution] } -} // namespace operations_research +} // namespace operations_research::routing -int main(int /*argc*/, char* /*argv*/[]) { - operations_research::VrpTimeWindows(); +int main(int argc, char* argv[]) { + InitGoogle(argv[0], &argc, &argv, true); + absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo); + operations_research::routing::VrpTimeWindows(); return EXIT_SUCCESS; } // [END program_part1] diff --git a/ortools/routing/samples/vrptw_store_solution_data.py b/ortools/routing/samples/vrptw_store_solution_data.py new file mode 100644 index 00000000000..ac90f20a76c --- /dev/null +++ b/ortools/routing/samples/vrptw_store_solution_data.py @@ -0,0 +1,259 @@ +#!/usr/bin/env python3 +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# [START program] +"""VRPTW example that stores routes and cumulative data in an array.""" + +# [START import] +from ortools.routing import enums_pb2 +from ortools.routing import pywraprouting + +# [END import] + + +# [START program_part1] +# [START data_model] +def create_data_model(): + """Stores the data for the problem.""" + data = {} + data["time_matrix"] = [ + [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7], + [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14], + [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9], + [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16], + [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14], + [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8], + [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5], + [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10], + [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6], + [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5], + [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4], + [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10], + [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8], + [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6], + [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2], + [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9], + [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0], + ] + data["time_windows"] = [ + (0, 5), # depot + (7, 12), # 1 + (10, 15), # 2 + (16, 18), # 3 + (10, 13), # 4 + (0, 5), # 5 + (5, 10), # 6 + (0, 4), # 7 + (5, 10), # 8 + (0, 3), # 9 + (10, 16), # 10 + (10, 15), # 11 + (0, 5), # 12 + (5, 10), # 13 + (7, 8), # 14 + (10, 15), # 15 + (11, 15), # 16 + ] + data["num_vehicles"] = 4 + data["depot"] = 0 + return data + + +# [END data_model] + + +# [START solution_printer] +def print_solution(routes, cumul_data): + """Print the solution.""" + total_time = 0 + route_str = "" + for i, route in enumerate(routes): + if len(route) <= 2: + continue + route_str += "Route " + str(i) + ":\n" + start_time = cumul_data[i][0][0] + end_time = cumul_data[i][0][1] + route_str += ( + " " + + str(route[0]) + + " Time(" + + str(start_time) + + ", " + + str(end_time) + + ")" + ) + for j in range(1, len(route)): + start_time = cumul_data[i][j][0] + end_time = cumul_data[i][j][1] + route_str += ( + " -> " + + str(route[j]) + + " Time(" + + str(start_time) + + ", " + + str(end_time) + + ")" + ) + route_str += f"\n Route time: {start_time}min\n\n" + total_time += cumul_data[i][len(route) - 1][0] + route_str += f"Total time: {total_time}min" + print(route_str) + + +# [END solution_printer] + + +# [START get_routes] +def get_routes(solution, routing, manager): + """Get vehicle routes from a solution and store them in an array.""" + # Get vehicle routes and store them in a two dimensional array whose + # i,j entry is the jth location visited by vehicle i along its route. + routes = [] + for route_nbr in range(routing.vehicles()): + index = routing.Start(route_nbr) + route = [manager.IndexToNode(index)] + while not routing.IsEnd(index): + index = solution.Value(routing.NextVar(index)) + route.append(manager.IndexToNode(index)) + routes.append(route) + return routes + + +# [END get_routes] + + +# [START get_cumulative_data] +def get_cumul_data(solution, routing, dimension): + """Get cumulative data from a dimension and store it in an array.""" + # Returns an array cumul_data whose i,j entry contains the minimum and + # maximum of CumulVar for the dimension at the jth node on route : + # - cumul_data[i][j][0] is the minimum. + # - cumul_data[i][j][1] is the maximum. + + cumul_data = [] + for route_nbr in range(routing.vehicles()): + route_data = [] + index = routing.Start(route_nbr) + dim_var = dimension.CumulVar(index) + route_data.append([solution.Min(dim_var), solution.Max(dim_var)]) + while not routing.IsEnd(index): + index = solution.Value(routing.NextVar(index)) + dim_var = dimension.CumulVar(index) + route_data.append([solution.Min(dim_var), solution.Max(dim_var)]) + cumul_data.append(route_data) + return cumul_data + + +# [END get_cumulative_data] + + +def main(): + """Solve the VRP with time windows.""" + # Instantiate the data problem. + # [START data] + data = create_data_model() + # [END data] + + # Create the routing index manager. + # [START index_manager] + manager = pywraprouting.RoutingIndexManager( + len(data["time_matrix"]), data["num_vehicles"], data["depot"] + ) + # [END index_manager] + + # Create Routing Model. + # [START routing_model] + routing = pywraprouting.RoutingModel(manager) + # [END routing_model] + + # Create and register a transit callback. + # [START transit_callback] + def time_callback(from_index, to_index): + """Returns the travel time between the two nodes.""" + # Convert from routing variable Index to time matrix NodeIndex. + from_node = manager.IndexToNode(from_index) + to_node = manager.IndexToNode(to_index) + return data["time_matrix"][from_node][to_node] + + transit_callback_index = routing.RegisterTransitCallback(time_callback) + # [END transit_callback] + + # Define cost of each arc. + # [START arc_cost] + routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) + # [END arc_cost] + + # Add Time Windows constraint. + # [START time_windows_constraint] + time = "Time" + + routing.AddDimension( + transit_callback_index, + 30, # allow waiting time + 30, # maximum time per vehicle + False, # Don't force cumulative time to be 0 at start of routes. + time, + ) + time_dimension = routing.GetDimensionOrDie(time) + # Add time window constraints for each location except depot. + for location_idx, time_window in enumerate(data["time_windows"]): + if location_idx == 0: + continue + index = manager.NodeToIndex(location_idx) + time_dimension.CumulVar(index).SetRange(time_window[0], time_window[1]) + # Add time window constraints for each vehicle start node. + for vehicle_id in range(data["num_vehicles"]): + index = routing.Start(vehicle_id) + time_dimension.CumulVar(index).SetRange( + data["time_windows"][0][0], data["time_windows"][0][1] + ) + # [END time_windows_constraint] + + # Instantiate route start and end times to produce feasible times. + # [START depot_start_end_times] + for i in range(data["num_vehicles"]): + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.Start(i)) + ) + routing.AddVariableMinimizedByFinalizer( + time_dimension.CumulVar(routing.End(i)) + ) + # [END depot_start_end_times] + + # Setting first solution heuristic. + # [START parameters] + search_parameters = pywraprouting.DefaultRoutingSearchParameters() + search_parameters.first_solution_strategy = ( + enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC + ) + # [END parameters] + + # Solve the problem. + # [START solve] + solution = routing.SolveWithParameters(search_parameters) + # [END solve] + + # Print solution. + # [START print_solution] + if solution: + routes = get_routes(solution, routing, manager) + cumul_data = get_cumul_data(solution, routing, time_dimension) + print_solution(routes, cumul_data) + # [END print_solution] + + +if __name__ == "__main__": + main() +# [END program_part1] +# [END program] diff --git a/ortools/constraint_solver/routing_sat.cc b/ortools/routing/sat.cc similarity index 99% rename from ortools/constraint_solver/routing_sat.cc rename to ortools/routing/sat.cc index d41e16733a2..9395d86c361 100644 --- a/ortools/constraint_solver/routing_sat.cc +++ b/ortools/routing/sat.cc @@ -28,12 +28,12 @@ #include "absl/types/span.h" #include "ortools/base/map_util.h" #include "ortools/constraint_solver/constraint_solver.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_types.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_solver.h" -#include "ortools/sat/integer.h" +#include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" #include "ortools/sat/sat_parameters.pb.h" #include "ortools/util/bitset.h" @@ -41,7 +41,7 @@ #include "ortools/util/saturated_arithmetic.h" #include "ortools/util/time_limit.h" -namespace operations_research { +namespace operations_research::routing { namespace sat { namespace { @@ -1219,4 +1219,4 @@ bool SolveModelWithSat(RoutingModel* model, objective, *model, arc_vars, solution); } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_search.cc b/ortools/routing/search.cc similarity index 95% rename from ortools/constraint_solver/routing_search.cc rename to ortools/routing/search.cc index 3590e60028e..c39bff40b86 100644 --- a/ortools/constraint_solver/routing_search.cc +++ b/ortools/routing/search.cc @@ -16,7 +16,7 @@ // and local search filters. // TODO(user): Move all existing routing search code here. -#include "ortools/constraint_solver/routing_search.h" +#include "ortools/routing/search.h" #include #include @@ -43,6 +43,7 @@ #include "absl/flags/flag.h" #include "absl/log/check.h" #include "absl/log/die_if_null.h" +#include "absl/log/log.h" #include "absl/strings/str_cat.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" @@ -56,12 +57,14 @@ #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_types.h" -#include "ortools/constraint_solver/routing_utils.h" #include "ortools/graph/christofides.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/heuristic_parameters.pb.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/parameters_utils.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" +#include "ortools/routing/utils.h" #include "ortools/util/bitset.h" #include "ortools/util/range_query_function.h" #include "ortools/util/saturated_arithmetic.h" @@ -77,7 +80,7 @@ ABSL_FLAG(int64_t, sweep_sectors, 1, "The number of sectors the space is divided into before it is sweeped" " by the ray."); -namespace operations_research { +namespace operations_research::routing { namespace { void ConvertAssignment(const RoutingModel* src_model, const Assignment* src, @@ -953,7 +956,8 @@ GlobalCheapestInsertionFilteredHeuristic:: gci_params_(parameters), node_index_to_vehicle_(model->Size(), -1), node_index_to_neighbors_by_cost_class_(nullptr), - empty_vehicle_type_curator_(nullptr) { + empty_vehicle_type_curator_(nullptr), + temp_inserted_nodes_(model->Size()) { CHECK_GT(gci_params_.neighbors_ratio, 0); CHECK_LE(gci_params_.neighbors_ratio, 1); CHECK_GE(gci_params_.min_neighbors, 1); @@ -1053,6 +1057,9 @@ bool GlobalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() { if (!InsertPairsAndNodesByRequirementTopologicalOrder()) { return unperform_unassigned_and_check(); } + if (!InsertPairsAndNodesByPrecedenceTopologicalOrder()) { + return unperform_unassigned_and_check(); + } // TODO(user): Adapt the pair insertions to also support seed and // sequential insertion. @@ -1101,6 +1108,46 @@ bool GlobalCheapestInsertionFilteredHeuristic:: return true; } +bool GlobalCheapestInsertionFilteredHeuristic:: + InsertPairsAndNodesByPrecedenceTopologicalOrder() { + const std::vector& pickup_delivery_pairs = + model()->GetPickupAndDeliveryPairs(); + for (const std::vector>& ordered_nodes : + model()->GetTopologicallySortedNodePrecedences()) { + for (const std::vector& nodes : ordered_nodes) { + std::map> pairs_to_insert_by_bucket; + for (int node : nodes) { + if (Contains(node)) continue; + if (!model()->IsPickup(node) && !model()->IsDelivery(node)) continue; + const std::optional + pickup_position = model()->GetPickupPosition(node); + if (pickup_position.has_value()) { + const int index = pickup_position->pd_pair_index; + pairs_to_insert_by_bucket[GetBucketOfPair( + pickup_delivery_pairs[index])] + .push_back(index); + } + const std::optional + delivery_position = model()->GetDeliveryPosition(node); + if (delivery_position.has_value()) { + const int index = delivery_position->pd_pair_index; + pairs_to_insert_by_bucket[GetBucketOfPair( + pickup_delivery_pairs[index])] + .push_back(index); + } + } + if (!InsertPairs(pairs_to_insert_by_bucket)) return false; + std::map> nodes_by_bucket; + for (int node : nodes) { + if (Contains(node)) continue; + nodes_by_bucket[GetBucketOfNode(node)].push_back(node); + } + if (!InsertNodesOnRoutes(nodes_by_bucket, {})) return false; + } + } + return true; +} + bool GlobalCheapestInsertionFilteredHeuristic::InsertPairs( const std::map>& pair_indices_by_bucket) { AdjustablePriorityQueue priority_queue; @@ -1955,12 +2002,10 @@ bool GlobalCheapestInsertionFilteredHeuristic::UpdateAfterPairInsertion( DCHECK(delivery_to_entries->at(delivery).empty()); // Update cost of existing entries after nodes which have new nexts // (pickup_position and delivery_position). - if (!UpdateExistingPairEntriesOnChain(pickup_position, Value(pickup_position), - priority_queue, pickup_to_entries, - delivery_to_entries) || - !UpdateExistingPairEntriesOnChain( - delivery_position, Value(delivery_position), priority_queue, - pickup_to_entries, delivery_to_entries)) { + if (!UpdateExistingPairEntriesAfter(pickup_position, priority_queue, + pickup_to_entries, delivery_to_entries) || + !UpdateExistingPairEntriesAfter(delivery_position, priority_queue, + pickup_to_entries, delivery_to_entries)) { return false; } // Add new entries after nodes which have been inserted (pickup and delivery). @@ -1981,45 +2026,41 @@ bool GlobalCheapestInsertionFilteredHeuristic::UpdateAfterPairInsertion( return true; } -bool GlobalCheapestInsertionFilteredHeuristic::UpdateExistingPairEntriesOnChain( - int64_t insert_after_start, int64_t insert_after_end, +bool GlobalCheapestInsertionFilteredHeuristic::UpdateExistingPairEntriesAfter( + int64_t insert_after, AdjustablePriorityQueue< GlobalCheapestInsertionFilteredHeuristic::PairEntry>* priority_queue, std::vector* pickup_to_entries, std::vector* delivery_to_entries) { - int64_t insert_after = insert_after_start; - while (insert_after != insert_after_end) { - DCHECK(!model()->IsEnd(insert_after)); - // Remove entries at 'insert_after' with nodes which have already been - // inserted and update remaining entries. - std::vector to_remove; - for (const PairEntries* pair_entries : - {&pickup_to_entries->at(insert_after), - &delivery_to_entries->at(insert_after)}) { - if (StopSearchAndCleanup(priority_queue)) return false; - for (PairEntry* const pair_entry : *pair_entries) { - DCHECK(priority_queue->Contains(pair_entry)); - if (Contains(pair_entry->pickup_to_insert()) || - Contains(pair_entry->delivery_to_insert())) { + DCHECK(!model()->IsEnd(insert_after)); + // Remove entries at 'insert_after' with nodes which have already been + // inserted and update remaining entries. + std::vector to_remove; + for (const PairEntries* pair_entries : + {&pickup_to_entries->at(insert_after), + &delivery_to_entries->at(insert_after)}) { + if (StopSearchAndCleanup(priority_queue)) return false; + for (PairEntry* const pair_entry : *pair_entries) { + DCHECK(priority_queue->Contains(pair_entry)); + if (Contains(pair_entry->pickup_to_insert()) || + Contains(pair_entry->delivery_to_insert())) { + to_remove.push_back(pair_entry); + } else { + DCHECK(pickup_to_entries->at(pair_entry->pickup_insert_after()) + .contains(pair_entry)); + DCHECK(delivery_to_entries->at(pair_entry->delivery_insert_after()) + .contains(pair_entry)); + if (!UpdatePairEntry(pair_entry, priority_queue)) { to_remove.push_back(pair_entry); - } else { - DCHECK(pickup_to_entries->at(pair_entry->pickup_insert_after()) - .contains(pair_entry)); - DCHECK(delivery_to_entries->at(pair_entry->delivery_insert_after()) - .contains(pair_entry)); - if (!UpdatePairEntry(pair_entry, priority_queue)) { - to_remove.push_back(pair_entry); - } } } } - for (PairEntry* const pair_entry : to_remove) { - DeletePairEntry(pair_entry, priority_queue, pickup_to_entries, - delivery_to_entries); - } - insert_after = Value(insert_after); + } + for (PairEntry* const pair_entry : to_remove) { + DeletePairEntry(pair_entry, priority_queue, pickup_to_entries, + delivery_to_entries); } return true; } @@ -2308,9 +2349,7 @@ bool GlobalCheapestInsertionFilteredHeuristic::UpdateAfterNodeInsertion( int64_t insert_after, bool all_vehicles, NodeEntryQueue* queue) { // Update cost of existing entries after "insert_after" which now have new // nexts. - if (!UpdateExistingNodeEntriesOnChain(nodes, vehicle, insert_after, - Value(insert_after), all_vehicles, - queue)) { + if (!AddNodeEntriesAfter(nodes, vehicle, insert_after, all_vehicles, queue)) { return false; } // Add new entries after "node" which has just been inserted. @@ -2318,9 +2357,6 @@ bool GlobalCheapestInsertionFilteredHeuristic::UpdateAfterNodeInsertion( // in the case where we have non-full neighborhoods. This will leverage the // incoming neighbors of the newly inserted node, in case they're not also // outgoing neighbors of 'insert_after'. - // NOTE: UpdateExistingNodeEntriesOnChain() could return the set of node - // indices that already have entries between insert_after and node, to avoid - // adding entries again for them when looking at incoming neighbors of node. if (!AddNodeEntriesAfter(nodes, vehicle, node, all_vehicles, queue)) { return false; } @@ -2328,46 +2364,68 @@ bool GlobalCheapestInsertionFilteredHeuristic::UpdateAfterNodeInsertion( return true; } -bool GlobalCheapestInsertionFilteredHeuristic::UpdateExistingNodeEntriesOnChain( - const SparseBitset& nodes, int vehicle, int64_t insert_after_start, - int64_t insert_after_end, bool all_vehicles, NodeEntryQueue* queue) { - int64_t insert_after = insert_after_start; - while (insert_after != insert_after_end) { - DCHECK(!model()->IsEnd(insert_after)); - AddNodeEntriesAfter(nodes, vehicle, insert_after, all_vehicles, queue); - insert_after = Value(insert_after); - } - return true; -} - bool GlobalCheapestInsertionFilteredHeuristic::AddNodeEntriesAfter( const SparseBitset& nodes, int vehicle, int64_t insert_after, bool all_vehicles, NodeEntryQueue* queue) { + temp_inserted_nodes_.ResetAllToFalse(); const int cost_class = model()->GetCostClassIndexOfVehicle(vehicle).value(); // Remove existing entries at 'insert_after', needed either when updating // entries or if unperformed node insertions were present. queue->ClearInsertions(insert_after); - const std::vector& neighbors = - node_index_to_neighbors_by_cost_class_ - ->GetOutgoingNeighborsOfNodeForCostClass(cost_class, insert_after); - if (neighbors.size() < nodes.NumberOfSetCallsWithDifferentArguments()) { - // Iterate on the neighbors. - for (int node : neighbors) { - if (StopSearch()) return false; - if (!Contains(node) && nodes[node]) { - AddNodeEntry(node, insert_after, vehicle, all_vehicles, queue); - } - } - } else { - // Iterate on the nodes to insert. - for (int node : nodes.PositionsSetAtLeastOnce()) { - if (StopSearch()) return false; - if (!Contains(node) && - node_index_to_neighbors_by_cost_class_->IsNeighborhoodArcForCostClass( - cost_class, insert_after, node)) { - AddNodeEntry(node, insert_after, vehicle, all_vehicles, queue); - } + + const auto add_node_entries_for_neighbors = + [this, &nodes, &queue, insert_after, vehicle, all_vehicles]( + absl::Span neighbors, + const std::function& is_neighbor) { + if (neighbors.size() < nodes.NumberOfSetCallsWithDifferentArguments()) { + // Iterate on the neighbors of 'node'. + for (int node : neighbors) { + if (StopSearch()) return false; + if (!Contains(node) && nodes[node] && !temp_inserted_nodes_[node]) { + temp_inserted_nodes_.Set(node); + AddNodeEntry(node, insert_after, vehicle, all_vehicles, queue); + } + } + } else { + // Iterate on the nodes to insert. + for (int node : nodes.PositionsSetAtLeastOnce()) { + if (StopSearch()) return false; + if (!Contains(node) && is_neighbor(node) && + !temp_inserted_nodes_[node]) { + temp_inserted_nodes_.Set(node); + AddNodeEntry(node, insert_after, vehicle, all_vehicles, queue); + } + } + } + return true; + }; + + if (!add_node_entries_for_neighbors( + node_index_to_neighbors_by_cost_class_ + ->GetOutgoingNeighborsOfNodeForCostClass(cost_class, + insert_after), + [this, insert_after, cost_class](int64_t node) { + return node_index_to_neighbors_by_cost_class_ + ->IsNeighborhoodArcForCostClass(cost_class, insert_after, node); + })) { + return false; + } + + if (!node_index_to_neighbors_by_cost_class_->IsFullNeighborhood()) { + // When we have a non-full neighborhood, we also add entries for incoming + // neighbors of the successor of 'insert_after'. + const int64_t insert_before = Value(insert_after); + if (model()->IsEnd(insert_before)) { + // We don't explore incoming neighbors of an end node. + return true; } + return add_node_entries_for_neighbors( + node_index_to_neighbors_by_cost_class_ + ->GetIncomingNeighborsOfNodeForCostClass(cost_class, insert_before), + [this, insert_before, cost_class](int64_t node) { + return node_index_to_neighbors_by_cost_class_ + ->IsNeighborhoodArcForCostClass(cost_class, node, insert_before); + }); } return true; } @@ -2511,9 +2569,7 @@ LocalCheapestInsertionFilteredHeuristic:: LocalCheapestInsertionFilteredHeuristic( RoutingModel* model, std::function stop_search, std::function evaluator, - RoutingSearchParameters::PairInsertionStrategy pair_insertion_strategy, - std::vector - insertion_sorting_properties, + LocalCheapestInsertionParameters lci_params, LocalSearchFilterManager* filter_manager, bool use_first_solution_hint, BinCapacities* bin_capacities, std::function&, @@ -2522,15 +2578,16 @@ LocalCheapestInsertionFilteredHeuristic:: : CheapestInsertionFilteredHeuristic(model, std::move(stop_search), std::move(evaluator), nullptr, filter_manager), - pair_insertion_strategy_(pair_insertion_strategy), - insertion_sorting_properties_(std::move(insertion_sorting_properties)), + pair_insertion_strategy_(lci_params.pickup_delivery_strategy()), + insertion_sorting_properties_(GetLocalCheapestInsertionSortingProperties( + lci_params.insertion_sorting_properties())), use_first_solution_hint_(use_first_solution_hint), bin_capacities_(bin_capacities), optimize_on_insertion_(std::move(optimize_on_insertion)), use_random_insertion_order_( !insertion_sorting_properties_.empty() && insertion_sorting_properties_.front() == - RoutingSearchParameters::SORTING_PROPERTY_RANDOM) { + LocalCheapestInsertionParameters::SORTING_PROPERTY_RANDOM) { DCHECK(!insertion_sorting_properties_.empty()); } @@ -2861,88 +2918,88 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { insertion_order_.reserve(model.Size() + model.GetPickupAndDeliveryPairs().size()); - auto get_insertion_properties = [this](int64_t penalty, - int64_t num_allowed_vehicles, - int64_t avg_distance_to_vehicle, - int64_t neg_min_distance_to_vehicles, - int hint_weight, - int reversed_hint_weight, - int64_t avg_dimension_usage) { - DCHECK_NE(0, num_allowed_vehicles); - absl::InlinedVector properties; - properties.reserve(insertion_sorting_properties_.size()); - - // Always consider hints first. We favor nodes with hints over nodes with - // reversed hints. - // TODO(user): Figure out a way to insert hinted nodes in a logical - // order. We could try toposorting hinted nodes. - properties.push_back(-hint_weight); - properties.push_back(-reversed_hint_weight); - - bool neg_min_distance_to_vehicles_appended = false; - for (const int property : insertion_sorting_properties_) { - switch (property) { - case RoutingSearchParameters::SORTING_PROPERTY_ALLOWED_VEHICLES: - properties.push_back(num_allowed_vehicles); - break; - case RoutingSearchParameters::SORTING_PROPERTY_PENALTY: - properties.push_back(CapOpp(penalty)); - break; - case RoutingSearchParameters:: - SORTING_PROPERTY_PENALTY_OVER_ALLOWED_VEHICLES_RATIO: - properties.push_back(CapOpp(penalty / num_allowed_vehicles)); - break; - case RoutingSearchParameters:: - SORTING_PROPERTY_HIGHEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS: - properties.push_back(CapOpp(avg_distance_to_vehicle)); - break; - case RoutingSearchParameters:: - SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS: - properties.push_back(avg_distance_to_vehicle); - break; - case RoutingSearchParameters:: - SORTING_PROPERTY_LOWEST_MIN_ARC_COST_TO_VEHICLE_START_ENDS: - properties.push_back(neg_min_distance_to_vehicles); - neg_min_distance_to_vehicles_appended = true; - break; - case RoutingSearchParameters::SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE: - properties.push_back(CapOpp(avg_dimension_usage)); - break; - default: - LOG(DFATAL) - << "Unknown RoutingSearchParameter::InsertionSortingProperty " - "used!"; - break; - } - } + auto get_insertion_properties = + [this](int64_t penalty, int64_t num_allowed_vehicles, + int64_t avg_distance_to_vehicle, + int64_t neg_min_distance_to_vehicles, int hint_weight, + int reversed_hint_weight, int64_t avg_dimension_usage) { + DCHECK_NE(0, num_allowed_vehicles); + absl::InlinedVector properties; + properties.reserve(insertion_sorting_properties_.size()); + + // Always consider hints first. We favor nodes with hints over nodes + // with reversed hints. + // TODO(user): Figure out a way to insert hinted nodes in a logical + // order. We could try toposorting hinted nodes. + properties.push_back(-hint_weight); + properties.push_back(-reversed_hint_weight); + + bool neg_min_distance_to_vehicles_appended = false; + for (const int property : insertion_sorting_properties_) { + switch (property) { + case LocalCheapestInsertionParameters:: + SORTING_PROPERTY_ALLOWED_VEHICLES: + properties.push_back(num_allowed_vehicles); + break; + case LocalCheapestInsertionParameters::SORTING_PROPERTY_PENALTY: + properties.push_back(CapOpp(penalty)); + break; + case LocalCheapestInsertionParameters:: + SORTING_PROPERTY_PENALTY_OVER_ALLOWED_VEHICLES_RATIO: + properties.push_back(CapOpp(penalty / num_allowed_vehicles)); + break; + case LocalCheapestInsertionParameters:: + SORTING_PROPERTY_HIGHEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS: + properties.push_back(CapOpp(avg_distance_to_vehicle)); + break; + case LocalCheapestInsertionParameters:: + SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS: + properties.push_back(avg_distance_to_vehicle); + break; + case LocalCheapestInsertionParameters:: + SORTING_PROPERTY_LOWEST_MIN_ARC_COST_TO_VEHICLE_START_ENDS: + properties.push_back(neg_min_distance_to_vehicles); + neg_min_distance_to_vehicles_appended = true; + break; + case LocalCheapestInsertionParameters:: + SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE: + properties.push_back(CapOpp(avg_dimension_usage)); + break; + default: + LOG(DFATAL) + << "Unknown RoutingSearchParameter::InsertionSortingProperty " + "used!"; + break; + } + } - // Historically the negative max distance to vehicles has always been - // considered to be the last property in the hierarchy defining how nodes - // are sorted for the LCI heuristic, so we add it here iff it wasn't added - // before - if (!neg_min_distance_to_vehicles_appended) { - properties.push_back(neg_min_distance_to_vehicles); - } + // Historically the negative max distance to vehicles has always been + // considered to be the last property in the hierarchy defining how + // nodes are sorted for the LCI heuristic, so we add it here iff it + // wasn't added before + if (!neg_min_distance_to_vehicles_appended) { + properties.push_back(neg_min_distance_to_vehicles); + } - return properties; - }; + return properties; + }; // Identify whether the selected properties require a more expensive // preprocessing. bool compute_avg_pickup_delivery_pair_distance_from_vehicles = false; bool compute_avg_dimension_usage = false; - for (const RoutingSearchParameters::InsertionSortingProperty property : - insertion_sorting_properties_) { + for (const LocalCheapestInsertionParameters::InsertionSortingProperty + property : insertion_sorting_properties_) { if (property == - RoutingSearchParameters:: + LocalCheapestInsertionParameters:: SORTING_PROPERTY_HIGHEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS || property == - RoutingSearchParameters:: + LocalCheapestInsertionParameters:: SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS) { compute_avg_pickup_delivery_pair_distance_from_vehicles = true; } - if (property == - RoutingSearchParameters::SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE) { + if (property == LocalCheapestInsertionParameters:: + SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE) { compute_avg_dimension_usage = true; } } @@ -3322,14 +3379,15 @@ bool LocalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() { const auto& pair = pairs[index]; switch (pair_insertion_strategy_) { - case RoutingSearchParameters::AUTOMATIC: - case RoutingSearchParameters::BEST_PICKUP_DELIVERY_PAIR: + case LocalCheapestInsertionParameters::AUTOMATIC: + case LocalCheapestInsertionParameters::BEST_PICKUP_DELIVERY_PAIR: InsertBestPair(pair); break; - case RoutingSearchParameters::BEST_PICKUP_THEN_BEST_DELIVERY: + case LocalCheapestInsertionParameters::BEST_PICKUP_THEN_BEST_DELIVERY: InsertBestPickupThenDelivery(pair); break; - case RoutingSearchParameters::BEST_PICKUP_DELIVERY_PAIR_MULTITOUR: + case LocalCheapestInsertionParameters:: + BEST_PICKUP_DELIVERY_PAIR_MULTITOUR: InsertBestPairMultitour(pair); break; default: @@ -4161,11 +4219,11 @@ SavingsFilteredHeuristic::SavingsFilteredHeuristic( SavingsParameters parameters, LocalSearchFilterManager* filter_manager) : RoutingFilteredHeuristic(model, std::move(stop_search), filter_manager), vehicle_type_curator_(nullptr), - savings_params_(parameters) { - DCHECK_GT(savings_params_.neighbors_ratio, 0); - DCHECK_LE(savings_params_.neighbors_ratio, 1); - DCHECK_GT(savings_params_.max_memory_usage_bytes, 0); - DCHECK_GT(savings_params_.arc_coefficient, 0); + savings_params_(std::move(parameters)) { + DCHECK_GT(savings_params_.neighbors_ratio(), 0); + DCHECK_LE(savings_params_.neighbors_ratio(), 1); + DCHECK_GT(savings_params_.max_memory_usage_bytes(), 0); + DCHECK_GT(savings_params_.arc_coefficient(), 0); } SavingsFilteredHeuristic::~SavingsFilteredHeuristic() = default; @@ -4300,7 +4358,7 @@ bool SavingsFilteredHeuristic::ComputeSavings() { return cost_and_node.second; }); } - if (savings_params_.add_reverse_arcs) { + if (savings_params_.add_reverse_arcs()) { AddSymmetricArcsToAdjacencyLists(&adjacency_lists); } if (StopSearch()) return false; @@ -4327,7 +4385,7 @@ bool SavingsFilteredHeuristic::ComputeSavings() { model()->GetArcCostForClass(after_node, end, cost_class); const double weighted_arc_cost_fp = - savings_params_.arc_coefficient * arc_cost; + savings_params_.arc_coefficient() * arc_cost; const int64_t weighted_arc_cost = weighted_arc_cost_fp < std::numeric_limits::max() ? static_cast(weighted_arc_cost_fp) @@ -4355,14 +4413,14 @@ int64_t SavingsFilteredHeuristic::MaxNumNeighborsPerNode( const int64_t size = model()->Size(); const int64_t num_neighbors_with_ratio = - std::max(1.0, size * savings_params_.neighbors_ratio); + std::max(1.0, size * savings_params_.neighbors_ratio()); // A single Saving takes 2*8 bytes of memory. // max_memory_usage_in_savings_unit = num_savings * multiplicative_factor, // Where multiplicative_factor is the memory taken (in Savings unit) for each // computed Saving. const double max_memory_usage_in_savings_unit = - savings_params_.max_memory_usage_bytes / 16; + savings_params_.max_memory_usage_bytes() / 16; // In the SavingsContainer, for each Saving, the Savings are stored: // - Once in "sorted_savings_per_vehicle_type", and (at most) once in @@ -5174,7 +5232,7 @@ class RouteConstructor { return true; } - bool FeasibleMerge(const std::vector& route1, + bool FeasibleMerge(absl::Span route1, const std::vector& route2, int node1, int node2, int route_index1, int route_index2, int vehicle_class, int64_t start_depot, int64_t end_depot) { @@ -5726,4 +5784,4 @@ DecisionBuilder* RoutingModel::MakeSelfDependentDimensionFinalizer( solver_->MakeLocalSearchPhase(first_solution, parameters); return finalizer; } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_search.h b/ortools/routing/search.h similarity index 96% rename from ortools/constraint_solver/routing_search.h rename to ortools/routing/search.h index 270dfa5f5a1..95da515b49f 100644 --- a/ortools/constraint_solver/routing_search.h +++ b/ortools/routing/search.h @@ -11,10 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_SEARCH_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_SEARCH_H_ - -#include +#ifndef OR_TOOLS_ROUTING_SEARCH_H_ +#define OR_TOOLS_ROUTING_SEARCH_H_ #include #include @@ -27,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -41,14 +40,15 @@ #include "ortools/base/adjustable_priority_queue.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" -#include "ortools/constraint_solver/routing.h" -#include "ortools/constraint_solver/routing_enums.pb.h" -#include "ortools/constraint_solver/routing_parameters.pb.h" -#include "ortools/constraint_solver/routing_types.h" -#include "ortools/constraint_solver/routing_utils.h" +#include "ortools/routing/enums.pb.h" +#include "ortools/routing/heuristic_parameters.pb.h" +#include "ortools/routing/parameters.pb.h" +#include "ortools/routing/routing.h" +#include "ortools/routing/types.h" +#include "ortools/routing/utils.h" #include "ortools/util/bitset.h" -namespace operations_research { +namespace operations_research::routing { // Solves a routing model using alternative models. This assumes that the models // are equivalent in the sense that a solution to one model is also a solution @@ -170,7 +170,7 @@ class VehicleTypeCurator { /// Returns the best value for the automatic first solution strategy, based on /// the given model parameters. -operations_research::FirstSolutionStrategy::Value +operations_research::routing::FirstSolutionStrategy::Value AutomaticFirstSolutionStrategy(bool has_pickup_deliveries, bool has_node_precedences, bool has_single_vehicle_node); @@ -641,6 +641,13 @@ class GlobalCheapestInsertionFilteredHeuristic /// case nodes are inserted based on the topological order of their type, /// given by the routing model's GetTopologicallySortedVisitTypes() method. bool InsertPairsAndNodesByRequirementTopologicalOrder(); + /// Inserts non-inserted single nodes or pickup/delivery pairs which are in + /// precedence constraints. + /// These nodes are inserted iff the precedence graph is acyclic, in which + /// case nodes are inserted based on the topological order of the precedence + /// graph, given by the routing model's + /// GetTopologicallySortedNodePrecedences() method. + bool InsertPairsAndNodesByPrecedenceTopologicalOrder(); /// Inserts non-inserted pickup and delivery pairs. Maintains a priority /// queue of possible pair insertions, which is incrementally updated when a @@ -759,12 +766,10 @@ class GlobalCheapestInsertionFilteredHeuristic AdjustablePriorityQueue* priority_queue, std::vector* pickup_to_entries, std::vector* delivery_to_entries); - /// Updates all existing pair entries inserting a node after nodes of the - /// chain starting at 'insert_after_start' and ending before - /// 'insert_after_end', and updates the priority queue accordingly. - bool UpdateExistingPairEntriesOnChain( - int64_t insert_after_start, int64_t insert_after_end, - AdjustablePriorityQueue* priority_queue, + /// Updates all existing pair entries inserting a node after 'insert_after' + /// and updates the priority queue accordingly. + bool UpdateExistingPairEntriesAfter( + int64_t insert_after, AdjustablePriorityQueue* priority_queue, std::vector* pickup_to_entries, std::vector* delivery_to_entries); /// Adds pair entries inserting either a pickup or a delivery after @@ -848,14 +853,6 @@ class GlobalCheapestInsertionFilteredHeuristic bool UpdateAfterNodeInsertion(const SparseBitset& nodes, int vehicle, int64_t node, int64_t insert_after, bool all_vehicles, NodeEntryQueue* queue); - /// Updates all existing node entries inserting a node after nodes of the - /// chain starting at 'insert_after_start' and ending before - /// 'insert_after_end', and updates the priority queue accordingly. - bool UpdateExistingNodeEntriesOnChain(const SparseBitset& nodes, - int vehicle, int64_t insert_after_start, - int64_t insert_after_end, - bool all_vehicles, - NodeEntryQueue* queue); /// Adds node entries inserting a node after "insert_after" and updates the /// priority queue accordingly. bool AddNodeEntriesAfter(const SparseBitset& nodes, int vehicle, @@ -921,6 +918,9 @@ class GlobalCheapestInsertionFilteredHeuristic std::unique_ptr empty_vehicle_type_curator_; + // Temporary member used to keep track of node insertions wherever needed. + SparseBitset temp_inserted_nodes_; + mutable EntryAllocator pair_entry_allocator_; }; @@ -1138,9 +1138,7 @@ class LocalCheapestInsertionFilteredHeuristic LocalCheapestInsertionFilteredHeuristic( RoutingModel* model, std::function stop_search, std::function evaluator, - RoutingSearchParameters::PairInsertionStrategy pair_insertion_strategy, - std::vector - insertion_sorting_properties, + LocalCheapestInsertionParameters lci_params, LocalSearchFilterManager* filter_manager, bool use_first_solution_hint, BinCapacities* bin_capacities = nullptr, std::function&, @@ -1221,8 +1219,9 @@ class LocalCheapestInsertionFilteredHeuristic } std::vector insertion_order_; - const RoutingSearchParameters::PairInsertionStrategy pair_insertion_strategy_; - std::vector + const LocalCheapestInsertionParameters::PairInsertionStrategy + pair_insertion_strategy_; + std::vector insertion_sorting_properties_; InsertionSequenceContainer insertion_container_; InsertionSequenceGenerator insertion_generator_; @@ -1341,21 +1340,6 @@ class ComparatorCheapestAdditionFilteredHeuristic /// and cost classes are taken into account. class SavingsFilteredHeuristic : public RoutingFilteredHeuristic { public: - struct SavingsParameters { - /// If neighbors_ratio < 1 then for each node only this ratio of its - /// neighbors leading to the smallest arc costs are considered. - double neighbors_ratio = 1.0; - /// The number of neighbors considered for each node is also adapted so that - /// the stored Savings don't use up more than max_memory_usage_bytes bytes. - double max_memory_usage_bytes = 6e9; - /// If add_reverse_arcs is true, the neighborhood relationships are - /// considered symmetrically. - bool add_reverse_arcs = false; - /// arc_coefficient is a strictly positive parameter indicating the - /// coefficient of the arc being considered in the Saving formula. - double arc_coefficient = 1.0; - }; - SavingsFilteredHeuristic(RoutingModel* model, std::function stop_search, SavingsParameters parameters, @@ -1551,6 +1535,6 @@ DecisionBuilder* MakeSweepDecisionBuilder(RoutingModel* model, // Returns a DecisionBuilder making all nodes unperformed. DecisionBuilder* MakeAllUnperformed(RoutingModel* model); -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_SEARCH_H_ +#endif // OR_TOOLS_ROUTING_SEARCH_H_ diff --git a/ortools/constraint_solver/routing_types.h b/ortools/routing/types.h similarity index 76% rename from ortools/constraint_solver/routing_types.h rename to ortools/routing/types.h index 1319ecd679b..fc7b9197f47 100644 --- a/ortools/constraint_solver/routing_types.h +++ b/ortools/routing/types.h @@ -11,17 +11,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_TYPES_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_TYPES_H_ +#ifndef OR_TOOLS_ROUTING_TYPES_H_ +#define OR_TOOLS_ROUTING_TYPES_H_ #include #include #include -#include "ortools/base/int_type.h" #include "ortools/util/piecewise_linear_function.h" +#include "ortools/util/strong_integers.h" -namespace operations_research { +namespace operations_research::routing { /// Defining common types used in the routing library outside the main /// RoutingModel class has several purposes: @@ -32,12 +32,12 @@ namespace operations_research { /// /// Users that depend on routing.{h,cc} should just use the /// RoutingModel:: equivalent, eg. RoutingModel::NodeIndex. -DEFINE_INT_TYPE(RoutingNodeIndex, int); -DEFINE_INT_TYPE(RoutingCostClassIndex, int); -DEFINE_INT_TYPE(RoutingDimensionIndex, int); -DEFINE_INT_TYPE(RoutingDisjunctionIndex, int); -DEFINE_INT_TYPE(RoutingVehicleClassIndex, int); -DEFINE_INT_TYPE(RoutingResourceClassIndex, int); +DEFINE_STRONG_INDEX_TYPE(RoutingNodeIndex); +DEFINE_STRONG_INDEX_TYPE(RoutingCostClassIndex); +DEFINE_STRONG_INDEX_TYPE(RoutingDimensionIndex); +DEFINE_STRONG_INDEX_TYPE(RoutingDisjunctionIndex); +DEFINE_STRONG_INDEX_TYPE(RoutingVehicleClassIndex); +DEFINE_STRONG_INDEX_TYPE(RoutingResourceClassIndex); /// Pickup and delivery pair representation, including alternatives for pickups /// and deliveries respectively. @@ -52,6 +52,6 @@ typedef std::function RoutingCumulDependentTransitCallback2; -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_TYPES_H_ +#endif // OR_TOOLS_ROUTING_TYPES_H_ diff --git a/ortools/constraint_solver/routing_utils.cc b/ortools/routing/utils.cc similarity index 97% rename from ortools/constraint_solver/routing_utils.cc rename to ortools/routing/utils.cc index 4911cdc937c..0c42ba9af9e 100644 --- a/ortools/constraint_solver/routing_utils.cc +++ b/ortools/routing/utils.cc @@ -11,9 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/constraint_solver/routing_utils.h" +#include "ortools/routing/utils.h" #include +#include +#include #include #include #include @@ -24,7 +26,7 @@ #include "absl/types/span.h" #include "ortools/util/saturated_arithmetic.h" -namespace operations_research { +namespace operations_research::routing { void BinCapacities::AddDimension( std::function load_demand_of_item_for_bin, @@ -173,4 +175,4 @@ bool FindMostExpensiveArcsOnRoute( return true; } -} // namespace operations_research +} // namespace operations_research::routing diff --git a/ortools/constraint_solver/routing_utils.h b/ortools/routing/utils.h similarity index 94% rename from ortools/constraint_solver/routing_utils.h rename to ortools/routing/utils.h index 1a856cf6b9c..d44174b3413 100644 --- a/ortools/constraint_solver/routing_utils.h +++ b/ortools/routing/utils.h @@ -11,8 +11,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_UTILS_H_ -#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_UTILS_H_ +#ifndef OR_TOOLS_ROUTING_UTILS_H_ +#define OR_TOOLS_ROUTING_UTILS_H_ #include #include @@ -21,7 +21,7 @@ #include "absl/types/span.h" -namespace operations_research { +namespace operations_research::routing { // Tracks whether bins constrained by several nonnegative dimensions can contain // items added incrementally. Also tracks soft violation costs. @@ -89,6 +89,6 @@ bool FindMostExpensiveArcsOnRoute( std::vector>* most_expensive_arc_starts_and_ranks, std::pair* first_expensive_arc_indices); -} // namespace operations_research +} // namespace operations_research::routing -#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_UTILS_H_ +#endif // OR_TOOLS_ROUTING_UTILS_H_ diff --git a/ortools/sat/2d_distances_propagator.cc b/ortools/sat/2d_distances_propagator.cc index 71b44cabc32..3d455420a5d 100644 --- a/ortools/sat/2d_distances_propagator.cc +++ b/ortools/sat/2d_distances_propagator.cc @@ -13,6 +13,7 @@ #include "ortools/sat/2d_distances_propagator.h" +#include #include #include #include @@ -39,21 +40,16 @@ namespace sat { Precedences2DPropagator::Precedences2DPropagator( NoOverlap2DConstraintHelper* helper, Model* model) : helper_(*helper), - binary_relations_maps_(model->GetOrCreate()), - shared_stats_(model->GetOrCreate()) { + linear2_bounds_(model->GetOrCreate()), + linear2_watcher_(model->GetOrCreate()), + shared_stats_(model->GetOrCreate()), + non_trivial_bounds_( + model->GetOrCreate()) { model->GetOrCreate()->SetPushAffineUbForBinaryRelation(); } -void Precedences2DPropagator::CollectPairsOfBoxesWithNonTrivialDistance() { - helper_.SynchronizeAndSetDirection(); - non_trivial_pairs_.clear(); - - struct VarUsage { - // boxes[0=x, 1=y][0=start, 1=end] - std::vector boxes[2][2]; - }; - absl::flat_hash_map var_to_box_and_coeffs; - +void Precedences2DPropagator::UpdateVarLookups() { + var_to_box_and_coeffs_.clear(); for (int dim = 0; dim < 2; ++dim) { const SchedulingConstraintHelper& dim_helper = dim == 0 ? helper_.x_helper() : helper_.y_helper(); @@ -61,48 +57,56 @@ void Precedences2DPropagator::CollectPairsOfBoxesWithNonTrivialDistance() { const absl::Span interval_points = j == 0 ? dim_helper.Starts() : dim_helper.Ends(); for (int i = 0; i < helper_.NumBoxes(); ++i) { - if (interval_points[i].var != kNoIntegerVariable) { - var_to_box_and_coeffs[PositiveVariable(interval_points[i].var)] - .boxes[dim][j] - .push_back(i); + const IntegerVariable var = interval_points[i].var; + if (var != kNoIntegerVariable) { + var_to_box_and_coeffs_[PositiveVariable(var)].boxes[dim][j].push_back( + i); } } } } +} +void Precedences2DPropagator::CollectNewPairsOfBoxesWithNonTrivialDistance() { + const absl::Span exprs = + non_trivial_bounds_->GetLinear2WithPotentialNonTrivalBounds(); + if (exprs.size() == num_known_linear2_) { + return; + } VLOG(2) << "CollectPairsOfBoxesWithNonTrivialDistance called, num_exprs: " - << binary_relations_maps_->GetAllExpressionsWithAffineBounds().size(); - for (const LinearExpression2& expr : - binary_relations_maps_->GetAllExpressionsWithAffineBounds()) { - auto it1 = var_to_box_and_coeffs.find(PositiveVariable(expr.vars[0])); - auto it2 = var_to_box_and_coeffs.find(PositiveVariable(expr.vars[1])); - if (it1 == var_to_box_and_coeffs.end() || - it2 == var_to_box_and_coeffs.end()) { - continue; - } + << exprs.size(); + const int previous_num_pairs = non_trivial_pairs_.size(); + for (; num_known_linear2_ < exprs.size(); ++num_known_linear2_) { + const LinearExpression2& positive_expr = exprs[num_known_linear2_]; + LinearExpression2 negated_expr = positive_expr; + negated_expr.Negate(); + for (const LinearExpression2& expr : {positive_expr, negated_expr}) { + auto it1 = var_to_box_and_coeffs_.find(PositiveVariable(expr.vars[0])); + auto it2 = var_to_box_and_coeffs_.find(PositiveVariable(expr.vars[1])); + if (it1 == var_to_box_and_coeffs_.end()) { + continue; + } + if (it2 == var_to_box_and_coeffs_.end()) { + continue; + } - const VarUsage& usage1 = it1->second; - const VarUsage& usage2 = it2->second; - for (int dim = 0; dim < 2; ++dim) { - const SchedulingConstraintHelper& dim_helper = - dim == 0 ? helper_.x_helper() : helper_.y_helper(); - for (const int box1 : usage1.boxes[dim][0 /* start */]) { - for (const int box2 : usage2.boxes[dim][1 /* end */]) { - if (box1 == box2) continue; - const AffineExpression& start = dim_helper.Starts()[box1]; - const AffineExpression& end = dim_helper.Ends()[box2]; - LinearExpression2 expr2; - expr2.vars[0] = start.var; - expr2.vars[1] = end.var; - expr2.coeffs[0] = start.coeff; - expr2.coeffs[1] = -end.coeff; - expr2.SimpleCanonicalization(); - expr2.DivideByGcd(); - if (expr == expr2) { - if (box1 < box2) { - non_trivial_pairs_.push_back({box1, box2}); - } else { - non_trivial_pairs_.push_back({box2, box1}); + const VarUsage& usage1 = it1->second; + const VarUsage& usage2 = it2->second; + for (int dim = 0; dim < 2; ++dim) { + const SchedulingConstraintHelper& dim_helper = + dim == 0 ? helper_.x_helper() : helper_.y_helper(); + for (const int box1 : usage1.boxes[dim][0 /* start */]) { + for (const int box2 : usage2.boxes[dim][1 /* end */]) { + if (box1 == box2) continue; + const auto [expr2, unused] = EncodeDifferenceLowerThan( + dim_helper.Starts()[box1], dim_helper.Ends()[box2], + /*ub=unused*/ 0); + if (expr == expr2) { + if (box1 < box2) { + non_trivial_pairs_.push_back({box1, box2}); + } else { + non_trivial_pairs_.push_back({box2, box1}); + } } } } @@ -110,20 +114,42 @@ void Precedences2DPropagator::CollectPairsOfBoxesWithNonTrivialDistance() { } } - gtl::STLSortAndRemoveDuplicates(&non_trivial_pairs_); + // Sort the new pairs. + std::sort(non_trivial_pairs_.begin() + previous_num_pairs, + non_trivial_pairs_.end()); + + // Remove duplicates from new pairs. + non_trivial_pairs_.erase( + std::unique(non_trivial_pairs_.begin() + previous_num_pairs, + non_trivial_pairs_.end()), + non_trivial_pairs_.end()); + + // Merge with the old pairs keeping sorted. + std::inplace_merge(non_trivial_pairs_.begin(), + non_trivial_pairs_.begin() + previous_num_pairs, + non_trivial_pairs_.end()); + + // Remove newly-added duplicates. + non_trivial_pairs_.erase( + std::unique(non_trivial_pairs_.begin(), non_trivial_pairs_.end()), + non_trivial_pairs_.end()); + + // Result should be sorted and without duplicates. + DCHECK(std::is_sorted(non_trivial_pairs_.begin(), non_trivial_pairs_.end())); + DCHECK(std::adjacent_find(non_trivial_pairs_.begin(), + non_trivial_pairs_.end()) == + non_trivial_pairs_.end()); } bool Precedences2DPropagator::Propagate() { if (!helper_.SynchronizeAndSetDirection()) return false; - if (last_helper_inprocessing_count_ != helper_.InProcessingCount() || - helper_.x_helper().CurrentDecisionLevel() == 0 || - last_num_expressions_ != - binary_relations_maps_->NumExpressionsWithAffineBounds()) { + if (last_helper_inprocessing_count_ != helper_.InProcessingCount()) { last_helper_inprocessing_count_ = helper_.InProcessingCount(); - last_num_expressions_ = - binary_relations_maps_->NumExpressionsWithAffineBounds(); - CollectPairsOfBoxesWithNonTrivialDistance(); + UpdateVarLookups(); + num_known_linear2_ = 0; + non_trivial_pairs_.clear(); } + CollectNewPairsOfBoxesWithNonTrivialDistance(); num_calls_++; @@ -147,15 +173,9 @@ bool Precedences2DPropagator::Propagate() { if (j == 1) { std::swap(b1, b2); } - LinearExpression2 expr; - expr.vars[0] = helper->Starts()[b1].var; - expr.vars[1] = helper->Ends()[b2].var; - expr.coeffs[0] = helper->Starts()[b1].coeff; - expr.coeffs[1] = -helper->Ends()[b2].coeff; - const IntegerValue ub_of_start_minus_end_value = - binary_relations_maps_->UpperBound(expr) + - helper->Starts()[b1].constant - helper->Ends()[b2].constant; - if (ub_of_start_minus_end_value >= 0) { + const auto [expr, ub_for_no_overlap] = EncodeDifferenceLowerThan( + helper->Starts()[b1], helper->Ends()[b2], 0); + if (linear2_bounds_->UpperBound(expr) >= ub_for_no_overlap) { is_unfeasible = false; break; } @@ -177,15 +197,10 @@ bool Precedences2DPropagator::Propagate() { if (j == 1) { std::swap(b1, b2); } - LinearExpression2 expr; - expr.vars[0] = helper->Starts()[b1].var; - expr.vars[1] = helper->Ends()[b2].var; - expr.coeffs[0] = helper->Starts()[b1].coeff; - expr.coeffs[1] = -helper->Ends()[b2].coeff; - binary_relations_maps_->AddReasonForUpperBoundLowerThan( - expr, - -(helper->Starts()[b1].constant - helper->Ends()[b2].constant) - 1, - helper_.x_helper().MutableLiteralReason(), + const auto [expr, ub] = EncodeDifferenceLowerThan( + helper->Starts()[b1], helper->Ends()[b2], -1); + linear2_bounds_->AddReasonForUpperBoundLowerThan( + expr, ub, helper_.x_helper().MutableLiteralReason(), helper_.x_helper().MutableIntegerReason()); } } @@ -199,7 +214,7 @@ bool Precedences2DPropagator::Propagate() { int Precedences2DPropagator::RegisterWith(GenericLiteralWatcher* watcher) { const int id = watcher->Register(this); helper_.WatchAllBoxes(id); - binary_relations_maps_->WatchAllLinearExpressions2(id); + linear2_watcher_->WatchAllLinearExpressions2(id); return id; } diff --git a/ortools/sat/2d_distances_propagator.h b/ortools/sat/2d_distances_propagator.h index b05e6b1a3dc..6c47f37f645 100644 --- a/ortools/sat/2d_distances_propagator.h +++ b/ortools/sat/2d_distances_propagator.h @@ -18,7 +18,9 @@ #include #include +#include "absl/container/flat_hash_map.h" #include "ortools/sat/integer.h" +#include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" #include "ortools/sat/no_overlap_2d_helper.h" #include "ortools/sat/precedences.h" @@ -28,10 +30,10 @@ namespace operations_research { namespace sat { // This class implements a propagator for non_overlap_2d constraints that uses -// the BinaryRelationsMaps to detect precedences between pairs of boxes and +// the Linear2Bounds to detect precedences between pairs of boxes and // detect a conflict if the precedences implies an overlap between the two -// boxes. For doing this efficiently, it keep track of pairs of boxes that have -// non-fixed precedences in the BinaryRelationsMaps and only check those in the +// boxes. For doing this efficiently, it keeps track of pairs of boxes that have +// non-fixed precedences in the Linear2Bounds and only check those in the // propagation. class Precedences2DPropagator : public PropagatorInterface { public: @@ -43,16 +45,26 @@ class Precedences2DPropagator : public PropagatorInterface { int RegisterWith(GenericLiteralWatcher* watcher); private: - void CollectPairsOfBoxesWithNonTrivialDistance(); + void CollectNewPairsOfBoxesWithNonTrivialDistance(); + void UpdateVarLookups(); std::vector> non_trivial_pairs_; + struct VarUsage { + // boxes[0=x, 1=y][0=start, 1=end] + std::vector boxes[2][2]; + }; + + absl::flat_hash_map var_to_box_and_coeffs_; NoOverlap2DConstraintHelper& helper_; - BinaryRelationsMaps* binary_relations_maps_; + Linear2Bounds* linear2_bounds_; + Linear2Watcher* linear2_watcher_; SharedStatistics* shared_stats_; + Linear2WithPotentialNonTrivalBounds* non_trivial_bounds_; int last_helper_inprocessing_count_ = -1; - int last_num_expressions_ = -1; + int num_known_linear2_ = 0; + int64_t last_linear2_timestamp_ = -1; int64_t num_conflicts_ = 0; int64_t num_calls_ = 0; diff --git a/ortools/sat/BUILD.bazel b/ortools/sat/BUILD.bazel index ba675c7341d..4dc706800de 100644 --- a/ortools/sat/BUILD.bazel +++ b/ortools/sat/BUILD.bazel @@ -126,7 +126,6 @@ cc_library( ":model", ":no_overlap_2d_helper", ":precedences", - ":sat_base", ":scheduling_helpers", ":synchronization", "//ortools/base:stl_util", @@ -313,8 +312,10 @@ cc_library( "@abseil-cpp//absl/hash", "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/numeric:int128", "@abseil-cpp//absl/random", "@abseil-cpp//absl/random:bit_gen_ref", + "@abseil-cpp//absl/random:distributions", "@abseil-cpp//absl/status", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:str_format", @@ -814,7 +815,6 @@ cc_library( deps = [ ":cp_model_cc_proto", ":cp_model_utils", - ":integer", ":integer_base", ":linear_constraint", ":model", @@ -1095,6 +1095,7 @@ cc_library( ":integer", ":integer_base", ":model", + ":precedences", ":presolve_context", ":presolve_util", ":probing", @@ -1132,7 +1133,6 @@ cc_library( "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/log:vlog_is_on", - "@abseil-cpp//absl/meta:type_traits", "@abseil-cpp//absl/numeric:int128", "@abseil-cpp//absl/random:distributions", "@abseil-cpp//absl/status:statusor", @@ -1750,6 +1750,7 @@ cc_library( ":sat_base", "//ortools/base", "//ortools/base:strong_vector", + "//ortools/util:bitset", "//ortools/util:saturated_arithmetic", "//ortools/util:sorted_interval_list", "//ortools/util:strong_integers", @@ -2054,6 +2055,7 @@ cc_library( deps = [ ":clause", ":cp_constraints", + ":cp_model_mapping", ":integer", ":integer_base", ":model", @@ -2080,6 +2082,7 @@ cc_library( "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/log:vlog_is_on", + "@abseil-cpp//absl/strings:str_format", "@abseil-cpp//absl/types:span", ], ) @@ -2103,6 +2106,7 @@ cc_test( "//ortools/base:parse_test_proto", "//ortools/util:sorted_interval_list", "@abseil-cpp//absl/container:flat_hash_map", + "@abseil-cpp//absl/types:span", ], ) @@ -2287,6 +2291,7 @@ cc_library( ":integer", ":integer_base", ":intervals", + ":linear_propagation", ":model", ":precedences", ":sat_base", @@ -2314,6 +2319,7 @@ cc_test( ":disjunctive", ":integer", ":integer_base", + ":integer_expr", ":integer_search", ":intervals", ":model", @@ -2568,7 +2574,6 @@ cc_library( "//ortools/base:stl_util", "//ortools/base:strong_vector", "//ortools/util:logging", - "//ortools/util:saturated_arithmetic", "//ortools/util:sorted_interval_list", "//ortools/util:strong_integers", "@abseil-cpp//absl/base:core_headers", @@ -3172,6 +3177,7 @@ cc_library( "@abseil-cpp//absl/container:inlined_vector", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/log:log_streamer", + "@abseil-cpp//absl/numeric:bits", "@abseil-cpp//absl/numeric:int128", "@abseil-cpp//absl/random", "@abseil-cpp//absl/random:bit_gen_ref", @@ -3201,6 +3207,7 @@ cc_test( "@abseil-cpp//absl/container:btree", "@abseil-cpp//absl/container:flat_hash_set", "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/numeric:bits", "@abseil-cpp//absl/numeric:int128", "@abseil-cpp//absl/random", "@abseil-cpp//absl/strings", @@ -3570,7 +3577,6 @@ cc_library( ":synchronization", ":timetable", ":util", - "//ortools/base:stl_util", "//ortools/util:bitset", "//ortools/util:saturated_arithmetic", "//ortools/util:strong_integers", @@ -3849,6 +3855,7 @@ cc_test( "//ortools/base:gmock_main", "//ortools/base:parse_test_proto", "//ortools/util:random_engine", + "@abseil-cpp//absl/strings", "@abseil-cpp//absl/types:span", ], ) @@ -4016,13 +4023,17 @@ cc_binary( "//ortools/base:path", "//ortools/util:file_util", "//ortools/util:logging", + "//ortools/util:sigint", "//ortools/util:sorted_interval_list", + "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/flags:flag", "@abseil-cpp//absl/log", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/log:flags", "@abseil-cpp//absl/strings", "@abseil-cpp//absl/strings:str_format", + "@abseil-cpp//absl/synchronization", + "@abseil-cpp//absl/types:span", "@protobuf", ], ) diff --git a/ortools/sat/colab/flags.py b/ortools/sat/colab/flags.py index 7057db3684a..c052cb5225a 100644 --- a/ortools/sat/colab/flags.py +++ b/ortools/sat/colab/flags.py @@ -15,92 +15,92 @@ class NotebookStringFlag: - """Stub for absl flag to be used within a jupyter notebook.""" + """Stub for absl flag to be used within a jupyter notebook.""" - def __init__(self, name: str, value: str, doc: str): - self.__name = name - self.__value = value - self.__doc__ = doc + def __init__(self, name: str, value: str, doc: str): + self.__name = name + self.__value = value + self.__doc__ = doc - @property - def value(self) -> str: - """Returns the value passed at creation.""" - return self.__value + @property + def value(self) -> str: + """Returns the value passed at creation.""" + return self.__value - @property - def name(self) -> str: - """Returns the name of the parameter.""" - return self.__name + @property + def name(self) -> str: + """Returns the name of the parameter.""" + return self.__name def define_string(name: str, value: str, doc: str): - return NotebookStringFlag(name, value, doc) + return NotebookStringFlag(name, value, doc) class NotebookIntFlag: - """Stub for absl flag to be used within a jupyter notebook.""" + """Stub for absl flag to be used within a jupyter notebook.""" - def __init__(self, name: str, value: int, doc: str): - self.__name = name - self.__value = value - self.__doc__ = doc + def __init__(self, name: str, value: int, doc: str): + self.__name = name + self.__value = value + self.__doc__ = doc - @property - def value(self) -> int: - """Returns the value passed at creation.""" - return self.__value + @property + def value(self) -> int: + """Returns the value passed at creation.""" + return self.__value - @property - def name(self) -> str: - """Returns the name of the parameter.""" - return self.__name + @property + def name(self) -> str: + """Returns the name of the parameter.""" + return self.__name def define_integer(name: str, value: int, doc: str): - return NotebookIntFlag(name, value, doc) + return NotebookIntFlag(name, value, doc) class NotebookFloatFlag: - """Stub for absl flag to be used within a jupyter notebook.""" + """Stub for absl flag to be used within a jupyter notebook.""" - def __init__(self, name: str, value: float, doc: str): - self.__name = name - self.__value = value - self.__doc__ = doc + def __init__(self, name: str, value: float, doc: str): + self.__name = name + self.__value = value + self.__doc__ = doc - @property - def value(self) -> float: - """Returns the value passed at creation.""" - return self.__value + @property + def value(self) -> float: + """Returns the value passed at creation.""" + return self.__value - @property - def name(self) -> str: - """Returns the name of the parameter.""" - return self.__name + @property + def name(self) -> str: + """Returns the name of the parameter.""" + return self.__name def define_float(name: str, value: bool, doc: str): - return NotebookFloatFlag(name, value, doc) + return NotebookFloatFlag(name, value, doc) class NotebookBoolFlag: - """Stub for absl flag to be used within a jupyter notebook.""" + """Stub for absl flag to be used within a jupyter notebook.""" - def __init__(self, name: str, value: bool, doc: str): - self.__name = name - self.__value = value - self.__doc__ = doc + def __init__(self, name: str, value: bool, doc: str): + self.__name = name + self.__value = value + self.__doc__ = doc - @property - def value(self) -> bool: - """Returns the value passed at creation.""" - return self.__value + @property + def value(self) -> bool: + """Returns the value passed at creation.""" + return self.__value - @property - def name(self) -> str: - """Returns the name of the parameter.""" - return self.__name + @property + def name(self) -> str: + """Returns the name of the parameter.""" + return self.__name def define_bool(name: str, value: bool, doc: str): - return NotebookBoolFlag(name, value, doc) + return NotebookBoolFlag(name, value, doc) diff --git a/ortools/sat/colab/visualization.py b/ortools/sat/colab/visualization.py index 725042fb273..c5fd01a1ef0 100644 --- a/ortools/sat/colab/visualization.py +++ b/ortools/sat/colab/visualization.py @@ -17,178 +17,178 @@ import random try: - from IPython.display import display - from IPython.display import SVG - import plotly.figure_factory as ff - import svgwrite + from IPython.display import display + from IPython.display import SVG + import plotly.figure_factory as ff + import svgwrite - correct_imports = True + correct_imports = True except ImportError: - correct_imports = False + correct_imports = False def RunFromIPython(): - if not correct_imports: - return False - try: - return __IPYTHON__ is not None - except NameError: - return False + if not correct_imports: + return False + try: + return __IPYTHON__ is not None + except NameError: + return False def ToDate(v): - return "2016-01-01 6:%02i:%02i" % (v / 60, v % 60) + return "2016-01-01 6:%02i:%02i" % (v / 60, v % 60) class ColorManager: - """Utility to create colors to use in visualization.""" - - def ScaledColor(self, sr, sg, sb, er, eg, eb, num_steps, step): - """Creates an interpolated rgb color between two rgb colors.""" - num_intervals = num_steps - 1 - dr = (er - sr) / num_intervals - dg = (eg - sg) / num_intervals - db = (eb - sb) / num_intervals - r = sr + dr * step - g = sg + dg * step - b = sb + db * step - return "rgb(%i, %i, %i)" % (r, g, b) - - def SeedRandomColor(self, seed=0): - random.seed(seed) - - def RandomColor(self): - return "rgb(%i,%i,%i)" % ( - random.randint(0, 255), - random.randint(0, 255), - random.randint(0, 255), - ) + """Utility to create colors to use in visualization.""" + + def ScaledColor(self, sr, sg, sb, er, eg, eb, num_steps, step): + """Creates an interpolated rgb color between two rgb colors.""" + num_intervals = num_steps - 1 + dr = (er - sr) / num_intervals + dg = (eg - sg) / num_intervals + db = (eb - sb) / num_intervals + r = sr + dr * step + g = sg + dg * step + b = sb + db * step + return "rgb(%i, %i, %i)" % (r, g, b) + + def SeedRandomColor(self, seed=0): + random.seed(seed) + + def RandomColor(self): + return "rgb(%i,%i,%i)" % ( + random.randint(0, 255), + random.randint(0, 255), + random.randint(0, 255), + ) def DisplayJobshop(starts, durations, machines, name): - """Simple function to display a jobshop solution using plotly.""" - - jobs_count = len(starts) - machines_count = len(starts[0]) - all_machines = range(0, machines_count) - all_jobs = range(0, jobs_count) - df = [] - for i in all_jobs: - for j in all_machines: - df.append( - dict( - Task="Resource%i" % machines[i][j], - Start=ToDate(starts[i][j]), - Finish=ToDate(starts[i][j] + durations[i][j]), - Resource="Job%i" % i, - ) - ) - - sorted_df = sorted(df, key=lambda k: k["Task"]) - - colors = {} - cm = ColorManager() - cm.SeedRandomColor(0) - for i in all_jobs: - colors["Job%i" % i] = cm.RandomColor() - - fig = ff.create_gantt( - sorted_df, - colors=colors, - index_col="Resource", - title=name, - show_colorbar=False, - showgrid_x=True, - showgrid_y=True, - group_tasks=True, - ) - fig.show() + """Simple function to display a jobshop solution using plotly.""" + + jobs_count = len(starts) + machines_count = len(starts[0]) + all_machines = range(0, machines_count) + all_jobs = range(0, jobs_count) + df = [] + for i in all_jobs: + for j in all_machines: + df.append( + dict( + Task="Resource%i" % machines[i][j], + Start=ToDate(starts[i][j]), + Finish=ToDate(starts[i][j] + durations[i][j]), + Resource="Job%i" % i, + ) + ) + + sorted_df = sorted(df, key=lambda k: k["Task"]) + + colors = {} + cm = ColorManager() + cm.SeedRandomColor(0) + for i in all_jobs: + colors["Job%i" % i] = cm.RandomColor() + + fig = ff.create_gantt( + sorted_df, + colors=colors, + index_col="Resource", + title=name, + show_colorbar=False, + showgrid_x=True, + showgrid_y=True, + group_tasks=True, + ) + fig.show() class SvgWrapper: - """Simple SVG wrapper to use in colab.""" - - def __init__(self, sizex, sizey, scaling=20.0): - self.__sizex = sizex - self.__sizey = sizey - self.__scaling = scaling - self.__offset = scaling - self.__dwg = svgwrite.Drawing( - size=( - self.__sizex * self.__scaling + self.__offset, - self.__sizey * self.__scaling + self.__offset * 2, - ) + """Simple SVG wrapper to use in colab.""" + + def __init__(self, sizex, sizey, scaling=20.0): + self.__sizex = sizex + self.__sizey = sizey + self.__scaling = scaling + self.__offset = scaling + self.__dwg = svgwrite.Drawing( + size=( + self.__sizex * self.__scaling + self.__offset, + self.__sizey * self.__scaling + self.__offset * 2, ) + ) - def Display(self): - display(SVG(self.__dwg.tostring())) - - def AddRectangle(self, x, y, dx, dy, fill, stroke="black", label=None): - """Draw a rectangle, dx and dy must be >= 0.""" - s = self.__scaling - o = self.__offset - corner = (x * s + o, (self.__sizey - y - dy) * s + o) - size = (dx * s - 1, dy * s - 1) - self.__dwg.add( - self.__dwg.rect(insert=corner, size=size, fill=fill, stroke=stroke) - ) - self.AddText(x + 0.5 * dx, y + 0.5 * dy, label) - - def AddText(self, x, y, label): - text = self.__dwg.text( - label, - insert=( - x * self.__scaling + self.__offset, - (self.__sizey - y) * self.__scaling + self.__offset, - ), - text_anchor="middle", - font_family="sans-serif", - font_size="%dpx" % (self.__scaling / 2), - ) - self.__dwg.add(text) - - def AddXScale(self, step=1): - """Add an scale on the x axis.""" - o = self.__offset - s = self.__scaling - y = self.__sizey * s + o / 2.0 + o - dy = self.__offset / 4.0 - self.__dwg.add( - self.__dwg.line((o, y), (self.__sizex * s + o, y), stroke="black") - ) - for i in range(0, int(self.__sizex) + 1, step): - self.__dwg.add( - self.__dwg.line( - (o + i * s, y - dy), (o + i * s, y + dy), stroke="black" - ) - ) - - def AddYScale(self, step=1): - """Add an scale on the y axis.""" - o = self.__offset - s = self.__scaling - x = o / 2.0 - dx = self.__offset / 4.0 - self.__dwg.add( - self.__dwg.line((x, o), (x, self.__sizey * s + o), stroke="black") - ) - for i in range(0, int(self.__sizey) + 1, step): - self.__dwg.add( - self.__dwg.line( - (x - dx, i * s + o), (x + dx, i * s + o), stroke="black" - ) - ) - - def AddTitle(self, title): - """Add a title to the drawing.""" - text = self.__dwg.text( - title, - insert=( - self.__offset + self.__sizex * self.__scaling / 2.0, - self.__offset / 2, - ), - text_anchor="middle", - font_family="sans-serif", - font_size="%dpx" % (self.__scaling / 2), - ) - self.__dwg.add(text) + def Display(self): + display(SVG(self.__dwg.tostring())) + + def AddRectangle(self, x, y, dx, dy, fill, stroke="black", label=None): + """Draw a rectangle, dx and dy must be >= 0.""" + s = self.__scaling + o = self.__offset + corner = (x * s + o, (self.__sizey - y - dy) * s + o) + size = (dx * s - 1, dy * s - 1) + self.__dwg.add( + self.__dwg.rect(insert=corner, size=size, fill=fill, stroke=stroke) + ) + self.AddText(x + 0.5 * dx, y + 0.5 * dy, label) + + def AddText(self, x, y, label): + text = self.__dwg.text( + label, + insert=( + x * self.__scaling + self.__offset, + (self.__sizey - y) * self.__scaling + self.__offset, + ), + text_anchor="middle", + font_family="sans-serif", + font_size="%dpx" % (self.__scaling / 2), + ) + self.__dwg.add(text) + + def AddXScale(self, step=1): + """Add an scale on the x axis.""" + o = self.__offset + s = self.__scaling + y = self.__sizey * s + o / 2.0 + o + dy = self.__offset / 4.0 + self.__dwg.add( + self.__dwg.line((o, y), (self.__sizex * s + o, y), stroke="black") + ) + for i in range(0, int(self.__sizex) + 1, step): + self.__dwg.add( + self.__dwg.line( + (o + i * s, y - dy), (o + i * s, y + dy), stroke="black" + ) + ) + + def AddYScale(self, step=1): + """Add an scale on the y axis.""" + o = self.__offset + s = self.__scaling + x = o / 2.0 + dx = self.__offset / 4.0 + self.__dwg.add( + self.__dwg.line((x, o), (x, self.__sizey * s + o), stroke="black") + ) + for i in range(0, int(self.__sizey) + 1, step): + self.__dwg.add( + self.__dwg.line( + (x - dx, i * s + o), (x + dx, i * s + o), stroke="black" + ) + ) + + def AddTitle(self, title): + """Add a title to the drawing.""" + text = self.__dwg.text( + title, + insert=( + self.__offset + self.__sizex * self.__scaling / 2.0, + self.__offset / 2, + ), + text_anchor="middle", + font_family="sans-serif", + font_size="%dpx" % (self.__scaling / 2), + ) + self.__dwg.add(text) diff --git a/ortools/sat/combine_solutions.cc b/ortools/sat/combine_solutions.cc index 679074675ef..872a9e4b4f3 100644 --- a/ortools/sat/combine_solutions.cc +++ b/ortools/sat/combine_solutions.cc @@ -53,7 +53,9 @@ std::optional> FindCombinedSolution( CHECK_EQ(new_solution.size(), base_solution.size()); const std::vector< std::shared_ptr::Solution>> - solutions = response_manager->SolutionsRepository().GetBestNSolutions(10); + solutions = + response_manager->SolutionPool().BestSolutions().GetBestNSolutions( + 10); for (int sol_idx = 0; sol_idx < solutions.size(); ++sol_idx) { std::shared_ptr::Solution> s = @@ -79,18 +81,21 @@ std::optional> FindCombinedSolution( PushedSolutionPointers PushAndMaybeCombineSolution( SharedResponseManager* response_manager, const CpModelProto& model_proto, absl::Span new_solution, const std::string& solution_info, - absl::Span base_solution, Model* model) { + std::shared_ptr::Solution> + base_solution) { PushedSolutionPointers result = {nullptr, nullptr}; - result.pushed_solution = - response_manager->NewSolution(new_solution, solution_info, model); - if (!base_solution.empty()) { + result.pushed_solution = response_manager->NewSolution( + new_solution, solution_info, nullptr, + base_solution == nullptr ? -1 : base_solution->source_id); + if (base_solution != nullptr) { std::string combined_solution_info = solution_info; std::optional> combined_solution = - FindCombinedSolution(model_proto, new_solution, base_solution, - response_manager, &combined_solution_info); + FindCombinedSolution(model_proto, new_solution, + base_solution->variable_values, response_manager, + &combined_solution_info); if (combined_solution.has_value()) { result.improved_solution = response_manager->NewSolution( - combined_solution.value(), combined_solution_info, model); + combined_solution.value(), combined_solution_info); } } return result; diff --git a/ortools/sat/combine_solutions.h b/ortools/sat/combine_solutions.h index 7106e9939e8..259e1cbca91 100644 --- a/ortools/sat/combine_solutions.h +++ b/ortools/sat/combine_solutions.h @@ -49,7 +49,8 @@ struct PushedSolutionPointers { PushedSolutionPointers PushAndMaybeCombineSolution( SharedResponseManager* response_manager, const CpModelProto& model_proto, absl::Span new_solution, const std::string& solution_info, - absl::Span base_solution = {}, Model* model = nullptr); + std::shared_ptr::Solution> + base_solution); } // namespace sat } // namespace operations_research diff --git a/ortools/sat/constraint_violation.cc b/ortools/sat/constraint_violation.cc index 0cd5f80d101..4f41c104992 100644 --- a/ortools/sat/constraint_violation.cc +++ b/ortools/sat/constraint_violation.cc @@ -1830,7 +1830,7 @@ void LsEvaluator::CompileOneConstraint(const ConstraintProto& ct) { void LsEvaluator::CompileConstraintsAndObjective( const std::vector& ignored_constraints, - const std::vector& additional_constraints) { + absl::Span additional_constraints) { constraints_.clear(); // The first compiled constraint is always the objective if present. diff --git a/ortools/sat/constraint_violation.h b/ortools/sat/constraint_violation.h index 54880e9f5aa..768fde9514b 100644 --- a/ortools/sat/constraint_violation.h +++ b/ortools/sat/constraint_violation.h @@ -434,7 +434,7 @@ class LsEvaluator { private: void CompileConstraintsAndObjective( const std::vector& ignored_constraints, - const std::vector& additional_constraints); + absl::Span additional_constraints); void CompileOneConstraint(const ConstraintProto& ct_proto); void BuildVarConstraintGraph(); diff --git a/ortools/sat/cp_model_lns.cc b/ortools/sat/cp_model_lns.cc index 5281a662a40..36990c16a9f 100644 --- a/ortools/sat/cp_model_lns.cc +++ b/ortools/sat/cp_model_lns.cc @@ -1237,7 +1237,7 @@ CpModelProto NeighborhoodGeneratorHelper::UpdatedModelProtoCopy() const { } bool NeighborhoodGenerator::ReadyToGenerate() const { - return (helper_.shared_response().SolutionsRepository().NumSolutions() > 0); + return helper_.shared_response().HasFeasibleSolution(); } double NeighborhoodGenerator::GetUCBScore(int64_t total_num_calls) const { diff --git a/ortools/sat/cp_model_lns_test.cc b/ortools/sat/cp_model_lns_test.cc index ac44009b939..68dc701a140 100644 --- a/ortools/sat/cp_model_lns_test.cc +++ b/ortools/sat/cp_model_lns_test.cc @@ -201,8 +201,10 @@ TYPED_TEST(GeneratorTest, ReadyToGenerate) { EXPECT_FALSE(generator.ReadyToGenerate()); shared_response_manager->NewSolution(solution.solution(), solution.solution_info(), &model); - shared_response_manager->MutableSolutionsRepository()->Synchronize(); - EXPECT_EQ(1, shared_response_manager->SolutionsRepository().NumSolutions()); + shared_response_manager->Synchronize(); + EXPECT_EQ( + 1, + shared_response_manager->SolutionPool().BestSolutions().NumSolutions()); EXPECT_TRUE(generator.ReadyToGenerate()); } @@ -301,7 +303,7 @@ TEST(RelaxationInducedNeighborhoodGeneratorTest, NoNeighborhoodGeneratedRINS) { solution.add_solution(0); shared_response_manager->NewSolution(solution.solution(), solution.solution_info(), &model); - shared_response_manager->MutableSolutionsRepository()->Synchronize(); + shared_response_manager->Synchronize(); lp_solutions.NewLPSolution({0.0}); lp_solutions.Synchronize(); diff --git a/ortools/sat/cp_model_loader.cc b/ortools/sat/cp_model_loader.cc index f053299657a..3a691a1ddf2 100644 --- a/ortools/sat/cp_model_loader.cc +++ b/ortools/sat/cp_model_loader.cc @@ -1261,7 +1261,7 @@ void LoadLinearConstraint(const ConstraintProto& ct, Model* m) { // Load precedences. if (!HasEnforcementLiteral(ct)) { - auto* precedences = m->GetOrCreate(); + auto* root_level_lin2_bounds = m->GetOrCreate(); // To avoid overflow in the code below, we tighten the bounds. // Note that we detect and do not add trivial relation. @@ -1272,7 +1272,7 @@ void LoadLinearConstraint(const ConstraintProto& ct, Model* m) { if (vars.size() == 2) { LinearExpression2 expr(vars[0], vars[1], coeffs[0], coeffs[1]); - precedences->AddBounds(expr, rhs_min, rhs_max); + root_level_lin2_bounds->Add(expr, rhs_min, rhs_max); } else if (vars.size() == 3) { // TODO(user): This is a weaker duplication of the logic of // BinaryRelationsMaps, but is is useful for the transitive closure in @@ -1293,7 +1293,8 @@ void LoadLinearConstraint(const ConstraintProto& ct, Model* m) { ? coeff * integer_trail->UpperBound(vars[other]).value() : coeff * integer_trail->LowerBound(vars[other]).value(); LinearExpression2 expr(vars[i], vars[j], coeffs[i], coeffs[j]); - precedences->AddBounds(expr, rhs_min - other_ub, rhs_max - other_lb); + root_level_lin2_bounds->Add(expr, rhs_min - other_ub, + rhs_max - other_lb); } } } diff --git a/ortools/sat/cp_model_mapping.h b/ortools/sat/cp_model_mapping.h index 1a82e4263e7..5cf63e3e2f2 100644 --- a/ortools/sat/cp_model_mapping.h +++ b/ortools/sat/cp_model_mapping.h @@ -24,7 +24,6 @@ #include "ortools/base/strong_vector.h" #include "ortools/sat/cp_model.pb.h" #include "ortools/sat/cp_model_utils.h" -#include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/linear_constraint.h" #include "ortools/sat/model.h" diff --git a/ortools/sat/cp_model_presolve.cc b/ortools/sat/cp_model_presolve.cc index d835856bf15..c17f6da2415 100644 --- a/ortools/sat/cp_model_presolve.cc +++ b/ortools/sat/cp_model_presolve.cc @@ -40,7 +40,6 @@ #include "absl/log/check.h" #include "absl/log/log.h" #include "absl/log/vlog_is_on.h" -#include "absl/meta/type_traits.h" #include "absl/numeric/int128.h" #include "absl/random/distributions.h" #include "absl/status/statusor.h" @@ -74,6 +73,7 @@ #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" +#include "ortools/sat/precedences.h" #include "ortools/sat/presolve_context.h" #include "ortools/sat/presolve_util.h" #include "ortools/sat/probing.h" @@ -7886,6 +7886,28 @@ void CpModelPresolver::Probe() { prober->ProbeBooleanVariables( context_->params().probing_deterministic_time_limit()); + for (const auto& [expr, ub] : model.GetOrCreate() + ->GetSortedNonTrivialUpperBounds()) { + if (expr.vars[0] == kNoIntegerVariable || + expr.vars[1] == kNoIntegerVariable) { + continue; + } + const IntegerVariable var0 = PositiveVariable(expr.vars[0]); + const IntegerVariable var1 = PositiveVariable(expr.vars[1]); + const int proto_var0 = mapping->GetProtoVariableFromIntegerVariable(var0); + const int proto_var1 = mapping->GetProtoVariableFromIntegerVariable(var1); + if (proto_var0 < 0 || proto_var1 < 0) continue; + const int64_t coeff0 = VariableIsPositive(expr.vars[0]) + ? expr.coeffs[0].value() + : -expr.coeffs[0].value(); + const int64_t coeff1 = VariableIsPositive(expr.vars[1]) + ? expr.coeffs[1].value() + : -expr.coeffs[1].value(); + known_linear2_.Add( + GetLinearExpression2FromProto(proto_var0, coeff0, proto_var1, coeff1), + kMinIntegerValue, ub); + } + probing_timer->AddCounter("probed", prober->num_decisions()); probing_timer->AddToWork( model.GetOrCreate()->GetElapsedDeterministicTime()); @@ -8798,6 +8820,7 @@ void CpModelPresolver::ExpandObjective() { } void CpModelPresolver::MergeNoOverlapConstraints() { + PresolveTimer timer("MergeNoOverlap", logger_, time_limit_); if (context_->ModelIsUnsat()) return; if (time_limit_->LimitReached()) return; diff --git a/ortools/sat/cp_model_search.cc b/ortools/sat/cp_model_search.cc index ff253b40414..6d18fd1ea1b 100644 --- a/ortools/sat/cp_model_search.cc +++ b/ortools/sat/cp_model_search.cc @@ -188,7 +188,6 @@ void AddExtraSchedulingPropagators(SatParameters& new_params) { new_params.set_use_energetic_reasoning_in_no_overlap_2d(true); new_params.set_use_area_energetic_reasoning_in_no_overlap_2d(true); new_params.set_use_try_edge_reasoning_in_no_overlap_2d(true); - new_params.set_no_overlap_2d_boolean_relations_limit(100); } // We want a random tie breaking among variables with equivalent values. @@ -758,7 +757,8 @@ absl::flat_hash_map GetNamedParameters( lns_params.set_log_search_progress(false); lns_params.set_debug_crash_on_bad_hint(false); // Can happen in lns. - lns_params.set_solution_pool_size(1); // Keep the best solution found. + lns_params.set_solution_pool_size(1); // Keep the best solution found. + lns_params.set_alternative_pool_size(0); // Disable. strategies["lns"] = lns_params; // Note that we only do this for the derived parameters. The strategy "lns" diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index 8aef798e1fe..e4c60ab3def 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -793,40 +793,6 @@ void LogSubsolverNames(absl::Span> subsolvers, SOLVER_LOG(logger, ""); } -void LogFinalStatistics(SharedClasses* shared) { - if (!shared->logger->LoggingIsEnabled()) return; - - shared->logger->FlushPendingThrottledLogs(/*ignore_rates=*/true); - SOLVER_LOG(shared->logger, ""); - - shared->stat_tables->Display(shared->logger); - shared->response->DisplayImprovementStatistics(); - - std::vector> table; - table.push_back({"Solution repositories", "Added", "Queried", "Synchro"}); - table.push_back(shared->response->SolutionsRepository().TableLineStats()); - table.push_back(shared->ls_hints->TableLineStats()); - if (shared->lp_solutions != nullptr) { - table.push_back(shared->lp_solutions->TableLineStats()); - } - if (shared->incomplete_solutions != nullptr) { - table.push_back(shared->incomplete_solutions->TableLineStats()); - } - SOLVER_LOG(shared->logger, FormatTable(table)); - - if (shared->bounds) { - shared->bounds->LogStatistics(shared->logger); - } - - if (shared->clauses) { - shared->clauses->LogStatistics(shared->logger); - } - - // Extra logging if needed. Note that these are mainly activated on - // --vmodule *some_file*=1 and are here for development. - shared->stats->Log(shared->logger); -} - void LaunchSubsolvers(const SatParameters& params, SharedClasses* shared, std::vector>& subsolvers, absl::Span ignored) { @@ -868,7 +834,7 @@ void LaunchSubsolvers(const SatParameters& params, SharedClasses* shared, for (int i = 0; i < subsolvers.size(); ++i) { subsolvers[i].reset(); } - LogFinalStatistics(shared); + shared->LogFinalStatistics(); } bool VarIsFixed(const CpModelProto& model_proto, int i) { @@ -1124,13 +1090,18 @@ class FullProblemSolver : public SubSolver { shared_->model_proto, shared_->bounds.get(), &local_model_); } + if (shared_->linear2_bounds != nullptr) { + RegisterLinear2BoundsImport(shared_->linear2_bounds.get(), + &local_model_); + } + // Note that this is done after the loading, so we will never export // problem clauses. if (shared_->clauses != nullptr) { const int id = shared_->clauses->RegisterNewId( + local_model_.Name(), /*may_terminate_early=*/stop_at_first_solution_ && - local_model_.GetOrCreate()->has_objective()); - shared_->clauses->SetWorkerNameForId(id, local_model_.Name()); + local_model_.GetOrCreate()->has_objective()); RegisterClausesLevelZeroImport(id, shared_->clauses.get(), &local_model_); @@ -1348,36 +1319,29 @@ class LnsSolver : public SubSolver { data.task_id = task_id; data.difficulty = generator_->difficulty(); data.deterministic_limit = generator_->deterministic_limit(); + data.initial_best_objective = + shared_->response->GetBestSolutionObjective(); // Choose a base solution for this neighborhood. + const auto base_solution = + shared_->response->SolutionPool().GetSolutionToImprove(random); CpSolverResponse base_response; - { - const SharedSolutionRepository& repo = - shared_->response->SolutionsRepository(); - if (repo.NumSolutions() > 0) { - base_response.set_status(CpSolverStatus::FEASIBLE); - std::shared_ptr::Solution> - solution = repo.GetRandomBiasedSolution(random); - base_response.mutable_solution()->Assign( - solution->variable_values.begin(), - solution->variable_values.end()); - - // Note: We assume that the solution rank is the solution internal - // objective. - data.initial_best_objective = repo.GetSolution(0)->rank; - data.base_objective = solution->rank; - } else { - base_response.set_status(CpSolverStatus::UNKNOWN); - - // If we do not have a solution, we use the current objective upper - // bound so that our code that compute an "objective" improvement - // works. - // - // TODO(user): this is non-deterministic. Fix. - data.initial_best_objective = - shared_->response->GetInnerObjectiveUpperBound(); - data.base_objective = data.initial_best_objective; - } + if (base_solution != nullptr) { + base_response.set_status(CpSolverStatus::FEASIBLE); + base_response.mutable_solution()->Assign( + base_solution->variable_values.begin(), + base_solution->variable_values.end()); + + // Note: We assume that the solution rank is the solution internal + // objective. + data.base_objective = base_solution->rank; + } else { + base_response.set_status(CpSolverStatus::UNKNOWN); + + // If we do not have a solution, we use the current objective upper + // bound so that our code that compute an "objective" improvement + // works. + data.base_objective = data.initial_best_objective; } Neighborhood neighborhood = @@ -1667,9 +1631,9 @@ class LnsSolver : public SubSolver { if (absl::MakeSpan(solution_values) != absl::MakeSpan(base_response.solution())) { new_solution = true; - PushAndMaybeCombineSolution( - shared_->response, shared_->model_proto, solution_values, - solution_info, base_response.solution(), /*model=*/nullptr); + PushAndMaybeCombineSolution(shared_->response, shared_->model_proto, + solution_values, solution_info, + base_solution); } } if (!neighborhood.is_reduced && @@ -1782,7 +1746,6 @@ void SolveCpModelParallel(SharedClasses* shared, Model* global_model) { subsolvers.push_back(std::make_unique( "synchronization_agent", [shared]() { shared->response->Synchronize(); - shared->response->MutableSolutionsRepository()->Synchronize(); shared->ls_hints->Synchronize(); if (shared->bounds != nullptr) { shared->bounds->Synchronize(); diff --git a/ortools/sat/cp_model_solver_helpers.cc b/ortools/sat/cp_model_solver_helpers.cc index 030cf124b5e..3e22dabbf51 100644 --- a/ortools/sat/cp_model_solver_helpers.cc +++ b/ortools/sat/cp_model_solver_helpers.cc @@ -847,6 +847,59 @@ void RegisterVariableBoundsLevelZeroImport( import_level_zero_bounds); } +void RegisterLinear2BoundsImport(SharedLinear2Bounds* shared_linear2_bounds, + Model* model) { + CHECK(shared_linear2_bounds != nullptr); + auto* cp_model_mapping = model->GetOrCreate(); + auto* root_linear2 = model->GetOrCreate(); + auto* sat_solver = model->GetOrCreate(); + const int import_id = + shared_linear2_bounds->RegisterNewImportId(model->Name()); + const auto& import_function = [import_id, shared_linear2_bounds, root_linear2, + cp_model_mapping, sat_solver, model]() { + const auto new_bounds = + shared_linear2_bounds->NewlyUpdatedBounds(import_id); + int num_imported = 0; + for (const auto& [proto_expr, bounds] : new_bounds) { + // Lets create the corresponding LinearExpression2. + LinearExpression2 expr; + for (const int i : {0, 1}) { + expr.vars[i] = cp_model_mapping->Integer(proto_expr.vars[i]); + expr.coeffs[i] = proto_expr.coeffs[i]; + } + const auto [lb, ub] = bounds; + const auto [lb_added, ub_added] = root_linear2->Add(expr, lb, ub); + if (!lb_added && !ub_added) continue; + ++num_imported; + + // TODO(user): Is it a good idea to add the linear constraint ? + // We might have many redundant linear2 relations that don't need + // propagation when we have chains of precedences. The root_linear2 should + // be up-to-date with transitive closure to avoid adding such relations + // (recompute it at level zero before this?). + // + // TODO(user): use IntegerValure directly in + // AddWeightedSumGreaterOrEqual() or use a lower-level API. + const std::vector coeffs = {expr.coeffs[0].value(), + expr.coeffs[1].value()}; + if (lb_added) { + AddWeightedSumGreaterOrEqual({}, absl::MakeSpan(expr.vars, 2), coeffs, + lb.value(), model); + if (sat_solver->ModelIsUnsat()) return false; + } + if (ub_added) { + AddWeightedSumLowerOrEqual({}, absl::MakeSpan(expr.vars, 2), coeffs, + ub.value(), model); + if (sat_solver->ModelIsUnsat()) return false; + } + } + shared_linear2_bounds->NotifyNumImported(import_id, num_imported); + return true; + }; + model->GetOrCreate()->callbacks.push_back( + import_function); +} + // Registers a callback that will report improving objective best bound. // It will be called each time new objective bound are propagated at level zero. void RegisterObjectiveBestBoundExport( @@ -1073,7 +1126,7 @@ void FillBinaryRelationRepository(const CpModelProto& model_proto, auto* encoder = model->GetOrCreate(); auto* mapping = model->GetOrCreate(); auto* repository = model->GetOrCreate(); - auto* relations_maps = model->GetOrCreate(); + auto* root_level_lin2_bounds = model->GetOrCreate(); for (const ConstraintProto& ct : model_proto.constraints()) { // Load conditional precedences and always true binary relations. @@ -1095,13 +1148,15 @@ void FillBinaryRelationRepository(const CpModelProto& model_proto, // var1_min <= var1 - delta.var2 <= var1_max, which is equivalent to // the default bounds if var2 = 0, and gives implied_lb <= var1 <= // var1_max + delta otherwise. - repository->Add(enforcement_literal, {var1, 1}, {var2, -delta}, + repository->Add(enforcement_literal, + LinearExpression2(var1, var2, 1, -delta), var1_domain.Min(), var1_domain.Max()); } else if (negated_var2 != kNoIntegerVariable) { // var1_min + delta <= var1 + delta.neg_var2 <= var1_max + delta, // which is equivalent to the default bounds if neg_var2 = 1, and // gives implied_lb <= var1 <= var1_max + delta otherwise. - repository->Add(enforcement_literal, {var1, 1}, {negated_var2, delta}, + repository->Add(enforcement_literal, + LinearExpression2(var1, negated_var2, 1, delta), var1_domain.Min() + delta, var1_domain.Max() + delta); } }; @@ -1137,23 +1192,19 @@ void FillBinaryRelationRepository(const CpModelProto& model_proto, if (ct.enforcement_literal().empty()) { if (vars.size() == 2) { - repository->Add(Literal(kNoLiteralIndex), {vars[0], coeffs[0]}, - {vars[1], coeffs[1]}, rhs_min, rhs_max); - - LinearExpression2 expr; - expr.vars[0] = vars[0]; - expr.vars[1] = vars[1]; - expr.coeffs[0] = coeffs[0]; - expr.coeffs[1] = coeffs[1]; - relations_maps->AddRelationBounds(expr, rhs_min, rhs_max); + const LinearExpression2 expr(vars[0], vars[1], coeffs[0], coeffs[1]); + root_level_lin2_bounds->Add(expr, rhs_min, rhs_max); } } else { const Literal lit = mapping->Literal(ct.enforcement_literal(0)); if (vars.size() == 1) { - repository->Add(lit, {vars[0], coeffs[0]}, {}, rhs_min, rhs_max); + repository->Add( + lit, LinearExpression2(vars[0], kNoIntegerVariable, coeffs[0], 0), + rhs_min, rhs_max); } else if (vars.size() == 2) { - repository->Add(lit, {vars[0], coeffs[0]}, {vars[1], coeffs[1]}, - rhs_min, rhs_max); + repository->Add( + lit, LinearExpression2(vars[0], vars[1], coeffs[0], coeffs[1]), + rhs_min, rhs_max); } } } @@ -1216,10 +1267,6 @@ void LoadBaseModel(const CpModelProto& model_proto, Model* model) { AddFullEncodingFromSearchBranching(model_proto, model); if (sat_solver->ModelIsUnsat()) return unsat(); - // Reserve space for the precedence relations. - model->GetOrCreate()->Resize( - model->GetOrCreate()->NumIntegerVariables().value()); - FillBinaryRelationRepository(model_proto, model); if (time_limit->LimitReached()) return; @@ -1292,7 +1339,7 @@ void LoadBaseModel(const CpModelProto& model_proto, Model* model) { model->GetOrCreate()->ProcessImplicationGraph( model->GetOrCreate()); - model->GetOrCreate()->Build(); + model->GetOrCreate()->Build(); } void LoadFeasibilityPump(const CpModelProto& model_proto, Model* model) { @@ -1794,7 +1841,7 @@ void QuickSolveWithHint(const CpModelProto& model_proto, Model* model) { // Tricky: We can only test that if we don't already have a feasible solution // like we do if the hint is complete. if (parameters->debug_crash_on_bad_hint() && - shared_response_manager->SolutionsRepository().NumSolutions() == 0 && + shared_response_manager->HasFeasibleSolution() && !model->GetOrCreate()->LimitReached() && status != SatSolver::Status::FEASIBLE) { LOG(FATAL) << "QuickSolveWithHint() didn't find a feasible solution." @@ -2092,6 +2139,10 @@ SharedClasses::SharedClasses(const CpModelProto* proto, Model* global_model) bounds->LoadDebugSolution(response->DebugSolution()); } + if (params.share_linear2_bounds()) { + linear2_bounds = std::make_unique(); + } + // Create extra shared classes if needed. Note that while these parameters // are true by default, we disable them if we don't have enough workers for // them in AdaptGlobalParameters(). @@ -2126,7 +2177,7 @@ void SharedClasses::RegisterSharedClassesInLocalModel(Model* local_model) { local_model->Register(stat_tables); // TODO(user): Use parameters and not the presence/absence of these class - // to decide when to use them. + // to decide when to use them? this is not clear. if (lp_solutions != nullptr) { local_model->Register(lp_solutions.get()); } @@ -2140,6 +2191,9 @@ void SharedClasses::RegisterSharedClassesInLocalModel(Model* local_model) { if (clauses != nullptr) { local_model->Register(clauses.get()); } + if (linear2_bounds != nullptr) { + local_model->Register(linear2_bounds.get()); + } } bool SharedClasses::SearchIsDone() { @@ -2152,5 +2206,37 @@ bool SharedClasses::SearchIsDone() { return false; } +void SharedClasses::LogFinalStatistics() { + if (!logger->LoggingIsEnabled()) return; + + logger->FlushPendingThrottledLogs(/*ignore_rates=*/true); + SOLVER_LOG(logger, ""); + + stat_tables->Display(logger); + response->DisplayImprovementStatistics(); + + std::vector> table; + table.push_back({"Solution repositories", "Added", "Queried", "Synchro"}); + response->SolutionPool().AddTableStats(&table); + table.push_back(ls_hints->TableLineStats()); + if (lp_solutions != nullptr) { + table.push_back(lp_solutions->TableLineStats()); + } + if (incomplete_solutions != nullptr) { + table.push_back(incomplete_solutions->TableLineStats()); + } + SOLVER_LOG(logger, FormatTable(table)); + + // TODO(user): we can combine the "bounds table" into one for shorter logs. + if (bounds != nullptr) bounds->LogStatistics(logger); + if (linear2_bounds != nullptr) linear2_bounds->LogStatistics(logger); + + if (clauses != nullptr) clauses->LogStatistics(logger); + + // Extra logging if needed. Note that these are mainly activated on + // --vmodule *some_file*=1 and are here for development. + stats->Log(logger); +} + } // namespace sat } // namespace operations_research diff --git a/ortools/sat/cp_model_solver_helpers.h b/ortools/sat/cp_model_solver_helpers.h index 1f46f77495d..af00cb32139 100644 --- a/ortools/sat/cp_model_solver_helpers.h +++ b/ortools/sat/cp_model_solver_helpers.h @@ -60,12 +60,15 @@ struct SharedClasses { std::unique_ptr lp_solutions; std::unique_ptr incomplete_solutions; std::unique_ptr clauses; + std::unique_ptr linear2_bounds; // call local_model->Register() on most of the class here, this allow to // more easily depends on one of the shared class deep within the solver. void RegisterSharedClassesInLocalModel(Model* local_model); bool SearchIsDone(); + + void LogFinalStatistics(); }; // Loads a CpModelProto inside the given model. @@ -119,6 +122,11 @@ int RegisterClausesLevelZeroImport(int id, SharedClausesManager* shared_clauses_manager, Model* model); +// This will register a level zero callback to imports new linear2 from the +// SharedLinear2Bounds. +void RegisterLinear2BoundsImport(SharedLinear2Bounds* shared_linear2_bounds, + Model* model); + void PostsolveResponseWrapper(const SatParameters& params, int num_variable_in_original_model, const CpModelProto& mapping_proto, diff --git a/ortools/sat/cp_model_solver_test.cc b/ortools/sat/cp_model_solver_test.cc index 63ab2fae0fb..e3d719b4000 100644 --- a/ortools/sat/cp_model_solver_test.cc +++ b/ortools/sat/cp_model_solver_test.cc @@ -109,7 +109,7 @@ TEST(StopAfterFirstSolutionTest, BooleanLinearOptimizationProblem) { Model model; SatParameters params; - params.set_num_search_workers(8); + params.set_num_workers(8); params.set_stop_after_first_solution(true); int num_solutions = 0; @@ -1070,10 +1070,12 @@ TEST(SolveCpModelTest, SolutionHintMinimizeL1DistanceTest) { // TODO(user): Instead, we might change the presolve to always try to keep the // given hint feasible. Model model; - model.Add( - NewSatParameters("repair_hint:true, stop_after_first_solution:true, " - "keep_all_feasible_solutions_in_presolve:true " - "num_workers:1")); + SatParameters params; + params.set_repair_hint(true); + params.set_stop_after_first_solution(true); + params.set_keep_all_feasible_solutions_in_presolve(true); + params.set_num_workers(1); + model.Add(NewSatParameters(params)); const CpSolverResponse response = SolveCpModel(model_proto, &model); EXPECT_THAT(response.status(), AnyOf(Eq(CpSolverStatus::OPTIMAL), Eq(CpSolverStatus::FEASIBLE))); diff --git a/ortools/sat/cumulative.cc b/ortools/sat/cumulative.cc index 3d910e779d8..8cde3428c07 100644 --- a/ortools/sat/cumulative.cc +++ b/ortools/sat/cumulative.cc @@ -212,8 +212,8 @@ std::function Cumulative( // having two independent constraint doing the same propagation. std::vector full_precedences; if (parameters.exploit_all_precedences()) { - model->GetOrCreate()->ComputeFullPrecedences( - index_to_end_vars, &full_precedences); + model->GetOrCreate() + ->ComputeFullPrecedences(index_to_end_vars, &full_precedences); } for (const FullIntegerPrecedence& data : full_precedences) { const int size = data.indices.size(); diff --git a/ortools/sat/cumulative_energy_test.cc b/ortools/sat/cumulative_energy_test.cc index a8b56e9905f..27f4cb929c6 100644 --- a/ortools/sat/cumulative_energy_test.cc +++ b/ortools/sat/cumulative_energy_test.cc @@ -176,8 +176,8 @@ bool SolveUsingNaiveModel(const EnergyInstance& instance) { std::vector intervals; std::vector consumptions; IntegerVariable one = model.Add(ConstantIntegerVariable(1)); - IntervalsRepository* intervals_repository = - model.GetOrCreate(); + auto* intervals_repository = model.GetOrCreate(); + auto* precedences = model.GetOrCreate(); for (const auto& task : instance.tasks) { if (task.is_optional) { @@ -207,7 +207,7 @@ bool SolveUsingNaiveModel(const EnergyInstance& instance) { CHECK_NE(start_expr.var, kNoIntegerVariable); const IntegerVariable start = start_expr.var; if (previous_start != kNoIntegerVariable) { - model.Add(LowerOrEqual(previous_start, start)); + precedences->AddPrecedence(previous_start, start); } else { first_start = start; } @@ -215,8 +215,8 @@ bool SolveUsingNaiveModel(const EnergyInstance& instance) { } // start[last] <= start[0] + duration_max - 1 if (previous_start != kNoIntegerVariable) { - model.Add(LowerOrEqualWithOffset(previous_start, first_start, - -task.duration_max + 1)); + precedences->AddPrecedenceWithOffset(previous_start, first_start, + -task.duration_max + 1); } } } diff --git a/ortools/sat/diffn.cc b/ortools/sat/diffn.cc index b848740606d..078de5eb928 100644 --- a/ortools/sat/diffn.cc +++ b/ortools/sat/diffn.cc @@ -32,7 +32,6 @@ #include "absl/log/vlog_is_on.h" #include "absl/numeric/bits.h" #include "absl/types/span.h" -#include "ortools/base/stl_util.h" #include "ortools/sat/2d_distances_propagator.h" #include "ortools/sat/2d_mandatory_overlap_propagator.h" #include "ortools/sat/2d_orthogonal_packing.h" @@ -277,11 +276,11 @@ void AddNonOverlappingRectangles(const std::vector& x, DCHECK_EQ(sat_solver->CurrentDecisionLevel(), 0); for (int i = 0; i < num_boxes; ++i) { - if (repository->IsAbsent(x[i])) continue; - if (repository->IsAbsent(y[i])) continue; + if (repository->IsOptional(x[i])) continue; + if (repository->IsOptional(y[i])) continue; for (int j = i + 1; j < num_boxes; ++j) { - if (repository->IsAbsent(x[j])) continue; - if (repository->IsAbsent(y[j])) continue; + if (repository->IsOptional(x[j])) continue; + if (repository->IsOptional(y[j])) continue; // At most one of these two x options is true. const Literal x_ij = repository->GetOrCreatePrecedenceLiteral( @@ -308,21 +307,7 @@ void AddNonOverlappingRectangles(const std::vector& x, } // At least one of the 4 options is true. - std::vector clause = {x_ij, x_ji, y_ij, y_ji}; - if (repository->IsOptional(x[i])) { - clause.push_back(repository->PresenceLiteral(x[i]).Negated()); - } - if (repository->IsOptional(y[i])) { - clause.push_back(repository->PresenceLiteral(y[i]).Negated()); - } - if (repository->IsOptional(x[j])) { - clause.push_back(repository->PresenceLiteral(x[j]).Negated()); - } - if (repository->IsOptional(y[j])) { - clause.push_back(repository->PresenceLiteral(y[j]).Negated()); - } - gtl::STLSortAndRemoveDuplicates(&clause); - if (!sat_solver->AddProblemClause(clause)) { + if (!sat_solver->AddProblemClause({x_ij, x_ji, y_ij, y_ji})) { return; } } diff --git a/ortools/sat/disjunctive.cc b/ortools/sat/disjunctive.cc index 0faa84e7606..c018335fceb 100644 --- a/ortools/sat/disjunctive.cc +++ b/ortools/sat/disjunctive.cc @@ -25,6 +25,7 @@ #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/intervals.h" +#include "ortools/sat/linear_propagation.h" #include "ortools/sat/model.h" #include "ortools/sat/precedences.h" #include "ortools/sat/sat_base.h" @@ -143,6 +144,9 @@ void AddDisjunctive(const std::vector& intervals, // using the fact that they are in disjunction. if (params.use_precedences_in_disjunctive_constraint() && !params.use_combined_no_overlap()) { + // Lets try to exploit linear3 too. + model->GetOrCreate()->SetPushAffineUbForBinaryRelation(); + for (const bool time_direction : {true, false}) { DisjunctivePrecedences* precedences = new DisjunctivePrecedences(time_direction, helper, model); @@ -276,8 +280,8 @@ bool DisjunctiveWithTwoItems::Propagate() { helper_->ClearReason(); helper_->AddPresenceReason(task_before); helper_->AddPresenceReason(task_after); - helper_->AddReasonForBeingBefore(task_before, task_after); - helper_->AddReasonForBeingBefore(task_after, task_before); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(task_before, task_after); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(task_after, task_before); return helper_->ReportConflict(); } @@ -295,7 +299,8 @@ bool DisjunctiveWithTwoItems::Propagate() { if (helper_->StartMin(task_after) < end_min_before) { // Reason for precedences if both present. helper_->ClearReason(); - helper_->AddReasonForBeingBefore(task_before, task_after); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(task_before, + task_after); // Reason for the bound push. helper_->AddPresenceReason(task_before); @@ -311,7 +316,8 @@ bool DisjunctiveWithTwoItems::Propagate() { if (helper_->EndMax(task_before) > start_max_after) { // Reason for precedences if both present. helper_->ClearReason(); - helper_->AddReasonForBeingBefore(task_before, task_after); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(task_before, + task_after); // Reason for the bound push. helper_->AddPresenceReason(task_after); @@ -527,7 +533,7 @@ bool DisjunctiveOverloadChecker::Propagate() { const int to_push = task_with_max_end_min.task_index; helper_->ClearReason(); helper_->AddPresenceReason(task); - helper_->AddReasonForBeingBefore(task, to_push); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(task, to_push); helper_->AddEndMinReason(task, end_min); if (!helper_->IncreaseStartMin(to_push, end_min)) { @@ -750,14 +756,19 @@ bool DisjunctiveSimplePrecedences::Propagate() { bool DisjunctiveSimplePrecedences::Push(TaskTime before, int t) { const int t_before = before.task_index; + DCHECK_NE(t_before, t); helper_->ClearReason(); helper_->AddPresenceReason(t_before); - helper_->AddReasonForBeingBefore(t_before, t); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(t_before, t); helper_->AddEndMinReason(t_before, before.time); if (!helper_->IncreaseStartMin(t, before.time)) { return false; } + if (helper_->CurrentDecisionLevel() == 0 && helper_->IsPresent(t_before) && + helper_->IsPresent(t)) { + if (!helper_->NotifyLevelZeroPrecedence(t_before, t)) return false; + } ++stats_.num_propagations; return true; } @@ -818,8 +829,8 @@ bool DisjunctiveSimplePrecedences::PropagateOneDirection() { helper_->ClearReason(); helper_->AddPresenceReason(blocking_task); helper_->AddPresenceReason(t); - helper_->AddReasonForBeingBefore(blocking_task, t); - helper_->AddReasonForBeingBefore(t, blocking_task); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(blocking_task, t); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(t, blocking_task); return helper_->ReportConflict(); } else if (end_min > best_task_before.time) { best_task_before = {t, end_min}; @@ -927,9 +938,13 @@ bool DisjunctiveDetectablePrecedences::Push(IntegerValue task_set_end_min, // Heuristic, if some tasks are known to be after the first one, // we just add the min-size as a reason. + // + // TODO(user): ideally we don't want to do that if we don't have a level + // zero precedence... if (i > critical_index && helper_->GetCurrentMinDistanceBetweenTasks( - sorted_tasks[critical_index].task, ct, - /*add_reason_if_after=*/true) >= 0) { + sorted_tasks[critical_index].task, ct) >= 0) { + helper_->AddReasonForBeingBeforeAssumingNoOverlap( + sorted_tasks[critical_index].task, ct); helper_->AddSizeMinReason(ct); } else { helper_->AddEnergyAfterReason(ct, sorted_tasks[i].size_min, window_start); @@ -937,9 +952,9 @@ bool DisjunctiveDetectablePrecedences::Push(IntegerValue task_set_end_min, // We only need the reason for being before if we don't already have // a static precedence between the tasks. - const IntegerValue dist = helper_->GetCurrentMinDistanceBetweenTasks( - ct, t, /*add_reason_if_after=*/true); + const IntegerValue dist = helper_->GetCurrentMinDistanceBetweenTasks(ct, t); if (dist >= 0) { + helper_->AddReasonForBeingBeforeAssumingNoOverlap(ct, t); energy_of_task_before += sorted_tasks[i].size_min; min_slack = std::min(min_slack, dist); } else { @@ -969,7 +984,7 @@ bool DisjunctiveDetectablePrecedences::Push(IntegerValue task_set_end_min, // Process detected precedence. if (helper_->CurrentDecisionLevel() == 0 && helper_->IsPresent(t)) { for (int i = critical_index; i < sorted_tasks.size(); ++i) { - if (!helper_->PropagatePrecedence(sorted_tasks[i].task, t)) { + if (!helper_->NotifyLevelZeroPrecedence(sorted_tasks[i].task, t)) { return false; } } @@ -1047,8 +1062,8 @@ bool DisjunctiveDetectablePrecedences::PropagateWithRanks() { helper_->ClearReason(); helper_->AddPresenceReason(blocking_task); helper_->AddPresenceReason(t); - helper_->AddReasonForBeingBefore(blocking_task, t); - helper_->AddReasonForBeingBefore(t, blocking_task); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(blocking_task, t); + helper_->AddReasonForBeingBeforeAssumingNoOverlap(t, blocking_task); return helper_->ReportConflict(); } else { if (!some_propagation && rank > highest_rank) { @@ -1210,13 +1225,13 @@ bool DisjunctivePrecedences::PropagateSubwindow() { // TODO(user): we should probably change the api to return a Span. // // TODO(user): If more than one set of task push the same variable, we - // probabaly only want to keep the best push? Maybe we want to process them + // probably only want to keep the best push? Maybe we want to process them // in reverse order of what we do here? indices_before_.clear(); IntegerValue local_start; IntegerValue local_end; for (; global_i < size; ++global_i) { - const PrecedenceRelations::PrecedenceData& data = before_[global_i]; + const EnforcedLinear2Bounds::PrecedenceData& data = before_[global_i]; if (data.var != var) break; const int index = data.index; const auto [t, start_of_t] = window_[index]; @@ -1249,6 +1264,7 @@ bool DisjunctivePrecedences::PropagateSubwindow() { int best_index = -1; const IntegerValue current_var_lb = integer_trail_->LowerBound(var); IntegerValue best_new_lb = current_var_lb; + IntegerValue min_offset_at_best = kMinIntegerValue; IntegerValue min_offset = kMaxIntegerValue; IntegerValue sum_of_duration = 0; for (int i = num_before; --i >= 0;) { @@ -1259,8 +1275,9 @@ bool DisjunctivePrecedences::PropagateSubwindow() { // the offset as much as possible. Note that the alternative of storing it // in PrecedenceData is not necessarily better and harder to update as we // dive/backtrack. - const IntegerValue inner_offset = -precedence_relations_->UpperBound( - LinearExpression2::Difference(end_exp.var, var)); + const IntegerValue inner_offset = + -linear2_bounds_->NonTrivialUpperBoundForGcd1( + LinearExpression2::Difference(end_exp.var, var)); DCHECK_NE(inner_offset, kMinIntegerValue); // We have var >= end_exp.var + inner_offset, so @@ -1275,7 +1292,7 @@ bool DisjunctivePrecedences::PropagateSubwindow() { // This is true if we skipped all task so far in this block. if (min_offset == kMaxIntegerValue) { // If only one task is left, we can abort. - // This avoid a GetConditionalOffset() lookup. + // This avoid a linear2_bounds_ lookup. if (i == 1) break; // Lower the end_min_when_all_present for better filtering later. @@ -1292,6 +1309,7 @@ bool DisjunctivePrecedences::PropagateSubwindow() { const IntegerValue start = task_time.time; const IntegerValue new_lb = start + sum_of_duration + min_offset; if (new_lb > best_new_lb) { + min_offset_at_best = min_offset; best_new_lb = new_lb; best_index = i; } @@ -1309,14 +1327,13 @@ bool DisjunctivePrecedences::PropagateSubwindow() { helper_->AddPresenceReason(ct); helper_->AddEnergyAfterReason(ct, helper_->SizeMin(ct), window_start); - // Fetch the explanation. + // Fetch the explanation of (var - end) >= min_offset // This is okay if a bit slow since we only do that when we push. - const AffineExpression& end_exp = helper_->Ends()[ct]; - const LinearExpression2 expr = - LinearExpression2::Difference(end_exp.var, var); - precedence_relations_->AddReasonForUpperBoundLowerThan( - expr, precedence_relations_->UpperBound(expr), - helper_->MutableLiteralReason(), helper_->MutableIntegerReason()); + const auto [expr, ub] = EncodeDifferenceLowerThan( + helper_->Ends()[ct], var, -min_offset_at_best); + linear2_bounds_->AddReasonForUpperBoundLowerThan( + expr, ub, helper_->MutableLiteralReason(), + helper_->MutableIntegerReason()); } ++stats_.num_propagations; if (!helper_->PushIntegerLiteral( @@ -1516,8 +1533,9 @@ bool DisjunctiveNotLast::PropagateSubwindow() { helper_->AddPresenceReason(ct); helper_->AddEnergyAfterReason(ct, sorted_tasks[i].size_min, window_start); - if (helper_->GetCurrentMinDistanceBetweenTasks( - ct, t, /*add_reason_if_after=*/true) < 0) { + if (helper_->GetCurrentMinDistanceBetweenTasks(ct, t) >= 0) { + helper_->AddReasonForBeingBeforeAssumingNoOverlap(ct, t); + } else { helper_->AddStartMaxReason(ct, largest_ct_start_max); } } @@ -1763,9 +1781,11 @@ bool DisjunctiveEdgeFinding::PropagateSubwindow(IntegerValue window_end_min) { task, event_size_[event], event >= second_event ? second_start : first_start); - const IntegerValue dist = helper_->GetCurrentMinDistanceBetweenTasks( - task, gray_task, /*add_reason_if_after=*/true); - if (dist < 0) { + const IntegerValue dist = + helper_->GetCurrentMinDistanceBetweenTasks(task, gray_task); + if (dist >= 0) { + helper_->AddReasonForBeingBeforeAssumingNoOverlap(task, gray_task); + } else { all_before = false; helper_->AddEndMaxReason(task, window_end); } @@ -1788,7 +1808,7 @@ bool DisjunctiveEdgeFinding::PropagateSubwindow(IntegerValue window_end_min) { for (int i = first_event; i < window_size; ++i) { const int task = window_[i].task_index; if (!is_gray_[task]) { - if (!helper_->PropagatePrecedence(task, gray_task)) { + if (!helper_->NotifyLevelZeroPrecedence(task, gray_task)) { return false; } } diff --git a/ortools/sat/disjunctive.h b/ortools/sat/disjunctive.h index bb77c41b126..8550fc2dd1a 100644 --- a/ortools/sat/disjunctive.h +++ b/ortools/sat/disjunctive.h @@ -353,7 +353,8 @@ class DisjunctivePrecedences : public PropagatorInterface { : time_direction_(time_direction), helper_(helper), integer_trail_(model->GetOrCreate()), - precedence_relations_(model->GetOrCreate()), + precedence_relations_(model->GetOrCreate()), + linear2_bounds_(model->GetOrCreate()), stats_("DisjunctivePrecedences", model) { window_.ClearAndReserve(helper->NumTasks()); index_to_end_vars_.ClearAndReserve(helper->NumTasks()); @@ -369,20 +370,21 @@ class DisjunctivePrecedences : public PropagatorInterface { const bool time_direction_; SchedulingConstraintHelper* helper_; IntegerTrail* integer_trail_; - PrecedenceRelations* precedence_relations_; + EnforcedLinear2Bounds* precedence_relations_; + Linear2Bounds* linear2_bounds_; FixedCapacityVector window_; FixedCapacityVector index_to_end_vars_; FixedCapacityVector indices_before_; std::vector skip_; - std::vector before_; + std::vector before_; PropagationStatistics stats_; }; // This is an optimization for the case when we have a big number of such -// pairwise constraints. This should be roughtly equivalent to what the general +// pairwise constraints. This should be roughly equivalent to what the general // disjunctive case is doing, but it dealt with variable size better and has a // lot less overhead. class DisjunctiveWithTwoItems : public PropagatorInterface { diff --git a/ortools/sat/disjunctive_test.cc b/ortools/sat/disjunctive_test.cc index 85c1db32043..5fa0063cf8a 100644 --- a/ortools/sat/disjunctive_test.cc +++ b/ortools/sat/disjunctive_test.cc @@ -31,6 +31,7 @@ #include "ortools/base/logging.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" +#include "ortools/sat/integer_expr.h" #include "ortools/sat/integer_search.h" #include "ortools/sat/intervals.h" #include "ortools/sat/model.h" @@ -238,8 +239,8 @@ TEST(DisjunctiveConstraintTest, Precedences) { Trail* trail = model.GetOrCreate(); IntegerTrail* integer_trail = model.GetOrCreate(); auto* precedences = model.GetOrCreate(); - auto* relations = model.GetOrCreate(); auto* intervals = model.GetOrCreate(); + auto* lin2_bounds = model.GetOrCreate(); const auto add_affine_coeff_one_precedence = [&](const AffineExpression e1, const AffineExpression& e2) { @@ -249,8 +250,8 @@ TEST(DisjunctiveConstraintTest, Precedences) { CHECK_EQ(e2.coeff, 1); precedences->AddPrecedenceWithOffset(e1.var, e2.var, e1.constant - e2.constant); - relations->AddUpperBound(LinearExpression2::Difference(e1.var, e2.var), - e2.constant - e1.constant); + lin2_bounds->AddUpperBound(LinearExpression2::Difference(e1.var, e2.var), + e2.constant - e1.constant); }; const int kStart(0); @@ -483,6 +484,22 @@ TEST(DisjunctiveTest, TwoIntervalsTest) { EXPECT_EQ(12, CountAllSolutions(instance, AddDisjunctive)); } +namespace { + +void AddLowerOrEqualWithOffset(AffineExpression a, IntegerVariable b, + int64_t offset, Model* model) { + const int64_t rhs = -a.constant.value() - offset; + std::vector vars = {a.var, b}; + std::vector coeffs = {a.coeff.value(), -1}; + AddWeightedSumLowerOrEqual({}, vars, coeffs, rhs, model); + + // We also need to register them. + model->GetOrCreate()->AddUpperBound( + LinearExpression2::Difference(a.var, b), rhs); +} + +} // namespace + TEST(DisjunctiveTest, Precedences) { Model model; @@ -493,10 +510,9 @@ TEST(DisjunctiveTest, Precedences) { const IntegerVariable var = model.Add(NewIntegerVariable(0, 10)); IntervalsRepository* intervals = model.GetOrCreate(); - model.Add( - AffineCoeffOneLowerOrEqualWithOffset(intervals->End(ids[0]), var, 5)); - model.Add( - AffineCoeffOneLowerOrEqualWithOffset(intervals->End(ids[1]), var, 4)); + + AddLowerOrEqualWithOffset(intervals->End(ids[0]), var, 5, &model); + AddLowerOrEqualWithOffset(intervals->End(ids[1]), var, 4, &model); EXPECT_TRUE(model.GetOrCreate()->Propagate()); EXPECT_EQ(model.Get(LowerBound(var)), (3 + 2) + std::min(4, 5)); diff --git a/ortools/sat/feasibility_jump.cc b/ortools/sat/feasibility_jump.cc index 3d57b0ff5b8..64ae336a883 100644 --- a/ortools/sat/feasibility_jump.cc +++ b/ortools/sat/feasibility_jump.cc @@ -364,8 +364,7 @@ std::function FeasibilityJumpSolver::GenerateTask(int64_t /*task_id*/) { // still finish each batch though). We will also reset the luby sequence. bool new_best_solution_was_found = false; if (type() == SubSolver::INCOMPLETE) { - const int64_t best = - shared_response_->SolutionsRepository().GetBestRank(); + const int64_t best = shared_response_->GetBestSolutionObjective().value(); if (best < state_->last_solution_rank) { states_->ResetLubyCounter(); new_best_solution_was_found = true; @@ -394,11 +393,9 @@ std::function FeasibilityJumpSolver::GenerateTask(int64_t /*task_id*/) { new_best_solution_was_found) { if (type() == SubSolver::INCOMPLETE) { // Choose a base solution for this neighborhood. - std::shared_ptr::Solution> - solution = shared_response_->SolutionsRepository() - .GetRandomBiasedSolution(random_); - state_->solution = solution->variable_values; - state_->base_solution = solution; + state_->base_solution = + shared_response_->SolutionPool().GetSolutionToImprove(random_); + state_->solution = state_->base_solution->variable_values; ++state_->num_solutions_imported; } else { if (!first_time) { @@ -427,6 +424,10 @@ std::function FeasibilityJumpSolver::GenerateTask(int64_t /*task_id*/) { } // Between chunk, we synchronize bounds. + // + // TODO(user): This do not play well with optimizing solution whose + // objective lag behind... Basically, we can run LS on old solution but will + // only consider it feasible if it improve the best known solution. bool recompute_compound_weights = false; if (linear_model_->model_proto().has_objective()) { const IntegerValue lb = shared_response_->GetInnerObjectiveLowerBound(); @@ -500,15 +501,15 @@ std::function FeasibilityJumpSolver::GenerateTask(int64_t /*task_id*/) { ++state_->counters.num_batches; if (DoSomeLinearIterations() && DoSomeGeneralIterations()) { // Checks for infeasibility induced by the non supported constraints. + // + // TODO(user): Checking the objective is faster and we could avoid to + // check feasibility if we are not going to keep the solution anyway. if (SolutionIsFeasible(linear_model_->model_proto(), state_->solution)) { auto pointers = PushAndMaybeCombineSolution( shared_response_, linear_model_->model_proto(), state_->solution, absl::StrCat(name(), "_", state_->options.name(), "(", OneLineStats(), ")"), - state_->base_solution == nullptr - ? absl::Span() - : state_->base_solution->variable_values, - /*model=*/nullptr); + state_->base_solution); // If we pushed a new solution, we use it as a new "base" so that we // will have a smaller delta on the next solution we find. state_->base_solution = pointers.pushed_solution; diff --git a/ortools/sat/feasibility_jump.h b/ortools/sat/feasibility_jump.h index d1975161c71..e40f5d42b99 100644 --- a/ortools/sat/feasibility_jump.h +++ b/ortools/sat/feasibility_jump.h @@ -510,7 +510,7 @@ class FeasibilityJumpSolver : public SubSolver { if (shared_response_->ProblemIsSolved()) return false; if (shared_time_limit_->LimitReached()) return false; - return (shared_response_->SolutionsRepository().NumSolutions() > 0) == + return shared_response_->HasFeasibleSolution() == (type() == SubSolver::INCOMPLETE); } diff --git a/ortools/sat/flaky_models_test.cc b/ortools/sat/flaky_models_test.cc index c233ab9070d..e00ebee9813 100644 --- a/ortools/sat/flaky_models_test.cc +++ b/ortools/sat/flaky_models_test.cc @@ -90,7 +90,7 @@ TEST(FlakyTest, Issue3108) { SatParameters parameters; parameters.set_log_search_progress(true); parameters.set_cp_model_probing_level(0); - parameters.set_num_search_workers(1); + parameters.set_num_workers(1); const CpSolverResponse response = SolveWithParameters(model_proto, parameters); EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); diff --git a/ortools/sat/integer.cc b/ortools/sat/integer.cc index 3adcb4d8bae..5ecc0455a8f 100644 --- a/ortools/sat/integer.cc +++ b/ortools/sat/integer.cc @@ -983,7 +983,8 @@ int IntegerTrail::FindTrailIndexOfVarBefore(IntegerVariable var, int IntegerTrail::FindLowestTrailIndexThatExplainBound( IntegerLiteral i_lit) const { DCHECK_LE(i_lit.bound, var_lbs_[i_lit.var]); - if (i_lit.bound <= LevelZeroLowerBound(i_lit.var)) return -1; + DCHECK(!IsTrueAtLevelZero(i_lit)); + int trail_index = var_trail_index_[i_lit.var]; // Check the validity of the cached index and use it if possible. This caching @@ -1003,6 +1004,7 @@ int IntegerTrail::FindLowestTrailIndexThatExplainBound( int prev_trail_index = trail_index; while (true) { + ++work_done_in_explain_lower_than_; if (trail_index >= var_trail_index_cache_threshold_) { var_trail_index_cache_[i_lit.var] = trail_index; } @@ -1171,10 +1173,9 @@ std::vector* IntegerTrail::InitializeConflict( lazy_reasons_.back().Explain(conflict, &tmp_queue_); } else { conflict->assign(literals_reason.begin(), literals_reason.end()); - const int num_vars = var_lbs_.size(); for (const IntegerLiteral& literal : bounds_reason) { - const int trail_index = FindLowestTrailIndexThatExplainBound(literal); - if (trail_index >= num_vars) tmp_queue_.push_back(trail_index); + if (IsTrueAtLevelZero(literal)) continue; + tmp_queue_.push_back(FindLowestTrailIndexThatExplainBound(literal)); } } return conflict; @@ -1553,9 +1554,8 @@ bool IntegerTrail::EnqueueInternal( // efficiency and a potential smaller reason. auto* conflict = InitializeConflict(i_lit, use_lazy_reason, literal_reason, integer_reason); - { - const int trail_index = FindLowestTrailIndexThatExplainBound(ub_reason); - if (trail_index >= 0) tmp_queue_.push_back(trail_index); + if (!IsTrueAtLevelZero(ub_reason)) { + tmp_queue_.push_back(FindLowestTrailIndexThatExplainBound(ub_reason)); } MergeReasonIntoInternal(conflict, NextConflictId()); return false; @@ -1771,12 +1771,10 @@ absl::Span IntegerTrail::Dependencies(int reason_index) const { int new_size = 0; int* data = trail_index_reason_buffer_.data() + start; - const int num_vars = var_lbs_.size(); for (int i = start; i < end; ++i) { - const int dep = - FindLowestTrailIndexThatExplainBound(bounds_reason_buffer_[i]); - if (dep >= num_vars) { - data[new_size++] = dep; + const IntegerLiteral to_explain = bounds_reason_buffer_[i]; + if (!IsTrueAtLevelZero(to_explain)) { + data[new_size++] = FindLowestTrailIndexThatExplainBound(to_explain); } } cached_sizes_[reason_index] = new_size; @@ -1818,14 +1816,10 @@ std::vector IntegerTrail::ReasonFor(IntegerLiteral literal) const { void IntegerTrail::MergeReasonInto(absl::Span literals, std::vector* output) const { DCHECK(tmp_queue_.empty()); - const int num_vars = var_lbs_.size(); for (const IntegerLiteral& literal : literals) { if (literal.IsAlwaysTrue()) continue; - const int trail_index = FindLowestTrailIndexThatExplainBound(literal); - - // Any indices lower than that means that there is no reason needed. - // Note that it is important for size to be signed because of -1 indices. - if (trail_index >= num_vars) tmp_queue_.push_back(trail_index); + if (IsTrueAtLevelZero(literal)) continue; + tmp_queue_.push_back(FindLowestTrailIndexThatExplainBound(literal)); } return MergeReasonIntoInternal(output, -1); } diff --git a/ortools/sat/integer.h b/ortools/sat/integer.h index 1c8cbf14381..14e485fdad5 100644 --- a/ortools/sat/integer.h +++ b/ortools/sat/integer.h @@ -505,6 +505,7 @@ class IntegerTrail final : public SatPropagator { // Same as above for an affine expression. IntegerValue LowerBound(AffineExpression expr) const; IntegerValue UpperBound(AffineExpression expr) const; + IntegerValue UpperBound(LinearExpression2 expr) const; bool IsFixed(AffineExpression expr) const; IntegerValue FixedValue(AffineExpression expr) const; @@ -522,6 +523,7 @@ class IntegerTrail final : public SatPropagator { // Returns the current value (if known) of an IntegerLiteral. bool IntegerLiteralIsTrue(IntegerLiteral l) const; bool IntegerLiteralIsFalse(IntegerLiteral l) const; + bool IsTrueAtLevelZero(IntegerLiteral l) const; // Returns globally valid lower/upper bound on the given integer variable. IntegerValue LevelZeroLowerBound(IntegerVariable var) const; @@ -795,39 +797,38 @@ class IntegerTrail final : public SatPropagator { void AddAllGreaterThanConstantReason(absl::Span exprs, IntegerValue target_min, std::vector* indices) const { - int64_t num_processed = 0; + constexpr int64_t check_period = 1e6; + int64_t limit_check = work_done_in_explain_lower_than_ + check_period; for (const AffineExpression& expr : exprs) { if (expr.IsConstant()) { DCHECK_GE(expr.constant, target_min); continue; } DCHECK_NE(expr.var, kNoIntegerVariable); + const IntegerLiteral to_explain = expr.GreaterOrEqual(target_min); + if (IsTrueAtLevelZero(to_explain)) continue; // On large routing problems, we can spend a lot of time in this loop. - // We check the time limit every 5 processed expressions. - if (++num_processed % 5 == 0 && time_limit_->LimitReached()) return; + if (work_done_in_explain_lower_than_ > limit_check) { + limit_check = work_done_in_explain_lower_than_ + check_period; + if (time_limit_->LimitReached()) return; + } // Skip if we already have an explanation for expr >= target_min. Note // that we already do that while processing the returned indices, so this // mainly save a FindLowestTrailIndexThatExplainBound() call per skipped // indices, which can still be costly. { - const int index = tmp_var_to_trail_index_in_queue_[expr.var]; + const int index = tmp_var_to_trail_index_in_queue_[to_explain.var]; if (index == std::numeric_limits::max()) continue; - if (index > 0 && - expr.ValueAt(integer_trail_[index].bound) >= target_min) { + if (index > 0 && integer_trail_[index].bound >= to_explain.bound) { has_dependency_ = true; continue; } } // We need to find the index that explain the bound. - // Note that this will skip if the condition is true at level zero. - const int index = - FindLowestTrailIndexThatExplainBound(expr.GreaterOrEqual(target_min)); - if (index >= 0) { - indices->push_back(index); - } + indices->push_back(FindLowestTrailIndexThatExplainBound(to_explain)); } } @@ -884,8 +885,8 @@ class IntegerTrail final : public SatPropagator { int64_t conflict_id) const; // Returns the lowest trail index of a TrailEntry that can be used to explain - // the given IntegerLiteral. The literal must be currently true (CHECKed). - // Returns -1 if the explanation is trivial. + // the given IntegerLiteral. The literal must be currently true but not true + // at level zero (DCHECKed). int FindLowestTrailIndexThatExplainBound(IntegerLiteral i_lit) const; // This must be called before Dependencies() or AppendLiteralsReason(). @@ -1032,6 +1033,8 @@ class IntegerTrail final : public SatPropagator { std::vector*> watchers_; std::vector reversible_classes_; + mutable int64_t work_done_in_explain_lower_than_ = 0; + mutable Domain temp_domain_; DelayedRootLevelDeduction* delayed_to_fix_; IntegerDomains* domains_; @@ -1375,6 +1378,17 @@ inline IntegerValue IntegerTrail::UpperBound(AffineExpression expr) const { return UpperBound(expr.var) * expr.coeff + expr.constant; } +inline IntegerValue IntegerTrail::UpperBound(LinearExpression2 expr) const { + expr.SimpleCanonicalization(); + IntegerValue result = 0; + for (int i = 0; i < 2; ++i) { + if (expr.coeffs[i] != 0) { + result += expr.coeffs[i] * UpperBound(expr.vars[i]); + } + } + return result; +} + inline bool IntegerTrail::IsFixed(AffineExpression expr) const { if (expr.var == kNoIntegerVariable) return true; return IsFixed(expr.var); @@ -1405,6 +1419,10 @@ inline bool IntegerTrail::IntegerLiteralIsFalse(IntegerLiteral l) const { return l.bound > UpperBound(l.var); } +inline bool IntegerTrail::IsTrueAtLevelZero(IntegerLiteral l) const { + return l.bound <= LevelZeroLowerBound(l.var); +} + // The level zero bounds are stored at the beginning of the trail and they also // serves as sentinels. Their index match the variables index. inline IntegerValue IntegerTrail::LevelZeroLowerBound( diff --git a/ortools/sat/integer_base.cc b/ortools/sat/integer_base.cc index 8af3a695ca7..29a7d8d1869 100644 --- a/ortools/sat/integer_base.cc +++ b/ortools/sat/integer_base.cc @@ -13,11 +13,15 @@ #include "ortools/sat/integer_base.h" +#include #include #include +#include #include +#include #include "absl/log/check.h" +#include "ortools/util/bitset.h" namespace operations_research::sat { @@ -79,22 +83,19 @@ bool LinearExpression2::NegateForCanonicalization() { return negate; } -void LinearExpression2::CanonicalizeAndUpdateBounds(IntegerValue& lb, - IntegerValue& ub, - bool allow_negation) { +bool LinearExpression2::CanonicalizeAndUpdateBounds(IntegerValue& lb, + IntegerValue& ub) { SimpleCanonicalization(); - if (coeffs[0] == 0 || coeffs[1] == 0) return; // abort. - - if (allow_negation) { - const bool negated = NegateForCanonicalization(); - if (negated) { - // We need to be able to negate without overflow. - CHECK_GE(lb, kMinIntegerValue); - CHECK_LE(ub, kMaxIntegerValue); - std::swap(lb, ub); - lb = -lb; - ub = -ub; - } + if (coeffs[0] == 0 || coeffs[1] == 0) return false; // abort. + + const bool negated = NegateForCanonicalization(); + if (negated) { + // We need to be able to negate without overflow. + CHECK_GE(lb, kMinIntegerValue); + CHECK_LE(ub, kMaxIntegerValue); + std::swap(lb, ub); + lb = -lb; + ub = -ub; } // Do gcd division. @@ -108,27 +109,71 @@ void LinearExpression2::CanonicalizeAndUpdateBounds(IntegerValue& lb, CHECK(coeffs[0] != 0 || vars[0] == kNoIntegerVariable); CHECK(coeffs[1] != 0 || vars[1] == kNoIntegerVariable); + + return negated; } -bool BestBinaryRelationBounds::Add(LinearExpression2 expr, IntegerValue lb, - IntegerValue ub) { - expr.CanonicalizeAndUpdateBounds(lb, ub); - if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) return false; +bool LinearExpression2::IsCanonicalized() const { + for (int i : {0, 1}) { + if ((vars[i] == kNoIntegerVariable) != (coeffs[i] == 0)) { + return false; + } + } + if (vars[0] >= vars[1]) return false; + + if (vars[0] == kNoIntegerVariable) return true; + + return coeffs[0] > 0 && coeffs[1] > 0; +} + +void LinearExpression2::MakeVariablesPositive() { + SimpleCanonicalization(); + for (int i = 0; i < 2; ++i) { + if (vars[i] != kNoIntegerVariable && !VariableIsPositive(vars[i])) { + coeffs[i] = -coeffs[i]; + vars[i] = NegationOf(vars[i]); + } + } +} + +std::pair +BestBinaryRelationBounds::Add(LinearExpression2 expr, IntegerValue lb, + IntegerValue ub) { + const bool negated = expr.CanonicalizeAndUpdateBounds(lb, ub); + + // We only store proper linear2. + if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) { + return {AddResult::INVALID, AddResult::INVALID}; + } auto [it, inserted] = best_bounds_.insert({expr, {lb, ub}}); - if (inserted) return true; + if (inserted) { + std::pair result = { + lb > kMinIntegerValue ? AddResult::ADDED : AddResult::INVALID, + ub < kMaxIntegerValue ? AddResult::ADDED : AddResult::INVALID}; + if (negated) std::swap(result.first, result.second); + return result; + } const auto [known_lb, known_ub] = it->second; - bool restricted = false; + + std::pair result = { + lb > kMinIntegerValue ? AddResult::NOT_BETTER : AddResult::INVALID, + ub < kMaxIntegerValue ? AddResult::NOT_BETTER : AddResult::INVALID}; if (lb > known_lb) { + result.first = (it->second.first == kMinIntegerValue) ? AddResult::ADDED + : AddResult::UPDATED; it->second.first = lb; - restricted = true; } if (ub < known_ub) { + result.second = (it->second.second == kMaxIntegerValue) + ? AddResult::ADDED + : AddResult::UPDATED; it->second.second = ub; - restricted = true; } - return restricted; + if (negated) std::swap(result.first, result.second); + return result; } RelationStatus BestBinaryRelationBounds::GetStatus(LinearExpression2 expr, @@ -165,4 +210,34 @@ IntegerValue BestBinaryRelationBounds::GetUpperBound( return kMaxIntegerValue; } +std::vector> +BestBinaryRelationBounds::GetSortedNonTrivialUpperBounds() const { + std::vector> root_relations_sorted; + root_relations_sorted.reserve(2 * best_bounds_.size()); + for (const auto& [expr, bounds] : best_bounds_) { + if (bounds.first != kMinIntegerValue) { + LinearExpression2 negated_expr = expr; + negated_expr.Negate(); + root_relations_sorted.push_back({negated_expr, -bounds.first}); + } + if (bounds.second != kMaxIntegerValue) { + root_relations_sorted.push_back({expr, bounds.second}); + } + } + std::sort(root_relations_sorted.begin(), root_relations_sorted.end()); + return root_relations_sorted; +} + +std::vector> +BestBinaryRelationBounds::GetSortedNonTrivialBounds() const { + std::vector> + root_relations_sorted; + root_relations_sorted.reserve(best_bounds_.size()); + for (const auto& [expr, bounds] : best_bounds_) { + root_relations_sorted.push_back({expr, bounds.first, bounds.second}); + } + std::sort(root_relations_sorted.begin(), root_relations_sorted.end()); + return root_relations_sorted; +} + } // namespace operations_research::sat diff --git a/ortools/sat/integer_base.h b/ortools/sat/integer_base.h index a86d15eb07f..ba4f04cdff0 100644 --- a/ortools/sat/integer_base.h +++ b/ortools/sat/integer_base.h @@ -95,6 +95,13 @@ inline IntegerValue FloorRatio(IntegerValue dividend, return result - adjust; } +// When the case positive_divisor == 1 is frequent, this is faster. +inline IntegerValue FloorRatioWithTest(IntegerValue dividend, + IntegerValue positive_divisor) { + if (positive_divisor == 1) return dividend; + return FloorRatio(dividend, positive_divisor); +} + // Overflows and saturated arithmetic. inline IntegerValue CapProdI(IntegerValue a, IntegerValue b) { @@ -369,26 +376,30 @@ struct LinearExpression2 { // This will not change any bounds on the LinearExpression2. // That is we will not potentially Negate() the expression like // CanonicalizeAndUpdateBounds() might do. - // Note that since kNoIntegerVariable=-1 and we sort the variables, if we any + // Note that since kNoIntegerVariable=-1 and we sort the variables, if we have // one zero and one non-zero we will always have the zero first. void SimpleCanonicalization(); // Fully canonicalizes the expression and updates the given bounds // accordingly. This is the same as SimpleCanonicalization(), DivideByGcd() // and the NegateForCanonicalization() with a proper updates of the bounds. - void CanonicalizeAndUpdateBounds(IntegerValue& lb, IntegerValue& ub, - bool allow_negation = false); + // Returns whether the expression was negated. + bool CanonicalizeAndUpdateBounds(IntegerValue& lb, IntegerValue& ub); // Divides the expression by the gcd of both coefficients, and returns it. // Note that we always return something >= 1 even if both coefficients are // zero. IntegerValue DivideByGcd(); + bool IsCanonicalized() const; + // Makes sure expr and -expr have the same canonical representation by // negating the expression of it is in the non-canonical form. Returns true if // the expression was negated. bool NegateForCanonicalization(); + void MakeVariablesPositive(); + absl::Span non_zero_vars() const { const int first = coeffs[0] == 0 ? 1 : 0; const int last = coeffs[1] == 0 ? 0 : 1; @@ -413,13 +424,31 @@ struct LinearExpression2 { IntegerValue coeffs[2]; IntegerVariable vars[2]; + + template + friend void AbslStringify(Sink& sink, const LinearExpression2& expr) { + absl::Format(&sink, "%d X%d + %d X%d", expr.coeffs[0].value(), + expr.vars[0].value(), expr.coeffs[1].value(), + expr.vars[1].value()); + } }; -inline std::ostream& operator<<(std::ostream& os, - const LinearExpression2& expr) { - os << absl::StrCat(expr.coeffs[0], " X", expr.vars[0], " + ", expr.coeffs[1], - " X", expr.vars[1]); - return os; +// Encodes (a - b <= ub) in (linear2 <= ub) format. +// Note that the returned expression is canonicalized and divided by its GCD. +inline std::pair EncodeDifferenceLowerThan( + AffineExpression a, AffineExpression b, IntegerValue ub) { + LinearExpression2 expr; + expr.vars[0] = a.var; + expr.coeffs[0] = a.coeff; + expr.vars[1] = b.var; + expr.coeffs[1] = -b.coeff; + IntegerValue rhs = ub + b.constant - a.constant; + + // Canonicalize. + expr.SimpleCanonicalization(); + const IntegerValue gcd = expr.DivideByGcd(); + rhs = FloorRatio(rhs, gcd); + return {std::move(expr), rhs}; } template @@ -437,9 +466,21 @@ class BestBinaryRelationBounds { public: // Register the fact that expr \in [lb, ub] is true. // - // Returns true if this fact is new, that is if the bounds are tighter than - // the current ones. - bool Add(LinearExpression2 expr, IntegerValue lb, IntegerValue ub); + // If lb==kMinIntegerValue it only register that expr <= ub (and symmetrically + // for ub==kMaxIntegerValue). + // + // Returns for each of the bound if it was restricted (added/updated), if it + // was ignored because a better or equal bound was already present, or if it + // was rejected because it was invalid (e.g. the expression was a degenerate + // linear2 or the bound was a min/max value). + enum class AddResult { + ADDED, + UPDATED, + NOT_BETTER, + INVALID, + }; + std::pair Add(LinearExpression2 expr, IntegerValue lb, + IntegerValue ub); // Returns the known status of expr <= bound. RelationStatus GetStatus(LinearExpression2 expr, IntegerValue lb, @@ -450,6 +491,18 @@ class BestBinaryRelationBounds { // entry in the hash-map. IntegerValue GetUpperBound(LinearExpression2 expr) const; + // Same as GetUpperBound() but assume the expression is already canonicalized. + // This is slightly faster. + IntegerValue UpperBoundWhenCanonicalized(LinearExpression2 expr) const; + + int64_t num_bounds() const { return best_bounds_.size(); } + + std::vector> + GetSortedNonTrivialUpperBounds() const; + + std::vector> + GetSortedNonTrivialBounds() const; + private: // The best bound on the given "canonicalized" expression. absl::flat_hash_map> @@ -512,6 +565,28 @@ std::ostream& operator<<(std::ostream& os, const ValueLiteralPair& p); DEFINE_STRONG_INDEX_TYPE(IntervalVariable); const IntervalVariable kNoIntervalVariable(-1); +// This functions appears in hot spot, and so it is important to inline it. +// +// TODO(user): Maybe introduce a CanonicalizedLinear2 class so we automatically +// get the better function, and it documents when we have canonicalized +// expression. +inline IntegerValue BestBinaryRelationBounds::UpperBoundWhenCanonicalized( + LinearExpression2 expr) const { + DCHECK_EQ(expr.DivideByGcd(), 1); + DCHECK(expr.IsCanonicalized()); + const bool negated = expr.NegateForCanonicalization(); + const auto it = best_bounds_.find(expr); + if (it != best_bounds_.end()) { + const auto [known_lb, known_ub] = it->second; + if (negated) { + return -known_lb; + } else { + return known_ub; + } + } + return kMaxIntegerValue; +} + // ============================================================================ // Implementation. // ============================================================================ @@ -552,8 +627,8 @@ inline IntegerLiteral AffineExpression::GreaterOrEqual( : IntegerLiteral::FalseLiteral(); } DCHECK_GT(coeff, 0); - return IntegerLiteral::GreaterOrEqual(var, - CeilRatio(bound - constant, coeff)); + return IntegerLiteral::GreaterOrEqual( + var, coeff == 1 ? bound - constant : CeilRatio(bound - constant, coeff)); } // var * coeff + constant <= bound. @@ -563,7 +638,8 @@ inline IntegerLiteral AffineExpression::LowerOrEqual(IntegerValue bound) const { : IntegerLiteral::FalseLiteral(); } DCHECK_GT(coeff, 0); - return IntegerLiteral::LowerOrEqual(var, FloorRatio(bound - constant, coeff)); + return IntegerLiteral::LowerOrEqual( + var, coeff == 1 ? bound - constant : FloorRatio(bound - constant, coeff)); } } // namespace sat diff --git a/ortools/sat/integer_base_test.cc b/ortools/sat/integer_base_test.cc index e3b069cd020..10774a554a1 100644 --- a/ortools/sat/integer_base_test.cc +++ b/ortools/sat/integer_base_test.cc @@ -13,6 +13,8 @@ #include "ortools/sat/integer_base.h" +#include + #include "gtest/gtest.h" namespace operations_research::sat { @@ -59,12 +61,17 @@ TEST(BestBinaryRelationBoundsTest, Basic) { expr.coeffs[0] = IntegerValue(1); expr.coeffs[1] = IntegerValue(-1); + using AddResult = BestBinaryRelationBounds::AddResult; + BestBinaryRelationBounds best_bounds; - EXPECT_TRUE(best_bounds.Add(expr, IntegerValue(0), IntegerValue(5))); - EXPECT_TRUE(best_bounds.Add(expr, IntegerValue(3), IntegerValue(8))); - EXPECT_TRUE(best_bounds.Add(expr, IntegerValue(-1), IntegerValue(4))); - EXPECT_FALSE( - best_bounds.Add(expr, IntegerValue(3), IntegerValue(4))); // best + EXPECT_EQ(best_bounds.Add(expr, IntegerValue(0), IntegerValue(5)), + std::make_pair(AddResult::ADDED, AddResult::ADDED)); + EXPECT_EQ(best_bounds.Add(expr, IntegerValue(3), IntegerValue(8)), + std::make_pair(AddResult::UPDATED, AddResult::NOT_BETTER)); + EXPECT_EQ(best_bounds.Add(expr, IntegerValue(-1), IntegerValue(4)), + std::make_pair(AddResult::NOT_BETTER, AddResult::UPDATED)); + EXPECT_EQ(best_bounds.Add(expr, IntegerValue(3), IntegerValue(4)), // best + std::make_pair(AddResult::NOT_BETTER, AddResult::NOT_BETTER)); EXPECT_EQ(RelationStatus::IS_TRUE, best_bounds.GetStatus(expr, IntegerValue(-10), IntegerValue(4))); @@ -85,8 +92,10 @@ TEST(BestBinaryRelationBoundsTest, UpperBound) { expr.coeffs[0] = IntegerValue(1); expr.coeffs[1] = IntegerValue(-1); + using AddResult = BestBinaryRelationBounds::AddResult; BestBinaryRelationBounds best_bounds; - EXPECT_TRUE(best_bounds.Add(expr, IntegerValue(0), IntegerValue(5))); + EXPECT_EQ(best_bounds.Add(expr, IntegerValue(0), IntegerValue(5)), + std::make_pair(AddResult::ADDED, AddResult::ADDED)); EXPECT_EQ(best_bounds.GetUpperBound(expr), IntegerValue(5)); diff --git a/ortools/sat/integer_search.cc b/ortools/sat/integer_search.cc index 7e095a2ebaf..f98b2935b63 100644 --- a/ortools/sat/integer_search.cc +++ b/ortools/sat/integer_search.cc @@ -394,7 +394,7 @@ std::function IntegerValueSelectionHeuristic( value_selection_heuristics.push_back( [model, response_manager](IntegerVariable var) { return SplitUsingBestSolutionValueInRepository( - var, response_manager->SolutionsRepository(), model); + var, response_manager->SolutionPool().BestSolutions(), model); }); } } @@ -872,7 +872,6 @@ std::function CumulativePrecedenceSearchHeuristic( // TODO(user): Add heuristic ordering for creating interesting precedence // first. bool found_precedence_to_add = false; - std::vector conflict; helper->ClearReason(); for (const int s : open_tasks) { for (const int t : open_tasks) { @@ -897,13 +896,13 @@ std::function CumulativePrecedenceSearchHeuristic( // fixed all literal, but if it is not, we can just return this // decision. if (trail->Assignment().LiteralIsFalse(Literal(existing))) { - conflict.push_back(Literal(existing)); + helper->MutableLiteralReason()->push_back(Literal(existing)); continue; } } else { // Make sure s could be before t. if (helper->EndMin(s) > helper->StartMax(t)) { - helper->AddReasonForBeingBefore(t, s); + helper->AddReasonForBeingBeforeAssumingNoOverlap(t, s); continue; } @@ -929,24 +928,24 @@ std::function CumulativePrecedenceSearchHeuristic( // // TODO(user): We need to add the reason for demand_min and capacity_max. // TODO(user): unfortunately we can't report it from here. - std::vector integer_reason = - *helper->MutableIntegerReason(); if (!h.capacity.IsConstant()) { - integer_reason.push_back( + helper->MutableIntegerReason()->push_back( integer_trail->UpperBoundAsLiteral(h.capacity)); } const auto& demands = h.demand_helper->Demands(); for (const int t : open_tasks) { if (helper->IsOptional(t)) { CHECK(trail->Assignment().LiteralIsTrue(helper->PresenceLiteral(t))); - conflict.push_back(helper->PresenceLiteral(t).Negated()); + helper->MutableLiteralReason()->push_back( + helper->PresenceLiteral(t).Negated()); } const AffineExpression d = demands[t]; if (!d.IsConstant()) { - integer_reason.push_back(integer_trail->LowerBoundAsLiteral(d)); + helper->MutableIntegerReason()->push_back( + integer_trail->LowerBoundAsLiteral(d)); } } - integer_trail->ReportConflict(conflict, integer_reason); + (void)helper->ReportConflict(); search_helper->NotifyThatConflictWasFoundDuringGetDecision(); if (VLOG_IS_ON(2)) { LOG(INFO) << "Conflict between precedences !"; @@ -1026,7 +1025,7 @@ std::function RandomizeOnRestartHeuristic( value_selection_heuristics.push_back( [model, response_manager](IntegerVariable var) { return SplitUsingBestSolutionValueInRepository( - var, response_manager->SolutionsRepository(), model); + var, response_manager->SolutionPool().BestSolutions(), model); }); value_selection_weight.push_back(5); } diff --git a/ortools/sat/intervals.cc b/ortools/sat/intervals.cc index c50429e71f2..113ad4e5d9a 100644 --- a/ortools/sat/intervals.cc +++ b/ortools/sat/intervals.cc @@ -43,7 +43,7 @@ IntervalsRepository::IntervalsRepository(Model* model) sat_solver_(model->GetOrCreate()), implications_(model->GetOrCreate()), integer_trail_(model->GetOrCreate()), - relations_maps_(model->GetOrCreate()) {} + reified_precedences_(model->GetOrCreate()) {} IntervalVariable IntervalsRepository::CreateInterval(IntegerVariable start, IntegerVariable end, @@ -155,9 +155,9 @@ IntervalsRepository::GetOrCreateDisjunctivePrecedenceLiteralIfNonTrivial( } // Abort if the relation is already known. - if (relations_maps_->GetLevelZeroPrecedenceStatus(a.end, b.start) == + if (reified_precedences_->GetLevelZeroPrecedenceStatus(a.end, b.start) == RelationStatus::IS_TRUE || - relations_maps_->GetLevelZeroPrecedenceStatus(b.end, a.start) == + reified_precedences_->GetLevelZeroPrecedenceStatus(b.end, a.start) == RelationStatus::IS_TRUE) { return kNoLiteralIndex; } @@ -181,10 +181,10 @@ IntervalsRepository::GetOrCreateDisjunctivePrecedenceLiteralIfNonTrivial( // Also insert it in precedences. if (enforcement_literals.empty()) { - relations_maps_->AddReifiedPrecedenceIfNonTrivial(a_before_b, a.end, - b.start); - relations_maps_->AddReifiedPrecedenceIfNonTrivial(a_before_b.Negated(), - b.end, a.start); + reified_precedences_->AddReifiedPrecedenceIfNonTrivial(a_before_b, a.end, + b.start); + reified_precedences_->AddReifiedPrecedenceIfNonTrivial(a_before_b.Negated(), + b.end, a.start); } enforcement_literals.push_back(a_before_b); @@ -212,12 +212,12 @@ IntervalsRepository::GetOrCreateDisjunctivePrecedenceLiteralIfNonTrivial( bool IntervalsRepository::CreatePrecedenceLiteralIfNonTrivial( AffineExpression x, AffineExpression y) { - const LiteralIndex index = relations_maps_->GetReifiedPrecedence(x, y); + const LiteralIndex index = reified_precedences_->GetReifiedPrecedence(x, y); if (index != kNoLiteralIndex) return false; // We want l => x <= y and not(l) => x > y <=> y + 1 <= x // Do not create l if the relation is always true or false. - if (relations_maps_->GetLevelZeroPrecedenceStatus(x, y) != + if (reified_precedences_->GetLevelZeroPrecedenceStatus(x, y) != RelationStatus::IS_UNKNOWN) { return false; } @@ -225,7 +225,7 @@ bool IntervalsRepository::CreatePrecedenceLiteralIfNonTrivial( // Create a new literal. const BooleanVariable boolean_var = sat_solver_->NewBooleanVariable(); const Literal x_before_y = Literal(boolean_var, true); - relations_maps_->AddReifiedPrecedenceIfNonTrivial(x_before_y, x, y); + reified_precedences_->AddReifiedPrecedenceIfNonTrivial(x_before_y, x, y); AffineExpression y_plus_one = y; y_plus_one.constant += 1; @@ -236,7 +236,7 @@ bool IntervalsRepository::CreatePrecedenceLiteralIfNonTrivial( LiteralIndex IntervalsRepository::GetPrecedenceLiteral( AffineExpression x, AffineExpression y) const { - return relations_maps_->GetReifiedPrecedence(x, y); + return reified_precedences_->GetReifiedPrecedence(x, y); } Literal IntervalsRepository::GetOrCreatePrecedenceLiteral(AffineExpression x, @@ -247,7 +247,7 @@ Literal IntervalsRepository::GetOrCreatePrecedenceLiteral(AffineExpression x, } CHECK(CreatePrecedenceLiteralIfNonTrivial(x, y)); - const LiteralIndex index = relations_maps_->GetReifiedPrecedence(x, y); + const LiteralIndex index = reified_precedences_->GetReifiedPrecedence(x, y); CHECK_NE(index, kNoLiteralIndex); return Literal(index); } diff --git a/ortools/sat/intervals.h b/ortools/sat/intervals.h index 8b36fd47d24..fe4f0fde0bf 100644 --- a/ortools/sat/intervals.h +++ b/ortools/sat/intervals.h @@ -28,6 +28,7 @@ #include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" #include "ortools/sat/no_overlap_2d_helper.h" +#include "ortools/sat/precedences.h" #include "ortools/sat/sat_base.h" #include "ortools/sat/sat_solver.h" #include "ortools/sat/scheduling_helpers.h" @@ -189,7 +190,7 @@ class IntervalsRepository { SatSolver* sat_solver_; BinaryImplicationGraph* implications_; IntegerTrail* integer_trail_; - BinaryRelationsMaps* relations_maps_; + ReifiedLinear2Bounds* reified_precedences_; // Literal indicating if the tasks is executed. Tasks that are always executed // will have a kNoLiteralIndex entry in this vector. diff --git a/ortools/sat/linear_propagation.cc b/ortools/sat/linear_propagation.cc index c77ff227529..330483c928f 100644 --- a/ortools/sat/linear_propagation.cc +++ b/ortools/sat/linear_propagation.cc @@ -384,8 +384,8 @@ LinearPropagator::LinearPropagator(Model* model) rev_int_repository_(model->GetOrCreate()), rev_integer_value_repository_( model->GetOrCreate()), - precedences_(model->GetOrCreate()), - binary_relations_(model->GetOrCreate()), + precedences_(model->GetOrCreate()), + linear3_bounds_(model->GetOrCreate()), random_(model->GetOrCreate()), shared_stats_(model->GetOrCreate()), watcher_id_(watcher_->Register(this)), @@ -538,7 +538,8 @@ bool LinearPropagator::Propagate() { // - Z + Y >= 6 ==> Z >= 1 // - (1) again to push T <= 10 and reach the propagation fixed point. Bitset64::View in_queue = in_queue_.view(); - const bool push_affine_ub = push_affine_ub_for_binary_relations_; + const bool push_affine_ub = push_affine_ub_for_binary_relations_ || + trail_->CurrentDecisionLevel() == 0; while (true) { // We always process the whole queue in FIFO order. // Note that the order really only matter for infeasible constraint so it @@ -612,7 +613,7 @@ bool LinearPropagator::Propagate() { // The rev_rhs was updated to: initial_rhs - lb(vars[2]) * coeffs[2]. const IntegerValue initial_rhs = info.rev_rhs + coeffs[2] * integer_trail_->LowerBound(vars[2]); - binary_relations_->AddAffineUpperBound( + linear3_bounds_->AddAffineUpperBound( expr, AffineExpression(vars[2], -coeffs[2], initial_rhs)); } else if (info.rev_size == 3) { for (int i = 0; i < 3; ++i) { @@ -623,7 +624,7 @@ bool LinearPropagator::Propagate() { expr.vars[1] = vars[b]; expr.coeffs[0] = coeffs[a]; expr.coeffs[1] = coeffs[b]; - binary_relations_->AddAffineUpperBound( + linear3_bounds_->AddAffineUpperBound( expr, AffineExpression(vars[i], -coeffs[i], info.rev_rhs)); } } diff --git a/ortools/sat/linear_propagation.h b/ortools/sat/linear_propagation.h index b98f46711ee..ab4027b665e 100644 --- a/ortools/sat/linear_propagation.h +++ b/ortools/sat/linear_propagation.h @@ -421,8 +421,8 @@ class LinearPropagator : public PropagatorInterface, TimeLimit* time_limit_; RevIntRepository* rev_int_repository_; RevIntegerValueRepository* rev_integer_value_repository_; - PrecedenceRelations* precedences_; - BinaryRelationsMaps* binary_relations_; + EnforcedLinear2Bounds* precedences_; + Linear2BoundsFromLinear3* linear3_bounds_; ModelRandomGenerator* random_; SharedStatistics* shared_stats_ = nullptr; const int watcher_id_; diff --git a/ortools/sat/linear_relaxation.cc b/ortools/sat/linear_relaxation.cc index 2a96fdb1f77..0bd15c832a8 100644 --- a/ortools/sat/linear_relaxation.cc +++ b/ortools/sat/linear_relaxation.cc @@ -699,8 +699,8 @@ std::optional DetectMakespanFromPrecedences( } std::vector output; - auto* precedences = model->GetOrCreate(); - precedences->ComputeFullPrecedences(end_vars, &output); + auto* evaluator = model->GetOrCreate(); + evaluator->ComputeFullPrecedences(end_vars, &output); for (const auto& p : output) { // TODO(user): What if we have more than one candidate makespan ? if (p.indices.size() != ends.size()) continue; diff --git a/ortools/sat/no_overlap_2d_helper.cc b/ortools/sat/no_overlap_2d_helper.cc index 9fee042fffb..94484b160ee 100644 --- a/ortools/sat/no_overlap_2d_helper.cc +++ b/ortools/sat/no_overlap_2d_helper.cc @@ -97,8 +97,8 @@ void ClearAndAddMandatoryOverlapReason(int box1, int box2, y->ClearReason(); y->AddPresenceReason(box1); y->AddPresenceReason(box2); - y->AddReasonForBeingBefore(box1, box2); - y->AddReasonForBeingBefore(box2, box1); + y->AddReasonForBeingBeforeAssumingNoOverlap(box1, box2); + y->AddReasonForBeingBeforeAssumingNoOverlap(box2, box1); } } // namespace @@ -162,7 +162,7 @@ bool LeftBoxBeforeRightBoxOnFirstDimension(int left, int right, x->ClearReason(); x->AddPresenceReason(left); x->AddPresenceReason(right); - x->AddReasonForBeingBefore(left, right); + x->AddReasonForBeingBeforeAssumingNoOverlap(left, right); x->AddEndMinReason(left, left_end_min); // left and right must overlap on y. ClearAndAddMandatoryOverlapReason(left, right, y); @@ -177,7 +177,7 @@ bool LeftBoxBeforeRightBoxOnFirstDimension(int left, int right, x->ClearReason(); x->AddPresenceReason(left); x->AddPresenceReason(right); - x->AddReasonForBeingBefore(left, right); + x->AddReasonForBeingBeforeAssumingNoOverlap(left, right); x->AddStartMaxReason(right, right_start_max); // left and right must overlap on y. ClearAndAddMandatoryOverlapReason(left, right, y); diff --git a/ortools/sat/precedences.cc b/ortools/sat/precedences.cc index 562795dd3cc..5618fb304a9 100644 --- a/ortools/sat/precedences.cc +++ b/ortools/sat/precedences.cc @@ -17,7 +17,9 @@ #include #include +#include #include +#include #include #include @@ -53,60 +55,270 @@ namespace operations_research { namespace sat { -bool PrecedenceRelations::AddBounds(LinearExpression2 expr, IntegerValue lb, - IntegerValue ub) { - expr.CanonicalizeAndUpdateBounds(lb, ub); +LinearExpression2Index Linear2WithPotentialNonTrivalBounds::AddOrGet( + LinearExpression2 original_expr) { + LinearExpression2 expr = original_expr; + DCHECK(expr.IsCanonicalized()); + DCHECK_EQ(expr.DivideByGcd(), 1); + DCHECK_NE(expr.coeffs[0], 0); + DCHECK_NE(expr.coeffs[1], 0); + const bool negated = expr.NegateForCanonicalization(); + auto [it, inserted] = expr_to_index_.insert({expr, exprs_.size()}); + if (inserted) { + CHECK_LT(2 * exprs_.size() + 1, + std::numeric_limits::max()); + exprs_.push_back(expr); + } + const LinearExpression2Index result = + negated ? NegationOf(LinearExpression2Index(2 * it->second)) + : LinearExpression2Index(2 * it->second); + + if (!inserted) return result; + + // Update our special coeff=1 lookup table. + if (expr.coeffs[0] == 1 && expr.coeffs[1] == 1) { + // +2 to handle possible negation. + const int new_size = + std::max(expr.vars[0].value(), expr.vars[1].value()) + 2; + if (new_size > coeff_one_var_lookup_.size()) { + coeff_one_var_lookup_.resize(new_size); + } + LinearExpression2 neg_expr = original_expr; + neg_expr.Negate(); + coeff_one_var_lookup_[original_expr.vars[0]].push_back(result); + coeff_one_var_lookup_[original_expr.vars[1]].push_back(result); + coeff_one_var_lookup_[neg_expr.vars[1]].push_back(NegationOf(result)); + coeff_one_var_lookup_[neg_expr.vars[0]].push_back(NegationOf(result)); + } + + // Update our per-variable and per-pair lookup tables. + IntegerVariable var1 = PositiveVariable(expr.vars[0]); + IntegerVariable var2 = PositiveVariable(expr.vars[1]); + if (var1 > var2) std::swap(var1, var2); + var_pair_to_bounds_[{var1, var2}].push_back(result); + var_to_bounds_[var1].push_back(result); + var_to_bounds_[var2].push_back(result); - if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) { - // This class handles only binary relationships, let something else handle - // the case where there is actually a single variable. - return false; + return result; +} + +void Linear2Watcher::NotifyBoundChanged(LinearExpression2 expr) { + DCHECK(expr.IsCanonicalized()); + DCHECK_EQ(expr.DivideByGcd(), 1); + ++timestamp_; + for (const int id : propagator_ids_) { + watcher_->CallOnNextPropagate(id); + } + for (IntegerVariable var : expr.non_zero_vars()) { + var = PositiveVariable(var); // TODO(user): Be more precise? + if (var >= var_timestamp_.size()) { + var_timestamp_.resize(var + 1, 0); + } + var_timestamp_[var]++; } +} - // Add to root_relations_. - // - // TODO(user): AddInternal() only returns true if this is the first relation - // between head and tail. But we can still avoid an extra lookup. - const bool add_ub = ub < LevelZeroUpperBound(expr); - LinearExpression2 expr_for_lb = expr; - expr_for_lb.Negate(); - const bool add_lb = lb > -LevelZeroUpperBound(expr_for_lb); - if (!add_ub && !add_lb) { +int64_t Linear2Watcher::VarTimestamp(IntegerVariable var) { + var = PositiveVariable(var); + return var < var_timestamp_.size() ? var_timestamp_[var] : 0; +} + +bool RootLevelLinear2Bounds::AddUpperBound(LinearExpression2Index index, + IntegerValue ub) { + const LinearExpression2 expr = non_trivial_bounds_->GetExpression(index); + const IntegerValue zero_level_ub = integer_trail_->LevelZeroUpperBound(expr); + if (ub >= zero_level_ub) { return false; } - - if (add_ub) { - AddInternal(expr, ub); + if (best_upper_bounds_.size() <= index) { + best_upper_bounds_.resize(index.value() + 1, kMaxIntegerValue); } - if (add_lb) { - AddInternal(expr_for_lb, -lb); + if (ub >= best_upper_bounds_[index]) { + return false; } + best_upper_bounds_[index] = ub; - // If we are not built, make sure there is enough room in the graph. - // TODO(user): Alternatively, force caller to do a Resize(). - const int max_node = - std::max(PositiveVariable(expr.vars[0]), PositiveVariable(expr.vars[1])) - .value() + - 1; - if (!is_built_ && max_node >= graph_.num_nodes()) { - graph_.AddNode(max_node); + ++num_updates_; + linear2_watcher_->NotifyBoundChanged(expr); + + // Share. + // + // TODO(user): It seems we could change the canonicalization to only use + // positive variable? that would simplify a bit the code here and not make it + // worse elsewhere? + if (shared_linear2_bounds_ != nullptr) { + const IntegerValue lb = -LevelZeroUpperBound(NegationOf(index)); + const int proto_var0 = + cp_model_mapping_->GetProtoVariableFromIntegerVariable( + PositiveVariable(expr.vars[0])); + const int proto_var1 = + cp_model_mapping_->GetProtoVariableFromIntegerVariable( + PositiveVariable(expr.vars[1])); + if (proto_var0 >= 0 && proto_var1 >= 0) { + // This is also a relation between cp_model proto variable. Share it! + // Note that since expr is canonicalized, this one should too. + SharedLinear2Bounds::Key key; + key.vars[0] = proto_var0; + key.coeffs[0] = + VariableIsPositive(expr.vars[0]) ? expr.coeffs[0] : -expr.coeffs[0]; + key.vars[1] = proto_var1; + key.coeffs[1] = + VariableIsPositive(expr.vars[1]) ? expr.coeffs[1] : -expr.coeffs[1]; + shared_linear2_bounds_->Add(shared_linear2_bounds_id_, key, lb, ub); + } } return true; } -bool PrecedenceRelations::AddUpperBound(LinearExpression2 expr, - IntegerValue ub) { - return AddBounds(expr, kMinIntegerValue, ub); +RootLevelLinear2Bounds::~RootLevelLinear2Bounds() { + if (!VLOG_IS_ON(1)) return; + std::vector> stats; + stats.push_back({"RootLevelLinear2Bounds/num_updates", num_updates_}); + shared_stats_->AddStats(stats); } -void PrecedenceRelations::PushConditionalRelation( - absl::Span enforcements, LinearExpression2 expr, - IntegerValue rhs) { +RelationStatus RootLevelLinear2Bounds::GetLevelZeroStatus( + LinearExpression2 expr, IntegerValue lb, IntegerValue ub) const { expr.SimpleCanonicalization(); if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) { - return; + return RelationStatus::IS_UNKNOWN; + } + const IntegerValue known_ub = LevelZeroUpperBound(expr); + expr.Negate(); + const IntegerValue known_lb = -LevelZeroUpperBound(expr); + if (lb <= known_lb && ub >= known_ub) return RelationStatus::IS_TRUE; + if (lb > known_ub || ub < known_lb) return RelationStatus::IS_FALSE; + + return RelationStatus::IS_UNKNOWN; +} + +IntegerValue RootLevelLinear2Bounds::GetUpperBoundNoTrail( + LinearExpression2Index index) const { + if (best_upper_bounds_.size() <= index) { + return kMaxIntegerValue; + } + return best_upper_bounds_[index]; +} + +std::vector> +RootLevelLinear2Bounds::GetSortedNonTrivialUpperBounds() const { + std::vector> result; + for (LinearExpression2Index index = LinearExpression2Index{0}; + index < best_upper_bounds_.size(); ++index) { + const IntegerValue ub = best_upper_bounds_[index]; + if (ub == kMaxIntegerValue) continue; + const LinearExpression2 expr = non_trivial_bounds_->GetExpression(index); + if (ub < integer_trail_->LevelZeroUpperBound(expr)) { + result.push_back({expr, ub}); + } + } + std::sort(result.begin(), result.end()); + return result; +} + +std::vector> +RootLevelLinear2Bounds::GetAllBoundsContainingVariable( + IntegerVariable var) const { + std::vector> result; + for (const LinearExpression2Index index : + non_trivial_bounds_->GetAllLinear2ContainingVariable(var)) { + const IntegerValue lb = -GetUpperBoundNoTrail(NegationOf(index)); + const IntegerValue ub = GetUpperBoundNoTrail(index); + const LinearExpression2 expr = non_trivial_bounds_->GetExpression(index); + const IntegerValue trail_lb = integer_trail_->LevelZeroLowerBound(expr); + const IntegerValue trail_ub = integer_trail_->LevelZeroUpperBound(expr); + if (lb <= trail_lb && ub >= trail_ub) continue; + LinearExpression2 explicit_vars_expr = expr; + if (explicit_vars_expr.vars[0] == NegationOf(var)) { + explicit_vars_expr.vars[0] = NegationOf(explicit_vars_expr.vars[0]); + explicit_vars_expr.coeffs[0] = -explicit_vars_expr.coeffs[0]; + } + if (explicit_vars_expr.vars[1] == NegationOf(var)) { + explicit_vars_expr.vars[1] = NegationOf(explicit_vars_expr.vars[1]); + explicit_vars_expr.coeffs[1] = -explicit_vars_expr.coeffs[1]; + } + if (explicit_vars_expr.vars[1] == var) { + std::swap(explicit_vars_expr.vars[0], explicit_vars_expr.vars[1]); + std::swap(explicit_vars_expr.coeffs[0], explicit_vars_expr.coeffs[1]); + } + DCHECK(explicit_vars_expr.vars[0] == var); + result.push_back( + {explicit_vars_expr, std::max(lb, trail_lb), std::min(ub, trail_ub)}); + } + return result; +} + +// Return a list of (lb <= expr <= ub), with expr.vars = {var1, var2}, where +// at least one of the bounds is non-trivial and the potential other +// non-trivial bound is tight. +std::vector> +RootLevelLinear2Bounds::GetAllBoundsContainingVariables( + IntegerVariable var1, IntegerVariable var2) const { + std::vector> result; + for (const LinearExpression2Index index : + non_trivial_bounds_->GetAllLinear2ContainingVariables(var1, var2)) { + const IntegerValue lb = -GetUpperBoundNoTrail(NegationOf(index)); + const IntegerValue ub = GetUpperBoundNoTrail(index); + const LinearExpression2 expr = non_trivial_bounds_->GetExpression(index); + const IntegerValue trail_lb = integer_trail_->LevelZeroLowerBound(expr); + const IntegerValue trail_ub = integer_trail_->LevelZeroUpperBound(expr); + if (lb <= trail_lb && ub >= trail_ub) continue; + + LinearExpression2 explicit_vars_expr = expr; + if (explicit_vars_expr.vars[0] == NegationOf(var1) || + explicit_vars_expr.vars[0] == NegationOf(var2)) { + explicit_vars_expr.vars[0] = NegationOf(explicit_vars_expr.vars[0]); + explicit_vars_expr.coeffs[0] = -explicit_vars_expr.coeffs[0]; + } + if (explicit_vars_expr.vars[1] == NegationOf(var1) || + explicit_vars_expr.vars[1] == NegationOf(var2)) { + explicit_vars_expr.vars[1] = NegationOf(explicit_vars_expr.vars[1]); + explicit_vars_expr.coeffs[1] = -explicit_vars_expr.coeffs[1]; + } + if (explicit_vars_expr.vars[1] == var1) { + std::swap(explicit_vars_expr.vars[0], explicit_vars_expr.vars[1]); + std::swap(explicit_vars_expr.coeffs[0], explicit_vars_expr.coeffs[1]); + } + DCHECK(explicit_vars_expr.vars[0] == var1 && + explicit_vars_expr.vars[1] == var2); + result.push_back( + {explicit_vars_expr, std::max(lb, trail_lb), std::min(ub, trail_ub)}); } + return result; +} + +std::vector +RootLevelLinear2Bounds::GetVariablesInSimpleRelation( + IntegerVariable var) const { + std::vector result; + for (const LinearExpression2Index index : + non_trivial_bounds_->GetAllLinear2ContainingVariableWithCoeffOne(var)) { + const LinearExpression2 expr = non_trivial_bounds_->GetExpression(index); + const IntegerVariable other = + (expr.vars[0] == var ? expr.vars[1] : expr.vars[0]); + DCHECK_EQ(expr.coeffs[0], 1); + DCHECK_EQ(expr.coeffs[1], 1); + DCHECK((expr.vars[0] == var && expr.vars[1] == other) || + (expr.vars[0] == other && expr.vars[1] == var)); + if (GetUpperBoundNoTrail(index) < + integer_trail_->LevelZeroUpperBound(expr)) { + result.push_back(other); + } + } + return result; +} + +EnforcedLinear2Bounds::~EnforcedLinear2Bounds() { + if (!VLOG_IS_ON(1)) return; + std::vector> stats; + stats.push_back({"EnforcedLinear2Bounds/num_conditional_relation_updates", + num_conditional_relation_updates_}); + shared_stats_->AddStats(stats); +} +void EnforcedLinear2Bounds::PushConditionalRelation( + absl::Span enforcements, LinearExpression2Index index, + IntegerValue rhs) { // This must be currently true. if (DEBUG_MODE) { for (const Literal l : enforcements) { @@ -115,51 +327,47 @@ void PrecedenceRelations::PushConditionalRelation( } if (enforcements.empty() || trail_->CurrentDecisionLevel() == 0) { - AddUpperBound(expr, rhs); + root_level_bounds_->AddUpperBound(index, rhs); return; } - const IntegerValue gcd = expr.DivideByGcd(); - rhs = FloorRatio(rhs, gcd); + if (rhs >= root_level_bounds_->LevelZeroUpperBound(index)) return; + const LinearExpression2 expr = non_trivial_bounds_->GetExpression(index); - // Ignore if no better than best_relations, otherwise increase it. - { - const auto [it, inserted] = best_relations_.insert({expr, rhs}); - if (!inserted) { - if (rhs >= it->second) return; // Ignore. - it->second = rhs; - } - } + linear2_watcher_->NotifyBoundChanged(expr); + ++num_conditional_relation_updates_; const int new_index = conditional_stack_.size(); - const auto [it, inserted] = conditional_relations_.insert({expr, new_index}); - if (inserted) { + if (conditional_relations_.size() <= index) { + conditional_relations_.resize(index.value() + 1, -1); + } + if (conditional_relations_[index] == -1) { + conditional_relations_[index] = new_index; CreateLevelEntryIfNeeded(); - conditional_stack_.emplace_back(/*prev_entry=*/-1, rhs, expr, enforcements); + conditional_stack_.emplace_back(/*prev_entry=*/-1, rhs, index, + enforcements); if (expr.coeffs[0] == 1 && expr.coeffs[1] == 1) { const int new_size = std::max(expr.vars[0].value(), expr.vars[1].value()) + 1; - if (new_size > conditional_after_.size()) { - conditional_after_.resize(new_size); + if (new_size > conditional_var_lookup_.size()) { + conditional_var_lookup_.resize(new_size); } - conditional_after_[expr.vars[0]].push_back(NegationOf(expr.vars[1])); - conditional_after_[expr.vars[1]].push_back(NegationOf(expr.vars[0])); + conditional_var_lookup_[expr.vars[0]].push_back(expr.vars[1]); + conditional_var_lookup_[expr.vars[1]].push_back(expr.vars[0]); } } else { - // We should only decrease because we ignored entry worse than the one in - // best_relations_. - const int prev_entry = it->second; - DCHECK_LT(rhs, conditional_stack_[prev_entry].rhs); + const int prev_entry = conditional_relations_[index]; + if (rhs >= conditional_stack_[prev_entry].rhs) return; // Update. - it->second = new_index; + conditional_relations_[index] = new_index; CreateLevelEntryIfNeeded(); - conditional_stack_.emplace_back(prev_entry, rhs, expr, enforcements); + conditional_stack_.emplace_back(prev_entry, rhs, index, enforcements); } } -void PrecedenceRelations::CreateLevelEntryIfNeeded() { +void EnforcedLinear2Bounds::CreateLevelEntryIfNeeded() { const int current = trail_->CurrentDecisionLevel(); if (!level_to_stack_size_.empty() && level_to_stack_size_.back().first == current) @@ -168,7 +376,7 @@ void PrecedenceRelations::CreateLevelEntryIfNeeded() { } // We only pop what is needed. -void PrecedenceRelations::SetLevel(int level) { +void EnforcedLinear2Bounds::SetLevel(int level) { while (!level_to_stack_size_.empty() && level_to_stack_size_.back().first > level) { const int target = level_to_stack_size_.back().second; @@ -177,18 +385,16 @@ void PrecedenceRelations::SetLevel(int level) { const ConditionalEntry& back = conditional_stack_.back(); if (back.prev_entry != -1) { conditional_relations_[back.key] = back.prev_entry; - UpdateBestRelation(back.key, conditional_stack_[back.prev_entry].rhs); } else { - UpdateBestRelation(back.key, kMaxIntegerValue); - conditional_relations_.erase(back.key); - - if (back.key.coeffs[0] == 1 && back.key.coeffs[1] == 1) { - DCHECK_EQ(conditional_after_[back.key.vars[0]].back(), - NegationOf(back.key.vars[1])); - DCHECK_EQ(conditional_after_[back.key.vars[1]].back(), - NegationOf(back.key.vars[0])); - conditional_after_[back.key.vars[0]].pop_back(); - conditional_after_[back.key.vars[1]].pop_back(); + conditional_relations_[back.key] = -1; + const LinearExpression2 expr = + non_trivial_bounds_->GetExpression(back.key); + + if (expr.coeffs[0] == 1 && expr.coeffs[1] == 1) { + DCHECK_EQ(conditional_var_lookup_[expr.vars[0]].back(), expr.vars[1]); + DCHECK_EQ(conditional_var_lookup_[expr.vars[1]].back(), expr.vars[0]); + conditional_var_lookup_[expr.vars[0]].pop_back(); + conditional_var_lookup_[expr.vars[1]].pop_back(); } } conditional_stack_.pop_back(); @@ -197,56 +403,63 @@ void PrecedenceRelations::SetLevel(int level) { } } -IntegerValue PrecedenceRelations::LevelZeroUpperBound( - LinearExpression2 expr) const { - expr.SimpleCanonicalization(); - const IntegerValue gcd = expr.DivideByGcd(); - const auto it = root_relations_.find(expr); - if (it != root_relations_.end()) { - return CapProdI(it->second, gcd); - } - return kMaxIntegerValue; -} - -void PrecedenceRelations::AddReasonForUpperBoundLowerThan( - LinearExpression2 expr, IntegerValue ub, +void EnforcedLinear2Bounds::AddReasonForUpperBoundLowerThan( + LinearExpression2Index index, IntegerValue ub, std::vector* literal_reason, std::vector* /*unused*/) const { - expr.SimpleCanonicalization(); - if (ub >= LevelZeroUpperBound(expr)) return; - const IntegerValue gcd = expr.DivideByGcd(); - const auto it = conditional_relations_.find(expr); - DCHECK(it != conditional_relations_.end()); + if (ub >= root_level_bounds_->LevelZeroUpperBound(index)) return; + DCHECK_LT(index, conditional_relations_.size()); + const int entry_index = conditional_relations_[index]; + DCHECK_NE(entry_index, -1); - const ConditionalEntry& entry = conditional_stack_[it->second]; + const ConditionalEntry& entry = conditional_stack_[entry_index]; if (DEBUG_MODE) { for (const Literal l : entry.enforcements) { CHECK(trail_->Assignment().LiteralIsTrue(l)); } } - DCHECK_EQ(CapProdI(gcd, entry.rhs), UpperBound(expr)); - DCHECK_LE(CapProdI(gcd, entry.rhs), ub); + DCHECK_LE(entry.rhs, ub); for (const Literal l : entry.enforcements) { literal_reason->push_back(l.Negated()); } } -IntegerValue PrecedenceRelations::UpperBound(LinearExpression2 expr) const { - expr.SimpleCanonicalization(); - const IntegerValue gcd = expr.DivideByGcd(); - const auto it = best_relations_.find(expr); - if (it != best_relations_.end()) { - return CapProdI(gcd, it->second); +IntegerValue EnforcedLinear2Bounds::GetUpperBoundFromEnforced( + LinearExpression2Index index) const { + if (index >= conditional_relations_.size()) { + return kMaxIntegerValue; + } + const int entry_index = conditional_relations_[index]; + if (entry_index == -1) { + return kMaxIntegerValue; + } else { + const ConditionalEntry& entry = conditional_stack_[entry_index]; + if (DEBUG_MODE) { + for (const Literal l : entry.enforcements) { + CHECK(trail_->Assignment().LiteralIsTrue(l)); + } + } + DCHECK_LT(entry.rhs, root_level_bounds_->LevelZeroUpperBound(index)); + return entry.rhs; } - DCHECK(!root_relations_.contains(expr)); - DCHECK(!conditional_relations_.contains(expr)); - return kMaxIntegerValue; } -void PrecedenceRelations::Build() { +void TransitivePrecedencesEvaluator::Build() { if (is_built_) return; is_built_ = true; + const std::vector> + root_relations_sorted = + root_level_bounds_->GetSortedNonTrivialUpperBounds(); + int max_node = 0; + for (const auto [expr, _] : root_relations_sorted) { + max_node = std::max(max_node, PositiveVariable(expr.vars[0]).value()); + max_node = std::max(max_node, PositiveVariable(expr.vars[1]).value()); + } + max_node++; + if (max_node >= graph_.num_nodes()) { + graph_.AddNode(max_node); + } const int num_nodes = graph_.num_nodes(); util_intops::StrongVector> before(num_nodes); @@ -254,15 +467,12 @@ void PrecedenceRelations::Build() { // We will construct a graph with the current relation from all_relations_. // And use this to compute the "closure". CHECK(arc_offsets_.empty()); - graph_.ReserveArcs(2 * root_relations_.size()); - std::vector> root_relations_sorted( - root_relations_.begin(), root_relations_.end()); - std::sort(root_relations_sorted.begin(), root_relations_sorted.end()); + graph_.ReserveArcs(2 * root_relations_sorted.size()); for (const auto [var_pair, negated_offset] : root_relations_sorted) { // TODO(user): Support negative offset? // // Note that if we only have >= 0 ones, if we do have a cycle, we could - // make sure all variales are the same, and otherwise, we have a DAG or a + // make sure all variables are the same, and otherwise, we have a DAG or a // conflict. const IntegerValue offset = -negated_offset; if (offset < 0) continue; @@ -334,19 +544,18 @@ void PrecedenceRelations::Build() { const IntegerValue arc_offset = arc_offsets_[arc]; if (++work > kWorkLimit) break; - if (AddInternal(LinearExpression2::Difference(tail_var, head_var), - -arc_offset)) { + if (root_level_bounds_->AddUpperBound( + LinearExpression2::Difference(tail_var, head_var), -arc_offset)) { before[head_var].push_back(tail_var); } for (const IntegerVariable before_var : before[tail_var]) { if (++work > kWorkLimit) break; - LinearExpression2 expr_for_key(before_var, tail_var, 1, -1); - expr_for_key.SimpleCanonicalization(); + const LinearExpression2 expr_for_key(before_var, tail_var, 1, -1); const IntegerValue offset = - -root_relations_.at(expr_for_key) + arc_offset; - if (AddInternal(LinearExpression2::Difference(before_var, head_var), - -offset)) { + -root_level_bounds_->LevelZeroUpperBound(expr_for_key) + arc_offset; + if (root_level_bounds_->AddUpperBound( + LinearExpression2::Difference(before_var, head_var), -offset)) { before[head_var].push_back(before_var); } } @@ -354,10 +563,10 @@ void PrecedenceRelations::Build() { } VLOG(2) << "Full precedences. Work=" << work - << " Relations=" << root_relations_.size(); + << " Relations=" << root_relations_sorted.size(); } -void PrecedenceRelations::ComputeFullPrecedences( +void TransitivePrecedencesEvaluator::ComputeFullPrecedences( absl::Span vars, std::vector* output) { output->clear(); @@ -451,12 +660,10 @@ void PrecedenceRelations::ComputeFullPrecedences( } } -void PrecedenceRelations::CollectPrecedences( +void EnforcedLinear2Bounds::CollectPrecedences( absl::Span vars, std::vector* output) { - // +1 for the negation. - const int needed_size = - std::max(after_.size(), conditional_after_.size()) + 1; + const int needed_size = integer_trail_->NumIntegerVariables().value(); var_to_degree_.resize(needed_size); var_to_last_index_.resize(needed_size); var_with_positive_degree_.resize(needed_size); @@ -469,7 +676,8 @@ void PrecedenceRelations::CollectPrecedences( int* var_to_degree = var_to_degree_.data(); int* var_to_last_index = var_to_last_index_.data(); const auto process = [&](int index, absl::Span v) { - for (const IntegerVariable after : v) { + for (const IntegerVariable other : v) { + const IntegerVariable after = NegationOf(other); DCHECK_LT(after, needed_size); if (var_to_degree[after.value()] == 0) { var_with_positive_degree[num_relevants++] = after; @@ -486,11 +694,9 @@ void PrecedenceRelations::CollectPrecedences( for (int index = 0; index < vars.size(); ++index) { const IntegerVariable var = vars[index]; - if (var < after_.size()) { - process(index, after_[var]); - } - if (var < conditional_after_.size()) { - process(index, conditional_after_[var]); + process(index, root_level_bounds_->GetVariablesInSimpleRelation(var)); + if (var < conditional_var_lookup_.size()) { + process(index, conditional_var_lookup_[var]); } } @@ -498,8 +704,9 @@ void PrecedenceRelations::CollectPrecedences( // For that we transform var_to_degree to point to the first position of // each lbvar in the output vector. int start = 0; - for (int i = 0; i < num_relevants; ++i) { - const IntegerVariable var = var_with_positive_degree[i]; + const absl::Span relevant_variables = + absl::MakeSpan(var_with_positive_degree, num_relevants); + for (const IntegerVariable var : relevant_variables) { const int degree = var_to_degree[var.value()]; if (degree > 1) { var_to_degree[var.value()] = start; @@ -520,8 +727,7 @@ void PrecedenceRelations::CollectPrecedences( // Cleanup var_to_degree, note that we don't need to clean // var_to_last_index_. - for (int i = 0; i < num_relevants; ++i) { - const IntegerVariable var = var_with_positive_degree[i]; + for (const IntegerVariable var : relevant_variables) { var_to_degree[var.value()] = 0; } } @@ -1149,37 +1355,16 @@ bool PrecedencesPropagator::BellmanFordTarjan(Trail* trail) { return true; } -void BinaryRelationRepository::Add(Literal lit, LinearTerm a, LinearTerm b, +void BinaryRelationRepository::Add(Literal lit, LinearExpression2 expr, IntegerValue lhs, IntegerValue rhs) { - if (lit.Index() != kNoLiteralIndex) { - num_enforced_relations_++; - DCHECK(a.coeff == 0 || a.var != kNoIntegerVariable); - DCHECK(b.coeff == 0 || b.var != kNoIntegerVariable); - } else { - DCHECK_NE(a.coeff, 0); - DCHECK_NE(b.coeff, 0); - DCHECK_NE(a.var, kNoIntegerVariable); - DCHECK_NE(b.var, kNoIntegerVariable); - } - - Relation r; - r.enforcement = lit; - r.a = a; - r.b = b; - r.lhs = lhs; - r.rhs = rhs; - - // We shall only consider positive variable here. - if (r.a.var != kNoIntegerVariable && !VariableIsPositive(r.a.var)) { - r.a.var = NegationOf(r.a.var); - r.a.coeff = -r.a.coeff; - } - if (r.b.var != kNoIntegerVariable && !VariableIsPositive(r.b.var)) { - r.b.var = NegationOf(r.b.var); - r.b.coeff = -r.b.coeff; - } - - relations_.push_back(std::move(r)); + expr.MakeVariablesPositive(); + CHECK_NE(lit.Index(), kNoLiteralIndex); + num_enforced_relations_++; + DCHECK(expr.coeffs[0] == 0 || expr.vars[0] != kNoIntegerVariable); + DCHECK(expr.coeffs[1] == 0 || expr.vars[1] != kNoIntegerVariable); + + relations_.push_back( + {.enforcement = lit, .expr = expr, .lhs = lhs, .rhs = rhs}); } void BinaryRelationRepository::AddPartialRelation(Literal lit, @@ -1188,37 +1373,25 @@ void BinaryRelationRepository::AddPartialRelation(Literal lit, DCHECK_NE(a, kNoIntegerVariable); DCHECK_NE(b, kNoIntegerVariable); DCHECK_NE(a, b); - Add(lit, LinearTerm(a, 1), LinearTerm(b, 1), 0, 0); + Add(lit, LinearExpression2(a, b, 1, 1), 0, 0); } void BinaryRelationRepository::Build() { DCHECK(!is_built_); is_built_ = true; std::vector> literal_key_values; - std::vector> var_key_values; const int num_relations = relations_.size(); literal_key_values.reserve(num_enforced_relations_); - var_key_values.reserve(num_relations - num_enforced_relations_); for (int i = 0; i < num_relations; ++i) { const Relation& r = relations_[i]; - if (r.enforcement.Index() == kNoLiteralIndex) { - var_key_values.emplace_back(r.a.var, i); - var_key_values.emplace_back(r.b.var, i); - std::pair key(r.a.var, r.b.var); - if (relations_[i].a.var > relations_[i].b.var) { - std::swap(key.first, key.second); - } - var_pair_to_relations_[key].push_back(i); - } else { - literal_key_values.emplace_back(r.enforcement.Index(), i); - } + literal_key_values.emplace_back(r.enforcement.Index(), i); } lit_to_relations_.ResetFromPairs(literal_key_values); - var_to_relations_.ResetFromPairs(var_key_values); } bool BinaryRelationRepository::PropagateLocalBounds( - const IntegerTrail& integer_trail, Literal lit, + const IntegerTrail& integer_trail, + const RootLevelLinear2Bounds& root_level_bounds, Literal lit, const absl::flat_hash_map& input, absl::flat_hash_map* output) const { DCHECK_NE(lit.Index(), kNoLiteralIndex); @@ -1241,24 +1414,28 @@ bool BinaryRelationRepository::PropagateLocalBounds( auto update_upper_bound_by_var = [&](IntegerVariable var, IntegerValue ub) { update_lower_bound_by_var(NegationOf(var), -ub); }; - auto update_var_bounds = [&](const LinearTerm& a, const LinearTerm& b, - IntegerValue lhs, IntegerValue rhs) { - if (a.coeff == 0) return; + auto update_var_bounds = [&](const LinearExpression2& expr, IntegerValue lhs, + IntegerValue rhs) { + if (expr.coeffs[0] == 0) return; // lb(b.y) <= b.y <= ub(b.y) and lhs <= a.x + b.y <= rhs imply // ceil((lhs - ub(b.y)) / a) <= x <= floor((rhs - lb(b.y)) / a) - if (b.coeff != 0) { - lhs = lhs - b.coeff * get_upper_bound(b.var); - rhs = rhs - b.coeff * get_lower_bound(b.var); + if (expr.coeffs[1] != 0) { + lhs = lhs - expr.coeffs[1] * get_upper_bound(expr.vars[1]); + rhs = rhs - expr.coeffs[1] * get_lower_bound(expr.vars[1]); } - update_lower_bound_by_var(a.var, MathUtil::CeilOfRatio(lhs, a.coeff)); - update_upper_bound_by_var(a.var, MathUtil::FloorOfRatio(rhs, a.coeff)); + update_lower_bound_by_var(expr.vars[0], + MathUtil::CeilOfRatio(lhs, expr.coeffs[0])); + update_upper_bound_by_var(expr.vars[0], + MathUtil::FloorOfRatio(rhs, expr.coeffs[0])); }; auto update_var_bounds_from_relation = [&](Relation r) { - r.a.MakeCoeffPositive(); - r.b.MakeCoeffPositive(); - update_var_bounds(r.a, r.b, r.lhs, r.rhs); - update_var_bounds(r.b, r.a, r.lhs, r.rhs); + r.expr.SimpleCanonicalization(); + + update_var_bounds(r.expr, r.lhs, r.rhs); + std::swap(r.expr.vars[0], r.expr.vars[1]); + std::swap(r.expr.coeffs[0], r.expr.coeffs[1]); + update_var_bounds(r.expr, r.lhs, r.rhs); }; if (lit.Index() < lit_to_relations_.size()) { for (const int relation_index : lit_to_relations_[lit]) { @@ -1266,9 +1443,10 @@ bool BinaryRelationRepository::PropagateLocalBounds( } } for (const auto& [var, _] : input) { - if (var >= var_to_relations_.size()) continue; - for (const int relation_index : var_to_relations_[var]) { - update_var_bounds_from_relation(relations_[relation_index]); + for (const auto& [expr, lb, ub] : + root_level_bounds.GetAllBoundsContainingVariable(var)) { + update_var_bounds_from_relation( + Relation{Literal(kNoLiteralIndex), expr, lb, ub}); } } @@ -1291,17 +1469,22 @@ bool GreaterThanAtLeastOneOfDetector::AddRelationFromIndices( const IntegerValue var_lb = integer_trail->LevelZeroLowerBound(var); for (const int index : indices) { Relation r = repository_.relation(index); - if (r.a.var != PositiveVariable(var)) std::swap(r.a, r.b); - CHECK_EQ(r.a.var, PositiveVariable(var)); + if (r.expr.vars[0] != PositiveVariable(var)) { + std::swap(r.expr.vars[0], r.expr.vars[1]); + std::swap(r.expr.coeffs[0], r.expr.coeffs[1]); + } + CHECK_EQ(r.expr.vars[0], PositiveVariable(var)); - if ((r.a.coeff == 1) == VariableIsPositive(var)) { + if ((r.expr.coeffs[0] == 1) == VariableIsPositive(var)) { // a + b >= lhs if (r.lhs <= kMinIntegerValue) continue; - exprs.push_back(AffineExpression(r.b.var, -r.b.coeff, r.lhs)); + exprs.push_back( + AffineExpression(r.expr.vars[1], -r.expr.coeffs[1], r.lhs)); } else { // -a + b <= rhs. if (r.rhs >= kMaxIntegerValue) continue; - exprs.push_back(AffineExpression(r.b.var, r.b.coeff, -r.rhs)); + exprs.push_back( + AffineExpression(r.expr.vars[1], r.expr.coeffs[1], -r.rhs)); } // Ignore this entry if it is always true. @@ -1349,11 +1532,13 @@ int GreaterThanAtLeastOneOfDetector:: for (const int index : repository_.IndicesOfRelationsEnforcedBy(l.Index())) { const Relation& r = repository_.relation(index); - if (r.a.var != kNoIntegerVariable && IntTypeAbs(r.a.coeff) == 1) { - infos.push_back({r.a.var, index}); + if (r.expr.vars[0] != kNoIntegerVariable && + IntTypeAbs(r.expr.coeffs[0]) == 1) { + infos.push_back({r.expr.vars[0], index}); } - if (r.b.var != kNoIntegerVariable && IntTypeAbs(r.b.coeff) == 1) { - infos.push_back({r.b.var, index}); + if (r.expr.vars[1] != kNoIntegerVariable && + IntTypeAbs(r.expr.coeffs[1]) == 1) { + infos.push_back({r.expr.vars[1], index}); } } } @@ -1403,17 +1588,19 @@ int GreaterThanAtLeastOneOfDetector:: for (int index = 0; index < repository_.size(); ++index) { const Relation& r = repository_.relation(index); if (r.enforcement.Index() == kNoLiteralIndex) continue; - if (r.a.var != kNoIntegerVariable && IntTypeAbs(r.a.coeff) == 1) { - if (r.a.var >= var_to_relations.size()) { - var_to_relations.resize(r.a.var + 1); + if (r.expr.vars[0] != kNoIntegerVariable && + IntTypeAbs(r.expr.coeffs[0]) == 1) { + if (r.expr.vars[0] >= var_to_relations.size()) { + var_to_relations.resize(r.expr.vars[0] + 1); } - var_to_relations[r.a.var].push_back(index); + var_to_relations[r.expr.vars[0]].push_back(index); } - if (r.b.var != kNoIntegerVariable && IntTypeAbs(r.b.coeff) == 1) { - if (r.b.var >= var_to_relations.size()) { - var_to_relations.resize(r.b.var + 1); + if (r.expr.vars[1] != kNoIntegerVariable && + IntTypeAbs(r.expr.coeffs[1]) == 1) { + if (r.expr.vars[1] >= var_to_relations.size()) { + var_to_relations.resize(r.expr.vars[1] + 1); } - var_to_relations[r.b.var].push_back(index); + var_to_relations[r.expr.vars[1]].push_back(index); } } @@ -1531,11 +1718,9 @@ int GreaterThanAtLeastOneOfDetector::AddGreaterThanAtLeastOneOfConstraints( return num_added_constraints; } -BinaryRelationsMaps::BinaryRelationsMaps(Model* model) - : integer_trail_(model->GetOrCreate()), - integer_encoder_(model->GetOrCreate()), - watcher_(model->GetOrCreate()), - shared_stats_(model->GetOrCreate()) { +ReifiedLinear2Bounds::ReifiedLinear2Bounds(Model* model) + : integer_encoder_(model->GetOrCreate()), + best_root_level_bounds_(model->GetOrCreate()) { int index = 0; model->GetOrCreate()->callbacks.push_back( [index = index, trail = model->GetOrCreate(), this]() mutable { @@ -1552,11 +1737,11 @@ BinaryRelationsMaps::BinaryRelationsMaps(Model* model) // Linear scan. for (const auto [l, expr, ub] : all_reified_relations_) { if (relevant_true_literals.contains(l)) { - AddRelationBounds(expr, kMinIntegerValue, ub); + best_root_level_bounds_->Add(expr, kMinIntegerValue, ub); VLOG(2) << "New fixed precedence: " << expr << " <= " << ub << " (was reified by " << l << ")"; } else if (relevant_true_literals.contains(l.Negated())) { - AddRelationBounds(expr, ub + 1, kMaxIntegerValue); + best_root_level_bounds_->Add(expr, ub + 1, kMaxIntegerValue); VLOG(2) << "New fixed precedence: " << expr << " > " << ub << " (was reified by not(" << l << "))"; } @@ -1565,112 +1750,26 @@ BinaryRelationsMaps::BinaryRelationsMaps(Model* model) }); } -BinaryRelationsMaps::~BinaryRelationsMaps() { +Linear2BoundsFromLinear3::~Linear2BoundsFromLinear3() { if (!VLOG_IS_ON(1)) return; std::vector> stats; - stats.push_back({"BinaryRelationsMaps/num_relations", num_updates_}); stats.push_back( - {"BinaryRelationsMaps/num_affine_updates", num_affine_updates_}); + {"Linear2BoundsFromLinear3/num_affine_updates", num_affine_updates_}); shared_stats_->AddStats(stats); } -IntegerValue BinaryRelationsMaps::GetImpliedUpperBound( - const LinearExpression2& expr) const { - DCHECK_GE(expr.coeffs[0], 0); - DCHECK_GE(expr.coeffs[1], 0); - IntegerValue implied_ub = 0; - for (const int i : {0, 1}) { - if (expr.coeffs[i] > 0) { - implied_ub += expr.coeffs[i] * integer_trail_->UpperBound(expr.vars[i]); - } - } - return implied_ub; -} - -std::pair -BinaryRelationsMaps::GetImpliedLevelZeroBounds( - const LinearExpression2& expr) const { - // Compute the implied bounds on the expression. - IntegerValue implied_lb = 0; - IntegerValue implied_ub = 0; - if (expr.coeffs[0] != 0) { - CHECK_GE(expr.vars[0], 0); - implied_lb += - expr.coeffs[0] * integer_trail_->LevelZeroLowerBound(expr.vars[0]); - implied_ub += - expr.coeffs[0] * integer_trail_->LevelZeroUpperBound(expr.vars[0]); - } - if (expr.coeffs[1] != 0) { - CHECK_GE(expr.vars[1], 0); - implied_lb += - expr.coeffs[1] * integer_trail_->LevelZeroLowerBound(expr.vars[1]); - implied_ub += - expr.coeffs[1] * integer_trail_->LevelZeroUpperBound(expr.vars[1]); - } - - return {implied_lb, implied_ub}; -} - -void BinaryRelationsMaps::AddRelationBounds(LinearExpression2 expr, - IntegerValue lb, IntegerValue ub) { - expr.CanonicalizeAndUpdateBounds(lb, ub); - const auto [implied_lb, implied_ub] = GetImpliedLevelZeroBounds(expr); - lb = std::max(lb, implied_lb); - ub = std::min(ub, implied_ub); - - if (lb > ub) return; // unsat ?? - if (lb == implied_lb && ub == implied_ub) return; // trivially true. - - if (best_root_level_bounds_.Add(expr, lb, ub)) { - // TODO(user): Also push them to a global shared repository after - // remapping IntegerVariable to proto indices. - ++num_updates_; - } -} - -RelationStatus BinaryRelationsMaps::GetLevelZeroStatus(LinearExpression2 expr, - IntegerValue lb, - IntegerValue ub) const { - expr.CanonicalizeAndUpdateBounds(lb, ub); - const auto [implied_lb, implied_ub] = GetImpliedLevelZeroBounds(expr); - lb = std::max(lb, implied_lb); - ub = std::min(ub, implied_ub); - - // Returns directly if the status can be derived from the implied bounds. - if (lb > ub) return RelationStatus::IS_FALSE; - if (lb == implied_lb && ub == implied_ub) return RelationStatus::IS_TRUE; - - // Relax as best_root_level_bounds_.GetStatus() might have older bounds. - if (lb == implied_lb) lb = kMinIntegerValue; - if (ub == implied_ub) ub = kMaxIntegerValue; - - return best_root_level_bounds_.GetStatus(expr, lb, ub); -} - -std::pair BinaryRelationsMaps::FromDifference( - const AffineExpression& a, const AffineExpression& b) const { - LinearExpression2 expr; - expr.vars[0] = a.var; - expr.vars[1] = b.var; - expr.coeffs[0] = a.coeff; - expr.coeffs[1] = -b.coeff; - IntegerValue lb = kMinIntegerValue; // unused. - IntegerValue ub = b.constant - a.constant; - expr.CanonicalizeAndUpdateBounds(lb, ub, /*allow_negation=*/false); - return {std::move(expr), ub}; -} - -RelationStatus BinaryRelationsMaps::GetLevelZeroPrecedenceStatus( +RelationStatus ReifiedLinear2Bounds::GetLevelZeroPrecedenceStatus( AffineExpression a, AffineExpression b) const { - const auto [expr, ub] = FromDifference(a, b); - return GetLevelZeroStatus(expr, kMinIntegerValue, ub); + const auto [expr, ub] = EncodeDifferenceLowerThan(a, b, 0); + return best_root_level_bounds_->GetLevelZeroStatus(expr, kMinIntegerValue, + ub); } -void BinaryRelationsMaps::AddReifiedPrecedenceIfNonTrivial(Literal l, - AffineExpression a, - AffineExpression b) { - const auto [expr, ub] = FromDifference(a, b); - const RelationStatus status = GetLevelZeroStatus(expr, kMinIntegerValue, ub); +void ReifiedLinear2Bounds::AddReifiedPrecedenceIfNonTrivial( + Literal l, AffineExpression a, AffineExpression b) { + const auto [expr, ub] = EncodeDifferenceLowerThan(a, b, 0); + const RelationStatus status = + best_root_level_bounds_->GetLevelZeroStatus(expr, kMinIntegerValue, ub); if (status != RelationStatus::IS_UNKNOWN) return; relation_to_lit_.insert({{expr, ub}, l}); @@ -1679,10 +1778,11 @@ void BinaryRelationsMaps::AddReifiedPrecedenceIfNonTrivial(Literal l, all_reified_relations_.push_back({l, expr, ub}); } -LiteralIndex BinaryRelationsMaps::GetReifiedPrecedence(AffineExpression a, - AffineExpression b) { - const auto [expr, ub] = FromDifference(a, b); - const RelationStatus status = GetLevelZeroStatus(expr, kMinIntegerValue, ub); +LiteralIndex ReifiedLinear2Bounds::GetReifiedPrecedence(AffineExpression a, + AffineExpression b) { + const auto [expr, ub] = EncodeDifferenceLowerThan(a, b, 0); + const RelationStatus status = + best_root_level_bounds_->GetLevelZeroStatus(expr, kMinIntegerValue, ub); if (status == RelationStatus::IS_TRUE) { return integer_encoder_->GetTrueLiteral().Index(); } @@ -1695,115 +1795,171 @@ LiteralIndex BinaryRelationsMaps::GetReifiedPrecedence(AffineExpression a, return it->second; } -bool BinaryRelationsMaps::AddAffineUpperBound(LinearExpression2 expr, - AffineExpression affine_ub) { - const IntegerValue new_ub = integer_trail_->UpperBound(affine_ub); - expr.SimpleCanonicalization(); - - // Not better than trivial upper bound. - if (GetImpliedUpperBound(expr) <= new_ub) return false; +Linear2BoundsFromLinear3::Linear2BoundsFromLinear3(Model* model) + : integer_trail_(model->GetOrCreate()), + trail_(model->GetOrCreate()), + linear2_watcher_(model->GetOrCreate()), + watcher_(model->GetOrCreate()), + shared_stats_(model->GetOrCreate()), + root_level_bounds_(model->GetOrCreate()), + non_trivial_bounds_( + model->GetOrCreate()) {} - // Not better than the root level upper bound. - if (best_root_level_bounds_.GetUpperBound(expr) <= new_ub) return false; +// Note that for speed we do not compare to the trivial or root level bounds. +// +// It is okay to still store it in the hash-map, since at worst we will have no +// more entries than 3 * number_of_linear3_in_the_problem. +bool Linear2BoundsFromLinear3::AddAffineUpperBound(LinearExpression2 expr, + AffineExpression affine_ub) { + expr.SimpleCanonicalization(); + if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) return false; - const IntegerValue gcd = expr.DivideByGcd(); + // At level zero, just add it to root_level_bounds_. + if (trail_->CurrentDecisionLevel() == 0) { + root_level_bounds_->AddUpperBound( + expr, integer_trail_->LevelZeroUpperBound(affine_ub)); + return false; // Not important. + } - const auto it = best_affine_ub_.find(expr); + // We have gcd * canonical_expr <= affine_ub, + // so we do need to store a "divisor". + const IntegerValue divisor = expr.DivideByGcd(); + auto it = best_affine_ub_.find(expr); if (it != best_affine_ub_.end()) { - const auto [old_affine_ub, old_gcd] = it->second; // We have an affine bound for this expr in the map. Can be exactly the // same, a better one or a worse one. - if (old_affine_ub == affine_ub && old_gcd == gcd) { - // The affine bound is already in the map. - NotifyWatchingPropagators(); // The affine bound was updated. + // + // Note that we expect exactly the same most of the time as it should be + // rare to have many linear3 "competing" for the same linear2 bound. + const auto [old_affine_ub, old_divisor] = it->second; + if (old_affine_ub == affine_ub && old_divisor == divisor) { + linear2_watcher_->NotifyBoundChanged(expr); return false; } - const IntegerValue old_ub = - FloorRatio(integer_trail_->UpperBound(old_affine_ub), old_gcd); + + const IntegerValue new_ub = + FloorRatioWithTest(integer_trail_->UpperBound(affine_ub), divisor); + const IntegerValue old_ub = FloorRatioWithTest( + integer_trail_->UpperBound(old_affine_ub), old_divisor); if (old_ub <= new_ub) return false; // old bound is better. + + it->second = {affine_ub, divisor}; // Overwrite. + } else { + // Note that this should almost never happen (only once per lin2). + non_trivial_bounds_->AddOrGet(expr); + best_affine_ub_[expr] = {affine_ub, divisor}; } - // We have gcd * canonical_expr <= affine_ub, so we do need to store a - // "divisor". ++num_affine_updates_; - best_affine_ub_[expr] = {affine_ub, gcd}; - NotifyWatchingPropagators(); + linear2_watcher_->NotifyBoundChanged(expr); return true; } -void BinaryRelationsMaps::NotifyWatchingPropagators() const { - for (const int id : propagator_ids_) { - watcher_->CallOnNextPropagate(id); - } -} - -IntegerValue BinaryRelationsMaps::UpperBound(LinearExpression2 expr) const { - expr.SimpleCanonicalization(); - - const IntegerValue trivial_ub = GetImpliedUpperBound(expr); - const IntegerValue root_level_ub = - best_root_level_bounds_.GetUpperBound(expr); - const IntegerValue best_ub = std::min(root_level_ub, trivial_ub); - - const IntegerValue gcd = expr.DivideByGcd(); +IntegerValue Linear2BoundsFromLinear3::GetUpperBoundFromLinear3( + LinearExpression2 expr) const { + DCHECK_EQ(expr.DivideByGcd(), 1); + DCHECK(expr.IsCanonicalized()); const auto it = best_affine_ub_.find(expr); if (it == best_affine_ub_.end()) { - return best_ub; + return kMaxIntegerValue; } else { const auto [affine, divisor] = it->second; - const IntegerValue canonical_ub = - FloorRatio(integer_trail_->UpperBound(affine), divisor); - return std::min(best_ub, CapProdI(gcd, canonical_ub)); + return FloorRatio(integer_trail_->UpperBound(affine), divisor); } } -// TODO(user): If the trivial bound is better, its explanation is different... -void BinaryRelationsMaps::AddReasonForUpperBoundLowerThan( +void Linear2BoundsFromLinear3::AddReasonForUpperBoundLowerThan( LinearExpression2 expr, IntegerValue ub, std::vector* /*literal_reason*/, std::vector* integer_reason) const { - expr.SimpleCanonicalization(); + DCHECK(expr.IsCanonicalized()); + DCHECK_EQ(expr.DivideByGcd(), 1); + DCHECK_LE(GetUpperBoundFromLinear3(expr), ub); - if (expr.coeffs[0] == 0 && expr.coeffs[1] == 0) return; // trivially zero + const auto it = best_affine_ub_.find(expr); + DCHECK(it != best_affine_ub_.end()); - // Starts by simple bounds. - if (best_root_level_bounds_.GetUpperBound(expr) <= ub) return; + // We want the reason for "expr <= ub" + // knowing that expr <= affine / divisor. + const auto [affine, divisor] = it->second; + integer_reason->push_back(affine.LowerOrEqual(CapProdI(ub + 1, divisor) - 1)); +} - // Add explanation if it is a trivial bound. - const IntegerValue implied_ub = GetImpliedUpperBound(expr); - if (implied_ub <= ub) { - const IntegerValue slack = ub - implied_ub; - expr.Negate(); // AppendRelaxedLinearReason() explains a lower bound. - absl::Span vars = expr.non_zero_vars(); - absl::Span coeffs = expr.non_zero_coeffs(); - integer_trail_->AppendRelaxedLinearReason(slack, coeffs, vars, - integer_reason); - return; +IntegerValue Linear2Bounds::UpperBound(LinearExpression2 expr) const { + expr.SimpleCanonicalization(); + if (expr.coeffs[0] == 0) { + return integer_trail_->UpperBound(expr); } - - // None of the bound above are enough, try the affine one. Note that gcd * - // expr <= ub, is the same as asking why expr <= FloorRatio(ub, gcd). + DCHECK_NE(expr.coeffs[1], 0); const IntegerValue gcd = expr.DivideByGcd(); - const auto it = best_affine_ub_.find(expr); - if (it == best_affine_ub_.end()) return; + IntegerValue ub = integer_trail_->UpperBound(expr); + const LinearExpression2Index index = non_trivial_bounds_->GetIndex(expr); + if (index != kNoLinearExpression2Index) { + ub = std::min(ub, root_level_bounds_->GetUpperBoundNoTrail(index)); + ub = std::min(ub, enforced_bounds_->GetUpperBoundFromEnforced(index)); + } + ub = std::min(ub, linear3_bounds_->GetUpperBoundFromLinear3(expr)); + return CapProdI(gcd, ub); +} - // We want the reason for "expr <= ub", that is the reason for - // - "gcd * canonical_expr <= ub" - // - "canonical_expr <= FloorRatio(ub, gcd); - // - // knowing that canonical_expr <= affine_ub / divisor. - const auto [affine, divisor] = it->second; - integer_reason->push_back( - affine.LowerOrEqual(CapProdI(FloorRatio(ub, gcd) + 1, divisor) - 1)); +IntegerValue Linear2Bounds::NonTrivialUpperBoundForGcd1( + LinearExpression2 expr) const { + expr.SimpleCanonicalization(); + if (expr.coeffs[0] == 0) { + return integer_trail_->UpperBound(expr); + } + DCHECK_NE(expr.coeffs[1], 0); + DCHECK_EQ(1, expr.DivideByGcd()); + IntegerValue ub = kMaxIntegerValue; + const LinearExpression2Index index = non_trivial_bounds_->GetIndex(expr); + if (index != kNoLinearExpression2Index) { + ub = std::min(ub, root_level_bounds_->GetUpperBoundNoTrail(index)); + ub = std::min(ub, enforced_bounds_->GetUpperBoundFromEnforced(index)); + } + ub = std::min(ub, linear3_bounds_->GetUpperBoundFromLinear3(expr)); + return ub; } -std::vector -BinaryRelationsMaps::GetAllExpressionsWithAffineBounds() const { - std::vector result; - for (const auto [expr, info] : best_affine_ub_) { - result.push_back(expr); +void Linear2Bounds::AddReasonForUpperBoundLowerThan( + LinearExpression2 expr, IntegerValue ub, + std::vector* literal_reason, + std::vector* integer_reason) const { + DCHECK_LE(UpperBound(expr), ub); + + // Explanation are by order of preference, with no reason needed first. + if (integer_trail_->LevelZeroUpperBound(expr) <= ub) { + return; } - return result; + expr.SimpleCanonicalization(); + const IntegerValue gcd = expr.DivideByGcd(); + ub = FloorRatio(ub, gcd); + const LinearExpression2Index index = non_trivial_bounds_->GetIndex(expr); + // This one is a single literal. + if (index != kNoLinearExpression2Index) { + if (root_level_bounds_->GetUpperBoundNoTrail(index) <= ub) { + return; + } + if (enforced_bounds_->GetUpperBoundFromEnforced(index) <= ub) { + return enforced_bounds_->AddReasonForUpperBoundLowerThan( + index, ub, literal_reason, integer_reason); + } + } + + // This one is a single var upper bound. + if (linear3_bounds_->GetUpperBoundFromLinear3(expr) <= ub) { + return linear3_bounds_->AddReasonForUpperBoundLowerThan( + expr, ub, literal_reason, integer_reason); + } + + // Trivial linear2 bounds from its variables. + const IntegerValue implied_ub = integer_trail_->UpperBound(expr); + const IntegerValue slack = ub - implied_ub; + DCHECK_GE(slack, 0); + expr.Negate(); // AppendRelaxedLinearReason() explains a lower bound. + absl::Span vars = expr.non_zero_vars(); + absl::Span coeffs = expr.non_zero_coeffs(); + integer_trail_->AppendRelaxedLinearReason(slack, coeffs, vars, + integer_reason); } } // namespace sat diff --git a/ortools/sat/precedences.h b/ortools/sat/precedences.h index 4ded3dbc4d2..586b28dd892 100644 --- a/ortools/sat/precedences.h +++ b/ortools/sat/precedences.h @@ -18,15 +18,20 @@ #include #include #include +#include #include #include +#include "absl/container/btree_set.h" #include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" #include "absl/container/inlined_vector.h" #include "absl/log/check.h" +#include "absl/strings/str_format.h" #include "absl/types/span.h" #include "ortools/base/strong_vector.h" #include "ortools/graph/graph.h" +#include "ortools/sat/cp_model_mapping.h" #include "ortools/sat/integer.h" #include "ortools/sat/integer_base.h" #include "ortools/sat/model.h" @@ -41,40 +46,323 @@ namespace operations_research { namespace sat { +DEFINE_STRONG_INDEX_TYPE(LinearExpression2Index); +const LinearExpression2Index kNoLinearExpression2Index(-1); +inline LinearExpression2Index NegationOf(LinearExpression2Index i) { + return LinearExpression2Index(i.value() ^ 1); +} + +inline bool Linear2IsPositive(LinearExpression2Index i) { + return (i.value() & 1) == 0; +} + +inline LinearExpression2Index PositiveLinear2(LinearExpression2Index i) { + return LinearExpression2Index(i.value() & (~1)); +} + +// Class to hold a list of LinearExpression2 that have (potentially) non-trivial +// bounds. This class is overzealous, in the sense that if a linear2 is in the +// list, it does not necessarily mean that it has a non-trivial bound, but the +// converse is true: if a linear2 is not in the list, +// Linear2Bounds::GetUpperBound() will return a trivial bound. +class Linear2WithPotentialNonTrivalBounds { + public: + Linear2WithPotentialNonTrivalBounds() = default; + + // Returns a never-changing index for the given linear expression. + // The expression must already be canonicalized and divided by its GCD. + LinearExpression2Index AddOrGet(LinearExpression2 expr); + + // Returns a never-changing index for the given linear expression if it is + // potentially non-trivial, otherwise returns kNoLinearExpression2Index. The + // expression must already be canonicalized and divided by its GCD. + LinearExpression2Index GetIndex(LinearExpression2 expr) const; + + LinearExpression2 GetExpression(LinearExpression2Index index) const; + + // Return all positive linear2 expressions that have a potentially non-trivial + // bound. When calling this code it is often a good idea to check both the + // expression on the span and its negation. The order is fixed forever and + // this span can only grow by appending new expressions. + absl::Span GetLinear2WithPotentialNonTrivalBounds() + const { + return exprs_; + } + + // Return a list of all potentially non-trivial LinearExpression2Indexes + // containing a given variable. + absl::Span GetAllLinear2ContainingVariable( + IntegerVariable var) const; + + // Return a list of all potentially non-trivial LinearExpression2Indexes + // containing a given pair of variables. + absl::Span GetAllLinear2ContainingVariables( + IntegerVariable var1, IntegerVariable var2) const; + + // For a given variable `var`, return all linear expressions with both + // coefficients 1 that have a potentially non trivial upper bound. For + // convenience it also returns the other variable to cheaply build the + // linear2. Note that using negation one can also recover x + y >= lb and x - + // y <= ub. + absl::Span + GetAllLinear2ContainingVariableWithCoeffOne(IntegerVariable var) const { + if (var >= coeff_one_var_lookup_.size()) return {}; + return coeff_one_var_lookup_[var]; + } + + private: + std::vector exprs_; + absl::flat_hash_map expr_to_index_; + + // Lookup table to find all the LinearExpression2 with a given variable and + // having both coefficient 1. + util_intops::StrongVector> + coeff_one_var_lookup_; + + // Map to implement GetAllBoundsContainingVariable(). + absl::flat_hash_map> + var_to_bounds_; + // Map to implement GetAllBoundsContainingVariables(). + absl::flat_hash_map, + absl::InlinedVector> + var_pair_to_bounds_; +}; + +// Simple "watcher" class that will be notified if a linear2 bound changed. It +// can also be queried to see if LinearExpression2 involving a specific variable +// changed since last time. +class Linear2Watcher { + public: + explicit Linear2Watcher(Model* model) + : watcher_(model->GetOrCreate()) {} + + // This assumes `expr` is canonicalized and divided by its gcd. + void NotifyBoundChanged(LinearExpression2 expr); + + // Register a GenericLiteralWatcher() id so that propagation is called as + // soon as a bound on a linear2 changed. + void WatchAllLinearExpressions2(int id) { propagator_ids_.insert(id); } + + // Allow to know if some bounds changed since last query. + int64_t Timestamp() const { return timestamp_; } + int64_t VarTimestamp(IntegerVariable var); + + private: + GenericLiteralWatcher* watcher_; + + int64_t timestamp_ = 0; + util_intops::StrongVector var_timestamp_; + absl::btree_set propagator_ids_; +}; + +// This holds all the relation lhs <= linear2 <= rhs that are true at level +// zero. It is the source of truth across all the solver for such bounds. +class RootLevelLinear2Bounds { + public: + explicit RootLevelLinear2Bounds(Model* model) + : integer_trail_(model->GetOrCreate()), + linear2_watcher_(model->GetOrCreate()), + shared_stats_(model->GetOrCreate()), + non_trivial_bounds_( + model->GetOrCreate()), + cp_model_mapping_(model->GetOrCreate()), + shared_linear2_bounds_(model->Mutable()), + shared_linear2_bounds_id_( + shared_linear2_bounds_ == nullptr + ? 0 + : shared_linear2_bounds_->RegisterNewId(model->Name())) {} + + ~RootLevelLinear2Bounds(); + + // Add a relation lb <= expr <= ub. If expr is not a proper linear2 expression + // (e.g. 0*x + y, y + y, y - y) it will be ignored. + // Returns a pair saying whether the lower/upper bounds for this expr became + // more restricted than what was currently stored. + std::pair Add(LinearExpression2 expr, IntegerValue lb, + IntegerValue ub) { + const bool negated = expr.CanonicalizeAndUpdateBounds(lb, ub); + if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) return {false, false}; + const LinearExpression2Index index = non_trivial_bounds_->AddOrGet(expr); + bool ub_changed = AddUpperBound(index, ub); + bool lb_changed = AddUpperBound(NegationOf(index), -lb); + if (negated) { + std::swap(lb_changed, ub_changed); + } + return {lb_changed, ub_changed}; + } + + bool AddUpperBound(LinearExpression2Index index, IntegerValue ub); + + // Same as above, but only update the upper bound. + bool AddUpperBound(LinearExpression2 expr, IntegerValue ub) { + expr.SimpleCanonicalization(); + if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) return false; + const IntegerValue gcd = expr.DivideByGcd(); + ub = FloorRatio(ub, gcd); + return AddUpperBound(non_trivial_bounds_->AddOrGet(expr), ub); + } + + IntegerValue LevelZeroUpperBound(LinearExpression2 expr) const { + expr.SimpleCanonicalization(); + if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) { + return integer_trail_->LevelZeroUpperBound(expr); + } + const IntegerValue gcd = expr.DivideByGcd(); + const LinearExpression2Index index = non_trivial_bounds_->GetIndex(expr); + if (index == kNoLinearExpression2Index) { + return integer_trail_->LevelZeroUpperBound(expr); + } + return CapProdI(gcd, LevelZeroUpperBound(index)); + } + + IntegerValue LevelZeroUpperBound(LinearExpression2Index index) const { + const LinearExpression2 expr = non_trivial_bounds_->GetExpression(index); + // TODO(user): Remove the expression from the root_level_relations_ if + // the zero-level bound got more restrictive. + return std::min(integer_trail_->LevelZeroUpperBound(expr), + GetUpperBoundNoTrail(index)); + } + + // Return a list of (expr <= ub) sorted by expr. They are guaranteed to be + // better than the trivial upper bound. + std::vector> + GetSortedNonTrivialUpperBounds() const; + + // Return a list of (lb <= expr <= ub), with expr.vars[0] = var, where at + // least one of the bounds is non-trivial and the potential other non-trivial + // bound is tight. + // + // As the class name indicates, all bounds are level zero ones. + std::vector> + GetAllBoundsContainingVariable(IntegerVariable var) const; + + // Return a list of (lb <= expr <= ub), with expr.vars = {var1, var2}, where + // at least one of the bounds is non-trivial and the potential other + // non-trivial bound is tight. + // + // As the class name indicates, all bounds are level zero ones. + std::vector> + GetAllBoundsContainingVariables(IntegerVariable var1, + IntegerVariable var2) const; + + // For a given variable `var`, return all variables `other` so that + // LinearExpression2(var, other, 1, 1) has a non trivial upper bound. + // Note that using negation one can also recover x + y >= lb and x - y <= ub. + std::vector GetVariablesInSimpleRelation( + IntegerVariable var) const; + + RelationStatus GetLevelZeroStatus(LinearExpression2 expr, IntegerValue lb, + IntegerValue ub) const; + + // Low-level function that returns the zero-level upper bound if it is + // non-trivial. Otherwise returns kMaxIntegerValue. This is a different + // behavior from LevelZeroUpperBound() that would return the implied + // zero-level bound from the trail for trivial ones. `expr` must be + // canonicalized and gcd-reduced. + IntegerValue GetUpperBoundNoTrail(LinearExpression2Index index) const; + + private: + IntegerTrail* integer_trail_; + Linear2Watcher* linear2_watcher_; + SharedStatistics* shared_stats_; + Linear2WithPotentialNonTrivalBounds* non_trivial_bounds_; + CpModelMapping* cp_model_mapping_; + SharedLinear2Bounds* shared_linear2_bounds_; // Might be nullptr. + + const int shared_linear2_bounds_id_; + + util_intops::StrongVector + best_upper_bounds_; + + int64_t num_updates_ = 0; +}; + struct FullIntegerPrecedence { IntegerVariable var; std::vector indices; std::vector offsets; }; -// Stores all the precedences relation of the form "a*x + b*y <= ub" -// that we could extract from the linear constraint of the model. These are -// stored in a directed graph. +// This class is used to compute the transitive closure of the level-zero +// precedence relations. // // TODO(user): Support conditional relation. // TODO(user): Support non-DAG like graph. // TODO(user): Support variable offset that can be updated as search progress. -class PrecedenceRelations : public ReversibleInterface { +class TransitivePrecedencesEvaluator { public: - explicit PrecedenceRelations(Model* model) + explicit TransitivePrecedencesEvaluator(Model* model) + : integer_trail_(model->GetOrCreate()), + shared_stats_(model->GetOrCreate()), + root_level_bounds_(model->GetOrCreate()) {} + + // Returns a set of relations var >= max_i(vars[index[i]] + offsets[i]). + // + // This currently only works if the precedence relation form a DAG. + // If not we will just abort. TODO(user): generalize. + // + // For more efficiency, this method ignores all linear2 expressions with any + // coefficient different from 1. + // + // TODO(user): Put some work limit in place, as this can be slow. Complexity + // is in O(vars.size()) * num_arcs. + // + // TODO(user): Since we don't need ALL precedences, we could just work on a + // sub-DAG of the full precedence graph instead of aborting. Or we can just + // support the general non-DAG cases. + // + // TODO(user): Many relations can be redundant. Filter them. + void ComputeFullPrecedences(absl::Span vars, + std::vector* output); + + // The current code requires the internal data to be processed once all + // root-level relations are loaded. + // + // If we don't have too many variable, we compute the full transitive closure + // and then push back to RootLevelLinear2Bounds if there is a relation between + // two variables. This can be used to optimize some scheduling propagation and + // reasons. + // + // Warning: If there are too many, this will NOT contain all relations. + // + // Returns kMaxIntegerValue if there are none, otherwise return an upper bound + // such that expr <= ub. + // + // TODO(user): Be more dynamic as we start to add relations during search. + void Build(); + + private: + IntegerTrail* integer_trail_; + SharedStatistics* shared_stats_; + RootLevelLinear2Bounds* root_level_bounds_; + + util::StaticGraph<> graph_; + std::vector arc_offsets_; + + bool is_built_ = false; + bool is_dag_ = false; + std::vector topological_order_; +}; + +// Stores all the precedences relation of the form "{lits} => a*x + b*y <= ub" +// that we could extract from the model. +class EnforcedLinear2Bounds : public ReversibleInterface { + public: + explicit EnforcedLinear2Bounds(Model* model) : params_(*model->GetOrCreate()), trail_(model->GetOrCreate()), - integer_trail_(model->GetOrCreate()) { + integer_trail_(model->GetOrCreate()), + linear2_watcher_(model->GetOrCreate()), + root_level_bounds_(model->GetOrCreate()), + shared_stats_(model->GetOrCreate()), + non_trivial_bounds_( + model->GetOrCreate()) { integer_trail_->RegisterReversibleClass(this); } - void Resize(int num_variables) { - graph_.ReserveNodes(num_variables); - graph_.AddNode(num_variables - 1); - } - - // Add a relation lb <= expr <= ub. If expr is not a proper linear2 expression - // (e.g. 0*x + y, y + y, y - y) it will be ignored. Returns true if it was - // added and is considered "new". - bool AddBounds(LinearExpression2 expr, IntegerValue lb, IntegerValue ub); - - // Same as above, but only for the upper bound. - bool AddUpperBound(LinearExpression2 expr, IntegerValue ub); + ~EnforcedLinear2Bounds() override; // Adds add relation (enf => expr <= rhs) that is assumed to be true at // the current level. @@ -88,37 +376,33 @@ class PrecedenceRelations : public ReversibleInterface { // If expr is not a proper linear2 expression (e.g. 0*x + y, y + y, y - y) it // will be ignored. void PushConditionalRelation(absl::Span enforcements, - LinearExpression2 expr, IntegerValue rhs); + LinearExpression2Index index, IntegerValue rhs); + + void PushConditionalRelation(absl::Span enforcements, + LinearExpression2 expr, IntegerValue rhs) { + expr.SimpleCanonicalization(); + if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) return; + const IntegerValue gcd = expr.DivideByGcd(); + rhs = FloorRatio(rhs, gcd); + return PushConditionalRelation(enforcements, + non_trivial_bounds_->AddOrGet(expr), rhs); + } // Called each time we change decision level. void SetLevel(int level) final; - // Returns a set of relations var >= max_i(vars[index[i]] + offsets[i]). + // Returns a set of precedences (var, index) such that we have a relation + // of the form var[index] <= var + offset. // - // This currently only works if the precedence relation form a DAG. - // If not we will just abort. TODO(user): generalize. - // - // For more efficiency, this method ignores all linear2 expressions with any - // coefficient different from 1. + // All entries for the same variable will be contiguous and sorted by index. + // We only list variable with at least two entries. The offset can be + // retrieved via Linear2Bounds::UpperBound(Difference(vars[index]), var)). // - // TODO(user): Put some work limit in place, as this can be slow. Complexity - // is in O(vars.size()) * num_arcs. - // - // TODO(user): Since we don't need ALL precedences, we could just work on a - // sub-DAG of the full precedence graph instead of aborting. Or we can just - // support the general non-DAG cases. + // This method currently ignores all linear2 expressions with any coefficient + // different from 1. // - // TODO(user): Many relations can be redundant. Filter them. - void ComputeFullPrecedences(absl::Span vars, - std::vector* output); - - // Returns a set of precedences (var, index) such that var is after - // vars[index]. All entries for the same variable will be contiguous and - // sorted by index. We only list variable with at least two entries. The - // offset can be retrieved via UpperBound(vars[index], var). - // - // For more efficiency, this method ignores all linear2 expressions with any - // coefficient different from 1. + // TODO(user): Ideally this should be moved to a new class and maybe augmented + // with other kind of precedences. struct PrecedenceData { IntegerVariable var; int index; @@ -126,137 +410,311 @@ class PrecedenceRelations : public ReversibleInterface { void CollectPrecedences(absl::Span vars, std::vector* output); - // If we don't have too many variable, we compute the full transitive closure - // and can query in O(1) if there is a relation between two variables. - // This can be used to optimize some scheduling propagation and reasons. - // - // Warning: If there are too many, this will NOT contain all relations. - // - // Returns kMaxIntegerValue if there are none, otherwise return an upper bound - // such that expr <= ub. - IntegerValue LevelZeroUpperBound(LinearExpression2 expr) const; - - // Returns the maximum value for expr, and the reason for it (all - // true). Note that we always check LevelZeroUpperBound() so if it is better, - // the returned literal reason will be empty. - // - // We separate the two because usually the reason is only needed when we push, - // which happen less often, so we don't mind doing two hash lookups, and we - // really want to optimize the UpperBound() instead. - // - // Important: This doesn't contains the transitive closure. - // Important: The span is only valid in a narrow scope. - IntegerValue UpperBound(LinearExpression2 expr) const; + // Low-level function that returns the upper bound if there is some enforced + // relations only. Otherwise always returns kMaxIntegerValue. + // `expr` must be canonicalized and gcd-reduced. + IntegerValue GetUpperBoundFromEnforced(LinearExpression2Index index) const; void AddReasonForUpperBoundLowerThan( - LinearExpression2 expr, IntegerValue ub, + LinearExpression2Index index, IntegerValue ub, std::vector* literal_reason, std::vector* integer_reason) const; - // The current code requires the internal data to be processed once all - // relations are loaded. - // - // TODO(user): Be more dynamic as we start to add relations during search. - void Build(); - private: void CreateLevelEntryIfNeeded(); - // expr <= ub. - bool AddInternal(LinearExpression2 expr, IntegerValue ub) { - expr.SimpleCanonicalization(); - if (expr.coeffs[0] == 0 || expr.coeffs[1] == 0) { - return false; - } - const auto [it, inserted] = root_relations_.insert({expr, ub}); - UpdateBestRelationIfBetter(expr, ub); - if (inserted) { - if (expr.coeffs[0] != 1 || expr.coeffs[1] != 1) { - return true; - } - const int new_size = - std::max(expr.vars[0].value(), expr.vars[1].value()) + 1; - if (new_size > after_.size()) after_.resize(new_size); - after_[expr.vars[0]].push_back(NegationOf(expr.vars[1])); - after_[expr.vars[1]].push_back(NegationOf(expr.vars[0])); - return true; - } - it->second = std::min(it->second, ub); - return false; - } - - void UpdateBestRelationIfBetter(LinearExpression2 expr, IntegerValue rhs) { - const auto [it, inserted] = best_relations_.insert({expr, rhs}); - if (!inserted) { - it->second = std::min(it->second, rhs); - } - } - - void UpdateBestRelation(LinearExpression2 expr, IntegerValue rhs) { - const auto it = root_relations_.find(expr); - if (it != root_relations_.end()) { - rhs = std::min(rhs, it->second); - } - if (rhs == kMaxIntegerValue) { - best_relations_.erase(expr); - } else { - best_relations_[expr] = rhs; - } - } - const SatParameters& params_; Trail* trail_; IntegerTrail* integer_trail_; + Linear2Watcher* linear2_watcher_; + RootLevelLinear2Bounds* root_level_bounds_; + SharedStatistics* shared_stats_; + Linear2WithPotentialNonTrivalBounds* non_trivial_bounds_; - util::StaticGraph<> graph_; - std::vector arc_offsets_; - - bool is_built_ = false; - bool is_dag_ = false; - std::vector topological_order_; + int64_t num_conditional_relation_updates_ = 0; // Conditional stack for push/pop of conditional relations. // // TODO(user): this kind of reversible hash_map is already implemented in // other part of the code. Consolidate. struct ConditionalEntry { - ConditionalEntry(int p, IntegerValue r, LinearExpression2 k, + ConditionalEntry(int p, IntegerValue r, LinearExpression2Index k, absl::Span e) : prev_entry(p), rhs(r), key(k), enforcements(e.begin(), e.end()) {} int prev_entry; IntegerValue rhs; - LinearExpression2 key; + LinearExpression2Index key; absl::InlinedVector enforcements; }; std::vector conditional_stack_; std::vector> level_to_stack_size_; - // This is always stored in the form (expr <= rhs). - // The conditional relations contains indices in the conditional_stack_. - absl::flat_hash_map root_relations_; - absl::flat_hash_map conditional_relations_; + // This is always stored in the form (expr <= rhs). + // The conditional relations contains indices in the conditional_stack_. + util_intops::StrongVector conditional_relations_; + + // Store for each variable x, the variables y that appears alongside it in + // lit => x + y <= ub. Note that conditional_var_lookup_ is updated on + // dive/backtrack. + util_intops::StrongVector> + conditional_var_lookup_; + + // Temp data for CollectPrecedences. + std::vector var_with_positive_degree_; + util_intops::StrongVector var_to_degree_; + util_intops::StrongVector var_to_last_index_; + std::vector tmp_precedences_; +}; + +// A relation of the form enforcement => expr \in [lhs, rhs]. +// Note that the [lhs, rhs] interval should always be within [min_activity, +// max_activity] where the activity is the value of expr. +struct Relation { + Literal enforcement; + LinearExpression2 expr; + IntegerValue lhs; + IntegerValue rhs; + + bool operator==(const Relation& other) const { + return enforcement == other.enforcement && expr == other.expr && + lhs == other.lhs && rhs == other.rhs; + } + + template + friend void AbslStringify(Sink& sink, const Relation& relation) { + absl::Format(&sink, "%s => %v in [%v, %v]", + relation.enforcement.DebugString(), relation.expr, + relation.lhs, relation.rhs); + } +}; + +// A repository of all the enforced linear constraints of size 1 or 2. +// +// TODO(user): This is not always needed, find a way to clean this once we +// don't need it. +class BinaryRelationRepository { + public: + int size() const { return relations_.size(); } + + // The returned relation is guaranteed to only have positive variables. + const Relation& relation(int index) const { return relations_[index]; } + + // Returns the indices of the relations that are enforced by the given + // literal. + absl::Span IndicesOfRelationsEnforcedBy(LiteralIndex lit) const { + if (lit >= lit_to_relations_.size()) return {}; + return lit_to_relations_[lit]; + } + + // Adds a conditional relation lit => expr \in [lhs, rhs] (one of the coeffs + // can be zero). + void Add(Literal lit, LinearExpression2 expr, IntegerValue lhs, + IntegerValue rhs); + + // Adds a partial conditional relation between two variables, with unspecified + // coefficients and bounds. + void AddPartialRelation(Literal lit, IntegerVariable a, IntegerVariable b); + + // Builds the literal to relations mapping. This should be called once all the + // relations have been added. + void Build(); + + // Assuming level-zero bounds + any (var >= value) in the input map, + // fills "output" with a "propagated" set of bounds assuming lit is true (by + // using the relations enforced by lit, as well as the non-enforced ones). + // Note that we will only fill bounds > level-zero ones in output. + // + // Returns false if the new bounds are infeasible at level zero. + // + // Important: by default this does not call output->clear() so we can take + // the max with already inferred bounds. + bool PropagateLocalBounds( + const IntegerTrail& integer_trail, + const RootLevelLinear2Bounds& root_level_bounds, Literal lit, + const absl::flat_hash_map& input, + absl::flat_hash_map* output) const; + + private: + bool is_built_ = false; + int num_enforced_relations_ = 0; + std::vector relations_; + CompactVectorVector lit_to_relations_; +}; + +// Class that keeps the best upper bound for a*x + b*y by using all the linear3 +// relations of the form a*x + b*y + c*z <= d. +class Linear2BoundsFromLinear3 { + public: + explicit Linear2BoundsFromLinear3(Model* model); + ~Linear2BoundsFromLinear3(); + + // If the given upper bound evaluate better than the current one we have, this + // will replace it and returns true, otherwise it returns false. + bool AddAffineUpperBound(LinearExpression2 expr, AffineExpression affine_ub); + + // Most users should just use Linear2Bounds::UpperBound() instead. + // + // Returns the upper bound only if there is some relations coming from a + // linear3. Otherwise always returns kMaxIntegerValue. + // `expr` must be canonicalized and gcd-reduced. + IntegerValue GetUpperBoundFromLinear3(LinearExpression2 expr) const; + + // Most users should use Linear2Bounds::AddReasonForUpperBoundLowerThan() + // instead. + // + // Adds the reason for GetUpperBoundFromLinear3() to be <= ub. + // `expr` must be canonicalized and gcd-reduced. + void AddReasonForUpperBoundLowerThan( + LinearExpression2 expr, IntegerValue ub, + std::vector* literal_reason, + std::vector* integer_reason) const; + + private: + IntegerTrail* integer_trail_; + Trail* trail_; + Linear2Watcher* linear2_watcher_; + GenericLiteralWatcher* watcher_; + SharedStatistics* shared_stats_; + RootLevelLinear2Bounds* root_level_bounds_; + Linear2WithPotentialNonTrivalBounds* non_trivial_bounds_; + + int64_t num_affine_updates_ = 0; + + // This stores linear2 <= AffineExpression / divisor. + // + // Note(user): This is a "cheap way" to not have to deal with backtracking, If + // we have many possible AffineExpression that bounds a LinearExpression2, we + // keep the best one during "search dive" but on backtrack we might have a + // sub-optimal relation. + absl::flat_hash_map> + best_affine_ub_; +}; + +// TODO(user): Merge with BinaryRelationRepository. Note that this one provides +// different indexing though, so it could be kept separate. +// TODO(user): Use LinearExpression2 instead of pairs of AffineExpression for +// consistency with other classes. +class ReifiedLinear2Bounds { + public: + explicit ReifiedLinear2Bounds(Model* model); + + // Return the status of a <= b; + RelationStatus GetLevelZeroPrecedenceStatus(AffineExpression a, + AffineExpression b) const; + + // Register the fact that l <=> ( a <= b ). + // These are considered equivalence relation. + void AddReifiedPrecedenceIfNonTrivial(Literal l, AffineExpression a, + AffineExpression b); + + // Returns kNoLiteralIndex if we don't have a literal <=> ( a <= b ), or + // returns that literal if we have one. Note that we will return the + // true/false literal if the status is known at level zero. + LiteralIndex GetReifiedPrecedence(AffineExpression a, AffineExpression b); + + private: + IntegerEncoder* integer_encoder_; + RootLevelLinear2Bounds* best_root_level_bounds_; + + // This stores relations l <=> (linear2 <= rhs). + absl::flat_hash_map, Literal> + relation_to_lit_; + + // This is used to detect relations that become fixed at level zero and + // "upgrade" them to non-enforced relations. Because we only do that when + // we fix variable, a linear scan shouldn't be too bad and is relatively + // compact memory wise. + absl::flat_hash_set variable_appearing_in_reified_relations_; + std::vector> + all_reified_relations_; +}; + +// Simple wrapper around the different repositories for bounds of linear2. +// This should provide the best bounds. +class Linear2Bounds { + public: + explicit Linear2Bounds(Model* model) + : integer_trail_(model->GetOrCreate()), + root_level_bounds_(model->GetOrCreate()), + enforced_bounds_(model->GetOrCreate()), + linear3_bounds_(model->GetOrCreate()), + non_trivial_bounds_( + model->GetOrCreate()) {} + + // Returns the best known upper-bound of the given LinearExpression2 at the + // current decision level. If its explanation is needed, it can be queried + // with the second function. + IntegerValue UpperBound(LinearExpression2 expr) const; + void AddReasonForUpperBoundLowerThan( + LinearExpression2 expr, IntegerValue ub, + std::vector* literal_reason, + std::vector* integer_reason) const; + + // Like UpperBound(), but optimized for the case of gcd == 1 and when we + // don't want the trivial bounds. + IntegerValue NonTrivialUpperBoundForGcd1(LinearExpression2 expr) const; + + private: + IntegerTrail* integer_trail_; + RootLevelLinear2Bounds* root_level_bounds_; + EnforcedLinear2Bounds* enforced_bounds_; + Linear2BoundsFromLinear3* linear3_bounds_; + Linear2WithPotentialNonTrivalBounds* non_trivial_bounds_; +}; + +// Detects if at least one of a subset of linear of size 2 or 1, touching the +// same variable, must be true. When this is the case we add a new propagator to +// propagate that fact. +// +// TODO(user): Shall we do that on the main thread before the workers are +// spawned? note that the probing version need the model to be loaded though. +class GreaterThanAtLeastOneOfDetector { + public: + explicit GreaterThanAtLeastOneOfDetector(Model* model) + : repository_(*model->GetOrCreate()) {} + + // Advanced usage. To be called once all the constraints have been added to + // the model. This will detect GreaterThanAtLeastOneOfConstraint(). + // Returns the number of added constraint. + // + // TODO(user): This can be quite slow, add some kind of deterministic limit + // so that we can use it all the time. + int AddGreaterThanAtLeastOneOfConstraints(Model* model, + bool auto_detect_clauses = false); + + private: + // Given an existing clause, sees if it can be used to add "greater than at + // least one of" type of constraints. Returns the number of such constraint + // added. + int AddGreaterThanAtLeastOneOfConstraintsFromClause( + absl::Span clause, Model* model); - // Contains std::min() of the offset from root_relations_ and - // conditional_relations_. - absl::flat_hash_map best_relations_; + // Another approach for AddGreaterThanAtLeastOneOfConstraints(), this one + // might be a bit slow as it relies on the propagation engine to detect + // clauses between incoming arcs presence literals. + // Returns the number of added constraints. + int AddGreaterThanAtLeastOneOfConstraintsWithClauseAutoDetection( + Model* model); - // Store for each variable x, the variables y that appears alongside it in - // LevelZeroUpperBound(expr) or UpperBound(expr). That is the variable - // that are after x with an offset. Note that conditional_after_ is updated on - // dive/backtrack. - util_intops::StrongVector> - after_; - util_intops::StrongVector> - conditional_after_; + // Once we identified a clause and relevant indices, this build the + // constraint. Returns true if we actually add it. + bool AddRelationFromIndices(IntegerVariable var, + absl::Span clause, + absl::Span indices, Model* model); - // Temp data for CollectPrecedences. - std::vector var_with_positive_degree_; - util_intops::StrongVector var_to_degree_; - util_intops::StrongVector var_to_last_index_; - std::vector tmp_precedences_; + BinaryRelationRepository& repository_; }; +// ============================================================================= +// Old precedences propagator. +// +// This is superseded by the new LinearPropagator and should only be used if the +// option 'new_linear_propagation' is false. We still keep it around to +// benchmark and test the new code vs this one. +// ============================================================================= + // This class implement a propagator on simple inequalities between integer // variables of the form (i1 + offset <= i2). The offset can be constant or // given by the value of a third integer variable. Offsets can also be negative. @@ -277,7 +735,7 @@ class PrecedencesPropagator : public SatPropagator, PropagatorInterface { public: explicit PrecedencesPropagator(Model* model) : SatPropagator("PrecedencesPropagator"), - relations_(model->GetOrCreate()), + relations_(model->GetOrCreate()), trail_(model->GetOrCreate()), integer_trail_(model->GetOrCreate()), shared_stats_(model->Mutable()), @@ -405,7 +863,7 @@ class PrecedencesPropagator : public SatPropagator, PropagatorInterface { // External class needed to get the IntegerVariable lower bounds and Enqueue // new ones. - PrecedenceRelations* relations_; + EnforcedLinear2Bounds* relations_; Trail* trail_; IntegerTrail* integer_trail_; SharedStatistics* shared_stats_ = nullptr; @@ -471,261 +929,6 @@ class PrecedencesPropagator : public SatPropagator, PropagatorInterface { int64_t num_enforcement_pushes_ = 0; }; -// Similar to AffineExpression, but with a zero constant. -// If coeff is zero, then this is always zero and var is ignored. -struct LinearTerm { - LinearTerm() = default; - LinearTerm(IntegerVariable v, IntegerValue c) : var(v), coeff(c) {} - - void MakeCoeffPositive() { - if (coeff < 0) { - coeff = -coeff; - var = NegationOf(var); - } - } - - bool operator==(const LinearTerm& other) const { - return var == other.var && coeff == other.coeff; - } - - IntegerVariable var = kNoIntegerVariable; - IntegerValue coeff = IntegerValue(0); -}; - -// A relation of the form enforcement => a + b \in [lhs, rhs]. -// Note that the [lhs, rhs] interval should always be within [min_activity, -// max_activity] where the activity is the value of a + b. -struct Relation { - Literal enforcement; - LinearTerm a; - LinearTerm b; - IntegerValue lhs; - IntegerValue rhs; - - bool operator==(const Relation& other) const { - return enforcement == other.enforcement && a == other.a && b == other.b && - lhs == other.lhs && rhs == other.rhs; - } -}; - -// A repository of all the enforced linear constraints of size 1 or 2, and of -// all the non-enforced linear constraints of size 2. -// -// TODO(user): This is not always needed, find a way to clean this once we -// don't need it. -class BinaryRelationRepository { - public: - int size() const { return relations_.size(); } - - // The returned relation is guaranteed to only have positive variables. - const Relation& relation(int index) const { return relations_[index]; } - - // Returns the indices of the relations that are enforced by the given - // literal. - absl::Span IndicesOfRelationsEnforcedBy(LiteralIndex lit) const { - if (lit >= lit_to_relations_.size()) return {}; - return lit_to_relations_[lit]; - } - - // Returns the indices of the non-enforced relations that contain the given - // (positive) variable. - absl::Span IndicesOfRelationsContaining( - IntegerVariable var) const { - if (var >= var_to_relations_.size()) return {}; - return var_to_relations_[var]; - } - - // Returns the indices of the non-enforced relations that contain the given - // (positive) variables. - absl::Span IndicesOfRelationsBetween(IntegerVariable var1, - IntegerVariable var2) const { - if (var1 > var2) std::swap(var1, var2); - const std::pair key(var1, var2); - const auto it = var_pair_to_relations_.find(key); - if (it == var_pair_to_relations_.end()) return {}; - return it->second; - } - - // Adds a conditional relation lit => a + b \in [lhs, rhs] (one of the terms - // can be zero), or an always true binary relation a + b \in [lhs, rhs] (both - // terms must be non-zero). - void Add(Literal lit, LinearTerm a, LinearTerm b, IntegerValue lhs, - IntegerValue rhs); - - // Adds a partial conditional relation between two variables, with unspecified - // coefficients and bounds. - void AddPartialRelation(Literal lit, IntegerVariable a, IntegerVariable b); - - // Builds the literal to relations mapping. This should be called once all the - // relations have been added. - void Build(); - - // Assuming level-zero bounds + any (var >= value) in the input map, - // fills "output" with a "propagated" set of bounds assuming lit is true (by - // using the relations enforced by lit, as well as the non-enforced ones). - // Note that we will only fill bounds > level-zero ones in output. - // - // Returns false if the new bounds are infeasible at level zero. - // - // Important: by default this does not call output->clear() so we can take - // the max with already inferred bounds. - bool PropagateLocalBounds( - const IntegerTrail& integer_trail, Literal lit, - const absl::flat_hash_map& input, - absl::flat_hash_map* output) const; - - private: - bool is_built_ = false; - int num_enforced_relations_ = 0; - std::vector relations_; - CompactVectorVector lit_to_relations_; - CompactVectorVector var_to_relations_; - absl::flat_hash_map, - std::vector> - var_pair_to_relations_; -}; - -// TODO(user): Merge with BinaryRelationRepository. Note that this one provides -// different indexing though, so it could be kept separate. The -// LinearExpression2 data structure is also slightly more efficient. -class BinaryRelationsMaps { - public: - explicit BinaryRelationsMaps(Model* model); - ~BinaryRelationsMaps(); - - // This mainly wraps BestBinaryRelationBounds, but in addition it checks the - // current LevelZero variable bounds to detect trivially true or false - // relation. - void AddRelationBounds(LinearExpression2 expr, IntegerValue lb, - IntegerValue ub); - RelationStatus GetLevelZeroStatus(LinearExpression2 expr, IntegerValue lb, - IntegerValue ub) const; - - // Return the status of a <= b; - RelationStatus GetLevelZeroPrecedenceStatus(AffineExpression a, - AffineExpression b) const; - - // Register the fact that l <=> ( a <= b ). - // These are considered equivalence relation. - void AddReifiedPrecedenceIfNonTrivial(Literal l, AffineExpression a, - AffineExpression b); - - // Returns kNoLiteralIndex if we don't have a literal <=> ( a <= b ), or - // returns that literal if we have one. Note that we will return the - // true/false literal if the status is known at level zero. - LiteralIndex GetReifiedPrecedence(AffineExpression a, AffineExpression b); - - // If the given upper bound evaluate better than the current one we have, this - // will replace it and returns true, otherwise it returns false. - // - // Note that we never store trivial upper bound (using the current variable - // domain). - bool AddAffineUpperBound(LinearExpression2 expr, AffineExpression affine_ub); - - // Returns the best known upper-bound of the given LinearExpression2 at the - // current decision level. If its explanation is needed, it can be queried - // with the second function. - IntegerValue UpperBound(LinearExpression2 expr) const; - void AddReasonForUpperBoundLowerThan( - LinearExpression2 expr, IntegerValue ub, - std::vector* literal_reason, - std::vector* integer_reason) const; - - // Warning, the order will not be deterministic. - std::vector GetAllExpressionsWithAffineBounds() const; - - int NumExpressionsWithAffineBounds() const { return best_affine_ub_.size(); } - - void WatchAllLinearExpressions2(int id) { propagator_ids_.insert(id); } - - private: - void NotifyWatchingPropagators() const; - - // Return the pair (a - b) <= rhs. - std::pair FromDifference( - const AffineExpression& a, const AffineExpression& b) const; - - IntegerValue GetImpliedUpperBound(const LinearExpression2& expr) const; - std::pair GetImpliedLevelZeroBounds( - const LinearExpression2& expr) const; - - IntegerTrail* integer_trail_; - IntegerEncoder* integer_encoder_; - GenericLiteralWatcher* watcher_; - SharedStatistics* shared_stats_; - BestBinaryRelationBounds best_root_level_bounds_; - - int64_t num_updates_ = 0; - int64_t num_affine_updates_ = 0; - - // This stores relations l <=> (linear2 <= rhs). - absl::flat_hash_map, Literal> - relation_to_lit_; - - // This is used to detect relations that become fixed at level zero and - // "upgrade" them to non-enforced relations. Because we only do that when - // we fix variable, a linear scan shouldn't be too bad and is relatively - // compact memory wise. - absl::flat_hash_set variable_appearing_in_reified_relations_; - std::vector> - all_reified_relations_; - - // This stores linear2 <= AffineExpression / divisor. - // - // Note(user): This is a "cheap way" to not have to deal with backtracking, If - // we have many possible AffineExpression that bounds a LinearExpression2, we - // keep the best one during "search dive" but on backtrack we might have a - // sub-optimal relation. - absl::flat_hash_map> - best_affine_ub_; - - absl::btree_set propagator_ids_; -}; - -// Detects if at least one of a subset of linear of size 2 or 1, touching the -// same variable, must be true. When this is the case we add a new propagator to -// propagate that fact. -// -// TODO(user): Shall we do that on the main thread before the workers are -// spawned? note that the probing version need the model to be loaded though. -class GreaterThanAtLeastOneOfDetector { - public: - explicit GreaterThanAtLeastOneOfDetector(Model* model) - : repository_(*model->GetOrCreate()) {} - - // Advanced usage. To be called once all the constraints have been added to - // the model. This will detect GreaterThanAtLeastOneOfConstraint(). - // Returns the number of added constraint. - // - // TODO(user): This can be quite slow, add some kind of deterministic limit - // so that we can use it all the time. - int AddGreaterThanAtLeastOneOfConstraints(Model* model, - bool auto_detect_clauses = false); - - private: - // Given an existing clause, sees if it can be used to add "greater than at - // least one of" type of constraints. Returns the number of such constraint - // added. - int AddGreaterThanAtLeastOneOfConstraintsFromClause( - absl::Span clause, Model* model); - - // Another approach for AddGreaterThanAtLeastOneOfConstraints(), this one - // might be a bit slow as it relies on the propagation engine to detect - // clauses between incoming arcs presence literals. - // Returns the number of added constraints. - int AddGreaterThanAtLeastOneOfConstraintsWithClauseAutoDetection( - Model* model); - - // Once we identified a clause and relevant indices, this build the - // constraint. Returns true if we actually add it. - bool AddRelationFromIndices(IntegerVariable var, - absl::Span clause, - absl::Span indices, Model* model); - - BinaryRelationRepository& repository_; -}; - // ============================================================================= // Implementation of the small API functions below. // ============================================================================= @@ -768,43 +971,6 @@ inline void PrecedencesPropagator::AddPrecedenceWithAllOptions( // Model based functions. // ============================================================================= -// a <= b. -inline std::function LowerOrEqual(IntegerVariable a, - IntegerVariable b) { - return [=](Model* model) { - return model->GetOrCreate()->AddPrecedence(a, b); - }; -} - -// a + offset <= b. -inline std::function LowerOrEqualWithOffset(IntegerVariable a, - IntegerVariable b, - int64_t offset) { - return [=](Model* model) { - LinearExpression2 expr(a, b, 1, -1); - model->GetOrCreate()->AddUpperBound( - expr, IntegerValue(-offset)); - model->GetOrCreate()->AddPrecedenceWithOffset( - a, b, IntegerValue(offset)); - }; -} - -// a + offset <= b. (when a and b are of the form 1 * var + offset). -inline std::function AffineCoeffOneLowerOrEqualWithOffset( - AffineExpression a, AffineExpression b, int64_t offset) { - CHECK_NE(a.var, kNoIntegerVariable); - CHECK_EQ(a.coeff, 1); - CHECK_NE(b.var, kNoIntegerVariable); - CHECK_EQ(b.coeff, 1); - return [=](Model* model) { - LinearExpression2 expr(a.var, b.var, 1, -1); - model->GetOrCreate()->AddUpperBound( - expr, -a.constant + b.constant - offset); - model->GetOrCreate()->AddPrecedenceWithOffset( - a.var, b.var, a.constant - b.constant + offset); - }; -} - // l => (a + b <= ub). inline void AddConditionalSum2LowerOrEqual( absl::Span enforcement_literals, IntegerVariable a, @@ -812,8 +978,8 @@ inline void AddConditionalSum2LowerOrEqual( // TODO(user): Refactor to be sure we do not miss any level zero relations. if (enforcement_literals.empty()) { LinearExpression2 expr(a, b, 1, 1); - model->GetOrCreate()->AddUpperBound(expr, - IntegerValue(ub)); + model->GetOrCreate()->AddUpperBound( + expr, IntegerValue(ub)); } PrecedencesPropagator* p = model->GetOrCreate(); @@ -832,34 +998,21 @@ inline void AddConditionalSum3LowerOrEqual( enforcement_literals); } -// a >= b. -inline std::function GreaterOrEqual(IntegerVariable a, - IntegerVariable b) { - return [=](Model* model) { - return model->GetOrCreate()->AddPrecedence(b, a); - }; -} - // a == b. +// +// ABSL_DEPRECATED("Use linear constraint API instead") inline std::function Equality(IntegerVariable a, IntegerVariable b) { return [=](Model* model) { - model->Add(LowerOrEqual(a, b)); - model->Add(LowerOrEqual(b, a)); - }; -} - -// a + offset == b. -inline std::function EqualityWithOffset(IntegerVariable a, - IntegerVariable b, - int64_t offset) { - return [=](Model* model) { - model->Add(LowerOrEqualWithOffset(a, b, offset)); - model->Add(LowerOrEqualWithOffset(b, a, -offset)); + auto* precedences = model->GetOrCreate(); + precedences->AddPrecedence(a, b); + precedences->AddPrecedence(b, a); }; } // is_le => (a + offset <= b). +// +// ABSL_DEPRECATED("Use linear constraint API instead") inline std::function ConditionalLowerOrEqualWithOffset( IntegerVariable a, IntegerVariable b, int64_t offset, Literal is_le) { return [=](Model* model) { @@ -868,6 +1021,58 @@ inline std::function ConditionalLowerOrEqualWithOffset( }; } +inline LinearExpression2Index Linear2WithPotentialNonTrivalBounds::GetIndex( + LinearExpression2 expr) const { + DCHECK(expr.IsCanonicalized()); + DCHECK_EQ(expr.DivideByGcd(), 1); + const bool negated = expr.NegateForCanonicalization(); + auto it = expr_to_index_.find(expr); + if (it == expr_to_index_.end()) return kNoLinearExpression2Index; + + const LinearExpression2Index positive_index(2 * it->second); + if (negated) { + return NegationOf(positive_index); + } else { + return positive_index; + } +} + +inline LinearExpression2 Linear2WithPotentialNonTrivalBounds::GetExpression( + LinearExpression2Index index) const { + DCHECK_NE(index, kNoLinearExpression2Index); + const int lookup_index = index.value() / 2; + DCHECK_LT(lookup_index, exprs_.size()); + if (Linear2IsPositive(index)) { + return exprs_[lookup_index]; + } else { + LinearExpression2 result = exprs_[lookup_index]; + result.Negate(); + return result; + } +} + +inline absl::Span +Linear2WithPotentialNonTrivalBounds::GetAllLinear2ContainingVariable( + IntegerVariable var) const { + const IntegerVariable positive_var = PositiveVariable(var); + auto it = var_to_bounds_.find(positive_var); + if (it == var_to_bounds_.end()) return {}; + return it->second; +} + +inline absl::Span +Linear2WithPotentialNonTrivalBounds::GetAllLinear2ContainingVariables( + IntegerVariable var1, IntegerVariable var2) const { + IntegerVariable positive_var1 = PositiveVariable(var1); + IntegerVariable positive_var2 = PositiveVariable(var2); + if (positive_var1 > positive_var2) { + std::swap(positive_var1, positive_var2); + } + auto it = var_pair_to_bounds_.find({positive_var1, positive_var2}); + if (it == var_pair_to_bounds_.end()) return {}; + return it->second; +} + } // namespace sat } // namespace operations_research diff --git a/ortools/sat/precedences_test.cc b/ortools/sat/precedences_test.cc index 781781d5f28..159469f659e 100644 --- a/ortools/sat/precedences_test.cc +++ b/ortools/sat/precedences_test.cc @@ -14,10 +14,12 @@ #include "ortools/sat/precedences.h" #include +#include #include #include #include "absl/container/flat_hash_map.h" +#include "absl/types/span.h" #include "gtest/gtest.h" #include "ortools/base/gmock.h" #include "ortools/base/parse_test_proto.h" @@ -38,6 +40,7 @@ namespace { using ::google::protobuf::contrib::parse_proto::ParseTestProto; using ::testing::ElementsAre; +using ::testing::FieldsAre; using ::testing::IsEmpty; using ::testing::UnorderedElementsAre; @@ -59,123 +62,136 @@ std::vector AddVariables(IntegerTrail* integer_trail) { return vars; } -TEST(PrecedenceRelationsTest, BasicAPI) { +TEST(EnforcedLinear2BoundsTest, BasicAPI) { Model model; IntegerTrail* integer_trail = model.GetOrCreate(); + auto* root_bounds = model.GetOrCreate(); + auto* precedence_builder = + model.GetOrCreate(); const std::vector vars = AddVariables(integer_trail); // Note that odd indices are for the negation. IntegerVariable a(0), b(2), c(4), d(6); - PrecedenceRelations precedences(&model); - precedences.AddUpperBound(LinearExpression2::Difference(a, b), -10); - precedences.AddUpperBound(LinearExpression2::Difference(d, c), -7); - precedences.AddUpperBound(LinearExpression2::Difference(b, d), -5); + root_bounds->AddUpperBound(LinearExpression2::Difference(a, b), -10); + root_bounds->AddUpperBound(LinearExpression2::Difference(d, c), -7); + root_bounds->AddUpperBound(LinearExpression2::Difference(b, d), -5); - precedences.Build(); + precedence_builder->Build(); EXPECT_EQ( - precedences.LevelZeroUpperBound(LinearExpression2::Difference(a, b)), + root_bounds->LevelZeroUpperBound(LinearExpression2::Difference(a, b)), -10); - EXPECT_EQ(precedences.LevelZeroUpperBound( + EXPECT_EQ(root_bounds->LevelZeroUpperBound( LinearExpression2::Difference(NegationOf(b), NegationOf(a))), -10); EXPECT_EQ( - precedences.LevelZeroUpperBound(LinearExpression2::Difference(a, c)), + root_bounds->LevelZeroUpperBound(LinearExpression2::Difference(a, c)), -22); - EXPECT_EQ(precedences.LevelZeroUpperBound( + EXPECT_EQ(root_bounds->LevelZeroUpperBound( LinearExpression2::Difference(NegationOf(c), NegationOf(a))), -22); EXPECT_EQ( - precedences.LevelZeroUpperBound(LinearExpression2::Difference(a, d)), + root_bounds->LevelZeroUpperBound(LinearExpression2::Difference(a, d)), -15); - EXPECT_EQ(precedences.LevelZeroUpperBound( + EXPECT_EQ(root_bounds->LevelZeroUpperBound( LinearExpression2::Difference(NegationOf(d), NegationOf(a))), -15); EXPECT_EQ( - precedences.LevelZeroUpperBound(LinearExpression2::Difference(d, a)), - kMaxIntegerValue); + root_bounds->LevelZeroUpperBound(LinearExpression2::Difference(d, a)), + 100); // Once built, we can update the offsets. // Note however that this would not propagate through the precedence graphs. - precedences.AddUpperBound(LinearExpression2::Difference(a, b), -15); + root_bounds->AddUpperBound(LinearExpression2::Difference(a, b), -15); EXPECT_EQ( - precedences.LevelZeroUpperBound(LinearExpression2::Difference(a, b)), + root_bounds->LevelZeroUpperBound(LinearExpression2::Difference(a, b)), -15); - EXPECT_EQ(precedences.LevelZeroUpperBound( + EXPECT_EQ(root_bounds->LevelZeroUpperBound( LinearExpression2::Difference(NegationOf(b), NegationOf(a))), -15); } -TEST(PrecedenceRelationsTest, CornerCase1) { +TEST(EnforcedLinear2BoundsTest, CornerCase1) { Model model; IntegerTrail* integer_trail = model.GetOrCreate(); + auto* root_bounds = model.GetOrCreate(); + auto* precedence_builder = + model.GetOrCreate(); const std::vector vars = AddVariables(integer_trail); // Note that odd indices are for the negation. IntegerVariable a(0), b(2), c(4), d(6); - PrecedenceRelations precedences(&model); - precedences.AddUpperBound(LinearExpression2::Difference(a, b), -10); - precedences.AddUpperBound(LinearExpression2::Difference(b, c), -7); - precedences.AddUpperBound(LinearExpression2::Difference(b, d), -5); - precedences.AddUpperBound(LinearExpression2::Difference(NegationOf(b), a), - -5); + root_bounds->AddUpperBound(LinearExpression2::Difference(a, b), -10); + root_bounds->AddUpperBound(LinearExpression2::Difference(b, c), -7); + root_bounds->AddUpperBound(LinearExpression2::Difference(b, d), -5); + root_bounds->AddUpperBound(LinearExpression2::Difference(NegationOf(b), a), + -5); - precedences.Build(); - EXPECT_EQ(precedences.LevelZeroUpperBound( + precedence_builder->Build(); + EXPECT_EQ(root_bounds->LevelZeroUpperBound( LinearExpression2::Difference(NegationOf(b), a)), -5); - EXPECT_EQ(precedences.LevelZeroUpperBound( + EXPECT_EQ(root_bounds->LevelZeroUpperBound( LinearExpression2::Difference(NegationOf(b), c)), -22); - EXPECT_EQ(precedences.LevelZeroUpperBound( + EXPECT_EQ(root_bounds->LevelZeroUpperBound( LinearExpression2::Difference(NegationOf(b), d)), -20); } -TEST(PrecedenceRelationsTest, CornerCase2) { +TEST(EnforcedLinear2BoundsTest, CornerCase2) { Model model; IntegerTrail* integer_trail = model.GetOrCreate(); + auto* root_bounds = model.GetOrCreate(); + auto* precedence_builder = + model.GetOrCreate(); const std::vector vars = AddVariables(integer_trail); // Note that odd indices are for the negation. IntegerVariable a(0), b(2), c(4), d(6); - PrecedenceRelations precedences(&model); - precedences.AddUpperBound(LinearExpression2::Difference(NegationOf(a), a), - -10); - precedences.AddUpperBound(LinearExpression2::Difference(a, b), -7); - precedences.AddUpperBound(LinearExpression2::Difference(a, c), -5); - precedences.AddUpperBound(LinearExpression2::Difference(a, d), -2); - EXPECT_EQ(precedences.LevelZeroUpperBound( + root_bounds->AddUpperBound(LinearExpression2::Difference(NegationOf(a), a), + -10); + root_bounds->AddUpperBound(LinearExpression2::Difference(a, b), -7); + root_bounds->AddUpperBound(LinearExpression2::Difference(a, c), -5); + root_bounds->AddUpperBound(LinearExpression2::Difference(a, d), -2); + EXPECT_EQ(root_bounds->LevelZeroUpperBound( LinearExpression2::Difference(NegationOf(b), NegationOf(a))), -7); - precedences.Build(); + precedence_builder->Build(); } -TEST(PrecedenceRelationsTest, CoefficientGreaterThanOne) { +TEST(EnforcedLinear2BoundsTest, CoefficientGreaterThanOne) { Model model; IntegerTrail* integer_trail = model.GetOrCreate(); + auto* root_bounds = model.GetOrCreate(); + auto* precedence_builder = + model.GetOrCreate(); const std::vector vars = AddVariables(integer_trail); // Note that odd indices are for the negation. IntegerVariable a(0), b(2), c(4); - PrecedenceRelations precedences(&model); - precedences.AddUpperBound(LinearExpression2(a, b, 3, -4), 7); - precedences.AddUpperBound(LinearExpression2(a, c, 2, -3), -5); - precedences.AddUpperBound(LinearExpression2(a, b, 6, -8), 5); - EXPECT_EQ(precedences.LevelZeroUpperBound(LinearExpression2(a, b, 9, -12)), + EnforcedLinear2Bounds precedences(&model); + root_bounds->AddUpperBound(LinearExpression2(a, b, 3, -4), 7); + root_bounds->AddUpperBound(LinearExpression2(a, c, 2, -3), -5); + root_bounds->AddUpperBound(LinearExpression2(a, b, 6, -8), 5); + EXPECT_EQ(root_bounds->LevelZeroUpperBound(LinearExpression2(a, b, 9, -12)), 6); - precedences.Build(); + precedence_builder->Build(); } -TEST(PrecedenceRelationsTest, ConditionalRelations) { +TEST(EnforcedLinear2BoundsTest, ConditionalRelations) { Model model; auto* sat_solver = model.GetOrCreate(); + auto* lin2_bounds = model.GetOrCreate(); auto* integer_trail = model.GetOrCreate(); + auto* precedences = model.GetOrCreate(); + auto* non_trivial_bounds = + model.GetOrCreate(); const std::vector vars = AddVariables(integer_trail); const Literal l(model.Add(NewBooleanVariable()), true); @@ -183,30 +199,28 @@ TEST(PrecedenceRelationsTest, ConditionalRelations) { // Note that odd indices are for the negation. IntegerVariable a(0), b(2); - PrecedenceRelations precedences(&model); - precedences.PushConditionalRelation({l}, LinearExpression2(a, b, 1, 1), 15); - precedences.PushConditionalRelation({l}, LinearExpression2(a, b, 1, 1), 20); + precedences->PushConditionalRelation({l}, LinearExpression2(a, b, 1, 1), 15); + precedences->PushConditionalRelation({l}, LinearExpression2(a, b, 1, 1), 20); + LinearExpression2 expr_a_plus_b = + LinearExpression2::Difference(a, NegationOf(b)); + expr_a_plus_b.SimpleCanonicalization(); // We only keep the best one. - EXPECT_EQ( - precedences.UpperBound(LinearExpression2::Difference(a, NegationOf(b))), - 15); + EXPECT_EQ(lin2_bounds->UpperBound(expr_a_plus_b), 15); std::vector literal_reason; std::vector integer_reason; - precedences.AddReasonForUpperBoundLowerThan( - LinearExpression2::Difference(a, NegationOf(b)), 15, &literal_reason, + precedences->AddReasonForUpperBoundLowerThan( + non_trivial_bounds->AddOrGet(expr_a_plus_b), 15, &literal_reason, &integer_reason); EXPECT_THAT(literal_reason, ElementsAre(l.Negated())); // Backtrack works. EXPECT_TRUE(sat_solver->ResetToLevelZero()); - EXPECT_EQ( - precedences.UpperBound(LinearExpression2::Difference(a, NegationOf(b))), - kMaxIntegerValue); + EXPECT_EQ(lin2_bounds->UpperBound(expr_a_plus_b), 200); literal_reason.clear(); integer_reason.clear(); - precedences.AddReasonForUpperBoundLowerThan( - LinearExpression2::Difference(a, NegationOf(b)), kMaxIntegerValue, + precedences->AddReasonForUpperBoundLowerThan( + non_trivial_bounds->AddOrGet(expr_a_plus_b), kMaxIntegerValue, &literal_reason, &integer_reason); EXPECT_THAT(literal_reason, IsEmpty()); } @@ -435,8 +449,9 @@ TEST(PrecedencesPropagatorTest, ZeroWeightCycleOnDiscreteDomain) { NewIntegerVariable(Domain::FromValues({3, 6, 9, 14, 16, 18, 20, 35}))); // Add the fact that a == b with two inequalities. - model.Add(LowerOrEqual(a, b)); - model.Add(LowerOrEqual(b, a)); + auto* precedences = model.GetOrCreate(); + precedences->AddPrecedence(a, b); + precedences->AddPrecedence(b, a); // After propagation, we should detect that the only common values fall in // [16, 20]. @@ -455,7 +470,8 @@ TEST(PrecedencesPropagatorTest, ConditionalPrecedencesOnFixedLiteral) { // To trigger the old bug, we need to add some precedences. IntegerVariable x = model.Add(NewIntegerVariable(0, 100)); IntegerVariable y = model.Add(NewIntegerVariable(50, 100)); - model.Add(LowerOrEqual(x, y)); + auto* precedences = model.GetOrCreate(); + precedences->AddPrecedence(x, y); // We then add a Boolean variable and fix it. // This will trigger a propagation. @@ -472,26 +488,27 @@ TEST(PrecedencesPropagatorTest, ConditionalPrecedencesOnFixedLiteral) { #undef EXPECT_BOUNDS_EQ -TEST(PrecedenceRelationsTest, CollectPrecedences) { +TEST(EnforcedLinear2BoundsTest, CollectPrecedences) { Model model; auto* integer_trail = model.GetOrCreate(); - auto* relations = model.GetOrCreate(); + auto* relations = model.GetOrCreate(); + auto* root_bounds = model.GetOrCreate(); std::vector vars = AddVariables(integer_trail); - relations->AddUpperBound(LinearExpression2::Difference(vars[0], vars[2]), - IntegerValue(-1)); - relations->AddUpperBound(LinearExpression2::Difference(vars[0], vars[5]), - IntegerValue(-1)); - relations->AddUpperBound(LinearExpression2::Difference(vars[1], vars[2]), - IntegerValue(-1)); - relations->AddUpperBound(LinearExpression2::Difference(vars[2], vars[4]), - IntegerValue(-1)); - relations->AddUpperBound(LinearExpression2::Difference(vars[3], vars[4]), - IntegerValue(-1)); - relations->AddUpperBound(LinearExpression2::Difference(vars[4], vars[5]), - IntegerValue(-1)); - - std::vector p; + root_bounds->AddUpperBound(LinearExpression2::Difference(vars[0], vars[2]), + IntegerValue(-1)); + root_bounds->AddUpperBound(LinearExpression2::Difference(vars[0], vars[5]), + IntegerValue(-1)); + root_bounds->AddUpperBound(LinearExpression2::Difference(vars[1], vars[2]), + IntegerValue(-1)); + root_bounds->AddUpperBound(LinearExpression2::Difference(vars[2], vars[4]), + IntegerValue(-1)); + root_bounds->AddUpperBound(LinearExpression2::Difference(vars[3], vars[4]), + IntegerValue(-1)); + root_bounds->AddUpperBound(LinearExpression2::Difference(vars[4], vars[5]), + IntegerValue(-1)); + + std::vector p; relations->CollectPrecedences({vars[0], vars[2], vars[3]}, &p); // Note that we do not return precedences with just one variable. @@ -511,46 +528,76 @@ TEST(PrecedenceRelationsTest, CollectPrecedences) { TEST(BinaryRelationRepositoryTest, Build) { Model model; - const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); - const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); - const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); + const IntegerVariable x = model.Add(NewIntegerVariable(-100, 100)); + const IntegerVariable y = model.Add(NewIntegerVariable(-100, 100)); + const IntegerVariable z = model.Add(NewIntegerVariable(-100, 100)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true); BinaryRelationRepository repository; - repository.Add(lit_a, {NegationOf(x), 1}, {y, 1}, 2, 8); - repository.Add(Literal(kNoLiteralIndex), {x, 2}, {y, -2}, 0, 10); - repository.Add(lit_a, {x, -3}, {NegationOf(y), 2}, 1, 15); - repository.Add(lit_b, {x, -3}, {kNoIntegerVariable, 0}, 3, 5); - repository.Add(Literal(kNoLiteralIndex), {x, 3}, {y, -1}, 5, 15); - repository.Add(Literal(kNoLiteralIndex), {x, 1}, {z, -1}, 0, 10); + RootLevelLinear2Bounds* root_level_bounds = + model.GetOrCreate(); + repository.Add(lit_a, LinearExpression2(NegationOf(x), y, 1, 1), 2, 8); + root_level_bounds->Add(LinearExpression2(x, y, 2, -2), 0, 10); + repository.Add(lit_a, LinearExpression2(x, NegationOf(y), -3, 2), 1, 15); + repository.Add(lit_b, LinearExpression2(x, kNoIntegerVariable, -3, 0), 3, 5); + root_level_bounds->Add(LinearExpression2(x, y, 3, -1), 5, 15); + root_level_bounds->Add(LinearExpression2::Difference(x, z), 0, 10); repository.AddPartialRelation(lit_b, x, z); repository.Build(); - EXPECT_EQ(repository.size(), 7); - EXPECT_EQ(repository.relation(0), (Relation{lit_a, {x, -1}, {y, 1}, 2, 8})); - EXPECT_EQ(repository.relation(1), - (Relation{Literal(kNoLiteralIndex), {x, 2}, {y, -2}, 0, 10})); - EXPECT_EQ(repository.relation(2), (Relation{lit_a, {x, -3}, {y, -2}, 1, 15})); - EXPECT_EQ(repository.relation(3), - (Relation{lit_b, {x, -3}, {kNoIntegerVariable, 0}, 3, 5})); - EXPECT_EQ(repository.relation(6), (Relation{lit_b, {x, 1}, {z, 1}, 0, 0})); - EXPECT_THAT(repository.IndicesOfRelationsEnforcedBy(lit_a), - UnorderedElementsAre(0, 2)); - EXPECT_THAT(repository.IndicesOfRelationsEnforcedBy(lit_b), - UnorderedElementsAre(3, 6)); - EXPECT_THAT(repository.IndicesOfRelationsContaining(x), - UnorderedElementsAre(1, 4, 5)); - EXPECT_THAT(repository.IndicesOfRelationsContaining(y), - UnorderedElementsAre(1, 4)); - EXPECT_THAT(repository.IndicesOfRelationsContaining(z), - UnorderedElementsAre(5)); - EXPECT_THAT(repository.IndicesOfRelationsBetween(x, y), - UnorderedElementsAre(1, 4)); - EXPECT_THAT(repository.IndicesOfRelationsBetween(y, x), - UnorderedElementsAre(1, 4)); - EXPECT_THAT(repository.IndicesOfRelationsBetween(x, z), - UnorderedElementsAre(5)); - EXPECT_THAT(repository.IndicesOfRelationsBetween(z, y), IsEmpty()); + auto get_rel = [&](absl::Span indexes) { + std::vector result; + for (int i : indexes) { + result.push_back(repository.relation(i)); + } + return result; + }; + std::vector all(repository.size()); + std::iota(all.begin(), all.end(), 0); + EXPECT_THAT( + get_rel(all), + UnorderedElementsAre( + Relation{lit_a, LinearExpression2(x, y, -1, 1), 2, 8}, + Relation{lit_a, LinearExpression2(x, y, -3, -2), 1, 15}, + Relation{lit_b, LinearExpression2(kNoIntegerVariable, x, 0, -3), 3, + 5}, + Relation{lit_b, LinearExpression2(x, z, 1, 1), 0, 0})); + EXPECT_THAT(get_rel(repository.IndicesOfRelationsEnforcedBy(lit_a)), + UnorderedElementsAre( + Relation{lit_a, LinearExpression2(x, y, -1, 1), 2, 8}, + Relation{lit_a, LinearExpression2(x, y, -3, -2), 1, 15})); + EXPECT_THAT( + get_rel(repository.IndicesOfRelationsEnforcedBy(lit_b)), + UnorderedElementsAre( + Relation{lit_b, LinearExpression2(kNoIntegerVariable, x, 0, -3), 3, + 5}, + Relation{lit_b, LinearExpression2(x, z, 1, 1), 0, 0})); + EXPECT_THAT(root_level_bounds->GetAllBoundsContainingVariable(x), + UnorderedElementsAre( + FieldsAre(LinearExpression2(x, NegationOf(y), 1, 1), 0, 5), + + FieldsAre(LinearExpression2(x, NegationOf(y), 3, 1), 5, 15), + FieldsAre(LinearExpression2(x, NegationOf(z), 1, 1), 0, 10))); + EXPECT_THAT( + root_level_bounds->GetAllBoundsContainingVariable(y), + UnorderedElementsAre(FieldsAre(LinearExpression2(y, x, -1, 1), 0, 5), + FieldsAre(LinearExpression2(y, x, -1, 3), 5, 15))); + EXPECT_THAT( + root_level_bounds->GetAllBoundsContainingVariable(z), + UnorderedElementsAre(FieldsAre(LinearExpression2(z, x, -1, 1), 0, 10))); + EXPECT_THAT( + root_level_bounds->GetAllBoundsContainingVariables(x, y), + UnorderedElementsAre(FieldsAre(LinearExpression2(x, y, 1, -1), 0, 5), + FieldsAre(LinearExpression2(x, y, 3, -1), 5, 15))); + EXPECT_THAT( + root_level_bounds->GetAllBoundsContainingVariables(y, x), + UnorderedElementsAre(FieldsAre(LinearExpression2(y, x, -1, 1), 0, 5), + FieldsAre(LinearExpression2(y, x, -1, 3), 5, 15))); + EXPECT_THAT( + root_level_bounds->GetAllBoundsContainingVariables(x, z), + UnorderedElementsAre(FieldsAre(LinearExpression2(x, z, 1, -1), 0, 10))); + EXPECT_THAT(root_level_bounds->GetAllBoundsContainingVariables(z, y), + IsEmpty()); } std::vector GetRelations(Model& model) { @@ -559,12 +606,11 @@ std::vector GetRelations(Model& model) { std::vector relations; for (int i = 0; i < repository.size(); ++i) { Relation r = repository.relation(i); - if (r.a.coeff < 0) { + if (r.expr.coeffs[0] < 0) { r = Relation({r.enforcement, - {r.a.var, -r.a.coeff}, - {r.b.var, -r.b.coeff}, - -r.rhs, - -r.lhs}); + LinearExpression2(r.expr.vars[0], r.expr.vars[1], + -r.expr.coeffs[0], -r.expr.coeffs[1]), + -r.rhs, -r.lhs}); } relations.push_back(r); } @@ -622,22 +668,16 @@ TEST(BinaryRelationRepositoryTest, LoadCpModelAddUnaryAndBinaryRelations) { LoadCpModel(model_proto, &model); const CpModelMapping& mapping = *model.GetOrCreate(); - EXPECT_THAT(GetRelations(model), - UnorderedElementsAre(Relation{mapping.Literal(0), - {mapping.Integer(2), 1}, - {mapping.Integer(3), -1}, - 0, - 10}, - Relation{mapping.Literal(1), - {mapping.Integer(2), 1}, - {kNoIntegerVariable, 0}, - 5, - 10}, - Relation{Literal(kNoLiteralIndex), - {mapping.Integer(2), 3}, - {mapping.Integer(3), -2}, - -10, - 10})); + EXPECT_THAT( + GetRelations(model), + UnorderedElementsAre(Relation{mapping.Literal(0), + LinearExpression2::Difference( + mapping.Integer(2), mapping.Integer(3)), + 0, 10}, + Relation{mapping.Literal(1), + LinearExpression2(kNoIntegerVariable, + mapping.Integer(2), 0, 1), + 5, 10})); } TEST(BinaryRelationRepositoryTest, @@ -672,8 +712,10 @@ TEST(BinaryRelationRepositoryTest, // - b => x - 10.a in [10, 90] EXPECT_THAT(GetRelations(model), UnorderedElementsAre( - Relation{mapping.Literal(0), {x, 1}, {b, -10}, 10, 90}, - Relation{mapping.Literal(1), {x, 1}, {a, -10}, 10, 90})); + Relation{mapping.Literal(0), LinearExpression2(b, x, 10, -1), + -90, -10}, + Relation{mapping.Literal(1), LinearExpression2(a, x, 10, -1), + -90, -10})); } TEST(BinaryRelationRepositoryTest, @@ -706,10 +748,12 @@ TEST(BinaryRelationRepositoryTest, // Two binary relations enforced by only one literal should be added: // - a => x + 10.b in [10, 90] // - b => x + 10.a in [10, 90] - EXPECT_THAT(GetRelations(model), - UnorderedElementsAre( - Relation{mapping.Literal(0), {x, 1}, {b, 10}, 10, 90}, - Relation{mapping.Literal(1), {x, 1}, {a, 10}, 10, 90})); + EXPECT_THAT( + GetRelations(model), + UnorderedElementsAre( + Relation{mapping.Literal(0), LinearExpression2(b, x, 10, 1), 10, 90}, + Relation{mapping.Literal(1), LinearExpression2(a, x, 10, 1), 10, + 90})); } TEST(BinaryRelationRepositoryTest, @@ -745,8 +789,9 @@ TEST(BinaryRelationRepositoryTest, EXPECT_THAT( GetRelations(model), UnorderedElementsAre( - Relation{mapping.Literal(0), {x, 1}, {b, 10}, 20, 100}, - Relation{mapping.Literal(1).Negated(), {x, 1}, {a, -10}, 10, 90})); + Relation{mapping.Literal(0), LinearExpression2(b, x, 10, 1), 20, 100}, + Relation{mapping.Literal(1).Negated(), + LinearExpression2(a, x, 10, -1), -90, -10})); } TEST(BinaryRelationRepositoryTest, @@ -782,8 +827,9 @@ TEST(BinaryRelationRepositoryTest, EXPECT_THAT( GetRelations(model), UnorderedElementsAre( - Relation{mapping.Literal(0), {x, 1}, {b, -10}, 0, 80}, - Relation{mapping.Literal(1).Negated(), {x, 1}, {a, 10}, 10, 90})); + Relation{mapping.Literal(0), LinearExpression2(b, x, 10, -1), -80, 0}, + Relation{mapping.Literal(1).Negated(), LinearExpression2(a, x, 10, 1), + 10, 90})); } TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_EnforcedRelation) { @@ -792,14 +838,17 @@ TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_EnforcedRelation) { const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); BinaryRelationRepository repository; - repository.Add(lit_a, {x, -1}, {y, 1}, 2, 10); // lit_a => y => x + 2 + RootLevelLinear2Bounds* root_level_bounds = + model.GetOrCreate(); + repository.Add(lit_a, LinearExpression2::Difference(y, x), 2, + 10); // lit_a => y => x + 2 repository.Build(); IntegerTrail* integer_trail = model.GetOrCreate(); absl::flat_hash_map input = {{x, 3}}; absl::flat_hash_map output; - const bool result = - repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output); + const bool result = repository.PropagateLocalBounds( + *integer_trail, *root_level_bounds, lit_a, input, &output); EXPECT_TRUE(result); EXPECT_THAT(output, UnorderedElementsAre(std::make_pair(NegationOf(x), -8), @@ -808,43 +857,50 @@ TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_EnforcedRelation) { TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_UnenforcedRelation) { Model model; - const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); - const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); + RootLevelLinear2Bounds* root_level_bounds = + model.GetOrCreate(); + const IntegerVariable x = model.Add(NewIntegerVariable(-100, 100)); + const IntegerVariable y = model.Add(NewIntegerVariable(-100, 100)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); - const Literal kNoLiteral = Literal(kNoLiteralIndex); BinaryRelationRepository repository; - repository.Add(lit_a, {x, -1}, {y, 1}, -5, 10); // lit_a => y => x - 5 - repository.Add(kNoLiteral, {x, -1}, {y, 1}, 2, 10); // y => x + 2 + repository.Add(lit_a, LinearExpression2(x, y, -1, 1), -5, + 10); // lit_a => y => x - 5 + root_level_bounds->Add(LinearExpression2(x, y, -1, 1), 2, + 10); // y => x + 2 repository.Build(); IntegerTrail* integer_trail = model.GetOrCreate(); absl::flat_hash_map input = {{x, 3}}; absl::flat_hash_map output; - const bool result = - repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output); + const bool result = repository.PropagateLocalBounds( + *integer_trail, *root_level_bounds, lit_a, input, &output); EXPECT_TRUE(result); - EXPECT_THAT(output, UnorderedElementsAre(std::make_pair(NegationOf(x), -8), + EXPECT_THAT(output, UnorderedElementsAre(std::make_pair(NegationOf(x), -98), std::make_pair(y, 5))); } TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_EnforcedBoundSmallerThanLevelZeroBound) { Model model; + RootLevelLinear2Bounds* root_level_bounds = + model.GetOrCreate(); const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); const Literal lit_b = Literal(model.Add(NewBooleanVariable()), true); BinaryRelationRepository repository; - repository.Add(lit_a, {x, -1}, {y, 1}, -5, 10); // lit_a => y => x - 5 - repository.Add(lit_b, {x, -1}, {y, 1}, 2, 10); // lit_b => y => x + 2 + repository.Add(lit_a, LinearExpression2::Difference(y, x), -5, + 10); // lit_a => y => x - 5 + repository.Add(lit_b, LinearExpression2::Difference(y, x), 2, + 10); // lit_b => y => x + 2 repository.Build(); IntegerTrail* integer_trail = model.GetOrCreate(); absl::flat_hash_map input = {{x, 3}}; absl::flat_hash_map output; - const bool result = - repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output); + const bool result = repository.PropagateLocalBounds( + *integer_trail, *root_level_bounds, lit_a, input, &output); EXPECT_TRUE(result); EXPECT_THAT(output, IsEmpty()); @@ -857,14 +913,17 @@ TEST(BinaryRelationRepositoryTest, const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); BinaryRelationRepository repository; - repository.Add(lit_a, {x, -1}, {y, 1}, 2, 10); // lit_a => y => x + 2 + RootLevelLinear2Bounds* root_level_bounds = + model.GetOrCreate(); + repository.Add(lit_a, LinearExpression2::Difference(y, x), 2, + 10); // lit_a => y => x + 2 repository.Build(); IntegerTrail* integer_trail = model.GetOrCreate(); absl::flat_hash_map input = {{x, 3}}; absl::flat_hash_map output = {{y, 8}}; - const bool result = - repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output); + const bool result = repository.PropagateLocalBounds( + *integer_trail, *root_level_bounds, lit_a, input, &output); EXPECT_TRUE(result); EXPECT_THAT(output, UnorderedElementsAre(std::make_pair(NegationOf(x), -8), @@ -877,14 +936,17 @@ TEST(BinaryRelationRepositoryTest, PropagateLocalBounds_Infeasible) { const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const Literal lit_a = Literal(model.Add(NewBooleanVariable()), true); BinaryRelationRepository repository; - repository.Add(lit_a, {x, -1}, {y, 1}, 8, 10); // lit_a => y => x + 8 + RootLevelLinear2Bounds* root_level_bounds = + model.GetOrCreate(); + repository.Add(lit_a, LinearExpression2::Difference(y, x), 8, + 10); // lit_a => y => x + 8 repository.Build(); IntegerTrail* integer_trail = model.GetOrCreate(); absl::flat_hash_map input = {{x, 3}}; absl::flat_hash_map output; - const bool result = - repository.PropagateLocalBounds(*integer_trail, lit_a, input, &output); + const bool result = repository.PropagateLocalBounds( + *integer_trail, *root_level_bounds, lit_a, input, &output); EXPECT_FALSE(result); EXPECT_THAT(output, UnorderedElementsAre(std::make_pair(NegationOf(x), -2), @@ -903,9 +965,12 @@ TEST(GreaterThanAtLeastOneOfDetectorTest, AddGreaterThanAtLeastOneOf) { model.Add(ClauseConstraint({lit_a, lit_b, lit_c})); auto* repository = model.GetOrCreate(); - repository->Add(lit_a, {a, -1}, {d, 1}, 2, 1000); // d >= a + 2 - repository->Add(lit_b, {b, -1}, {d, 1}, -1, 1000); // d >= b -1 - repository->Add(lit_c, {c, -1}, {d, 1}, 0, 1000); // d >= c + repository->Add(lit_a, LinearExpression2::Difference(d, a), 2, + 1000); // d >= a + 2 + repository->Add(lit_b, LinearExpression2::Difference(d, b), -1, + 1000); // d >= b -1 + repository->Add(lit_c, LinearExpression2::Difference(d, c), 0, + 1000); // d >= c repository->Build(); auto* detector = model.GetOrCreate(); @@ -931,9 +996,11 @@ TEST(GreaterThanAtLeastOneOfDetectorTest, model.Add(ClauseConstraint({lit_a, lit_b, lit_c})); auto* repository = model.GetOrCreate(); - repository->Add(lit_a, {a, -1}, {d, 1}, 2, 1000); // d >= a + 2 - repository->Add(lit_b, {b, -1}, {d, 1}, -1, 1000); // d >= b -1 - repository->Add(lit_c, {c, -1}, {d, 1}, 0, 1000); // d >= c + repository->Add(lit_a, LinearExpression2(a, d, -1, 1), 2, + 1000); // d >= a + 2 + repository->Add(lit_b, LinearExpression2(b, d, -1, 1), -1, + 1000); // d >= b -1 + repository->Add(lit_c, LinearExpression2(c, d, -1, 1), 0, 1000); // d >= c repository->Build(); auto* detector = model.GetOrCreate(); @@ -947,7 +1014,7 @@ TEST(GreaterThanAtLeastOneOfDetectorTest, EXPECT_EQ(model.Get(LowerBound(d)), std::min({2 + 2, 5 - 1, 3 + 0})); } -TEST(PrecedencesPropagatorTest, ComputeFullPrecedencesIfCycle) { +TEST(TransitivePrecedencesEvaluatorTest, ComputeFullPrecedencesIfCycle) { Model model; std::vector vars(10); for (int i = 0; i < vars.size(); ++i) { @@ -955,18 +1022,19 @@ TEST(PrecedencesPropagatorTest, ComputeFullPrecedencesIfCycle) { } // Even if the weight are compatible, we will fail here. - model.Add(LowerOrEqualWithOffset(vars[0], vars[1], 2)); - model.Add(LowerOrEqualWithOffset(vars[1], vars[2], 2)); - model.Add(LowerOrEqualWithOffset(vars[2], vars[1], -10)); - model.Add(LowerOrEqualWithOffset(vars[0], vars[2], 5)); + auto* r = model.GetOrCreate(); + r->AddUpperBound(LinearExpression2::Difference(vars[0], vars[1]), -2); + r->AddUpperBound(LinearExpression2::Difference(vars[1], vars[2]), -2); + r->AddUpperBound(LinearExpression2::Difference(vars[2], vars[1]), 10); + r->AddUpperBound(LinearExpression2::Difference(vars[0], vars[2]), -5); std::vector precedences; - model.GetOrCreate()->ComputeFullPrecedences( + model.GetOrCreate()->ComputeFullPrecedences( {vars[0], vars[1]}, &precedences); EXPECT_TRUE(precedences.empty()); } -TEST(PrecedencesPropagatorTest, BasicFiltering) { +TEST(TransitivePrecedencesEvaluatorTest, BasicTest1) { Model model; std::vector vars(10); for (int i = 0; i < vars.size(); ++i) { @@ -978,14 +1046,15 @@ TEST(PrecedencesPropagatorTest, BasicFiltering) { // 0 2 -- 4 // \ / // 3 - model.Add(LowerOrEqualWithOffset(vars[0], vars[1], 2)); - model.Add(LowerOrEqualWithOffset(vars[1], vars[2], 2)); - model.Add(LowerOrEqualWithOffset(vars[0], vars[3], 1)); - model.Add(LowerOrEqualWithOffset(vars[3], vars[2], 2)); - model.Add(LowerOrEqualWithOffset(vars[2], vars[4], 2)); + auto* r = model.GetOrCreate(); + r->AddUpperBound(LinearExpression2::Difference(vars[0], vars[1]), -2); + r->AddUpperBound(LinearExpression2::Difference(vars[1], vars[2]), -2); + r->AddUpperBound(LinearExpression2::Difference(vars[0], vars[3]), -1); + r->AddUpperBound(LinearExpression2::Difference(vars[3], vars[2]), -2); + r->AddUpperBound(LinearExpression2::Difference(vars[2], vars[4]), -2); std::vector precedences; - model.GetOrCreate()->ComputeFullPrecedences( + model.GetOrCreate()->ComputeFullPrecedences( {vars[0], vars[1], vars[3]}, &precedences); // We only output size at least 2, and "relevant" precedences. @@ -996,7 +1065,7 @@ TEST(PrecedencesPropagatorTest, BasicFiltering) { EXPECT_THAT(precedences[0].indices, ElementsAre(0, 1, 2)); } -TEST(PrecedencesPropagatorTest, BasicFiltering2) { +TEST(TransitivePrecedencesEvaluatorTest, BasicTest2) { Model model; std::vector vars(10); for (int i = 0; i < vars.size(); ++i) { @@ -1008,15 +1077,16 @@ TEST(PrecedencesPropagatorTest, BasicFiltering2) { // 0 2 -- 4 // \ / / // 3 5 - model.Add(LowerOrEqualWithOffset(vars[0], vars[1], 2)); - model.Add(LowerOrEqualWithOffset(vars[1], vars[2], 2)); - model.Add(LowerOrEqualWithOffset(vars[0], vars[3], 1)); - model.Add(LowerOrEqualWithOffset(vars[3], vars[2], 2)); - model.Add(LowerOrEqualWithOffset(vars[2], vars[4], 2)); - model.Add(LowerOrEqualWithOffset(vars[5], vars[4], 7)); + auto* r = model.GetOrCreate(); + r->AddUpperBound(LinearExpression2::Difference(vars[0], vars[1]), -2); + r->AddUpperBound(LinearExpression2::Difference(vars[1], vars[2]), -2); + r->AddUpperBound(LinearExpression2::Difference(vars[0], vars[3]), -1); + r->AddUpperBound(LinearExpression2::Difference(vars[3], vars[2]), -2); + r->AddUpperBound(LinearExpression2::Difference(vars[2], vars[4]), -2); + r->AddUpperBound(LinearExpression2::Difference(vars[5], vars[4]), -7); std::vector precedences; - model.GetOrCreate()->ComputeFullPrecedences( + model.GetOrCreate()->ComputeFullPrecedences( {vars[0], vars[1], vars[3]}, &precedences); // Same as before here. @@ -1027,7 +1097,7 @@ TEST(PrecedencesPropagatorTest, BasicFiltering2) { // But if we ask for 5, we will get two results. precedences.clear(); - model.GetOrCreate()->ComputeFullPrecedences( + model.GetOrCreate()->ComputeFullPrecedences( {vars[0], vars[1], vars[3], vars[5]}, &precedences); ASSERT_EQ(precedences.size(), 2); EXPECT_EQ(precedences[0].var, vars[2]); @@ -1043,6 +1113,7 @@ TEST(BinaryRelationMapsTest, AffineUpperBound) { const IntegerVariable x = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable z = model.Add(NewIntegerVariable(0, 2)); + const IntegerVariable w = model.Add(NewIntegerVariable(0, 20)); // x - y; LinearExpression2 expr; @@ -1052,35 +1123,41 @@ TEST(BinaryRelationMapsTest, AffineUpperBound) { expr.coeffs[1] = IntegerValue(-1); // Starts with trivial level zero bound. - auto* tested = model.GetOrCreate(); - EXPECT_EQ(tested->UpperBound(expr), IntegerValue(10)); + auto* bounds = model.GetOrCreate(); + auto* lin3_bounds = model.GetOrCreate(); + auto* root_bounds = model.GetOrCreate(); + EXPECT_EQ(bounds->UpperBound(expr), IntegerValue(10)); + + auto* search = model.GetOrCreate(); + search->TakeDecision( + Literal(search->GetDecisionLiteral(BooleanOrIntegerLiteral( + IntegerLiteral::LowerOrEqual(w, IntegerValue(10)))))); // Lets add a relation. - tested->AddRelationBounds(expr, IntegerValue(-5), IntegerValue(5)); - EXPECT_EQ(tested->UpperBound(expr), IntegerValue(5)); + root_bounds->Add(expr, IntegerValue(-5), IntegerValue(5)); + EXPECT_EQ(bounds->UpperBound(expr), IntegerValue(5)); // Note that we canonicalize with gcd. expr.coeffs[0] *= 3; expr.coeffs[1] *= 3; - EXPECT_EQ(tested->UpperBound(expr), IntegerValue(15)); + EXPECT_EQ(bounds->UpperBound(expr), IntegerValue(15)); // Lets add an affine upper bound to that expression <= 4 * z + 1. - EXPECT_TRUE(tested->AddAffineUpperBound( + EXPECT_TRUE(lin3_bounds->AddAffineUpperBound( expr, AffineExpression(z, IntegerValue(4), IntegerValue(1)))); - EXPECT_EQ(tested->UpperBound(expr), IntegerValue(9)); + EXPECT_EQ(bounds->UpperBound(expr), IntegerValue(9)); // Lets test the reason, first push a new bound. - auto* search = model.GetOrCreate(); search->TakeDecision( Literal(search->GetDecisionLiteral(BooleanOrIntegerLiteral( IntegerLiteral::LowerOrEqual(z, IntegerValue(1)))))); // Because of gcd, even though ub(affine) is now 5, we get 3, - EXPECT_EQ(tested->UpperBound(expr), IntegerValue(3)); + EXPECT_EQ(bounds->UpperBound(expr), IntegerValue(3)); { std::vector literal_reason; std::vector integer_reason; - tested->AddReasonForUpperBoundLowerThan(expr, IntegerValue(4), + bounds->AddReasonForUpperBoundLowerThan(expr, IntegerValue(4), &literal_reason, &integer_reason); EXPECT_THAT(literal_reason, ElementsAre()); EXPECT_THAT(integer_reason, @@ -1091,7 +1168,7 @@ TEST(BinaryRelationMapsTest, AffineUpperBound) { { std::vector literal_reason; std::vector integer_reason; - tested->AddReasonForUpperBoundLowerThan(expr, IntegerValue(9), + bounds->AddReasonForUpperBoundLowerThan(expr, IntegerValue(9), &literal_reason, &integer_reason); EXPECT_THAT(literal_reason, ElementsAre()); EXPECT_THAT(integer_reason, @@ -1101,7 +1178,7 @@ TEST(BinaryRelationMapsTest, AffineUpperBound) { // This is implied by the level zero relation x <= 5 std::vector literal_reason; std::vector integer_reason; - tested->AddReasonForUpperBoundLowerThan(expr, IntegerValue(15), + bounds->AddReasonForUpperBoundLowerThan(expr, IntegerValue(15), &literal_reason, &integer_reason); EXPECT_THAT(literal_reason, ElementsAre()); EXPECT_THAT(integer_reason, ElementsAre()); @@ -1110,7 +1187,7 @@ TEST(BinaryRelationMapsTest, AffineUpperBound) { // Note that the bound works on the canonicalized expr. expr.coeffs[0] /= 3; expr.coeffs[1] /= 3; - EXPECT_EQ(tested->UpperBound(expr), IntegerValue(1)); + EXPECT_EQ(bounds->UpperBound(expr), IntegerValue(1)); } } // namespace diff --git a/ortools/sat/python/cp_model.py b/ortools/sat/python/cp_model.py index 0a8780d1c33..0c7f1028b4a 100644 --- a/ortools/sat/python/cp_model.py +++ b/ortools/sat/python/cp_model.py @@ -100,8 +100,12 @@ CHOOSE_FIRST = cp_model_pb2.DecisionStrategyProto.CHOOSE_FIRST CHOOSE_LOWEST_MIN = cp_model_pb2.DecisionStrategyProto.CHOOSE_LOWEST_MIN CHOOSE_HIGHEST_MAX = cp_model_pb2.DecisionStrategyProto.CHOOSE_HIGHEST_MAX -CHOOSE_MIN_DOMAIN_SIZE = cp_model_pb2.DecisionStrategyProto.CHOOSE_MIN_DOMAIN_SIZE -CHOOSE_MAX_DOMAIN_SIZE = cp_model_pb2.DecisionStrategyProto.CHOOSE_MAX_DOMAIN_SIZE +CHOOSE_MIN_DOMAIN_SIZE = ( + cp_model_pb2.DecisionStrategyProto.CHOOSE_MIN_DOMAIN_SIZE +) +CHOOSE_MAX_DOMAIN_SIZE = ( + cp_model_pb2.DecisionStrategyProto.CHOOSE_MAX_DOMAIN_SIZE +) # Domain reduction strategy SELECT_MIN_VALUE = cp_model_pb2.DecisionStrategyProto.SELECT_MIN_VALUE @@ -125,7 +129,9 @@ RANDOMIZED_SEARCH = sat_parameters_pb2.SatParameters.RANDOMIZED_SEARCH # Type aliases -IntegralT = Union[int, np.int8, np.uint8, np.int32, np.uint32, np.int64, np.uint64] +IntegralT = Union[ + int, np.int8, np.uint8, np.int32, np.uint32, np.int64, np.uint64 +] IntegralTypes = ( int, np.int8, @@ -171,2940 +177,2991 @@ def display_bounds(bounds: Sequence[int]) -> str: - """Displays a flattened list of intervals.""" - out = "" - for i in range(0, len(bounds), 2): - if i != 0: - out += ", " - if bounds[i] == bounds[i + 1]: - out += str(bounds[i]) - else: - out += str(bounds[i]) + ".." + str(bounds[i + 1]) - return out + """Displays a flattened list of intervals.""" + out = "" + for i in range(0, len(bounds), 2): + if i != 0: + out += ", " + if bounds[i] == bounds[i + 1]: + out += str(bounds[i]) + else: + out += str(bounds[i]) + ".." + str(bounds[i + 1]) + return out def short_name(model: cp_model_pb2.CpModelProto, i: int) -> str: - """Returns a short name of an integer variable, or its negation.""" - if i < 0: - return f"not({short_name(model, -i - 1)})" - v = model.variables[i] - if v.name: - return v.name - elif len(v.domain) == 2 and v.domain[0] == v.domain[1]: - return str(v.domain[0]) - else: - return f"[{display_bounds(v.domain)}]" + """Returns a short name of an integer variable, or its negation.""" + if i < 0: + return f"not({short_name(model, -i - 1)})" + v = model.variables[i] + if v.name: + return v.name + elif len(v.domain) == 2 and v.domain[0] == v.domain[1]: + return str(v.domain[0]) + else: + return f"[{display_bounds(v.domain)}]" def short_expr_name( model: cp_model_pb2.CpModelProto, e: cp_model_pb2.LinearExpressionProto ) -> str: - """Pretty-print LinearExpressionProto instances.""" - if not e.vars: - return str(e.offset) - if len(e.vars) == 1: - var_name = short_name(model, e.vars[0]) - coeff = e.coeffs[0] - result = "" - if coeff == 1: - result = var_name - elif coeff == -1: - result = f"-{var_name}" - elif coeff != 0: - result = f"{coeff} * {var_name}" - if e.offset > 0: - result = f"{result} + {e.offset}" - elif e.offset < 0: - result = f"{result} - {-e.offset}" - return result - # TODO(user): Support more than affine expressions. - return str(e) + """Pretty-print LinearExpressionProto instances.""" + if not e.vars: + return str(e.offset) + if len(e.vars) == 1: + var_name = short_name(model, e.vars[0]) + coeff = e.coeffs[0] + result = "" + if coeff == 1: + result = var_name + elif coeff == -1: + result = f"-{var_name}" + elif coeff != 0: + result = f"{coeff} * {var_name}" + if e.offset > 0: + result = f"{result} + {e.offset}" + elif e.offset < 0: + result = f"{result} - {-e.offset}" + return result + # TODO(user): Support more than affine expressions. + return str(e) class IntVar(cmh.BaseIntVar): - """An integer variable. + """An integer variable. + + An IntVar is an object that can take on any integer value within defined + ranges. Variables appear in constraint like: + + x + y >= 5 + AllDifferent([x, y, z]) + + Solving a model is equivalent to finding, for each variable, a single value + from the set of initial values (called the initial domain), such that the + model is feasible, or optimal if you provided an objective function. + """ + + def __init__( + self, + model: cp_model_pb2.CpModelProto, + domain: Union[int, sorted_interval_list.Domain], + is_boolean: bool, + name: Optional[str], + ) -> None: + """See CpModel.new_int_var below.""" + self.__model: cp_model_pb2.CpModelProto = model + # Python do not support multiple __init__ methods. + # This method is only called from the CpModel class. + # We hack the parameter to support the two cases: + # case 1: + # model is a CpModelProto, domain is a Domain, and name is a string. + # case 2: + # model is a CpModelProto, domain is an index (int), and name is None. + if isinstance(domain, IntegralTypes) and name is None: + cmh.BaseIntVar.__init__(self, int(domain), is_boolean) + else: + cmh.BaseIntVar.__init__(self, len(model.variables), is_boolean) + proto: cp_model_pb2.IntegerVariableProto = self.__model.variables.add() + proto.domain.extend( + cast(sorted_interval_list.Domain, domain).flattened_intervals() + ) + if name is not None: + proto.name = name + + def __copy__(self) -> "IntVar": + """Returns a shallowcopy of the variable.""" + return IntVar(self.__model, self.index, self.is_boolean, None) + + def __deepcopy__(self, memo: Any) -> "IntVar": + """Returns a deepcopy of the variable.""" + return IntVar( + copy.deepcopy(self.__model, memo), self.index, self.is_boolean, None + ) + + @property + def proto(self) -> cp_model_pb2.IntegerVariableProto: + """Returns the variable protobuf.""" + return self.__model.variables[self.index] + + @property + def model_proto(self) -> cp_model_pb2.CpModelProto: + """Returns the model protobuf.""" + return self.__model + + def is_equal_to(self, other: Any) -> bool: + """Returns true if self == other in the python sense.""" + if not isinstance(other, IntVar): + return False + return self.index == other.index + + def __str__(self) -> str: + if not self.proto.name: + if ( + len(self.proto.domain) == 2 + and self.proto.domain[0] == self.proto.domain[1] + ): + # Special case for constants. + return str(self.proto.domain[0]) + elif self.is_boolean: + return f"BooleanVar({self.__index})" + else: + return f"IntVar({self.__index})" + else: + return self.proto.name - An IntVar is an object that can take on any integer value within defined - ranges. Variables appear in constraint like: + def __repr__(self) -> str: + return f"{self}({display_bounds(self.proto.domain)})" - x + y >= 5 - AllDifferent([x, y, z]) + @property + def name(self) -> str: + if not self.proto or not self.proto.name: + return "" + return self.proto.name - Solving a model is equivalent to finding, for each variable, a single value - from the set of initial values (called the initial domain), such that the - model is feasible, or optimal if you provided an objective function. - """ + # Pre PEP8 compatibility. + # pylint: disable=invalid-name + def Name(self) -> str: + return self.name - def __init__( - self, - model: cp_model_pb2.CpModelProto, - domain: Union[int, sorted_interval_list.Domain], - is_boolean: bool, - name: Optional[str], - ) -> None: - """See CpModel.new_int_var below.""" - self.__model: cp_model_pb2.CpModelProto = model - # Python do not support multiple __init__ methods. - # This method is only called from the CpModel class. - # We hack the parameter to support the two cases: - # case 1: - # model is a CpModelProto, domain is a Domain, and name is a string. - # case 2: - # model is a CpModelProto, domain is an index (int), and name is None. - if isinstance(domain, IntegralTypes) and name is None: - cmh.BaseIntVar.__init__(self, int(domain), is_boolean) - else: - cmh.BaseIntVar.__init__(self, len(model.variables), is_boolean) - proto: cp_model_pb2.IntegerVariableProto = self.__model.variables.add() - proto.domain.extend( - cast(sorted_interval_list.Domain, domain).flattened_intervals() - ) - if name is not None: - proto.name = name + def Proto(self) -> cp_model_pb2.IntegerVariableProto: + return self.proto - def __copy__(self) -> "IntVar": - """Returns a shallowcopy of the variable.""" - return IntVar(self.__model, self.index, self.is_boolean, None) + # pylint: enable=invalid-name - def __deepcopy__(self, memo: Any) -> "IntVar": - """Returns a deepcopy of the variable.""" - return IntVar( - copy.deepcopy(self.__model, memo), self.index, self.is_boolean, None - ) - @property - def proto(self) -> cp_model_pb2.IntegerVariableProto: - """Returns the variable protobuf.""" - return self.__model.variables[self.index] - - @property - def model_proto(self) -> cp_model_pb2.CpModelProto: - """Returns the model protobuf.""" - return self.__model - - def is_equal_to(self, other: Any) -> bool: - """Returns true if self == other in the python sense.""" - if not isinstance(other, IntVar): - return False - return self.index == other.index - - def __str__(self) -> str: - if not self.proto.name: - if ( - len(self.proto.domain) == 2 - and self.proto.domain[0] == self.proto.domain[1] - ): - # Special case for constants. - return str(self.proto.domain[0]) - elif self.is_boolean: - return f"BooleanVar({self.__index})" - else: - return f"IntVar({self.__index})" - else: - return self.proto.name +class Constraint: + """Base class for constraints. - def __repr__(self) -> str: - return f"{self}({display_bounds(self.proto.domain)})" + Constraints are built by the CpModel through the add methods. + Once created by the CpModel class, they are automatically added to the model. + The purpose of this class is to allow specification of enforcement literals + for this constraint. - @property - def name(self) -> str: - if not self.proto or not self.proto.name: - return "" - return self.proto.name + b = model.new_bool_var('b') + x = model.new_int_var(0, 10, 'x') + y = model.new_int_var(0, 10, 'y') - # Pre PEP8 compatibility. - # pylint: disable=invalid-name - def Name(self) -> str: - return self.name + model.add(x + 2 * y == 5).only_enforce_if(b.negated()) + """ - def Proto(self) -> cp_model_pb2.IntegerVariableProto: - return self.proto + def __init__( + self, + cp_model: "CpModel", + ) -> None: + self.__index: int = len(cp_model.proto.constraints) + self.__cp_model: "CpModel" = cp_model + self.__constraint: cp_model_pb2.ConstraintProto = ( + cp_model.proto.constraints.add() + ) - # pylint: enable=invalid-name + @overload + def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": + ... + @overload + def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": + ... -class Constraint: - """Base class for constraints. + def only_enforce_if(self, *boolvar) -> "Constraint": + """Adds an enforcement literal to the constraint. - Constraints are built by the CpModel through the add methods. - Once created by the CpModel class, they are automatically added to the model. - The purpose of this class is to allow specification of enforcement literals - for this constraint. + This method adds one or more literals (that is, a boolean variable or its + negation) as enforcement literals. The conjunction of all these literals + determines whether the constraint is active or not. It acts as an + implication, so if the conjunction is true, it implies that the constraint + must be enforced. If it is false, then the constraint is ignored. - b = model.new_bool_var('b') - x = model.new_int_var(0, 10, 'x') - y = model.new_int_var(0, 10, 'y') + BoolOr, BoolAnd, and linear constraints all support enforcement literals. - model.add(x + 2 * y == 5).only_enforce_if(b.negated()) - """ + Args: + *boolvar: One or more Boolean literals. - def __init__( - self, - cp_model: "CpModel", - ) -> None: - self.__index: int = len(cp_model.proto.constraints) - self.__cp_model: "CpModel" = cp_model - self.__constraint: cp_model_pb2.ConstraintProto = ( - cp_model.proto.constraints.add() + Returns: + self. + """ + for lit in expand_generator_or_tuple(boolvar): + if (cmn.is_boolean(lit) and lit) or ( + isinstance(lit, IntegralTypes) and lit == 1 + ): + # Always true. Do nothing. + pass + elif (cmn.is_boolean(lit) and not lit) or ( + isinstance(lit, IntegralTypes) and lit == 0 + ): + self.__constraint.enforcement_literal.append( + self.__cp_model.new_constant(0).index ) + else: + self.__constraint.enforcement_literal.append( + cast(cmh.Literal, lit).index + ) + return self - @overload - def only_enforce_if(self, boolvar: Iterable[LiteralT]) -> "Constraint": ... - - @overload - def only_enforce_if(self, *boolvar: LiteralT) -> "Constraint": ... - - def only_enforce_if(self, *boolvar) -> "Constraint": - """Adds an enforcement literal to the constraint. - - This method adds one or more literals (that is, a boolean variable or its - negation) as enforcement literals. The conjunction of all these literals - determines whether the constraint is active or not. It acts as an - implication, so if the conjunction is true, it implies that the constraint - must be enforced. If it is false, then the constraint is ignored. - - BoolOr, BoolAnd, and linear constraints all support enforcement literals. - - Args: - *boolvar: One or more Boolean literals. - - Returns: - self. - """ - for lit in expand_generator_or_tuple(boolvar): - if (cmn.is_boolean(lit) and lit) or ( - isinstance(lit, IntegralTypes) and lit == 1 - ): - # Always true. Do nothing. - pass - elif (cmn.is_boolean(lit) and not lit) or ( - isinstance(lit, IntegralTypes) and lit == 0 - ): - self.__constraint.enforcement_literal.append( - self.__cp_model.new_constant(0).index - ) - else: - self.__constraint.enforcement_literal.append( - cast(cmh.Literal, lit).index - ) - return self - - def with_name(self, name: str) -> "Constraint": - """Sets the name of the constraint.""" - if name: - self.__constraint.name = name - else: - self.__constraint.ClearField("name") - return self + def with_name(self, name: str) -> "Constraint": + """Sets the name of the constraint.""" + if name: + self.__constraint.name = name + else: + self.__constraint.ClearField("name") + return self - @property - def name(self) -> str: - """Returns the name of the constraint.""" - if not self.__constraint or not self.__constraint.name: - return "" - return self.__constraint.name + @property + def name(self) -> str: + """Returns the name of the constraint.""" + if not self.__constraint or not self.__constraint.name: + return "" + return self.__constraint.name - @property - def index(self) -> int: - """Returns the index of the constraint in the model.""" - return self.__index + @property + def index(self) -> int: + """Returns the index of the constraint in the model.""" + return self.__index - @property - def proto(self) -> cp_model_pb2.ConstraintProto: - """Returns the constraint protobuf.""" - return self.__constraint + @property + def proto(self) -> cp_model_pb2.ConstraintProto: + """Returns the constraint protobuf.""" + return self.__constraint - # Pre PEP8 compatibility. - # pylint: disable=invalid-name - OnlyEnforceIf = only_enforce_if - WithName = with_name + # Pre PEP8 compatibility. + # pylint: disable=invalid-name + OnlyEnforceIf = only_enforce_if + WithName = with_name - def Name(self) -> str: - return self.name + def Name(self) -> str: + return self.name - def Index(self) -> int: - return self.index + def Index(self) -> int: + return self.index - def Proto(self) -> cp_model_pb2.ConstraintProto: - return self.proto + def Proto(self) -> cp_model_pb2.ConstraintProto: + return self.proto - # pylint: enable=invalid-name + # pylint: enable=invalid-name class VariableList: - """Stores all integer variables of the model.""" - - def __init__(self) -> None: - self.__var_list: list[IntVar] = [] - - def append(self, var: IntVar) -> None: - assert var.index == len(self.__var_list) - self.__var_list.append(var) - - def get(self, index: int) -> IntVar: - if index < 0 or index >= len(self.__var_list): - raise ValueError("Index out of bounds.") - return self.__var_list[index] - - def rebuild_expr( - self, - proto: cp_model_pb2.LinearExpressionProto, - ) -> LinearExprT: - """Recreate a LinearExpr from a LinearExpressionProto.""" - num_elements = len(proto.vars) - if num_elements == 0: - return proto.offset - elif num_elements == 1: - var = self.get(proto.vars[0]) - return LinearExpr.affine( - var, proto.coeffs[0], proto.offset - ) # pytype: disable=bad-return-type - else: - variables = [] - for var_index in range(len(proto.vars)): - var = self.get(var_index) - variables.append(var) - if proto.offset != 0: - coeffs = [] - coeffs.extend(proto.coeffs) - coeffs.append(1) - variables.append(proto.offset) - return LinearExpr.weighted_sum(variables, coeffs) - else: - return LinearExpr.weighted_sum(variables, proto.coeffs) + """Stores all integer variables of the model.""" + + def __init__(self) -> None: + self.__var_list: list[IntVar] = [] + + def append(self, var: IntVar) -> None: + assert var.index == len(self.__var_list) + self.__var_list.append(var) + + def get(self, index: int) -> IntVar: + if index < 0 or index >= len(self.__var_list): + raise ValueError("Index out of bounds.") + return self.__var_list[index] + + def rebuild_expr( + self, + proto: cp_model_pb2.LinearExpressionProto, + ) -> LinearExprT: + """Recreate a LinearExpr from a LinearExpressionProto.""" + num_elements = len(proto.vars) + if num_elements == 0: + return proto.offset + elif num_elements == 1: + var = self.get(proto.vars[0]) + return LinearExpr.affine( + var, proto.coeffs[0], proto.offset + ) # pytype: disable=bad-return-type + else: + variables = [] + for var_index in range(len(proto.vars)): + var = self.get(var_index) + variables.append(var) + if proto.offset != 0: + coeffs = [] + coeffs.extend(proto.coeffs) + coeffs.append(1) + variables.append(proto.offset) + return LinearExpr.weighted_sum(variables, coeffs) + else: + return LinearExpr.weighted_sum(variables, proto.coeffs) class IntervalVar: - """Represents an Interval variable. + """Represents an Interval variable. + + An interval variable is both a constraint and a variable. It is defined by + three integer variables: start, size, and end. + + It is a constraint because, internally, it enforces that start + size == end. + + It is also a variable as it can appear in specific scheduling constraints: + NoOverlap, NoOverlap2D, Cumulative. + + Optionally, an enforcement literal can be added to this constraint, in which + case these scheduling constraints will ignore interval variables with + enforcement literals assigned to false. Conversely, these constraints will + also set these enforcement literals to false if they cannot fit these + intervals into the schedule. + + Raises: + ValueError: if start, size, end are not defined, or have the wrong type. + """ + + def __init__( + self, + model: cp_model_pb2.CpModelProto, + var_list: VariableList, + start: Union[cp_model_pb2.LinearExpressionProto, int], + size: Optional[cp_model_pb2.LinearExpressionProto], + end: Optional[cp_model_pb2.LinearExpressionProto], + is_present_index: Optional[int], + name: Optional[str], + ) -> None: + self.__model: cp_model_pb2.CpModelProto = model + self.__var_list: VariableList = var_list + self.__index: int + self.__ct: cp_model_pb2.ConstraintProto + # As with the IntVar::__init__ method, we hack the __init__ method to + # support two use cases: + # case 1: called when creating a new interval variable. + # {start|size|end} are linear expressions, is_present_index is either + # None or the index of a Boolean literal. name is a string + # case 2: called when querying an existing interval variable. + # start_index is an int, all parameters after are None. + if isinstance(start, int): + if size is not None: + raise ValueError("size should be None") + if end is not None: + raise ValueError("end should be None") + if is_present_index is not None: + raise ValueError("is_present_index should be None") + self.__index = cast(int, start) + self.__ct = model.constraints[self.__index] + else: + self.__index = len(model.constraints) + self.__ct = self.__model.constraints.add() + if start is None: + raise TypeError("start is not defined") + self.__ct.interval.start.CopyFrom(start) + if size is None: + raise TypeError("size is not defined") + self.__ct.interval.size.CopyFrom(size) + if end is None: + raise TypeError("end is not defined") + self.__ct.interval.end.CopyFrom(end) + if is_present_index is not None: + self.__ct.enforcement_literal.append(is_present_index) + if name: + self.__ct.name = name + + @property + def index(self) -> int: + """Returns the index of the interval constraint in the model.""" + return self.__index + + @property + def proto(self) -> cp_model_pb2.ConstraintProto: + """Returns the interval protobuf.""" + return self.__model.constraints[self.__index] + + @property + def model_proto(self) -> cp_model_pb2.CpModelProto: + """Returns the model protobuf.""" + return self.__model + + def __str__(self): + return self.proto.name + + def __repr__(self): + interval = self.proto.interval + if self.proto.enforcement_literal: + return ( + f"{self.proto.name}(start =" + f" {short_expr_name(self.__model, interval.start)}, size =" + f" {short_expr_name(self.__model, interval.size)}, end =" + f" {short_expr_name(self.__model, interval.end)}, is_present =" + f" {short_name(self.__model, self.proto.enforcement_literal[0])})" + ) + else: + return ( + f"{self.proto.name}(start =" + f" {short_expr_name(self.__model, interval.start)}, size =" + f" {short_expr_name(self.__model, interval.size)}, end =" + f" {short_expr_name(self.__model, interval.end)})" + ) - An interval variable is both a constraint and a variable. It is defined by - three integer variables: start, size, and end. + @property + def name(self) -> str: + if not self.proto or not self.proto.name: + return "" + return self.proto.name - It is a constraint because, internally, it enforces that start + size == end. + def start_expr(self) -> LinearExprT: + return self.__var_list.rebuild_expr(self.proto.interval.start) - It is also a variable as it can appear in specific scheduling constraints: - NoOverlap, NoOverlap2D, Cumulative. + def size_expr(self) -> LinearExprT: + return self.__var_list.rebuild_expr(self.proto.interval.size) - Optionally, an enforcement literal can be added to this constraint, in which - case these scheduling constraints will ignore interval variables with - enforcement literals assigned to false. Conversely, these constraints will - also set these enforcement literals to false if they cannot fit these - intervals into the schedule. + def end_expr(self) -> LinearExprT: + return self.__var_list.rebuild_expr(self.proto.interval.end) - Raises: - ValueError: if start, size, end are not defined, or have the wrong type. - """ + # Pre PEP8 compatibility. + # pylint: disable=invalid-name + def Name(self) -> str: + return self.name - def __init__( - self, - model: cp_model_pb2.CpModelProto, - var_list: VariableList, - start: Union[cp_model_pb2.LinearExpressionProto, int], - size: Optional[cp_model_pb2.LinearExpressionProto], - end: Optional[cp_model_pb2.LinearExpressionProto], - is_present_index: Optional[int], - name: Optional[str], - ) -> None: - self.__model: cp_model_pb2.CpModelProto = model - self.__var_list: VariableList = var_list - self.__index: int - self.__ct: cp_model_pb2.ConstraintProto - # As with the IntVar::__init__ method, we hack the __init__ method to - # support two use cases: - # case 1: called when creating a new interval variable. - # {start|size|end} are linear expressions, is_present_index is either - # None or the index of a Boolean literal. name is a string - # case 2: called when querying an existing interval variable. - # start_index is an int, all parameters after are None. - if isinstance(start, int): - if size is not None: - raise ValueError("size should be None") - if end is not None: - raise ValueError("end should be None") - if is_present_index is not None: - raise ValueError("is_present_index should be None") - self.__index = cast(int, start) - self.__ct = model.constraints[self.__index] - else: - self.__index = len(model.constraints) - self.__ct = self.__model.constraints.add() - if start is None: - raise TypeError("start is not defined") - self.__ct.interval.start.CopyFrom(start) - if size is None: - raise TypeError("size is not defined") - self.__ct.interval.size.CopyFrom(size) - if end is None: - raise TypeError("end is not defined") - self.__ct.interval.end.CopyFrom(end) - if is_present_index is not None: - self.__ct.enforcement_literal.append(is_present_index) - if name: - self.__ct.name = name - - @property - def index(self) -> int: - """Returns the index of the interval constraint in the model.""" - return self.__index - - @property - def proto(self) -> cp_model_pb2.ConstraintProto: - """Returns the interval protobuf.""" - return self.__model.constraints[self.__index] - - @property - def model_proto(self) -> cp_model_pb2.CpModelProto: - """Returns the model protobuf.""" - return self.__model - - def __str__(self): - return self.proto.name - - def __repr__(self): - interval = self.proto.interval - if self.proto.enforcement_literal: - return ( - f"{self.proto.name}(start =" - f" {short_expr_name(self.__model, interval.start)}, size =" - f" {short_expr_name(self.__model, interval.size)}, end =" - f" {short_expr_name(self.__model, interval.end)}, is_present =" - f" {short_name(self.__model, self.proto.enforcement_literal[0])})" - ) - else: - return ( - f"{self.proto.name}(start =" - f" {short_expr_name(self.__model, interval.start)}, size =" - f" {short_expr_name(self.__model, interval.size)}, end =" - f" {short_expr_name(self.__model, interval.end)})" - ) + def Index(self) -> int: + return self.index - @property - def name(self) -> str: - if not self.proto or not self.proto.name: - return "" - return self.proto.name + def Proto(self) -> cp_model_pb2.ConstraintProto: + return self.proto - def start_expr(self) -> LinearExprT: - return self.__var_list.rebuild_expr(self.proto.interval.start) + StartExpr = start_expr + SizeExpr = size_expr + EndExpr = end_expr - def size_expr(self) -> LinearExprT: - return self.__var_list.rebuild_expr(self.proto.interval.size) + # pylint: enable=invalid-name - def end_expr(self) -> LinearExprT: - return self.__var_list.rebuild_expr(self.proto.interval.end) - # Pre PEP8 compatibility. - # pylint: disable=invalid-name - def Name(self) -> str: - return self.name +def object_is_a_true_literal(literal: LiteralT) -> bool: + """Checks if literal is either True, or a Boolean literals fixed to True.""" + if isinstance(literal, IntVar): + proto = literal.proto + return ( + len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 + ) + if isinstance(literal, cmh.NotBooleanVariable): + proto = literal.negated().proto + return ( + len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 + ) + if isinstance(literal, IntegralTypes): + return int(literal) == 1 + return False - def Index(self) -> int: - return self.index - def Proto(self) -> cp_model_pb2.ConstraintProto: - return self.proto +def object_is_a_false_literal(literal: LiteralT) -> bool: + """Checks if literal is either False, or a Boolean literals fixed to False.""" + if isinstance(literal, IntVar): + proto = literal.proto + return ( + len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 + ) + if isinstance(literal, cmh.NotBooleanVariable): + proto = literal.negated().proto + return ( + len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 + ) + if isinstance(literal, IntegralTypes): + return int(literal) == 0 + return False - StartExpr = start_expr - SizeExpr = size_expr - EndExpr = end_expr - # pylint: enable=invalid-name +class CpModel: + """Methods for building a CP model. + Methods beginning with: -def object_is_a_true_literal(literal: LiteralT) -> bool: - """Checks if literal is either True, or a Boolean literals fixed to True.""" - if isinstance(literal, IntVar): - proto = literal.proto - return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 - if isinstance(literal, cmh.NotBooleanVariable): - proto = literal.negated().proto - return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 - if isinstance(literal, IntegralTypes): - return int(literal) == 1 - return False + * ```New``` create integer, boolean, or interval variables. + * ```add``` create new constraints and add them to the model. + """ + def __init__(self) -> None: + self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto() + self.__constant_map: Dict[IntegralT, int] = {} + self.__var_list: VariableList = VariableList() -def object_is_a_false_literal(literal: LiteralT) -> bool: - """Checks if literal is either False, or a Boolean literals fixed to False.""" - if isinstance(literal, IntVar): - proto = literal.proto - return len(proto.domain) == 2 and proto.domain[0] == 0 and proto.domain[1] == 0 - if isinstance(literal, cmh.NotBooleanVariable): - proto = literal.negated().proto - return len(proto.domain) == 2 and proto.domain[0] == 1 and proto.domain[1] == 1 - if isinstance(literal, IntegralTypes): - return int(literal) == 0 - return False + # Naming. + @property + def name(self) -> str: + """Returns the name of the model.""" + if not self.__model or not self.__model.name: + return "" + return self.__model.name + @name.setter + def name(self, name: str): + """Sets the name of the model.""" + self.__model.name = name -class CpModel: - """Methods for building a CP model. + # Integer variable. - Methods beginning with: + def _append_int_var(self, var: IntVar) -> IntVar: + """Appends an integer variable to the list of variables.""" + self.__var_list.append(var) + return var - * ```New``` create integer, boolean, or interval variables. - * ```add``` create new constraints and add them to the model. - """ + def _get_int_var(self, index: int) -> IntVar: + return self.__var_list.get(index) - def __init__(self) -> None: - self.__model: cp_model_pb2.CpModelProto = cp_model_pb2.CpModelProto() - self.__constant_map: Dict[IntegralT, int] = {} - self.__var_list: VariableList = VariableList() - - # Naming. - @property - def name(self) -> str: - """Returns the name of the model.""" - if not self.__model or not self.__model.name: - return "" - return self.__model.name - - @name.setter - def name(self, name: str): - """Sets the name of the model.""" - self.__model.name = name - - # Integer variable. - - def _append_int_var(self, var: IntVar) -> IntVar: - """Appends an integer variable to the list of variables.""" - self.__var_list.append(var) - return var - - def _get_int_var(self, index: int) -> IntVar: - return self.__var_list.get(index) - - def rebuild_from_linear_expression_proto( - self, - proto: cp_model_pb2.LinearExpressionProto, - ) -> LinearExpr: - return self.__var_list.rebuild_expr(proto) - - def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: - """Create an integer variable with domain [lb, ub]. - - The CP-SAT solver is limited to integer variables. If you have fractional - values, scale them up so that they become integers; if you have strings, - encode them as integers. - - Args: - lb: Lower bound for the variable. - ub: Upper bound for the variable. - name: The name of the variable. - - Returns: - a variable whose domain is [lb, ub]. - """ - domain_is_boolean = lb >= 0 and ub <= 1 - return self._append_int_var( - IntVar( - self.__model, - sorted_interval_list.Domain(lb, ub), - domain_is_boolean, - name, - ) - ) + def rebuild_from_linear_expression_proto( + self, + proto: cp_model_pb2.LinearExpressionProto, + ) -> LinearExpr: + return self.__var_list.rebuild_expr(proto) - def new_int_var_from_domain( - self, domain: sorted_interval_list.Domain, name: str - ) -> IntVar: - """Create an integer variable from a domain. - - A domain is a set of integers specified by a collection of intervals. - For example, `model.new_int_var_from_domain(cp_model. - Domain.from_intervals([[1, 2], [4, 6]]), 'x')` - - Args: - domain: An instance of the Domain class. - name: The name of the variable. - - Returns: - a variable whose domain is the given domain. - """ - domain_is_boolean = domain.min() >= 0 and domain.max() <= 1 - return self._append_int_var( - IntVar(self.__model, domain, domain_is_boolean, name) - ) + def new_int_var(self, lb: IntegralT, ub: IntegralT, name: str) -> IntVar: + """Create an integer variable with domain [lb, ub]. - def new_bool_var(self, name: str) -> IntVar: - """Creates a 0-1 variable with the given name.""" - return self._append_int_var( - IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name) - ) + The CP-SAT solver is limited to integer variables. If you have fractional + values, scale them up so that they become integers; if you have strings, + encode them as integers. - def new_constant(self, value: IntegralT) -> IntVar: - """Declares a constant integer.""" - index: int = self.get_or_make_index_from_constant(value) - return self._get_int_var(index) - - def new_int_var_series( - self, - name: str, - index: pd.Index, - lower_bounds: Union[IntegralT, pd.Series], - upper_bounds: Union[IntegralT, pd.Series], - ) -> pd.Series: - """Creates a series of (scalar-valued) variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - lower_bounds (Union[int, pd.Series]): A lower bound for variables in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - upper_bounds (Union[int, pd.Series]): An upper bound for variables in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - - Returns: - pd.Series: The variable set indexed by its corresponding dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - ValueError: if the `lowerbound` is greater than the `upperbound`. - ValueError: if the index of `lower_bound`, or `upper_bound` does not match - the input index. - """ - if not isinstance(index, pd.Index): - raise TypeError("Non-index object is used as index") - if not name.isidentifier(): - raise ValueError(f"name={name!r} is not a valid identifier") - if ( - isinstance(lower_bounds, IntegralTypes) - and isinstance(upper_bounds, IntegralTypes) - and lower_bounds > upper_bounds - ): - raise ValueError( - f"lower_bound={lower_bounds} is greater than" - f" upper_bound={upper_bounds} for variable set={name}" - ) + Args: + lb: Lower bound for the variable. + ub: Upper bound for the variable. + name: The name of the variable. - lower_bounds = _convert_to_integral_series_and_validate_index( - lower_bounds, index - ) - upper_bounds = _convert_to_integral_series_and_validate_index( - upper_bounds, index - ) - return pd.Series( - index=index, - data=[ - # pylint: disable=g-complex-comprehension - self._append_int_var( - IntVar( - model=self.__model, - name=f"{name}[{i}]", - domain=sorted_interval_list.Domain( - lower_bounds[i], upper_bounds[i] - ), - is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1, - ) - ) - for i in index - ], + Returns: + a variable whose domain is [lb, ub]. + """ + domain_is_boolean = lb >= 0 and ub <= 1 + return self._append_int_var( + IntVar( + self.__model, + sorted_interval_list.Domain(lb, ub), + domain_is_boolean, + name, ) + ) + + def new_int_var_from_domain( + self, domain: sorted_interval_list.Domain, name: str + ) -> IntVar: + """Create an integer variable from a domain. + + A domain is a set of integers specified by a collection of intervals. + For example, `model.new_int_var_from_domain(cp_model. + Domain.from_intervals([[1, 2], [4, 6]]), 'x')` + + Args: + domain: An instance of the Domain class. + name: The name of the variable. + + Returns: + a variable whose domain is the given domain. + """ + domain_is_boolean = domain.min() >= 0 and domain.max() <= 1 + return self._append_int_var( + IntVar(self.__model, domain, domain_is_boolean, name) + ) + + def new_bool_var(self, name: str) -> IntVar: + """Creates a 0-1 variable with the given name.""" + return self._append_int_var( + IntVar(self.__model, sorted_interval_list.Domain(0, 1), True, name) + ) + + def new_constant(self, value: IntegralT) -> IntVar: + """Declares a constant integer.""" + index: int = self.get_or_make_index_from_constant(value) + return self._get_int_var(index) + + def new_int_var_series( + self, + name: str, + index: pd.Index, + lower_bounds: Union[IntegralT, pd.Series], + upper_bounds: Union[IntegralT, pd.Series], + ) -> pd.Series: + """Creates a series of (scalar-valued) variables with the given name. + + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. + lower_bounds (Union[int, pd.Series]): A lower bound for variables in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. + upper_bounds (Union[int, pd.Series]): An upper bound for variables in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. - def new_bool_var_series( - self, - name: str, - index: pd.Index, - ) -> pd.Series: - """Creates a series of (scalar-valued) variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - - Returns: - pd.Series: The variable set indexed by its corresponding dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - """ - if not isinstance(index, pd.Index): - raise TypeError("Non-index object is used as index") - if not name.isidentifier(): - raise ValueError(f"name={name!r} is not a valid identifier") - return pd.Series( - index=index, - data=[ - # pylint: disable=g-complex-comprehension - self._append_int_var( - IntVar( - model=self.__model, - name=f"{name}[{i}]", - domain=sorted_interval_list.Domain(0, 1), - is_boolean=True, - ) + Returns: + pd.Series: The variable set indexed by its corresponding dimensions. + + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the `lowerbound` is greater than the `upperbound`. + ValueError: if the index of `lower_bound`, or `upper_bound` does not match + the input index. + """ + if not isinstance(index, pd.Index): + raise TypeError("Non-index object is used as index") + if not name.isidentifier(): + raise ValueError(f"name={name!r} is not a valid identifier") + if ( + isinstance(lower_bounds, IntegralTypes) + and isinstance(upper_bounds, IntegralTypes) + and lower_bounds > upper_bounds + ): + raise ValueError( + f"lower_bound={lower_bounds} is greater than" + f" upper_bound={upper_bounds} for variable set={name}" + ) + + lower_bounds = _convert_to_integral_series_and_validate_index( + lower_bounds, index + ) + upper_bounds = _convert_to_integral_series_and_validate_index( + upper_bounds, index + ) + return pd.Series( + index=index, + data=[ + # pylint: disable=g-complex-comprehension + self._append_int_var( + IntVar( + model=self.__model, + name=f"{name}[{i}]", + domain=sorted_interval_list.Domain( + lower_bounds[i], upper_bounds[i] + ), + is_boolean=lower_bounds[i] >= 0 and upper_bounds[i] <= 1, ) - for i in index - ], - ) + ) + for i in index + ], + ) - # Linear constraints. + def new_bool_var_series( + self, + name: str, + index: pd.Index, + ) -> pd.Series: + """Creates a series of (scalar-valued) variables with the given name. - def add_linear_constraint( - self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT - ) -> Constraint: - """Adds the constraint: `lb <= linear_expr <= ub`.""" - return self.add_linear_expression_in_domain( - linear_expr, sorted_interval_list.Domain(lb, ub) - ) + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. - def add_linear_expression_in_domain( - self, - linear_expr: LinearExprT, - domain: sorted_interval_list.Domain, - ) -> Constraint: - """Adds the constraint: `linear_expr` in `domain`.""" - if isinstance(linear_expr, LinearExpr): - ble = BoundedLinearExpression(linear_expr, domain) - if not ble.ok: - raise TypeError( - "Cannot add a linear expression containing floating point" - f" coefficients or constants: {type(linear_expr).__name__!r}" + Returns: + pd.Series: The variable set indexed by its corresponding dimensions. + + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + """ + if not isinstance(index, pd.Index): + raise TypeError("Non-index object is used as index") + if not name.isidentifier(): + raise ValueError(f"name={name!r} is not a valid identifier") + return pd.Series( + index=index, + data=[ + # pylint: disable=g-complex-comprehension + self._append_int_var( + IntVar( + model=self.__model, + name=f"{name}[{i}]", + domain=sorted_interval_list.Domain(0, 1), + is_boolean=True, ) - return self.add(ble) - if isinstance(linear_expr, IntegralTypes): - if not domain.contains(int(linear_expr)): - return self.add_bool_or([]) # Evaluate to false. - else: - return self.add_bool_and([]) # Evaluate to true. + ) + for i in index + ], + ) + + # Linear constraints. + + def add_linear_constraint( + self, linear_expr: LinearExprT, lb: IntegralT, ub: IntegralT + ) -> Constraint: + """Adds the constraint: `lb <= linear_expr <= ub`.""" + return self.add_linear_expression_in_domain( + linear_expr, sorted_interval_list.Domain(lb, ub) + ) + + def add_linear_expression_in_domain( + self, + linear_expr: LinearExprT, + domain: sorted_interval_list.Domain, + ) -> Constraint: + """Adds the constraint: `linear_expr` in `domain`.""" + if isinstance(linear_expr, LinearExpr): + ble = BoundedLinearExpression(linear_expr, domain) + if not ble.ok: raise TypeError( - "not supported:" - f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})" + "Cannot add a linear expression containing floating point" + f" coefficients or constants: {type(linear_expr).__name__!r}" ) + return self.add(ble) + if isinstance(linear_expr, IntegralTypes): + if not domain.contains(int(linear_expr)): + return self.add_bool_or([]) # Evaluate to false. + else: + return self.add_bool_and([]) # Evaluate to true. + raise TypeError( + "not supported:" + f" CpModel.add_linear_expression_in_domain({type(linear_expr).__name__!r})" + ) + + def add( + self, ct: Union[BoundedLinearExpression, bool, np.bool_] + ) -> Constraint: + """Adds a `BoundedLinearExpression` to the model. - def add(self, ct: Union[BoundedLinearExpression, bool, np.bool_]) -> Constraint: - """Adds a `BoundedLinearExpression` to the model. - - Args: - ct: A [`BoundedLinearExpression`](#boundedlinearexpression). - - Returns: - An instance of the `Constraint` class. - - Raises: - TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean. - """ - if isinstance(ct, BoundedLinearExpression): - result = Constraint(self) - model_ct = self.__model.constraints[result.index] - for var in ct.vars: - model_ct.linear.vars.append(var.index) - model_ct.linear.coeffs.extend(ct.coeffs) - model_ct.linear.domain.extend( - [ - cmn.capped_subtraction(x, ct.offset) - for x in ct.bounds.flattened_intervals() - ] - ) - return result - if ct and cmn.is_boolean(ct): - return self.add_bool_or([True]) - if not ct and cmn.is_boolean(ct): - return self.add_bool_or([]) # Evaluate to false. - raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})") + Args: + ct: A [`BoundedLinearExpression`](#boundedlinearexpression). - # General Integer Constraints. + Returns: + An instance of the `Constraint` class. - @overload - def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: ... + Raises: + TypeError: If the `ct` is not a `BoundedLinearExpression` or a Boolean. + """ + if isinstance(ct, BoundedLinearExpression): + result = Constraint(self) + model_ct = self.__model.constraints[result.index] + for var in ct.vars: + model_ct.linear.vars.append(var.index) + model_ct.linear.coeffs.extend(ct.coeffs) + model_ct.linear.domain.extend([ + cmn.capped_subtraction(x, ct.offset) + for x in ct.bounds.flattened_intervals() + ]) + return result + if ct and cmn.is_boolean(ct): + return self.add_bool_or([True]) + if not ct and cmn.is_boolean(ct): + return self.add_bool_or([]) # Evaluate to false. + raise TypeError(f"not supported: CpModel.add({type(ct).__name__!r})") + + # General Integer Constraints. + + @overload + def add_all_different(self, expressions: Iterable[LinearExprT]) -> Constraint: + ... + + @overload + def add_all_different(self, *expressions: LinearExprT) -> Constraint: + ... + + def add_all_different(self, *expressions): + """Adds AllDifferent(expressions). + + This constraint forces all expressions to have different values. - @overload - def add_all_different(self, *expressions: LinearExprT) -> Constraint: ... + Args: + *expressions: simple expressions of the form a * var + constant. - def add_all_different(self, *expressions): - """Adds AllDifferent(expressions). + Returns: + An instance of the `Constraint` class. + """ + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + expanded = expand_generator_or_tuple(expressions) + model_ct.all_diff.exprs.extend( + self.parse_linear_expression(x) for x in expanded + ) + return ct + + def add_element( + self, + index: LinearExprT, + expressions: Sequence[LinearExprT], + target: LinearExprT, + ) -> Constraint: + """Adds the element constraint: `expressions[index] == target`. - This constraint forces all expressions to have different values. + Args: + index: The index of the selected expression in the array. It must be an + affine expression (a * var + b). + expressions: A list of affine expressions. + target: The expression constrained to be equal to the selected expression. + It must be an affine expression (a * var + b). - Args: - *expressions: simple expressions of the form a * var + constant. + Returns: + An instance of the `Constraint` class. + """ - Returns: - An instance of the `Constraint` class. - """ - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - expanded = expand_generator_or_tuple(expressions) - model_ct.all_diff.exprs.extend( - self.parse_linear_expression(x) for x in expanded - ) - return ct - - def add_element( - self, - index: LinearExprT, - expressions: Sequence[LinearExprT], - target: LinearExprT, - ) -> Constraint: - """Adds the element constraint: `expressions[index] == target`. - - Args: - index: The index of the selected expression in the array. It must be an - affine expression (a * var + b). - expressions: A list of affine expressions. - target: The expression constrained to be equal to the selected expression. - It must be an affine expression (a * var + b). - - Returns: - An instance of the `Constraint` class. - """ - - if not expressions: - raise ValueError("add_element expects a non-empty expressions array") - - if isinstance(index, IntegralTypes): - expression: LinearExprT = list(expressions)[int(index)] - return self.add(expression == target) - - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index)) - model_ct.element.exprs.extend( - [self.parse_linear_expression(e) for e in expressions] - ) - model_ct.element.linear_target.CopyFrom(self.parse_linear_expression(target)) - return ct - - def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: - """Adds Circuit(arcs). - - Adds a circuit constraint from a sparse list of arcs that encode the graph. - - A circuit is a unique Hamiltonian cycle in a subgraph of the total - graph. In case a node 'i' is not in the cycle, then there must be a - loop arc 'i -> i' associated with a true literal. Otherwise - this constraint will fail. - - Args: - arcs: a list of arcs. An arc is a tuple (source_node, destination_node, - literal). The arc is selected in the circuit if the literal is true. - Both source_node and destination_node must be integers between 0 and the - number of nodes - 1. - - Returns: - An instance of the `Constraint` class. - - Raises: - ValueError: If the list of arcs is empty. - """ - if not arcs: - raise ValueError("add_circuit expects a non-empty array of arcs") - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - for arc in arcs: - model_ct.circuit.tails.append(arc[0]) - model_ct.circuit.heads.append(arc[1]) - model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2])) - return ct - - def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: - """Adds a multiple circuit constraint, aka the 'VRP' constraint. - - The direct graph where arc #i (from tails[i] to head[i]) is present iff - literals[i] is true must satisfy this set of properties: - - #incoming arcs == 1 except for node 0. - - #outgoing arcs == 1 except for node 0. - - for node zero, #incoming arcs == #outgoing arcs. - - There are no duplicate arcs. - - Self-arcs are allowed except for node 0. - - There is no cycle in this graph, except through node 0. - - Args: - arcs: a list of arcs. An arc is a tuple (source_node, destination_node, - literal). The arc is selected in the circuit if the literal is true. - Both source_node and destination_node must be integers between 0 and the - number of nodes - 1. - - Returns: - An instance of the `Constraint` class. - - Raises: - ValueError: If the list of arcs is empty. - """ - if not arcs: - raise ValueError("add_multiple_circuit expects a non-empty array of arcs") - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - for arc in arcs: - model_ct.routes.tails.append(arc[0]) - model_ct.routes.heads.append(arc[1]) - model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2])) - return ct - - def add_allowed_assignments( - self, - expressions: Sequence[LinearExprT], - tuples_list: Iterable[Sequence[IntegralT]], - ) -> Constraint: - """Adds AllowedAssignments(expressions, tuples_list). - - An AllowedAssignments constraint is a constraint on an array of affine - expressions, which requires that when all expressions are assigned values, - the - resulting array equals one of the tuples in `tuple_list`. - - Args: - expressions: A list of affine expressions (a * var + b). - tuples_list: A list of admissible tuples. Each tuple must have the same - length as the expressions, and the ith value of a tuple corresponds to - the ith expression. - - Returns: - An instance of the `Constraint` class. - - Raises: - TypeError: If a tuple does not have the same size as the list of - expressions. - ValueError: If the array of expressions is empty. - """ - - if not expressions: - raise ValueError( - "add_allowed_assignments expects a non-empty expressions array" - ) + if not expressions: + raise ValueError("add_element expects a non-empty expressions array") - ct: Constraint = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.table.exprs.extend( - [self.parse_linear_expression(e) for e in expressions] - ) - arity: int = len(expressions) - for one_tuple in tuples_list: - if len(one_tuple) != arity: - raise TypeError(f"Tuple {one_tuple!r} has the wrong arity") - - # duck-typing (no explicit type checks here) - try: - for one_tuple in tuples_list: - model_ct.table.values.extend(one_tuple) - except ValueError as ex: - raise TypeError( - "add_xxx_assignment: Not an integer or does not fit in an int64_t:" - f" {type(ex.args).__name__!r}" - ) from ex - - return ct - - def add_forbidden_assignments( - self, - expressions: Sequence[LinearExprT], - tuples_list: Iterable[Sequence[IntegralT]], - ) -> Constraint: - """Adds add_forbidden_assignments(expressions, [tuples_list]). - - A ForbiddenAssignments constraint is a constraint on an array of affine - expressions where the list of impossible combinations is provided in the - tuples list. - - Args: - expressions: A list of affine expressions (a * var + b). - tuples_list: A list of forbidden tuples. Each tuple must have the same - length as the expressions, and the *i*th value of a tuple corresponds to - the *i*th expression. - - Returns: - An instance of the `Constraint` class. - - Raises: - TypeError: If a tuple does not have the same size as the list of - expressions. - ValueError: If the array of expressions is empty. - """ - - if not expressions: - raise ValueError( - "add_forbidden_assignments expects a non-empty expressions array" - ) + if isinstance(index, IntegralTypes): + expression: LinearExprT = list(expressions)[int(index)] + return self.add(expression == target) - index: int = len(self.__model.constraints) - ct: Constraint = self.add_allowed_assignments(expressions, tuples_list) - self.__model.constraints[index].table.negated = True - return ct - - def add_automaton( - self, - transition_expressions: Sequence[LinearExprT], - starting_state: IntegralT, - final_states: Sequence[IntegralT], - transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], - ) -> Constraint: - """Adds an automaton constraint. - - An automaton constraint takes a list of affine expressions (a * var + b) (of - size *n*), an initial state, a set of final states, and a set of - transitions. A transition is a triplet (*tail*, *transition*, *head*), where - *tail* and *head* are states, and *transition* is the label of an arc from - *head* to *tail*, corresponding to the value of one expression in the list - of - expressions. - - This automaton will be unrolled into a flow with *n* + 1 phases. Each phase - contains the possible states of the automaton. The first state contains the - initial state. The last phase contains the final states. - - Between two consecutive phases *i* and *i* + 1, the automaton creates a set - of arcs. For each transition (*tail*, *transition*, *head*), it will add - an arc from the state *tail* of phase *i* and the state *head* of phase - *i* + 1. This arc is labeled by the value *transition* of the expression - `expressions[i]`. That is, this arc can only be selected if `expressions[i]` - is assigned the value *transition*. - - A feasible solution of this constraint is an assignment of expressions such - that, starting from the initial state in phase 0, there is a path labeled by - the values of the expressions that ends in one of the final states in the - final phase. - - Args: - transition_expressions: A non-empty list of affine expressions (a * var + - b) whose values correspond to the labels of the arcs traversed by the - automaton. - starting_state: The initial state of the automaton. - final_states: A non-empty list of admissible final states. - transition_triples: A list of transitions for the automaton, in the - following format (current_state, variable_value, next_state). - - Returns: - An instance of the `Constraint` class. - - Raises: - ValueError: if `transition_expressions`, `final_states`, or - `transition_triples` are empty. - """ - - if not transition_expressions: - raise ValueError( - "add_automaton expects a non-empty transition_expressions array" - ) - if not final_states: - raise ValueError("add_automaton expects some final states") + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.element.linear_index.CopyFrom(self.parse_linear_expression(index)) + model_ct.element.exprs.extend( + [self.parse_linear_expression(e) for e in expressions] + ) + model_ct.element.linear_target.CopyFrom( + self.parse_linear_expression(target) + ) + return ct - if not transition_triples: - raise ValueError("add_automaton expects some transition triples") + def add_circuit(self, arcs: Sequence[ArcT]) -> Constraint: + """Adds Circuit(arcs). - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.automaton.exprs.extend( - [self.parse_linear_expression(e) for e in transition_expressions] - ) - model_ct.automaton.starting_state = starting_state - for v in final_states: - model_ct.automaton.final_states.append(v) - for t in transition_triples: - if len(t) != 3: - raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)") - model_ct.automaton.transition_tail.append(t[0]) - model_ct.automaton.transition_label.append(t[1]) - model_ct.automaton.transition_head.append(t[2]) - return ct - - def add_inverse( - self, - variables: Sequence[VariableT], - inverse_variables: Sequence[VariableT], - ) -> Constraint: - """Adds Inverse(variables, inverse_variables). - - An inverse constraint enforces that if `variables[i]` is assigned a value - `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. - - Args: - variables: An array of integer variables. - inverse_variables: An array of integer variables. - - Returns: - An instance of the `Constraint` class. - - Raises: - TypeError: if variables and inverse_variables have different lengths, or - if they are empty. - """ - - if not variables or not inverse_variables: - raise TypeError("The Inverse constraint does not accept empty arrays") - if len(variables) != len(inverse_variables): - raise TypeError( - "In the inverse constraint, the two array variables and" - " inverse_variables must have the same length." - ) - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.inverse.f_direct.extend([self.get_or_make_index(x) for x in variables]) - model_ct.inverse.f_inverse.extend( - [self.get_or_make_index(x) for x in inverse_variables] - ) - return ct + Adds a circuit constraint from a sparse list of arcs that encode the graph. - def add_reservoir_constraint( - self, - times: Iterable[LinearExprT], - level_changes: Iterable[LinearExprT], - min_level: int, - max_level: int, - ) -> Constraint: - """Adds Reservoir(times, level_changes, min_level, max_level). + A circuit is a unique Hamiltonian cycle in a subgraph of the total + graph. In case a node 'i' is not in the cycle, then there must be a + loop arc 'i -> i' associated with a true literal. Otherwise + this constraint will fail. - Maintains a reservoir level within bounds. The water level starts at 0, and - at any time, it must be between min_level and max_level. + Args: + arcs: a list of arcs. An arc is a tuple (source_node, destination_node, + literal). The arc is selected in the circuit if the literal is true. + Both source_node and destination_node must be integers between 0 and the + number of nodes - 1. - If the affine expression `times[i]` is assigned a value t, then the current - level changes by `level_changes[i]`, which is constant, at time t. + Returns: + An instance of the `Constraint` class. - Note that min level must be <= 0, and the max level must be >= 0. Please - use fixed level_changes to simulate initial state. + Raises: + ValueError: If the list of arcs is empty. + """ + if not arcs: + raise ValueError("add_circuit expects a non-empty array of arcs") + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + for arc in arcs: + model_ct.circuit.tails.append(arc[0]) + model_ct.circuit.heads.append(arc[1]) + model_ct.circuit.literals.append(self.get_or_make_boolean_index(arc[2])) + return ct + + def add_multiple_circuit(self, arcs: Sequence[ArcT]) -> Constraint: + """Adds a multiple circuit constraint, aka the 'VRP' constraint. + + The direct graph where arc #i (from tails[i] to head[i]) is present iff + literals[i] is true must satisfy this set of properties: + - #incoming arcs == 1 except for node 0. + - #outgoing arcs == 1 except for node 0. + - for node zero, #incoming arcs == #outgoing arcs. + - There are no duplicate arcs. + - Self-arcs are allowed except for node 0. + - There is no cycle in this graph, except through node 0. - Therefore, at any time: - sum(level_changes[i] if times[i] <= t) in [min_level, max_level] + Args: + arcs: a list of arcs. An arc is a tuple (source_node, destination_node, + literal). The arc is selected in the circuit if the literal is true. + Both source_node and destination_node must be integers between 0 and the + number of nodes - 1. - Args: - times: A list of 1-var affine expressions (a * x + b) which specify the - time of the filling or emptying the reservoir. - level_changes: A list of integer values that specifies the amount of the - emptying or filling. Currently, variable demands are not supported. - min_level: At any time, the level of the reservoir must be greater or - equal than the min level. - max_level: At any time, the level of the reservoir must be less or equal - than the max level. + Returns: + An instance of the `Constraint` class. - Returns: - An instance of the `Constraint` class. + Raises: + ValueError: If the list of arcs is empty. + """ + if not arcs: + raise ValueError("add_multiple_circuit expects a non-empty array of arcs") + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + for arc in arcs: + model_ct.routes.tails.append(arc[0]) + model_ct.routes.heads.append(arc[1]) + model_ct.routes.literals.append(self.get_or_make_boolean_index(arc[2])) + return ct + + def add_allowed_assignments( + self, + expressions: Sequence[LinearExprT], + tuples_list: Iterable[Sequence[IntegralT]], + ) -> Constraint: + """Adds AllowedAssignments(expressions, tuples_list). + + An AllowedAssignments constraint is a constraint on an array of affine + expressions, which requires that when all expressions are assigned values, + the + resulting array equals one of the tuples in `tuple_list`. - Raises: - ValueError: if max_level < min_level. + Args: + expressions: A list of affine expressions (a * var + b). + tuples_list: A list of admissible tuples. Each tuple must have the same + length as the expressions, and the ith value of a tuple corresponds to + the ith expression. - ValueError: if max_level < 0. + Returns: + An instance of the `Constraint` class. - ValueError: if min_level > 0 - """ + Raises: + TypeError: If a tuple does not have the same size as the list of + expressions. + ValueError: If the array of expressions is empty. + """ - if max_level < min_level: - raise ValueError("Reservoir constraint must have a max_level >= min_level") + if not expressions: + raise ValueError( + "add_allowed_assignments expects a non-empty expressions array" + ) + + ct: Constraint = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.table.exprs.extend( + [self.parse_linear_expression(e) for e in expressions] + ) + arity: int = len(expressions) + for one_tuple in tuples_list: + if len(one_tuple) != arity: + raise TypeError(f"Tuple {one_tuple!r} has the wrong arity") + + # duck-typing (no explicit type checks here) + try: + for one_tuple in tuples_list: + model_ct.table.values.extend(one_tuple) + except ValueError as ex: + raise TypeError( + "add_xxx_assignment: Not an integer or does not fit in an int64_t:" + f" {type(ex.args).__name__!r}" + ) from ex + + return ct + + def add_forbidden_assignments( + self, + expressions: Sequence[LinearExprT], + tuples_list: Iterable[Sequence[IntegralT]], + ) -> Constraint: + """Adds add_forbidden_assignments(expressions, [tuples_list]). + + A ForbiddenAssignments constraint is a constraint on an array of affine + expressions where the list of impossible combinations is provided in the + tuples list. - if max_level < 0: - raise ValueError("Reservoir constraint must have a max_level >= 0") + Args: + expressions: A list of affine expressions (a * var + b). + tuples_list: A list of forbidden tuples. Each tuple must have the same + length as the expressions, and the *i*th value of a tuple corresponds to + the *i*th expression. - if min_level > 0: - raise ValueError("Reservoir constraint must have a min_level <= 0") + Returns: + An instance of the `Constraint` class. - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.reservoir.time_exprs.extend( - [self.parse_linear_expression(x) for x in times] - ) - model_ct.reservoir.level_changes.extend( - [self.parse_linear_expression(x) for x in level_changes] - ) - model_ct.reservoir.min_level = min_level - model_ct.reservoir.max_level = max_level - return ct - - def add_reservoir_constraint_with_active( - self, - times: Iterable[LinearExprT], - level_changes: Iterable[LinearExprT], - actives: Iterable[LiteralT], - min_level: int, - max_level: int, - ) -> Constraint: - """Adds Reservoir(times, level_changes, actives, min_level, max_level). - - Maintains a reservoir level within bounds. The water level starts at 0, and - at any time, it must be between min_level and max_level. - - If the variable `times[i]` is assigned a value t, and `actives[i]` is - `True`, then the current level changes by `level_changes[i]`, which is - constant, - at time t. - - Note that min level must be <= 0, and the max level must be >= 0. Please - use fixed level_changes to simulate initial state. - - Therefore, at any time: - sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, - max_level] - - - The array of boolean variables 'actives', if defined, indicates which - actions are actually performed. - - Args: - times: A list of 1-var affine expressions (a * x + b) which specify the - time of the filling or emptying the reservoir. - level_changes: A list of integer values that specifies the amount of the - emptying or filling. Currently, variable demands are not supported. - actives: a list of boolean variables. They indicates if the - emptying/refilling events actually take place. - min_level: At any time, the level of the reservoir must be greater or - equal than the min level. - max_level: At any time, the level of the reservoir must be less or equal - than the max level. - - Returns: - An instance of the `Constraint` class. - - Raises: - ValueError: if max_level < min_level. - - ValueError: if max_level < 0. - - ValueError: if min_level > 0 - """ - - if max_level < min_level: - raise ValueError("Reservoir constraint must have a max_level >= min_level") - - if max_level < 0: - raise ValueError("Reservoir constraint must have a max_level >= 0") - - if min_level > 0: - raise ValueError("Reservoir constraint must have a min_level <= 0") - - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.reservoir.time_exprs.extend( - [self.parse_linear_expression(x) for x in times] - ) - model_ct.reservoir.level_changes.extend( - [self.parse_linear_expression(x) for x in level_changes] - ) - model_ct.reservoir.active_literals.extend( - [self.get_or_make_boolean_index(x) for x in actives] - ) - model_ct.reservoir.min_level = min_level - model_ct.reservoir.max_level = max_level - return ct + Raises: + TypeError: If a tuple does not have the same size as the list of + expressions. + ValueError: If the array of expressions is empty. + """ - def add_map_domain( - self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 - ): - """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" - - for i, bool_var in enumerate(bool_var_array): - b_index = bool_var.index - var_index = var.index - model_ct = self.__model.constraints.add() - model_ct.linear.vars.append(var_index) - model_ct.linear.coeffs.append(1) - offset_as_int = int(offset) - model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i]) - model_ct.enforcement_literal.append(b_index) - - model_ct = self.__model.constraints.add() - model_ct.linear.vars.append(var_index) - model_ct.linear.coeffs.append(1) - model_ct.enforcement_literal.append(-b_index - 1) - if offset + i - 1 >= INT_MIN: - model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1]) - if offset + i + 1 <= INT_MAX: - model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX]) - - def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: - """Adds `a => b` (`a` implies `b`).""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) - model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) - return ct - - @overload - def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: ... - - @overload - def add_bool_or(self, *literals: LiteralT) -> Constraint: ... - - def add_bool_or(self, *literals): - """Adds `Or(literals) == true`: sum(literals) >= 1.""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.bool_or.literals.extend( - [ - self.get_or_make_boolean_index(x) - for x in expand_generator_or_tuple(literals) - ] - ) - return ct + if not expressions: + raise ValueError( + "add_forbidden_assignments expects a non-empty expressions array" + ) + + index: int = len(self.__model.constraints) + ct: Constraint = self.add_allowed_assignments(expressions, tuples_list) + self.__model.constraints[index].table.negated = True + return ct + + def add_automaton( + self, + transition_expressions: Sequence[LinearExprT], + starting_state: IntegralT, + final_states: Sequence[IntegralT], + transition_triples: Sequence[Tuple[IntegralT, IntegralT, IntegralT]], + ) -> Constraint: + """Adds an automaton constraint. + + An automaton constraint takes a list of affine expressions (a * var + b) (of + size *n*), an initial state, a set of final states, and a set of + transitions. A transition is a triplet (*tail*, *transition*, *head*), where + *tail* and *head* are states, and *transition* is the label of an arc from + *head* to *tail*, corresponding to the value of one expression in the list + of + expressions. + + This automaton will be unrolled into a flow with *n* + 1 phases. Each phase + contains the possible states of the automaton. The first state contains the + initial state. The last phase contains the final states. + + Between two consecutive phases *i* and *i* + 1, the automaton creates a set + of arcs. For each transition (*tail*, *transition*, *head*), it will add + an arc from the state *tail* of phase *i* and the state *head* of phase + *i* + 1. This arc is labeled by the value *transition* of the expression + `expressions[i]`. That is, this arc can only be selected if `expressions[i]` + is assigned the value *transition*. + + A feasible solution of this constraint is an assignment of expressions such + that, starting from the initial state in phase 0, there is a path labeled by + the values of the expressions that ends in one of the final states in the + final phase. - @overload - def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: ... + Args: + transition_expressions: A non-empty list of affine expressions (a * var + + b) whose values correspond to the labels of the arcs traversed by the + automaton. + starting_state: The initial state of the automaton. + final_states: A non-empty list of admissible final states. + transition_triples: A list of transitions for the automaton, in the + following format (current_state, variable_value, next_state). - @overload - def add_at_least_one(self, *literals: LiteralT) -> Constraint: ... + Returns: + An instance of the `Constraint` class. - def add_at_least_one(self, *literals): - """Same as `add_bool_or`: `sum(literals) >= 1`.""" - return self.add_bool_or(*literals) + Raises: + ValueError: if `transition_expressions`, `final_states`, or + `transition_triples` are empty. + """ - @overload - def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: ... + if not transition_expressions: + raise ValueError( + "add_automaton expects a non-empty transition_expressions array" + ) + if not final_states: + raise ValueError("add_automaton expects some final states") + + if not transition_triples: + raise ValueError("add_automaton expects some transition triples") + + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.automaton.exprs.extend( + [self.parse_linear_expression(e) for e in transition_expressions] + ) + model_ct.automaton.starting_state = starting_state + for v in final_states: + model_ct.automaton.final_states.append(v) + for t in transition_triples: + if len(t) != 3: + raise TypeError(f"Tuple {t!r} has the wrong arity (!= 3)") + model_ct.automaton.transition_tail.append(t[0]) + model_ct.automaton.transition_label.append(t[1]) + model_ct.automaton.transition_head.append(t[2]) + return ct + + def add_inverse( + self, + variables: Sequence[VariableT], + inverse_variables: Sequence[VariableT], + ) -> Constraint: + """Adds Inverse(variables, inverse_variables). + + An inverse constraint enforces that if `variables[i]` is assigned a value + `j`, then `inverse_variables[j]` is assigned a value `i`. And vice versa. - @overload - def add_at_most_one(self, *literals: LiteralT) -> Constraint: ... + Args: + variables: An array of integer variables. + inverse_variables: An array of integer variables. - def add_at_most_one(self, *literals): - """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.at_most_one.literals.extend( - [ - self.get_or_make_boolean_index(x) - for x in expand_generator_or_tuple(literals) - ] - ) - return ct - - @overload - def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: ... - - @overload - def add_exactly_one(self, *literals: LiteralT) -> Constraint: ... - - def add_exactly_one(self, *literals): - """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.exactly_one.literals.extend( - [ - self.get_or_make_boolean_index(x) - for x in expand_generator_or_tuple(literals) - ] - ) - return ct - - @overload - def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: ... - - @overload - def add_bool_and(self, *literals: LiteralT) -> Constraint: ... - - def add_bool_and(self, *literals): - """Adds `And(literals) == true`.""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.bool_and.literals.extend( - [ - self.get_or_make_boolean_index(x) - for x in expand_generator_or_tuple(literals) - ] - ) - return ct + Returns: + An instance of the `Constraint` class. - @overload - def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: ... + Raises: + TypeError: if variables and inverse_variables have different lengths, or + if they are empty. + """ - @overload - def add_bool_xor(self, *literals: LiteralT) -> Constraint: ... + if not variables or not inverse_variables: + raise TypeError("The Inverse constraint does not accept empty arrays") + if len(variables) != len(inverse_variables): + raise TypeError( + "In the inverse constraint, the two array variables and" + " inverse_variables must have the same length." + ) + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.inverse.f_direct.extend( + [self.get_or_make_index(x) for x in variables] + ) + model_ct.inverse.f_inverse.extend( + [self.get_or_make_index(x) for x in inverse_variables] + ) + return ct + + def add_reservoir_constraint( + self, + times: Iterable[LinearExprT], + level_changes: Iterable[LinearExprT], + min_level: int, + max_level: int, + ) -> Constraint: + """Adds Reservoir(times, level_changes, min_level, max_level). + + Maintains a reservoir level within bounds. The water level starts at 0, and + at any time, it must be between min_level and max_level. + + If the affine expression `times[i]` is assigned a value t, then the current + level changes by `level_changes[i]`, which is constant, at time t. + + Note that min level must be <= 0, and the max level must be >= 0. Please + use fixed level_changes to simulate initial state. + + Therefore, at any time: + sum(level_changes[i] if times[i] <= t) in [min_level, max_level] - def add_bool_xor(self, *literals): - """Adds `XOr(literals) == true`. + Args: + times: A list of 1-var affine expressions (a * x + b) which specify the + time of the filling or emptying the reservoir. + level_changes: A list of integer values that specifies the amount of the + emptying or filling. Currently, variable demands are not supported. + min_level: At any time, the level of the reservoir must be greater or + equal than the min level. + max_level: At any time, the level of the reservoir must be less or equal + than the max level. - In contrast to add_bool_or and add_bool_and, it does not support - .only_enforce_if(). + Returns: + An instance of the `Constraint` class. - Args: - *literals: the list of literals in the constraint. + Raises: + ValueError: if max_level < min_level. - Returns: - An `Constraint` object. - """ - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.bool_xor.literals.extend( - [ - self.get_or_make_boolean_index(x) - for x in expand_generator_or_tuple(literals) - ] - ) - return ct - - def add_min_equality( - self, target: LinearExprT, exprs: Iterable[LinearExprT] - ) -> Constraint: - """Adds `target == Min(exprs)`.""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.lin_max.exprs.extend( - [self.parse_linear_expression(x, True) for x in exprs] - ) - model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) - return ct - - def add_max_equality( - self, target: LinearExprT, exprs: Iterable[LinearExprT] - ) -> Constraint: - """Adds `target == Max(exprs)`.""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.lin_max.exprs.extend([self.parse_linear_expression(x) for x in exprs]) - model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) - return ct - - def add_division_equality( - self, target: LinearExprT, num: LinearExprT, denom: LinearExprT - ) -> Constraint: - """Adds `target == num // denom` (integer division rounded towards 0).""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.int_div.exprs.append(self.parse_linear_expression(num)) - model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) - model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) - return ct - - def add_abs_equality(self, target: LinearExprT, expr: LinearExprT) -> Constraint: - """Adds `target == Abs(expr)`.""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) - model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) - model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) - return ct - - def add_modulo_equality( - self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT - ) -> Constraint: - """Adds `target = expr % mod`. - - It uses the C convention, that is the result is the remainder of the - integral division rounded towards 0. - - For example: - * 10 % 3 = 1 - * -10 % 3 = -1 - * 10 % -3 = 1 - * -10 % -3 = -1 - - Args: - target: the target expression. - expr: the expression to compute the modulo of. - mod: the modulus expression. - - Returns: - A `Constraint` object. - """ - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) - model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) - model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) - return ct - - def add_multiplication_equality( - self, - target: LinearExprT, - *expressions: Union[Iterable[LinearExprT], LinearExprT], - ) -> Constraint: - """Adds `target == expressions[0] * .. * expressions[n]`.""" - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.int_prod.exprs.extend( - [ - self.parse_linear_expression(expr) - for expr in expand_generator_or_tuple(expressions) - ] - ) - model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) - return ct + ValueError: if max_level < 0. - # Scheduling support + ValueError: if min_level > 0 + """ - def new_interval_var( - self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str - ) -> IntervalVar: - """Creates an interval variable from start, size, and end. + if max_level < min_level: + raise ValueError( + "Reservoir constraint must have a max_level >= min_level" + ) + + if max_level < 0: + raise ValueError("Reservoir constraint must have a max_level >= 0") + + if min_level > 0: + raise ValueError("Reservoir constraint must have a min_level <= 0") + + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.reservoir.time_exprs.extend( + [self.parse_linear_expression(x) for x in times] + ) + model_ct.reservoir.level_changes.extend( + [self.parse_linear_expression(x) for x in level_changes] + ) + model_ct.reservoir.min_level = min_level + model_ct.reservoir.max_level = max_level + return ct + + def add_reservoir_constraint_with_active( + self, + times: Iterable[LinearExprT], + level_changes: Iterable[LinearExprT], + actives: Iterable[LiteralT], + min_level: int, + max_level: int, + ) -> Constraint: + """Adds Reservoir(times, level_changes, actives, min_level, max_level). + + Maintains a reservoir level within bounds. The water level starts at 0, and + at any time, it must be between min_level and max_level. + + If the variable `times[i]` is assigned a value t, and `actives[i]` is + `True`, then the current level changes by `level_changes[i]`, which is + constant, + at time t. + + Note that min level must be <= 0, and the max level must be >= 0. Please + use fixed level_changes to simulate initial state. + + Therefore, at any time: + sum(level_changes[i] * actives[i] if times[i] <= t) in [min_level, + max_level] + + + The array of boolean variables 'actives', if defined, indicates which + actions are actually performed. - An interval variable is a constraint, that is itself used in other - constraints like NoOverlap. + Args: + times: A list of 1-var affine expressions (a * x + b) which specify the + time of the filling or emptying the reservoir. + level_changes: A list of integer values that specifies the amount of the + emptying or filling. Currently, variable demands are not supported. + actives: a list of boolean variables. They indicates if the + emptying/refilling events actually take place. + min_level: At any time, the level of the reservoir must be greater or + equal than the min level. + max_level: At any time, the level of the reservoir must be less or equal + than the max level. - Internally, it ensures that `start + size == end`. + Returns: + An instance of the `Constraint` class. - Args: - start: The start of the interval. It must be of the form a * var + b. - size: The size of the interval. It must be of the form a * var + b. - end: The end of the interval. It must be of the form a * var + b. - name: The name of the interval variable. + Raises: + ValueError: if max_level < min_level. - Returns: - An `IntervalVar` object. - """ + ValueError: if max_level < 0. - start_expr = self.parse_linear_expression(start) - size_expr = self.parse_linear_expression(size) - end_expr = self.parse_linear_expression(end) - if len(start_expr.vars) > 1: - raise TypeError( - "cp_model.new_interval_var: start must be 1-var affine or constant." - ) - if len(size_expr.vars) > 1: - raise TypeError( - "cp_model.new_interval_var: size must be 1-var affine or constant." - ) - if len(end_expr.vars) > 1: - raise TypeError( - "cp_model.new_interval_var: end must be 1-var affine or constant." - ) - return IntervalVar( - self.__model, - self.__var_list, - start_expr, - size_expr, - end_expr, - None, - name, - ) + ValueError: if min_level > 0 + """ - def new_interval_var_series( - self, - name: str, - index: pd.Index, - starts: Union[LinearExprT, pd.Series], - sizes: Union[LinearExprT, pd.Series], - ends: Union[LinearExprT, pd.Series], - ) -> pd.Series: - """Creates a series of interval variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - starts (Union[LinearExprT, pd.Series]): The start of each interval in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - sizes (Union[LinearExprT, pd.Series]): The size of each interval in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - ends (Union[LinearExprT, pd.Series]): The ends of each interval in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - - Returns: - pd.Series: The interval variable set indexed by its corresponding - dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - ValueError: if the all the indexes do not match. - """ - if not isinstance(index, pd.Index): - raise TypeError("Non-index object is used as index") - if not name.isidentifier(): - raise ValueError(f"name={name!r} is not a valid identifier") - - starts = _convert_to_linear_expr_series_and_validate_index(starts, index) - sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) - ends = _convert_to_linear_expr_series_and_validate_index(ends, index) - interval_array = [] - for i in index: - interval_array.append( - self.new_interval_var( - start=starts[i], - size=sizes[i], - end=ends[i], - name=f"{name}[{i}]", - ) - ) - return pd.Series(index=index, data=interval_array) - - def new_fixed_size_interval_var( - self, start: LinearExprT, size: IntegralT, name: str - ) -> IntervalVar: - """Creates an interval variable from start, and a fixed size. - - An interval variable is a constraint, that is itself used in other - constraints like NoOverlap. - - Args: - start: The start of the interval. It must be of the form a * var + b. - size: The size of the interval. It must be an integer value. - name: The name of the interval variable. - - Returns: - An `IntervalVar` object. - """ - start_expr = self.parse_linear_expression(start) - size_expr = self.parse_linear_expression(size) - end_expr = self.parse_linear_expression(start + size) - if len(start_expr.vars) > 1: - raise TypeError( - "cp_model.new_interval_var: start must be affine or constant." - ) - return IntervalVar( - self.__model, - self.__var_list, - start_expr, - size_expr, - end_expr, - None, - name, - ) + if max_level < min_level: + raise ValueError( + "Reservoir constraint must have a max_level >= min_level" + ) + + if max_level < 0: + raise ValueError("Reservoir constraint must have a max_level >= 0") + + if min_level > 0: + raise ValueError("Reservoir constraint must have a min_level <= 0") + + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.reservoir.time_exprs.extend( + [self.parse_linear_expression(x) for x in times] + ) + model_ct.reservoir.level_changes.extend( + [self.parse_linear_expression(x) for x in level_changes] + ) + model_ct.reservoir.active_literals.extend( + [self.get_or_make_boolean_index(x) for x in actives] + ) + model_ct.reservoir.min_level = min_level + model_ct.reservoir.max_level = max_level + return ct + + def add_map_domain( + self, var: IntVar, bool_var_array: Iterable[IntVar], offset: IntegralT = 0 + ): + """Adds `var == i + offset <=> bool_var_array[i] == true for all i`.""" + + for i, bool_var in enumerate(bool_var_array): + b_index = bool_var.index + var_index = var.index + model_ct = self.__model.constraints.add() + model_ct.linear.vars.append(var_index) + model_ct.linear.coeffs.append(1) + offset_as_int = int(offset) + model_ct.linear.domain.extend([offset_as_int + i, offset_as_int + i]) + model_ct.enforcement_literal.append(b_index) + + model_ct = self.__model.constraints.add() + model_ct.linear.vars.append(var_index) + model_ct.linear.coeffs.append(1) + model_ct.enforcement_literal.append(-b_index - 1) + if offset + i - 1 >= INT_MIN: + model_ct.linear.domain.extend([INT_MIN, offset_as_int + i - 1]) + if offset + i + 1 <= INT_MAX: + model_ct.linear.domain.extend([offset_as_int + i + 1, INT_MAX]) + + def add_implication(self, a: LiteralT, b: LiteralT) -> Constraint: + """Adds `a => b` (`a` implies `b`).""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.bool_or.literals.append(self.get_or_make_boolean_index(b)) + model_ct.enforcement_literal.append(self.get_or_make_boolean_index(a)) + return ct + + @overload + def add_bool_or(self, literals: Iterable[LiteralT]) -> Constraint: + ... + + @overload + def add_bool_or(self, *literals: LiteralT) -> Constraint: + ... + + def add_bool_or(self, *literals): + """Adds `Or(literals) == true`: sum(literals) >= 1.""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.bool_or.literals.extend([ + self.get_or_make_boolean_index(x) + for x in expand_generator_or_tuple(literals) + ]) + return ct + + @overload + def add_at_least_one(self, literals: Iterable[LiteralT]) -> Constraint: + ... + + @overload + def add_at_least_one(self, *literals: LiteralT) -> Constraint: + ... + + def add_at_least_one(self, *literals): + """Same as `add_bool_or`: `sum(literals) >= 1`.""" + return self.add_bool_or(*literals) + + @overload + def add_at_most_one(self, literals: Iterable[LiteralT]) -> Constraint: + ... + + @overload + def add_at_most_one(self, *literals: LiteralT) -> Constraint: + ... + + def add_at_most_one(self, *literals): + """Adds `AtMostOne(literals)`: `sum(literals) <= 1`.""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.at_most_one.literals.extend([ + self.get_or_make_boolean_index(x) + for x in expand_generator_or_tuple(literals) + ]) + return ct + + @overload + def add_exactly_one(self, literals: Iterable[LiteralT]) -> Constraint: + ... + + @overload + def add_exactly_one(self, *literals: LiteralT) -> Constraint: + ... + + def add_exactly_one(self, *literals): + """Adds `ExactlyOne(literals)`: `sum(literals) == 1`.""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.exactly_one.literals.extend([ + self.get_or_make_boolean_index(x) + for x in expand_generator_or_tuple(literals) + ]) + return ct + + @overload + def add_bool_and(self, literals: Iterable[LiteralT]) -> Constraint: + ... + + @overload + def add_bool_and(self, *literals: LiteralT) -> Constraint: + ... + + def add_bool_and(self, *literals): + """Adds `And(literals) == true`.""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.bool_and.literals.extend([ + self.get_or_make_boolean_index(x) + for x in expand_generator_or_tuple(literals) + ]) + return ct + + @overload + def add_bool_xor(self, literals: Iterable[LiteralT]) -> Constraint: + ... + + @overload + def add_bool_xor(self, *literals: LiteralT) -> Constraint: + ... + + def add_bool_xor(self, *literals): + """Adds `XOr(literals) == true`. + + In contrast to add_bool_or and add_bool_and, it does not support + .only_enforce_if(). - def new_fixed_size_interval_var_series( - self, - name: str, - index: pd.Index, - starts: Union[LinearExprT, pd.Series], - sizes: Union[IntegralT, pd.Series], - ) -> pd.Series: - """Creates a series of interval variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - starts (Union[LinearExprT, pd.Series]): The start of each interval in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in - the set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - - Returns: - pd.Series: The interval variable set indexed by its corresponding - dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - ValueError: if the all the indexes do not match. - """ - if not isinstance(index, pd.Index): - raise TypeError("Non-index object is used as index") - if not name.isidentifier(): - raise ValueError(f"name={name!r} is not a valid identifier") - - starts = _convert_to_linear_expr_series_and_validate_index(starts, index) - sizes = _convert_to_integral_series_and_validate_index(sizes, index) - interval_array = [] - for i in index: - interval_array.append( - self.new_fixed_size_interval_var( - start=starts[i], - size=sizes[i], - name=f"{name}[{i}]", - ) - ) - return pd.Series(index=index, data=interval_array) - - def new_optional_interval_var( - self, - start: LinearExprT, - size: LinearExprT, - end: LinearExprT, - is_present: LiteralT, - name: str, - ) -> IntervalVar: - """Creates an optional interval var from start, size, end, and is_present. - - An optional interval variable is a constraint, that is itself used in other - constraints like NoOverlap. This constraint is protected by a presence - literal that indicates if it is active or not. - - Internally, it ensures that `is_present` implies `start + size == - end`. - - Args: - start: The start of the interval. It must be of the form a * var + b. - size: The size of the interval. It must be of the form a * var + b. - end: The end of the interval. It must be of the form a * var + b. - is_present: A literal that indicates if the interval is active or not. A - inactive interval is simply ignored by all constraints. - name: The name of the interval variable. - - Returns: - An `IntervalVar` object. - """ - - # Creates the IntervalConstraintProto object. - is_present_index = self.get_or_make_boolean_index(is_present) - start_expr = self.parse_linear_expression(start) - size_expr = self.parse_linear_expression(size) - end_expr = self.parse_linear_expression(end) - if len(start_expr.vars) > 1: - raise TypeError( - "cp_model.new_interval_var: start must be affine or constant." - ) - if len(size_expr.vars) > 1: - raise TypeError( - "cp_model.new_interval_var: size must be affine or constant." - ) - if len(end_expr.vars) > 1: - raise TypeError( - "cp_model.new_interval_var: end must be affine or constant." - ) - return IntervalVar( - self.__model, - self.__var_list, - start_expr, - size_expr, - end_expr, - is_present_index, - name, - ) + Args: + *literals: the list of literals in the constraint. - def new_optional_interval_var_series( - self, - name: str, - index: pd.Index, - starts: Union[LinearExprT, pd.Series], - sizes: Union[LinearExprT, pd.Series], - ends: Union[LinearExprT, pd.Series], - are_present: Union[LiteralT, pd.Series], - ) -> pd.Series: - """Creates a series of interval variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - starts (Union[LinearExprT, pd.Series]): The start of each interval in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - sizes (Union[LinearExprT, pd.Series]): The size of each interval in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - ends (Union[LinearExprT, pd.Series]): The ends of each interval in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - are_present (Union[LiteralT, pd.Series]): The performed literal of each - interval in the set. If a `pd.Series` is passed in, it will be based on - the corresponding values of the pd.Series. - - Returns: - pd.Series: The interval variable set indexed by its corresponding - dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - ValueError: if the all the indexes do not match. - """ - if not isinstance(index, pd.Index): - raise TypeError("Non-index object is used as index") - if not name.isidentifier(): - raise ValueError(f"name={name!r} is not a valid identifier") - - starts = _convert_to_linear_expr_series_and_validate_index(starts, index) - sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) - ends = _convert_to_linear_expr_series_and_validate_index(ends, index) - are_present = _convert_to_literal_series_and_validate_index(are_present, index) - - interval_array = [] - for i in index: - interval_array.append( - self.new_optional_interval_var( - start=starts[i], - size=sizes[i], - end=ends[i], - is_present=are_present[i], - name=f"{name}[{i}]", - ) - ) - return pd.Series(index=index, data=interval_array) - - def new_optional_fixed_size_interval_var( - self, - start: LinearExprT, - size: IntegralT, - is_present: LiteralT, - name: str, - ) -> IntervalVar: - """Creates an interval variable from start, and a fixed size. - - An interval variable is a constraint, that is itself used in other - constraints like NoOverlap. - - Args: - start: The start of the interval. It must be of the form a * var + b. - size: The size of the interval. It must be an integer value. - is_present: A literal that indicates if the interval is active or not. A - inactive interval is simply ignored by all constraints. - name: The name of the interval variable. - - Returns: - An `IntervalVar` object. - """ - start_expr = self.parse_linear_expression(start) - size_expr = self.parse_linear_expression(size) - end_expr = self.parse_linear_expression(start + size) - if len(start_expr.vars) > 1: - raise TypeError( - "cp_model.new_interval_var: start must be affine or constant." - ) - is_present_index = self.get_or_make_boolean_index(is_present) - return IntervalVar( - self.__model, - self.__var_list, - start_expr, - size_expr, - end_expr, - is_present_index, - name, - ) + Returns: + An `Constraint` object. + """ + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.bool_xor.literals.extend([ + self.get_or_make_boolean_index(x) + for x in expand_generator_or_tuple(literals) + ]) + return ct + + def add_min_equality( + self, target: LinearExprT, exprs: Iterable[LinearExprT] + ) -> Constraint: + """Adds `target == Min(exprs)`.""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.lin_max.exprs.extend( + [self.parse_linear_expression(x, True) for x in exprs] + ) + model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target, True)) + return ct + + def add_max_equality( + self, target: LinearExprT, exprs: Iterable[LinearExprT] + ) -> Constraint: + """Adds `target == Max(exprs)`.""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.lin_max.exprs.extend( + [self.parse_linear_expression(x) for x in exprs] + ) + model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) + return ct + + def add_division_equality( + self, target: LinearExprT, num: LinearExprT, denom: LinearExprT + ) -> Constraint: + """Adds `target == num // denom` (integer division rounded towards 0).""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.int_div.exprs.append(self.parse_linear_expression(num)) + model_ct.int_div.exprs.append(self.parse_linear_expression(denom)) + model_ct.int_div.target.CopyFrom(self.parse_linear_expression(target)) + return ct + + def add_abs_equality( + self, target: LinearExprT, expr: LinearExprT + ) -> Constraint: + """Adds `target == Abs(expr)`.""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.lin_max.exprs.append(self.parse_linear_expression(expr)) + model_ct.lin_max.exprs.append(self.parse_linear_expression(expr, True)) + model_ct.lin_max.target.CopyFrom(self.parse_linear_expression(target)) + return ct + + def add_modulo_equality( + self, target: LinearExprT, expr: LinearExprT, mod: LinearExprT + ) -> Constraint: + """Adds `target = expr % mod`. + + It uses the C convention, that is the result is the remainder of the + integral division rounded towards 0. + + For example: + * 10 % 3 = 1 + * -10 % 3 = -1 + * 10 % -3 = 1 + * -10 % -3 = -1 - def new_optional_fixed_size_interval_var_series( - self, - name: str, - index: pd.Index, - starts: Union[LinearExprT, pd.Series], - sizes: Union[IntegralT, pd.Series], - are_present: Union[LiteralT, pd.Series], - ) -> pd.Series: - """Creates a series of interval variables with the given name. - - Args: - name (str): Required. The name of the variable set. - index (pd.Index): Required. The index to use for the variable set. - starts (Union[LinearExprT, pd.Series]): The start of each interval in the - set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in - the set. If a `pd.Series` is passed in, it will be based on the - corresponding values of the pd.Series. - are_present (Union[LiteralT, pd.Series]): The performed literal of each - interval in the set. If a `pd.Series` is passed in, it will be based on - the corresponding values of the pd.Series. - - Returns: - pd.Series: The interval variable set indexed by its corresponding - dimensions. - - Raises: - TypeError: if the `index` is invalid (e.g. a `DataFrame`). - ValueError: if the `name` is not a valid identifier or already exists. - ValueError: if the all the indexes do not match. - """ - if not isinstance(index, pd.Index): - raise TypeError("Non-index object is used as index") - if not name.isidentifier(): - raise ValueError(f"name={name!r} is not a valid identifier") - - starts = _convert_to_linear_expr_series_and_validate_index(starts, index) - sizes = _convert_to_integral_series_and_validate_index(sizes, index) - are_present = _convert_to_literal_series_and_validate_index(are_present, index) - interval_array = [] - for i in index: - interval_array.append( - self.new_optional_fixed_size_interval_var( - start=starts[i], - size=sizes[i], - is_present=are_present[i], - name=f"{name}[{i}]", - ) - ) - return pd.Series(index=index, data=interval_array) + Args: + target: the target expression. + expr: the expression to compute the modulo of. + mod: the modulus expression. - def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: - """Adds NoOverlap(interval_vars). + Returns: + A `Constraint` object. + """ + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.int_mod.exprs.append(self.parse_linear_expression(expr)) + model_ct.int_mod.exprs.append(self.parse_linear_expression(mod)) + model_ct.int_mod.target.CopyFrom(self.parse_linear_expression(target)) + return ct + + def add_multiplication_equality( + self, + target: LinearExprT, + *expressions: Union[Iterable[LinearExprT], LinearExprT], + ) -> Constraint: + """Adds `target == expressions[0] * .. * expressions[n]`.""" + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.int_prod.exprs.extend([ + self.parse_linear_expression(expr) + for expr in expand_generator_or_tuple(expressions) + ]) + model_ct.int_prod.target.CopyFrom(self.parse_linear_expression(target)) + return ct + + # Scheduling support + + def new_interval_var( + self, start: LinearExprT, size: LinearExprT, end: LinearExprT, name: str + ) -> IntervalVar: + """Creates an interval variable from start, size, and end. + + An interval variable is a constraint, that is itself used in other + constraints like NoOverlap. + + Internally, it ensures that `start + size == end`. - A NoOverlap constraint ensures that all present intervals do not overlap - in time. + Args: + start: The start of the interval. It must be of the form a * var + b. + size: The size of the interval. It must be of the form a * var + b. + end: The end of the interval. It must be of the form a * var + b. + name: The name of the interval variable. - Args: - interval_vars: The list of interval variables to constrain. + Returns: + An `IntervalVar` object. + """ - Returns: - An instance of the `Constraint` class. - """ - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.no_overlap.intervals.extend( - [self.get_interval_index(x) for x in interval_vars] - ) - return ct - - def add_no_overlap_2d( - self, - x_intervals: Iterable[IntervalVar], - y_intervals: Iterable[IntervalVar], - ) -> Constraint: - """Adds NoOverlap2D(x_intervals, y_intervals). - - A NoOverlap2D constraint ensures that all present rectangles do not overlap - on a plane. Each rectangle is aligned with the X and Y axis, and is defined - by two intervals which represent its projection onto the X and Y axis. - - Furthermore, one box is optional if at least one of the x or y interval is - optional. - - Args: - x_intervals: The X coordinates of the rectangles. - y_intervals: The Y coordinates of the rectangles. - - Returns: - An instance of the `Constraint` class. - """ - ct = Constraint(self) - model_ct = self.__model.constraints[ct.index] - model_ct.no_overlap_2d.x_intervals.extend( - [self.get_interval_index(x) for x in x_intervals] - ) - model_ct.no_overlap_2d.y_intervals.extend( - [self.get_interval_index(x) for x in y_intervals] - ) - return ct - - def add_cumulative( - self, - intervals: Iterable[IntervalVar], - demands: Iterable[LinearExprT], - capacity: LinearExprT, - ) -> Constraint: - """Adds Cumulative(intervals, demands, capacity). - - This constraint enforces that: - - for all t: - sum(demands[i] - if (start(intervals[i]) <= t < end(intervals[i])) and - (intervals[i] is present)) <= capacity - - Args: - intervals: The list of intervals. - demands: The list of demands for each interval. Each demand must be >= 0. - Each demand can be a 1-var affine expression (a * x + b). - capacity: The maximum capacity of the cumulative constraint. It can be a - 1-var affine expression (a * x + b). - - Returns: - An instance of the `Constraint` class. - """ - cumulative = Constraint(self) - model_ct = self.__model.constraints[cumulative.index] - model_ct.cumulative.intervals.extend( - [self.get_interval_index(x) for x in intervals] - ) - for d in demands: - model_ct.cumulative.demands.append(self.parse_linear_expression(d)) - model_ct.cumulative.capacity.CopyFrom(self.parse_linear_expression(capacity)) - return cumulative - - # Support for model cloning. - def clone(self) -> "CpModel": - """Reset the model, and creates a new one from a CpModelProto instance.""" - clone = CpModel() - clone.proto.CopyFrom(self.proto) - clone.rebuild_var_and_constant_map() - return clone - - def rebuild_var_and_constant_map(self): - """Internal method used during model cloning.""" - for i, var in enumerate(self.__model.variables): - if len(var.domain) == 2 and var.domain[0] == var.domain[1]: - self.__constant_map[var.domain[0]] = i - is_boolean = ( - len(var.domain) == 2 and var.domain[0] >= 0 and var.domain[1] <= 1 - ) - self.__var_list.append(IntVar(self.__model, i, is_boolean, None)) - - def get_bool_var_from_proto_index(self, index: int) -> IntVar: - """Returns an already created Boolean variable from its index.""" - result = self._get_int_var(index) - if not result.is_boolean: - raise ValueError( - f"get_bool_var_from_proto_index: index {index} does not reference a" - " boolean variable" - ) - return result + start_expr = self.parse_linear_expression(start) + size_expr = self.parse_linear_expression(size) + end_expr = self.parse_linear_expression(end) + if len(start_expr.vars) > 1: + raise TypeError( + "cp_model.new_interval_var: start must be 1-var affine or constant." + ) + if len(size_expr.vars) > 1: + raise TypeError( + "cp_model.new_interval_var: size must be 1-var affine or constant." + ) + if len(end_expr.vars) > 1: + raise TypeError( + "cp_model.new_interval_var: end must be 1-var affine or constant." + ) + return IntervalVar( + self.__model, + self.__var_list, + start_expr, + size_expr, + end_expr, + None, + name, + ) + + def new_interval_var_series( + self, + name: str, + index: pd.Index, + starts: Union[LinearExprT, pd.Series], + sizes: Union[LinearExprT, pd.Series], + ends: Union[LinearExprT, pd.Series], + ) -> pd.Series: + """Creates a series of interval variables with the given name. - def get_int_var_from_proto_index(self, index: int) -> IntVar: - """Returns an already created integer variable from its index.""" - return self._get_int_var(index) + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. + starts (Union[LinearExprT, pd.Series]): The start of each interval in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. + sizes (Union[LinearExprT, pd.Series]): The size of each interval in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. + ends (Union[LinearExprT, pd.Series]): The ends of each interval in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. - def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: - """Returns an already created interval variable from its index.""" - if index < 0 or index >= len(self.__model.constraints): - raise ValueError( - f"get_interval_var_from_proto_index: out of bound index {index}" - ) - ct = self.__model.constraints[index] - if not ct.HasField("interval"): - raise ValueError( - f"get_interval_var_from_proto_index: index {index} does not" - " reference an" + " interval variable" - ) + Returns: + pd.Series: The interval variable set indexed by its corresponding + dimensions. - return IntervalVar(self.__model, self.__var_list, index, None, None, None, None) + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the all the indexes do not match. + """ + if not isinstance(index, pd.Index): + raise TypeError("Non-index object is used as index") + if not name.isidentifier(): + raise ValueError(f"name={name!r} is not a valid identifier") + + starts = _convert_to_linear_expr_series_and_validate_index(starts, index) + sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) + ends = _convert_to_linear_expr_series_and_validate_index(ends, index) + interval_array = [] + for i in index: + interval_array.append( + self.new_interval_var( + start=starts[i], + size=sizes[i], + end=ends[i], + name=f"{name}[{i}]", + ) + ) + return pd.Series(index=index, data=interval_array) + + def new_fixed_size_interval_var( + self, start: LinearExprT, size: IntegralT, name: str + ) -> IntervalVar: + """Creates an interval variable from start, and a fixed size. + + An interval variable is a constraint, that is itself used in other + constraints like NoOverlap. - # Helpers. + Args: + start: The start of the interval. It must be of the form a * var + b. + size: The size of the interval. It must be an integer value. + name: The name of the interval variable. - def __str__(self) -> str: - return str(self.__model) + Returns: + An `IntervalVar` object. + """ + start_expr = self.parse_linear_expression(start) + size_expr = self.parse_linear_expression(size) + end_expr = self.parse_linear_expression(start + size) + if len(start_expr.vars) > 1: + raise TypeError( + "cp_model.new_interval_var: start must be affine or constant." + ) + return IntervalVar( + self.__model, + self.__var_list, + start_expr, + size_expr, + end_expr, + None, + name, + ) + + def new_fixed_size_interval_var_series( + self, + name: str, + index: pd.Index, + starts: Union[LinearExprT, pd.Series], + sizes: Union[IntegralT, pd.Series], + ) -> pd.Series: + """Creates a series of interval variables with the given name. - @property - def proto(self) -> cp_model_pb2.CpModelProto: - """Returns the underlying CpModelProto.""" - return self.__model + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. + starts (Union[LinearExprT, pd.Series]): The start of each interval in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. + sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in + the set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. - def negated(self, index: int) -> int: - return -index - 1 + Returns: + pd.Series: The interval variable set indexed by its corresponding + dimensions. - def get_or_make_index(self, arg: VariableT) -> int: - """Returns the index of a variable, its negation, or a number.""" - if isinstance(arg, IntVar): - return arg.index - if isinstance(arg, IntegralTypes): - return self.get_or_make_index_from_constant(arg) - raise TypeError( - f"NotSupported: model.get_or_make_index({type(arg).__name__!r})" - ) + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the all the indexes do not match. + """ + if not isinstance(index, pd.Index): + raise TypeError("Non-index object is used as index") + if not name.isidentifier(): + raise ValueError(f"name={name!r} is not a valid identifier") + + starts = _convert_to_linear_expr_series_and_validate_index(starts, index) + sizes = _convert_to_integral_series_and_validate_index(sizes, index) + interval_array = [] + for i in index: + interval_array.append( + self.new_fixed_size_interval_var( + start=starts[i], + size=sizes[i], + name=f"{name}[{i}]", + ) + ) + return pd.Series(index=index, data=interval_array) + + def new_optional_interval_var( + self, + start: LinearExprT, + size: LinearExprT, + end: LinearExprT, + is_present: LiteralT, + name: str, + ) -> IntervalVar: + """Creates an optional interval var from start, size, end, and is_present. + + An optional interval variable is a constraint, that is itself used in other + constraints like NoOverlap. This constraint is protected by a presence + literal that indicates if it is active or not. + + Internally, it ensures that `is_present` implies `start + size == + end`. - def get_or_make_boolean_index(self, arg: LiteralT) -> int: - """Returns an index from a boolean expression.""" - if isinstance(arg, IntVar): - self.assert_is_boolean_variable(arg) - return arg.index - if isinstance(arg, cmh.NotBooleanVariable): - self.assert_is_boolean_variable(arg.negated()) - return arg.index - if isinstance(arg, IntegralTypes): - if arg == ~int(False): - return self.get_or_make_index_from_constant(1) - if arg == ~int(True): - return self.get_or_make_index_from_constant(0) - arg = cmn.assert_is_zero_or_one(arg) - return self.get_or_make_index_from_constant(arg) - if cmn.is_boolean(arg): - return self.get_or_make_index_from_constant(int(arg)) - raise TypeError( - "not supported:" f" model.get_or_make_boolean_index({type(arg).__name__!r})" - ) + Args: + start: The start of the interval. It must be of the form a * var + b. + size: The size of the interval. It must be of the form a * var + b. + end: The end of the interval. It must be of the form a * var + b. + is_present: A literal that indicates if the interval is active or not. A + inactive interval is simply ignored by all constraints. + name: The name of the interval variable. - def get_interval_index(self, arg: IntervalVar) -> int: - if not isinstance(arg, IntervalVar): - raise TypeError( - f"NotSupported: model.get_interval_index({type(arg).__name__!r})" - ) - return arg.index - - def get_or_make_index_from_constant(self, value: IntegralT) -> int: - if value in self.__constant_map: - return self.__constant_map[value] - constant_var = self.new_int_var(value, value, "") - self.__constant_map[value] = constant_var.index - return constant_var.index - - def parse_linear_expression( - self, linear_expr: LinearExprT, negate: bool = False - ) -> cp_model_pb2.LinearExpressionProto: - """Returns a LinearExpressionProto built from a LinearExpr instance.""" - result: cp_model_pb2.LinearExpressionProto = ( - cp_model_pb2.LinearExpressionProto() - ) - mult = -1 if negate else 1 - if isinstance(linear_expr, IntegralTypes): - result.offset = int(linear_expr) * mult - return result - - # Raises TypeError if linear_expr is not an integer. - flat_expr = cmh.FlatIntExpr(linear_expr) - result.offset = flat_expr.offset * mult - for var in flat_expr.vars: - result.vars.append(var.index) - for coeff in flat_expr.coeffs: - result.coeffs.append(coeff * mult) - return result - - def _set_objective(self, obj: ObjLinearExprT, minimize: bool): - """Sets the objective of the model.""" - self.clear_objective() - if isinstance(obj, IntegralTypes): - self.__model.objective.offset = int(obj) - self.__model.objective.scaling_factor = 1.0 - elif isinstance(obj, LinearExpr): - if obj.is_integer(): - int_obj = cmh.FlatIntExpr(obj) - for var in int_obj.vars: - self.__model.objective.vars.append(var.index) - if minimize: - self.__model.objective.scaling_factor = 1.0 - self.__model.objective.offset = int_obj.offset - self.__model.objective.coeffs.extend(int_obj.coeffs) - else: - self.__model.objective.scaling_factor = -1.0 - self.__model.objective.offset = -int_obj.offset - for c in int_obj.coeffs: - self.__model.objective.coeffs.append(-c) - else: - float_obj = cmh.FlatFloatExpr(obj) - for var in float_obj.vars: - self.__model.floating_point_objective.vars.append(var.index) - self.__model.floating_point_objective.coeffs.extend(float_obj.coeffs) - self.__model.floating_point_objective.maximize = not minimize - self.__model.floating_point_objective.offset = float_obj.offset - else: - raise TypeError( - f"TypeError: {type(obj).__name__!r} is not a valid objective" - ) + Returns: + An `IntervalVar` object. + """ - def minimize(self, obj: ObjLinearExprT): - """Sets the objective of the model to minimize(obj).""" - self._set_objective(obj, minimize=True) + # Creates the IntervalConstraintProto object. + is_present_index = self.get_or_make_boolean_index(is_present) + start_expr = self.parse_linear_expression(start) + size_expr = self.parse_linear_expression(size) + end_expr = self.parse_linear_expression(end) + if len(start_expr.vars) > 1: + raise TypeError( + "cp_model.new_interval_var: start must be affine or constant." + ) + if len(size_expr.vars) > 1: + raise TypeError( + "cp_model.new_interval_var: size must be affine or constant." + ) + if len(end_expr.vars) > 1: + raise TypeError( + "cp_model.new_interval_var: end must be affine or constant." + ) + return IntervalVar( + self.__model, + self.__var_list, + start_expr, + size_expr, + end_expr, + is_present_index, + name, + ) + + def new_optional_interval_var_series( + self, + name: str, + index: pd.Index, + starts: Union[LinearExprT, pd.Series], + sizes: Union[LinearExprT, pd.Series], + ends: Union[LinearExprT, pd.Series], + are_present: Union[LiteralT, pd.Series], + ) -> pd.Series: + """Creates a series of interval variables with the given name. - def maximize(self, obj: ObjLinearExprT): - """Sets the objective of the model to maximize(obj).""" - self._set_objective(obj, minimize=False) + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. + starts (Union[LinearExprT, pd.Series]): The start of each interval in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. + sizes (Union[LinearExprT, pd.Series]): The size of each interval in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. + ends (Union[LinearExprT, pd.Series]): The ends of each interval in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. + are_present (Union[LiteralT, pd.Series]): The performed literal of each + interval in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. - def has_objective(self) -> bool: - return self.__model.HasField("objective") or self.__model.HasField( - "floating_point_objective" - ) + Returns: + pd.Series: The interval variable set indexed by its corresponding + dimensions. - def clear_objective(self): - self.__model.ClearField("objective") - self.__model.ClearField("floating_point_objective") - - def add_decision_strategy( - self, - variables: Sequence[IntVar], - var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, - domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, - ) -> None: - """Adds a search strategy to the model. - - Args: - variables: a list of variables this strategy will assign. - var_strategy: heuristic to choose the next variable to assign. - domain_strategy: heuristic to reduce the domain of the selected variable. - Currently, this is advanced code: the union of all strategies added to - the model must be complete, i.e. instantiates all variables. Otherwise, - solve() will fail. - """ - - strategy: cp_model_pb2.DecisionStrategyProto = ( - self.__model.search_strategy.add() - ) - for v in variables: - expr = strategy.exprs.add() - if v.index >= 0: - expr.vars.append(v.index) - expr.coeffs.append(1) - else: - expr.vars.append(self.negated(v.index)) - expr.coeffs.append(-1) - expr.offset = 1 - - strategy.variable_selection_strategy = var_strategy - strategy.domain_reduction_strategy = domain_strategy - - def model_stats(self) -> str: - """Returns a string containing some model statistics.""" - return cmh.CpSatHelper.model_stats(self.__model) - - def validate(self) -> str: - """Returns a string indicating that the model is invalid.""" - return cmh.CpSatHelper.validate_model(self.__model) - - def export_to_file(self, file: str) -> bool: - """Write the model as a protocol buffer to 'file'. - - Args: - file: file to write the model to. If the filename ends with 'txt', the - model will be written as a text file, otherwise, the binary format will - be used. - - Returns: - True if the model was correctly written. - """ - return cmh.CpSatHelper.write_model_to_file(self.__model, file) - - @overload - def add_hint(self, var: IntVar, value: int) -> None: ... - - @overload - def add_hint(self, literal: BoolVarT, value: bool) -> None: ... - - def add_hint(self, var, value) -> None: - """Adds 'var == value' as a hint to the solver.""" - if var.index >= 0: - self.__model.solution_hint.vars.append(self.get_or_make_index(var)) - self.__model.solution_hint.values.append(int(value)) + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the all the indexes do not match. + """ + if not isinstance(index, pd.Index): + raise TypeError("Non-index object is used as index") + if not name.isidentifier(): + raise ValueError(f"name={name!r} is not a valid identifier") + + starts = _convert_to_linear_expr_series_and_validate_index(starts, index) + sizes = _convert_to_linear_expr_series_and_validate_index(sizes, index) + ends = _convert_to_linear_expr_series_and_validate_index(ends, index) + are_present = _convert_to_literal_series_and_validate_index( + are_present, index + ) + + interval_array = [] + for i in index: + interval_array.append( + self.new_optional_interval_var( + start=starts[i], + size=sizes[i], + end=ends[i], + is_present=are_present[i], + name=f"{name}[{i}]", + ) + ) + return pd.Series(index=index, data=interval_array) + + def new_optional_fixed_size_interval_var( + self, + start: LinearExprT, + size: IntegralT, + is_present: LiteralT, + name: str, + ) -> IntervalVar: + """Creates an interval variable from start, and a fixed size. + + An interval variable is a constraint, that is itself used in other + constraints like NoOverlap. + + Args: + start: The start of the interval. It must be of the form a * var + b. + size: The size of the interval. It must be an integer value. + is_present: A literal that indicates if the interval is active or not. A + inactive interval is simply ignored by all constraints. + name: The name of the interval variable. + + Returns: + An `IntervalVar` object. + """ + start_expr = self.parse_linear_expression(start) + size_expr = self.parse_linear_expression(size) + end_expr = self.parse_linear_expression(start + size) + if len(start_expr.vars) > 1: + raise TypeError( + "cp_model.new_interval_var: start must be affine or constant." + ) + is_present_index = self.get_or_make_boolean_index(is_present) + return IntervalVar( + self.__model, + self.__var_list, + start_expr, + size_expr, + end_expr, + is_present_index, + name, + ) + + def new_optional_fixed_size_interval_var_series( + self, + name: str, + index: pd.Index, + starts: Union[LinearExprT, pd.Series], + sizes: Union[IntegralT, pd.Series], + are_present: Union[LiteralT, pd.Series], + ) -> pd.Series: + """Creates a series of interval variables with the given name. + + Args: + name (str): Required. The name of the variable set. + index (pd.Index): Required. The index to use for the variable set. + starts (Union[LinearExprT, pd.Series]): The start of each interval in the + set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. + sizes (Union[IntegralT, pd.Series]): The fixed size of each interval in + the set. If a `pd.Series` is passed in, it will be based on the + corresponding values of the pd.Series. + are_present (Union[LiteralT, pd.Series]): The performed literal of each + interval in the set. If a `pd.Series` is passed in, it will be based on + the corresponding values of the pd.Series. + + Returns: + pd.Series: The interval variable set indexed by its corresponding + dimensions. + + Raises: + TypeError: if the `index` is invalid (e.g. a `DataFrame`). + ValueError: if the `name` is not a valid identifier or already exists. + ValueError: if the all the indexes do not match. + """ + if not isinstance(index, pd.Index): + raise TypeError("Non-index object is used as index") + if not name.isidentifier(): + raise ValueError(f"name={name!r} is not a valid identifier") + + starts = _convert_to_linear_expr_series_and_validate_index(starts, index) + sizes = _convert_to_integral_series_and_validate_index(sizes, index) + are_present = _convert_to_literal_series_and_validate_index( + are_present, index + ) + interval_array = [] + for i in index: + interval_array.append( + self.new_optional_fixed_size_interval_var( + start=starts[i], + size=sizes[i], + is_present=are_present[i], + name=f"{name}[{i}]", + ) + ) + return pd.Series(index=index, data=interval_array) + + def add_no_overlap(self, interval_vars: Iterable[IntervalVar]) -> Constraint: + """Adds NoOverlap(interval_vars). + + A NoOverlap constraint ensures that all present intervals do not overlap + in time. + + Args: + interval_vars: The list of interval variables to constrain. + + Returns: + An instance of the `Constraint` class. + """ + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.no_overlap.intervals.extend( + [self.get_interval_index(x) for x in interval_vars] + ) + return ct + + def add_no_overlap_2d( + self, + x_intervals: Iterable[IntervalVar], + y_intervals: Iterable[IntervalVar], + ) -> Constraint: + """Adds NoOverlap2D(x_intervals, y_intervals). + + A NoOverlap2D constraint ensures that all present rectangles do not overlap + on a plane. Each rectangle is aligned with the X and Y axis, and is defined + by two intervals which represent its projection onto the X and Y axis. + + Furthermore, one box is optional if at least one of the x or y interval is + optional. + + Args: + x_intervals: The X coordinates of the rectangles. + y_intervals: The Y coordinates of the rectangles. + + Returns: + An instance of the `Constraint` class. + """ + ct = Constraint(self) + model_ct = self.__model.constraints[ct.index] + model_ct.no_overlap_2d.x_intervals.extend( + [self.get_interval_index(x) for x in x_intervals] + ) + model_ct.no_overlap_2d.y_intervals.extend( + [self.get_interval_index(x) for x in y_intervals] + ) + return ct + + def add_cumulative( + self, + intervals: Iterable[IntervalVar], + demands: Iterable[LinearExprT], + capacity: LinearExprT, + ) -> Constraint: + """Adds Cumulative(intervals, demands, capacity). + + This constraint enforces that: + + for all t: + sum(demands[i] + if (start(intervals[i]) <= t < end(intervals[i])) and + (intervals[i] is present)) <= capacity + + Args: + intervals: The list of intervals. + demands: The list of demands for each interval. Each demand must be >= 0. + Each demand can be a 1-var affine expression (a * x + b). + capacity: The maximum capacity of the cumulative constraint. It can be a + 1-var affine expression (a * x + b). + + Returns: + An instance of the `Constraint` class. + """ + cumulative = Constraint(self) + model_ct = self.__model.constraints[cumulative.index] + model_ct.cumulative.intervals.extend( + [self.get_interval_index(x) for x in intervals] + ) + for d in demands: + model_ct.cumulative.demands.append(self.parse_linear_expression(d)) + model_ct.cumulative.capacity.CopyFrom( + self.parse_linear_expression(capacity) + ) + return cumulative + + # Support for model cloning. + def clone(self) -> "CpModel": + """Reset the model, and creates a new one from a CpModelProto instance.""" + clone = CpModel() + clone.proto.CopyFrom(self.proto) + clone.rebuild_var_and_constant_map() + return clone + + def rebuild_var_and_constant_map(self): + """Internal method used during model cloning.""" + for i, var in enumerate(self.__model.variables): + if len(var.domain) == 2 and var.domain[0] == var.domain[1]: + self.__constant_map[var.domain[0]] = i + is_boolean = ( + len(var.domain) == 2 and var.domain[0] >= 0 and var.domain[1] <= 1 + ) + self.__var_list.append(IntVar(self.__model, i, is_boolean, None)) + + def get_bool_var_from_proto_index(self, index: int) -> IntVar: + """Returns an already created Boolean variable from its index.""" + result = self._get_int_var(index) + if not result.is_boolean: + raise ValueError( + f"get_bool_var_from_proto_index: index {index} does not reference a" + " boolean variable" + ) + return result + + def get_int_var_from_proto_index(self, index: int) -> IntVar: + """Returns an already created integer variable from its index.""" + return self._get_int_var(index) + + def get_interval_var_from_proto_index(self, index: int) -> IntervalVar: + """Returns an already created interval variable from its index.""" + if index < 0 or index >= len(self.__model.constraints): + raise ValueError( + f"get_interval_var_from_proto_index: out of bound index {index}" + ) + ct = self.__model.constraints[index] + if not ct.HasField("interval"): + raise ValueError( + f"get_interval_var_from_proto_index: index {index} does not" + " reference an" + + " interval variable" + ) + + return IntervalVar( + self.__model, self.__var_list, index, None, None, None, None + ) + + # Helpers. + + def __str__(self) -> str: + return str(self.__model) + + @property + def proto(self) -> cp_model_pb2.CpModelProto: + """Returns the underlying CpModelProto.""" + return self.__model + + def negated(self, index: int) -> int: + return -index - 1 + + def get_or_make_index(self, arg: VariableT) -> int: + """Returns the index of a variable, its negation, or a number.""" + if isinstance(arg, IntVar): + return arg.index + if isinstance(arg, IntegralTypes): + return self.get_or_make_index_from_constant(arg) + raise TypeError( + f"NotSupported: model.get_or_make_index({type(arg).__name__!r})" + ) + + def get_or_make_boolean_index(self, arg: LiteralT) -> int: + """Returns an index from a boolean expression.""" + if isinstance(arg, IntVar): + self.assert_is_boolean_variable(arg) + return arg.index + if isinstance(arg, cmh.NotBooleanVariable): + self.assert_is_boolean_variable(arg.negated()) + return arg.index + if isinstance(arg, IntegralTypes): + if arg == ~int(False): + return self.get_or_make_index_from_constant(1) + if arg == ~int(True): + return self.get_or_make_index_from_constant(0) + arg = cmn.assert_is_zero_or_one(arg) + return self.get_or_make_index_from_constant(arg) + if cmn.is_boolean(arg): + return self.get_or_make_index_from_constant(int(arg)) + raise TypeError( + "not supported:" + f" model.get_or_make_boolean_index({type(arg).__name__!r})" + ) + + def get_interval_index(self, arg: IntervalVar) -> int: + if not isinstance(arg, IntervalVar): + raise TypeError( + f"NotSupported: model.get_interval_index({type(arg).__name__!r})" + ) + return arg.index + + def get_or_make_index_from_constant(self, value: IntegralT) -> int: + if value in self.__constant_map: + return self.__constant_map[value] + constant_var = self.new_int_var(value, value, "") + self.__constant_map[value] = constant_var.index + return constant_var.index + + def parse_linear_expression( + self, linear_expr: LinearExprT, negate: bool = False + ) -> cp_model_pb2.LinearExpressionProto: + """Returns a LinearExpressionProto built from a LinearExpr instance.""" + result: cp_model_pb2.LinearExpressionProto = ( + cp_model_pb2.LinearExpressionProto() + ) + mult = -1 if negate else 1 + if isinstance(linear_expr, IntegralTypes): + result.offset = int(linear_expr) * mult + return result + + # Raises TypeError if linear_expr is not an integer. + flat_expr = cmh.FlatIntExpr(linear_expr) + result.offset = flat_expr.offset * mult + for var in flat_expr.vars: + result.vars.append(var.index) + for coeff in flat_expr.coeffs: + result.coeffs.append(coeff * mult) + return result + + def _set_objective(self, obj: ObjLinearExprT, minimize: bool): + """Sets the objective of the model.""" + self.clear_objective() + if isinstance(obj, IntegralTypes): + self.__model.objective.offset = int(obj) + self.__model.objective.scaling_factor = 1.0 + elif isinstance(obj, LinearExpr): + if obj.is_integer(): + int_obj = cmh.FlatIntExpr(obj) + for var in int_obj.vars: + self.__model.objective.vars.append(var.index) + if minimize: + self.__model.objective.scaling_factor = 1.0 + self.__model.objective.offset = int_obj.offset + self.__model.objective.coeffs.extend(int_obj.coeffs) else: - self.__model.solution_hint.vars.append(self.negated(var.index)) - self.__model.solution_hint.values.append(int(not value)) - - def clear_hints(self): - """Removes any solution hint from the model.""" - self.__model.ClearField("solution_hint") - - def add_assumption(self, lit: LiteralT) -> None: - """Adds the literal to the model as assumptions.""" - self.__model.assumptions.append(self.get_or_make_boolean_index(lit)) - - def add_assumptions(self, literals: Iterable[LiteralT]) -> None: - """Adds the literals to the model as assumptions.""" - for lit in literals: - self.add_assumption(lit) - - def clear_assumptions(self) -> None: - """Removes all assumptions from the model.""" - self.__model.ClearField("assumptions") - - # Helpers. - def assert_is_boolean_variable(self, x: LiteralT) -> None: - if isinstance(x, IntVar): - var = self.__model.variables[x.index] - if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: - raise TypeError( - f"TypeError: {type(x).__name__!r} is not a boolean variable" - ) - elif not isinstance(x, cmh.NotBooleanVariable): - raise TypeError( - f"TypeError: {type(x).__name__!r} is not a boolean variable" - ) + self.__model.objective.scaling_factor = -1.0 + self.__model.objective.offset = -int_obj.offset + for c in int_obj.coeffs: + self.__model.objective.coeffs.append(-c) + else: + float_obj = cmh.FlatFloatExpr(obj) + for var in float_obj.vars: + self.__model.floating_point_objective.vars.append(var.index) + self.__model.floating_point_objective.coeffs.extend(float_obj.coeffs) + self.__model.floating_point_objective.maximize = not minimize + self.__model.floating_point_objective.offset = float_obj.offset + else: + raise TypeError( + f"TypeError: {type(obj).__name__!r} is not a valid objective" + ) + + def minimize(self, obj: ObjLinearExprT): + """Sets the objective of the model to minimize(obj).""" + self._set_objective(obj, minimize=True) + + def maximize(self, obj: ObjLinearExprT): + """Sets the objective of the model to maximize(obj).""" + self._set_objective(obj, minimize=False) + + def has_objective(self) -> bool: + return self.__model.HasField("objective") or self.__model.HasField( + "floating_point_objective" + ) + + def clear_objective(self): + self.__model.ClearField("objective") + self.__model.ClearField("floating_point_objective") + + def add_decision_strategy( + self, + variables: Sequence[IntVar], + var_strategy: cp_model_pb2.DecisionStrategyProto.VariableSelectionStrategy, + domain_strategy: cp_model_pb2.DecisionStrategyProto.DomainReductionStrategy, + ) -> None: + """Adds a search strategy to the model. - # Compatibility with pre PEP8 - # pylint: disable=invalid-name - - def Name(self) -> str: - return self.name - - def SetName(self, name: str) -> None: - self.name = name - - def Proto(self) -> cp_model_pb2.CpModelProto: - return self.proto - - NewIntVar = new_int_var - NewIntVarFromDomain = new_int_var_from_domain - NewBoolVar = new_bool_var - NewConstant = new_constant - NewIntVarSeries = new_int_var_series - NewBoolVarSeries = new_bool_var_series - AddLinearConstraint = add_linear_constraint - AddLinearExpressionInDomain = add_linear_expression_in_domain - Add = add - AddAllDifferent = add_all_different - AddElement = add_element - AddCircuit = add_circuit - AddMultipleCircuit = add_multiple_circuit - AddAllowedAssignments = add_allowed_assignments - AddForbiddenAssignments = add_forbidden_assignments - AddAutomaton = add_automaton - AddInverse = add_inverse - AddReservoirConstraint = add_reservoir_constraint - AddReservoirConstraintWithActive = add_reservoir_constraint_with_active - AddImplication = add_implication - AddBoolOr = add_bool_or - AddAtLeastOne = add_at_least_one - AddAtMostOne = add_at_most_one - AddExactlyOne = add_exactly_one - AddBoolAnd = add_bool_and - AddBoolXOr = add_bool_xor - AddMinEquality = add_min_equality - AddMaxEquality = add_max_equality - AddDivisionEquality = add_division_equality - AddAbsEquality = add_abs_equality - AddModuloEquality = add_modulo_equality - AddMultiplicationEquality = add_multiplication_equality - NewIntervalVar = new_interval_var - NewIntervalVarSeries = new_interval_var_series - NewFixedSizeIntervalVar = new_fixed_size_interval_var - NewOptionalIntervalVar = new_optional_interval_var - NewOptionalIntervalVarSeries = new_optional_interval_var_series - NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var - NewOptionalFixedSizeIntervalVarSeries = new_optional_fixed_size_interval_var_series - AddNoOverlap = add_no_overlap - AddNoOverlap2D = add_no_overlap_2d - AddCumulative = add_cumulative - Clone = clone - GetBoolVarFromProtoIndex = get_bool_var_from_proto_index - GetIntVarFromProtoIndex = get_int_var_from_proto_index - GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index - Minimize = minimize - Maximize = maximize - HasObjective = has_objective - ClearObjective = clear_objective - AddDecisionStrategy = add_decision_strategy - ModelStats = model_stats - Validate = validate - ExportToFile = export_to_file - AddHint = add_hint - ClearHints = clear_hints - AddAssumption = add_assumption - AddAssumptions = add_assumptions - ClearAssumptions = clear_assumptions - - # pylint: enable=invalid-name + Args: + variables: a list of variables this strategy will assign. + var_strategy: heuristic to choose the next variable to assign. + domain_strategy: heuristic to reduce the domain of the selected variable. + Currently, this is advanced code: the union of all strategies added to + the model must be complete, i.e. instantiates all variables. Otherwise, + solve() will fail. + """ + + strategy: cp_model_pb2.DecisionStrategyProto = ( + self.__model.search_strategy.add() + ) + for v in variables: + expr = strategy.exprs.add() + if v.index >= 0: + expr.vars.append(v.index) + expr.coeffs.append(1) + else: + expr.vars.append(self.negated(v.index)) + expr.coeffs.append(-1) + expr.offset = 1 + + strategy.variable_selection_strategy = var_strategy + strategy.domain_reduction_strategy = domain_strategy + + def model_stats(self) -> str: + """Returns a string containing some model statistics.""" + return cmh.CpSatHelper.model_stats(self.__model) + + def validate(self) -> str: + """Returns a string indicating that the model is invalid.""" + return cmh.CpSatHelper.validate_model(self.__model) + + def export_to_file(self, file: str) -> bool: + """Write the model as a protocol buffer to 'file'. + + Args: + file: file to write the model to. If the filename ends with 'txt', the + model will be written as a text file, otherwise, the binary format will + be used. + + Returns: + True if the model was correctly written. + """ + return cmh.CpSatHelper.write_model_to_file(self.__model, file) + + def remove_all_names(self) -> None: + """Removes all names from the model.""" + self.__model.ClearField("name") + for v in self.__model.variables: + v.ClearField("name") + for c in self.__model.constraints: + c.ClearField("name") + + @overload + def add_hint(self, var: IntVar, value: int) -> None: + ... + + @overload + def add_hint(self, literal: BoolVarT, value: bool) -> None: + ... + + def add_hint(self, var, value) -> None: + """Adds 'var == value' as a hint to the solver.""" + if var.index >= 0: + self.__model.solution_hint.vars.append(self.get_or_make_index(var)) + self.__model.solution_hint.values.append(int(value)) + else: + self.__model.solution_hint.vars.append(self.negated(var.index)) + self.__model.solution_hint.values.append(int(not value)) + + def clear_hints(self): + """Removes any solution hint from the model.""" + self.__model.ClearField("solution_hint") + + def add_assumption(self, lit: LiteralT) -> None: + """Adds the literal to the model as assumptions.""" + self.__model.assumptions.append(self.get_or_make_boolean_index(lit)) + + def add_assumptions(self, literals: Iterable[LiteralT]) -> None: + """Adds the literals to the model as assumptions.""" + for lit in literals: + self.add_assumption(lit) + + def clear_assumptions(self) -> None: + """Removes all assumptions from the model.""" + self.__model.ClearField("assumptions") + + # Helpers. + def assert_is_boolean_variable(self, x: LiteralT) -> None: + if isinstance(x, IntVar): + var = self.__model.variables[x.index] + if len(var.domain) != 2 or var.domain[0] < 0 or var.domain[1] > 1: + raise TypeError( + f"TypeError: {type(x).__name__!r} is not a boolean variable" + ) + elif not isinstance(x, cmh.NotBooleanVariable): + raise TypeError( + f"TypeError: {type(x).__name__!r} is not a boolean variable" + ) + + # Compatibility with pre PEP8 + # pylint: disable=invalid-name + + def Name(self) -> str: + return self.name + + def SetName(self, name: str) -> None: + self.name = name + + def Proto(self) -> cp_model_pb2.CpModelProto: + return self.proto + + NewIntVar = new_int_var + NewIntVarFromDomain = new_int_var_from_domain + NewBoolVar = new_bool_var + NewConstant = new_constant + NewIntVarSeries = new_int_var_series + NewBoolVarSeries = new_bool_var_series + AddLinearConstraint = add_linear_constraint + AddLinearExpressionInDomain = add_linear_expression_in_domain + Add = add + AddAllDifferent = add_all_different + AddElement = add_element + AddCircuit = add_circuit + AddMultipleCircuit = add_multiple_circuit + AddAllowedAssignments = add_allowed_assignments + AddForbiddenAssignments = add_forbidden_assignments + AddAutomaton = add_automaton + AddInverse = add_inverse + AddReservoirConstraint = add_reservoir_constraint + AddReservoirConstraintWithActive = add_reservoir_constraint_with_active + AddImplication = add_implication + AddBoolOr = add_bool_or + AddAtLeastOne = add_at_least_one + AddAtMostOne = add_at_most_one + AddExactlyOne = add_exactly_one + AddBoolAnd = add_bool_and + AddBoolXOr = add_bool_xor + AddMinEquality = add_min_equality + AddMaxEquality = add_max_equality + AddDivisionEquality = add_division_equality + AddAbsEquality = add_abs_equality + AddModuloEquality = add_modulo_equality + AddMultiplicationEquality = add_multiplication_equality + NewIntervalVar = new_interval_var + NewIntervalVarSeries = new_interval_var_series + NewFixedSizeIntervalVar = new_fixed_size_interval_var + NewOptionalIntervalVar = new_optional_interval_var + NewOptionalIntervalVarSeries = new_optional_interval_var_series + NewOptionalFixedSizeIntervalVar = new_optional_fixed_size_interval_var + NewOptionalFixedSizeIntervalVarSeries = ( + new_optional_fixed_size_interval_var_series + ) + AddNoOverlap = add_no_overlap + AddNoOverlap2D = add_no_overlap_2d + AddCumulative = add_cumulative + Clone = clone + GetBoolVarFromProtoIndex = get_bool_var_from_proto_index + GetIntVarFromProtoIndex = get_int_var_from_proto_index + GetIntervalVarFromProtoIndex = get_interval_var_from_proto_index + Minimize = minimize + Maximize = maximize + HasObjective = has_objective + ClearObjective = clear_objective + AddDecisionStrategy = add_decision_strategy + ModelStats = model_stats + Validate = validate + ExportToFile = export_to_file + AddHint = add_hint + ClearHints = clear_hints + AddAssumption = add_assumption + AddAssumptions = add_assumptions + ClearAssumptions = clear_assumptions + + # pylint: enable=invalid-name @overload def expand_generator_or_tuple( args: Union[Tuple[LiteralT, ...], Iterable[LiteralT]], -) -> Union[Iterable[LiteralT], LiteralT]: ... +) -> Union[Iterable[LiteralT], LiteralT]: + ... @overload def expand_generator_or_tuple( args: Union[Tuple[LinearExprT, ...], Iterable[LinearExprT]], -) -> Union[Iterable[LinearExprT], LinearExprT]: ... +) -> Union[Iterable[LinearExprT], LinearExprT]: + ... def expand_generator_or_tuple(args): - if hasattr(args, "__len__"): # Tuple - if len(args) != 1: - return args - if isinstance(args[0], (NumberTypes, LinearExpr)): - return args - # Generator - return args[0] + if hasattr(args, "__len__"): # Tuple + if len(args) != 1: + return args + if isinstance(args[0], (NumberTypes, LinearExpr)): + return args + # Generator + return args[0] class CpSolver: - """Main solver class. - - The purpose of this class is to search for a solution to the model provided - to the solve() method. - - Once solve() is called, this class allows inspecting the solution found - with the value() and boolean_value() methods, as well as general statistics - about the solve procedure. - """ + """Main solver class. + + The purpose of this class is to search for a solution to the model provided + to the solve() method. + + Once solve() is called, this class allows inspecting the solution found + with the value() and boolean_value() methods, as well as general statistics + about the solve procedure. + """ + + def __init__(self) -> None: + self.__response_wrapper: Optional[cmh.ResponseWrapper] = None + self.parameters: sat_parameters_pb2.SatParameters = ( + sat_parameters_pb2.SatParameters() + ) + self.log_callback: Optional[Callable[[str], None]] = None + self.best_bound_callback: Optional[Callable[[float], None]] = None + self.__solve_wrapper: Optional[cmh.SolveWrapper] = None + self.__lock: threading.Lock = threading.Lock() + + def solve( + self, + model: CpModel, + solution_callback: Optional["CpSolverSolutionCallback"] = None, + ) -> cp_model_pb2.CpSolverStatus: + """Solves a problem and passes each solution to the callback if not null.""" + with self.__lock: + self.__solve_wrapper = cmh.SolveWrapper() + + self.__solve_wrapper.set_parameters(self.parameters) + if solution_callback is not None: + self.__solve_wrapper.add_solution_callback(solution_callback) + + if self.log_callback is not None: + self.__solve_wrapper.add_log_callback(self.log_callback) + + if self.best_bound_callback is not None: + self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback) + + self.__response_wrapper = ( + self.__solve_wrapper.solve_and_return_response_wrapper(model.proto) + ) + + if solution_callback is not None: + self.__solve_wrapper.clear_solution_callback(solution_callback) + + with self.__lock: + self.__solve_wrapper = None + + return self.__response_wrapper.status() + + def stop_search(self) -> None: + """Stops the current search asynchronously.""" + with self.__lock: + if self.__solve_wrapper: + self.__solve_wrapper.stop_search() + + def value(self, expression: LinearExprT) -> int: + """Returns the value of a linear expression after solve.""" + return self._checked_response.value(expression) + + def values(self, variables: _IndexOrSeries) -> pd.Series: + """Returns the values of the input variables. + + If `variables` is a `pd.Index`, then the output will be indexed by the + variables. If `variables` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. - def __init__(self) -> None: - self.__response_wrapper: Optional[cmh.ResponseWrapper] = None - self.parameters: sat_parameters_pb2.SatParameters = ( - sat_parameters_pb2.SatParameters() - ) - self.log_callback: Optional[Callable[[str], None]] = None - self.best_bound_callback: Optional[Callable[[float], None]] = None - self.__solve_wrapper: Optional[cmh.SolveWrapper] = None - self.__lock: threading.Lock = threading.Lock() - - def solve( - self, - model: CpModel, - solution_callback: Optional["CpSolverSolutionCallback"] = None, - ) -> cp_model_pb2.CpSolverStatus: - """Solves a problem and passes each solution to the callback if not null.""" - with self.__lock: - self.__solve_wrapper = cmh.SolveWrapper() - - self.__solve_wrapper.set_parameters(self.parameters) - if solution_callback is not None: - self.__solve_wrapper.add_solution_callback(solution_callback) - - if self.log_callback is not None: - self.__solve_wrapper.add_log_callback(self.log_callback) - - if self.best_bound_callback is not None: - self.__solve_wrapper.add_best_bound_callback(self.best_bound_callback) - - self.__response_wrapper = ( - self.__solve_wrapper.solve_and_return_response_wrapper(model.proto) - ) - - if solution_callback is not None: - self.__solve_wrapper.clear_solution_callback(solution_callback) + Args: + variables (Union[pd.Index, pd.Series]): The set of variables from which to + get the values. - with self.__lock: - self.__solve_wrapper = None + Returns: + pd.Series: The values of all variables in the set. - return self.__response_wrapper.status() + Raises: + RuntimeError: if solve() has not been called. + """ + if self.__response_wrapper is None: + raise RuntimeError("solve() has not been called.") + return pd.Series( + data=[self.__response_wrapper.value(var) for var in variables], + index=_get_index(variables), + ) - def stop_search(self) -> None: - """Stops the current search asynchronously.""" - with self.__lock: - if self.__solve_wrapper: - self.__solve_wrapper.stop_search() + def float_value(self, expression: LinearExprT) -> float: + """Returns the value of a linear expression after solve.""" + return self._checked_response.float_value(expression) - def value(self, expression: LinearExprT) -> int: - """Returns the value of a linear expression after solve.""" - return self._checked_response.value(expression) + def float_values(self, expressions: _IndexOrSeries) -> pd.Series: + """Returns the float values of the input linear expressions. - def values(self, variables: _IndexOrSeries) -> pd.Series: - """Returns the values of the input variables. + If `expressions` is a `pd.Index`, then the output will be indexed by the + variables. If `variables` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. - If `variables` is a `pd.Index`, then the output will be indexed by the - variables. If `variables` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. + Args: + expressions (Union[pd.Index, pd.Series]): The set of expressions from + which to get the values. - Args: - variables (Union[pd.Index, pd.Series]): The set of variables from which to - get the values. + Returns: + pd.Series: The values of all variables in the set. - Returns: - pd.Series: The values of all variables in the set. + Raises: + RuntimeError: if solve() has not been called. + """ + if self.__response_wrapper is None: + raise RuntimeError("solve() has not been called.") + return pd.Series( + data=[ + self.__response_wrapper.float_value(expr) for expr in expressions + ], + index=_get_index(expressions), + ) + + def boolean_value(self, literal: LiteralT) -> bool: + """Returns the boolean value of a literal after solve.""" + return self._checked_response.boolean_value(literal) + + def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: + """Returns the values of the input variables. + + If `variables` is a `pd.Index`, then the output will be indexed by the + variables. If `variables` is a `pd.Series` indexed by the underlying + dimensions, then the output will be indexed by the same underlying + dimensions. - Raises: - RuntimeError: if solve() has not been called. - """ - if self.__response_wrapper is None: - raise RuntimeError("solve() has not been called.") - return pd.Series( - data=[self.__response_wrapper.value(var) for var in variables], - index=_get_index(variables), - ) + Args: + variables (Union[pd.Index, pd.Series]): The set of variables from which to + get the values. - def float_value(self, expression: LinearExprT) -> float: - """Returns the value of a linear expression after solve.""" - return self._checked_response.float_value(expression) - - def float_values(self, expressions: _IndexOrSeries) -> pd.Series: - """Returns the float values of the input linear expressions. - - If `expressions` is a `pd.Index`, then the output will be indexed by the - variables. If `variables` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. - - Args: - expressions (Union[pd.Index, pd.Series]): The set of expressions from - which to get the values. - - Returns: - pd.Series: The values of all variables in the set. - - Raises: - RuntimeError: if solve() has not been called. - """ - if self.__response_wrapper is None: - raise RuntimeError("solve() has not been called.") - return pd.Series( - data=[self.__response_wrapper.float_value(expr) for expr in expressions], - index=_get_index(expressions), - ) + Returns: + pd.Series: The values of all variables in the set. - def boolean_value(self, literal: LiteralT) -> bool: - """Returns the boolean value of a literal after solve.""" - return self._checked_response.boolean_value(literal) - - def boolean_values(self, variables: _IndexOrSeries) -> pd.Series: - """Returns the values of the input variables. - - If `variables` is a `pd.Index`, then the output will be indexed by the - variables. If `variables` is a `pd.Series` indexed by the underlying - dimensions, then the output will be indexed by the same underlying - dimensions. - - Args: - variables (Union[pd.Index, pd.Series]): The set of variables from which to - get the values. - - Returns: - pd.Series: The values of all variables in the set. - - Raises: - RuntimeError: if solve() has not been called. - """ - if self.__response_wrapper is None: - raise RuntimeError("solve() has not been called.") - return pd.Series( - data=[ - self.__response_wrapper.boolean_value(literal) for literal in variables - ], - index=_get_index(variables), - ) + Raises: + RuntimeError: if solve() has not been called. + """ + if self.__response_wrapper is None: + raise RuntimeError("solve() has not been called.") + return pd.Series( + data=[ + self.__response_wrapper.boolean_value(literal) + for literal in variables + ], + index=_get_index(variables), + ) + + @property + def objective_value(self) -> float: + """Returns the value of the objective after solve.""" + return self._checked_response.objective_value() + + @property + def best_objective_bound(self) -> float: + """Returns the best lower (upper) bound found when min(max)imizing.""" + return self._checked_response.best_objective_bound() + + @property + def num_booleans(self) -> int: + """Returns the number of boolean variables managed by the SAT solver.""" + return self._checked_response.num_booleans() + + @property + def num_conflicts(self) -> int: + """Returns the number of conflicts since the creation of the solver.""" + return self._checked_response.num_conflicts() + + @property + def num_branches(self) -> int: + """Returns the number of search branches explored by the solver.""" + return self._checked_response.num_branches() + + @property + def wall_time(self) -> float: + """Returns the wall time in seconds since the creation of the solver.""" + return self._checked_response.wall_time() + + @property + def user_time(self) -> float: + """Returns the user time in seconds since the creation of the solver.""" + return self._checked_response.user_time() + + @property + def response_proto(self) -> cp_model_pb2.CpSolverResponse: + """Returns the response object.""" + return self._checked_response.response() + + def response_stats(self) -> str: + """Returns some statistics on the solution found as a string.""" + return self._checked_response.response_stats() + + def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: + """Returns the indices of the infeasible assumptions.""" + return self._checked_response.sufficient_assumptions_for_infeasibility() + + def status_name(self, status: Optional[Any] = None) -> str: + """Returns the name of the status returned by solve().""" + if status is None: + status = self._checked_response.status() + return cp_model_pb2.CpSolverStatus.Name(status) + + def solution_info(self) -> str: + """Returns some information on the solve process. + + Returns some information on how the solution was found, or the reason + why the model or the parameters are invalid. - @property - def objective_value(self) -> float: - """Returns the value of the objective after solve.""" - return self._checked_response.objective_value() + Raises: + RuntimeError: if solve() has not been called. + """ + return self._checked_response.solution_info() - @property - def best_objective_bound(self) -> float: - """Returns the best lower (upper) bound found when min(max)imizing.""" - return self._checked_response.best_objective_bound() + @property + def _checked_response(self) -> cmh.ResponseWrapper: + """Checks solve() has been called, and returns a response wrapper.""" + if self.__response_wrapper is None: + raise RuntimeError("solve() has not been called.") + return self.__response_wrapper - @property - def num_booleans(self) -> int: - """Returns the number of boolean variables managed by the SAT solver.""" - return self._checked_response.num_booleans() + # Compatibility with pre PEP8 + # pylint: disable=invalid-name - @property - def num_conflicts(self) -> int: - """Returns the number of conflicts since the creation of the solver.""" - return self._checked_response.num_conflicts() + def BestObjectiveBound(self) -> float: + return self.best_objective_bound - @property - def num_branches(self) -> int: - """Returns the number of search branches explored by the solver.""" - return self._checked_response.num_branches() + def BooleanValue(self, literal: LiteralT) -> bool: + return self.boolean_value(literal) - @property - def wall_time(self) -> float: - """Returns the wall time in seconds since the creation of the solver.""" - return self._checked_response.wall_time() + def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series: + return self.boolean_values(variables) - @property - def user_time(self) -> float: - """Returns the user time in seconds since the creation of the solver.""" - return self._checked_response.user_time() + def NumBooleans(self) -> int: + return self.num_booleans - @property - def response_proto(self) -> cp_model_pb2.CpSolverResponse: - """Returns the response object.""" - return self._checked_response.response() + def NumConflicts(self) -> int: + return self.num_conflicts - def response_stats(self) -> str: - """Returns some statistics on the solution found as a string.""" - return self._checked_response.response_stats() + def NumBranches(self) -> int: + return self.num_branches - def sufficient_assumptions_for_infeasibility(self) -> Sequence[int]: - """Returns the indices of the infeasible assumptions.""" - return self._checked_response.sufficient_assumptions_for_infeasibility() + def ObjectiveValue(self) -> float: + return self.objective_value - def status_name(self, status: Optional[Any] = None) -> str: - """Returns the name of the status returned by solve().""" - if status is None: - status = self._checked_response.status() - return cp_model_pb2.CpSolverStatus.Name(status) + def ResponseProto(self) -> cp_model_pb2.CpSolverResponse: + return self.response_proto - def solution_info(self) -> str: - """Returns some information on the solve process. + def ResponseStats(self) -> str: + return self.response_stats() - Returns some information on how the solution was found, or the reason - why the model or the parameters are invalid. + def Solve( + self, + model: CpModel, + solution_callback: Optional["CpSolverSolutionCallback"] = None, + ) -> cp_model_pb2.CpSolverStatus: + return self.solve(model, solution_callback) - Raises: - RuntimeError: if solve() has not been called. - """ - return self._checked_response.solution_info() + def SolutionInfo(self) -> str: + return self.solution_info() - @property - def _checked_response(self) -> cmh.ResponseWrapper: - """Checks solve() has been called, and returns a response wrapper.""" - if self.__response_wrapper is None: - raise RuntimeError("solve() has not been called.") - return self.__response_wrapper + def StatusName(self, status: Optional[Any] = None) -> str: + return self.status_name(status) - # Compatibility with pre PEP8 - # pylint: disable=invalid-name + def StopSearch(self) -> None: + self.stop_search() - def BestObjectiveBound(self) -> float: - return self.best_objective_bound + def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]: + return self.sufficient_assumptions_for_infeasibility() - def BooleanValue(self, literal: LiteralT) -> bool: - return self.boolean_value(literal) + def UserTime(self) -> float: + return self.user_time - def BooleanValues(self, variables: _IndexOrSeries) -> pd.Series: - return self.boolean_values(variables) + def Value(self, expression: LinearExprT) -> int: + return self.value(expression) - def NumBooleans(self) -> int: - return self.num_booleans + def Values(self, variables: _IndexOrSeries) -> pd.Series: + return self.values(variables) - def NumConflicts(self) -> int: - return self.num_conflicts + def WallTime(self) -> float: + return self.wall_time - def NumBranches(self) -> int: - return self.num_branches + def SolveWithSolutionCallback( + self, model: CpModel, callback: "CpSolverSolutionCallback" + ) -> cp_model_pb2.CpSolverStatus: + """DEPRECATED Use solve() with the callback argument.""" + warnings.warn( + "solve_with_solution_callback is deprecated; use solve() with" + + "the callback argument.", + DeprecationWarning, + ) + return self.solve(model, callback) - def ObjectiveValue(self) -> float: - return self.objective_value + def SearchForAllSolutions( + self, model: CpModel, callback: "CpSolverSolutionCallback" + ) -> cp_model_pb2.CpSolverStatus: + """DEPRECATED Use solve() with the right parameter. - def ResponseProto(self) -> cp_model_pb2.CpSolverResponse: - return self.response_proto + Search for all solutions of a satisfiability problem. - def ResponseStats(self) -> str: - return self.response_stats() - - def Solve( - self, - model: CpModel, - solution_callback: Optional["CpSolverSolutionCallback"] = None, - ) -> cp_model_pb2.CpSolverStatus: - return self.solve(model, solution_callback) + This method searches for all feasible solutions of a given model. + Then it feeds the solution to the callback. - def SolutionInfo(self) -> str: - return self.solution_info() + Note that the model cannot contain an objective. - def StatusName(self, status: Optional[Any] = None) -> str: - return self.status_name(status) + Args: + model: The model to solve. + callback: The callback that will be called at each solution. - def StopSearch(self) -> None: - self.stop_search() + Returns: + The status of the solve: - def SufficientAssumptionsForInfeasibility(self) -> Sequence[int]: - return self.sufficient_assumptions_for_infeasibility() + * *FEASIBLE* if some solutions have been found + * *INFEASIBLE* if the solver has proved there are no solution + * *OPTIMAL* if all solutions have been found + """ + warnings.warn( + "search_for_all_solutions is deprecated; use solve() with" + + "enumerate_all_solutions = True.", + DeprecationWarning, + ) + if model.has_objective(): + raise TypeError( + "Search for all solutions is only defined on satisfiability problems" + ) + # Store old parameter. + enumerate_all = self.parameters.enumerate_all_solutions + self.parameters.enumerate_all_solutions = True + + status: cp_model_pb2.CpSolverStatus = self.solve(model, callback) + + # Restore parameter. + self.parameters.enumerate_all_solutions = enumerate_all + return status - def UserTime(self) -> float: - return self.user_time - def Value(self, expression: LinearExprT) -> int: - return self.value(expression) +# pylint: enable=invalid-name - def Values(self, variables: _IndexOrSeries) -> pd.Series: - return self.values(variables) - def WallTime(self) -> float: - return self.wall_time +class CpSolverSolutionCallback(cmh.SolutionCallback): + """Solution callback. - def SolveWithSolutionCallback( - self, model: CpModel, callback: "CpSolverSolutionCallback" - ) -> cp_model_pb2.CpSolverStatus: - """DEPRECATED Use solve() with the callback argument.""" - warnings.warn( - "solve_with_solution_callback is deprecated; use solve() with" - + "the callback argument.", - DeprecationWarning, - ) - return self.solve(model, callback) + This class implements a callback that will be called at each new solution + found during search. - def SearchForAllSolutions( - self, model: CpModel, callback: "CpSolverSolutionCallback" - ) -> cp_model_pb2.CpSolverStatus: - """DEPRECATED Use solve() with the right parameter. + The method on_solution_callback() will be called by the solver, and must be + implemented. The current solution can be queried using the boolean_value() + and value() methods. - Search for all solutions of a satisfiability problem. + These methods returns the same information as their counterpart in the + `CpSolver` class. + """ - This method searches for all feasible solutions of a given model. - Then it feeds the solution to the callback. + def __init__(self) -> None: + cmh.SolutionCallback.__init__(self) - Note that the model cannot contain an objective. + def OnSolutionCallback(self) -> None: + """Proxy for the same method in snake case.""" + self.on_solution_callback() - Args: - model: The model to solve. - callback: The callback that will be called at each solution. + def boolean_value(self, lit: LiteralT) -> bool: + """Returns the boolean value of a boolean literal. - Returns: - The status of the solve: + Args: + lit: A boolean variable or its negation. - * *FEASIBLE* if some solutions have been found - * *INFEASIBLE* if the solver has proved there are no solution - * *OPTIMAL* if all solutions have been found - """ - warnings.warn( - "search_for_all_solutions is deprecated; use solve() with" - + "enumerate_all_solutions = True.", - DeprecationWarning, - ) - if model.has_objective(): - raise TypeError( - "Search for all solutions is only defined on satisfiability problems" - ) - # Store old parameter. - enumerate_all = self.parameters.enumerate_all_solutions - self.parameters.enumerate_all_solutions = True + Returns: + The Boolean value of the literal in the solution. - status: cp_model_pb2.CpSolverStatus = self.solve(model, callback) + Raises: + RuntimeError: if `lit` is not a boolean variable or its negation. + """ + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.BooleanValue(lit) - # Restore parameter. - self.parameters.enumerate_all_solutions = enumerate_all - return status + def value(self, expression: LinearExprT) -> int: + """Evaluates an linear expression in the current solution. + Args: + expression: a linear expression of the model. -# pylint: enable=invalid-name + Returns: + An integer value equal to the evaluation of the linear expression + against the current solution. + Raises: + RuntimeError: if 'expression' is not a LinearExpr. + """ + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.Value(expression) -class CpSolverSolutionCallback(cmh.SolutionCallback): - """Solution callback. + def float_value(self, expression: LinearExprT) -> float: + """Evaluates an linear expression in the current solution. - This class implements a callback that will be called at each new solution - found during search. + Args: + expression: a linear expression of the model. - The method on_solution_callback() will be called by the solver, and must be - implemented. The current solution can be queried using the boolean_value() - and value() methods. + Returns: + An integer value equal to the evaluation of the linear expression + against the current solution. - These methods returns the same information as their counterpart in the - `CpSolver` class. + Raises: + RuntimeError: if 'expression' is not a LinearExpr. """ - - def __init__(self) -> None: - cmh.SolutionCallback.__init__(self) - - def OnSolutionCallback(self) -> None: - """Proxy for the same method in snake case.""" - self.on_solution_callback() - - def boolean_value(self, lit: LiteralT) -> bool: - """Returns the boolean value of a boolean literal. - - Args: - lit: A boolean variable or its negation. - - Returns: - The Boolean value of the literal in the solution. - - Raises: - RuntimeError: if `lit` is not a boolean variable or its negation. - """ - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.BooleanValue(lit) - - def value(self, expression: LinearExprT) -> int: - """Evaluates an linear expression in the current solution. - - Args: - expression: a linear expression of the model. - - Returns: - An integer value equal to the evaluation of the linear expression - against the current solution. - - Raises: - RuntimeError: if 'expression' is not a LinearExpr. - """ - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.Value(expression) - - def float_value(self, expression: LinearExprT) -> float: - """Evaluates an linear expression in the current solution. - - Args: - expression: a linear expression of the model. - - Returns: - An integer value equal to the evaluation of the linear expression - against the current solution. - - Raises: - RuntimeError: if 'expression' is not a LinearExpr. - """ - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.FloatValue(expression) - - def has_response(self) -> bool: - return self.HasResponse() - - def stop_search(self) -> None: - """Stops the current search asynchronously.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - self.StopSearch() - - @property - def objective_value(self) -> float: - """Returns the value of the objective after solve.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.ObjectiveValue() - - @property - def best_objective_bound(self) -> float: - """Returns the best lower (upper) bound found when min(max)imizing.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.BestObjectiveBound() - - @property - def num_booleans(self) -> int: - """Returns the number of boolean variables managed by the SAT solver.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.NumBooleans() - - @property - def num_conflicts(self) -> int: - """Returns the number of conflicts since the creation of the solver.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.NumConflicts() - - @property - def num_branches(self) -> int: - """Returns the number of search branches explored by the solver.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.NumBranches() - - @property - def num_integer_propagations(self) -> int: - """Returns the number of integer propagations done by the solver.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.NumIntegerPropagations() - - @property - def num_boolean_propagations(self) -> int: - """Returns the number of Boolean propagations done by the solver.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.NumBooleanPropagations() - - @property - def deterministic_time(self) -> float: - """Returns the determistic time in seconds since the creation of the solver.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.DeterministicTime() - - @property - def wall_time(self) -> float: - """Returns the wall time in seconds since the creation of the solver.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.WallTime() - - @property - def user_time(self) -> float: - """Returns the user time in seconds since the creation of the solver.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.UserTime() - - @property - def response_proto(self) -> cp_model_pb2.CpSolverResponse: - """Returns the response object.""" - if not self.has_response(): - raise RuntimeError("solve() has not been called.") - return self.Response() + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.FloatValue(expression) + + def has_response(self) -> bool: + return self.HasResponse() + + def stop_search(self) -> None: + """Stops the current search asynchronously.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + self.StopSearch() + + @property + def objective_value(self) -> float: + """Returns the value of the objective after solve.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.ObjectiveValue() + + @property + def best_objective_bound(self) -> float: + """Returns the best lower (upper) bound found when min(max)imizing.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.BestObjectiveBound() + + @property + def num_booleans(self) -> int: + """Returns the number of boolean variables managed by the SAT solver.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.NumBooleans() + + @property + def num_conflicts(self) -> int: + """Returns the number of conflicts since the creation of the solver.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.NumConflicts() + + @property + def num_branches(self) -> int: + """Returns the number of search branches explored by the solver.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.NumBranches() + + @property + def num_integer_propagations(self) -> int: + """Returns the number of integer propagations done by the solver.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.NumIntegerPropagations() + + @property + def num_boolean_propagations(self) -> int: + """Returns the number of Boolean propagations done by the solver.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.NumBooleanPropagations() + + @property + def deterministic_time(self) -> float: + """Returns the determistic time in seconds since the creation of the solver.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.DeterministicTime() + + @property + def wall_time(self) -> float: + """Returns the wall time in seconds since the creation of the solver.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.WallTime() + + @property + def user_time(self) -> float: + """Returns the user time in seconds since the creation of the solver.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.UserTime() + + @property + def response_proto(self) -> cp_model_pb2.CpSolverResponse: + """Returns the response object.""" + if not self.has_response(): + raise RuntimeError("solve() has not been called.") + return self.Response() class ObjectiveSolutionPrinter(CpSolverSolutionCallback): - """Display the objective value and time of intermediate solutions.""" - - def __init__(self) -> None: - CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 - self.__start_time = time.time() - - def on_solution_callback(self) -> None: - """Called on each new solution.""" - current_time = time.time() - obj = self.objective_value - print( - f"Solution {self.__solution_count}, time =" - f" {current_time - self.__start_time:0.2f} s, objective = {obj}", - flush=True, - ) - self.__solution_count += 1 + """Display the objective value and time of intermediate solutions.""" - def solution_count(self) -> int: - """Returns the number of solutions found.""" - return self.__solution_count + def __init__(self) -> None: + CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 + self.__start_time = time.time() + def on_solution_callback(self) -> None: + """Called on each new solution.""" + current_time = time.time() + obj = self.objective_value + print( + f"Solution {self.__solution_count}, time =" + f" {current_time - self.__start_time:0.2f} s, objective = {obj}", + flush=True, + ) + self.__solution_count += 1 -class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback): - """Print intermediate solutions (objective, variable values, time).""" - - def __init__(self, variables: Sequence[IntVar]) -> None: - CpSolverSolutionCallback.__init__(self) - self.__variables: Sequence[IntVar] = variables - self.__solution_count: int = 0 - self.__start_time: float = time.time() - - def on_solution_callback(self) -> None: - """Called on each new solution.""" - current_time = time.time() - obj = self.objective_value - print( - f"Solution {self.__solution_count}, time =" - f" {current_time - self.__start_time:0.2f} s, objective = {obj}" - ) - for v in self.__variables: - print(f" {v} = {self.value(v)}", end=" ") - print(flush=True) - self.__solution_count += 1 + def solution_count(self) -> int: + """Returns the number of solutions found.""" + return self.__solution_count - @property - def solution_count(self) -> int: - """Returns the number of solutions found.""" - return self.__solution_count +class VarArrayAndObjectiveSolutionPrinter(CpSolverSolutionCallback): + """Print intermediate solutions (objective, variable values, time).""" + + def __init__(self, variables: Sequence[IntVar]) -> None: + CpSolverSolutionCallback.__init__(self) + self.__variables: Sequence[IntVar] = variables + self.__solution_count: int = 0 + self.__start_time: float = time.time() + + def on_solution_callback(self) -> None: + """Called on each new solution.""" + current_time = time.time() + obj = self.objective_value + print( + f"Solution {self.__solution_count}, time =" + f" {current_time - self.__start_time:0.2f} s, objective = {obj}" + ) + for v in self.__variables: + print(f" {v} = {self.value(v)}", end=" ") + print(flush=True) + self.__solution_count += 1 + + @property + def solution_count(self) -> int: + """Returns the number of solutions found.""" + return self.__solution_count -class VarArraySolutionPrinter(CpSolverSolutionCallback): - """Print intermediate solutions (variable values, time).""" - - def __init__(self, variables: Sequence[IntVar]) -> None: - CpSolverSolutionCallback.__init__(self) - self.__variables: Sequence[IntVar] = variables - self.__solution_count: int = 0 - self.__start_time: float = time.time() - - def on_solution_callback(self) -> None: - """Called on each new solution.""" - current_time = time.time() - print( - f"Solution {self.__solution_count}, time =" - f" {current_time - self.__start_time:0.2f} s" - ) - for v in self.__variables: - print(f" {v} = {self.value(v)}", end=" ") - print(flush=True) - self.__solution_count += 1 - @property - def solution_count(self) -> int: - """Returns the number of solutions found.""" - return self.__solution_count +class VarArraySolutionPrinter(CpSolverSolutionCallback): + """Print intermediate solutions (variable values, time).""" + + def __init__(self, variables: Sequence[IntVar]) -> None: + CpSolverSolutionCallback.__init__(self) + self.__variables: Sequence[IntVar] = variables + self.__solution_count: int = 0 + self.__start_time: float = time.time() + + def on_solution_callback(self) -> None: + """Called on each new solution.""" + current_time = time.time() + print( + f"Solution {self.__solution_count}, time =" + f" {current_time - self.__start_time:0.2f} s" + ) + for v in self.__variables: + print(f" {v} = {self.value(v)}", end=" ") + print(flush=True) + self.__solution_count += 1 + + @property + def solution_count(self) -> int: + """Returns the number of solutions found.""" + return self.__solution_count def _get_index(obj: _IndexOrSeries) -> pd.Index: - """Returns the indices of `obj` as a `pd.Index`.""" - if isinstance(obj, pd.Series): - return obj.index - return obj + """Returns the indices of `obj` as a `pd.Index`.""" + if isinstance(obj, pd.Series): + return obj.index + return obj def _convert_to_integral_series_and_validate_index( value_or_series: Union[IntegralT, pd.Series], index: pd.Index ) -> pd.Series: - """Returns a pd.Series of the given index with the corresponding values. - - Args: - value_or_series: the values to be converted (if applicable). - index: the index of the resulting pd.Series. - - Returns: - pd.Series: The set of values with the given index. - - Raises: - TypeError: If the type of `value_or_series` is not recognized. - ValueError: If the index does not match. - """ - if isinstance(value_or_series, IntegralTypes): - return pd.Series(data=value_or_series, index=index) - elif isinstance(value_or_series, pd.Series): - if value_or_series.index.equals(index): - return value_or_series - else: - raise ValueError("index does not match") + """Returns a pd.Series of the given index with the corresponding values. + + Args: + value_or_series: the values to be converted (if applicable). + index: the index of the resulting pd.Series. + + Returns: + pd.Series: The set of values with the given index. + + Raises: + TypeError: If the type of `value_or_series` is not recognized. + ValueError: If the index does not match. + """ + if isinstance(value_or_series, IntegralTypes): + return pd.Series(data=value_or_series, index=index) + elif isinstance(value_or_series, pd.Series): + if value_or_series.index.equals(index): + return value_or_series else: - raise TypeError(f"invalid type={type(value_or_series).__name__!r}") + raise ValueError("index does not match") + else: + raise TypeError(f"invalid type={type(value_or_series).__name__!r}") def _convert_to_linear_expr_series_and_validate_index( value_or_series: Union[LinearExprT, pd.Series], index: pd.Index ) -> pd.Series: - """Returns a pd.Series of the given index with the corresponding values. - - Args: - value_or_series: the values to be converted (if applicable). - index: the index of the resulting pd.Series. - - Returns: - pd.Series: The set of values with the given index. - - Raises: - TypeError: If the type of `value_or_series` is not recognized. - ValueError: If the index does not match. - """ - if isinstance(value_or_series, IntegralTypes): - return pd.Series(data=value_or_series, index=index) - elif isinstance(value_or_series, pd.Series): - if value_or_series.index.equals(index): - return value_or_series - else: - raise ValueError("index does not match") + """Returns a pd.Series of the given index with the corresponding values. + + Args: + value_or_series: the values to be converted (if applicable). + index: the index of the resulting pd.Series. + + Returns: + pd.Series: The set of values with the given index. + + Raises: + TypeError: If the type of `value_or_series` is not recognized. + ValueError: If the index does not match. + """ + if isinstance(value_or_series, IntegralTypes): + return pd.Series(data=value_or_series, index=index) + elif isinstance(value_or_series, pd.Series): + if value_or_series.index.equals(index): + return value_or_series else: - raise TypeError(f"invalid type={type(value_or_series).__name__!r}") + raise ValueError("index does not match") + else: + raise TypeError(f"invalid type={type(value_or_series).__name__!r}") def _convert_to_literal_series_and_validate_index( value_or_series: Union[LiteralT, pd.Series], index: pd.Index ) -> pd.Series: - """Returns a pd.Series of the given index with the corresponding values. - - Args: - value_or_series: the values to be converted (if applicable). - index: the index of the resulting pd.Series. - - Returns: - pd.Series: The set of values with the given index. - - Raises: - TypeError: If the type of `value_or_series` is not recognized. - ValueError: If the index does not match. - """ - if isinstance(value_or_series, IntegralTypes): - return pd.Series(data=value_or_series, index=index) - elif isinstance(value_or_series, pd.Series): - if value_or_series.index.equals(index): - return value_or_series - else: - raise ValueError("index does not match") + """Returns a pd.Series of the given index with the corresponding values. + + Args: + value_or_series: the values to be converted (if applicable). + index: the index of the resulting pd.Series. + + Returns: + pd.Series: The set of values with the given index. + + Raises: + TypeError: If the type of `value_or_series` is not recognized. + ValueError: If the index does not match. + """ + if isinstance(value_or_series, IntegralTypes): + return pd.Series(data=value_or_series, index=index) + elif isinstance(value_or_series, pd.Series): + if value_or_series.index.equals(index): + return value_or_series else: - raise TypeError(f"invalid type={type(value_or_series).__name__!r}") + raise ValueError("index does not match") + else: + raise TypeError(f"invalid type={type(value_or_series).__name__!r}") diff --git a/ortools/sat/python/cp_model_helper.cc b/ortools/sat/python/cp_model_helper.cc index c2aae7fd4c2..10e57c76579 100644 --- a/ortools/sat/python/cp_model_helper.cc +++ b/ortools/sat/python/cp_model_helper.cc @@ -503,8 +503,10 @@ PYBIND11_MODULE(cp_model_helper, m) { py::class_(m, "ResponseWrapper") .def("best_objective_bound", &ResponseWrapper::BestObjectiveBound) - .def("boolean_value", &ResponseWrapper::BooleanValue, py::arg("lit")) - .def("boolean_value", &ResponseWrapper::FixedBooleanValue, py::arg("lit")) + .def("boolean_value", &ResponseWrapper::BooleanValue, + py::arg("lit").none(false)) + .def("boolean_value", &ResponseWrapper::FixedBooleanValue, + py::arg("lit").none(false)) .def("deterministic_time", &ResponseWrapper::DeterministicTime) .def("num_binary_propagations", &ResponseWrapper::NumBinaryPropagations) .def("num_booleans", &ResponseWrapper::NumBooleans) @@ -520,10 +522,12 @@ PYBIND11_MODULE(cp_model_helper, m) { .def("sufficient_assumptions_for_infeasibility", &ResponseWrapper::SufficientAssumptionsForInfeasibility) .def("user_time", &ResponseWrapper::UserTime) - .def("float_value", &ResponseWrapper::FloatValue, py::arg("expr")) - .def("float_value", &ResponseWrapper::FixedFloatValue, py::arg("value")) - .def("value", &ResponseWrapper::Value, py::arg("expr")) - .def("value", &ResponseWrapper::FixedValue, py::arg("value")) + .def("float_value", &ResponseWrapper::FloatValue, + py::arg("expr").none(false)) + .def("float_value", &ResponseWrapper::FixedFloatValue, + py::arg("value").none(false)) + .def("value", &ResponseWrapper::Value, py::arg("expr").none(false)) + .def("value", &ResponseWrapper::FixedValue, py::arg("value").none(false)) .def("wall_time", &ResponseWrapper::WallTime); py::class_(m, "SolveWrapper") @@ -893,11 +897,7 @@ PYBIND11_MODULE(cp_model_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddInPlace(other); - return expr; - } - return expr->Add(other); + return (num_uses == 4) ? expr->AddInPlace(other) : expr->Add(other); }, py::arg("other").none(false), DOC(operations_research, sat, python, LinearExpr, Add)) @@ -907,11 +907,8 @@ PYBIND11_MODULE(cp_model_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddIntInPlace(cst); - return expr; - } - return expr->AddInt(cst); + return (num_uses == 4) ? expr->AddIntInPlace(cst) + : expr->AddInt(cst); }, DOC(operations_research, sat, python, LinearExpr, AddInt)) .def( @@ -920,11 +917,8 @@ PYBIND11_MODULE(cp_model_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddFloatInPlace(cst); - return expr; - } - return expr->AddFloat(cst); + return (num_uses == 4) ? expr->AddFloatInPlace(cst) + : expr->AddFloat(cst); }, py::arg("other").none(false), DOC(operations_research, sat, python, LinearExpr, AddFloat)) @@ -934,11 +928,8 @@ PYBIND11_MODULE(cp_model_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddIntInPlace(cst); - return expr; - } - return expr->AddInt(cst); + return (num_uses == 4) ? expr->AddIntInPlace(cst) + : expr->AddInt(cst); }, py::arg("cst"), DOC(operations_research, sat, python, LinearExpr, AddInt)) @@ -948,14 +939,34 @@ PYBIND11_MODULE(cp_model_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddFloatInPlace(cst); - return expr; - } - return expr->AddFloat(cst); + return (num_uses == 4) ? expr->AddFloatInPlace(cst) + : expr->AddFloat(cst); }, py::arg("cst"), DOC(operations_research, sat, python, LinearExpr, AddFloat)) + .def( + "__iadd__", + [](std::shared_ptr expr, + std::shared_ptr other) -> std::shared_ptr { + return expr->AddInPlace(other); + }, + py::arg("other").none(false), + DOC(operations_research, sat, python, LinearExpr, Add)) + .def( + "__iadd__", + [](std::shared_ptr expr, + int64_t cst) -> std::shared_ptr { + return expr->AddIntInPlace(cst); + }, + DOC(operations_research, sat, python, LinearExpr, AddInt)) + .def( + "__iadd__", + [](std::shared_ptr expr, + double cst) -> std::shared_ptr { + return expr->AddFloatInPlace(cst); + }, + py::arg("other").none(false), + DOC(operations_research, sat, python, LinearExpr, AddFloat)) .def( "__sub__", [](py::object self, @@ -963,11 +974,8 @@ PYBIND11_MODULE(cp_model_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddInPlace(other->Neg()); - return expr; - } - return expr->Sub(other); + return (num_uses == 4) ? expr->AddInPlace(other->Neg()) + : expr->Sub(other); }, py::arg("other").none(false), DOC(operations_research, sat, python, LinearExpr, Sub)) @@ -977,11 +985,8 @@ PYBIND11_MODULE(cp_model_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddIntInPlace(-cst); - return expr; - } - return expr->SubInt(cst); + return (num_uses == 4) ? expr->AddIntInPlace(-cst) + : expr->SubInt(cst); }, py::arg("cst"), DOC(operations_research, sat, python, LinearExpr, SubInt)) @@ -991,14 +996,34 @@ PYBIND11_MODULE(cp_model_helper, m) { const int num_uses = Py_REFCNT(self.ptr()); std::shared_ptr expr = self.cast>(); - if (num_uses == 4) { - expr->AddFloatInPlace(-cst); - return expr; - } - return expr->SubFloat(cst); + return (num_uses == 4) ? expr->AddFloatInPlace(-cst) + : expr->SubFloat(cst); }, py::arg("cst"), DOC(operations_research, sat, python, LinearExpr, SubFloat)) + .def( + "__isub__", + [](std::shared_ptr expr, + std::shared_ptr other) -> std::shared_ptr { + return expr->AddInPlace(other->Neg()); + }, + py::arg("other").none(false), + DOC(operations_research, sat, python, LinearExpr, Sub)) + .def( + "__isub__", + [](std::shared_ptr expr, + int64_t cst) -> std::shared_ptr { + return expr->AddIntInPlace(-cst); + }, + DOC(operations_research, sat, python, LinearExpr, SubInt)) + .def( + "__isub__", + [](std::shared_ptr expr, + double cst) -> std::shared_ptr { + return expr->AddFloatInPlace(-cst); + }, + py::arg("other").none(false), + DOC(operations_research, sat, python, LinearExpr, SubFloat)) .def_property_readonly("num_exprs", &SumArray::num_exprs) .def_property_readonly("int_offset", &SumArray::int_offset) .def_property_readonly("double_offset", &SumArray::double_offset); @@ -1010,45 +1035,9 @@ PYBIND11_MODULE(cp_model_helper, m) { .def_property_readonly("coefficient", &FloatAffine::coefficient) .def_property_readonly("offset", &FloatAffine::offset); - // We adding an operator like __add__(int), we need to add all overloads, - // otherwise they are not found. py::class_, LinearExpr>( m, "IntAffine", DOC(operations_research, sat, python, IntAffine)) .def(py::init, int64_t, int64_t>()) - .def("__add__", &LinearExpr::Add, py::arg("other").none(false), - DOC(operations_research, sat, python, LinearExpr, Add)) - .def("__add__", &IntAffine::AddInt, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, AddInt)) - .def("__add__", &LinearExpr::AddFloat, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, AddFloat)) - .def("__radd__", &LinearExpr::Add, py::arg("other").none(false), - DOC(operations_research, sat, python, LinearExpr, Add)) - .def("__radd__", &IntAffine::AddInt, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, AddInt)) - .def("__radd__", &LinearExpr::AddFloat, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, AddFloat)) - .def("__sub__", &LinearExpr::Sub, py::arg("other").none(false), - DOC(operations_research, sat, python, LinearExpr, Sub)) - .def("__sub__", &IntAffine::SubInt, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, SubInt)) - .def("__sub__", &LinearExpr::SubFloat, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, SubFloat)) - .def("__rsub__", &LinearExpr::RSub, py::arg("other").none(false), - DOC(operations_research, sat, python, LinearExpr, RSub)) - .def("__rsub__", &IntAffine::RSubInt, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, RSubInt)) - .def("__rsub__", &LinearExpr::SubFloat, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, RSubFloat)) - .def("__mul__", &IntAffine::MulInt, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, MulInt)) - .def("__mul__", &LinearExpr::MulFloat, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, MulFloat)) - .def("__rmul__", &IntAffine::MulInt, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, MulInt)) - .def("__rmul__", &LinearExpr::MulFloat, py::arg("cst"), - DOC(operations_research, sat, python, LinearExpr, MulFloat)) - .def("__neg__", &IntAffine::Neg, - DOC(operations_research, sat, python, LinearExpr, Neg)) .def_property_readonly("expression", &IntAffine::expression, "Returns the linear expression.") .def_property_readonly("coefficient", &IntAffine::coefficient, diff --git a/ortools/sat/python/cp_model_helper_test.py b/ortools/sat/python/cp_model_helper_test.py index e8ee7c4695c..0bc14ea2192 100644 --- a/ortools/sat/python/cp_model_helper_test.py +++ b/ortools/sat/python/cp_model_helper_test.py @@ -26,62 +26,62 @@ class Callback(cmh.SolutionCallback): - def __init__(self): - cmh.SolutionCallback.__init__(self) - self.__solution_count = 0 + def __init__(self): + cmh.SolutionCallback.__init__(self) + self.__solution_count = 0 - def OnSolutionCallback(self): - print("New Solution") - self.__solution_count += 1 + def OnSolutionCallback(self): + print("New Solution") + self.__solution_count += 1 - def solution_count(self): - return self.__solution_count + def solution_count(self): + return self.__solution_count class BestBoundCallback: - def __init__(self): - self.best_bound: float = 0.0 + def __init__(self): + self.best_bound: float = 0.0 - def new_best_bound(self, bb: float): - self.best_bound = bb + def new_best_bound(self, bb: float): + self.best_bound = bb class TestIntVar(cmh.BaseIntVar): - def __init__(self, index: int, name: str, is_boolean: bool = False) -> None: - cmh.BaseIntVar.__init__(self, index, is_boolean) - self._name = name + def __init__(self, index: int, name: str, is_boolean: bool = False) -> None: + cmh.BaseIntVar.__init__(self, index, is_boolean) + self._name = name - def __str__(self) -> str: - return self._name + def __str__(self) -> str: + return self._name - def __repr__(self) -> str: - return self._name + def __repr__(self) -> str: + return self._name class CpModelHelperTest(absltest.TestCase): - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() - def test_variable_domain(self): - model_string = """ + def test_variable_domain(self): + model_string = """ variables { domain: [ -10, 10 ] } variables { domain: [ -5, -5, 3, 6 ] } """ - model = cp_model_pb2.CpModelProto() - self.assertTrue(text_format.Parse(model_string, model)) + model = cp_model_pb2.CpModelProto() + self.assertTrue(text_format.Parse(model_string, model)) - d0 = cmh.CpSatHelper.variable_domain(model.variables[0]) - d1 = cmh.CpSatHelper.variable_domain(model.variables[1]) + d0 = cmh.CpSatHelper.variable_domain(model.variables[0]) + d1 = cmh.CpSatHelper.variable_domain(model.variables[1]) - self.assertEqual(d0.flattened_intervals(), [-10, 10]) - self.assertEqual(d1.flattened_intervals(), [-5, -5, 3, 6]) + self.assertEqual(d0.flattened_intervals(), [-10, 10]) + self.assertEqual(d1.flattened_intervals(), [-5, -5, 3, 6]) - def test_simple_solve(self): - model_string = """ + def test_simple_solve(self): + model_string = """ variables { domain: -10 domain: 10 } variables { domain: -10 domain: 10 } variables { domain: -461168601842738790 domain: 461168601842738790 } @@ -112,17 +112,17 @@ def test_simple_solve(self): coeffs: -1 scaling_factor: -1 }""" - model = cp_model_pb2.CpModelProto() - self.assertTrue(text_format.Parse(model_string, model)) + model = cp_model_pb2.CpModelProto() + self.assertTrue(text_format.Parse(model_string, model)) - solve_wrapper = cmh.SolveWrapper() - response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) + solve_wrapper = cmh.SolveWrapper() + response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) - self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) - self.assertEqual(30.0, response_wrapper.objective_value()) + self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) + self.assertEqual(30.0, response_wrapper.objective_value()) - def test_simple_solve_with_core(self): - model_string = """ + def test_simple_solve_with_core(self): + model_string = """ variables { domain: -10 domain: 10 } variables { domain: -10 domain: 10 } variables { domain: -461168601842738790 domain: 461168601842738790 } @@ -153,64 +153,67 @@ def test_simple_solve_with_core(self): coeffs: -1 scaling_factor: -1 }""" - model = cp_model_pb2.CpModelProto() - self.assertTrue(text_format.Parse(model_string, model)) - - parameters = sat_parameters_pb2.SatParameters(optimize_with_core=True) - - solve_wrapper = cmh.SolveWrapper() - solve_wrapper.set_parameters(parameters) - response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) - - self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) - self.assertEqual(30.0, response_wrapper.objective_value()) - - def test_simple_solve_with_proto_api(self): - model = cp_model_pb2.CpModelProto() - x = model.variables.add() - x.domain.extend([-10, 10]) - y = model.variables.add() - y.domain.extend([-10, 10]) - obj_var = model.variables.add() - obj_var.domain.extend([-461168601842738790, 461168601842738790]) - ct = model.constraints.add() - ct.linear.vars.extend([0, 1, 2]) - ct.linear.coeffs.extend([1, 2, -1]) - ct.linear.domain.extend([0, 0]) - model.objective.vars.append(2) - model.objective.coeffs.append(-1) - model.objective.scaling_factor = -1 - - solve_wrapper = cmh.SolveWrapper() - response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) - - self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) - self.assertEqual(30.0, response_wrapper.objective_value()) - self.assertEqual(30.0, response_wrapper.best_objective_bound()) - - def test_solution_callback(self): - model_string = """ + model = cp_model_pb2.CpModelProto() + self.assertTrue(text_format.Parse(model_string, model)) + + parameters = sat_parameters_pb2.SatParameters(optimize_with_core=True) + + solve_wrapper = cmh.SolveWrapper() + solve_wrapper.set_parameters(parameters) + response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) + + self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) + self.assertEqual(30.0, response_wrapper.objective_value()) + + def test_simple_solve_with_proto_api(self): + model = cp_model_pb2.CpModelProto() + x = model.variables.add() + x.domain.extend([-10, 10]) + y = model.variables.add() + y.domain.extend([-10, 10]) + obj_var = model.variables.add() + obj_var.domain.extend([-461168601842738790, 461168601842738790]) + ct = model.constraints.add() + ct.linear.vars.extend([0, 1, 2]) + ct.linear.coeffs.extend([1, 2, -1]) + ct.linear.domain.extend([0, 0]) + model.objective.vars.append(2) + model.objective.coeffs.append(-1) + model.objective.scaling_factor = -1 + + solve_wrapper = cmh.SolveWrapper() + response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) + + self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) + self.assertEqual(30.0, response_wrapper.objective_value()) + self.assertEqual(30.0, response_wrapper.best_objective_bound()) + self.assertRaises(TypeError, response_wrapper.value, None) + self.assertRaises(TypeError, response_wrapper.float_value, None) + self.assertRaises(TypeError, response_wrapper.boolean_value, None) + + def test_solution_callback(self): + model_string = """ variables { domain: 0 domain: 5 } variables { domain: 0 domain: 5 } constraints { linear { vars: 0 vars: 1 coeffs: 1 coeffs: 1 domain: 6 domain: 6 } } """ - model = cp_model_pb2.CpModelProto() - self.assertTrue(text_format.Parse(model_string, model)) - - solve_wrapper = cmh.SolveWrapper() - callback = Callback() - solve_wrapper.add_solution_callback(callback) - params = sat_parameters_pb2.SatParameters() - params.enumerate_all_solutions = True - solve_wrapper.set_parameters(params) - response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) - - self.assertEqual(5, callback.solution_count()) - self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) - - def test_best_bound_callback(self): - model_string = """ + model = cp_model_pb2.CpModelProto() + self.assertTrue(text_format.Parse(model_string, model)) + + solve_wrapper = cmh.SolveWrapper() + callback = Callback() + solve_wrapper.add_solution_callback(callback) + params = sat_parameters_pb2.SatParameters() + params.enumerate_all_solutions = True + solve_wrapper.set_parameters(params) + response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) + + self.assertEqual(5, callback.solution_count()) + self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) + + def test_best_bound_callback(self): + model_string = """ variables { domain: 0 domain: 1 } variables { domain: 0 domain: 1 } variables { domain: 0 domain: 1 } @@ -222,24 +225,24 @@ def test_best_bound_callback(self): offset: 0.6 } """ - model = cp_model_pb2.CpModelProto() - self.assertTrue(text_format.Parse(model_string, model)) - - solve_wrapper = cmh.SolveWrapper() - best_bound_callback = BestBoundCallback() - solve_wrapper.add_best_bound_callback(best_bound_callback.new_best_bound) - params = sat_parameters_pb2.SatParameters() - params.num_workers = 1 - params.linearization_level = 2 - params.log_search_progress = True - solve_wrapper.set_parameters(params) - response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) - - self.assertEqual(2.6, best_bound_callback.best_bound) - self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) - - def test_model_stats(self): - model_string = """ + model = cp_model_pb2.CpModelProto() + self.assertTrue(text_format.Parse(model_string, model)) + + solve_wrapper = cmh.SolveWrapper() + best_bound_callback = BestBoundCallback() + solve_wrapper.add_best_bound_callback(best_bound_callback.new_best_bound) + params = sat_parameters_pb2.SatParameters() + params.num_workers = 1 + params.linearization_level = 2 + params.log_search_progress = True + solve_wrapper.set_parameters(params) + response_wrapper = solve_wrapper.solve_and_return_response_wrapper(model) + + self.assertEqual(2.6, best_bound_callback.best_bound) + self.assertEqual(cp_model_pb2.OPTIMAL, response_wrapper.status()) + + def test_model_stats(self): + model_string = """ variables { domain: -10 domain: 10 } variables { domain: -10 domain: 10 } variables { domain: -1000 domain: 1000 } @@ -272,101 +275,101 @@ def test_model_stats(self): } name: 'testModelStats' """ - model = cp_model_pb2.CpModelProto() - self.assertTrue(text_format.Parse(model_string, model)) - stats = cmh.CpSatHelper.model_stats(model) - self.assertTrue(stats) - - def test_int_lin_expr(self): - x = TestIntVar(0, "x") - self.assertTrue(x.is_integer()) - self.assertIsInstance(x, cmh.BaseIntVar) - self.assertIsInstance(x, cmh.LinearExpr) - e1 = x + 2 - self.assertTrue(e1.is_integer()) - self.assertEqual(str(e1), "(x + 2)") - e2 = 3 + x - self.assertTrue(e2.is_integer()) - self.assertEqual(str(e2), "(x + 3)") - y = TestIntVar(1, "y") - e3 = y * 5 - self.assertTrue(e3.is_integer()) - self.assertEqual(str(e3), "(5 * y)") - e4 = -2 * y - self.assertTrue(e4.is_integer()) - self.assertEqual(str(e4), "(-2 * y)") - e5 = x - 1 - self.assertTrue(e5.is_integer()) - self.assertEqual(str(e5), "(x - 1)") - e6 = x - 2 * y - self.assertTrue(e6.is_integer()) - self.assertEqual(str(e6), "(x - (2 * y))") - z = TestIntVar(2, "z", True) - e7 = -z - self.assertTrue(e7.is_integer()) - self.assertEqual(str(e7), "(-z)") - not_z = ~z - self.assertTrue(not_z.is_integer()) - self.assertEqual(str(not_z), "not(z)") - self.assertEqual(not_z.index, -3) - - e8 = cmh.LinearExpr.sum([x, y, z]) - self.assertEqual(str(e8), "(x + y + z)") - e9 = cmh.LinearExpr.sum([x, y, z, 11]) - self.assertEqual(str(e9), "(x + y + z + 11)") - e10 = cmh.LinearExpr.weighted_sum([x, y, z], [1, 2, 3]) - self.assertEqual(str(e10), "(x + 2 * y + 3 * z)") - e11 = cmh.LinearExpr.weighted_sum([x, y, z, 5], [1, 2, 3, -1]) - self.assertEqual(str(e11), "(x + 2 * y + 3 * z - 5)") - - e12 = x - y - 2 * z - self.assertEqual(str(e12), "(-(2 * z) + (x - y))") - - def test_float_lin_expr(self): - x = TestIntVar(0, "x") - self.assertTrue(x.is_integer()) - self.assertIsInstance(x, TestIntVar) - self.assertIsInstance(x, cmh.LinearExpr) - e1 = x + 2.5 - self.assertFalse(e1.is_integer()) - self.assertEqual(str(e1), "(x + 2.5)") - e2 = 3.1 + x - self.assertFalse(e2.is_integer()) - self.assertEqual(str(e2), "(x + 3.1)") - y = TestIntVar(1, "y") - e3 = y * 5.2 - self.assertFalse(e3.is_integer()) - self.assertEqual(str(e3), "(5.2 * y)") - e4 = -2.25 * y - self.assertFalse(e4.is_integer()) - self.assertEqual(str(e4), "(-2.25 * y)") - e5 = x - 1.1 - self.assertFalse(e5.is_integer()) - self.assertEqual(str(e5), "(x - 1.1)") - e6 = x + 2.4 * y - self.assertFalse(e6.is_integer()) - self.assertEqual(str(e6), "(x + (2.4 * y))") - e7 = x - 2.4 * y - self.assertFalse(e7.is_integer()) - self.assertEqual(str(e7), "(x - (2.4 * y))") - - z = TestIntVar(2, "z") - e8 = cmh.LinearExpr.sum([x, y, z, -2]) - self.assertTrue(e8.is_integer()) - self.assertEqual(str(e8), "(x + y + z - 2)") - e9 = cmh.LinearExpr.sum([x, y, z, 1.5]) - self.assertFalse(e9.is_integer()) - self.assertEqual(str(e9), "(x + y + z + 1.5)") - e10 = cmh.LinearExpr.weighted_sum([x, y, z], [1.0, 2.25, 5.5]) - self.assertFalse(e10.is_integer()) - self.assertEqual(str(e10), "(x + 2.25 * y + 5.5 * z)") - e11 = cmh.LinearExpr.weighted_sum([x, y, z, 1.5], [1.0, 2.25, 5.5, -1]) - self.assertFalse(e11.is_integer()) - self.assertEqual(str(e11), "(x + 2.25 * y + 5.5 * z - 1.5)") - e12 = (x + 2) * 3.1 - self.assertFalse(e12.is_integer()) - self.assertEqual(str(e12), "(3.1 * (x + 2))") + model = cp_model_pb2.CpModelProto() + self.assertTrue(text_format.Parse(model_string, model)) + stats = cmh.CpSatHelper.model_stats(model) + self.assertTrue(stats) + + def test_int_lin_expr(self): + x = TestIntVar(0, "x") + self.assertTrue(x.is_integer()) + self.assertIsInstance(x, cmh.BaseIntVar) + self.assertIsInstance(x, cmh.LinearExpr) + e1 = x + 2 + self.assertTrue(e1.is_integer()) + self.assertEqual(str(e1), "(x + 2)") + e2 = 3 + x + self.assertTrue(e2.is_integer()) + self.assertEqual(str(e2), "(x + 3)") + y = TestIntVar(1, "y") + e3 = y * 5 + self.assertTrue(e3.is_integer()) + self.assertEqual(str(e3), "(5 * y)") + e4 = -2 * y + self.assertTrue(e4.is_integer()) + self.assertEqual(str(e4), "(-2 * y)") + e5 = x - 1 + self.assertTrue(e5.is_integer()) + self.assertEqual(str(e5), "(x - 1)") + e6 = x - 2 * y + self.assertTrue(e6.is_integer()) + self.assertEqual(str(e6), "(x + (-2 * y))") + z = TestIntVar(2, "z", True) + e7 = -z + self.assertTrue(e7.is_integer()) + self.assertEqual(str(e7), "(-z)") + not_z = ~z + self.assertTrue(not_z.is_integer()) + self.assertEqual(str(not_z), "not(z)") + self.assertEqual(not_z.index, -3) + + e8 = cmh.LinearExpr.sum([x, y, z]) + self.assertEqual(str(e8), "(x + y + z)") + e9 = cmh.LinearExpr.sum([x, y, z, 11]) + self.assertEqual(str(e9), "(x + y + z + 11)") + e10 = cmh.LinearExpr.weighted_sum([x, y, z], [1, 2, 3]) + self.assertEqual(str(e10), "(x + 2 * y + 3 * z)") + e11 = cmh.LinearExpr.weighted_sum([x, y, z, 5], [1, 2, 3, -1]) + self.assertEqual(str(e11), "(x + 2 * y + 3 * z - 5)") + + e12 = x - y - 2 * z + self.assertEqual(str(e12), "(x + (-y) + (-2 * z))") + + def test_float_lin_expr(self): + x = TestIntVar(0, "x") + self.assertTrue(x.is_integer()) + self.assertIsInstance(x, TestIntVar) + self.assertIsInstance(x, cmh.LinearExpr) + e1 = x + 2.5 + self.assertFalse(e1.is_integer()) + self.assertEqual(str(e1), "(x + 2.5)") + e2 = 3.1 + x + self.assertFalse(e2.is_integer()) + self.assertEqual(str(e2), "(x + 3.1)") + y = TestIntVar(1, "y") + e3 = y * 5.2 + self.assertFalse(e3.is_integer()) + self.assertEqual(str(e3), "(5.2 * y)") + e4 = -2.25 * y + self.assertFalse(e4.is_integer()) + self.assertEqual(str(e4), "(-2.25 * y)") + e5 = x - 1.1 + self.assertFalse(e5.is_integer()) + self.assertEqual(str(e5), "(x - 1.1)") + e6 = x + 2.4 * y + self.assertFalse(e6.is_integer()) + self.assertEqual(str(e6), "(x + (2.4 * y))") + e7 = x - 2.4 * y + self.assertFalse(e7.is_integer()) + self.assertEqual(str(e7), "(x + (-(2.4 * y)))") + + z = TestIntVar(2, "z") + e8 = cmh.LinearExpr.sum([x, y, z, -2]) + self.assertTrue(e8.is_integer()) + self.assertEqual(str(e8), "(x + y + z - 2)") + e9 = cmh.LinearExpr.sum([x, y, z, 1.5]) + self.assertFalse(e9.is_integer()) + self.assertEqual(str(e9), "(x + y + z + 1.5)") + e10 = cmh.LinearExpr.weighted_sum([x, y, z], [1.0, 2.25, 5.5]) + self.assertFalse(e10.is_integer()) + self.assertEqual(str(e10), "(x + 2.25 * y + 5.5 * z)") + e11 = cmh.LinearExpr.weighted_sum([x, y, z, 1.5], [1.0, 2.25, 5.5, -1]) + self.assertFalse(e11.is_integer()) + self.assertEqual(str(e11), "(x + 2.25 * y + 5.5 * z - 1.5)") + e12 = (x + 2) * 3.1 + self.assertFalse(e12.is_integer()) + self.assertEqual(str(e12), "(3.1 * (x + 2))") if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/sat/python/cp_model_numbers.py b/ortools/sat/python/cp_model_numbers.py index 26b7928df51..8b9140d3e12 100644 --- a/ortools/sat/python/cp_model_numbers.py +++ b/ortools/sat/python/cp_model_numbers.py @@ -23,45 +23,47 @@ def is_boolean(x: Any) -> bool: - """Checks if the x is a boolean.""" - if isinstance(x, bool): - return True - if isinstance(x, np.bool_): - return True - return False + """Checks if the x is a boolean.""" + if isinstance(x, bool): + return True + if isinstance(x, np.bool_): + return True + return False def assert_is_zero_or_one(x: Any) -> int: - """Asserts that x is 0 or 1 and returns it as an int.""" - if not isinstance(x, numbers.Integral): - raise TypeError(f"Not a boolean: {x} of type {type(x)}") - x_as_int = int(x) - if x_as_int < 0 or x_as_int > 1: - raise TypeError(f"Not a boolean: {x}") - return x_as_int + """Asserts that x is 0 or 1 and returns it as an int.""" + if not isinstance(x, numbers.Integral): + raise TypeError(f"Not a boolean: {x} of type {type(x)}") + x_as_int = int(x) + if x_as_int < 0 or x_as_int > 1: + raise TypeError(f"Not a boolean: {x}") + return x_as_int def to_capped_int64(v: int) -> int: - """Restrict v within [INT_MIN..INT_MAX] range.""" - if v > INT_MAX: - return INT_MAX - if v < INT_MIN: - return INT_MIN - return v + """Restrict v within [INT_MIN..INT_MAX] range.""" + if v > INT_MAX: + return INT_MAX + if v < INT_MIN: + return INT_MIN + return v def capped_subtraction(x: int, y: int) -> int: - """Saturated arithmetics. Returns x - y truncated to the int64_t range.""" - if y == 0: - return x - if x == y: - if x == INT_MAX or x == INT_MIN: - raise OverflowError("Integer NaN: subtracting INT_MAX or INT_MIN to itself") - return 0 + """Saturated arithmetics. Returns x - y truncated to the int64_t range.""" + if y == 0: + return x + if x == y: if x == INT_MAX or x == INT_MIN: - return x - if y == INT_MAX: - return INT_MIN - if y == INT_MIN: - return INT_MAX - return to_capped_int64(x - y) + raise OverflowError( + "Integer NaN: subtracting INT_MAX or INT_MIN to itself" + ) + return 0 + if x == INT_MAX or x == INT_MIN: + return x + if y == INT_MAX: + return INT_MIN + if y == INT_MIN: + return INT_MAX + return to_capped_int64(x - y) diff --git a/ortools/sat/python/cp_model_numbers_test.py b/ortools/sat/python/cp_model_numbers_test.py index 1e0b91a0fc8..2dec225d87b 100644 --- a/ortools/sat/python/cp_model_numbers_test.py +++ b/ortools/sat/python/cp_model_numbers_test.py @@ -22,42 +22,42 @@ class CpModelNumbersTest(absltest.TestCase): - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() - - def test_is_boolean(self): - self.assertTrue(cmn.is_boolean(True)) - self.assertTrue(cmn.is_boolean(False)) - self.assertFalse(cmn.is_boolean(1)) - self.assertFalse(cmn.is_boolean(0)) - self.assertTrue(cmn.is_boolean(np.bool_(1))) - self.assertTrue(cmn.is_boolean(np.bool_(0))) - - def test_to_capped_int64(self): - self.assertEqual(cmn.to_capped_int64(cmn.INT_MAX), cmn.INT_MAX) - self.assertEqual(cmn.to_capped_int64(cmn.INT_MAX + 1), cmn.INT_MAX) - self.assertEqual(cmn.to_capped_int64(cmn.INT_MIN), cmn.INT_MIN) - self.assertEqual(cmn.to_capped_int64(cmn.INT_MIN - 1), cmn.INT_MIN) - self.assertEqual(cmn.to_capped_int64(15), 15) - - def test_capped_subtraction(self): - self.assertEqual(cmn.capped_subtraction(10, 5), 5) - self.assertEqual(cmn.capped_subtraction(cmn.INT_MIN, 5), cmn.INT_MIN) - self.assertEqual(cmn.capped_subtraction(cmn.INT_MIN, -5), cmn.INT_MIN) - self.assertEqual(cmn.capped_subtraction(cmn.INT_MAX, 5), cmn.INT_MAX) - self.assertEqual(cmn.capped_subtraction(cmn.INT_MAX, -5), cmn.INT_MAX) - self.assertEqual(cmn.capped_subtraction(2, cmn.INT_MIN), cmn.INT_MAX) - self.assertEqual(cmn.capped_subtraction(2, cmn.INT_MAX), cmn.INT_MIN) - self.assertRaises( - OverflowError, cmn.capped_subtraction, cmn.INT_MAX, cmn.INT_MAX - ) - self.assertRaises( - OverflowError, cmn.capped_subtraction, cmn.INT_MIN, cmn.INT_MIN - ) - self.assertRaises(TypeError, cmn.capped_subtraction, 5, "dummy") - self.assertRaises(TypeError, cmn.capped_subtraction, "dummy", 5) + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() + + def test_is_boolean(self): + self.assertTrue(cmn.is_boolean(True)) + self.assertTrue(cmn.is_boolean(False)) + self.assertFalse(cmn.is_boolean(1)) + self.assertFalse(cmn.is_boolean(0)) + self.assertTrue(cmn.is_boolean(np.bool_(1))) + self.assertTrue(cmn.is_boolean(np.bool_(0))) + + def test_to_capped_int64(self): + self.assertEqual(cmn.to_capped_int64(cmn.INT_MAX), cmn.INT_MAX) + self.assertEqual(cmn.to_capped_int64(cmn.INT_MAX + 1), cmn.INT_MAX) + self.assertEqual(cmn.to_capped_int64(cmn.INT_MIN), cmn.INT_MIN) + self.assertEqual(cmn.to_capped_int64(cmn.INT_MIN - 1), cmn.INT_MIN) + self.assertEqual(cmn.to_capped_int64(15), 15) + + def test_capped_subtraction(self): + self.assertEqual(cmn.capped_subtraction(10, 5), 5) + self.assertEqual(cmn.capped_subtraction(cmn.INT_MIN, 5), cmn.INT_MIN) + self.assertEqual(cmn.capped_subtraction(cmn.INT_MIN, -5), cmn.INT_MIN) + self.assertEqual(cmn.capped_subtraction(cmn.INT_MAX, 5), cmn.INT_MAX) + self.assertEqual(cmn.capped_subtraction(cmn.INT_MAX, -5), cmn.INT_MAX) + self.assertEqual(cmn.capped_subtraction(2, cmn.INT_MIN), cmn.INT_MAX) + self.assertEqual(cmn.capped_subtraction(2, cmn.INT_MAX), cmn.INT_MIN) + self.assertRaises( + OverflowError, cmn.capped_subtraction, cmn.INT_MAX, cmn.INT_MAX + ) + self.assertRaises( + OverflowError, cmn.capped_subtraction, cmn.INT_MIN, cmn.INT_MIN + ) + self.assertRaises(TypeError, cmn.capped_subtraction, 5, "dummy") + self.assertRaises(TypeError, cmn.capped_subtraction, "dummy", 5) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/sat/python/cp_model_test.py b/ortools/sat/python/cp_model_test.py index 7d16abcf425..c301f0e3af0 100644 --- a/ortools/sat/python/cp_model_test.py +++ b/ortools/sat/python/cp_model_test.py @@ -27,2060 +27,2113 @@ class SolutionCounter(cp_model.CpSolverSolutionCallback): - """Count solutions.""" + """Count solutions.""" - def __init__(self) -> None: - cp_model.CpSolverSolutionCallback.__init__(self) - self.__solution_count = 0 + def __init__(self) -> None: + cp_model.CpSolverSolutionCallback.__init__(self) + self.__solution_count = 0 - def on_solution_callback(self) -> None: - self.__solution_count += 1 + def on_solution_callback(self) -> None: + self.__solution_count += 1 - @property - def solution_count(self) -> int: - return self.__solution_count + @property + def solution_count(self) -> int: + return self.__solution_count class SolutionSum(cp_model.CpSolverSolutionCallback): - """Record the sum of variables in the solution.""" + """Record the sum of variables in the solution.""" - def __init__(self, variables: list[cp_model.IntVar]) -> None: - cp_model.CpSolverSolutionCallback.__init__(self) - self.__sum: int = 0 - self.__vars = variables + def __init__(self, variables: list[cp_model.IntVar]) -> None: + cp_model.CpSolverSolutionCallback.__init__(self) + self.__sum: int = 0 + self.__vars = variables - def on_solution_callback(self) -> None: - self.__sum = sum(self.value(x) for x in self.__vars) + def on_solution_callback(self) -> None: + self.__sum = sum(self.value(x) for x in self.__vars) - @property - def sum(self) -> int: - return self.__sum + @property + def sum(self) -> int: + return self.__sum class SolutionFloatValue(cp_model.CpSolverSolutionCallback): - """Record the evaluation of a float expression in the solution.""" + """Record the evaluation of a float expression in the solution.""" - def __init__(self, expr: cp_model.LinearExpr) -> None: - cp_model.CpSolverSolutionCallback.__init__(self) - self.__expr: cp_model.LinearExpr = expr - self.__value: float = 0.0 + def __init__(self, expr: cp_model.LinearExpr) -> None: + cp_model.CpSolverSolutionCallback.__init__(self) + self.__expr: cp_model.LinearExpr = expr + self.__value: float = 0.0 - def on_solution_callback(self) -> None: - self.__value = self.float_value(self.__expr) + def on_solution_callback(self) -> None: + self.__value = self.float_value(self.__expr) - @property - def value(self) -> float: - return self.__value + @property + def value(self) -> float: + return self.__value class SolutionObjective(cp_model.CpSolverSolutionCallback): - """Record the objective value of the solution.""" + """Record the objective value of the solution.""" - def __init__(self) -> None: - cp_model.CpSolverSolutionCallback.__init__(self) - self.__obj: float = 0 + def __init__(self) -> None: + cp_model.CpSolverSolutionCallback.__init__(self) + self.__obj: float = 0 - def on_solution_callback(self) -> None: - self.__obj = self.objective_value + def on_solution_callback(self) -> None: + self.__obj = self.objective_value - @property - def obj(self) -> float: - return self.__obj + @property + def obj(self) -> float: + return self.__obj class RecordSolution(cp_model.CpSolverSolutionCallback): - """Record the objective value of the solution.""" - - def __init__( - self, - int_vars: list[cp_model.VariableT], - bool_vars: list[cp_model.LiteralT], - ) -> None: - cp_model.CpSolverSolutionCallback.__init__(self) - self.__int_vars = int_vars - self.__bool_vars = bool_vars - self.__int_var_values: list[int] = [] - self.__bool_var_values: list[bool] = [] - - def on_solution_callback(self) -> None: - for int_var in self.__int_vars: - self.__int_var_values.append(self.value(int_var)) - for bool_var in self.__bool_vars: - self.__bool_var_values.append(self.boolean_value(bool_var)) - - @property - def int_var_values(self) -> list[int]: - return self.__int_var_values - - @property - def bool_var_values(self) -> list[bool]: - return self.__bool_var_values + """Record the objective value of the solution.""" + + def __init__( + self, + int_vars: list[cp_model.VariableT], + bool_vars: list[cp_model.LiteralT], + ) -> None: + cp_model.CpSolverSolutionCallback.__init__(self) + self.__int_vars = int_vars + self.__bool_vars = bool_vars + self.__int_var_values: list[int] = [] + self.__bool_var_values: list[bool] = [] + + def on_solution_callback(self) -> None: + for int_var in self.__int_vars: + self.__int_var_values.append(self.value(int_var)) + for bool_var in self.__bool_vars: + self.__bool_var_values.append(self.boolean_value(bool_var)) + + @property + def int_var_values(self) -> list[int]: + return self.__int_var_values + + @property + def bool_var_values(self) -> list[bool]: + return self.__bool_var_values class TimeRecorder(cp_model.CpSolverSolutionCallback): - def __init__(self) -> None: - super().__init__() - self.__last_time: float = 0.0 + def __init__(self) -> None: + super().__init__() + self.__last_time: float = 0.0 - def on_solution_callback(self) -> None: - self.__last_time = time.time() + def on_solution_callback(self) -> None: + self.__last_time = time.time() - @property - def last_time(self) -> float: - return self.__last_time + @property + def last_time(self) -> float: + return self.__last_time class RaiseException(cp_model.CpSolverSolutionCallback): - def __init__(self, msg: str) -> None: - super().__init__() - self.__msg = msg + def __init__(self, msg: str) -> None: + super().__init__() + self.__msg = msg - def on_solution_callback(self) -> None: - raise ValueError(self.__msg) + def on_solution_callback(self) -> None: + raise ValueError(self.__msg) class LogToString: - """Record log in a string.""" + """Record log in a string.""" - def __init__(self) -> None: - self.__log = "" + def __init__(self) -> None: + self.__log = "" - def new_message(self, message: str) -> None: - self.__log += message - self.__log += "\n" + def new_message(self, message: str) -> None: + self.__log += message + self.__log += "\n" - @property - def log(self) -> str: - return self.__log + @property + def log(self) -> str: + return self.__log class BestBoundCallback: - def __init__(self) -> None: - self.best_bound: float = 0.0 + def __init__(self) -> None: + self.best_bound: float = 0.0 - def new_best_bound(self, bb: float) -> None: - self.best_bound = bb + def new_best_bound(self, bb: float) -> None: + self.best_bound = bb class BestBoundTimeCallback: - def __init__(self) -> None: - self.__last_time: float = 0.0 + def __init__(self) -> None: + self.__last_time: float = 0.0 - def new_best_bound(self, unused_bb: float): - self.__last_time = time.time() + def new_best_bound(self, unused_bb: float): + self.__last_time = time.time() - @property - def last_time(self) -> float: - return self.__last_time + @property + def last_time(self) -> float: + return self.__last_time class CpModelTest(absltest.TestCase): - def tearDown(self) -> None: - super().tearDown() - sys.stdout.flush() - - def test_create_integer_variable(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - self.assertEqual("x", str(x)) - self.assertEqual("x(-10..10)", repr(x)) - y = model.new_int_var_from_domain( - cp_model.Domain.from_intervals([[2, 4], [7]]), "y" + def tearDown(self) -> None: + super().tearDown() + sys.stdout.flush() + + def test_create_integer_variable(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + self.assertEqual("x", str(x)) + self.assertEqual("x(-10..10)", repr(x)) + y = model.new_int_var_from_domain( + cp_model.Domain.from_intervals([[2, 4], [7]]), "y" + ) + self.assertEqual("y", str(y)) + self.assertEqual("y(2..4, 7)", repr(y)) + z = model.new_int_var_from_domain( + cp_model.Domain.from_values([2, 3, 4, 7]), "z" + ) + self.assertEqual("z", str(z)) + self.assertEqual("z(2..4, 7)", repr(z)) + t = model.new_int_var_from_domain( + cp_model.Domain.from_flat_intervals([2, 4, 7, 7]), "t" + ) + self.assertEqual("t", str(t)) + self.assertEqual("t(2..4, 7)", repr(t)) + cst = model.new_constant(5) + self.assertEqual("5", str(cst)) + + def test_hash_int_var(self) -> None: + model = cp_model.CpModel() + var_a = model.new_int_var(0, 2, "a") + variables = set() + variables.add(var_a) + + def test_literal(self) -> None: + model = cp_model.CpModel() + x = model.new_bool_var("x") + self.assertEqual("x", str(x)) + self.assertEqual("not(x)", str(~x)) + self.assertEqual("not(x)", str(x.negated())) + self.assertEqual(x.negated().negated(), x) + self.assertEqual(x.negated().negated().index, x.index) + y = model.new_int_var(0, 1, "y") + self.assertEqual("y", str(y)) + self.assertEqual("not(y)", str(~y)) + zero = model.new_constant(0) + self.assertEqual("0", str(zero)) + self.assertEqual("not(0)", str(~zero)) + one = model.new_constant(1) + self.assertEqual("1", str(one)) + self.assertEqual("not(1)", str(~one)) + z = model.new_int_var(0, 2, "z") + self.assertRaises(TypeError, z.negated) + self.assertRaises(TypeError, z.__invert__) + + def test_negation(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + b = model.new_bool_var("b") + nb = b.negated() + self.assertEqual(b.negated(), nb) + self.assertEqual(~b, nb) + self.assertEqual(b.negated().negated(), b) + self.assertEqual(~(~b), b) + self.assertEqual(nb.index, -b.index - 1) + self.assertRaises(TypeError, x.negated) + + def test_issue_4654(self) -> None: + model = cp_model.CpModel() + x = model.NewIntVar(0, 1, "x") + y = model.NewIntVar(0, 2, "y") + z = model.NewIntVar(0, 3, "z") + expr = x - y - 2 * z + self.assertEqual(str(expr), "(x + (-y) + (-2 * z))") + + def test_equality_overload(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(0, 5, "y") + self.assertEqual(x, x) + self.assertNotEqual(x, y) + + def test_linear(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + model.add_linear_constraint(x + 2 * y, 0, 10) + model.minimize(y) + solver = cp_model.CpSolver() + self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) + self.assertEqual(10, solver.value(x)) + self.assertEqual(-5, solver.value(y)) + + def test_none_argument(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + model.add_linear_constraint(x + 2 * y, 0, 10) + model.minimize(y) + solver = cp_model.CpSolver() + self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) + self.assertRaises(TypeError, solver.value, None) + self.assertRaises(TypeError, solver.float_value, None) + self.assertRaises(TypeError, solver.boolean_value, None) + + def test_linear_constraint(self) -> None: + model = cp_model.CpModel() + model.add_linear_constraint(5, 0, 10) + model.add_linear_constraint(-1, 0, 10) + self.assertLen(model.proto.constraints, 2) + self.assertTrue(model.proto.constraints[0].HasField("bool_and")) + self.assertEmpty(model.proto.constraints[0].bool_and.literals) + self.assertTrue(model.proto.constraints[1].HasField("bool_or")) + self.assertEmpty(model.proto.constraints[1].bool_or.literals) + + def test_linear_non_equal(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + ct = model.add(-x + y != 3).proto + self.assertLen(ct.linear.domain, 4) + self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) + self.assertEqual(2, ct.linear.domain[1]) + self.assertEqual(4, ct.linear.domain[2]) + self.assertEqual(cp_model.INT_MAX, ct.linear.domain[3]) + + def test_eq(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + ct = model.add(x == 2).proto + self.assertLen(ct.linear.vars, 1) + self.assertLen(ct.linear.coeffs, 1) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(2, ct.linear.domain[0]) + self.assertEqual(2, ct.linear.domain[1]) + + def testGe(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + ct = model.add(x >= 2).proto + self.assertLen(ct.linear.vars, 1) + self.assertLen(ct.linear.coeffs, 1) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(2, ct.linear.domain[0]) + self.assertEqual(cp_model.INT_MAX, ct.linear.domain[1]) + + def test_gt(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + ct = model.add(x > 2).proto + self.assertLen(ct.linear.vars, 1) + self.assertLen(ct.linear.coeffs, 1) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(3, ct.linear.domain[0]) + self.assertEqual(cp_model.INT_MAX, ct.linear.domain[1]) + + def test_le(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + ct = model.add(x <= 2).proto + self.assertLen(ct.linear.vars, 1) + self.assertLen(ct.linear.coeffs, 1) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) + self.assertEqual(2, ct.linear.domain[1]) + + def test_lt(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + ct = model.add(x < 2).proto + self.assertLen(ct.linear.vars, 1) + self.assertLen(ct.linear.coeffs, 1) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) + self.assertEqual(1, ct.linear.domain[1]) + + def test_eq_var(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + ct = model.add(x == y + 2).proto + self.assertLen(ct.linear.vars, 2) + self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) + self.assertLen(ct.linear.coeffs, 2) + self.assertEqual(0, ct.linear.coeffs[0] + ct.linear.coeffs[1]) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(2, ct.linear.domain[0]) + self.assertEqual(2, ct.linear.domain[1]) + + def test_ge_var(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + ct = model.add(x >= 1 - y).proto + self.assertLen(ct.linear.vars, 2) + self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) + self.assertLen(ct.linear.coeffs, 2) + self.assertEqual(1, ct.linear.coeffs[0]) + self.assertEqual(1, ct.linear.coeffs[1]) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(1, ct.linear.domain[0]) + self.assertEqual(cp_model.INT_MAX, ct.linear.domain[1]) + + def test_gt_var(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + ct = model.add(x > 1 - y).proto + self.assertLen(ct.linear.vars, 2) + self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) + self.assertLen(ct.linear.coeffs, 2) + self.assertEqual(1, ct.linear.coeffs[0]) + self.assertEqual(1, ct.linear.coeffs[1]) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(2, ct.linear.domain[0]) + self.assertEqual(cp_model.INT_MAX, ct.linear.domain[1]) + + def test_le_var(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + ct = model.add(x <= 1 - y).proto + self.assertLen(ct.linear.vars, 2) + self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) + self.assertLen(ct.linear.coeffs, 2) + self.assertEqual(1, ct.linear.coeffs[0]) + self.assertEqual(1, ct.linear.coeffs[1]) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) + self.assertEqual(1, ct.linear.domain[1]) + + def test_lt_var(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + ct = model.add(x < 1 - y).proto + self.assertLen(ct.linear.vars, 2) + self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) + self.assertLen(ct.linear.coeffs, 2) + self.assertEqual(1, ct.linear.coeffs[0]) + self.assertEqual(1, ct.linear.coeffs[1]) + self.assertLen(ct.linear.domain, 2) + self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) + self.assertEqual(0, ct.linear.domain[1]) + + def test_linear_non_equal_with_constant(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + ct = model.add(x + y + 5 != 3).proto + self.assertLen(ct.linear.domain, 4) + # Checks that saturated arithmetics worked. + self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) + self.assertEqual(-3, ct.linear.domain[1]) + self.assertEqual(-1, ct.linear.domain[2]) + self.assertEqual(cp_model.INT_MAX, ct.linear.domain[3]) + + def test_linear_with_enforcement(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + b = model.new_bool_var("b") + model.add_linear_constraint(x + 2 * y, 0, 10).only_enforce_if(b.negated()) + model.minimize(y) + self.assertLen(model.proto.constraints, 1) + self.assertEqual(-3, model.proto.constraints[0].enforcement_literal[0]) + c = model.new_bool_var("c") + model.add_linear_constraint(x + 4 * y, 0, 10).only_enforce_if([b, c]) + self.assertLen(model.proto.constraints, 2) + self.assertEqual(2, model.proto.constraints[1].enforcement_literal[0]) + self.assertEqual(3, model.proto.constraints[1].enforcement_literal[1]) + model.add_linear_constraint(x + 5 * y, 0, 10).only_enforce_if( + c.negated(), b + ) + self.assertLen(model.proto.constraints, 3) + self.assertEqual(-4, model.proto.constraints[2].enforcement_literal[0]) + self.assertEqual(2, model.proto.constraints[2].enforcement_literal[1]) + + def test_names(self) -> None: + model = cp_model.CpModel() + model.name = "test_model" + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + ct = model.add_linear_constraint(x + 2 * y, 0, 10).with_name( + "test_constraint" + ) + self.assertEqual(model.name, "test_model") + self.assertEqual(x.name, "x") + self.assertEqual("test_constraint", ct.name) + model.remove_all_names() + self.assertEmpty(model.name) + self.assertEmpty(x.name) + self.assertEmpty(ct.name) + + def test_natural_api_minimize(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + model.add(x * 2 - 1 * y == 1) + model.minimize(x * 1 - 2 * y + 3) + solver = cp_model.CpSolver() + self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) + self.assertEqual(5, solver.value(x)) + self.assertEqual(15, solver.value(x * 3)) + self.assertEqual(6, solver.value(1 + x)) + self.assertEqual(-10.0, solver.objective_value) + + def test_natural_api_maximize_float(self) -> None: + model = cp_model.CpModel() + x = model.new_bool_var("x") + y = model.new_int_var(0, 10, "y") + model.maximize(x.negated() * 3.5 + x.negated() - y + 2 * y + 1.6) + solver = cp_model.CpSolver() + self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) + self.assertFalse(solver.boolean_value(x)) + self.assertTrue(solver.boolean_value(x.negated())) + self.assertEqual(-10, solver.value(-y)) + self.assertEqual(16.1, solver.objective_value) + + def test_natural_api_maximize_complex(self) -> None: + model = cp_model.CpModel() + x1 = model.new_bool_var("x1") + x2 = model.new_bool_var("x1") + x3 = model.new_bool_var("x1") + x4 = model.new_bool_var("x1") + model.maximize( + cp_model.LinearExpr.sum([x1, x2]) + + cp_model.LinearExpr.weighted_sum([x3, x4.negated()], [2, 4]) + ) + solver = cp_model.CpSolver() + self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) + self.assertEqual(5, solver.value(3 + 2 * x1)) + self.assertEqual(3, solver.value(x1 + x2 + x3)) + self.assertEqual( + 1, solver.value(cp_model.LinearExpr.sum([x1, x2, x3, 0, -2])) + ) + self.assertEqual( + 7, + solver.value( + cp_model.LinearExpr.weighted_sum([x1, x2, x4, 3], [2, 2, 2, 1]) + ), + ) + self.assertEqual(5, solver.value(5 * x4.negated())) + self.assertEqual(8, solver.objective_value) + + def test_natural_api_maximize(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + model.add(2 * x - y == 1) + model.maximize(x - 2 * y + 3) + solver = cp_model.CpSolver() + self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) + self.assertEqual(-4, solver.value(x)) + self.assertEqual(-9, solver.value(y)) + self.assertEqual(17, solver.objective_value) + + def test_minimize_constant(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + model.add(x >= -1) + model.minimize(10) + solver = cp_model.CpSolver() + self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) + self.assertEqual(10, solver.objective_value) + + def test_maximize_constant(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + model.add(x >= -1) + model.maximize(5) + solver = cp_model.CpSolver() + self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) + self.assertEqual(5, solver.objective_value) + + def test_add_true(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + model.add(3 >= -1) + model.minimize(x) + solver = cp_model.CpSolver() + self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) + self.assertEqual(-10, solver.value(x)) + + def test_add_false(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + model.add(3 <= -1) + model.minimize(x) + solver = cp_model.CpSolver() + self.assertEqual("INFEASIBLE", solver.status_name(solver.solve(model))) + + def test_sum(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 2, f"x{i}") for i in range(100)] + model.add(sum(x) <= 1) + model.maximize(x[99]) + solver = cp_model.CpSolver() + self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) + self.assertEqual(1.0, solver.objective_value) + for i in range(100): + self.assertEqual(solver.value(x[i]), 1 if i == 99 else 0) + + def test_sum_parsing(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 2, f"x{i}") for i in range(5)] + s1 = cp_model.LinearExpr.sum(x) + self.assertTrue(s1.is_integer()) + flat_s1 = cp_model.FlatIntExpr(s1) + self.assertLen(flat_s1.vars, 5) + self.assertEqual(0, flat_s1.offset) + + s2 = cp_model.LinearExpr.sum(x[0], x[2], x[4]) + self.assertTrue(s2.is_integer()) + flat_s2 = cp_model.FlatIntExpr(s2) + self.assertLen(flat_s2.vars, 3) + self.assertEqual(0, flat_s2.offset) + + s3 = cp_model.LinearExpr.sum(x[0], x[2], 2, x[4], -4) + self.assertTrue(s3.is_integer()) + flat_s3 = cp_model.FlatIntExpr(s3) + self.assertLen(flat_s3.vars, 3) + self.assertEqual(-2, flat_s3.offset) + + s4 = cp_model.LinearExpr.sum(x[0], x[2], 2.5) + self.assertFalse(s4.is_integer()) + flat_s4 = cp_model.FlatFloatExpr(s4) + self.assertLen(flat_s4.vars, 2) + self.assertEqual(2.5, flat_s4.offset) + + s5 = cp_model.LinearExpr.sum(x[0], x[2], 2, 1.5) + self.assertFalse(s5.is_integer()) + flat_s5 = cp_model.FlatFloatExpr(s5) + self.assertLen(flat_s5.vars, 2) + self.assertEqual(3.5, flat_s5.offset) + self.assertEqual(str(s5), "(x0 + x2 + 3.5)") + + s5b = cp_model.LinearExpr.sum(x[0], x[2], 2, -2.5) + self.assertFalse(s5b.is_integer()) + self.assertEqual(str(s5b), "(x0 + x2 - 0.5)") + flat_s5b = cp_model.FlatFloatExpr(s5b) + self.assertLen(flat_s5b.vars, 2) + self.assertEqual(-0.5, flat_s5b.offset) + + s6 = cp_model.LinearExpr.sum(x[0], x[2], np.int8(-1), np.int64(-4)) + self.assertTrue(s6.is_integer()) + flat_s6 = cp_model.FlatIntExpr(s6) + self.assertLen(flat_s6.vars, 2) + self.assertEqual(-5, flat_s6.offset) + + s7 = cp_model.LinearExpr.sum(x[0], x[2], np.float64(2.0), np.float32(1.5)) + self.assertFalse(s7.is_integer()) + flat_s7 = cp_model.FlatFloatExpr(s7) + self.assertLen(flat_s7.vars, 2) + self.assertEqual(3.5, flat_s7.offset) + + s8 = cp_model.LinearExpr.sum(x[0], 3) + self.assertTrue(s8.is_integer()) + self.assertIsInstance(s8, cmh.IntAffine) + self.assertEqual(s8.expression, x[0]) + self.assertEqual(s8.coefficient, 1) + self.assertEqual(s8.offset, 3) + + s9 = cp_model.LinearExpr.sum(x[0], -2.1) + self.assertFalse(s9.is_integer()) + self.assertIsInstance(s9, cmh.FloatAffine) + self.assertEqual(s9.expression, x[0]) + self.assertEqual(s9.coefficient, 1.0) + self.assertEqual(s9.offset, -2.1) + self.assertEqual(str(s9), "(x0 - 2.1)") + + s10 = cp_model.LinearExpr.sum(x[0], 1, -1) + self.assertTrue(s10.is_integer()) + self.assertIsInstance(s10, cp_model.IntVar) + self.assertEqual(s10, x[0]) + + s11 = cp_model.LinearExpr.sum(x[0]) + self.assertTrue(s11.is_integer()) + self.assertIsInstance(s11, cp_model.IntVar) + self.assertEqual(s11, x[0]) + + s12 = cp_model.LinearExpr.sum(x[0], -x[2], -3) + self.assertEqual(str(s12), "(x0 + (-x2) - 3)") + self.assertEqual( + repr(s12), + "SumArray(x0(0..2), IntAffine(expr=x2(0..2), coeff=-1, offset=0)," + " int_offset=-3)", + ) + flat_int_s12 = cp_model.FlatIntExpr(s12) + self.assertEqual(str(flat_int_s12), "(x0 - x2 - 3)") + self.assertEqual( + repr(flat_int_s12), + "FlatIntExpr([x0(0..2), x2(0..2)], [1, -1], -3)", + ) + flat_float_s12 = cp_model.FlatFloatExpr(s12) + self.assertEqual(str(flat_float_s12), "(x0 - x2 - 3)") + self.assertEqual( + repr(flat_float_s12), + "FlatFloatExpr([x0(0..2), x2(0..2)], [1, -1], -3)", + ) + + s13 = cp_model.LinearExpr.sum(2) + self.assertEqual(str(s13), "2") + self.assertEqual(repr(s13), "IntConstant(2)") + + s14 = cp_model.LinearExpr.sum(2.5) + self.assertEqual(str(s14), "2.5") + self.assertEqual(repr(s14), "FloatConstant(2.5)") + + class FakeNpDTypeA: + + def __init__(self): + self.dtype = 2 + pass + + def __str__(self): + return "FakeNpDTypeA" + + class FakeNpDTypeB: + + def __init__(self): + self.is_integer = False + pass + + def __str__(self): + return "FakeNpDTypeB" + + with self.assertRaises(TypeError): + cp_model.LinearExpr.sum(x[0], x[2], "foo") + + with self.assertRaises(TypeError): + cp_model.LinearExpr.sum(x[0], x[2], FakeNpDTypeA()) + + with self.assertRaises(TypeError): + cp_model.LinearExpr.sum(x[0], x[2], FakeNpDTypeB()) + + def test_weighted_sum_parsing(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 2, f"x{i}") for i in range(5)] + c = [1, -2, 2, 3, 0.0] + float_c = [1, -1.0, 2, 3, 0.0] + + s1 = cp_model.LinearExpr.weighted_sum(x, c) + self.assertTrue(s1.is_integer()) + flat_s1 = cp_model.FlatIntExpr(s1) + self.assertLen(flat_s1.vars, 4) + self.assertEqual(0, flat_s1.offset) + + s2 = cp_model.LinearExpr.weighted_sum(x, float_c) + self.assertFalse(s2.is_integer()) + flat_s2 = cp_model.FlatFloatExpr(s2) + self.assertLen(flat_s2.vars, 4) + self.assertEqual(0, flat_s2.offset) + + s3 = cp_model.LinearExpr.weighted_sum(x + [2], c + [-1]) + self.assertTrue(s3.is_integer()) + flat_s3 = cp_model.FlatIntExpr(s3) + self.assertLen(flat_s3.vars, 4) + self.assertEqual(-2, flat_s3.offset) + + s4 = cp_model.LinearExpr.weighted_sum(x + [2], float_c + [-1.0]) + self.assertFalse(s4.is_integer()) + flat_s4 = cp_model.FlatFloatExpr(s4) + self.assertLen(flat_s4.vars, 4) + self.assertEqual(-2, flat_s4.offset) + + s5 = cp_model.LinearExpr.weighted_sum(x + [np.int16(2)], c + [-1]) + self.assertTrue(s5.is_integer()) + flat_s5 = cp_model.FlatIntExpr(s5) + self.assertLen(flat_s5.vars, 4) + self.assertEqual(-2, flat_s5.offset) + + s6 = cp_model.LinearExpr.weighted_sum([2], [1]) + self.assertEqual(repr(s6), "IntConstant(2)") + + s7 = cp_model.LinearExpr.weighted_sum([2], [1.25]) + self.assertEqual(repr(s7), "FloatConstant(2.5)") + + def test_sum_with_api(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 2, f"x{i}") for i in range(100)] + self.assertEqual(cp_model.LinearExpr.sum([x[0]]), x[0]) + self.assertEqual(cp_model.LinearExpr.sum([x[0], 0]), x[0]) + self.assertEqual(cp_model.LinearExpr.sum([x[0], 0.0]), x[0]) + self.assertEqual( + repr(cp_model.LinearExpr.sum([x[0], 2])), + repr(cp_model.LinearExpr.affine(x[0], 1, 2)), + ) + model.add(cp_model.LinearExpr.sum(x) <= 1) + model.maximize(x[99]) + solver = cp_model.CpSolver() + self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) + self.assertEqual(1.0, solver.objective_value) + for i in range(100): + self.assertEqual(solver.value(x[i]), 1 if i == 99 else 0) + + def test_weighted_sum(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 2, f"x{i}") for i in range(100)] + c = [2] * 100 + model.add(cp_model.LinearExpr.weighted_sum(x, c) <= 3) + model.maximize(x[99]) + solver = cp_model.CpSolver() + self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) + self.assertEqual(1.0, solver.objective_value) + for i in range(100): + self.assertEqual(solver.value(x[i]), 1 if i == 99 else 0) + + with self.assertRaises(ValueError): + cp_model.LinearExpr.weighted_sum([x[0]], [1, 2]) + with self.assertRaises(ValueError): + cp_model.LinearExpr.weighted_sum([x[0]], [1.1, 2.2]) + with self.assertRaises(ValueError): + cp_model.LinearExpr.weighted_sum([x[0], 3, 5], [1, 2]) + with self.assertRaises(ValueError): + cp_model.LinearExpr.weighted_sum([x[0], 2.2, 3], [1.1, 2.2]) + with self.assertRaises(ValueError): + cp_model.LinearExpr.WeightedSum([x[0]], [1, 2]) + with self.assertRaises(ValueError): + cp_model.LinearExpr.WeightedSum([x[0]], [1.1, 2.2]) + + def test_all_different(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] + model.add_all_different(x) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].all_diff.exprs, 5) + + def test_all_different_gen(self) -> None: + model = cp_model.CpModel() + model.add_all_different(model.new_int_var(0, 4, f"x{i}") for i in range(5)) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].all_diff.exprs, 5) + + def test_all_different_list(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] + model.add_all_different(x[0], x[1], x[2], x[3], x[4]) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].all_diff.exprs, 5) + + def test_element(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] + model.add_element(x[0], [x[1], 2, 4, x[2]], x[4]) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].element.exprs, 4) + self.assertEqual(0, model.proto.constraints[0].element.linear_index.vars[0]) + self.assertEqual( + 4, model.proto.constraints[0].element.linear_target.vars[0] + ) + with self.assertRaises(ValueError): + model.add_element(x[0], [], x[4]) + + def test_fixed_element(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 4, f"x{i}") for i in range(4)] + model.add_element(1, [x[0], 2, 4, x[2]], x[3]) + self.assertLen(model.proto.variables, 4) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].linear.vars, 1) + self.assertEqual(x[3].index, model.proto.constraints[0].linear.vars[0]) + self.assertEqual(1, model.proto.constraints[0].linear.coeffs[0]) + self.assertEqual([2, 2], model.proto.constraints[0].linear.domain) + + def test_affine_element(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] + model.add_element(x[0] + 1, [2 * x[1] - 2, 2, 4, x[2]], x[4] - 1) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].element.exprs, 4) + self.assertEqual(0, model.proto.constraints[0].element.linear_index.vars[0]) + self.assertEqual( + 1, model.proto.constraints[0].element.linear_index.coeffs[0] + ) + self.assertEqual(1, model.proto.constraints[0].element.linear_index.offset) + + self.assertEqual( + 4, model.proto.constraints[0].element.linear_target.vars[0] + ) + self.assertEqual( + 1, model.proto.constraints[0].element.linear_target.coeffs[0] + ) + self.assertEqual( + -1, model.proto.constraints[0].element.linear_target.offset + ) + self.assertEqual( + 4, model.proto.constraints[0].element.linear_target.vars[0] + ) + expr0 = model.proto.constraints[0].element.exprs[0] + self.assertEqual(1, expr0.vars[0]) + self.assertEqual(2, expr0.coeffs[0]) + self.assertEqual(-2, expr0.offset) + + def testCircuit(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + arcs: list[tuple[int, int, cp_model.LiteralT]] = [ + (i, i + 1, x[i]) for i in range(5) + ] + model.add_circuit(arcs) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].circuit.heads, 5) + self.assertLen(model.proto.constraints[0].circuit.tails, 5) + self.assertLen(model.proto.constraints[0].circuit.literals, 5) + with self.assertRaises(ValueError): + model.add_circuit([]) + + def test_multiple_circuit(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + arcs: list[tuple[int, int, cp_model.LiteralT]] = [ + (i, i + 1, x[i]) for i in range(5) + ] + model.add_multiple_circuit(arcs) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].routes.heads, 5) + self.assertLen(model.proto.constraints[0].routes.tails, 5) + self.assertLen(model.proto.constraints[0].routes.literals, 5) + with self.assertRaises(ValueError): + model.add_multiple_circuit([]) + + def test_allowed_assignments(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] + model.add_allowed_assignments( + x, [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0, 0)] + ) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].table.exprs, 5) + self.assertLen(model.proto.constraints[0].table.values, 15) + with self.assertRaises(TypeError): + model.add_allowed_assignments( + x, + [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0)], + ) + with self.assertRaises(ValueError): + model.add_allowed_assignments( + [], + [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0)], + ) + + def test_forbidden_assignments(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] + model.add_forbidden_assignments( + x, [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0, 0)] + ) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].table.exprs, 5) + self.assertLen(model.proto.constraints[0].table.values, 15) + self.assertTrue(model.proto.constraints[0].table.negated) + self.assertRaises( + TypeError, + model.add_forbidden_assignments, + x, + [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0)], + ) + self.assertRaises( + ValueError, + model.add_forbidden_assignments, + [], + [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0)], + ) + + def test_automaton(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] + model.add_automaton( + x, 0, [2, 3], [(0, 0, 0), (0, 1, 1), (1, 2, 2), (2, 3, 3)] + ) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].automaton.exprs, 5) + self.assertLen(model.proto.constraints[0].automaton.transition_tail, 4) + self.assertLen(model.proto.constraints[0].automaton.transition_head, 4) + self.assertLen(model.proto.constraints[0].automaton.transition_label, 4) + self.assertLen(model.proto.constraints[0].automaton.final_states, 2) + self.assertEqual(0, model.proto.constraints[0].automaton.starting_state) + with self.assertRaises(TypeError): + model.add_automaton( + x, + 0, + [2, 3], + [(0, 0, 0), (0, 1, 1), (2, 2), (2, 3, 3)], + ) + with self.assertRaises(ValueError): + model.add_automaton( + [], + 0, + [2, 3], + [(0, 0, 0), (0, 1, 1), (2, 3, 3)], + ) + with self.assertRaises(ValueError): + model.add_automaton( + x, + 0, + [], + [(0, 0, 0), (0, 1, 1), (2, 3, 3)], + ) + with self.assertRaises(ValueError): + model.add_automaton(x, 0, [2, 3], []) + + def test_inverse(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] + y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] + model.add_inverse(x, y) + self.assertLen(model.proto.variables, 10) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].inverse.f_direct, 5) + self.assertLen(model.proto.constraints[0].inverse.f_inverse, 5) + + def test_max_equality(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] + model.add_max_equality(x, y) + self.assertLen(model.proto.variables, 6) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].lin_max.exprs, 5) + self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) + self.assertEqual(1, model.proto.constraints[0].lin_max.target.coeffs[0]) + + def test_min_equality(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] + model.add_min_equality(x, y) + self.assertLen(model.proto.variables, 6) + self.assertLen(model.proto.constraints[0].lin_max.exprs, 5) + self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) + self.assertEqual(-1, model.proto.constraints[0].lin_max.target.coeffs[0]) + + def test_min_equality_list(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] + model.add_min_equality(x, [y[0], y[2], y[1], y[3]]) + self.assertLen(model.proto.variables, 6) + self.assertLen(model.proto.constraints[0].lin_max.exprs, 4) + self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) + self.assertEqual(-1, model.proto.constraints[0].lin_max.target.coeffs[0]) + + def test_min_equality_tuple(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] + model.add_min_equality(x, (y[0], y[2], y[1], y[3])) + self.assertLen(model.proto.variables, 6) + self.assertLen(model.proto.constraints[0].lin_max.exprs, 4) + self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) + self.assertEqual(-1, model.proto.constraints[0].lin_max.target.coeffs[0]) + + def test_min_equality_generator(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] + model.add_min_equality(x, (z for z in y)) + self.assertLen(model.proto.variables, 6) + self.assertLen(model.proto.constraints[0].lin_max.exprs, 5) + self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) + self.assertEqual(-1, model.proto.constraints[0].lin_max.target.coeffs[0]) + + def test_min_equality_with_constant(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = model.new_int_var(0, 4, "y") + model.add_min_equality(x, [y, 3]) + self.assertLen(model.proto.variables, 2) + self.assertLen(model.proto.constraints, 1) + lin_max = model.proto.constraints[0].lin_max + self.assertLen(lin_max.exprs, 2) + self.assertLen(lin_max.exprs[0].vars, 1) + self.assertEqual(1, lin_max.exprs[0].vars[0]) + self.assertEqual(-1, lin_max.exprs[0].coeffs[0]) + self.assertEqual(0, lin_max.exprs[0].offset) + self.assertEmpty(lin_max.exprs[1].vars) + self.assertEqual(-3, lin_max.exprs[1].offset) + + def test_abs(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = model.new_int_var(-5, 5, "y") + model.add_abs_equality(x, y) + self.assertLen(model.proto.variables, 2) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].lin_max.exprs, 2) + self.assertEqual(1, model.proto.constraints[0].lin_max.exprs[0].vars[0]) + self.assertEqual(1, model.proto.constraints[0].lin_max.exprs[0].coeffs[0]) + self.assertEqual(1, model.proto.constraints[0].lin_max.exprs[1].vars[0]) + self.assertEqual(-1, model.proto.constraints[0].lin_max.exprs[1].coeffs[0]) + passed = False + error_msg = None + try: + abs(x) + except NotImplementedError as e: + error_msg = str(e) + passed = True + self.assertEqual( + "calling abs() on a linear expression is not supported, " + "please use CpModel.add_abs_equality", + error_msg, + ) + self.assertTrue(passed) + + def test_issue4568(self) -> None: + model = cp_model.CpModel() + target = 11 + value = model.new_int_var(0, 10, "") + defect = model.new_int_var(0, cp_model.INT32_MAX, "") + model.add_abs_equality(defect, value - target) + model.minimize(defect) + + solver = cp_model.CpSolver() + status = solver.Solve(model) + self.assertEqual(status, cp_model.OPTIMAL) + self.assertEqual(solver.objective_value, 1.0) + + def test_division(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(0, 50, "y") + model.add_division_equality(x, y, 6) + self.assertLen(model.proto.variables, 2) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].int_div.exprs, 2) + self.assertEqual(model.proto.constraints[0].int_div.exprs[0].vars[0], 1) + self.assertEqual(model.proto.constraints[0].int_div.exprs[0].coeffs[0], 1) + self.assertEmpty(model.proto.constraints[0].int_div.exprs[1].vars) + self.assertEqual(model.proto.constraints[0].int_div.exprs[1].offset, 6) + passed = False + error_msg = None + try: + x / 3 + except NotImplementedError as e: + error_msg = str(e) + passed = True + self.assertEqual( + "calling // on a linear expression is not supported, " + "please use CpModel.add_division_equality", + error_msg, + ) + self.assertTrue(passed) + + def testModulo(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(0, 50, "y") + model.add_modulo_equality(x, y, 6) + self.assertLen(model.proto.variables, 2) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].int_mod.exprs, 2) + self.assertEqual(model.proto.constraints[0].int_mod.exprs[0].vars[0], 1) + self.assertEqual(model.proto.constraints[0].int_mod.exprs[0].coeffs[0], 1) + self.assertEmpty(model.proto.constraints[0].int_mod.exprs[1].vars) + self.assertEqual(model.proto.constraints[0].int_mod.exprs[1].offset, 6) + passed = False + error_msg = None + try: + x % 3 + except NotImplementedError as e: + error_msg = str(e) + passed = True + self.assertEqual( + "calling %% on a linear expression is not supported, " + "please use CpModel.add_modulo_equality", + error_msg, + ) + self.assertTrue(passed) + + def test_multiplication_equality(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] + model.add_multiplication_equality(x, y) + self.assertLen(model.proto.variables, 6) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].int_prod.exprs, 5) + self.assertEqual(0, model.proto.constraints[0].int_prod.target.vars[0]) + + def test_implication(self) -> None: + model = cp_model.CpModel() + x = model.new_bool_var("x") + y = model.new_bool_var("y") + model.add_implication(x, y) + self.assertLen(model.proto.variables, 2) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].bool_or.literals, 1) + self.assertLen(model.proto.constraints[0].enforcement_literal, 1) + self.assertEqual(x.index, model.proto.constraints[0].enforcement_literal[0]) + self.assertEqual(y.index, model.proto.constraints[0].bool_or.literals[0]) + + def test_bool_or(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + model.add_bool_or(x) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].bool_or.literals, 5) + model.add_bool_or([x[0], x[1], False]) + self.assertLen(model.proto.variables, 6) + with self.assertRaises(TypeError): + model.add_bool_or([x[2], 2]) + y = model.new_int_var(0, 4, "y") + with self.assertRaises(TypeError): + model.add_bool_or([y, False]) + + def test_bool_or_list_or_get(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + model.add_bool_or(x) + model.add_bool_or(True, x[0], x[2]) + model.add_bool_or(False, x[0]) + model.add_bool_or(x[i] for i in [0, 2, 3, 4]) + self.assertLen(model.proto.variables, 7) + self.assertLen(model.proto.constraints, 4) + self.assertLen(model.proto.constraints[0].bool_or.literals, 5) + self.assertLen(model.proto.constraints[1].bool_or.literals, 3) + self.assertLen(model.proto.constraints[2].bool_or.literals, 2) + self.assertLen(model.proto.constraints[3].bool_or.literals, 4) + + def test_at_least_one(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + model.add_at_least_one(x) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].bool_or.literals, 5) + model.add_at_least_one([x[0], x[1], False]) + self.assertLen(model.proto.variables, 6) + self.assertRaises(TypeError, model.add_at_least_one, [x[2], 2]) + y = model.new_int_var(0, 4, "y") + self.assertRaises(TypeError, model.add_at_least_one, [y, False]) + + def test_at_most_one(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + model.add_at_most_one(x) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].at_most_one.literals, 5) + model.add_at_most_one([x[0], x[1], False]) + self.assertLen(model.proto.variables, 6) + self.assertRaises(TypeError, model.add_at_most_one, [x[2], 2]) + y = model.new_int_var(0, 4, "y") + self.assertRaises(TypeError, model.add_at_most_one, [y, False]) + + def test_exactly_one(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + model.add_exactly_one(x) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].exactly_one.literals, 5) + model.add_exactly_one([x[0], x[1], False]) + self.assertLen(model.proto.variables, 6) + self.assertRaises(TypeError, model.add_exactly_one, [x[2], 2]) + y = model.new_int_var(0, 4, "y") + self.assertRaises(TypeError, model.add_exactly_one, [y, False]) + + def test_bool_and(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + model.add_bool_and(x) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].bool_and.literals, 5) + model.add_bool_and([x[1], x[2].negated(), True]) + self.assertEqual(1, model.proto.constraints[1].bool_and.literals[0]) + self.assertEqual(-3, model.proto.constraints[1].bool_and.literals[1]) + self.assertEqual(5, model.proto.constraints[1].bool_and.literals[2]) + + def test_bool_x_or(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + model.add_bool_xor(x) + self.assertLen(model.proto.variables, 5) + self.assertLen(model.proto.constraints, 1) + self.assertLen(model.proto.constraints[0].bool_xor.literals, 5) + + def test_map_domain(self) -> None: + model = cp_model.CpModel() + x = [model.new_bool_var(f"x{i}") for i in range(5)] + y = model.new_int_var(0, 10, "y") + model.add_map_domain(y, x, 2) + self.assertLen(model.proto.variables, 6) + self.assertLen(model.proto.constraints, 10) + + def test_interval(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = model.new_int_var(0, 3, "y") + i = model.new_interval_var(x, 3, y, "i") + self.assertEqual(0, i.index) + + j = model.new_fixed_size_interval_var(x, 2, "j") + self.assertEqual(1, j.index) + start_expr = j.start_expr() + size_expr = j.size_expr() + end_expr = j.end_expr() + self.assertEqual(x.index, start_expr.index) + self.assertEqual(size_expr, 2) + self.assertEqual(str(end_expr), "(x + 2)") + + def test_rebuild_from_linear_expression_proto(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = model.new_int_var(0, 1, "y") + z = model.new_int_var(0, 5, "z") + i = model.new_interval_var(x, y, z, "i") + self.assertEqual(i.start_expr(), x) + self.assertEqual(i.size_expr(), y) + self.assertEqual(i.end_expr(), z) + self.assertEqual(~i.size_expr(), ~y) + self.assertRaises(TypeError, i.start_expr().negated) + + proto = cp_model_pb2.LinearExpressionProto() + proto.vars.append(x.index) + proto.coeffs.append(1) + proto.vars.append(y.index) + proto.coeffs.append(2) + expr1 = model.rebuild_from_linear_expression_proto(proto) + canonical_expr1 = cmh.FlatIntExpr(expr1) + self.assertEqual(canonical_expr1.vars[0], x) + self.assertEqual(canonical_expr1.vars[1], y) + self.assertEqual(canonical_expr1.coeffs[0], 1) + self.assertEqual(canonical_expr1.coeffs[1], 2) + self.assertEqual(canonical_expr1.offset, 0) + self.assertEqual(~canonical_expr1.vars[1], ~y) + self.assertRaises(TypeError, canonical_expr1.vars[0].negated) + + proto.offset = 2 + expr2 = model.rebuild_from_linear_expression_proto(proto) + canonical_expr2 = cmh.FlatIntExpr(expr2) + self.assertEqual(canonical_expr2.vars[0], x) + self.assertEqual(canonical_expr2.vars[1], y) + self.assertEqual(canonical_expr2.coeffs[0], 1) + self.assertEqual(canonical_expr2.coeffs[1], 2) + self.assertEqual(canonical_expr2.offset, 2) + + def test_absent_interval(self) -> None: + model = cp_model.CpModel() + i = model.new_optional_interval_var(1, 0, 1, False, "") + self.assertEqual(0, i.index) + + def test_optional_interval(self) -> None: + model = cp_model.CpModel() + b = model.new_bool_var("b") + x = model.new_int_var(0, 4, "x") + y = model.new_int_var(0, 3, "y") + i = model.new_optional_interval_var(x, 3, y, b, "i") + j = model.new_optional_interval_var(x, y, 10, b, "j") + k = model.new_optional_interval_var(x, -y, 10, b, "k") + l = model.new_optional_interval_var(x, 10, -y, b, "l") + self.assertEqual(0, i.index) + self.assertEqual(1, j.index) + self.assertEqual(2, k.index) + self.assertEqual(3, l.index) + with self.assertRaises(TypeError): + model.new_optional_interval_var(1, 2, 3, x, "x") + with self.assertRaises(TypeError): + model.new_optional_interval_var(b + x, 2, 3, b, "x") + with self.assertRaises(TypeError): + model.new_optional_interval_var(1, 2, 3, b + 1, "x") + + def test_no_overlap(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = model.new_int_var(0, 3, "y") + z = model.new_int_var(0, 3, "y") + i = model.new_interval_var(x, 3, y, "i") + j = model.new_interval_var(x, 5, z, "j") + ct = model.add_no_overlap([i, j]) + self.assertEqual(2, ct.index) + self.assertLen(ct.proto.no_overlap.intervals, 2) + self.assertEqual(0, ct.proto.no_overlap.intervals[0]) + self.assertEqual(1, ct.proto.no_overlap.intervals[1]) + + def test_no_overlap2d(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = model.new_int_var(0, 3, "y") + z = model.new_int_var(0, 3, "y") + i = model.new_interval_var(x, 3, y, "i") + j = model.new_interval_var(x, 5, z, "j") + ct = model.add_no_overlap_2d([i, j], [j, i]) + self.assertEqual(2, ct.index) + self.assertLen(ct.proto.no_overlap_2d.x_intervals, 2) + self.assertEqual(0, ct.proto.no_overlap_2d.x_intervals[0]) + self.assertEqual(1, ct.proto.no_overlap_2d.x_intervals[1]) + self.assertLen(ct.proto.no_overlap_2d.y_intervals, 2) + self.assertEqual(1, ct.proto.no_overlap_2d.y_intervals[0]) + self.assertEqual(0, ct.proto.no_overlap_2d.y_intervals[1]) + + def test_cumulative(self) -> None: + model = cp_model.CpModel() + intervals = [ + model.new_interval_var( + model.new_int_var(0, 10, f"s_{i}"), + 5, + model.new_int_var(5, 15, f"e_{i}"), + f"interval[{i}]", ) - self.assertEqual("y", str(y)) - self.assertEqual("y(2..4, 7)", repr(y)) - z = model.new_int_var_from_domain( - cp_model.Domain.from_values([2, 3, 4, 7]), "z" - ) - self.assertEqual("z", str(z)) - self.assertEqual("z(2..4, 7)", repr(z)) - t = model.new_int_var_from_domain( - cp_model.Domain.from_flat_intervals([2, 4, 7, 7]), "t" - ) - self.assertEqual("t", str(t)) - self.assertEqual("t(2..4, 7)", repr(t)) - cst = model.new_constant(5) - self.assertEqual("5", str(cst)) - - def test_hash_int_var(self) -> None: - model = cp_model.CpModel() - var_a = model.new_int_var(0, 2, "a") - variables = set() - variables.add(var_a) - - def test_literal(self) -> None: - model = cp_model.CpModel() - x = model.new_bool_var("x") - self.assertEqual("x", str(x)) - self.assertEqual("not(x)", str(~x)) - self.assertEqual("not(x)", str(x.negated())) - self.assertEqual(x.negated().negated(), x) - self.assertEqual(x.negated().negated().index, x.index) - y = model.new_int_var(0, 1, "y") - self.assertEqual("y", str(y)) - self.assertEqual("not(y)", str(~y)) - zero = model.new_constant(0) - self.assertEqual("0", str(zero)) - self.assertEqual("not(0)", str(~zero)) - one = model.new_constant(1) - self.assertEqual("1", str(one)) - self.assertEqual("not(1)", str(~one)) - z = model.new_int_var(0, 2, "z") - self.assertRaises(TypeError, z.negated) - self.assertRaises(TypeError, z.__invert__) - - def test_negation(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - b = model.new_bool_var("b") - nb = b.negated() - self.assertEqual(b.negated(), nb) - self.assertEqual(~b, nb) - self.assertEqual(b.negated().negated(), b) - self.assertEqual(~(~b), b) - self.assertEqual(nb.index, -b.index - 1) - self.assertRaises(TypeError, x.negated) - - def test_issue_4654(self) -> None: - model = cp_model.CpModel() - x = model.NewIntVar(0, 1, "x") - y = model.NewIntVar(0, 2, "y") - z = model.NewIntVar(0, 3, "z") - expr = x - y - 2 * z - self.assertEqual(str(expr), "(-(2 * z) + (x - y))") - - def test_equality_overload(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(0, 5, "y") - self.assertEqual(x, x) - self.assertNotEqual(x, y) - - def test_linear(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - model.add_linear_constraint(x + 2 * y, 0, 10) - model.minimize(y) - solver = cp_model.CpSolver() - self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) - self.assertEqual(10, solver.value(x)) - self.assertEqual(-5, solver.value(y)) - - def test_linear_constraint(self) -> None: - model = cp_model.CpModel() - model.add_linear_constraint(5, 0, 10) - model.add_linear_constraint(-1, 0, 10) - self.assertLen(model.proto.constraints, 2) - self.assertTrue(model.proto.constraints[0].HasField("bool_and")) - self.assertEmpty(model.proto.constraints[0].bool_and.literals) - self.assertTrue(model.proto.constraints[1].HasField("bool_or")) - self.assertEmpty(model.proto.constraints[1].bool_or.literals) - - def test_linear_non_equal(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - ct = model.add(-x + y != 3).proto - self.assertLen(ct.linear.domain, 4) - self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) - self.assertEqual(2, ct.linear.domain[1]) - self.assertEqual(4, ct.linear.domain[2]) - self.assertEqual(cp_model.INT_MAX, ct.linear.domain[3]) - - def test_eq(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - ct = model.add(x == 2).proto - self.assertLen(ct.linear.vars, 1) - self.assertLen(ct.linear.coeffs, 1) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(2, ct.linear.domain[0]) - self.assertEqual(2, ct.linear.domain[1]) - - def testGe(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - ct = model.add(x >= 2).proto - self.assertLen(ct.linear.vars, 1) - self.assertLen(ct.linear.coeffs, 1) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(2, ct.linear.domain[0]) - self.assertEqual(cp_model.INT_MAX, ct.linear.domain[1]) - - def test_gt(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - ct = model.add(x > 2).proto - self.assertLen(ct.linear.vars, 1) - self.assertLen(ct.linear.coeffs, 1) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(3, ct.linear.domain[0]) - self.assertEqual(cp_model.INT_MAX, ct.linear.domain[1]) - - def test_le(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - ct = model.add(x <= 2).proto - self.assertLen(ct.linear.vars, 1) - self.assertLen(ct.linear.coeffs, 1) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) - self.assertEqual(2, ct.linear.domain[1]) - - def test_lt(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - ct = model.add(x < 2).proto - self.assertLen(ct.linear.vars, 1) - self.assertLen(ct.linear.coeffs, 1) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) - self.assertEqual(1, ct.linear.domain[1]) - - def test_eq_var(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - ct = model.add(x == y + 2).proto - self.assertLen(ct.linear.vars, 2) - self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) - self.assertLen(ct.linear.coeffs, 2) - self.assertEqual(0, ct.linear.coeffs[0] + ct.linear.coeffs[1]) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(2, ct.linear.domain[0]) - self.assertEqual(2, ct.linear.domain[1]) - - def test_ge_var(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - ct = model.add(x >= 1 - y).proto - self.assertLen(ct.linear.vars, 2) - self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) - self.assertLen(ct.linear.coeffs, 2) - self.assertEqual(1, ct.linear.coeffs[0]) - self.assertEqual(1, ct.linear.coeffs[1]) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(1, ct.linear.domain[0]) - self.assertEqual(cp_model.INT_MAX, ct.linear.domain[1]) - - def test_gt_var(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - ct = model.add(x > 1 - y).proto - self.assertLen(ct.linear.vars, 2) - self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) - self.assertLen(ct.linear.coeffs, 2) - self.assertEqual(1, ct.linear.coeffs[0]) - self.assertEqual(1, ct.linear.coeffs[1]) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(2, ct.linear.domain[0]) - self.assertEqual(cp_model.INT_MAX, ct.linear.domain[1]) - - def test_le_var(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - ct = model.add(x <= 1 - y).proto - self.assertLen(ct.linear.vars, 2) - self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) - self.assertLen(ct.linear.coeffs, 2) - self.assertEqual(1, ct.linear.coeffs[0]) - self.assertEqual(1, ct.linear.coeffs[1]) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) - self.assertEqual(1, ct.linear.domain[1]) - - def test_lt_var(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - ct = model.add(x < 1 - y).proto - self.assertLen(ct.linear.vars, 2) - self.assertEqual(1, ct.linear.vars[0] + ct.linear.vars[1]) - self.assertLen(ct.linear.coeffs, 2) - self.assertEqual(1, ct.linear.coeffs[0]) - self.assertEqual(1, ct.linear.coeffs[1]) - self.assertLen(ct.linear.domain, 2) - self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) - self.assertEqual(0, ct.linear.domain[1]) - - def test_linear_non_equal_with_constant(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - ct = model.add(x + y + 5 != 3).proto - self.assertLen(ct.linear.domain, 4) - # Checks that saturated arithmetics worked. - self.assertEqual(cp_model.INT_MIN, ct.linear.domain[0]) - self.assertEqual(-3, ct.linear.domain[1]) - self.assertEqual(-1, ct.linear.domain[2]) - self.assertEqual(cp_model.INT_MAX, ct.linear.domain[3]) - - def test_linear_with_enforcement(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - b = model.new_bool_var("b") - model.add_linear_constraint(x + 2 * y, 0, 10).only_enforce_if(b.negated()) - model.minimize(y) - self.assertLen(model.proto.constraints, 1) - self.assertEqual(-3, model.proto.constraints[0].enforcement_literal[0]) - c = model.new_bool_var("c") - model.add_linear_constraint(x + 4 * y, 0, 10).only_enforce_if([b, c]) - self.assertLen(model.proto.constraints, 2) - self.assertEqual(2, model.proto.constraints[1].enforcement_literal[0]) - self.assertEqual(3, model.proto.constraints[1].enforcement_literal[1]) - model.add_linear_constraint(x + 5 * y, 0, 10).only_enforce_if(c.negated(), b) - self.assertLen(model.proto.constraints, 3) - self.assertEqual(-4, model.proto.constraints[2].enforcement_literal[0]) - self.assertEqual(2, model.proto.constraints[2].enforcement_literal[1]) - - def test_constraint_with_name(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - ct = model.add_linear_constraint(x + 2 * y, 0, 10).with_name("test_constraint") - self.assertEqual("test_constraint", ct.name) - - def test_natural_api_minimize(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - model.add(x * 2 - 1 * y == 1) - model.minimize(x * 1 - 2 * y + 3) - solver = cp_model.CpSolver() - self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) - self.assertEqual(5, solver.value(x)) - self.assertEqual(15, solver.value(x * 3)) - self.assertEqual(6, solver.value(1 + x)) - self.assertEqual(-10.0, solver.objective_value) - - def test_natural_api_maximize_float(self) -> None: - model = cp_model.CpModel() - x = model.new_bool_var("x") - y = model.new_int_var(0, 10, "y") - model.maximize(x.negated() * 3.5 + x.negated() - y + 2 * y + 1.6) - solver = cp_model.CpSolver() - self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) - self.assertFalse(solver.boolean_value(x)) - self.assertTrue(solver.boolean_value(x.negated())) - self.assertEqual(-10, solver.value(-y)) - self.assertEqual(16.1, solver.objective_value) - - def test_natural_api_maximize_complex(self) -> None: - model = cp_model.CpModel() - x1 = model.new_bool_var("x1") - x2 = model.new_bool_var("x1") - x3 = model.new_bool_var("x1") - x4 = model.new_bool_var("x1") - model.maximize( - cp_model.LinearExpr.sum([x1, x2]) - + cp_model.LinearExpr.weighted_sum([x3, x4.negated()], [2, 4]) - ) - solver = cp_model.CpSolver() - self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) - self.assertEqual(5, solver.value(3 + 2 * x1)) - self.assertEqual(3, solver.value(x1 + x2 + x3)) - self.assertEqual(1, solver.value(cp_model.LinearExpr.sum([x1, x2, x3, 0, -2]))) - self.assertEqual( - 7, - solver.value( - cp_model.LinearExpr.weighted_sum([x1, x2, x4, 3], [2, 2, 2, 1]) - ), - ) - self.assertEqual(5, solver.value(5 * x4.negated())) - self.assertEqual(8, solver.objective_value) - - def test_natural_api_maximize(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - model.add(2 * x - y == 1) - model.maximize(x - 2 * y + 3) - solver = cp_model.CpSolver() - self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) - self.assertEqual(-4, solver.value(x)) - self.assertEqual(-9, solver.value(y)) - self.assertEqual(17, solver.objective_value) - - def test_minimize_constant(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - model.add(x >= -1) - model.minimize(10) - solver = cp_model.CpSolver() - self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) - self.assertEqual(10, solver.objective_value) - - def test_maximize_constant(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - model.add(x >= -1) - model.maximize(5) - solver = cp_model.CpSolver() - self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) - self.assertEqual(5, solver.objective_value) - - def test_add_true(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - model.add(3 >= -1) - model.minimize(x) - solver = cp_model.CpSolver() - self.assertEqual("OPTIMAL", solver.status_name(solver.solve(model))) - self.assertEqual(-10, solver.value(x)) - - def test_add_false(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - model.add(3 <= -1) - model.minimize(x) - solver = cp_model.CpSolver() - self.assertEqual("INFEASIBLE", solver.status_name(solver.solve(model))) - - def test_sum(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 2, f"x{i}") for i in range(100)] - model.add(sum(x) <= 1) - model.maximize(x[99]) - solver = cp_model.CpSolver() - self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) - self.assertEqual(1.0, solver.objective_value) - for i in range(100): - self.assertEqual(solver.value(x[i]), 1 if i == 99 else 0) - - def test_sum_parsing(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 2, f"x{i}") for i in range(5)] - s1 = cp_model.LinearExpr.sum(x) - self.assertTrue(s1.is_integer()) - flat_s1 = cp_model.FlatIntExpr(s1) - self.assertLen(flat_s1.vars, 5) - self.assertEqual(0, flat_s1.offset) - - s2 = cp_model.LinearExpr.sum(x[0], x[2], x[4]) - self.assertTrue(s2.is_integer()) - flat_s2 = cp_model.FlatIntExpr(s2) - self.assertLen(flat_s2.vars, 3) - self.assertEqual(0, flat_s2.offset) - - s3 = cp_model.LinearExpr.sum(x[0], x[2], 2, x[4], -4) - self.assertTrue(s3.is_integer()) - flat_s3 = cp_model.FlatIntExpr(s3) - self.assertLen(flat_s3.vars, 3) - self.assertEqual(-2, flat_s3.offset) - - s4 = cp_model.LinearExpr.sum(x[0], x[2], 2.5) - self.assertFalse(s4.is_integer()) - flat_s4 = cp_model.FlatFloatExpr(s4) - self.assertLen(flat_s4.vars, 2) - self.assertEqual(2.5, flat_s4.offset) - - s5 = cp_model.LinearExpr.sum(x[0], x[2], 2, 1.5) - self.assertFalse(s5.is_integer()) - flat_s5 = cp_model.FlatFloatExpr(s5) - self.assertLen(flat_s5.vars, 2) - self.assertEqual(3.5, flat_s5.offset) - self.assertEqual(str(s5), "(x0 + x2 + 3.5)") - - s5b = cp_model.LinearExpr.sum(x[0], x[2], 2, -2.5) - self.assertFalse(s5b.is_integer()) - self.assertEqual(str(s5b), "(x0 + x2 - 0.5)") - flat_s5b = cp_model.FlatFloatExpr(s5b) - self.assertLen(flat_s5b.vars, 2) - self.assertEqual(-0.5, flat_s5b.offset) - - s6 = cp_model.LinearExpr.sum(x[0], x[2], np.int8(-1), np.int64(-4)) - self.assertTrue(s6.is_integer()) - flat_s6 = cp_model.FlatIntExpr(s6) - self.assertLen(flat_s6.vars, 2) - self.assertEqual(-5, flat_s6.offset) - - s7 = cp_model.LinearExpr.sum(x[0], x[2], np.float64(2.0), np.float32(1.5)) - self.assertFalse(s7.is_integer()) - flat_s7 = cp_model.FlatFloatExpr(s7) - self.assertLen(flat_s7.vars, 2) - self.assertEqual(3.5, flat_s7.offset) - - s8 = cp_model.LinearExpr.sum(x[0], 3) - self.assertTrue(s8.is_integer()) - self.assertIsInstance(s8, cmh.IntAffine) - self.assertEqual(s8.expression, x[0]) - self.assertEqual(s8.coefficient, 1) - self.assertEqual(s8.offset, 3) - - s9 = cp_model.LinearExpr.sum(x[0], -2.1) - self.assertFalse(s9.is_integer()) - self.assertIsInstance(s9, cmh.FloatAffine) - self.assertEqual(s9.expression, x[0]) - self.assertEqual(s9.coefficient, 1.0) - self.assertEqual(s9.offset, -2.1) - self.assertEqual(str(s9), "(x0 - 2.1)") - - s10 = cp_model.LinearExpr.sum(x[0], 1, -1) - self.assertTrue(s10.is_integer()) - self.assertIsInstance(s10, cp_model.IntVar) - self.assertEqual(s10, x[0]) - - s11 = cp_model.LinearExpr.sum(x[0]) - self.assertTrue(s11.is_integer()) - self.assertIsInstance(s11, cp_model.IntVar) - self.assertEqual(s11, x[0]) - - s12 = cp_model.LinearExpr.sum(x[0], -x[2], -3) - self.assertEqual(str(s12), "(x0 + (-x2) - 3)") - self.assertEqual( - repr(s12), - "SumArray(x0(0..2), IntAffine(expr=x2(0..2), coeff=-1, offset=0)," - " int_offset=-3)", - ) - flat_int_s12 = cp_model.FlatIntExpr(s12) - self.assertEqual(str(flat_int_s12), "(x0 - x2 - 3)") - self.assertEqual( - repr(flat_int_s12), - "FlatIntExpr([x0(0..2), x2(0..2)], [1, -1], -3)", - ) - flat_float_s12 = cp_model.FlatFloatExpr(s12) - self.assertEqual(str(flat_float_s12), "(x0 - x2 - 3)") - self.assertEqual( - repr(flat_float_s12), - "FlatFloatExpr([x0(0..2), x2(0..2)], [1, -1], -3)", - ) - - s13 = cp_model.LinearExpr.sum(2) - self.assertEqual(str(s13), "2") - self.assertEqual(repr(s13), "IntConstant(2)") - - s14 = cp_model.LinearExpr.sum(2.5) - self.assertEqual(str(s14), "2.5") - self.assertEqual(repr(s14), "FloatConstant(2.5)") - - class FakeNpDTypeA: - - def __init__(self): - self.dtype = 2 - pass - - def __str__(self): - return "FakeNpDTypeA" - - class FakeNpDTypeB: - - def __init__(self): - self.is_integer = False - pass - - def __str__(self): - return "FakeNpDTypeB" - - with self.assertRaises(TypeError): - cp_model.LinearExpr.sum(x[0], x[2], "foo") - - with self.assertRaises(TypeError): - cp_model.LinearExpr.sum(x[0], x[2], FakeNpDTypeA()) - - with self.assertRaises(TypeError): - cp_model.LinearExpr.sum(x[0], x[2], FakeNpDTypeB()) - - def test_weighted_sum_parsing(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 2, f"x{i}") for i in range(5)] - c = [1, -2, 2, 3, 0.0] - float_c = [1, -1.0, 2, 3, 0.0] - - s1 = cp_model.LinearExpr.weighted_sum(x, c) - self.assertTrue(s1.is_integer()) - flat_s1 = cp_model.FlatIntExpr(s1) - self.assertLen(flat_s1.vars, 4) - self.assertEqual(0, flat_s1.offset) - - s2 = cp_model.LinearExpr.weighted_sum(x, float_c) - self.assertFalse(s2.is_integer()) - flat_s2 = cp_model.FlatFloatExpr(s2) - self.assertLen(flat_s2.vars, 4) - self.assertEqual(0, flat_s2.offset) - - s3 = cp_model.LinearExpr.weighted_sum(x + [2], c + [-1]) - self.assertTrue(s3.is_integer()) - flat_s3 = cp_model.FlatIntExpr(s3) - self.assertLen(flat_s3.vars, 4) - self.assertEqual(-2, flat_s3.offset) - - s4 = cp_model.LinearExpr.weighted_sum(x + [2], float_c + [-1.0]) - self.assertFalse(s4.is_integer()) - flat_s4 = cp_model.FlatFloatExpr(s4) - self.assertLen(flat_s4.vars, 4) - self.assertEqual(-2, flat_s4.offset) - - s5 = cp_model.LinearExpr.weighted_sum(x + [np.int16(2)], c + [-1]) - self.assertTrue(s5.is_integer()) - flat_s5 = cp_model.FlatIntExpr(s5) - self.assertLen(flat_s5.vars, 4) - self.assertEqual(-2, flat_s5.offset) - - s6 = cp_model.LinearExpr.weighted_sum([2], [1]) - self.assertEqual(repr(s6), "IntConstant(2)") - - s7 = cp_model.LinearExpr.weighted_sum([2], [1.25]) - self.assertEqual(repr(s7), "FloatConstant(2.5)") - - def test_sum_with_api(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 2, f"x{i}") for i in range(100)] - self.assertEqual(cp_model.LinearExpr.sum([x[0]]), x[0]) - self.assertEqual(cp_model.LinearExpr.sum([x[0], 0]), x[0]) - self.assertEqual(cp_model.LinearExpr.sum([x[0], 0.0]), x[0]) - self.assertEqual( - repr(cp_model.LinearExpr.sum([x[0], 2])), - repr(cp_model.LinearExpr.affine(x[0], 1, 2)), - ) - model.add(cp_model.LinearExpr.sum(x) <= 1) - model.maximize(x[99]) - solver = cp_model.CpSolver() - self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) - self.assertEqual(1.0, solver.objective_value) - for i in range(100): - self.assertEqual(solver.value(x[i]), 1 if i == 99 else 0) - - def test_weighted_sum(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 2, f"x{i}") for i in range(100)] - c = [2] * 100 - model.add(cp_model.LinearExpr.weighted_sum(x, c) <= 3) - model.maximize(x[99]) - solver = cp_model.CpSolver() - self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) - self.assertEqual(1.0, solver.objective_value) - for i in range(100): - self.assertEqual(solver.value(x[i]), 1 if i == 99 else 0) - - with self.assertRaises(ValueError): - cp_model.LinearExpr.weighted_sum([x[0]], [1, 2]) - with self.assertRaises(ValueError): - cp_model.LinearExpr.weighted_sum([x[0]], [1.1, 2.2]) - with self.assertRaises(ValueError): - cp_model.LinearExpr.weighted_sum([x[0], 3, 5], [1, 2]) - with self.assertRaises(ValueError): - cp_model.LinearExpr.weighted_sum([x[0], 2.2, 3], [1.1, 2.2]) - with self.assertRaises(ValueError): - cp_model.LinearExpr.WeightedSum([x[0]], [1, 2]) - with self.assertRaises(ValueError): - cp_model.LinearExpr.WeightedSum([x[0]], [1.1, 2.2]) - - def test_all_different(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] - model.add_all_different(x) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].all_diff.exprs, 5) - - def test_all_different_gen(self) -> None: - model = cp_model.CpModel() - model.add_all_different(model.new_int_var(0, 4, f"x{i}") for i in range(5)) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].all_diff.exprs, 5) - - def test_all_different_list(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] - model.add_all_different(x[0], x[1], x[2], x[3], x[4]) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].all_diff.exprs, 5) - - def test_element(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] - model.add_element(x[0], [x[1], 2, 4, x[2]], x[4]) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].element.exprs, 4) - self.assertEqual(0, model.proto.constraints[0].element.linear_index.vars[0]) - self.assertEqual(4, model.proto.constraints[0].element.linear_target.vars[0]) - with self.assertRaises(ValueError): - model.add_element(x[0], [], x[4]) - - def test_fixed_element(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 4, f"x{i}") for i in range(4)] - model.add_element(1, [x[0], 2, 4, x[2]], x[3]) - self.assertLen(model.proto.variables, 4) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].linear.vars, 1) - self.assertEqual(x[3].index, model.proto.constraints[0].linear.vars[0]) - self.assertEqual(1, model.proto.constraints[0].linear.coeffs[0]) - self.assertEqual([2, 2], model.proto.constraints[0].linear.domain) - - def test_affine_element(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] - model.add_element(x[0] + 1, [2 * x[1] - 2, 2, 4, x[2]], x[4] - 1) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].element.exprs, 4) - self.assertEqual(0, model.proto.constraints[0].element.linear_index.vars[0]) - self.assertEqual(1, model.proto.constraints[0].element.linear_index.coeffs[0]) - self.assertEqual(1, model.proto.constraints[0].element.linear_index.offset) - - self.assertEqual(4, model.proto.constraints[0].element.linear_target.vars[0]) - self.assertEqual(1, model.proto.constraints[0].element.linear_target.coeffs[0]) - self.assertEqual(-1, model.proto.constraints[0].element.linear_target.offset) - self.assertEqual(4, model.proto.constraints[0].element.linear_target.vars[0]) - expr0 = model.proto.constraints[0].element.exprs[0] - self.assertEqual(1, expr0.vars[0]) - self.assertEqual(2, expr0.coeffs[0]) - self.assertEqual(-2, expr0.offset) - - def testCircuit(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - arcs: list[tuple[int, int, cp_model.LiteralT]] = [ - (i, i + 1, x[i]) for i in range(5) - ] - model.add_circuit(arcs) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].circuit.heads, 5) - self.assertLen(model.proto.constraints[0].circuit.tails, 5) - self.assertLen(model.proto.constraints[0].circuit.literals, 5) - with self.assertRaises(ValueError): - model.add_circuit([]) - - def test_multiple_circuit(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - arcs: list[tuple[int, int, cp_model.LiteralT]] = [ - (i, i + 1, x[i]) for i in range(5) - ] - model.add_multiple_circuit(arcs) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].routes.heads, 5) - self.assertLen(model.proto.constraints[0].routes.tails, 5) - self.assertLen(model.proto.constraints[0].routes.literals, 5) - with self.assertRaises(ValueError): - model.add_multiple_circuit([]) - - def test_allowed_assignments(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] - model.add_allowed_assignments( - x, [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0, 0)] - ) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].table.exprs, 5) - self.assertLen(model.proto.constraints[0].table.values, 15) - with self.assertRaises(TypeError): - model.add_allowed_assignments( - x, - [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0)], - ) - with self.assertRaises(ValueError): - model.add_allowed_assignments( - [], - [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0)], - ) - - def test_forbidden_assignments(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] - model.add_forbidden_assignments( - x, [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0, 0)] - ) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].table.exprs, 5) - self.assertLen(model.proto.constraints[0].table.values, 15) - self.assertTrue(model.proto.constraints[0].table.negated) - self.assertRaises( - TypeError, - model.add_forbidden_assignments, - x, - [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0)], - ) - self.assertRaises( - ValueError, - model.add_forbidden_assignments, - [], - [(0, 1, 2, 3, 4), (4, 3, 2, 1, 1), (0, 0, 0, 0)], - ) - - def test_automaton(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] - model.add_automaton(x, 0, [2, 3], [(0, 0, 0), (0, 1, 1), (1, 2, 2), (2, 3, 3)]) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].automaton.exprs, 5) - self.assertLen(model.proto.constraints[0].automaton.transition_tail, 4) - self.assertLen(model.proto.constraints[0].automaton.transition_head, 4) - self.assertLen(model.proto.constraints[0].automaton.transition_label, 4) - self.assertLen(model.proto.constraints[0].automaton.final_states, 2) - self.assertEqual(0, model.proto.constraints[0].automaton.starting_state) - with self.assertRaises(TypeError): - model.add_automaton( - x, - 0, - [2, 3], - [(0, 0, 0), (0, 1, 1), (2, 2), (2, 3, 3)], - ) - with self.assertRaises(ValueError): - model.add_automaton( - [], - 0, - [2, 3], - [(0, 0, 0), (0, 1, 1), (2, 3, 3)], - ) - with self.assertRaises(ValueError): - model.add_automaton( - x, - 0, - [], - [(0, 0, 0), (0, 1, 1), (2, 3, 3)], - ) - with self.assertRaises(ValueError): - model.add_automaton(x, 0, [2, 3], []) - - def test_inverse(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 4, f"x{i}") for i in range(5)] - y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] - model.add_inverse(x, y) - self.assertLen(model.proto.variables, 10) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].inverse.f_direct, 5) - self.assertLen(model.proto.constraints[0].inverse.f_inverse, 5) - - def test_max_equality(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] - model.add_max_equality(x, y) - self.assertLen(model.proto.variables, 6) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].lin_max.exprs, 5) - self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) - self.assertEqual(1, model.proto.constraints[0].lin_max.target.coeffs[0]) - - def test_min_equality(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] - model.add_min_equality(x, y) - self.assertLen(model.proto.variables, 6) - self.assertLen(model.proto.constraints[0].lin_max.exprs, 5) - self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) - self.assertEqual(-1, model.proto.constraints[0].lin_max.target.coeffs[0]) - - def test_min_equality_list(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] - model.add_min_equality(x, [y[0], y[2], y[1], y[3]]) - self.assertLen(model.proto.variables, 6) - self.assertLen(model.proto.constraints[0].lin_max.exprs, 4) - self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) - self.assertEqual(-1, model.proto.constraints[0].lin_max.target.coeffs[0]) - - def test_min_equality_tuple(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] - model.add_min_equality(x, (y[0], y[2], y[1], y[3])) - self.assertLen(model.proto.variables, 6) - self.assertLen(model.proto.constraints[0].lin_max.exprs, 4) - self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) - self.assertEqual(-1, model.proto.constraints[0].lin_max.target.coeffs[0]) - - def test_min_equality_generator(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] - model.add_min_equality(x, (z for z in y)) - self.assertLen(model.proto.variables, 6) - self.assertLen(model.proto.constraints[0].lin_max.exprs, 5) - self.assertEqual(0, model.proto.constraints[0].lin_max.target.vars[0]) - self.assertEqual(-1, model.proto.constraints[0].lin_max.target.coeffs[0]) - - def test_min_equality_with_constant(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = model.new_int_var(0, 4, "y") - model.add_min_equality(x, [y, 3]) - self.assertLen(model.proto.variables, 2) - self.assertLen(model.proto.constraints, 1) - lin_max = model.proto.constraints[0].lin_max - self.assertLen(lin_max.exprs, 2) - self.assertLen(lin_max.exprs[0].vars, 1) - self.assertEqual(1, lin_max.exprs[0].vars[0]) - self.assertEqual(-1, lin_max.exprs[0].coeffs[0]) - self.assertEqual(0, lin_max.exprs[0].offset) - self.assertEmpty(lin_max.exprs[1].vars) - self.assertEqual(-3, lin_max.exprs[1].offset) - - def test_abs(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = model.new_int_var(-5, 5, "y") - model.add_abs_equality(x, y) - self.assertLen(model.proto.variables, 2) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].lin_max.exprs, 2) - self.assertEqual(1, model.proto.constraints[0].lin_max.exprs[0].vars[0]) - self.assertEqual(1, model.proto.constraints[0].lin_max.exprs[0].coeffs[0]) - self.assertEqual(1, model.proto.constraints[0].lin_max.exprs[1].vars[0]) - self.assertEqual(-1, model.proto.constraints[0].lin_max.exprs[1].coeffs[0]) - passed = False - error_msg = None - try: - abs(x) - except NotImplementedError as e: - error_msg = str(e) - passed = True - self.assertEqual( - "calling abs() on a linear expression is not supported, " - "please use CpModel.add_abs_equality", - error_msg, - ) - self.assertTrue(passed) - - def test_issue4568(self) -> None: - model = cp_model.CpModel() - target = 11 - value = model.new_int_var(0, 10, "") - defect = model.new_int_var(0, cp_model.INT32_MAX, "") - model.add_abs_equality(defect, value - target) - model.minimize(defect) - - solver = cp_model.CpSolver() - status = solver.Solve(model) - self.assertEqual(status, cp_model.OPTIMAL) - self.assertEqual(solver.objective_value, 1.0) - - def test_division(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 10, "x") - y = model.new_int_var(0, 50, "y") - model.add_division_equality(x, y, 6) - self.assertLen(model.proto.variables, 2) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].int_div.exprs, 2) - self.assertEqual(model.proto.constraints[0].int_div.exprs[0].vars[0], 1) - self.assertEqual(model.proto.constraints[0].int_div.exprs[0].coeffs[0], 1) - self.assertEmpty(model.proto.constraints[0].int_div.exprs[1].vars) - self.assertEqual(model.proto.constraints[0].int_div.exprs[1].offset, 6) - passed = False - error_msg = None - try: - x / 3 - except NotImplementedError as e: - error_msg = str(e) - passed = True - self.assertEqual( - "calling // on a linear expression is not supported, " - "please use CpModel.add_division_equality", - error_msg, - ) - self.assertTrue(passed) - - def testModulo(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 10, "x") - y = model.new_int_var(0, 50, "y") - model.add_modulo_equality(x, y, 6) - self.assertLen(model.proto.variables, 2) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].int_mod.exprs, 2) - self.assertEqual(model.proto.constraints[0].int_mod.exprs[0].vars[0], 1) - self.assertEqual(model.proto.constraints[0].int_mod.exprs[0].coeffs[0], 1) - self.assertEmpty(model.proto.constraints[0].int_mod.exprs[1].vars) - self.assertEqual(model.proto.constraints[0].int_mod.exprs[1].offset, 6) - passed = False - error_msg = None - try: - x % 3 - except NotImplementedError as e: - error_msg = str(e) - passed = True - self.assertEqual( - "calling %% on a linear expression is not supported, " - "please use CpModel.add_modulo_equality", - error_msg, - ) - self.assertTrue(passed) - - def test_multiplication_equality(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = [model.new_int_var(0, 4, f"y{i}") for i in range(5)] - model.add_multiplication_equality(x, y) - self.assertLen(model.proto.variables, 6) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].int_prod.exprs, 5) - self.assertEqual(0, model.proto.constraints[0].int_prod.target.vars[0]) - - def test_implication(self) -> None: - model = cp_model.CpModel() - x = model.new_bool_var("x") - y = model.new_bool_var("y") - model.add_implication(x, y) - self.assertLen(model.proto.variables, 2) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].bool_or.literals, 1) - self.assertLen(model.proto.constraints[0].enforcement_literal, 1) - self.assertEqual(x.index, model.proto.constraints[0].enforcement_literal[0]) - self.assertEqual(y.index, model.proto.constraints[0].bool_or.literals[0]) - - def test_bool_or(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - model.add_bool_or(x) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].bool_or.literals, 5) - model.add_bool_or([x[0], x[1], False]) - self.assertLen(model.proto.variables, 6) - with self.assertRaises(TypeError): - model.add_bool_or([x[2], 2]) - y = model.new_int_var(0, 4, "y") - with self.assertRaises(TypeError): - model.add_bool_or([y, False]) - - def test_bool_or_list_or_get(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - model.add_bool_or(x) - model.add_bool_or(True, x[0], x[2]) - model.add_bool_or(False, x[0]) - model.add_bool_or(x[i] for i in [0, 2, 3, 4]) - self.assertLen(model.proto.variables, 7) - self.assertLen(model.proto.constraints, 4) - self.assertLen(model.proto.constraints[0].bool_or.literals, 5) - self.assertLen(model.proto.constraints[1].bool_or.literals, 3) - self.assertLen(model.proto.constraints[2].bool_or.literals, 2) - self.assertLen(model.proto.constraints[3].bool_or.literals, 4) - - def test_at_least_one(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - model.add_at_least_one(x) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].bool_or.literals, 5) - model.add_at_least_one([x[0], x[1], False]) - self.assertLen(model.proto.variables, 6) - self.assertRaises(TypeError, model.add_at_least_one, [x[2], 2]) - y = model.new_int_var(0, 4, "y") - self.assertRaises(TypeError, model.add_at_least_one, [y, False]) - - def test_at_most_one(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - model.add_at_most_one(x) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].at_most_one.literals, 5) - model.add_at_most_one([x[0], x[1], False]) - self.assertLen(model.proto.variables, 6) - self.assertRaises(TypeError, model.add_at_most_one, [x[2], 2]) - y = model.new_int_var(0, 4, "y") - self.assertRaises(TypeError, model.add_at_most_one, [y, False]) - - def test_exactly_one(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - model.add_exactly_one(x) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].exactly_one.literals, 5) - model.add_exactly_one([x[0], x[1], False]) - self.assertLen(model.proto.variables, 6) - self.assertRaises(TypeError, model.add_exactly_one, [x[2], 2]) - y = model.new_int_var(0, 4, "y") - self.assertRaises(TypeError, model.add_exactly_one, [y, False]) - - def test_bool_and(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - model.add_bool_and(x) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].bool_and.literals, 5) - model.add_bool_and([x[1], x[2].negated(), True]) - self.assertEqual(1, model.proto.constraints[1].bool_and.literals[0]) - self.assertEqual(-3, model.proto.constraints[1].bool_and.literals[1]) - self.assertEqual(5, model.proto.constraints[1].bool_and.literals[2]) - - def test_bool_x_or(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - model.add_bool_xor(x) - self.assertLen(model.proto.variables, 5) - self.assertLen(model.proto.constraints, 1) - self.assertLen(model.proto.constraints[0].bool_xor.literals, 5) - - def test_map_domain(self) -> None: - model = cp_model.CpModel() - x = [model.new_bool_var(f"x{i}") for i in range(5)] - y = model.new_int_var(0, 10, "y") - model.add_map_domain(y, x, 2) - self.assertLen(model.proto.variables, 6) - self.assertLen(model.proto.constraints, 10) - - def test_interval(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = model.new_int_var(0, 3, "y") - i = model.new_interval_var(x, 3, y, "i") - self.assertEqual(0, i.index) - - j = model.new_fixed_size_interval_var(x, 2, "j") - self.assertEqual(1, j.index) - start_expr = j.start_expr() - size_expr = j.size_expr() - end_expr = j.end_expr() - self.assertEqual(x.index, start_expr.index) - self.assertEqual(size_expr, 2) - self.assertEqual(str(end_expr), "(x + 2)") - - def test_rebuild_from_linear_expression_proto(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = model.new_int_var(0, 1, "y") - z = model.new_int_var(0, 5, "z") - i = model.new_interval_var(x, y, z, "i") - self.assertEqual(i.start_expr(), x) - self.assertEqual(i.size_expr(), y) - self.assertEqual(i.end_expr(), z) - self.assertEqual(~i.size_expr(), ~y) - self.assertRaises(TypeError, i.start_expr().negated) - - proto = cp_model_pb2.LinearExpressionProto() - proto.vars.append(x.index) - proto.coeffs.append(1) - proto.vars.append(y.index) - proto.coeffs.append(2) - expr1 = model.rebuild_from_linear_expression_proto(proto) - canonical_expr1 = cmh.FlatIntExpr(expr1) - self.assertEqual(canonical_expr1.vars[0], x) - self.assertEqual(canonical_expr1.vars[1], y) - self.assertEqual(canonical_expr1.coeffs[0], 1) - self.assertEqual(canonical_expr1.coeffs[1], 2) - self.assertEqual(canonical_expr1.offset, 0) - self.assertEqual(~canonical_expr1.vars[1], ~y) - self.assertRaises(TypeError, canonical_expr1.vars[0].negated) - - proto.offset = 2 - expr2 = model.rebuild_from_linear_expression_proto(proto) - canonical_expr2 = cmh.FlatIntExpr(expr2) - self.assertEqual(canonical_expr2.vars[0], x) - self.assertEqual(canonical_expr2.vars[1], y) - self.assertEqual(canonical_expr2.coeffs[0], 1) - self.assertEqual(canonical_expr2.coeffs[1], 2) - self.assertEqual(canonical_expr2.offset, 2) - - def test_absent_interval(self) -> None: - model = cp_model.CpModel() - i = model.new_optional_interval_var(1, 0, 1, False, "") - self.assertEqual(0, i.index) - - def test_optional_interval(self) -> None: - model = cp_model.CpModel() - b = model.new_bool_var("b") - x = model.new_int_var(0, 4, "x") - y = model.new_int_var(0, 3, "y") - i = model.new_optional_interval_var(x, 3, y, b, "i") - j = model.new_optional_interval_var(x, y, 10, b, "j") - k = model.new_optional_interval_var(x, -y, 10, b, "k") - l = model.new_optional_interval_var(x, 10, -y, b, "l") - self.assertEqual(0, i.index) - self.assertEqual(1, j.index) - self.assertEqual(2, k.index) - self.assertEqual(3, l.index) - with self.assertRaises(TypeError): - model.new_optional_interval_var(1, 2, 3, x, "x") - with self.assertRaises(TypeError): - model.new_optional_interval_var(b + x, 2, 3, b, "x") - with self.assertRaises(TypeError): - model.new_optional_interval_var(1, 2, 3, b + 1, "x") - - def test_no_overlap(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = model.new_int_var(0, 3, "y") - z = model.new_int_var(0, 3, "y") - i = model.new_interval_var(x, 3, y, "i") - j = model.new_interval_var(x, 5, z, "j") - ct = model.add_no_overlap([i, j]) - self.assertEqual(2, ct.index) - self.assertLen(ct.proto.no_overlap.intervals, 2) - self.assertEqual(0, ct.proto.no_overlap.intervals[0]) - self.assertEqual(1, ct.proto.no_overlap.intervals[1]) - - def test_no_overlap2d(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = model.new_int_var(0, 3, "y") - z = model.new_int_var(0, 3, "y") - i = model.new_interval_var(x, 3, y, "i") - j = model.new_interval_var(x, 5, z, "j") - ct = model.add_no_overlap_2d([i, j], [j, i]) - self.assertEqual(2, ct.index) - self.assertLen(ct.proto.no_overlap_2d.x_intervals, 2) - self.assertEqual(0, ct.proto.no_overlap_2d.x_intervals[0]) - self.assertEqual(1, ct.proto.no_overlap_2d.x_intervals[1]) - self.assertLen(ct.proto.no_overlap_2d.y_intervals, 2) - self.assertEqual(1, ct.proto.no_overlap_2d.y_intervals[0]) - self.assertEqual(0, ct.proto.no_overlap_2d.y_intervals[1]) - - def test_cumulative(self) -> None: - model = cp_model.CpModel() - intervals = [ - model.new_interval_var( - model.new_int_var(0, 10, f"s_{i}"), - 5, - model.new_int_var(5, 15, f"e_{i}"), - f"interval[{i}]", - ) - for i in range(10) - ] - demands = [1, 3, 5, 2, 4, 5, 3, 4, 2, 3] - capacity = 4 - ct = model.add_cumulative(intervals, demands, capacity) - self.assertEqual(10, ct.index) - self.assertLen(ct.proto.cumulative.intervals, 10) - with self.assertRaises(TypeError): - model.add_cumulative([intervals[0], 3], [2, 3], 3) - - def test_get_or_make_index_from_constant(self) -> None: - model = cp_model.CpModel() - self.assertEqual(0, model.get_or_make_index_from_constant(3)) - self.assertEqual(0, model.get_or_make_index_from_constant(3)) - self.assertEqual(1, model.get_or_make_index_from_constant(5)) - model_var = model.proto.variables[0] - self.assertLen(model_var.domain, 2) - self.assertEqual(3, model_var.domain[0]) - self.assertEqual(3, model_var.domain[1]) - - def test_str(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - self.assertEqual(str(x == 2), "x == 2") - self.assertEqual(str(x >= 2), "x >= 2") - self.assertEqual(str(x <= 2), "x <= 2") - self.assertEqual(str(x > 2), "x >= 3") - self.assertEqual(str(x < 2), "x <= 1") - self.assertEqual(str(x != 2), "x != 2") - self.assertEqual(str(x * 3), "(3 * x)") - self.assertEqual(str(-x), "(-x)") - self.assertEqual(str(x + 3), "(x + 3)") - self.assertEqual(str(x <= cp_model.INT_MAX), "True (unbounded expr x)") - self.assertEqual(str(x != 9223372036854775807), "x <= 9223372036854775806") - self.assertEqual(str(x != -9223372036854775808), "x >= -9223372036854775807") - y = model.new_int_var(0, 4, "y") - self.assertEqual( - str(cp_model.LinearExpr.weighted_sum([x, y + 1, 2], [1, -2, 3])), - "(x - 2 * (y + 1) + 6)", - ) - self.assertEqual(str(cp_model.LinearExpr.term(x, 3)), "(3 * x)") - self.assertEqual(str(x != y), "(x - y) != 0") - self.assertEqual( - "0 <= x <= 10", - str(cp_model.BoundedLinearExpression(x, cp_model.Domain(0, 10))), - ) - e1 = 2 * cp_model.LinearExpr.sum([x, y]) - flat_e1 = cmh.FlatIntExpr(e1) - self.assertEqual(str(e1), "(2 * (x + y))") - self.assertEqual(flat_e1.vars, [x, y]) - self.assertEqual(flat_e1.coeffs, [2, 2]) - self.assertEqual(flat_e1.offset, 0) - repeat_flat_e1 = cmh.FlatIntExpr(flat_e1 + 3) - self.assertEqual(repeat_flat_e1.vars, [x, y]) - self.assertEqual(repeat_flat_e1.coeffs, [2, 2]) - self.assertEqual(repeat_flat_e1.offset, 3) - float_flat_e1 = cmh.FlatFloatExpr(flat_e1) - self.assertEqual(float_flat_e1.vars, [x, y]) - self.assertEqual(float_flat_e1.coeffs, [2.0, 2.0]) - self.assertEqual(float_flat_e1.offset, 0.0) - repeat_float_flat_e1 = cmh.FlatFloatExpr(float_flat_e1 - 2.5) - self.assertEqual(repeat_float_flat_e1.vars, [x, y]) - self.assertEqual(repeat_float_flat_e1.coeffs, [2.0, 2.0]) - self.assertEqual(repeat_float_flat_e1.offset, -2.5) - - b = model.new_bool_var("b") - self.assertEqual(str(cp_model.LinearExpr.term(b.negated(), 3)), "(3 * not(b))") - - i = model.new_interval_var(x, 2, y, "i") - self.assertEqual(str(i), "i") - - def test_repr(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 4, "x") - y = model.new_int_var(0, 3, "y") - z = model.new_int_var(0, 3, "z") - self.assertEqual(repr(x), "x(0..4)") - self.assertEqual(repr(x + 0), "x(0..4)") - self.assertEqual(repr(x + 0.0), "x(0..4)") - self.assertEqual(repr(x - 0), "x(0..4)") - self.assertEqual(repr(x - 0.0), "x(0..4)") - self.assertEqual(repr(x * 1), "x(0..4)") - self.assertEqual(repr(x * 1.0), "x(0..4)") - self.assertEqual(repr(x * 0), "IntConstant(0)") - self.assertEqual(repr(x * 0.0), "IntConstant(0)") - self.assertEqual(repr(x * 2), "IntAffine(expr=x(0..4), coeff=2, offset=0)") - self.assertEqual( - repr(x + 1.5), "FloatAffine(expr=x(0..4), coeff=1, offset=1.5)" - ) - self.assertEqual(repr(x + y), "SumArray(x(0..4), y(0..3))") - self.assertEqual( - repr(cp_model.LinearExpr.sum([x, y, z])), - "SumArray(x(0..4), y(0..3), z(0..3))", - ) - self.assertEqual( - repr(cp_model.LinearExpr.weighted_sum([x, y, 2], [1, 2, 3])), - "IntWeightedSum([x(0..4), y(0..3)], [1, 2], 6)", - ) - i = model.new_interval_var(x, 2, y, "i") - self.assertEqual(repr(i), "i(start = x, size = 2, end = y)") - b = model.new_bool_var("b") - self.assertEqual(repr(b), "b(0..1)") - self.assertEqual(repr(~b), "NotBooleanVariable(index=3)") - x1 = model.new_int_var(0, 4, "x1") - y1 = model.new_int_var(0, 3, "y1") - j = model.new_optional_interval_var(x1, 2, y1, b, "j") - self.assertEqual(repr(j), "j(start = x1, size = 2, end = y1, is_present = b)") - x2 = model.new_int_var(0, 4, "x2") - y2 = model.new_int_var(0, 3, "y2") - k = model.new_optional_interval_var(x2, 2, y2, b.negated(), "k") - self.assertEqual( - repr(k), "k(start = x2, size = 2, end = y2, is_present = not(b))" - ) - - def testDisplayBounds(self) -> None: - self.assertEqual("10..20", cp_model.display_bounds([10, 20])) - self.assertEqual("10", cp_model.display_bounds([10, 10])) - self.assertEqual("10..15, 20..30", cp_model.display_bounds([10, 15, 20, 30])) - - def test_short_name(self) -> None: - model = cp_model.CpModel() - model.proto.variables.add(domain=[5, 10]) - self.assertEqual("[5..10]", cp_model.short_name(model.proto, 0)) - - def test_integer_expression_errors(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 1, "x") - y = model.new_int_var(0, 3, "y") - self.assertRaises(TypeError, x.__mul__, y) - self.assertRaises(NotImplementedError, x.__div__, y) - self.assertRaises(NotImplementedError, x.__truediv__, y) - self.assertRaises(NotImplementedError, x.__mod__, y) - self.assertRaises(NotImplementedError, x.__pow__, y) - self.assertRaises(NotImplementedError, x.__lshift__, y) - self.assertRaises(NotImplementedError, x.__rshift__, y) - self.assertRaises(NotImplementedError, x.__and__, y) - self.assertRaises(NotImplementedError, x.__or__, y) - self.assertRaises(NotImplementedError, x.__xor__, y) - self.assertRaises(ArithmeticError, x.__lt__, cp_model.INT_MIN) - self.assertRaises(ArithmeticError, x.__gt__, cp_model.INT_MAX) - self.assertRaises(TypeError, x.__add__, "dummy") - self.assertRaises(TypeError, x.__mul__, "dummy") - - def test_model_errors(self) -> None: - model = cp_model.CpModel() - self.assertRaises(TypeError, model.add, "dummy") - self.assertRaises(TypeError, model.get_or_make_index, "dummy") - self.assertRaises(TypeError, model.minimize, "dummy") - - def test_solver_errors(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 1, "x") - y = model.new_int_var(-10, 10, "y") - model.add_linear_constraint(x + 2 * y, 0, 10) - model.minimize(y) - solver = cp_model.CpSolver() - self.assertRaises(RuntimeError, solver.value, x) - solver.solve(model) - self.assertRaises(TypeError, solver.value, "not_a_variable") - self.assertRaises(TypeError, model.add_bool_or, [x, y]) - - def test_has_objective_minimize(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 1, "x") - y = model.new_int_var(-10, 10, "y") - model.add_linear_constraint(x + 2 * y, 0, 10) - self.assertFalse(model.has_objective()) - model.minimize(y) - self.assertTrue(model.has_objective()) - - def test_has_objective_maximize(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 1, "x") - y = model.new_int_var(-10, 10, "y") - model.add_linear_constraint(x + 2 * y, 0, 10) - self.assertFalse(model.has_objective()) - model.maximize(y) - self.assertTrue(model.has_objective()) - - def test_search_for_all_solutions(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - y = model.new_int_var(0, 5, "y") - model.add_linear_constraint(x + y, 6, 6) - - solver = cp_model.CpSolver() - solver.parameters.enumerate_all_solutions = True - solution_counter = SolutionCounter() - status = solver.solve(model, solution_counter) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(5, solution_counter.solution_count) - - def test_solve_with_solution_callback(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - y = model.new_int_var(0, 5, "y") - model.add_linear_constraint(x + y, 6, 6) - - solver = cp_model.CpSolver() - solution_sum = SolutionSum([x, y]) - self.assertRaises(RuntimeError, solution_sum.value, x) - status = solver.solve(model, solution_sum) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(6, solution_sum.sum) - - def test_solve_with_float_value_in_callback(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - y = model.new_int_var(0, 5, "y") - model.add_linear_constraint(x + y, 6, 6) - - solver = cp_model.CpSolver() - solution_float_value = SolutionFloatValue((x + y) * 0.5) - status = solver.solve(model, solution_float_value) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(3.0, solution_float_value.value) - - def test_best_bound_callback(self) -> None: - model = cp_model.CpModel() - x0 = model.new_bool_var("x0") - x1 = model.new_bool_var("x1") - x2 = model.new_bool_var("x2") - x3 = model.new_bool_var("x3") - model.add_bool_or(x0, x1, x2, x3) - model.minimize(3 * x0 + 2 * x1 + 4 * x2 + 5 * x3 + 0.6) - - solver = cp_model.CpSolver() - best_bound_callback = BestBoundCallback() - solver.best_bound_callback = best_bound_callback.new_best_bound - solver.parameters.num_workers = 1 - solver.parameters.linearization_level = 2 - status = solver.solve(model) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(2.6, best_bound_callback.best_bound) - - def test_value(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 10, "x") - y = model.new_int_var(0, 10, "y") - model.add(x + 2 * y == 29) - solver = cp_model.CpSolver() - status = solver.solve(model) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(solver.value(x), 9) - self.assertEqual(solver.value(y), 10) - self.assertEqual(solver.value(2), 2) - - def test_float_value(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 10, "x") - y = model.new_int_var(0, 10, "y") - model.add(x + 2 * y == 29) - solver = cp_model.CpSolver() - status = solver.solve(model) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(solver.float_value(x * 1.5 + 0.25), 13.75) - self.assertEqual(solver.float_value(2.25), 2.25) - - def test_boolean_value(self) -> None: - model = cp_model.CpModel() - x = model.new_bool_var("x") - y = model.new_bool_var("y") - z = model.new_bool_var("z") - model.add_bool_or([x, z.negated()]) - model.add_bool_or([x, z]) - model.add_bool_or([x.negated(), y.negated()]) - solver = cp_model.CpSolver() - status = solver.solve(model) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(solver.boolean_value(x), True) - self.assertEqual(solver.value(x), 1 - solver.value(x.negated())) - self.assertEqual(solver.value(y), 1 - solver.value(y.negated())) - self.assertEqual(solver.value(z), 1 - solver.value(z.negated())) - self.assertEqual(solver.boolean_value(y), False) - self.assertEqual(solver.boolean_value(True), True) - self.assertEqual(solver.boolean_value(False), False) - self.assertEqual(solver.boolean_value(2), True) - self.assertEqual(solver.boolean_value(0), False) - - def test_unsupported_operators(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 10, "x") - y = model.new_int_var(0, 10, "y") - z = model.new_int_var(0, 10, "z") - - with self.assertRaises(NotImplementedError): - model.add(x == min(y, z)) - with self.assertRaises(NotImplementedError): - if x > y: - print("passed1") - with self.assertRaises(NotImplementedError): - if x == 2: - print("passed2") - - def test_is_literal_true_false(self) -> None: - model = cp_model.CpModel() - x = model.new_constant(0) - self.assertFalse(cp_model.object_is_a_true_literal(x)) - self.assertTrue(cp_model.object_is_a_false_literal(x)) - self.assertTrue(cp_model.object_is_a_true_literal(x.negated())) - self.assertFalse(cp_model.object_is_a_false_literal(x.negated())) - self.assertTrue(cp_model.object_is_a_true_literal(True)) - self.assertTrue(cp_model.object_is_a_false_literal(False)) - self.assertFalse(cp_model.object_is_a_true_literal(False)) - self.assertFalse(cp_model.object_is_a_false_literal(True)) - - def test_solve_minimize_with_solution_callback(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - y = model.new_int_var(0, 5, "y") - model.add_linear_constraint(x + y, 6, 6) - model.maximize(x + 2 * y) - - solver = cp_model.CpSolver() - solution_obj = SolutionObjective() - status = solver.solve(model, solution_obj) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(11, solution_obj.obj) - - def test_solution_value(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - b = model.new_bool_var("b") - model.add_decision_strategy( - [x], cp_model.CHOOSE_MIN_DOMAIN_SIZE, cp_model.SELECT_MAX_VALUE - ) - model.add_decision_strategy( - [b], cp_model.CHOOSE_MIN_DOMAIN_SIZE, cp_model.SELECT_MIN_VALUE - ) - solver = cp_model.CpSolver() - solver.parameters.keep_all_feasible_solutions_in_presolve = True - solver.parameters.num_workers = 1 - solution_recorder = RecordSolution([3, x, 1 - x], [1, False, ~b]) - status = solver.solve(model, solution_recorder) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual([3, 5, -4], solution_recorder.int_var_values) - self.assertEqual([True, False, True], solution_recorder.bool_var_values) - - def test_solution_hinting(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - y = model.new_int_var(0, 5, "y") - model.add_linear_constraint(x + y, 6, 6) - model.add_hint(x, 2) - model.add_hint(y, 4) - solver = cp_model.CpSolver() - solver.parameters.cp_model_presolve = False - status = solver.solve(model) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(2, solver.value(x)) - self.assertEqual(4, solver.value(y)) - - def test_solution_hinting_with_booleans(self) -> None: - model = cp_model.CpModel() - x = model.new_bool_var("x") - y = model.new_bool_var("y") - model.add_linear_constraint(x + y, 1, 1) - model.add_hint(x, True) - model.add_hint(~y, True) - solver = cp_model.CpSolver() - solver.parameters.cp_model_presolve = False - status = solver.solve(model) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertTrue(solver.boolean_value(x)) - self.assertFalse(solver.boolean_value(y)) - - def test_stats(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - y = model.new_int_var(0, 5, "y") - model.add_linear_constraint(x + y, 4, 6) - model.add_linear_constraint(2 * x + y, 0, 10) - model.maximize(x + 2 * y) - - solver = cp_model.CpSolver() - status = solver.solve(model) - self.assertEqual(cp_model.OPTIMAL, status) - self.assertEqual(solver.num_booleans, 0) - self.assertEqual(solver.num_conflicts, 0) - self.assertEqual(solver.num_branches, 0) - self.assertGreater(solver.wall_time, 0.0) - - def test_search_strategy(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - y = model.new_int_var(0, 5, "y") - z = model.new_bool_var("z") - model.add_decision_strategy( - [y, x, z.negated()], - cp_model.CHOOSE_MIN_DOMAIN_SIZE, - cp_model.SELECT_MAX_VALUE, - ) - self.assertLen(model.proto.search_strategy, 1) - strategy = model.proto.search_strategy[0] - self.assertLen(strategy.exprs, 3) - self.assertEqual(y.index, strategy.exprs[0].vars[0]) - self.assertEqual(1, strategy.exprs[0].coeffs[0]) - self.assertEqual(x.index, strategy.exprs[1].vars[0]) - self.assertEqual(1, strategy.exprs[1].coeffs[0]) - self.assertEqual(z.index, strategy.exprs[2].vars[0]) - self.assertEqual(-1, strategy.exprs[2].coeffs[0]) - self.assertEqual(1, strategy.exprs[2].offset) - self.assertEqual( - cp_model.CHOOSE_MIN_DOMAIN_SIZE, strategy.variable_selection_strategy - ) - self.assertEqual(cp_model.SELECT_MAX_VALUE, strategy.domain_reduction_strategy) - - def test_model_and_response_stats(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - y = model.new_int_var(0, 5, "y") - model.add_linear_constraint(x + y, 6, 6) - model.maximize(x + 2 * y) - self.assertTrue(model.model_stats()) - - solver = cp_model.CpSolver() - solver.solve(model) - self.assertTrue(solver.response_stats()) - - def test_validate_model(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 5, "x") - y = model.new_int_var(0, 5, "y") - model.add_linear_constraint(x + y, 6, 6) - model.maximize(x + 2 * y) - self.assertFalse(model.validate()) - - def test_validate_model_with_overflow(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, cp_model.INT_MAX, "x") - y = model.new_int_var(0, 10, "y") - model.add_linear_constraint(x + y, 6, cp_model.INT_MAX) - model.maximize(x + 2 * y) - self.assertTrue(model.validate()) - - def test_copy_model(self) -> None: - model = cp_model.CpModel() - b = model.new_bool_var("b") - x = model.new_int_var(0, 4, "x") - y = model.new_int_var(0, 3, "y") - i = model.new_optional_interval_var(x, 12, y, b, "i") - lin = model.add(x + y <= 10) - - new_model = model.clone() - clone_b = new_model.get_bool_var_from_proto_index(b.index) - clone_x = new_model.get_int_var_from_proto_index(x.index) - clone_y = new_model.get_int_var_from_proto_index(y.index) - clone_i = new_model.get_interval_var_from_proto_index(i.index) - - self.assertEqual(b.index, clone_b.index) - self.assertEqual(x.index, clone_x.index) - self.assertEqual(y.index, clone_y.index) - self.assertEqual(i.index, clone_i.index) - - solo_copy_b = copy.copy(b) - self.assertEqual(b.index, solo_copy_b.index) - self.assertEqual(b.is_boolean, solo_copy_b.is_boolean) - self.assertIs(solo_copy_b.model_proto, b.model_proto) - solo_copy_x = copy.copy(x) - self.assertEqual(x.index, solo_copy_x.index) - self.assertEqual(x.is_boolean, solo_copy_x.is_boolean) - self.assertIs(solo_copy_x.model_proto, x.model_proto) - solo_copy_i = copy.copy(i) - self.assertEqual(i.index, solo_copy_i.index) - self.assertIs(solo_copy_i.model_proto, i.model_proto) - - model_copy = copy.copy(model) - copy_b = model_copy.get_bool_var_from_proto_index(b.index) - copy_x = model_copy.get_int_var_from_proto_index(x.index) - copy_y = model_copy.get_int_var_from_proto_index(y.index) - copy_i = model_copy.get_interval_var_from_proto_index(i.index) - - self.assertEqual(b.index, copy_b.index) - self.assertEqual(x.index, copy_x.index) - self.assertEqual(y.index, copy_y.index) - self.assertEqual(i.index, copy_i.index) - self.assertEqual(b.is_boolean, copy_b.is_boolean) - self.assertEqual(x.is_boolean, copy_x.is_boolean) - self.assertEqual(y.is_boolean, copy_y.is_boolean) - self.assertIs(copy_b.model_proto, b.model_proto) - self.assertIs(copy_x.model_proto, x.model_proto) - self.assertIs(copy_i.model_proto, i.model_proto) - - model_deepcopy = copy.deepcopy(model) - deepcopy_b = model_deepcopy.get_bool_var_from_proto_index(b.index) - deepcopy_x = model_deepcopy.get_int_var_from_proto_index(x.index) - deepcopy_y = model_deepcopy.get_int_var_from_proto_index(y.index) - deepcopy_i = model_deepcopy.get_interval_var_from_proto_index(i.index) - - self.assertEqual(b.index, deepcopy_b.index) - self.assertEqual(x.index, deepcopy_x.index) - self.assertEqual(y.index, deepcopy_y.index) - self.assertEqual(i.index, deepcopy_i.index) - self.assertEqual(b.is_boolean, deepcopy_b.is_boolean) - self.assertEqual(x.is_boolean, deepcopy_x.is_boolean) - self.assertEqual(y.is_boolean, deepcopy_y.is_boolean) - self.assertIsNot(deepcopy_b.model_proto, b.model_proto) - self.assertIsNot(deepcopy_x.model_proto, x.model_proto) - self.assertIsNot(deepcopy_y.model_proto, y.model_proto) - self.assertIsNot(deepcopy_i.model_proto, i.model_proto) - self.assertIs(deepcopy_b.model_proto, deepcopy_x.model_proto) - self.assertIs(deepcopy_b.model_proto, deepcopy_y.model_proto) - self.assertIs(deepcopy_b.model_proto, deepcopy_i.model_proto) - - with self.assertRaises(ValueError): - new_model.get_bool_var_from_proto_index(-1) - - with self.assertRaises(ValueError): - new_model.get_int_var_from_proto_index(-1) - - with self.assertRaises(ValueError): - new_model.get_interval_var_from_proto_index(-1) - - with self.assertRaises(ValueError): - new_model.get_bool_var_from_proto_index(x.index) - - with self.assertRaises(ValueError): - new_model.get_interval_var_from_proto_index(lin.index) - - interval_ct = new_model.proto.constraints[copy_i.index].interval - self.assertEqual(12, interval_ct.size.offset) - - class Composite: - - def __init__(self, model: cp_model.CpModel, var: cp_model.IntVar): - self.model = model - self.var = var - - c = Composite(model, x) - copy_c = copy.copy(c) - self.assertIs(copy_c.model, c.model) - self.assertIs(copy_c.var, c.var) - - deepcopy_c = copy.deepcopy(c) - self.assertIsNot(deepcopy_c.model, c.model) - self.assertIsNot(deepcopy_c.var, c.var) - self.assertIs(deepcopy_c.model.proto, deepcopy_c.var.model_proto) - self.assertIs( - deepcopy_c.var, - deepcopy_c.model.get_int_var_from_proto_index(x.index), - ) - - def test_custom_log(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - y = model.new_int_var(-10, 10, "y") - model.add_linear_constraint(x + 2 * y, 0, 10) - model.minimize(y) - solver = cp_model.CpSolver() - solver.parameters.log_search_progress = True - solver.parameters.log_to_stdout = False - log_callback = LogToString() - solver.log_callback = log_callback.new_message - - self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) - self.assertEqual(10, solver.value(x)) - self.assertEqual(-5, solver.value(y)) - - self.assertRegex(log_callback.log, ".*log_to_stdout.*") - - def test_issue2762(self) -> None: - model = cp_model.CpModel() - - x = [model.new_bool_var("a"), model.new_bool_var("b")] - with self.assertRaises(NotImplementedError): - model.add((x[0] != 0) or (x[1] != 0)) - - def test_model_error(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, -2, f"x{i}") for i in range(100)] - model.add(sum(x) <= 1) - solver = cp_model.CpSolver() - solver.parameters.log_search_progress = True - self.assertEqual(cp_model.MODEL_INVALID, solver.solve(model)) - self.assertEqual(solver.solution_info(), 'var #0 has no domain(): name: "x0"') - - def test_int_var_series(self) -> None: - df = pd.DataFrame([1, -1, 1], columns=["coeffs"]) - model = cp_model.CpModel() - x = model.new_int_var_series( - name="x", index=df.index, lower_bounds=0, upper_bounds=5 - ) - model.minimize(df.coeffs.dot(x)) - solver = cp_model.CpSolver() - self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) - solution = solver.values(x) - self.assertTrue((solution.values == [0, 5, 0]).all()) - self.assertRaises(TypeError, x.apply, lambda x: ~x) - y = model.new_int_var_series( - name="y", index=df.index, lower_bounds=-1, upper_bounds=1 - ) - self.assertRaises(TypeError, y.apply, lambda x: ~x) - z = model.new_int_var_series( - name="y", index=df.index, lower_bounds=0, upper_bounds=1 - ) - _ = z.apply(lambda x: ~x) - - def test_bool_var_series(self) -> None: - df = pd.DataFrame([1, -1, 1], columns=["coeffs"]) - model = cp_model.CpModel() - x = model.new_bool_var_series(name="x", index=df.index) - _ = x.apply(lambda x: ~x) - y = model.new_int_var_series( - name="y", index=df.index, lower_bounds=0, upper_bounds=1 - ) - _ = y.apply(lambda x: ~x) - model.minimize(df.coeffs.dot(x)) - solver = cp_model.CpSolver() - self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) - solution = solver.boolean_values(x) - self.assertTrue((solution.values == [False, True, False]).all()) - - def test_fixed_size_interval_var_series(self) -> None: - df = pd.DataFrame([2, 4, 6], columns=["size"]) - model = cp_model.CpModel() - starts = model.new_int_var_series( - name="starts", index=df.index, lower_bounds=0, upper_bounds=5 - ) - presences = model.new_bool_var_series(name="rresences", index=df.index) - fixed_size_intervals = model.new_fixed_size_interval_var_series( - name="fixed_size_intervals", - index=df.index, - starts=starts, - sizes=df.size, - ) - opt_fixed_size_intervals = model.new_optional_fixed_size_interval_var_series( + for i in range(10) + ] + demands = [1, 3, 5, 2, 4, 5, 3, 4, 2, 3] + capacity = 4 + ct = model.add_cumulative(intervals, demands, capacity) + self.assertEqual(10, ct.index) + self.assertLen(ct.proto.cumulative.intervals, 10) + with self.assertRaises(TypeError): + model.add_cumulative([intervals[0], 3], [2, 3], 3) + + def test_get_or_make_index_from_constant(self) -> None: + model = cp_model.CpModel() + self.assertEqual(0, model.get_or_make_index_from_constant(3)) + self.assertEqual(0, model.get_or_make_index_from_constant(3)) + self.assertEqual(1, model.get_or_make_index_from_constant(5)) + model_var = model.proto.variables[0] + self.assertLen(model_var.domain, 2) + self.assertEqual(3, model_var.domain[0]) + self.assertEqual(3, model_var.domain[1]) + + def test_str(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + self.assertEqual(str(x == 2), "x == 2") + self.assertEqual(str(x >= 2), "x >= 2") + self.assertEqual(str(x <= 2), "x <= 2") + self.assertEqual(str(x > 2), "x >= 3") + self.assertEqual(str(x < 2), "x <= 1") + self.assertEqual(str(x != 2), "x != 2") + self.assertEqual(str(x * 3), "(3 * x)") + self.assertEqual(str(-x), "(-x)") + self.assertEqual(str(x + 3), "(x + 3)") + self.assertEqual(str(x <= cp_model.INT_MAX), "True (unbounded expr x)") + self.assertEqual(str(x != 9223372036854775807), "x <= 9223372036854775806") + self.assertEqual( + str(x != -9223372036854775808), "x >= -9223372036854775807" + ) + y = model.new_int_var(0, 4, "y") + self.assertEqual( + str(cp_model.LinearExpr.weighted_sum([x, y + 1, 2], [1, -2, 3])), + "(x - 2 * (y + 1) + 6)", + ) + self.assertEqual(str(cp_model.LinearExpr.term(x, 3)), "(3 * x)") + self.assertEqual(str(x != y), "(x - y) != 0") + self.assertEqual( + "0 <= x <= 10", + str(cp_model.BoundedLinearExpression(x, cp_model.Domain(0, 10))), + ) + e1 = 2 * cp_model.LinearExpr.sum([x, y]) + flat_e1 = cmh.FlatIntExpr(e1) + self.assertEqual(str(e1), "(2 * (x + y))") + self.assertEqual(flat_e1.vars, [x, y]) + self.assertEqual(flat_e1.coeffs, [2, 2]) + self.assertEqual(flat_e1.offset, 0) + repeat_flat_e1 = cmh.FlatIntExpr(flat_e1 + 3) + self.assertEqual(repeat_flat_e1.vars, [x, y]) + self.assertEqual(repeat_flat_e1.coeffs, [2, 2]) + self.assertEqual(repeat_flat_e1.offset, 3) + float_flat_e1 = cmh.FlatFloatExpr(flat_e1) + self.assertEqual(float_flat_e1.vars, [x, y]) + self.assertEqual(float_flat_e1.coeffs, [2.0, 2.0]) + self.assertEqual(float_flat_e1.offset, 0.0) + repeat_float_flat_e1 = cmh.FlatFloatExpr(float_flat_e1 - 2.5) + self.assertEqual(repeat_float_flat_e1.vars, [x, y]) + self.assertEqual(repeat_float_flat_e1.coeffs, [2.0, 2.0]) + self.assertEqual(repeat_float_flat_e1.offset, -2.5) + + b = model.new_bool_var("b") + self.assertEqual( + str(cp_model.LinearExpr.term(b.negated(), 3)), "(3 * not(b))" + ) + + i = model.new_interval_var(x, 2, y, "i") + self.assertEqual(str(i), "i") + + def test_repr(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 4, "x") + y = model.new_int_var(0, 3, "y") + z = model.new_int_var(0, 3, "z") + self.assertEqual(repr(x), "x(0..4)") + self.assertEqual(repr(x + 0), "x(0..4)") + self.assertEqual(repr(x + 0.0), "x(0..4)") + self.assertEqual(repr(x - 0), "x(0..4)") + self.assertEqual(repr(x - 0.0), "x(0..4)") + self.assertEqual(repr(x * 1), "x(0..4)") + self.assertEqual(repr(x * 1.0), "x(0..4)") + self.assertEqual(repr(x * 0), "IntConstant(0)") + self.assertEqual(repr(x * 0.0), "IntConstant(0)") + self.assertEqual(repr(x * 2), "IntAffine(expr=x(0..4), coeff=2, offset=0)") + self.assertEqual( + repr(x + 1.5), "FloatAffine(expr=x(0..4), coeff=1, offset=1.5)" + ) + self.assertEqual(repr(x + y), "SumArray(x(0..4), y(0..3))") + self.assertEqual( + repr(cp_model.LinearExpr.sum([x, y, z])), + "SumArray(x(0..4), y(0..3), z(0..3))", + ) + self.assertEqual( + repr(cp_model.LinearExpr.weighted_sum([x, y, 2], [1, 2, 3])), + "IntWeightedSum([x(0..4), y(0..3)], [1, 2], 6)", + ) + i = model.new_interval_var(x, 2, y, "i") + self.assertEqual(repr(i), "i(start = x, size = 2, end = y)") + b = model.new_bool_var("b") + self.assertEqual(repr(b), "b(0..1)") + self.assertEqual(repr(~b), "NotBooleanVariable(index=3)") + x1 = model.new_int_var(0, 4, "x1") + y1 = model.new_int_var(0, 3, "y1") + j = model.new_optional_interval_var(x1, 2, y1, b, "j") + self.assertEqual( + repr(j), "j(start = x1, size = 2, end = y1, is_present = b)" + ) + x2 = model.new_int_var(0, 4, "x2") + y2 = model.new_int_var(0, 3, "y2") + k = model.new_optional_interval_var(x2, 2, y2, b.negated(), "k") + self.assertEqual( + repr(k), "k(start = x2, size = 2, end = y2, is_present = not(b))" + ) + + def testDisplayBounds(self) -> None: + self.assertEqual("10..20", cp_model.display_bounds([10, 20])) + self.assertEqual("10", cp_model.display_bounds([10, 10])) + self.assertEqual( + "10..15, 20..30", cp_model.display_bounds([10, 15, 20, 30]) + ) + + def test_short_name(self) -> None: + model = cp_model.CpModel() + model.proto.variables.add(domain=[5, 10]) + self.assertEqual("[5..10]", cp_model.short_name(model.proto, 0)) + + def test_integer_expression_errors(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 1, "x") + y = model.new_int_var(0, 3, "y") + self.assertRaises(TypeError, x.__mul__, y) + self.assertRaises(NotImplementedError, x.__div__, y) + self.assertRaises(NotImplementedError, x.__truediv__, y) + self.assertRaises(NotImplementedError, x.__mod__, y) + self.assertRaises(NotImplementedError, x.__pow__, y) + self.assertRaises(NotImplementedError, x.__lshift__, y) + self.assertRaises(NotImplementedError, x.__rshift__, y) + self.assertRaises(NotImplementedError, x.__and__, y) + self.assertRaises(NotImplementedError, x.__or__, y) + self.assertRaises(NotImplementedError, x.__xor__, y) + self.assertRaises(ArithmeticError, x.__lt__, cp_model.INT_MIN) + self.assertRaises(ArithmeticError, x.__gt__, cp_model.INT_MAX) + self.assertRaises(TypeError, x.__add__, "dummy") + self.assertRaises(TypeError, x.__mul__, "dummy") + + def test_model_errors(self) -> None: + model = cp_model.CpModel() + self.assertRaises(TypeError, model.add, "dummy") + self.assertRaises(TypeError, model.get_or_make_index, "dummy") + self.assertRaises(TypeError, model.minimize, "dummy") + + def test_solver_errors(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 1, "x") + y = model.new_int_var(-10, 10, "y") + model.add_linear_constraint(x + 2 * y, 0, 10) + model.minimize(y) + solver = cp_model.CpSolver() + self.assertRaises(RuntimeError, solver.value, x) + solver.solve(model) + self.assertRaises(TypeError, solver.value, "not_a_variable") + self.assertRaises(TypeError, model.add_bool_or, [x, y]) + + def test_has_objective_minimize(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 1, "x") + y = model.new_int_var(-10, 10, "y") + model.add_linear_constraint(x + 2 * y, 0, 10) + self.assertFalse(model.has_objective()) + model.minimize(y) + self.assertTrue(model.has_objective()) + + def test_has_objective_maximize(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 1, "x") + y = model.new_int_var(-10, 10, "y") + model.add_linear_constraint(x + 2 * y, 0, 10) + self.assertFalse(model.has_objective()) + model.maximize(y) + self.assertTrue(model.has_objective()) + + def test_search_for_all_solutions(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + y = model.new_int_var(0, 5, "y") + model.add_linear_constraint(x + y, 6, 6) + + solver = cp_model.CpSolver() + solver.parameters.enumerate_all_solutions = True + solution_counter = SolutionCounter() + status = solver.solve(model, solution_counter) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(5, solution_counter.solution_count) + + def test_solve_with_solution_callback(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + y = model.new_int_var(0, 5, "y") + model.add_linear_constraint(x + y, 6, 6) + + solver = cp_model.CpSolver() + solution_sum = SolutionSum([x, y]) + self.assertRaises(RuntimeError, solution_sum.value, x) + status = solver.solve(model, solution_sum) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(6, solution_sum.sum) + + def test_solve_with_float_value_in_callback(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + y = model.new_int_var(0, 5, "y") + model.add_linear_constraint(x + y, 6, 6) + + solver = cp_model.CpSolver() + solution_float_value = SolutionFloatValue((x + y) * 0.5) + status = solver.solve(model, solution_float_value) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(3.0, solution_float_value.value) + + def test_best_bound_callback(self) -> None: + model = cp_model.CpModel() + x0 = model.new_bool_var("x0") + x1 = model.new_bool_var("x1") + x2 = model.new_bool_var("x2") + x3 = model.new_bool_var("x3") + model.add_bool_or(x0, x1, x2, x3) + model.minimize(3 * x0 + 2 * x1 + 4 * x2 + 5 * x3 + 0.6) + + solver = cp_model.CpSolver() + best_bound_callback = BestBoundCallback() + solver.best_bound_callback = best_bound_callback.new_best_bound + solver.parameters.num_workers = 1 + solver.parameters.linearization_level = 2 + status = solver.solve(model) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(2.6, best_bound_callback.best_bound) + + def test_value(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(0, 10, "y") + model.add(x + 2 * y == 29) + solver = cp_model.CpSolver() + status = solver.solve(model) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(solver.value(x), 9) + self.assertEqual(solver.value(y), 10) + self.assertEqual(solver.value(2), 2) + + def test_float_value(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(0, 10, "y") + model.add(x + 2 * y == 29) + solver = cp_model.CpSolver() + status = solver.solve(model) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(solver.float_value(x * 1.5 + 0.25), 13.75) + self.assertEqual(solver.float_value(2.25), 2.25) + + def test_boolean_value(self) -> None: + model = cp_model.CpModel() + x = model.new_bool_var("x") + y = model.new_bool_var("y") + z = model.new_bool_var("z") + model.add_bool_or([x, z.negated()]) + model.add_bool_or([x, z]) + model.add_bool_or([x.negated(), y.negated()]) + solver = cp_model.CpSolver() + status = solver.solve(model) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(solver.boolean_value(x), True) + self.assertEqual(solver.value(x), 1 - solver.value(x.negated())) + self.assertEqual(solver.value(y), 1 - solver.value(y.negated())) + self.assertEqual(solver.value(z), 1 - solver.value(z.negated())) + self.assertEqual(solver.boolean_value(y), False) + self.assertEqual(solver.boolean_value(True), True) + self.assertEqual(solver.boolean_value(False), False) + self.assertEqual(solver.boolean_value(2), True) + self.assertEqual(solver.boolean_value(0), False) + + def test_unsupported_operators(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(0, 10, "y") + z = model.new_int_var(0, 10, "z") + + with self.assertRaises(NotImplementedError): + model.add(x == min(y, z)) + with self.assertRaises(NotImplementedError): + if x > y: + print("passed1") + with self.assertRaises(NotImplementedError): + if x == 2: + print("passed2") + + def test_is_literal_true_false(self) -> None: + model = cp_model.CpModel() + x = model.new_constant(0) + self.assertFalse(cp_model.object_is_a_true_literal(x)) + self.assertTrue(cp_model.object_is_a_false_literal(x)) + self.assertTrue(cp_model.object_is_a_true_literal(x.negated())) + self.assertFalse(cp_model.object_is_a_false_literal(x.negated())) + self.assertTrue(cp_model.object_is_a_true_literal(True)) + self.assertTrue(cp_model.object_is_a_false_literal(False)) + self.assertFalse(cp_model.object_is_a_true_literal(False)) + self.assertFalse(cp_model.object_is_a_false_literal(True)) + + def test_solve_minimize_with_solution_callback(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + y = model.new_int_var(0, 5, "y") + model.add_linear_constraint(x + y, 6, 6) + model.maximize(x + 2 * y) + + solver = cp_model.CpSolver() + solution_obj = SolutionObjective() + status = solver.solve(model, solution_obj) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(11, solution_obj.obj) + + def test_solution_value(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + b = model.new_bool_var("b") + model.add_decision_strategy( + [x], cp_model.CHOOSE_MIN_DOMAIN_SIZE, cp_model.SELECT_MAX_VALUE + ) + model.add_decision_strategy( + [b], cp_model.CHOOSE_MIN_DOMAIN_SIZE, cp_model.SELECT_MIN_VALUE + ) + solver = cp_model.CpSolver() + solver.parameters.keep_all_feasible_solutions_in_presolve = True + solver.parameters.num_workers = 1 + solution_recorder = RecordSolution([3, x, 1 - x], [1, False, ~b]) + status = solver.solve(model, solution_recorder) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual([3, 5, -4], solution_recorder.int_var_values) + self.assertEqual([True, False, True], solution_recorder.bool_var_values) + + def test_solution_hinting(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + y = model.new_int_var(0, 5, "y") + model.add_linear_constraint(x + y, 6, 6) + model.add_hint(x, 2) + model.add_hint(y, 4) + solver = cp_model.CpSolver() + solver.parameters.cp_model_presolve = False + status = solver.solve(model) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(2, solver.value(x)) + self.assertEqual(4, solver.value(y)) + + def test_solution_hinting_with_booleans(self) -> None: + model = cp_model.CpModel() + x = model.new_bool_var("x") + y = model.new_bool_var("y") + model.add_linear_constraint(x + y, 1, 1) + model.add_hint(x, True) + model.add_hint(~y, True) + solver = cp_model.CpSolver() + solver.parameters.cp_model_presolve = False + status = solver.solve(model) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertTrue(solver.boolean_value(x)) + self.assertFalse(solver.boolean_value(y)) + + def test_stats(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + y = model.new_int_var(0, 5, "y") + model.add_linear_constraint(x + y, 4, 6) + model.add_linear_constraint(2 * x + y, 0, 10) + model.maximize(x + 2 * y) + + solver = cp_model.CpSolver() + status = solver.solve(model) + self.assertEqual(cp_model.OPTIMAL, status) + self.assertEqual(solver.num_booleans, 0) + self.assertEqual(solver.num_conflicts, 0) + self.assertEqual(solver.num_branches, 0) + self.assertGreater(solver.wall_time, 0.0) + + def test_search_strategy(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + y = model.new_int_var(0, 5, "y") + z = model.new_bool_var("z") + model.add_decision_strategy( + [y, x, z.negated()], + cp_model.CHOOSE_MIN_DOMAIN_SIZE, + cp_model.SELECT_MAX_VALUE, + ) + self.assertLen(model.proto.search_strategy, 1) + strategy = model.proto.search_strategy[0] + self.assertLen(strategy.exprs, 3) + self.assertEqual(y.index, strategy.exprs[0].vars[0]) + self.assertEqual(1, strategy.exprs[0].coeffs[0]) + self.assertEqual(x.index, strategy.exprs[1].vars[0]) + self.assertEqual(1, strategy.exprs[1].coeffs[0]) + self.assertEqual(z.index, strategy.exprs[2].vars[0]) + self.assertEqual(-1, strategy.exprs[2].coeffs[0]) + self.assertEqual(1, strategy.exprs[2].offset) + self.assertEqual( + cp_model.CHOOSE_MIN_DOMAIN_SIZE, strategy.variable_selection_strategy + ) + self.assertEqual( + cp_model.SELECT_MAX_VALUE, strategy.domain_reduction_strategy + ) + + def test_model_and_response_stats(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + y = model.new_int_var(0, 5, "y") + model.add_linear_constraint(x + y, 6, 6) + model.maximize(x + 2 * y) + self.assertTrue(model.model_stats()) + + solver = cp_model.CpSolver() + solver.solve(model) + self.assertTrue(solver.response_stats()) + + def test_validate_model(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 5, "x") + y = model.new_int_var(0, 5, "y") + model.add_linear_constraint(x + y, 6, 6) + model.maximize(x + 2 * y) + self.assertFalse(model.validate()) + + def test_validate_model_with_overflow(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, cp_model.INT_MAX, "x") + y = model.new_int_var(0, 10, "y") + model.add_linear_constraint(x + y, 6, cp_model.INT_MAX) + model.maximize(x + 2 * y) + self.assertTrue(model.validate()) + + def test_copy_model(self) -> None: + model = cp_model.CpModel() + b = model.new_bool_var("b") + x = model.new_int_var(0, 4, "x") + y = model.new_int_var(0, 3, "y") + i = model.new_optional_interval_var(x, 12, y, b, "i") + lin = model.add(x + y <= 10) + + new_model = model.clone() + clone_b = new_model.get_bool_var_from_proto_index(b.index) + clone_x = new_model.get_int_var_from_proto_index(x.index) + clone_y = new_model.get_int_var_from_proto_index(y.index) + clone_i = new_model.get_interval_var_from_proto_index(i.index) + + self.assertEqual(b.index, clone_b.index) + self.assertEqual(x.index, clone_x.index) + self.assertEqual(y.index, clone_y.index) + self.assertEqual(i.index, clone_i.index) + + solo_copy_b = copy.copy(b) + self.assertEqual(b.index, solo_copy_b.index) + self.assertEqual(b.is_boolean, solo_copy_b.is_boolean) + self.assertIs(solo_copy_b.model_proto, b.model_proto) + solo_copy_x = copy.copy(x) + self.assertEqual(x.index, solo_copy_x.index) + self.assertEqual(x.is_boolean, solo_copy_x.is_boolean) + self.assertIs(solo_copy_x.model_proto, x.model_proto) + solo_copy_i = copy.copy(i) + self.assertEqual(i.index, solo_copy_i.index) + self.assertIs(solo_copy_i.model_proto, i.model_proto) + + model_copy = copy.copy(model) + copy_b = model_copy.get_bool_var_from_proto_index(b.index) + copy_x = model_copy.get_int_var_from_proto_index(x.index) + copy_y = model_copy.get_int_var_from_proto_index(y.index) + copy_i = model_copy.get_interval_var_from_proto_index(i.index) + + self.assertEqual(b.index, copy_b.index) + self.assertEqual(x.index, copy_x.index) + self.assertEqual(y.index, copy_y.index) + self.assertEqual(i.index, copy_i.index) + self.assertEqual(b.is_boolean, copy_b.is_boolean) + self.assertEqual(x.is_boolean, copy_x.is_boolean) + self.assertEqual(y.is_boolean, copy_y.is_boolean) + self.assertIs(copy_b.model_proto, b.model_proto) + self.assertIs(copy_x.model_proto, x.model_proto) + self.assertIs(copy_i.model_proto, i.model_proto) + + model_deepcopy = copy.deepcopy(model) + deepcopy_b = model_deepcopy.get_bool_var_from_proto_index(b.index) + deepcopy_x = model_deepcopy.get_int_var_from_proto_index(x.index) + deepcopy_y = model_deepcopy.get_int_var_from_proto_index(y.index) + deepcopy_i = model_deepcopy.get_interval_var_from_proto_index(i.index) + + self.assertEqual(b.index, deepcopy_b.index) + self.assertEqual(x.index, deepcopy_x.index) + self.assertEqual(y.index, deepcopy_y.index) + self.assertEqual(i.index, deepcopy_i.index) + self.assertEqual(b.is_boolean, deepcopy_b.is_boolean) + self.assertEqual(x.is_boolean, deepcopy_x.is_boolean) + self.assertEqual(y.is_boolean, deepcopy_y.is_boolean) + self.assertIsNot(deepcopy_b.model_proto, b.model_proto) + self.assertIsNot(deepcopy_x.model_proto, x.model_proto) + self.assertIsNot(deepcopy_y.model_proto, y.model_proto) + self.assertIsNot(deepcopy_i.model_proto, i.model_proto) + self.assertIs(deepcopy_b.model_proto, deepcopy_x.model_proto) + self.assertIs(deepcopy_b.model_proto, deepcopy_y.model_proto) + self.assertIs(deepcopy_b.model_proto, deepcopy_i.model_proto) + + with self.assertRaises(ValueError): + new_model.get_bool_var_from_proto_index(-1) + + with self.assertRaises(ValueError): + new_model.get_int_var_from_proto_index(-1) + + with self.assertRaises(ValueError): + new_model.get_interval_var_from_proto_index(-1) + + with self.assertRaises(ValueError): + new_model.get_bool_var_from_proto_index(x.index) + + with self.assertRaises(ValueError): + new_model.get_interval_var_from_proto_index(lin.index) + + interval_ct = new_model.proto.constraints[copy_i.index].interval + self.assertEqual(12, interval_ct.size.offset) + + class Composite: + + def __init__(self, model: cp_model.CpModel, var: cp_model.IntVar): + self.model = model + self.var = var + + c = Composite(model, x) + copy_c = copy.copy(c) + self.assertIs(copy_c.model, c.model) + self.assertIs(copy_c.var, c.var) + + deepcopy_c = copy.deepcopy(c) + self.assertIsNot(deepcopy_c.model, c.model) + self.assertIsNot(deepcopy_c.var, c.var) + self.assertIs(deepcopy_c.model.proto, deepcopy_c.var.model_proto) + self.assertIs( + deepcopy_c.var, + deepcopy_c.model.get_int_var_from_proto_index(x.index), + ) + + def test_custom_log(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + y = model.new_int_var(-10, 10, "y") + model.add_linear_constraint(x + 2 * y, 0, 10) + model.minimize(y) + solver = cp_model.CpSolver() + solver.parameters.log_search_progress = True + solver.parameters.log_to_stdout = False + log_callback = LogToString() + solver.log_callback = log_callback.new_message + + self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) + self.assertEqual(10, solver.value(x)) + self.assertEqual(-5, solver.value(y)) + + self.assertRegex(log_callback.log, ".*log_to_stdout.*") + + def test_issue2762(self) -> None: + model = cp_model.CpModel() + + x = [model.new_bool_var("a"), model.new_bool_var("b")] + with self.assertRaises(NotImplementedError): + model.add((x[0] != 0) or (x[1] != 0)) + + def test_model_error(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, -2, f"x{i}") for i in range(100)] + model.add(sum(x) <= 1) + solver = cp_model.CpSolver() + solver.parameters.log_search_progress = True + self.assertEqual(cp_model.MODEL_INVALID, solver.solve(model)) + self.assertEqual( + solver.solution_info(), 'var #0 has no domain(): name: "x0"' + ) + + def test_int_var_series(self) -> None: + df = pd.DataFrame([1, -1, 1], columns=["coeffs"]) + model = cp_model.CpModel() + x = model.new_int_var_series( + name="x", index=df.index, lower_bounds=0, upper_bounds=5 + ) + model.minimize(df.coeffs.dot(x)) + solver = cp_model.CpSolver() + self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) + solution = solver.values(x) + self.assertTrue((solution.values == [0, 5, 0]).all()) + self.assertRaises(TypeError, x.apply, lambda x: ~x) + y = model.new_int_var_series( + name="y", index=df.index, lower_bounds=-1, upper_bounds=1 + ) + self.assertRaises(TypeError, y.apply, lambda x: ~x) + z = model.new_int_var_series( + name="y", index=df.index, lower_bounds=0, upper_bounds=1 + ) + _ = z.apply(lambda x: ~x) + + def test_bool_var_series(self) -> None: + df = pd.DataFrame([1, -1, 1], columns=["coeffs"]) + model = cp_model.CpModel() + x = model.new_bool_var_series(name="x", index=df.index) + _ = x.apply(lambda x: ~x) + y = model.new_int_var_series( + name="y", index=df.index, lower_bounds=0, upper_bounds=1 + ) + _ = y.apply(lambda x: ~x) + model.minimize(df.coeffs.dot(x)) + solver = cp_model.CpSolver() + self.assertEqual(cp_model.OPTIMAL, solver.solve(model)) + solution = solver.boolean_values(x) + self.assertTrue((solution.values == [False, True, False]).all()) + + def test_fixed_size_interval_var_series(self) -> None: + df = pd.DataFrame([2, 4, 6], columns=["size"]) + model = cp_model.CpModel() + starts = model.new_int_var_series( + name="starts", index=df.index, lower_bounds=0, upper_bounds=5 + ) + presences = model.new_bool_var_series(name="rresences", index=df.index) + fixed_size_intervals = model.new_fixed_size_interval_var_series( + name="fixed_size_intervals", + index=df.index, + starts=starts, + sizes=df.size, + ) + opt_fixed_size_intervals = ( + model.new_optional_fixed_size_interval_var_series( name="fixed_size_intervals", index=df.index, starts=starts, sizes=df.size, are_present=presences, ) - model.add_no_overlap( - fixed_size_intervals.to_list() + opt_fixed_size_intervals.to_list() - ) - self.assertLen(model.proto.constraints, 7) - - def test_interval_var_series(self) -> None: - df = pd.DataFrame([2, 4, 6], columns=["size"]) - model = cp_model.CpModel() - starts = model.new_int_var_series( - name="starts", index=df.index, lower_bounds=0, upper_bounds=5 - ) - sizes = model.new_int_var_series( - name="sizes", index=df.index, lower_bounds=2, upper_bounds=4 - ) - ends = model.new_int_var_series( - name="ends", index=df.index, lower_bounds=0, upper_bounds=10 - ) - presences = model.new_bool_var_series(name="presences", index=df.index) - intervals = model.new_interval_var_series( - name="fixed_size_intervals", - index=df.index, - starts=starts, - sizes=sizes, - ends=ends, - ) - fixed_intervals = model.new_fixed_size_interval_var_series( - name="fixed_size_intervals", - index=df.index, - starts=starts, - sizes=3, - ) - opt_intervals = model.new_optional_interval_var_series( - name="fixed_size_intervals", - index=df.index, - starts=starts, - sizes=sizes, - ends=ends, - are_present=presences, - ) - absent_fixed_intervals = model.new_optional_fixed_size_interval_var_series( - name="fixed_size_intervals", - index=df.index, - starts=starts, - sizes=3, - are_present=False, - ) - model.add_no_overlap( - intervals.to_list() - + opt_intervals.to_list() - + fixed_intervals.to_list() - + absent_fixed_intervals.to_list() - ) - self.assertLen(model.proto.constraints, 13) - - def test_compare_with_none(self) -> None: - model = cp_model.CpModel() - x = model.new_int_var(0, 10, "x") - self.assertRaises(TypeError, x.__eq__, None) - self.assertRaises(TypeError, x.__ne__, None) - self.assertRaises(TypeError, x.__lt__, None) - self.assertRaises(TypeError, x.__le__, None) - self.assertRaises(TypeError, x.__gt__, None) - self.assertRaises(TypeError, x.__ge__, None) - - def test_small_series(self) -> None: - # OR-Tools issue #4525. - model = cp_model.CpModel() - x = model.new_bool_var("foo") - y = model.new_bool_var("bar") - z = model.new_bool_var("baz") - - s1 = pd.Series([x, y], index=[1, 2]) - self.assertEqual(str(s1.sum()), "(foo + bar)") - s2 = pd.Series([1, -1], index=[1, 2]) - self.assertEqual(str(s1.dot(s2)), "(foo + (-bar))") - - s3 = pd.Series([x], index=[1]) - self.assertIs(s3.sum(), x) - s4 = pd.Series([1], index=[1]) - self.assertIs(s3.dot(s4), x) - - s5 = pd.Series([x, y, z], index=[1, 2, 3]) - self.assertEqual(str(s5.sum()), "(foo + bar + baz)") - s6 = pd.Series([1, -2, 1], index=[1, 2, 3]) - self.assertEqual(str(s5.dot(s6)), "(foo + (-2 * bar) + baz)") - - def test_issue4376_sat_model(self) -> None: - letters: str = "BCFLMRT" - - def symbols_from_string(text: str) -> list[int]: - return [letters.index(char) for char in text] - - def rotate_symbols(symbols: list[int], turns: int) -> list[int]: - return symbols[turns:] + symbols[:turns] - - data = """FMRC + ) + model.add_no_overlap( + fixed_size_intervals.to_list() + opt_fixed_size_intervals.to_list() + ) + self.assertLen(model.proto.constraints, 7) + + def test_interval_var_series(self) -> None: + df = pd.DataFrame([2, 4, 6], columns=["size"]) + model = cp_model.CpModel() + starts = model.new_int_var_series( + name="starts", index=df.index, lower_bounds=0, upper_bounds=5 + ) + sizes = model.new_int_var_series( + name="sizes", index=df.index, lower_bounds=2, upper_bounds=4 + ) + ends = model.new_int_var_series( + name="ends", index=df.index, lower_bounds=0, upper_bounds=10 + ) + presences = model.new_bool_var_series(name="presences", index=df.index) + intervals = model.new_interval_var_series( + name="fixed_size_intervals", + index=df.index, + starts=starts, + sizes=sizes, + ends=ends, + ) + fixed_intervals = model.new_fixed_size_interval_var_series( + name="fixed_size_intervals", + index=df.index, + starts=starts, + sizes=3, + ) + opt_intervals = model.new_optional_interval_var_series( + name="fixed_size_intervals", + index=df.index, + starts=starts, + sizes=sizes, + ends=ends, + are_present=presences, + ) + absent_fixed_intervals = model.new_optional_fixed_size_interval_var_series( + name="fixed_size_intervals", + index=df.index, + starts=starts, + sizes=3, + are_present=False, + ) + model.add_no_overlap( + intervals.to_list() + + opt_intervals.to_list() + + fixed_intervals.to_list() + + absent_fixed_intervals.to_list() + ) + self.assertLen(model.proto.constraints, 13) + + def test_compare_with_none(self) -> None: + model = cp_model.CpModel() + x = model.new_int_var(0, 10, "x") + self.assertRaises(TypeError, x.__eq__, None) + self.assertRaises(TypeError, x.__ne__, None) + self.assertRaises(TypeError, x.__lt__, None) + self.assertRaises(TypeError, x.__le__, None) + self.assertRaises(TypeError, x.__gt__, None) + self.assertRaises(TypeError, x.__ge__, None) + + def test_small_series(self) -> None: + # OR-Tools issue #4525. + model = cp_model.CpModel() + x = model.new_bool_var("foo") + y = model.new_bool_var("bar") + z = model.new_bool_var("baz") + + s1 = pd.Series([x, y], index=[1, 2]) + self.assertEqual(str(s1.sum()), "(foo + bar)") + s2 = pd.Series([1, -1], index=[1, 2]) + self.assertEqual(str(s1.dot(s2)), "(foo + (-bar))") + + s3 = pd.Series([x], index=[1]) + self.assertIs(s3.sum(), x) + s4 = pd.Series([1], index=[1]) + self.assertIs(s3.dot(s4), x) + + s5 = pd.Series([x, y, z], index=[1, 2, 3]) + self.assertEqual(str(s5.sum()), "(foo + bar + baz)") + s6 = pd.Series([1, -2, 1], index=[1, 2, 3]) + self.assertEqual(str(s5.dot(s6)), "(foo + (-2 * bar) + baz)") + + def test_issue4376_sat_model(self) -> None: + letters: str = "BCFLMRT" + + def symbols_from_string(text: str) -> list[int]: + return [letters.index(char) for char in text] + + def rotate_symbols(symbols: list[int], turns: int) -> list[int]: + return symbols[turns:] + symbols[:turns] + + data = """FMRC FTLB MCBR FRTM @@ -2117,407 +2170,427 @@ def rotate_symbols(symbols: list[int], turns: int) -> list[int]: RBFM TRFM""" - tiles = [symbols_from_string(line) for line in data.splitlines()] - - model = cp_model.CpModel() - - # choices[i, x, y, r] is true iff we put tile i in cell (x,y) with - # rotation r. - choices = {} - for i in range(len(tiles)): - for x in range(6): - for y in range(6): - for r in range(4): - choices[(i, x, y, r)] = model.new_bool_var( - f"tile_{i}_{x}_{y}_{r}" - ) - - # corners[x, y, s] is true iff the corner at (x,y) contains symbol s. - corners = {} - for x in range(7): - for y in range(7): - for s in range(7): - corners[(x, y, s)] = model.new_bool_var(f"corner_{x}_{y}_{s}") - - # Placing a tile puts a symbol in each corner. - for (i, x, y, r), choice in choices.items(): - symbols = rotate_symbols(tiles[i], r) - model.add_implication(choice, corners[x, y, symbols[0]]) - model.add_implication(choice, corners[x, y + 1, symbols[1]]) - model.add_implication(choice, corners[x + 1, y + 1, symbols[2]]) - model.add_implication(choice, corners[x + 1, y, symbols[3]]) - - # We must make exactly one choice for each tile. - for i in range(len(tiles)): - tmp_literals = [] - for x in range(6): - for y in range(6): - for r in range(4): - tmp_literals.append(choices[(i, x, y, r)]) - model.add_exactly_one(tmp_literals) - - # We must make exactly one choice for each square. - for x, y in itertools.product(range(6), range(6)): - tmp_literals = [] - for i in range(len(tiles)): - for r in range(4): - tmp_literals.append(choices[(i, x, y, r)]) - model.add_exactly_one(tmp_literals) - - # Each corner contains exactly one symbol. - for x, y in itertools.product(range(7), range(7)): - model.add_exactly_one(corners[x, y, s] for s in range(7)) - - # Solve. - solver = cp_model.CpSolver() - solver.parameters.num_workers = 8 - solver.parameters.max_time_in_seconds = 20 - solver.parameters.log_search_progress = True - solver.parameters.cp_model_presolve = False - solver.parameters.symmetry_level = 0 - - solution_callback = TimeRecorder() - status = solver.Solve(model, solution_callback) - if status == cp_model.OPTIMAL: - self.assertLess(time.time(), solution_callback.last_time + 5.0) - - def test_issue4376_minimize_model(self) -> None: - model = cp_model.CpModel() - - jobs = [ - [3, 3], # [duration, width] - [2, 5], - [1, 3], - [3, 7], - [7, 3], - [2, 2], - [2, 2], - [5, 5], - [10, 2], - [4, 3], - [2, 6], - [1, 2], - [6, 8], - [4, 5], - [3, 7], - ] - - max_width = 10 - - horizon = sum(t[0] for t in jobs) - - intervals = [] - intervals0 = [] - intervals1 = [] - performed = [] - starts = [] - ends = [] - demands = [] - - for i, job in enumerate(jobs): - # Create main interval. - start = model.new_int_var(0, horizon, f"start_{i}") - duration, width = job - end = model.new_int_var(0, horizon, f"end_{i}") - interval = model.new_interval_var(start, duration, end, f"interval_{i}") - starts.append(start) - intervals.append(interval) - ends.append(end) - demands.append(width) - - # Create an optional copy of interval to be executed on machine 0. - performed_on_m0 = model.new_bool_var(f"perform_{i}_on_m0") - performed.append(performed_on_m0) - start0 = model.new_int_var(0, horizon, f"start_{i}_on_m0") - end0 = model.new_int_var(0, horizon, f"end_{i}_on_m0") - interval0 = model.new_optional_interval_var( - start0, duration, end0, performed_on_m0, f"interval_{i}_on_m0" - ) - intervals0.append(interval0) - - # Create an optional copy of interval to be executed on machine 1. - start1 = model.new_int_var(0, horizon, f"start_{i}_on_m1") - end1 = model.new_int_var(0, horizon, f"end_{i}_on_m1") - interval1 = model.new_optional_interval_var( - start1, - duration, - end1, - ~performed_on_m0, - f"interval_{i}_on_m1", - ) - intervals1.append(interval1) - - # We only propagate the constraint if the tasks is performed on the - # machine. - model.add(start0 == start).only_enforce_if(performed_on_m0) - model.add(start1 == start).only_enforce_if(~performed_on_m0) - - # Width constraint (modeled as a cumulative) - model.add_cumulative(intervals, demands, max_width) - - # Choose which machine to perform the jobs on. - model.add_no_overlap(intervals0) - model.add_no_overlap(intervals1) - - # Objective variable. - makespan = model.new_int_var(0, horizon, "makespan") - model.add_max_equality(makespan, ends) - model.minimize(makespan) - - # Symmetry breaking. - model.add(performed[0] == 0) - - # Solve. - solver = cp_model.CpSolver() - solver.parameters.num_workers = 8 - solver.parameters.max_time_in_seconds = 50 - solver.parameters.log_search_progress = True - solution_callback = TimeRecorder() - best_bound_callback = BestBoundTimeCallback() - solver.best_bound_callback = best_bound_callback.new_best_bound - status = solver.Solve(model, solution_callback) - if status == cp_model.OPTIMAL: - self.assertLess( - time.time(), - max(best_bound_callback.last_time, solution_callback.last_time) + 9.0, - ) - - def test_issue4434(self) -> None: - model = cp_model.CpModel() - i = model.NewIntVar(0, 10, "i") - j = model.NewIntVar(0, 10, "j") - - # Causes a mypy error: Argument has incompatible type - # "BoundedLinearExpression | bool"; expected "BoundedLinearExpression" - expr_eq: cp_model.BoundedLinearExpression = i + j == 5 - expr_ne: cp_model.BoundedLinearExpression = i + j != 5 - - # This works fine with other comparison operators - expr_ge: cp_model.BoundedLinearExpression = i + j >= 5 - - self.assertIsNotNone(expr_eq) - self.assertIsNotNone(expr_ne) - self.assertIsNotNone(expr_ge) - - def test_raise_python_exception_in_callback(self) -> None: - model = cp_model.CpModel() - - jobs = [ - [3, 3], # [duration, width] - [2, 5], - [1, 3], - [3, 7], - [7, 3], - [2, 2], - [2, 2], - [5, 5], - [10, 2], - [4, 3], - [2, 6], - [1, 2], - [6, 8], - [4, 5], - [3, 7], - ] - - max_width = 10 - - horizon = sum(t[0] for t in jobs) - - intervals = [] - intervals0 = [] - intervals1 = [] - performed = [] - starts = [] - ends = [] - demands = [] - - for i, job in enumerate(jobs): - # Create main interval. - start = model.new_int_var(0, horizon, f"start_{i}") - duration, width = job - end = model.new_int_var(0, horizon, f"end_{i}") - interval = model.new_interval_var(start, duration, end, f"interval_{i}") - starts.append(start) - intervals.append(interval) - ends.append(end) - demands.append(width) - - # Create an optional copy of interval to be executed on machine 0. - performed_on_m0 = model.new_bool_var(f"perform_{i}_on_m0") - performed.append(performed_on_m0) - start0 = model.new_int_var(0, horizon, f"start_{i}_on_m0") - end0 = model.new_int_var(0, horizon, f"end_{i}_on_m0") - interval0 = model.new_optional_interval_var( - start0, duration, end0, performed_on_m0, f"interval_{i}_on_m0" - ) - intervals0.append(interval0) - - # Create an optional copy of interval to be executed on machine 1. - start1 = model.new_int_var(0, horizon, f"start_{i}_on_m1") - end1 = model.new_int_var(0, horizon, f"end_{i}_on_m1") - interval1 = model.new_optional_interval_var( - start1, - duration, - end1, - ~performed_on_m0, - f"interval_{i}_on_m1", - ) - intervals1.append(interval1) - - # We only propagate the constraint if the tasks is performed on the - # machine. - model.add(start0 == start).only_enforce_if(performed_on_m0) - model.add(start1 == start).only_enforce_if(~performed_on_m0) - - # Width constraint (modeled as a cumulative) - model.add_cumulative(intervals, demands, max_width) - - # Choose which machine to perform the jobs on. - model.add_no_overlap(intervals0) - model.add_no_overlap(intervals1) - - # Objective variable. - makespan = model.new_int_var(0, horizon, "makespan") - model.add_max_equality(makespan, ends) - model.minimize(makespan) - - # Symmetry breaking. - model.add(performed[0] == 0) - - solver = cp_model.CpSolver() - solver.parameters.log_search_progress = True - solver.parameters.num_workers = 1 - msg: str = "this is my test message" - callback = RaiseException(msg) - - with self.assertRaisesRegex(ValueError, msg): - solver.solve(model, callback) - - def test_in_place_sum_modifications(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 10, f"x{i}") for i in range(5)] - y = [model.new_int_var(0, 10, f"y{i}") for i in range(5)] - e1 = sum(x) - self.assertIsInstance(e1, cmh.SumArray) - self.assertEqual(e1.int_offset, 0) - self.assertEqual(e1.double_offset, 0) - self.assertEqual(e1.num_exprs, 5) - e1_str = str(e1) - _ = e1 + y[0] - _ = sum(y) + e1 - self.assertEqual(e1_str, str(e1)) - - e2 = sum(x) - 2 - y[0] - 0.1 - e2_str = str(e2) - self.assertIsInstance(e2, cmh.SumArray) - self.assertEqual(e2.int_offset, -2) - self.assertEqual(e2.double_offset, -0.1) - self.assertEqual(e2.num_exprs, 6) - _ = e2 + 2.5 - self.assertEqual(str(e2), e2_str) - - e3 = 1.2 + sum(x) + 0.3 - self.assertIsInstance(e3, cmh.SumArray) - self.assertEqual(e3.int_offset, 0) - self.assertEqual(e3.double_offset, 1.5) - self.assertEqual(e3.num_exprs, 5) - - def test_large_sum(self) -> None: - model = cp_model.CpModel() - x = [model.new_int_var(0, 10, f"x{i}") for i in range(100000)] - model.add(sum(x) == 10) - - def test_simplification1(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = (x * 2) * 2 - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(4, prod.coefficient) - self.assertEqual(0, prod.offset) - - def test_simplification2(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (x * 2) - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(4, prod.coefficient) - self.assertEqual(0, prod.offset) - - def test_simplification3(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = (2 * x) * 2 - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(4, prod.coefficient) - self.assertEqual(0, prod.offset) - - def test_simplification4(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (2 * x) - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(4, prod.coefficient) - self.assertEqual(0, prod.offset) - - def test_simplification5(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (x + 1) - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(2, prod.coefficient) - self.assertEqual(2, prod.offset) - - def test_simplification6(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = (x + 1) * 2 - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(2, prod.coefficient) - self.assertEqual(2, prod.offset) - - def test_simplification7(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (x - 1) - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(2, prod.coefficient) - self.assertEqual(-2, prod.offset) - - def test_simplification8(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = (x - 1) * 2 - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(2, prod.coefficient) - self.assertEqual(-2, prod.offset) - - def test_simplification9(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = 2 * (1 - x) - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(-2, prod.coefficient) - self.assertEqual(2, prod.offset) - - def test_simplification10(self): - model = cp_model.CpModel() - x = model.new_int_var(-10, 10, "x") - prod = (1 - x) * 2 - self.assertIsInstance(prod, cmh.IntAffine) - self.assertEqual(x, prod.expression) - self.assertEqual(-2, prod.coefficient) - self.assertEqual(2, prod.offset) + tiles = [symbols_from_string(line) for line in data.splitlines()] + + model = cp_model.CpModel() + + # choices[i, x, y, r] is true iff we put tile i in cell (x,y) with + # rotation r. + choices = {} + for i in range(len(tiles)): + for x in range(6): + for y in range(6): + for r in range(4): + choices[(i, x, y, r)] = model.new_bool_var(f"tile_{i}_{x}_{y}_{r}") + + # corners[x, y, s] is true iff the corner at (x,y) contains symbol s. + corners = {} + for x in range(7): + for y in range(7): + for s in range(7): + corners[(x, y, s)] = model.new_bool_var(f"corner_{x}_{y}_{s}") + + # Placing a tile puts a symbol in each corner. + for (i, x, y, r), choice in choices.items(): + symbols = rotate_symbols(tiles[i], r) + model.add_implication(choice, corners[x, y, symbols[0]]) + model.add_implication(choice, corners[x, y + 1, symbols[1]]) + model.add_implication(choice, corners[x + 1, y + 1, symbols[2]]) + model.add_implication(choice, corners[x + 1, y, symbols[3]]) + + # We must make exactly one choice for each tile. + for i in range(len(tiles)): + tmp_literals = [] + for x in range(6): + for y in range(6): + for r in range(4): + tmp_literals.append(choices[(i, x, y, r)]) + model.add_exactly_one(tmp_literals) + + # We must make exactly one choice for each square. + for x, y in itertools.product(range(6), range(6)): + tmp_literals = [] + for i in range(len(tiles)): + for r in range(4): + tmp_literals.append(choices[(i, x, y, r)]) + model.add_exactly_one(tmp_literals) + + # Each corner contains exactly one symbol. + for x, y in itertools.product(range(7), range(7)): + model.add_exactly_one(corners[x, y, s] for s in range(7)) + + # Solve. + solver = cp_model.CpSolver() + solver.parameters.num_workers = 8 + solver.parameters.max_time_in_seconds = 20 + solver.parameters.log_search_progress = True + solver.parameters.cp_model_presolve = False + solver.parameters.symmetry_level = 0 + + solution_callback = TimeRecorder() + status = solver.Solve(model, solution_callback) + if status == cp_model.OPTIMAL: + self.assertLess(time.time(), solution_callback.last_time + 5.0) + + def test_issue4376_minimize_model(self) -> None: + model = cp_model.CpModel() + + jobs = [ + [3, 3], # [duration, width] + [2, 5], + [1, 3], + [3, 7], + [7, 3], + [2, 2], + [2, 2], + [5, 5], + [10, 2], + [4, 3], + [2, 6], + [1, 2], + [6, 8], + [4, 5], + [3, 7], + ] + + max_width = 10 + + horizon = sum(t[0] for t in jobs) + + intervals = [] + intervals0 = [] + intervals1 = [] + performed = [] + starts = [] + ends = [] + demands = [] + + for i, job in enumerate(jobs): + # Create main interval. + start = model.new_int_var(0, horizon, f"start_{i}") + duration, width = job + end = model.new_int_var(0, horizon, f"end_{i}") + interval = model.new_interval_var(start, duration, end, f"interval_{i}") + starts.append(start) + intervals.append(interval) + ends.append(end) + demands.append(width) + + # Create an optional copy of interval to be executed on machine 0. + performed_on_m0 = model.new_bool_var(f"perform_{i}_on_m0") + performed.append(performed_on_m0) + start0 = model.new_int_var(0, horizon, f"start_{i}_on_m0") + end0 = model.new_int_var(0, horizon, f"end_{i}_on_m0") + interval0 = model.new_optional_interval_var( + start0, duration, end0, performed_on_m0, f"interval_{i}_on_m0" + ) + intervals0.append(interval0) + + # Create an optional copy of interval to be executed on machine 1. + start1 = model.new_int_var(0, horizon, f"start_{i}_on_m1") + end1 = model.new_int_var(0, horizon, f"end_{i}_on_m1") + interval1 = model.new_optional_interval_var( + start1, + duration, + end1, + ~performed_on_m0, + f"interval_{i}_on_m1", + ) + intervals1.append(interval1) + + # We only propagate the constraint if the tasks is performed on the + # machine. + model.add(start0 == start).only_enforce_if(performed_on_m0) + model.add(start1 == start).only_enforce_if(~performed_on_m0) + + # Width constraint (modeled as a cumulative) + model.add_cumulative(intervals, demands, max_width) + + # Choose which machine to perform the jobs on. + model.add_no_overlap(intervals0) + model.add_no_overlap(intervals1) + + # Objective variable. + makespan = model.new_int_var(0, horizon, "makespan") + model.add_max_equality(makespan, ends) + model.minimize(makespan) + + # Symmetry breaking. + model.add(performed[0] == 0) + + # Solve. + solver = cp_model.CpSolver() + solver.parameters.num_workers = 8 + solver.parameters.max_time_in_seconds = 50 + solver.parameters.log_search_progress = True + solution_callback = TimeRecorder() + best_bound_callback = BestBoundTimeCallback() + solver.best_bound_callback = best_bound_callback.new_best_bound + status = solver.Solve(model, solution_callback) + if status == cp_model.OPTIMAL: + self.assertLess( + time.time(), + max(best_bound_callback.last_time, solution_callback.last_time) + 9.0, + ) + + def test_issue4434(self) -> None: + model = cp_model.CpModel() + i = model.NewIntVar(0, 10, "i") + j = model.NewIntVar(0, 10, "j") + + # Causes a mypy error: Argument has incompatible type + # "BoundedLinearExpression | bool"; expected "BoundedLinearExpression" + expr_eq: cp_model.BoundedLinearExpression = i + j == 5 + expr_ne: cp_model.BoundedLinearExpression = i + j != 5 + + # This works fine with other comparison operators + expr_ge: cp_model.BoundedLinearExpression = i + j >= 5 + + self.assertIsNotNone(expr_eq) + self.assertIsNotNone(expr_ne) + self.assertIsNotNone(expr_ge) + + def test_raise_python_exception_in_callback(self) -> None: + model = cp_model.CpModel() + + jobs = [ + [3, 3], # [duration, width] + [2, 5], + [1, 3], + [3, 7], + [7, 3], + [2, 2], + [2, 2], + [5, 5], + [10, 2], + [4, 3], + [2, 6], + [1, 2], + [6, 8], + [4, 5], + [3, 7], + ] + + max_width = 10 + + horizon = sum(t[0] for t in jobs) + + intervals = [] + intervals0 = [] + intervals1 = [] + performed = [] + starts = [] + ends = [] + demands = [] + + for i, job in enumerate(jobs): + # Create main interval. + start = model.new_int_var(0, horizon, f"start_{i}") + duration, width = job + end = model.new_int_var(0, horizon, f"end_{i}") + interval = model.new_interval_var(start, duration, end, f"interval_{i}") + starts.append(start) + intervals.append(interval) + ends.append(end) + demands.append(width) + + # Create an optional copy of interval to be executed on machine 0. + performed_on_m0 = model.new_bool_var(f"perform_{i}_on_m0") + performed.append(performed_on_m0) + start0 = model.new_int_var(0, horizon, f"start_{i}_on_m0") + end0 = model.new_int_var(0, horizon, f"end_{i}_on_m0") + interval0 = model.new_optional_interval_var( + start0, duration, end0, performed_on_m0, f"interval_{i}_on_m0" + ) + intervals0.append(interval0) + + # Create an optional copy of interval to be executed on machine 1. + start1 = model.new_int_var(0, horizon, f"start_{i}_on_m1") + end1 = model.new_int_var(0, horizon, f"end_{i}_on_m1") + interval1 = model.new_optional_interval_var( + start1, + duration, + end1, + ~performed_on_m0, + f"interval_{i}_on_m1", + ) + intervals1.append(interval1) + + # We only propagate the constraint if the tasks is performed on the + # machine. + model.add(start0 == start).only_enforce_if(performed_on_m0) + model.add(start1 == start).only_enforce_if(~performed_on_m0) + + # Width constraint (modeled as a cumulative) + model.add_cumulative(intervals, demands, max_width) + + # Choose which machine to perform the jobs on. + model.add_no_overlap(intervals0) + model.add_no_overlap(intervals1) + + # Objective variable. + makespan = model.new_int_var(0, horizon, "makespan") + model.add_max_equality(makespan, ends) + model.minimize(makespan) + + # Symmetry breaking. + model.add(performed[0] == 0) + + solver = cp_model.CpSolver() + solver.parameters.log_search_progress = True + solver.parameters.num_workers = 1 + msg: str = "this is my test message" + callback = RaiseException(msg) + + with self.assertRaisesRegex(ValueError, msg): + solver.solve(model, callback) + + def test_in_place_sum_modifications(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 10, f"x{i}") for i in range(5)] + y = [model.new_int_var(0, 10, f"y{i}") for i in range(5)] + e1 = sum(x) + self.assertIsInstance(e1, cmh.SumArray) + self.assertEqual(e1.int_offset, 0) + self.assertEqual(e1.double_offset, 0) + self.assertEqual(e1.num_exprs, 5) + e1_str = str(e1) + _ = e1 + y[0] + _ = sum(y) + e1 + self.assertEqual(e1_str, str(e1)) + + e2 = sum(x) - 2 - y[0] - 0.1 + e2_str = str(e2) + self.assertIsInstance(e2, cmh.SumArray) + self.assertEqual(e2.int_offset, -2) + self.assertEqual(e2.double_offset, -0.1) + self.assertEqual(e2.num_exprs, 6) + _ = e2 + 2.5 + self.assertEqual(str(e2), e2_str) + + e3 = 1.2 + sum(x) + 0.3 + self.assertIsInstance(e3, cmh.SumArray) + self.assertEqual(e3.int_offset, 0) + self.assertEqual(e3.double_offset, 1.5) + self.assertEqual(e3.num_exprs, 5) + + def test_large_sum(self) -> None: + model = cp_model.CpModel() + x = [model.new_int_var(0, 10, f"x{i}") for i in range(100000)] + model.add(sum(x) == 10) + + def test_large_iadd(self): + model = cp_model.CpModel() + s = 0 + for _ in range(300000): + s += model.new_bool_var("") + model.add(s == 10) + + def test_large_isub(self): + model = cp_model.CpModel() + s = 0 + for _ in range(300000): + s -= model.new_bool_var("") + model.add(s == 10) + + def test_radd(self): + model = cp_model.CpModel() + x = [model.new_int_var(0, 10, f"x{i}") for i in range(10)] + expr = 1 + sum(x) + self.assertEqual( + str(expr), "(x0 + x1 + x2 + x3 + x4 + x5 + x6 + x7 + x8 + x9 + 1)" + ) + + def test_simplification1(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = (x * 2) * 2 + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(4, prod.coefficient) + self.assertEqual(0, prod.offset) + + def test_simplification2(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (x * 2) + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(4, prod.coefficient) + self.assertEqual(0, prod.offset) + + def test_simplification3(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = (2 * x) * 2 + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(4, prod.coefficient) + self.assertEqual(0, prod.offset) + + def test_simplification4(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (2 * x) + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(4, prod.coefficient) + self.assertEqual(0, prod.offset) + + def test_simplification5(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (x + 1) + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(2, prod.coefficient) + self.assertEqual(2, prod.offset) + + def test_simplification6(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = (x + 1) * 2 + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(2, prod.coefficient) + self.assertEqual(2, prod.offset) + + def test_simplification7(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (x - 1) + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(2, prod.coefficient) + self.assertEqual(-2, prod.offset) + + def test_simplification8(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = (x - 1) * 2 + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(2, prod.coefficient) + self.assertEqual(-2, prod.offset) + + def test_simplification9(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = 2 * (1 - x) + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(-2, prod.coefficient) + self.assertEqual(2, prod.offset) + + def test_simplification10(self): + model = cp_model.CpModel() + x = model.new_int_var(-10, 10, "x") + prod = (1 - x) * 2 + self.assertIsInstance(prod, cmh.IntAffine) + self.assertEqual(x, prod.expression) + self.assertEqual(-2, prod.coefficient) + self.assertEqual(2, prod.offset) if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/sat/python/linear_expr.cc b/ortools/sat/python/linear_expr.cc index b158fb93436..f8c2954f62f 100644 --- a/ortools/sat/python/linear_expr.cc +++ b/ortools/sat/python/linear_expr.cc @@ -89,9 +89,8 @@ std::shared_ptr LinearExpr::AddFloat(double cst) { std::shared_ptr LinearExpr::Sub(std::shared_ptr other) { std::vector> exprs; exprs.push_back(shared_from_this()); - exprs.push_back(other); - const std::vector coeffs = {1, -1}; - return std::make_shared(exprs, coeffs, 0); + exprs.push_back(other->MulInt(-1)); + return std::make_shared(exprs); } std::shared_ptr LinearExpr::SubInt(int64_t cst) { @@ -341,8 +340,20 @@ SumArray::SumArray(std::vector> exprs, DCHECK_GE(exprs_.size(), 2); } -void SumArray::AddInPlace(std::shared_ptr expr) { +std::shared_ptr SumArray::AddInPlace( + std::shared_ptr expr) { exprs_.push_back(std::move(expr)); + return shared_from_this(); +} + +std::shared_ptr SumArray::AddIntInPlace(int64_t cst) { + int_offset_ += cst; + return shared_from_this(); +} + +std::shared_ptr SumArray::AddFloatInPlace(double cst) { + double_offset_ += cst; + return shared_from_this(); } bool SumArray::VisitAsInt(IntExprVisitor& lin, int64_t c) { diff --git a/ortools/sat/python/linear_expr.h b/ortools/sat/python/linear_expr.h index 631f17f05f3..06d973f9ea4 100644 --- a/ortools/sat/python/linear_expr.h +++ b/ortools/sat/python/linear_expr.h @@ -103,27 +103,27 @@ class LinearExpr : public std::enable_shared_from_this { /// Returns (this) + (expr). std::shared_ptr Add(std::shared_ptr other); /// Returns (this) + (cst). - std::shared_ptr AddInt(int64_t cst); + virtual std::shared_ptr AddInt(int64_t cst); /// Returns (this) + (cst). std::shared_ptr AddFloat(double cst); /// Returns (this) - (expr). std::shared_ptr Sub(std::shared_ptr other); /// Returns (this) - (cst). - std::shared_ptr SubInt(int64_t cst); + virtual std::shared_ptr SubInt(int64_t cst); /// Returns (this) - (cst). std::shared_ptr SubFloat(double cst); /// Returns (expr) - (this). std::shared_ptr RSub(std::shared_ptr other); /// Returns (cst) - (this). - std::shared_ptr RSubInt(int64_t cst); + virtual std::shared_ptr RSubInt(int64_t cst); /// Returns (cst) - (this). std::shared_ptr RSubFloat(double cst); /// Returns (this) * (cst). - std::shared_ptr MulInt(int64_t cst); + virtual std::shared_ptr MulInt(int64_t cst); /// Returns (this) * (cst). std::shared_ptr MulFloat(double cst); /// Returns -(this). - std::shared_ptr Neg(); + virtual std::shared_ptr Neg(); /// Returns (this) == (rhs). std::shared_ptr Eq(std::shared_ptr rhs); @@ -286,9 +286,9 @@ class SumArray : public LinearExpr { std::string ToString() const override; std::string DebugString() const override; - void AddInPlace(std::shared_ptr expr); - void AddIntInPlace(int64_t cst) { int_offset_ += cst; } - void AddFloatInPlace(double cst) { double_offset_ += cst; } + std::shared_ptr AddInPlace(std::shared_ptr expr); + std::shared_ptr AddIntInPlace(int64_t cst); + std::shared_ptr AddFloatInPlace(double cst); int num_exprs() const { return exprs_.size(); } int64_t int_offset() const { return int_offset_; } double double_offset() const { return double_offset_; } @@ -381,11 +381,11 @@ class IntAffine : public LinearExpr { /// Returns the offset. int64_t offset() const { return offset_; } - std::shared_ptr AddInt(int64_t cst); - std::shared_ptr SubInt(int64_t cst); - std::shared_ptr RSubInt(int64_t cst); - std::shared_ptr MulInt(int64_t cst); - std::shared_ptr Neg(); + std::shared_ptr AddInt(int64_t cst) override; + std::shared_ptr SubInt(int64_t cst) override; + std::shared_ptr RSubInt(int64_t cst) override; + std::shared_ptr MulInt(int64_t cst) override; + std::shared_ptr Neg() override; private: std::shared_ptr expr_; diff --git a/ortools/sat/rins.cc b/ortools/sat/rins.cc index 11a6672285d..fbd01e762f2 100644 --- a/ortools/sat/rins.cc +++ b/ortools/sat/rins.cc @@ -204,14 +204,11 @@ ReducedDomainNeighborhood GetRinsRensNeighborhood( if (relaxation_values.empty()) return reduced_domains; // Not generated. std::bernoulli_distribution three_out_of_four(0.75); - - if (response_manager != nullptr && - response_manager->SolutionsRepository().NumSolutions() > 0 && + if (response_manager != nullptr && response_manager->HasFeasibleSolution() && three_out_of_four(random)) { // Rins. std::shared_ptr::Solution> solution = - response_manager->SolutionsRepository().GetRandomBiasedSolution( - random); + response_manager->SolutionPool().GetSolutionToImprove(random); FillRinsNeighborhood(solution->variable_values, relaxation_values, difficulty, random, reduced_domains); reduced_domains.source_info = "rins_"; diff --git a/ortools/sat/rins_test.cc b/ortools/sat/rins_test.cc index 74ff5995da9..17740b6d105 100644 --- a/ortools/sat/rins_test.cc +++ b/ortools/sat/rins_test.cc @@ -16,6 +16,7 @@ #include #include +#include "absl/strings/match.h" #include "absl/types/span.h" #include "gtest/gtest.h" #include "ortools/base/parse_test_proto.h" @@ -150,19 +151,24 @@ TEST(GetRinsRensNeighborhoodTest, GetRinsRensNeighborhoodLP) { // Add a lp solution. lp_solutions.NewLPSolution({3.5, 5}); lp_solutions.Synchronize(); + // Add a solution. CpSolverResponse solution; solution.add_solution(4); solution.add_solution(5); shared_response_manager->NewSolution(solution.solution(), solution.solution_info(), &model); - shared_response_manager->MutableSolutionsRepository()->Synchronize(); + shared_response_manager->Synchronize(); - const ReducedDomainNeighborhood rins_neighborhood = GetRinsRensNeighborhood( - shared_response_manager, &lp_solutions, &incomplete_solutions, - /*difficulty=*/0.5, random); + ReducedDomainNeighborhood rins_neighborhood; + for (int i = 0; i < 100; ++i) { + rins_neighborhood = GetRinsRensNeighborhood( + shared_response_manager, &lp_solutions, &incomplete_solutions, + /*difficulty=*/0.5, random); + if (absl::StartsWith(rins_neighborhood.source_info, "rins")) break; + } - EXPECT_EQ(rins_neighborhood.reduced_domain_vars.size(), 0); + EXPECT_TRUE(rins_neighborhood.reduced_domain_vars.empty()); EXPECT_EQ(rins_neighborhood.fixed_vars.size(), 1); EXPECT_EQ(rins_neighborhood.fixed_vars[0].first, 1); EXPECT_EQ(rins_neighborhood.fixed_vars[0].second, 5); diff --git a/ortools/sat/routing_cuts.cc b/ortools/sat/routing_cuts.cc index 13f484a901f..1da0ac49e11 100644 --- a/ortools/sat/routing_cuts.cc +++ b/ortools/sat/routing_cuts.cc @@ -124,6 +124,7 @@ MinOutgoingFlowHelper::MinOutgoingFlowHelper( trail_(*model->GetOrCreate()), integer_trail_(*model->GetOrCreate()), integer_encoder_(*model->GetOrCreate()), + root_level_bounds_(*model->GetOrCreate()), shared_stats_(model->GetOrCreate()), in_subset_(num_nodes, false), index_in_subset_(num_nodes, -1), @@ -629,7 +630,8 @@ int MinOutgoingFlowHelper::ComputeMinOutgoingFlow( // If this arc cannot be taken skip. tmp_lbs.clear(); if (!binary_relation_repository_.PropagateLocalBounds( - integer_trail_, lit, node_var_lower_bounds_[tail], &tmp_lbs)) { + integer_trail_, root_level_bounds_, lit, + node_var_lower_bounds_[tail], &tmp_lbs)) { continue; } @@ -755,8 +757,8 @@ int MinOutgoingFlowHelper::ComputeTightMinOutgoingFlow( // If this arc cannot be taken skip. tmp_lbs.clear(); if (!binary_relation_repository_.PropagateLocalBounds( - integer_trail_, literals_[outgoing_arc_index], path_bounds, - &tmp_lbs)) { + integer_trail_, root_level_bounds_, + literals_[outgoing_arc_index], path_bounds, &tmp_lbs)) { continue; } @@ -916,7 +918,7 @@ bool MinOutgoingFlowHelper::SubsetMightBeServedWithKRoutes( absl::flat_hash_map copy = state.lbs; return binary_relation_repository_.PropagateLocalBounds( - integer_trail_, unique_lit, copy, &state.lbs); + integer_trail_, root_level_bounds_, unique_lit, copy, &state.lbs); }; // We always start with the first node in this case. @@ -1011,7 +1013,8 @@ bool MinOutgoingFlowHelper::SubsetMightBeServedWithKRoutes( } } else { if (!binary_relation_repository_.PropagateLocalBounds( - integer_trail_, literal, from_state.lbs, &to_state.lbs)) { + integer_trail_, root_level_bounds_, literal, from_state.lbs, + &to_state.lbs)) { continue; } } @@ -1077,12 +1080,24 @@ struct LocalRelation { IntegerVariable UniqueSharedVariable(const sat::Relation& r1, const sat::Relation& r2) { - DCHECK_NE(r1.a.var, r1.b.var); - DCHECK_NE(r2.a.var, r2.b.var); - if (r1.a.var == r2.a.var && r1.b.var != r2.b.var) return r1.a.var; - if (r1.a.var == r2.b.var && r1.b.var != r2.a.var) return r1.a.var; - if (r1.b.var == r2.a.var && r1.a.var != r2.b.var) return r1.b.var; - if (r1.b.var == r2.b.var && r1.a.var != r2.a.var) return r1.b.var; + DCHECK_NE(r1.expr.vars[0], r1.expr.vars[1]); + DCHECK_NE(r2.expr.vars[0], r2.expr.vars[1]); + if (r1.expr.vars[0] == r2.expr.vars[0] && + r1.expr.vars[1] != r2.expr.vars[1]) { + return r1.expr.vars[0]; + } + if (r1.expr.vars[0] == r2.expr.vars[1] && + r1.expr.vars[1] != r2.expr.vars[0]) { + return r1.expr.vars[0]; + } + if (r1.expr.vars[1] == r2.expr.vars[0] && + r1.expr.vars[0] != r2.expr.vars[1]) { + return r1.expr.vars[1]; + } + if (r1.expr.vars[1] == r2.expr.vars[1] && + r1.expr.vars[0] != r2.expr.vars[0]) { + return r1.expr.vars[1]; + } return kNoIntegerVariable; } @@ -1254,10 +1269,11 @@ class RouteRelationsBuilder { binary_relation_repository_.IndicesOfRelationsEnforcedBy( literals_[i])) { const auto& r = binary_relation_repository_.relation(relation_index); - if (r.a.var == kNoIntegerVariable || r.b.var == kNoIntegerVariable) { + if (r.expr.vars[0] == kNoIntegerVariable || + r.expr.vars[1] == kNoIntegerVariable) { continue; } - cc_finder.AddEdge(r.a.var, r.b.var); + cc_finder.AddEdge(r.expr.vars[0], r.expr.vars[1]); } } const std::vector> connected_components = @@ -1283,10 +1299,11 @@ class RouteRelationsBuilder { binary_relation_repository_.IndicesOfRelationsEnforcedBy( literals_[i])) { const auto& r = binary_relation_repository_.relation(relation_index); - if (r.a.var == kNoIntegerVariable || r.b.var == kNoIntegerVariable) { + if (r.expr.vars[0] == kNoIntegerVariable || + r.expr.vars[1] == kNoIntegerVariable) { continue; } - const int dimension = dimension_by_var_[r.a.var]; + const int dimension = dimension_by_var_[r.expr.vars[0]]; adjacent_relation_indices_[dimension][tails_[i]].push_back( relation_index); adjacent_relation_indices_[dimension][heads_[i]].push_back( @@ -1360,24 +1377,25 @@ class RouteRelationsBuilder { binary_relation_repository_.IndicesOfRelationsEnforcedBy( literals_[arc_index])) { const auto& r = binary_relation_repository_.relation(relation_index); - if (r.a.var == kNoIntegerVariable || r.b.var == kNoIntegerVariable) { + if (r.expr.vars[0] == kNoIntegerVariable || + r.expr.vars[1] == kNoIntegerVariable) { continue; } - if (r.a.var == node_expr.var) { + if (r.expr.vars[0] == node_expr.var) { if (candidate_var != kNoIntegerVariable && - candidate_var != r.b.var) { + candidate_var != r.expr.vars[1]) { candidate_var_is_unique = false; break; } - candidate_var = r.b.var; + candidate_var = r.expr.vars[1]; } - if (r.b.var == node_expr.var) { + if (r.expr.vars[1] == node_expr.var) { if (candidate_var != kNoIntegerVariable && - candidate_var != r.a.var) { + candidate_var != r.expr.vars[0]) { candidate_var_is_unique = false; break; } - candidate_var = r.a.var; + candidate_var = r.expr.vars[0]; } } if (candidate_var != kNoIntegerVariable && candidate_var_is_unique) { @@ -1394,6 +1412,8 @@ class RouteRelationsBuilder { const auto& integer_encoder = *model->GetOrCreate(); const auto& trail = *model->GetOrCreate(); const auto& integer_trail = *model->GetOrCreate(); + const auto& root_level_bounds = + *model->GetOrCreate(); DCHECK_EQ(trail.CurrentDecisionLevel(), 0); flat_arc_dim_relations_ = std::vector( @@ -1488,21 +1508,26 @@ class RouteRelationsBuilder { // Try to match the relation variables with the node expression // variables. First swap the relation terms if needed (this does not // change the relation bounds). - if ((r.a.var != kNoIntegerVariable && r.a.var == head_expr.var) || - (r.b.var != kNoIntegerVariable && r.b.var == tail_expr.var)) { - std::swap(r.a, r.b); + if ((r.expr.vars[0] != kNoIntegerVariable && + r.expr.vars[0] == head_expr.var) || + (r.expr.vars[1] != kNoIntegerVariable && + r.expr.vars[1] == tail_expr.var)) { + std::swap(r.expr.vars[0], r.expr.vars[1]); + std::swap(r.expr.coeffs[0], r.expr.coeffs[1]); } // If the relation has only one term, try to remove the variable // in the node expression corresponding to the missing term. - if (r.a.var == kNoIntegerVariable) { + if (r.expr.vars[0] == kNoIntegerVariable) { if (!to_constant(tail_expr)) continue; - } else if (r.b.var == kNoIntegerVariable) { + } else if (r.expr.vars[1] == kNoIntegerVariable) { if (!to_constant(head_expr)) continue; } // If the relation and node expression variables do not match, we // cannot use this relation for this arc. - if (!((tail_expr.var == r.a.var && head_expr.var == r.b.var) || - (tail_expr.var == r.b.var && head_expr.var == r.a.var))) { + if (!((tail_expr.var == r.expr.vars[0] && + head_expr.var == r.expr.vars[1]) || + (tail_expr.var == r.expr.vars[1] && + head_expr.var == r.expr.vars[0]))) { continue; } ComputeArcRelation(i, dimension, tail_expr, head_expr, r, @@ -1512,13 +1537,12 @@ class RouteRelationsBuilder { // Check if we can use non-enforced relations to improve the relations. if (!tail_expr.IsEmpty() && !head_expr.IsEmpty()) { - for (const int relation_index : - binary_relation_repository_.IndicesOfRelationsBetween( + for (const auto& [expr, lb, ub] : + root_level_bounds.GetAllBoundsContainingVariables( tail_expr.var, head_expr.var)) { - ComputeArcRelation( - i, dimension, tail_expr, head_expr, - binary_relation_repository_.relation(relation_index), - integer_trail); + ComputeArcRelation(i, dimension, tail_expr, head_expr, + Relation{Literal(kNoLiteralIndex), expr, lb, ub}, + integer_trail); } } @@ -1553,20 +1577,25 @@ class RouteRelationsBuilder { const NodeExpression& tail_expr, const NodeExpression& head_expr, sat::Relation r, const IntegerTrail& integer_trail) { - DCHECK((r.a.var == tail_expr.var && r.b.var == head_expr.var) || - (r.a.var == head_expr.var && r.b.var == tail_expr.var)); - if (r.a.var != tail_expr.var) std::swap(r.a, r.b); - if (r.a.coeff == 0 || tail_expr.coeff == 0) { - LocalRelation result = ComputeArcUnaryRelation(head_expr, tail_expr, - r.b.coeff, r.lhs, r.rhs); + DCHECK( + (r.expr.vars[0] == tail_expr.var && r.expr.vars[1] == head_expr.var) || + (r.expr.vars[0] == head_expr.var && r.expr.vars[1] == tail_expr.var)); + if (r.expr.vars[0] != tail_expr.var) { + std::swap(r.expr.vars[0], r.expr.vars[1]); + std::swap(r.expr.coeffs[0], r.expr.coeffs[1]); + } + if (r.expr.coeffs[0] == 0 || tail_expr.coeff == 0) { + LocalRelation result = ComputeArcUnaryRelation( + head_expr, tail_expr, r.expr.coeffs[1], r.lhs, r.rhs); std::swap(result.tail_coeff, result.head_coeff); ProcessNewArcRelation(arc_index, dimension, result); return; } - if (r.b.coeff == 0 || head_expr.coeff == 0) { - ProcessNewArcRelation(arc_index, dimension, - ComputeArcUnaryRelation(tail_expr, head_expr, - r.a.coeff, r.lhs, r.rhs)); + if (r.expr.coeffs[1] == 0 || head_expr.coeff == 0) { + ProcessNewArcRelation( + arc_index, dimension, + ComputeArcUnaryRelation(tail_expr, head_expr, r.expr.coeffs[0], r.lhs, + r.rhs)); return; } const auto [lhs, rhs] = @@ -1680,14 +1709,16 @@ IntegerValue GetDifferenceLowerBound( // TODO(user): overflows could happen if the node expressions are // provided by the user in the model proto. auto lower_bound = [&](IntegerValue k) { - const IntegerValue y_coeff = y_expr.coeff - k * r.b.coeff; - const IntegerValue x_coeff = k * (-r.a.coeff) - x_expr.coeff; + const IntegerValue y_coeff = y_expr.coeff - k * r.expr.coeffs[1]; + const IntegerValue x_coeff = k * (-r.expr.coeffs[0]) - x_expr.coeff; return y_coeff * (y_coeff >= 0 ? y_var_bounds.first : y_var_bounds.second) + x_coeff * (x_coeff >= 0 ? x_var_bounds.first : x_var_bounds.second) + k * (k >= 0 ? r.lhs : r.rhs); }; - const IntegerValue k_x = MathUtil::FloorOfRatio(x_expr.coeff, -r.a.coeff); - const IntegerValue k_y = MathUtil::FloorOfRatio(y_expr.coeff, r.b.coeff); + const IntegerValue k_x = + MathUtil::FloorOfRatio(x_expr.coeff, -r.expr.coeffs[0]); + const IntegerValue k_y = + MathUtil::FloorOfRatio(y_expr.coeff, r.expr.coeffs[1]); IntegerValue result = lower_bound(0); result = std::max(result, lower_bound(k_x)); result = std::max(result, lower_bound(k_x + 1)); @@ -1702,14 +1733,14 @@ std::pair GetDifferenceBounds( const sat::Relation& r, const std::pair& x_var_bounds, const std::pair& y_var_bounds) { - DCHECK_EQ(x_expr.var, r.a.var); - DCHECK_EQ(y_expr.var, r.b.var); + DCHECK_EQ(x_expr.var, r.expr.vars[0]); + DCHECK_EQ(y_expr.var, r.expr.vars[1]); DCHECK_NE(x_expr.var, kNoIntegerVariable); DCHECK_NE(y_expr.var, kNoIntegerVariable); DCHECK_NE(x_expr.coeff, 0); DCHECK_NE(y_expr.coeff, 0); - DCHECK_NE(r.a.coeff, 0); - DCHECK_NE(r.b.coeff, 0); + DCHECK_NE(r.expr.coeffs[0], 0); + DCHECK_NE(r.expr.coeffs[1], 0); const IntegerValue lb = GetDifferenceLowerBound(x_expr, y_expr, r, x_var_bounds, y_var_bounds); const IntegerValue ub = -GetDifferenceLowerBound( @@ -1830,6 +1861,7 @@ BinaryRelationRepository ComputePartialBinaryRelationRepository( ToPositiveIntegerVariable(vars[0]), ToPositiveIntegerVariable(vars[1])); } + Model empty_model; repository.Build(); return repository; } @@ -1917,6 +1949,7 @@ class RoutingCutHelper { *model->GetOrCreate()), random_(model->GetOrCreate()), encoder_(model->GetOrCreate()), + root_level_bounds_(*model->GetOrCreate()), in_subset_(num_nodes, false), self_arc_literal_(num_nodes_), self_arc_lp_value_(num_nodes_), @@ -2050,6 +2083,7 @@ class RoutingCutHelper { const BinaryRelationRepository& binary_relation_repository_; ModelRandomGenerator* random_; IntegerEncoder* encoder_; + const RootLevelLinear2Bounds& root_level_bounds_; std::vector in_subset_; @@ -2755,7 +2789,8 @@ void RoutingCutHelper::GenerateCutsForInfeasiblePaths( const Literal next_literal = literals_[arc_index]; next_state.bounds = state.bounds; if (binary_relation_repository_.PropagateLocalBounds( - integer_trail_, next_literal, state.bounds, &next_state.bounds)) { + integer_trail_, root_level_bounds_, next_literal, state.bounds, + &next_state.bounds)) { // Do not explore "long" paths to keep the search time bounded. if (path_length < max_path_length) { path_nodes[next_state.last_node] = true; diff --git a/ortools/sat/routing_cuts.h b/ortools/sat/routing_cuts.h index 584e0cca199..0713f01bc43 100644 --- a/ortools/sat/routing_cuts.h +++ b/ortools/sat/routing_cuts.h @@ -545,6 +545,7 @@ class MinOutgoingFlowHelper { const Trail& trail_; const IntegerTrail& integer_trail_; const IntegerEncoder& integer_encoder_; + const RootLevelLinear2Bounds& root_level_bounds_; SharedStatistics* shared_stats_; // Temporary data used by ComputeMinOutgoingFlow(). Always contain default diff --git a/ortools/sat/routing_cuts_test.cc b/ortools/sat/routing_cuts_test.cc index 9b38802d5f6..39bb0469ee4 100644 --- a/ortools/sat/routing_cuts_test.cc +++ b/ortools/sat/routing_cuts_test.cc @@ -65,7 +65,7 @@ std::pair ExactDifferenceBounds( IntegerValue ub = kMinIntegerValue; for (IntegerValue x = x_bounds.first; x <= x_bounds.second; ++x) { for (IntegerValue y = y_bounds.first; y <= y_bounds.second; ++y) { - const IntegerValue r_value = x * r.a.coeff + y * r.b.coeff; + const IntegerValue r_value = x * r.expr.coeffs[0] + y * r.expr.coeffs[1]; if (r_value < r.lhs || r_value > r.rhs) continue; const IntegerValue difference = y_expr.ValueAt(y) - x_expr.ValueAt(x); lb = std::min(lb, difference); @@ -101,8 +101,7 @@ TEST(GetDifferenceBounds, RandomTest) { const NodeExpression y_expr(y, B, absl::Uniform(random, -5, 5)); const Relation r{ .enforcement = lit, - .a = LinearTerm(x, a), - .b = LinearTerm(y, b), + .expr = LinearExpression2(x, y, a, b), .lhs = lhs, .rhs = rhs, }; @@ -162,8 +161,8 @@ TEST(MinOutgoingFlowHelperTest, CapacityConstraints) { // picked up by the vehicle leaving n. const int head_load = head == 0 ? 0 : head + 10; // loads[head] - loads[tail] >= head_load - repository->Add(literal, {loads[head], 1}, {loads[tail], -1}, head_load, - 1000); + repository->Add(literal, LinearExpression2(loads[head], loads[tail], 1, -1), + head_load, 1000); } repository->Build(); // Subject under test. @@ -230,11 +229,13 @@ TEST_P(DimensionBasedMinOutgoingFlowHelperTest, BasicCapacities) { if (tail == 0 || head == 0) continue; if (pickup) { // loads[head] - loads[tail] >= demand - repository->Add(literal, {loads[head], 1}, {loads[tail], -1}, + repository->Add(literal, + LinearExpression2(loads[head], loads[tail], 1, -1), demands[use_outgoing_load ? head : tail], 1000); } else { // loads[tail] - loads[head] >= demand - repository->Add(literal, {loads[tail], 1}, {loads[head], -1}, + repository->Add(literal, + LinearExpression2(loads[tail], loads[head], 1, -1), demands[use_outgoing_load ? head : tail], 1000); } } @@ -301,11 +302,13 @@ TEST_P(DimensionBasedMinOutgoingFlowHelperTest, const int tail = tails[i]; if (pickup) { // loads[head] - loads[tail] >= demand - repository->Add(literals[i], {loads[head], 1}, {loads[tail], -1}, + repository->Add(literals[i], + LinearExpression2::Difference(loads[head], loads[tail]), demands[use_outgoing_load ? head : tail], 1000); } else { // loads[tail] - loads[head] >= demand - repository->Add(literals[i], {loads[tail], 1}, {loads[head], -1}, + repository->Add(literals[i], + LinearExpression2::Difference(loads[tail], loads[head]), demands[use_outgoing_load ? head : tail], 1000); } } @@ -357,8 +360,8 @@ TEST(MinOutgoingFlowHelperTest, NodeExpressionWithConstant) { auto* repository = model.GetOrCreate(); // Capacity constraint: (offset_load2 + offset) - load1 >= demand1 - repository->Add(literals[0], {offset_load2, 1}, {load1, -1}, demand1 - offset, - 1000); + repository->Add(literals[0], LinearExpression2(offset_load2, load1, 1, -1), + demand1 - offset, 1000); repository->Build(); std::unique_ptr route_relations_helper = RouteRelationsHelper::Create(num_nodes, tails, heads, literals, @@ -398,7 +401,8 @@ TEST(MinOutgoingFlowHelperTest, ConstantNodeExpression) { auto* repository = model.GetOrCreate(); // Capacity constraint: load2 - load1 >= demand1 - repository->Add(literals[0], {kNoIntegerVariable, 0}, {load1, -1}, + repository->Add(literals[0], + LinearExpression2(kNoIntegerVariable, load1, 0, -1), demand1 - load2, 1000); repository->Build(); std::unique_ptr route_relations_helper = @@ -451,7 +455,8 @@ TEST(MinOutgoingFlowHelperTest, NodeExpressionUsingArcLiteralAsVariable) { // Capacity constraint: load2 - load1 >= demand1. This expands to // (capacity - demand2 - demand3 * l) - load1 >= demand1, i.e., // -demand3 * l - load1 >= demand1 + demand2 - capacity - repository->Add(literals[0], {arc_2_3_var, -demand3}, {load1, -1}, + repository->Add(literals[0], + LinearExpression2(arc_2_3_var, load1, -demand3, -1), demand1 + demand2 - capacity, 1000); // Capacity constraint: load3 - load2 >= demand2. This expands to // (capacity - demand3) - (capacity - demand2 - demand3 * l) >= demand2 which, @@ -508,7 +513,8 @@ TEST(MinOutgoingFlowHelperTest, // Capacity constraint: load2 - load1 >= demand1. This expands to // (capacity - demand2 - demand3 + demand3 * l) - load1 >= demand1, i.e., // demand3 * l - load1 >= demand1 + demand2 + demand3 - capacity - repository->Add(literals[0], {arc_2_3_var, demand3}, {load1, -1}, + repository->Add(literals[0], + LinearExpression2(arc_2_3_var, load1, demand3, -1), demand1 + demand2 + demand3 - capacity, 1000); // Capacity constraint: load3 - load2 >= demand2. This expands to // (capacity - demand3) - (capacity - demand2 - demand3 + demand3 * l) >= @@ -566,7 +572,7 @@ TEST(MinOutgoingFlowHelperTest, ArcNodeExpressionsWithSharedVariable) { // Capacity constraint: load2 - load1 >= demand1. This expands to // (capacity - demand2 - demand3) - coeff * x - load1 >= demand1, i.e., // -coeff * x - load1 >= demand1 + demand2 + demand3 - capacity. - repository->Add(literals[0], {x, -coeff}, {load1, -1}, + repository->Add(literals[0], LinearExpression2(x, load1, -coeff, -1), demand1 + demand2 + demand3 - capacity, 1000); // Capacity constraint: load3 - load2 >= demand2. This expands to // (capacity - demand3) - (capacity - demand2 - demand3) >= demand2, which @@ -629,12 +635,14 @@ TEST(MinOutgoingFlowHelperTest, UnaryRelationForTwoNodeExpressions) { // constraint is enforced by arc_1_2_lit we can assume it is true, which // implies that x = 0. Hence the constraint simplifies to load1 <= capacity - // demand2 - demand1. - repository->Add(literals[0], {load1, 1}, {kNoIntegerVariable, 0}, 0, + repository->Add(literals[0], + LinearExpression2(load1, kNoIntegerVariable, 1, 0), 0, capacity - demand1 - demand2); // Capacity constraint: load3 - load2 >= demand2. This expands to // load3 - ((capacity - demand2) - demand1 * x) >= demand2, i.e. to load3 + // demand1 * x >= capacity - repository->Add(literals[1], {load3, 1}, {x, demand1}, capacity, 1000); + repository->Add(literals[1], LinearExpression2(load3, x, 1, demand1), + capacity, 1000); repository->Build(); std::unique_ptr route_relations_helper = RouteRelationsHelper::Create(num_nodes, tails, heads, literals, @@ -687,8 +695,10 @@ TEST(MinOutgoingFlowHelperTest, NodeMustBeInnerNode) { auto* repository = model.GetOrCreate(); for (int i = 0; i < num_arcs; ++i) { // loads[head] - loads[tail] >= demand[arc] - repository->Add(literals[i], {loads[heads[i]], 1}, {loads[tails[i]], -1}, - demands[i], 1000); + repository->Add( + literals[i], + LinearExpression2(loads[heads[i]], loads[tails[i]], 1, -1), + demands[i], 1000); } repository->Build(); @@ -745,8 +755,10 @@ TEST(MinOutgoingFlowHelperTest, BetterUseOfUpperBound) { auto* repository = model.GetOrCreate(); for (int i = 0; i < num_arcs; ++i) { // loads[head] - loads[tail] >= demand[arc] - repository->Add(literals[i], {loads[heads[i]], 1}, {loads[tails[i]], -1}, - demands[i], 1000); + repository->Add( + literals[i], + LinearExpression2::Difference(loads[heads[i]], loads[tails[i]]), + demands[i], 1000); } repository->Build(); const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions( @@ -783,8 +795,9 @@ TEST(MinOutgoingFlowHelperTest, DimensionBasedMinOutgoingFlow_IsolatedNodes) { literals.push_back(Literal(model.Add(NewBooleanVariable()), true)); variables.push_back(model.Add(NewIntegerVariable(0, 100))); // Dummy relation, used only to associate a variable with each node. - repository->Add(literals.back(), {variables[head], 1}, {variables[0], -1}, - 1, 100); + repository->Add(literals.back(), + LinearExpression2(variables[head], variables[0], 1, -1), 1, + 100); } repository->Build(); const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions( @@ -834,8 +847,8 @@ TEST(MinOutgoingFlowHelperTest, TimeWindows) { const auto& [tail, head] = arc; const int travel_time = 10 - tail; // times[head] - times[tail] >= travel_time - repository->Add(literal, {times[head], 1}, {times[tail], -1}, travel_time, - 1000); + repository->Add(literal, LinearExpression2(times[head], times[tail], 1, -1), + travel_time, 1000); } repository->Build(); // Subject under test. @@ -963,10 +976,14 @@ TEST(MinOutgoingFlowHelperTest, SubsetMightBeServedWithKRoutes) { const auto& [tail, head] = arc; // vars[head] >= vars[tail] + load[head]; - repository->Add(literal, {cumul_vars_1[head], 1}, {cumul_vars_1[tail], -1}, - load1[head], 10000); - repository->Add(literal, {cumul_vars_2[head], 1}, {cumul_vars_2[tail], -1}, - load2[head], 10000); + repository->Add( + literal, + LinearExpression2(cumul_vars_1[head], cumul_vars_1[tail], 1, -1), + load1[head], 10000); + repository->Add( + literal, + LinearExpression2(cumul_vars_2[head], cumul_vars_2[tail], 1, -1), + load2[head], 10000); } repository->Build(); @@ -1031,10 +1048,14 @@ TEST(MinOutgoingFlowHelperTest, SubsetMightBeServedWithKRoutesRandom) { const auto& [tail, head] = arc; // vars[head] >= vars[tail] + load[head]; - repository->Add(literal, {cumul_vars_1[head], 1}, {cumul_vars_1[tail], -1}, - load1[head], 10000); - repository->Add(literal, {cumul_vars_2[head], 1}, {cumul_vars_2[tail], -1}, - load2[head], 10000); + repository->Add( + literal, + LinearExpression2::Difference(cumul_vars_1[head], cumul_vars_1[tail]), + load1[head], 10000); + repository->Add( + literal, + LinearExpression2::Difference(cumul_vars_2[head], cumul_vars_2[tail]), + load2[head], 10000); } repository->Build(); @@ -1160,8 +1181,10 @@ TEST(MinOutgoingFlowHelperTest, const Literal literal = literals[arc]; // vars[head] >= vars[tail] + travel_times[arc]; - repository->Add(literal, {cumul_vars[head], 1}, {cumul_vars[tail], -1}, - travel_times[arc], 10000); + repository->Add( + literal, + LinearExpression2::Difference(cumul_vars[head], cumul_vars[tail]), + travel_times[arc], 10000); } repository->Build(); @@ -1387,14 +1410,16 @@ TEST(RouteRelationsHelperTest, Basic) { const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); BinaryRelationRepository repository; - repository.Add(literals[0], {a, 1}, {b, -1}, 50, 1000); - repository.Add(literals[1], {a, 1}, {c, -1}, 70, 1000); - repository.Add(literals[2], {c, 1}, {b, -1}, 40, 1000); - repository.Add(literals[0], {NegationOf(u), -1}, {NegationOf(v), 1}, 4, 100); - repository.Add(literals[1], {u, 1}, {w, -1}, 4, 100); - repository.Add(literals[2], {w, -1}, {v, 1}, -100, -3); - repository.Add(literals[3], {x, 1}, {w, -1}, 5, 100); - repository.Add(literals[4], {z, 1}, {y, -1}, 7, 100); + repository.Add(literals[0], LinearExpression2::Difference(a, b), 50, 1000); + repository.Add(literals[1], LinearExpression2::Difference(a, c), 70, 1000); + repository.Add(literals[2], LinearExpression2::Difference(c, b), 40, 1000); + repository.Add(literals[0], + LinearExpression2(NegationOf(u), NegationOf(v), -1, 1), 4, + 100); + repository.Add(literals[1], LinearExpression2::Difference(u, w), 4, 100); + repository.Add(literals[2], LinearExpression2(w, v, -1, 1), -100, -3); + repository.Add(literals[3], LinearExpression2::Difference(x, w), 5, 100); + repository.Add(literals[4], LinearExpression2::Difference(z, y), 7, 100); repository.Build(); const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions( @@ -1480,15 +1505,16 @@ TEST(RouteRelationsHelperTest, UnenforcedRelations) { const IntegerVariable c = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable d = model.Add(NewIntegerVariable(0, 100)); BinaryRelationRepository repository; - repository.Add(literals[0], {b, 1}, {a, -1}, 1, 1); - repository.Add(literals[1], {c, 1}, {b, -1}, 2, 2); - repository.Add(literals[2], {d, 1}, {c, -1}, 3, 3); - repository.Add(literals[3], {a, 1}, {d, -1}, 4, 4); + RootLevelLinear2Bounds* bounds = model.GetOrCreate(); + repository.Add(literals[0], LinearExpression2::Difference(b, a), 1, 1); + repository.Add(literals[1], LinearExpression2::Difference(c, b), 2, 2); + repository.Add(literals[2], LinearExpression2::Difference(d, c), 3, 3); + repository.Add(literals[3], LinearExpression2::Difference(a, d), 4, 4); // Several unenforced relations on the diagonal arc. The one with the +/-1 // coefficients should be preferred. - repository.Add(Literal(kNoLiteralIndex), {c, 3}, {a, -2}, 1, 9); - repository.Add(Literal(kNoLiteralIndex), {c, 1}, {a, -1}, 5, 5); - repository.Add(Literal(kNoLiteralIndex), {c, 2}, {a, -3}, 3, 8); + bounds->Add(LinearExpression2(c, a, 3, -2), 1, 9); + bounds->Add(LinearExpression2(c, a, 1, -1), 5, 5); + bounds->Add(LinearExpression2(c, a, 2, -3), 3, 8); repository.Build(); const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions( @@ -1529,13 +1555,13 @@ TEST(RouteRelationsHelperTest, SeveralVariablesPerNode) { const IntegerVariable y = model.Add(NewIntegerVariable(0, 10)); const IntegerVariable z = model.Add(NewIntegerVariable(0, 10)); BinaryRelationRepository repository; - repository.Add(literals[0], {b, 1}, {a, -1}, 50, 1000); - repository.Add(literals[1], {c, 1}, {b, -1}, 70, 1000); - repository.Add(literals[0], {z, 1}, {y, -1}, 5, 100); - repository.Add(literals[1], {y, 1}, {x, -1}, 7, 100); + repository.Add(literals[0], LinearExpression2::Difference(b, a), 50, 1000); + repository.Add(literals[1], LinearExpression2::Difference(c, b), 70, 1000); + repository.Add(literals[0], LinearExpression2::Difference(z, y), 5, 100); + repository.Add(literals[1], LinearExpression2::Difference(y, x), 7, 100); // Weird relation linking time and load variables, causing all the variables // to be in a single "dimension". - repository.Add(literals[0], {x, 1}, {a, -1}, 0, 100); + repository.Add(literals[0], LinearExpression2::Difference(x, a), 0, 100); repository.Build(); const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions( @@ -1561,7 +1587,7 @@ TEST(RouteRelationsHelperTest, ComplexVariableRelations) { const IntegerVariable b = model.Add(NewIntegerVariable(0, 1)); BinaryRelationRepository repository; // "complex" relation with non +1/-1 coefficients. - repository.Add(literals[0], {b, 10}, {a, 1}, 0, 150); + repository.Add(literals[0], LinearExpression2(b, a, 10, 1), 0, 150); repository.Build(); const RoutingCumulExpressions cumuls = { @@ -1625,10 +1651,10 @@ TEST(RouteRelationsHelperTest, SeveralRelationsPerArc) { const IntegerVariable b = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable c = model.Add(NewIntegerVariable(0, 100)); BinaryRelationRepository repository; - repository.Add(literals[0], {b, 1}, {a, -1}, 50, 1000); - repository.Add(literals[1], {c, 1}, {b, -1}, 70, 1000); + repository.Add(literals[0], LinearExpression2::Difference(b, a), 50, 1000); + repository.Add(literals[1], LinearExpression2::Difference(c, b), 70, 1000); // Add a second relation for some arc. - repository.Add(literals[1], {c, 2}, {b, -3}, 100, 200); + repository.Add(literals[1], LinearExpression2(c, b, 2, -3), 100, 200); repository.Build(); const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions( @@ -1661,8 +1687,8 @@ TEST(RouteRelationsHelperTest, SeveralArcsPerLiteral) { const IntegerVariable b = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable c = model.Add(NewIntegerVariable(0, 100)); BinaryRelationRepository repository; - repository.Add(literals[0], {b, 1}, {a, -1}, 50, 1000); - repository.Add(literals[0], {c, 1}, {b, -1}, 40, 1000); + repository.Add(literals[0], LinearExpression2::Difference(b, a), 50, 1000); + repository.Add(literals[0], LinearExpression2::Difference(c, b), 40, 1000); repository.Build(); const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions( @@ -1703,13 +1729,13 @@ TEST(RouteRelationsHelperTest, InconsistentRelationIsSkipped) { const IntegerVariable e = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable f = model.Add(NewIntegerVariable(0, 100)); BinaryRelationRepository repository; - repository.Add(literals[0], {b, 1}, {a, -1}, 0, 0); - repository.Add(literals[1], {c, 1}, {b, -1}, 1, 1); - repository.Add(literals[2], {d, 1}, {c, -1}, 2, 2); - repository.Add(literals[3], {e, 1}, {d, -1}, 3, 3); - repository.Add(literals[4], {f, 1}, {b, -1}, 4, 4); + repository.Add(literals[0], LinearExpression2::Difference(b, a), 0, 0); + repository.Add(literals[1], LinearExpression2::Difference(c, b), 1, 1); + repository.Add(literals[2], LinearExpression2::Difference(d, c), 2, 2); + repository.Add(literals[3], LinearExpression2::Difference(e, d), 3, 3); + repository.Add(literals[4], LinearExpression2::Difference(f, b), 4, 4); // Inconsistent relation for arc 5->3 (should be between f and d). - repository.Add(literals[5], {f, 2}, {b, -1}, 5, 5); + repository.Add(literals[5], LinearExpression2(f, b, 2, -1), 5, 5); repository.Build(); const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions( @@ -1763,16 +1789,16 @@ TEST(RouteRelationsHelperTest, InconsistentRelationWithMultipleArcsPerLiteral) { const IntegerVariable d = model.Add(NewIntegerVariable(0, 100)); const IntegerVariable e = model.Add(NewIntegerVariable(0, 100)); BinaryRelationRepository repository; - repository.Add(literals[0], {b, 1}, {a, -1}, 0, 0); - repository.Add(literals[1], {c, 1}, {b, -1}, 1, 1); - repository.Add(literals[2], {d, 1}, {c, -1}, 2, 2); - repository.Add(literals[3], {a, 1}, {d, -1}, 3, 3); + repository.Add(literals[0], LinearExpression2::Difference(b, a), 0, 0); + repository.Add(literals[1], LinearExpression2::Difference(c, b), 1, 1); + repository.Add(literals[2], LinearExpression2::Difference(d, c), 2, 2); + repository.Add(literals[3], LinearExpression2::Difference(a, d), 3, 3); // Inconsistent relation for arc 4->1 (should be between e and b). Note that // arcs 4->1 and 4->3 are enforced by the same literal, thus both should // be true at the same time, hence the crossed bounds below. - repository.Add(literals[4], {e, 1}, {d, -1}, 4, 4); - repository.Add(literals[5], {e, 1}, {d, -1}, 5, 5); + repository.Add(literals[4], LinearExpression2::Difference(e, d), 4, 4); + repository.Add(literals[5], LinearExpression2::Difference(e, d), 5, 5); repository.Build(); const RoutingCumulExpressions cumuls = DetectDimensionsAndCumulExpressions( @@ -2406,7 +2432,8 @@ TEST(CreateCVRPCutGeneratorTest, InfeasiblePathCuts) { const int head = heads[i]; if (tail == 0 || head == 0) continue; // loads[head] >= loads[tail] + demand[tail] - repository->Add(literals[i], {loads[head], 1}, {loads[tail], -1}, + repository->Add(literals[i], + LinearExpression2(loads[head], loads[tail], 1, -1), demands[tail], 10000); } repository->Build(); diff --git a/ortools/sat/samples/all_different_except_zero_sample_sat.py b/ortools/sat/samples/all_different_except_zero_sample_sat.py index 9c3ea7edd85..abcf5f61385 100644 --- a/ortools/sat/samples/all_different_except_zero_sample_sat.py +++ b/ortools/sat/samples/all_different_except_zero_sample_sat.py @@ -20,65 +20,65 @@ def all_different_except_0(): - """Encode the AllDifferentExcept0 constraint.""" - - # Model. - model = cp_model.CpModel() - - # Declare our primary variable. - x = [model.new_int_var(0, 10, f"x{i}") for i in range(5)] - - # Expand the AllDifferentExcept0 constraint. - variables_per_value = collections.defaultdict(list) - all_values = set() - - for var in x: - all_encoding_literals = [] - # Domains of variables are represented by flat intervals. - for i in range(0, len(var.proto.domain), 2): - start = var.proto.domain[i] - end = var.proto.domain[i + 1] - for value in range(start, end + 1): # Intervals are inclusive. - # Create the literal attached to var == value. - bool_var = model.new_bool_var(f"{var} == {value}") - model.add(var == value).only_enforce_if(bool_var) - - # Collect all encoding literals for a given variable. - all_encoding_literals.append(bool_var) - - # Collect all encoding literals for a given value. - variables_per_value[value].append(bool_var) - - # Collect all different values. - all_values.add(value) - - # One variable must have exactly one value. - model.add_exactly_one(all_encoding_literals) - - # Add the all_different constraints. - for value, literals in variables_per_value.items(): - if value == 0: - continue - model.add_at_most_one(literals) - - model.add(x[0] == 0) - model.add(x[1] == 0) - - model.maximize(sum(x)) - - # Create a solver and solve. - solver = cp_model.CpSolver() - status = solver.solve(model) - - # Checks and prints the output. - if status == cp_model.OPTIMAL: - print(f"Optimal solution: {solver.objective_value}, expected: 27.0") - elif status == cp_model.FEASIBLE: - print(f"Feasible solution: {solver.objective_value}, optimal 27.0") - elif status == cp_model.INFEASIBLE: - print("The model is infeasible") - else: - print("Something went wrong. Please check the status and the log") + """Encode the AllDifferentExcept0 constraint.""" + + # Model. + model = cp_model.CpModel() + + # Declare our primary variable. + x = [model.new_int_var(0, 10, f"x{i}") for i in range(5)] + + # Expand the AllDifferentExcept0 constraint. + variables_per_value = collections.defaultdict(list) + all_values = set() + + for var in x: + all_encoding_literals = [] + # Domains of variables are represented by flat intervals. + for i in range(0, len(var.proto.domain), 2): + start = var.proto.domain[i] + end = var.proto.domain[i + 1] + for value in range(start, end + 1): # Intervals are inclusive. + # Create the literal attached to var == value. + bool_var = model.new_bool_var(f"{var} == {value}") + model.add(var == value).only_enforce_if(bool_var) + + # Collect all encoding literals for a given variable. + all_encoding_literals.append(bool_var) + + # Collect all encoding literals for a given value. + variables_per_value[value].append(bool_var) + + # Collect all different values. + all_values.add(value) + + # One variable must have exactly one value. + model.add_exactly_one(all_encoding_literals) + + # Add the all_different constraints. + for value, literals in variables_per_value.items(): + if value == 0: + continue + model.add_at_most_one(literals) + + model.add(x[0] == 0) + model.add(x[1] == 0) + + model.maximize(sum(x)) + + # Create a solver and solve. + solver = cp_model.CpSolver() + status = solver.solve(model) + + # Checks and prints the output. + if status == cp_model.OPTIMAL: + print(f"Optimal solution: {solver.objective_value}, expected: 27.0") + elif status == cp_model.FEASIBLE: + print(f"Feasible solution: {solver.objective_value}, optimal 27.0") + elif status == cp_model.INFEASIBLE: + print("The model is infeasible") + else: + print("Something went wrong. Please check the status and the log") all_different_except_0() diff --git a/ortools/sat/samples/assignment_groups_sat.py b/ortools/sat/samples/assignment_groups_sat.py index 2ea4fc973b0..cef6171ea2f 100644 --- a/ortools/sat/samples/assignment_groups_sat.py +++ b/ortools/sat/samples/assignment_groups_sat.py @@ -21,124 +21,126 @@ def main() -> None: - # Data - # [START data] - costs = [ - [90, 76, 75, 70, 50, 74], - [35, 85, 55, 65, 48, 101], - [125, 95, 90, 105, 59, 120], - [45, 110, 95, 115, 104, 83], - [60, 105, 80, 75, 59, 62], - [45, 65, 110, 95, 47, 31], - [38, 51, 107, 41, 69, 99], - [47, 85, 57, 71, 92, 77], - [39, 63, 97, 49, 118, 56], - [47, 101, 71, 60, 88, 109], - [17, 39, 103, 64, 61, 92], - [101, 45, 83, 59, 92, 27], - ] - num_workers = len(costs) - num_tasks = len(costs[0]) - # [END data] - - # Allowed groups of workers: - # [START allowed_groups] - group1 = [ - [0, 0, 1, 1], # Workers 2, 3 - [0, 1, 0, 1], # Workers 1, 3 - [0, 1, 1, 0], # Workers 1, 2 - [1, 1, 0, 0], # Workers 0, 1 - [1, 0, 1, 0], # Workers 0, 2 - ] - - group2 = [ - [0, 0, 1, 1], # Workers 6, 7 - [0, 1, 0, 1], # Workers 5, 7 - [0, 1, 1, 0], # Workers 5, 6 - [1, 1, 0, 0], # Workers 4, 5 - [1, 0, 0, 1], # Workers 4, 7 - ] - - group3 = [ - [0, 0, 1, 1], # Workers 10, 11 - [0, 1, 0, 1], # Workers 9, 11 - [0, 1, 1, 0], # Workers 9, 10 - [1, 0, 1, 0], # Workers 8, 10 - [1, 0, 0, 1], # Workers 8, 11 - ] - # [END allowed_groups] - - # Model - # [START model] - model = cp_model.CpModel() - # [END model] - - # Variables - # [START variables] - x = {} - for worker in range(num_workers): - for task in range(num_tasks): - x[worker, task] = model.new_bool_var(f"x[{worker},{task}]") - # [END variables] - - # Constraints - # [START constraints] - # Each worker is assigned to at most one task. - for worker in range(num_workers): - model.add_at_most_one(x[worker, task] for task in range(num_tasks)) - - # Each task is assigned to exactly one worker. + # Data + # [START data] + costs = [ + [90, 76, 75, 70, 50, 74], + [35, 85, 55, 65, 48, 101], + [125, 95, 90, 105, 59, 120], + [45, 110, 95, 115, 104, 83], + [60, 105, 80, 75, 59, 62], + [45, 65, 110, 95, 47, 31], + [38, 51, 107, 41, 69, 99], + [47, 85, 57, 71, 92, 77], + [39, 63, 97, 49, 118, 56], + [47, 101, 71, 60, 88, 109], + [17, 39, 103, 64, 61, 92], + [101, 45, 83, 59, 92, 27], + ] + num_workers = len(costs) + num_tasks = len(costs[0]) + # [END data] + + # Allowed groups of workers: + # [START allowed_groups] + group1 = [ + [0, 0, 1, 1], # Workers 2, 3 + [0, 1, 0, 1], # Workers 1, 3 + [0, 1, 1, 0], # Workers 1, 2 + [1, 1, 0, 0], # Workers 0, 1 + [1, 0, 1, 0], # Workers 0, 2 + ] + + group2 = [ + [0, 0, 1, 1], # Workers 6, 7 + [0, 1, 0, 1], # Workers 5, 7 + [0, 1, 1, 0], # Workers 5, 6 + [1, 1, 0, 0], # Workers 4, 5 + [1, 0, 0, 1], # Workers 4, 7 + ] + + group3 = [ + [0, 0, 1, 1], # Workers 10, 11 + [0, 1, 0, 1], # Workers 9, 11 + [0, 1, 1, 0], # Workers 9, 10 + [1, 0, 1, 0], # Workers 8, 10 + [1, 0, 0, 1], # Workers 8, 11 + ] + # [END allowed_groups] + + # Model + # [START model] + model = cp_model.CpModel() + # [END model] + + # Variables + # [START variables] + x = {} + for worker in range(num_workers): for task in range(num_tasks): - model.add_exactly_one(x[worker, task] for worker in range(num_workers)) - # [END constraints] - - # [START assignments] - # Create variables for each worker, indicating whether they work on some task. - work = {} - for worker in range(num_workers): - work[worker] = model.new_bool_var(f"work[{worker}]") - - for worker in range(num_workers): - for task in range(num_tasks): - model.add(work[worker] == sum(x[worker, task] for task in range(num_tasks))) - - # Define the allowed groups of worders - model.add_allowed_assignments([work[0], work[1], work[2], work[3]], group1) - model.add_allowed_assignments([work[4], work[5], work[6], work[7]], group2) - model.add_allowed_assignments([work[8], work[9], work[10], work[11]], group3) - # [END assignments] - - # Objective - # [START objective] - objective_terms = [] + x[worker, task] = model.new_bool_var(f"x[{worker},{task}]") + # [END variables] + + # Constraints + # [START constraints] + # Each worker is assigned to at most one task. + for worker in range(num_workers): + model.add_at_most_one(x[worker, task] for task in range(num_tasks)) + + # Each task is assigned to exactly one worker. + for task in range(num_tasks): + model.add_exactly_one(x[worker, task] for worker in range(num_workers)) + # [END constraints] + + # [START assignments] + # Create variables for each worker, indicating whether they work on some task. + work = {} + for worker in range(num_workers): + work[worker] = model.new_bool_var(f"work[{worker}]") + + for worker in range(num_workers): + for task in range(num_tasks): + model.add( + work[worker] == sum(x[worker, task] for task in range(num_tasks)) + ) + + # Define the allowed groups of worders + model.add_allowed_assignments([work[0], work[1], work[2], work[3]], group1) + model.add_allowed_assignments([work[4], work[5], work[6], work[7]], group2) + model.add_allowed_assignments([work[8], work[9], work[10], work[11]], group3) + # [END assignments] + + # Objective + # [START objective] + objective_terms = [] + for worker in range(num_workers): + for task in range(num_tasks): + objective_terms.append(costs[worker][task] * x[worker, task]) + model.minimize(sum(objective_terms)) + # [END objective] + + # Solve + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] + + # Print solution. + # [START print_solution] + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print(f"Total cost = {solver.objective_value}\n") for worker in range(num_workers): - for task in range(num_tasks): - objective_terms.append(costs[worker][task] * x[worker, task]) - model.minimize(sum(objective_terms)) - # [END objective] - - # Solve - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] - - # Print solution. - # [START print_solution] - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print(f"Total cost = {solver.objective_value}\n") - for worker in range(num_workers): - for task in range(num_tasks): - if solver.boolean_value(x[worker, task]): - print( - f"Worker {worker} assigned to task {task}." - + f" Cost = {costs[worker][task]}" - ) - else: - print("No solution found.") - # [END print_solution] + for task in range(num_tasks): + if solver.boolean_value(x[worker, task]): + print( + f"Worker {worker} assigned to task {task}." + + f" Cost = {costs[worker][task]}" + ) + else: + print("No solution found.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/assignment_sat.py b/ortools/sat/samples/assignment_sat.py index 96f20e8f744..50f6bc6fcf5 100644 --- a/ortools/sat/samples/assignment_sat.py +++ b/ortools/sat/samples/assignment_sat.py @@ -26,9 +26,9 @@ def main() -> None: - # Data - # [START data_model] - data_str = """ + # Data + # [START data_model] + data_str = """ worker task cost w1 t1 90 w1 t2 80 @@ -52,55 +52,55 @@ def main() -> None: w5 t4 100 """ - data = pd.read_table(io.StringIO(data_str), sep=r"\s+") - # [END data_model] - - # Model - # [START model] - model = cp_model.CpModel() - # [END model] - - # Variables - # [START variables] - x = model.new_bool_var_series(name="x", index=data.index) - # [END variables] - - # Constraints - # [START constraints] - # Each worker is assigned to at most one task. - for unused_name, tasks in data.groupby("worker"): - model.add_at_most_one(x[tasks.index]) - - # Each task is assigned to exactly one worker. - for unused_name, workers in data.groupby("task"): - model.add_exactly_one(x[workers.index]) - # [END constraints] - - # Objective - # [START objective] - model.minimize(data.cost.dot(x)) - # [END objective] - - # Solve - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] - - # Print solution. - # [START print_solution] - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print(f"Total cost = {solver.objective_value}\n") - selected = data.loc[solver.boolean_values(x).loc[lambda x: x].index] - for unused_index, row in selected.iterrows(): - print(f"{row.task} assigned to {row.worker} with a cost of {row.cost}") - elif status == cp_model.INFEASIBLE: - print("No solution found") - else: - print("Something is wrong, check the status and the log of the solve") - # [END print_solution] + data = pd.read_table(io.StringIO(data_str), sep=r"\s+") + # [END data_model] + + # Model + # [START model] + model = cp_model.CpModel() + # [END model] + + # Variables + # [START variables] + x = model.new_bool_var_series(name="x", index=data.index) + # [END variables] + + # Constraints + # [START constraints] + # Each worker is assigned to at most one task. + for unused_name, tasks in data.groupby("worker"): + model.add_at_most_one(x[tasks.index]) + + # Each task is assigned to exactly one worker. + for unused_name, workers in data.groupby("task"): + model.add_exactly_one(x[workers.index]) + # [END constraints] + + # Objective + # [START objective] + model.minimize(data.cost.dot(x)) + # [END objective] + + # Solve + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] + + # Print solution. + # [START print_solution] + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print(f"Total cost = {solver.objective_value}\n") + selected = data.loc[solver.boolean_values(x).loc[lambda x: x].index] + for unused_index, row in selected.iterrows(): + print(f"{row.task} assigned to {row.worker} with a cost of {row.cost}") + elif status == cp_model.INFEASIBLE: + print("No solution found") + else: + print("Something is wrong, check the status and the log of the solve") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/assignment_task_sizes_sat.py b/ortools/sat/samples/assignment_task_sizes_sat.py index 0baca4a6df0..2a64061eab4 100644 --- a/ortools/sat/samples/assignment_task_sizes_sat.py +++ b/ortools/sat/samples/assignment_task_sizes_sat.py @@ -21,86 +21,86 @@ def main() -> None: - # Data - # [START data] - costs = [ - [90, 76, 75, 70, 50, 74, 12, 68], - [35, 85, 55, 65, 48, 101, 70, 83], - [125, 95, 90, 105, 59, 120, 36, 73], - [45, 110, 95, 115, 104, 83, 37, 71], - [60, 105, 80, 75, 59, 62, 93, 88], - [45, 65, 110, 95, 47, 31, 81, 34], - [38, 51, 107, 41, 69, 99, 115, 48], - [47, 85, 57, 71, 92, 77, 109, 36], - [39, 63, 97, 49, 118, 56, 92, 61], - [47, 101, 71, 60, 88, 109, 52, 90], - ] - num_workers = len(costs) - num_tasks = len(costs[0]) + # Data + # [START data] + costs = [ + [90, 76, 75, 70, 50, 74, 12, 68], + [35, 85, 55, 65, 48, 101, 70, 83], + [125, 95, 90, 105, 59, 120, 36, 73], + [45, 110, 95, 115, 104, 83, 37, 71], + [60, 105, 80, 75, 59, 62, 93, 88], + [45, 65, 110, 95, 47, 31, 81, 34], + [38, 51, 107, 41, 69, 99, 115, 48], + [47, 85, 57, 71, 92, 77, 109, 36], + [39, 63, 97, 49, 118, 56, 92, 61], + [47, 101, 71, 60, 88, 109, 52, 90], + ] + num_workers = len(costs) + num_tasks = len(costs[0]) - task_sizes = [10, 7, 3, 12, 15, 4, 11, 5] - # Maximum total of task sizes for any worker - total_size_max = 15 - # [END data] + task_sizes = [10, 7, 3, 12, 15, 4, 11, 5] + # Maximum total of task sizes for any worker + total_size_max = 15 + # [END data] - # Model - # [START model] - model = cp_model.CpModel() - # [END model] + # Model + # [START model] + model = cp_model.CpModel() + # [END model] - # Variables - # [START variables] - x = {} - for worker in range(num_workers): - for task in range(num_tasks): - x[worker, task] = model.new_bool_var(f"x[{worker},{task}]") - # [END variables] + # Variables + # [START variables] + x = {} + for worker in range(num_workers): + for task in range(num_tasks): + x[worker, task] = model.new_bool_var(f"x[{worker},{task}]") + # [END variables] - # Constraints - # [START constraints] - # Each worker is assigned to at most one task. - for worker in range(num_workers): - model.add( - sum(task_sizes[task] * x[worker, task] for task in range(num_tasks)) - <= total_size_max - ) + # Constraints + # [START constraints] + # Each worker is assigned to at most one task. + for worker in range(num_workers): + model.add( + sum(task_sizes[task] * x[worker, task] for task in range(num_tasks)) + <= total_size_max + ) - # Each task is assigned to exactly one worker. - for task in range(num_tasks): - model.add_exactly_one(x[worker, task] for worker in range(num_workers)) - # [END constraints] + # Each task is assigned to exactly one worker. + for task in range(num_tasks): + model.add_exactly_one(x[worker, task] for worker in range(num_workers)) + # [END constraints] - # Objective - # [START objective] - objective_terms = [] - for worker in range(num_workers): - for task in range(num_tasks): - objective_terms.append(costs[worker][task] * x[worker, task]) - model.minimize(sum(objective_terms)) - # [END objective] + # Objective + # [START objective] + objective_terms = [] + for worker in range(num_workers): + for task in range(num_tasks): + objective_terms.append(costs[worker][task] * x[worker, task]) + model.minimize(sum(objective_terms)) + # [END objective] - # Solve - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] + # Solve + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] - # Print solution. - # [START print_solution] - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print(f"Total cost = {solver.objective_value}\n") - for worker in range(num_workers): - for task in range(num_tasks): - if solver.boolean_value(x[worker, task]): - print( - f"Worker {worker} assigned to task {task}." - + f" Cost = {costs[worker][task]}" - ) - else: - print("No solution found.") - # [END print_solution] + # Print solution. + # [START print_solution] + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print(f"Total cost = {solver.objective_value}\n") + for worker in range(num_workers): + for task in range(num_tasks): + if solver.boolean_value(x[worker, task]): + print( + f"Worker {worker} assigned to task {task}." + + f" Cost = {costs[worker][task]}" + ) + else: + print("No solution found.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/assignment_teams_sat.py b/ortools/sat/samples/assignment_teams_sat.py index 375087e856c..0ff4b7f2b5b 100644 --- a/ortools/sat/samples/assignment_teams_sat.py +++ b/ortools/sat/samples/assignment_teams_sat.py @@ -21,93 +21,93 @@ def main() -> None: - # Data - # [START data] - costs = [ - [90, 76, 75, 70], - [35, 85, 55, 65], - [125, 95, 90, 105], - [45, 110, 95, 115], - [60, 105, 80, 75], - [45, 65, 110, 95], - ] - num_workers = len(costs) - num_tasks = len(costs[0]) - - team1 = [0, 2, 4] - team2 = [1, 3, 5] - # Maximum total of tasks for any team - team_max = 2 - # [END data] - - # Model - # [START model] - model = cp_model.CpModel() - # [END model] - - # Variables - # [START variables] - x = {} - for worker in range(num_workers): - for task in range(num_tasks): - x[worker, task] = model.new_bool_var(f"x[{worker},{task}]") - # [END variables] - - # Constraints - # [START constraints] - # Each worker is assigned to at most one task. - for worker in range(num_workers): - model.add_at_most_one(x[worker, task] for task in range(num_tasks)) + # Data + # [START data] + costs = [ + [90, 76, 75, 70], + [35, 85, 55, 65], + [125, 95, 90, 105], + [45, 110, 95, 115], + [60, 105, 80, 75], + [45, 65, 110, 95], + ] + num_workers = len(costs) + num_tasks = len(costs[0]) + + team1 = [0, 2, 4] + team2 = [1, 3, 5] + # Maximum total of tasks for any team + team_max = 2 + # [END data] + + # Model + # [START model] + model = cp_model.CpModel() + # [END model] + + # Variables + # [START variables] + x = {} + for worker in range(num_workers): + for task in range(num_tasks): + x[worker, task] = model.new_bool_var(f"x[{worker},{task}]") + # [END variables] + + # Constraints + # [START constraints] + # Each worker is assigned to at most one task. + for worker in range(num_workers): + model.add_at_most_one(x[worker, task] for task in range(num_tasks)) + + # Each task is assigned to exactly one worker. + for task in range(num_tasks): + model.add_exactly_one(x[worker, task] for worker in range(num_workers)) + + # Each team takes at most two tasks. + team1_tasks = [] + for worker in team1: + for task in range(num_tasks): + team1_tasks.append(x[worker, task]) + model.add(sum(team1_tasks) <= team_max) - # Each task is assigned to exactly one worker. + team2_tasks = [] + for worker in team2: + for task in range(num_tasks): + team2_tasks.append(x[worker, task]) + model.add(sum(team2_tasks) <= team_max) + # [END constraints] + + # Objective + # [START objective] + objective_terms = [] + for worker in range(num_workers): for task in range(num_tasks): - model.add_exactly_one(x[worker, task] for worker in range(num_workers)) - - # Each team takes at most two tasks. - team1_tasks = [] - for worker in team1: - for task in range(num_tasks): - team1_tasks.append(x[worker, task]) - model.add(sum(team1_tasks) <= team_max) - - team2_tasks = [] - for worker in team2: - for task in range(num_tasks): - team2_tasks.append(x[worker, task]) - model.add(sum(team2_tasks) <= team_max) - # [END constraints] - - # Objective - # [START objective] - objective_terms = [] + objective_terms.append(costs[worker][task] * x[worker, task]) + model.minimize(sum(objective_terms)) + # [END objective] + + # Solve + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] + + # Print solution. + # [START print_solution] + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print(f"Total cost = {solver.objective_value}\n") for worker in range(num_workers): - for task in range(num_tasks): - objective_terms.append(costs[worker][task] * x[worker, task]) - model.minimize(sum(objective_terms)) - # [END objective] - - # Solve - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] - - # Print solution. - # [START print_solution] - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print(f"Total cost = {solver.objective_value}\n") - for worker in range(num_workers): - for task in range(num_tasks): - if solver.boolean_value(x[worker, task]): - print( - f"Worker {worker} assigned to task {task}." - + f" Cost = {costs[worker][task]}" - ) - else: - print("No solution found.") - # [END print_solution] + for task in range(num_tasks): + if solver.boolean_value(x[worker, task]): + print( + f"Worker {worker} assigned to task {task}." + + f" Cost = {costs[worker][task]}" + ) + else: + print("No solution found.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/assumptions_sample_sat.py b/ortools/sat/samples/assumptions_sample_sat.py index 62501b9b2f3..ed9a5e0d028 100644 --- a/ortools/sat/samples/assumptions_sample_sat.py +++ b/ortools/sat/samples/assumptions_sample_sat.py @@ -21,49 +21,49 @@ def main() -> None: - """Showcases assumptions.""" - # Creates the model. - # [START model] - model = cp_model.CpModel() - # [END model] + """Showcases assumptions.""" + # Creates the model. + # [START model] + model = cp_model.CpModel() + # [END model] - # Creates the variables. - # [START variables] - x = model.new_int_var(0, 10, "x") - y = model.new_int_var(0, 10, "y") - z = model.new_int_var(0, 10, "z") - a = model.new_bool_var("a") - b = model.new_bool_var("b") - c = model.new_bool_var("c") - # [END variables] + # Creates the variables. + # [START variables] + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(0, 10, "y") + z = model.new_int_var(0, 10, "z") + a = model.new_bool_var("a") + b = model.new_bool_var("b") + c = model.new_bool_var("c") + # [END variables] - # Creates the constraints. - # [START constraints] - model.add(x > y).only_enforce_if(a) - model.add(y > z).only_enforce_if(b) - model.add(z > x).only_enforce_if(c) - # [END constraints] + # Creates the constraints. + # [START constraints] + model.add(x > y).only_enforce_if(a) + model.add(y > z).only_enforce_if(b) + model.add(z > x).only_enforce_if(c) + # [END constraints] - # Add assumptions - model.add_assumptions([a, b, c]) + # Add assumptions + model.add_assumptions([a, b, c]) - # Creates a solver and solves. - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] + # Creates a solver and solves. + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] - # Print solution. - # [START print_solution] - print(f"Status = {solver.status_name(status)}") - if status == cp_model.INFEASIBLE: - print( - "sufficient_assumptions_for_infeasibility = " - f"{solver.sufficient_assumptions_for_infeasibility()}" - ) - # [END print_solution] + # Print solution. + # [START print_solution] + print(f"Status = {solver.status_name(status)}") + if status == cp_model.INFEASIBLE: + print( + "sufficient_assumptions_for_infeasibility = " + f"{solver.sufficient_assumptions_for_infeasibility()}" + ) + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/bin_packing_sat.py b/ortools/sat/samples/bin_packing_sat.py index 8477160ea34..9b01ffbc4a7 100644 --- a/ortools/sat/samples/bin_packing_sat.py +++ b/ortools/sat/samples/bin_packing_sat.py @@ -28,9 +28,9 @@ # [START program_part1] # [START data_model] def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame]: - """Create the data for the example.""" + """Create the data for the example.""" - items_str = """ + items_str = """ item weight i1 48 i2 30 @@ -45,7 +45,7 @@ def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame]: i11 30 """ - bins_str = """ + bins_str = """ bin capacity b1 100 b2 100 @@ -56,92 +56,92 @@ def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame]: b7 100 """ - items = pd.read_table(io.StringIO(items_str), index_col=0, sep=r"\s+") - bins = pd.read_table(io.StringIO(bins_str), index_col=0, sep=r"\s+") - return items, bins - # [END data_model] + items = pd.read_table(io.StringIO(items_str), index_col=0, sep=r"\s+") + bins = pd.read_table(io.StringIO(bins_str), index_col=0, sep=r"\s+") + return items, bins + # [END data_model] def main() -> None: - # [START data] - items, bins = create_data_model() - # [END data] - # [END program_part1] - - # [START model] - # Create the model. - model = cp_model.CpModel() - # [END model] - - # [START program_part2] - # [START variables] - # Variables - # x[i, j] = 1 if item i is packed in bin j. - items_x_bins = pd.MultiIndex.from_product( - [items.index, bins.index], names=["item", "bin"] + # [START data] + items, bins = create_data_model() + # [END data] + # [END program_part1] + + # [START model] + # Create the model. + model = cp_model.CpModel() + # [END model] + + # [START program_part2] + # [START variables] + # Variables + # x[i, j] = 1 if item i is packed in bin j. + items_x_bins = pd.MultiIndex.from_product( + [items.index, bins.index], names=["item", "bin"] + ) + x = model.new_bool_var_series(name="x", index=items_x_bins) + + # y[j] = 1 if bin j is used. + y = model.new_bool_var_series(name="y", index=bins.index) + # [END variables] + + # [START constraints] + # Constraints + # Each item must be in exactly one bin. + for unused_name, all_copies in x.groupby("item"): + model.add_exactly_one(x[all_copies.index]) + + # The amount packed in each bin cannot exceed its capacity. + for selected_bin in bins.index: + items_in_bin = x.xs(selected_bin, level="bin") + model.add( + items_in_bin.dot(items.weight) + <= bins.loc[selected_bin].capacity * y[selected_bin] ) - x = model.new_bool_var_series(name="x", index=items_x_bins) - - # y[j] = 1 if bin j is used. - y = model.new_bool_var_series(name="y", index=bins.index) - # [END variables] - - # [START constraints] - # Constraints - # Each item must be in exactly one bin. - for unused_name, all_copies in x.groupby("item"): - model.add_exactly_one(x[all_copies.index]) - - # The amount packed in each bin cannot exceed its capacity. - for selected_bin in bins.index: - items_in_bin = x.xs(selected_bin, level="bin") - model.add( - items_in_bin.dot(items.weight) - <= bins.loc[selected_bin].capacity * y[selected_bin] - ) - # [END constraints] - - # [START objective] - # Objective: minimize the number of bins used. - model.minimize(y.sum()) - # [END objective] - - # [START solve] - # Create the solver and solve the model. - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] - - # [START print_solution] - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print(f"Number of bins used = {solver.objective_value}") - - x_values = solver.boolean_values(x) - y_values = solver.boolean_values(y) - active_bins = y_values.loc[lambda x: x].index - - for b in active_bins: - print(f"Bin {b}") - items_in_active_bin = x_values.xs(b, level="bin").loc[lambda x: x].index - for item in items_in_active_bin: - print(f" Item {item} - weight {items.loc[item].weight}") - print( - " Packed items weight:" - f" {items.loc[items_in_active_bin].sum().to_string()}" - ) - print() - - print(f"Total packed weight: {items.weight.sum()}") - print() - print(f"Time = {solver.wall_time} seconds") - elif status == cp_model.INFEASIBLE: - print("No solution found") - else: - print("Something is wrong, check the status and the log of the solve") - # [END print_solution] + # [END constraints] + + # [START objective] + # Objective: minimize the number of bins used. + model.minimize(y.sum()) + # [END objective] + + # [START solve] + # Create the solver and solve the model. + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] + + # [START print_solution] + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print(f"Number of bins used = {solver.objective_value}") + + x_values = solver.boolean_values(x) + y_values = solver.boolean_values(y) + active_bins = y_values.loc[lambda x: x].index + + for b in active_bins: + print(f"Bin {b}") + items_in_active_bin = x_values.xs(b, level="bin").loc[lambda x: x].index + for item in items_in_active_bin: + print(f" Item {item} - weight {items.loc[item].weight}") + print( + " Packed items weight:" + f" {items.loc[items_in_active_bin].sum().to_string()}" + ) + print() + + print(f"Total packed weight: {items.weight.sum()}") + print() + print(f"Time = {solver.wall_time} seconds") + elif status == cp_model.INFEASIBLE: + print("No solution found") + else: + print("Something is wrong, check the status and the log of the solve") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program_part2] # [END program] diff --git a/ortools/sat/samples/binpacking_problem_sat.py b/ortools/sat/samples/binpacking_problem_sat.py index d1e671b3846..ee341231cda 100644 --- a/ortools/sat/samples/binpacking_problem_sat.py +++ b/ortools/sat/samples/binpacking_problem_sat.py @@ -19,62 +19,62 @@ def binpacking_problem_sat(): - """Solves a bin-packing problem using the CP-SAT solver.""" - # Data. - bin_capacity = 100 - slack_capacity = 20 - num_bins = 5 - all_bins = range(num_bins) - - items = [(20, 6), (15, 6), (30, 4), (45, 3)] - num_items = len(items) - all_items = range(num_items) - - # Model. - model = cp_model.CpModel() - - # Main variables. - x = {} - for i in all_items: - num_copies = items[i][1] - for b in all_bins: - x[(i, b)] = model.new_int_var(0, num_copies, f"x[{i},{b}]") - - # Load variables. - load = [model.new_int_var(0, bin_capacity, f"load[{b}]") for b in all_bins] - - # Slack variables. - slacks = [model.new_bool_var(f"slack[{b}]") for b in all_bins] - - # Links load and x. + """Solves a bin-packing problem using the CP-SAT solver.""" + # Data. + bin_capacity = 100 + slack_capacity = 20 + num_bins = 5 + all_bins = range(num_bins) + + items = [(20, 6), (15, 6), (30, 4), (45, 3)] + num_items = len(items) + all_items = range(num_items) + + # Model. + model = cp_model.CpModel() + + # Main variables. + x = {} + for i in all_items: + num_copies = items[i][1] for b in all_bins: - model.add(load[b] == sum(x[(i, b)] * items[i][0] for i in all_items)) - - # Place all items. - for i in all_items: - model.add(sum(x[(i, b)] for b in all_bins) == items[i][1]) - - # Links load and slack through an equivalence relation. - safe_capacity = bin_capacity - slack_capacity - for b in all_bins: - # slack[b] => load[b] <= safe_capacity. - model.add(load[b] <= safe_capacity).only_enforce_if(slacks[b]) - # not(slack[b]) => load[b] > safe_capacity. - model.add(load[b] > safe_capacity).only_enforce_if(~slacks[b]) - - # Maximize sum of slacks. - model.maximize(sum(slacks)) - - # Solves and prints out the solution. - solver = cp_model.CpSolver() - status = solver.solve(model) - print(f"solve status: {solver.status_name(status)}") - if status == cp_model.OPTIMAL: - print(f"Optimal objective value: {solver.objective_value}") - print("Statistics") - print(f" - conflicts : {solver.num_conflicts}") - print(f" - branches : {solver.num_branches}") - print(f" - wall time : {solver.wall_time}s") + x[(i, b)] = model.new_int_var(0, num_copies, f"x[{i},{b}]") + + # Load variables. + load = [model.new_int_var(0, bin_capacity, f"load[{b}]") for b in all_bins] + + # Slack variables. + slacks = [model.new_bool_var(f"slack[{b}]") for b in all_bins] + + # Links load and x. + for b in all_bins: + model.add(load[b] == sum(x[(i, b)] * items[i][0] for i in all_items)) + + # Place all items. + for i in all_items: + model.add(sum(x[(i, b)] for b in all_bins) == items[i][1]) + + # Links load and slack through an equivalence relation. + safe_capacity = bin_capacity - slack_capacity + for b in all_bins: + # slack[b] => load[b] <= safe_capacity. + model.add(load[b] <= safe_capacity).only_enforce_if(slacks[b]) + # not(slack[b]) => load[b] > safe_capacity. + model.add(load[b] > safe_capacity).only_enforce_if(~slacks[b]) + + # Maximize sum of slacks. + model.maximize(sum(slacks)) + + # Solves and prints out the solution. + solver = cp_model.CpSolver() + status = solver.solve(model) + print(f"solve status: {solver.status_name(status)}") + if status == cp_model.OPTIMAL: + print(f"Optimal objective value: {solver.objective_value}") + print("Statistics") + print(f" - conflicts : {solver.num_conflicts}") + print(f" - branches : {solver.num_branches}") + print(f" - wall time : {solver.wall_time}s") binpacking_problem_sat() diff --git a/ortools/sat/samples/bool_and_int_var_product_sample_sat.py b/ortools/sat/samples/bool_and_int_var_product_sample_sat.py index 5b1144e644e..f7797b83660 100644 --- a/ortools/sat/samples/bool_and_int_var_product_sample_sat.py +++ b/ortools/sat/samples/bool_and_int_var_product_sample_sat.py @@ -18,56 +18,56 @@ class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables + def __init__(self, variables: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables - def on_solution_callback(self) -> None: - for v in self.__variables: - print(f"{v}={self.value(v)}", end=" ") - print() + def on_solution_callback(self) -> None: + for v in self.__variables: + print(f"{v}={self.value(v)}", end=" ") + print() def build_product_var( model: cp_model.CpModel, b: cp_model.IntVar, x: cp_model.IntVar, name: str ) -> cp_model.IntVar: - """Builds the product of a Boolean variable and an integer variable.""" - p = model.new_int_var_from_domain( - cp_model.Domain.from_flat_intervals(x.proto.domain).union_with( - cp_model.Domain(0, 0) - ), - name, - ) - model.add(p == x).only_enforce_if(b) - model.add(p == 0).only_enforce_if(~b) - return p + """Builds the product of a Boolean variable and an integer variable.""" + p = model.new_int_var_from_domain( + cp_model.Domain.from_flat_intervals(x.proto.domain).union_with( + cp_model.Domain(0, 0) + ), + name, + ) + model.add(p == x).only_enforce_if(b) + model.add(p == 0).only_enforce_if(~b) + return p def bool_and_int_var_product_sample_sat(): - """Encoding of the product of two Boolean variables. + """Encoding of the product of two Boolean variables. - p == x * y, which is the same as p <=> x and y - """ - model = cp_model.CpModel() - b = model.new_bool_var("b") - x = model.new_int_var_from_domain( - cp_model.Domain.from_values([1, 2, 3, 5, 6, 7, 9, 10]), "x" - ) - p = build_product_var(model, b, x, "p") + p == x * y, which is the same as p <=> x and y + """ + model = cp_model.CpModel() + b = model.new_bool_var("b") + x = model.new_int_var_from_domain( + cp_model.Domain.from_values([1, 2, 3, 5, 6, 7, 9, 10]), "x" + ) + p = build_product_var(model, b, x, "p") - # Search for x and b values in increasing order. - model.add_decision_strategy( - [b, x], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE - ) + # Search for x and b values in increasing order. + model.add_decision_strategy( + [b, x], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE + ) - # Create a solver and solve. - solver = cp_model.CpSolver() - solution_printer = VarArraySolutionPrinter([x, b, p]) - solver.parameters.enumerate_all_solutions = True - solver.parameters.search_branching = cp_model.FIXED_SEARCH - solver.solve(model, solution_printer) + # Create a solver and solve. + solver = cp_model.CpSolver() + solution_printer = VarArraySolutionPrinter([x, b, p]) + solver.parameters.enumerate_all_solutions = True + solver.parameters.search_branching = cp_model.FIXED_SEARCH + solver.solve(model, solution_printer) bool_and_int_var_product_sample_sat() diff --git a/ortools/sat/samples/bool_or_sample_sat.py b/ortools/sat/samples/bool_or_sample_sat.py index bb7ea1362fb..0a8f620a478 100644 --- a/ortools/sat/samples/bool_or_sample_sat.py +++ b/ortools/sat/samples/bool_or_sample_sat.py @@ -19,15 +19,15 @@ def bool_or_sample_sat(): - model = cp_model.CpModel() + model = cp_model.CpModel() - x = model.new_bool_var("x") - y = model.new_bool_var("y") + x = model.new_bool_var("x") + y = model.new_bool_var("y") - model.add_bool_or([x, y.negated()]) - # The [] is not mandatory. - # ~y is equivalent to y.negated() - model.add_bool_or(x, ~y) + model.add_bool_or([x, y.negated()]) + # The [] is not mandatory. + # ~y is equivalent to y.negated() + model.add_bool_or(x, ~y) bool_or_sample_sat() diff --git a/ortools/sat/samples/boolean_product_sample_sat.py b/ortools/sat/samples/boolean_product_sample_sat.py index 171fd27a094..555dca40afa 100644 --- a/ortools/sat/samples/boolean_product_sample_sat.py +++ b/ortools/sat/samples/boolean_product_sample_sat.py @@ -19,27 +19,27 @@ def boolean_product_sample_sat(): - """Encoding of the product of two Boolean variables. - - p == x * y, which is the same as p <=> x and y - """ - model = cp_model.CpModel() - x = model.new_bool_var("x") - y = model.new_bool_var("y") - p = model.new_bool_var("p") - - # x and y implies p, rewrite as not(x and y) or p. - model.add_bool_or(~x, ~y, p) - - # p implies x and y, expanded into two implications. - model.add_implication(p, x) - model.add_implication(p, y) - - # Create a solver and solve. - solver = cp_model.CpSolver() - solution_printer = cp_model.VarArraySolutionPrinter([x, y, p]) - solver.parameters.enumerate_all_solutions = True - solver.solve(model, solution_printer) + """Encoding of the product of two Boolean variables. + + p == x * y, which is the same as p <=> x and y + """ + model = cp_model.CpModel() + x = model.new_bool_var("x") + y = model.new_bool_var("y") + p = model.new_bool_var("p") + + # x and y implies p, rewrite as not(x and y) or p. + model.add_bool_or(~x, ~y, p) + + # p implies x and y, expanded into two implications. + model.add_implication(p, x) + model.add_implication(p, y) + + # Create a solver and solve. + solver = cp_model.CpSolver() + solution_printer = cp_model.VarArraySolutionPrinter([x, y, p]) + solver.parameters.enumerate_all_solutions = True + solver.solve(model, solution_printer) boolean_product_sample_sat() diff --git a/ortools/sat/samples/channeling_sample_sat.py b/ortools/sat/samples/channeling_sample_sat.py index aecbad65f92..db8f9c1aa52 100644 --- a/ortools/sat/samples/channeling_sample_sat.py +++ b/ortools/sat/samples/channeling_sample_sat.py @@ -19,55 +19,57 @@ class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables + def __init__(self, variables: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables - def on_solution_callback(self) -> None: - for v in self.__variables: - print(f"{v}={self.value(v)}", end=" ") - print() + def on_solution_callback(self) -> None: + for v in self.__variables: + print(f"{v}={self.value(v)}", end=" ") + print() def channeling_sample_sat(): - """Demonstrates how to link integer constraints together.""" + """Demonstrates how to link integer constraints together.""" - # Create the CP-SAT model. - model = cp_model.CpModel() + # Create the CP-SAT model. + model = cp_model.CpModel() - # Declare our two primary variables. - x = model.new_int_var(0, 10, "x") - y = model.new_int_var(0, 10, "y") + # Declare our two primary variables. + x = model.new_int_var(0, 10, "x") + y = model.new_int_var(0, 10, "y") - # Declare our intermediate boolean variable. - b = model.new_bool_var("b") + # Declare our intermediate boolean variable. + b = model.new_bool_var("b") - # Implement b == (x >= 5). - model.add(x >= 5).only_enforce_if(b) - model.add(x < 5).only_enforce_if(~b) + # Implement b == (x >= 5). + model.add(x >= 5).only_enforce_if(b) + model.add(x < 5).only_enforce_if(~b) - # Create our two half-reified constraints. - # First, b implies (y == 10 - x). - model.add(y == 10 - x).only_enforce_if(b) - # Second, not(b) implies y == 0. - model.add(y == 0).only_enforce_if(~b) + # Create our two half-reified constraints. + # First, b implies (y == 10 - x). + model.add(y == 10 - x).only_enforce_if(b) + # Second, not(b) implies y == 0. + model.add(y == 0).only_enforce_if(~b) - # Search for x values in increasing order. - model.add_decision_strategy([x], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE) + # Search for x values in increasing order. + model.add_decision_strategy( + [x], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE + ) - # Create a solver and solve with a fixed search. - solver = cp_model.CpSolver() + # Create a solver and solve with a fixed search. + solver = cp_model.CpSolver() - # Force the solver to follow the decision strategy exactly. - solver.parameters.search_branching = cp_model.FIXED_SEARCH - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True + # Force the solver to follow the decision strategy exactly. + solver.parameters.search_branching = cp_model.FIXED_SEARCH + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True - # Search and print out all solutions. - solution_printer = VarArraySolutionPrinter([x, y, b]) - solver.solve(model, solution_printer) + # Search and print out all solutions. + solution_printer = VarArraySolutionPrinter([x, y, b]) + solver.solve(model, solution_printer) channeling_sample_sat() diff --git a/ortools/sat/samples/clone_model_sample_sat.py b/ortools/sat/samples/clone_model_sample_sat.py index 6f32e4cb484..68121bcd47f 100644 --- a/ortools/sat/samples/clone_model_sample_sat.py +++ b/ortools/sat/samples/clone_model_sample_sat.py @@ -21,61 +21,65 @@ def clone_model_sample_sat(): - """Showcases cloning a model.""" - # Creates the model. - # [START model] - model = cp_model.CpModel() - # [END model] - - # Creates the variables. - # [START variables] - num_vals = 3 - x = model.new_int_var(0, num_vals - 1, "x") - y = model.new_int_var(0, num_vals - 1, "y") - z = model.new_int_var(0, num_vals - 1, "z") - # [END variables] - - # Creates the constraints. - # [START constraints] - model.add(x != y) - # [END constraints] - - # [START objective] - model.maximize(x + 2 * y + 3 * z) - # [END objective] - - # Creates a solver and solves. - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] - - if status == cp_model.OPTIMAL: - print("Optimal value of the original model: {}".format(solver.objective_value)) - - # [START clone] - # Creates a dictionary holding the model and the variables you want to use. - to_clone = { - "model": model, - "x": x, - "y": y, - "z": z, - } - - # Deep copy the dictionary. - clone = copy.deepcopy(to_clone) - - # Retrieve the cloned model and variables. - cloned_model: cp_model.CpModel = clone["model"] - cloned_x = clone["x"] - cloned_y = clone["y"] - cloned_model.add(cloned_x + cloned_y <= 1) - # [END clone] - - status = solver.solve(cloned_model) - - if status == cp_model.OPTIMAL: - print("Optimal value of the modified model: {}".format(solver.objective_value)) + """Showcases cloning a model.""" + # Creates the model. + # [START model] + model = cp_model.CpModel() + # [END model] + + # Creates the variables. + # [START variables] + num_vals = 3 + x = model.new_int_var(0, num_vals - 1, "x") + y = model.new_int_var(0, num_vals - 1, "y") + z = model.new_int_var(0, num_vals - 1, "z") + # [END variables] + + # Creates the constraints. + # [START constraints] + model.add(x != y) + # [END constraints] + + # [START objective] + model.maximize(x + 2 * y + 3 * z) + # [END objective] + + # Creates a solver and solves. + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] + + if status == cp_model.OPTIMAL: + print( + "Optimal value of the original model: {}".format(solver.objective_value) + ) + + # [START clone] + # Creates a dictionary holding the model and the variables you want to use. + to_clone = { + "model": model, + "x": x, + "y": y, + "z": z, + } + + # Deep copy the dictionary. + clone = copy.deepcopy(to_clone) + + # Retrieve the cloned model and variables. + cloned_model: cp_model.CpModel = clone["model"] + cloned_x = clone["x"] + cloned_y = clone["y"] + cloned_model.add(cloned_x + cloned_y <= 1) + # [END clone] + + status = solver.solve(cloned_model) + + if status == cp_model.OPTIMAL: + print( + "Optimal value of the modified model: {}".format(solver.objective_value) + ) clone_model_sample_sat() diff --git a/ortools/sat/samples/cp_is_fun_sat.py b/ortools/sat/samples/cp_is_fun_sat.py index 7a8aeaedc02..5db99c40686 100644 --- a/ortools/sat/samples/cp_is_fun_sat.py +++ b/ortools/sat/samples/cp_is_fun_sat.py @@ -28,85 +28,85 @@ # [START solution_printer] class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables - self.__solution_count = 0 + def __init__(self, variables: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables + self.__solution_count = 0 - def on_solution_callback(self) -> None: - self.__solution_count += 1 - for v in self.__variables: - print(f"{v}={self.value(v)}", end=" ") - print() + def on_solution_callback(self) -> None: + self.__solution_count += 1 + for v in self.__variables: + print(f"{v}={self.value(v)}", end=" ") + print() - @property - def solution_count(self) -> int: - return self.__solution_count - # [END solution_printer] + @property + def solution_count(self) -> int: + return self.__solution_count + # [END solution_printer] def main() -> None: - """solve the CP+IS+FUN==TRUE cryptarithm.""" - # Constraint programming engine - # [START model] - model = cp_model.CpModel() - # [END model] - - # [START variables] - base = 10 - - c = model.new_int_var(1, base - 1, "C") - p = model.new_int_var(0, base - 1, "P") - i = model.new_int_var(1, base - 1, "I") - s = model.new_int_var(0, base - 1, "S") - f = model.new_int_var(1, base - 1, "F") - u = model.new_int_var(0, base - 1, "U") - n = model.new_int_var(0, base - 1, "N") - t = model.new_int_var(1, base - 1, "T") - r = model.new_int_var(0, base - 1, "R") - e = model.new_int_var(0, base - 1, "E") - - # We need to group variables in a list to use the constraint AllDifferent. - letters = [c, p, i, s, f, u, n, t, r, e] - - # Verify that we have enough digits. - assert base >= len(letters) - # [END variables] - - # Define constraints. - # [START constraints] - model.add_all_different(letters) - - # CP + IS + FUN = TRUE - model.add( - c * base + p + i * base + s + f * base * base + u * base + n - == t * base * base * base + r * base * base + u * base + e - ) - # [END constraints] - - # Creates a solver and solves the model. - # [START solve] - solver = cp_model.CpSolver() - solution_printer = VarArraySolutionPrinter(letters) - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - # Solve. - status = solver.solve(model, solution_printer) - # [END solve] - - # Statistics. - # [START statistics] - print("\nStatistics") - print(f" status : {solver.status_name(status)}") - print(f" conflicts: {solver.num_conflicts}") - print(f" branches : {solver.num_branches}") - print(f" wall time: {solver.wall_time} s") - print(f" sol found: {solution_printer.solution_count}") - # [END statistics] + """solve the CP+IS+FUN==TRUE cryptarithm.""" + # Constraint programming engine + # [START model] + model = cp_model.CpModel() + # [END model] + + # [START variables] + base = 10 + + c = model.new_int_var(1, base - 1, "C") + p = model.new_int_var(0, base - 1, "P") + i = model.new_int_var(1, base - 1, "I") + s = model.new_int_var(0, base - 1, "S") + f = model.new_int_var(1, base - 1, "F") + u = model.new_int_var(0, base - 1, "U") + n = model.new_int_var(0, base - 1, "N") + t = model.new_int_var(1, base - 1, "T") + r = model.new_int_var(0, base - 1, "R") + e = model.new_int_var(0, base - 1, "E") + + # We need to group variables in a list to use the constraint AllDifferent. + letters = [c, p, i, s, f, u, n, t, r, e] + + # Verify that we have enough digits. + assert base >= len(letters) + # [END variables] + + # Define constraints. + # [START constraints] + model.add_all_different(letters) + + # CP + IS + FUN = TRUE + model.add( + c * base + p + i * base + s + f * base * base + u * base + n + == t * base * base * base + r * base * base + u * base + e + ) + # [END constraints] + + # Creates a solver and solves the model. + # [START solve] + solver = cp_model.CpSolver() + solution_printer = VarArraySolutionPrinter(letters) + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + # Solve. + status = solver.solve(model, solution_printer) + # [END solve] + + # Statistics. + # [START statistics] + print("\nStatistics") + print(f" status : {solver.status_name(status)}") + print(f" conflicts: {solver.num_conflicts}") + print(f" branches : {solver.num_branches}") + print(f" wall time: {solver.wall_time} s") + print(f" sol found: {solution_printer.solution_count}") + # [END statistics] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/cp_sat_example.py b/ortools/sat/samples/cp_sat_example.py index f7f68b23659..86f34976ac1 100755 --- a/ortools/sat/samples/cp_sat_example.py +++ b/ortools/sat/samples/cp_sat_example.py @@ -21,57 +21,57 @@ def main() -> None: - """Minimal CP-SAT example to showcase calling the solver.""" - # Creates the model. - # [START model] - model = cp_model.CpModel() - # [END model] + """Minimal CP-SAT example to showcase calling the solver.""" + # Creates the model. + # [START model] + model = cp_model.CpModel() + # [END model] - # Creates the variables. - # [START variables] - var_upper_bound = max(50, 45, 37) - x = model.new_int_var(0, var_upper_bound, "x") - y = model.new_int_var(0, var_upper_bound, "y") - z = model.new_int_var(0, var_upper_bound, "z") - # [END variables] + # Creates the variables. + # [START variables] + var_upper_bound = max(50, 45, 37) + x = model.new_int_var(0, var_upper_bound, "x") + y = model.new_int_var(0, var_upper_bound, "y") + z = model.new_int_var(0, var_upper_bound, "z") + # [END variables] - # Creates the constraints. - # [START constraints] - model.add(2 * x + 7 * y + 3 * z <= 50) - model.add(3 * x - 5 * y + 7 * z <= 45) - model.add(5 * x + 2 * y - 6 * z <= 37) - # [END constraints] + # Creates the constraints. + # [START constraints] + model.add(2 * x + 7 * y + 3 * z <= 50) + model.add(3 * x - 5 * y + 7 * z <= 45) + model.add(5 * x + 2 * y - 6 * z <= 37) + # [END constraints] - # [START objective] - model.maximize(2 * x + 2 * y + 3 * z) - # [END objective] + # [START objective] + model.maximize(2 * x + 2 * y + 3 * z) + # [END objective] - # Creates a solver and solves the model. - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] + # Creates a solver and solves the model. + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] - # [START print_solution] - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print(f"Maximum of objective function: {solver.objective_value}\n") - print(f"x = {solver.value(x)}") - print(f"y = {solver.value(y)}") - print(f"z = {solver.value(z)}") - else: - print("No solution found.") - # [END print_solution] + # [START print_solution] + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print(f"Maximum of objective function: {solver.objective_value}\n") + print(f"x = {solver.value(x)}") + print(f"y = {solver.value(y)}") + print(f"z = {solver.value(z)}") + else: + print("No solution found.") + # [END print_solution] - # Statistics. - # [START statistics] - print("\nStatistics") - print(f" status : {solver.status_name(status)}") - print(f" conflicts: {solver.num_conflicts}") - print(f" branches : {solver.num_branches}") - print(f" wall time: {solver.wall_time} s") - # [END statistics] + # Statistics. + # [START statistics] + print("\nStatistics") + print(f" status : {solver.status_name(status)}") + print(f" conflicts: {solver.num_conflicts}") + print(f" branches : {solver.num_branches}") + print(f" wall time: {solver.wall_time} s") + # [END statistics] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/cumulative_variable_profile_sample_sat.py b/ortools/sat/samples/cumulative_variable_profile_sample_sat.py index d03bdec0aff..e88f76e8667 100644 --- a/ortools/sat/samples/cumulative_variable_profile_sample_sat.py +++ b/ortools/sat/samples/cumulative_variable_profile_sample_sat.py @@ -23,9 +23,9 @@ def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: - """Creates the dataframes that describes the model.""" + """Creates the dataframes that describes the model.""" - max_load_str: str = """ + max_load_str: str = """ start_hour max_load 0 0 2 0 @@ -41,7 +41,7 @@ def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: 22 0 """ - min_load_str: str = """ + min_load_str: str = """ start_hour min_load 0 0 2 0 @@ -57,7 +57,7 @@ def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: 22 0 """ - tasks_str: str = """ + tasks_str: str = """ name duration load priority t1 60 3 2 t2 180 2 1 @@ -91,11 +91,11 @@ def create_data_model() -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]: t30 90 4 2 """ - max_load_df = pd.read_table(io.StringIO(max_load_str), sep=r"\s+") - min_load_df = pd.read_table(io.StringIO(min_load_str), sep=r"\s+") - tasks_df = pd.read_table(io.StringIO(tasks_str), index_col=0, sep=r"\s+") - return max_load_df, min_load_df, tasks_df - # [END data_model] + max_load_df = pd.read_table(io.StringIO(max_load_str), sep=r"\s+") + min_load_df = pd.read_table(io.StringIO(min_load_str), sep=r"\s+") + tasks_df = pd.read_table(io.StringIO(tasks_str), index_col=0, sep=r"\s+") + return max_load_df, min_load_df, tasks_df + # [END data_model] def check_solution( @@ -105,179 +105,181 @@ def check_solution( period_length: int, horizon: int, ) -> bool: - """Checks the solution validity against the min and max load constraints.""" - minutes_per_hour = 60 - actual_load_profile = [0 for _ in range(horizon)] - min_load_profile = [0 for _ in range(horizon)] - max_load_profile = [0 for _ in range(horizon)] - - # The complexity of the checker is linear in the number of time points, and - # should be improved. - for task in tasks: - for t in range(task[1]): - actual_load_profile[task[0] + t] += task[2] - for row in max_load_df.itertuples(): - for t in range(period_length): - max_load_profile[row.start_hour * minutes_per_hour + t] = row.max_load - for row in min_load_df.itertuples(): - for t in range(period_length): - min_load_profile[row.start_hour * minutes_per_hour + t] = row.min_load - - for time in range(horizon): - if actual_load_profile[time] > max_load_profile[time]: - print( - f"actual load {actual_load_profile[time]} at time {time} is greater" - f" than max load {max_load_profile[time]}" - ) - return False - if actual_load_profile[time] < min_load_profile[time]: - print( - f"actual load {actual_load_profile[time]} at time {time} is" - f" less than min load {min_load_profile[time]}" - ) - return False - return True + """Checks the solution validity against the min and max load constraints.""" + minutes_per_hour = 60 + actual_load_profile = [0 for _ in range(horizon)] + min_load_profile = [0 for _ in range(horizon)] + max_load_profile = [0 for _ in range(horizon)] + + # The complexity of the checker is linear in the number of time points, and + # should be improved. + for task in tasks: + for t in range(task[1]): + actual_load_profile[task[0] + t] += task[2] + for row in max_load_df.itertuples(): + for t in range(period_length): + max_load_profile[row.start_hour * minutes_per_hour + t] = row.max_load + for row in min_load_df.itertuples(): + for t in range(period_length): + min_load_profile[row.start_hour * minutes_per_hour + t] = row.min_load + + for time in range(horizon): + if actual_load_profile[time] > max_load_profile[time]: + print( + f"actual load {actual_load_profile[time]} at time {time} is greater" + f" than max load {max_load_profile[time]}" + ) + return False + if actual_load_profile[time] < min_load_profile[time]: + print( + f"actual load {actual_load_profile[time]} at time {time} is" + f" less than min load {min_load_profile[time]}" + ) + return False + return True def main(_) -> None: - """Create the model and solves it.""" - max_load_df, min_load_df, tasks_df = create_data_model() - - # Create the model. - model = cp_model.CpModel() - - # Get the max capacity from the capacity dataframe. - max_load = max_load_df.max_load.max() - print(f"Max capacity = {max_load}") - print(f"#tasks = {len(tasks_df)}") - - minutes_per_hour: int = 60 - horizon: int = 24 * 60 - - # Variables - starts = model.new_int_var_series( - name="starts", - lower_bounds=0, - upper_bounds=horizon - tasks_df.duration, - index=tasks_df.index, - ) - performed = model.new_bool_var_series(name="performed", index=tasks_df.index) - - intervals = model.new_optional_fixed_size_interval_var_series( - name="intervals", - index=tasks_df.index, - starts=starts, - sizes=tasks_df.duration, - are_present=performed, - ) - - # Set up the max profile. We use fixed (intervals, demands) to fill in the - # space between the actual max load profile and the max capacity. - time_period_max_intervals = model.new_fixed_size_interval_var_series( - name="time_period_max_intervals", - index=max_load_df.index, - starts=max_load_df.start_hour * minutes_per_hour, - sizes=minutes_per_hour * 2, - ) - time_period_max_heights = max_load - max_load_df.max_load - - # Cumulative constraint for the max profile. - model.add_cumulative( - intervals.to_list() + time_period_max_intervals.to_list(), - tasks_df.load.to_list() + time_period_max_heights.to_list(), - max_load, - ) - - # Set up complemented intervals (from 0 to start, and from start + size to - # horizon). - prefix_intervals = model.new_optional_interval_var_series( - name="prefix_intervals", - index=tasks_df.index, - starts=0, - sizes=starts, - ends=starts, - are_present=performed, - ) - - suffix_intervals = model.new_optional_interval_var_series( - name="suffix_intervals", - index=tasks_df.index, - starts=starts + tasks_df.duration, - sizes=horizon - starts - tasks_df.duration, - ends=horizon, - are_present=performed, - ) - - # Set up the min profile. We use complemented intervals to maintain the - # complement of the work load, and fixed intervals to enforce the min - # number of active workers per time period. - # - # Note that this works only if the max load cumulative is also added to the - # model. - time_period_min_intervals = model.new_fixed_size_interval_var_series( - name="time_period_min_intervals", - index=min_load_df.index, - starts=min_load_df.start_hour * minutes_per_hour, - sizes=minutes_per_hour * 2, - ) - time_period_min_heights = min_load_df.min_load - - # We take into account optional intervals. The actual capacity of the min load - # cumulative is the sum of all the active demands. - sum_of_demands = sum(tasks_df.load) - complement_capacity = model.new_int_var(0, sum_of_demands, "complement_capacity") - model.add(complement_capacity == performed.dot(tasks_df.load)) - - # Cumulative constraint for the min profile. - model.add_cumulative( - prefix_intervals.to_list() - + suffix_intervals.to_list() - + time_period_min_intervals.to_list(), - tasks_df.load.to_list() - + tasks_df.load.to_list() - + time_period_min_heights.to_list(), - complement_capacity, - ) - - # Objective: maximize the value of performed intervals. - # 1 is the max priority. - max_priority = max(tasks_df.priority) - model.maximize(sum(performed * (max_priority + 1 - tasks_df.priority))) - - # Create the solver and solve the model. - solver = cp_model.CpSolver() - # solver.parameters.log_search_progress = True # Uncomment to see the logs. - solver.parameters.num_workers = 16 - solver.parameters.max_time_in_seconds = 30.0 - status = solver.solve(model) - - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - start_values = solver.values(starts) - performed_values = solver.boolean_values(performed) - tasks: list[tuple[int, int, int]] = [] - for task in tasks_df.index: - if performed_values[task]: - print( - f'task {task} duration={tasks_df["duration"][task]} ' - f'load={tasks_df["load"][task]} starts at {start_values[task]}' - ) - tasks.append( - (start_values[task], tasks_df.duration[task], tasks_df.load[task]) - ) - else: - print(f"task {task} is not performed") - assert check_solution( - tasks=tasks, - min_load_df=min_load_df, - max_load_df=max_load_df, - period_length=2 * minutes_per_hour, - horizon=horizon, + """Create the model and solves it.""" + max_load_df, min_load_df, tasks_df = create_data_model() + + # Create the model. + model = cp_model.CpModel() + + # Get the max capacity from the capacity dataframe. + max_load = max_load_df.max_load.max() + print(f"Max capacity = {max_load}") + print(f"#tasks = {len(tasks_df)}") + + minutes_per_hour: int = 60 + horizon: int = 24 * 60 + + # Variables + starts = model.new_int_var_series( + name="starts", + lower_bounds=0, + upper_bounds=horizon - tasks_df.duration, + index=tasks_df.index, + ) + performed = model.new_bool_var_series(name="performed", index=tasks_df.index) + + intervals = model.new_optional_fixed_size_interval_var_series( + name="intervals", + index=tasks_df.index, + starts=starts, + sizes=tasks_df.duration, + are_present=performed, + ) + + # Set up the max profile. We use fixed (intervals, demands) to fill in the + # space between the actual max load profile and the max capacity. + time_period_max_intervals = model.new_fixed_size_interval_var_series( + name="time_period_max_intervals", + index=max_load_df.index, + starts=max_load_df.start_hour * minutes_per_hour, + sizes=minutes_per_hour * 2, + ) + time_period_max_heights = max_load - max_load_df.max_load + + # Cumulative constraint for the max profile. + model.add_cumulative( + intervals.to_list() + time_period_max_intervals.to_list(), + tasks_df.load.to_list() + time_period_max_heights.to_list(), + max_load, + ) + + # Set up complemented intervals (from 0 to start, and from start + size to + # horizon). + prefix_intervals = model.new_optional_interval_var_series( + name="prefix_intervals", + index=tasks_df.index, + starts=0, + sizes=starts, + ends=starts, + are_present=performed, + ) + + suffix_intervals = model.new_optional_interval_var_series( + name="suffix_intervals", + index=tasks_df.index, + starts=starts + tasks_df.duration, + sizes=horizon - starts - tasks_df.duration, + ends=horizon, + are_present=performed, + ) + + # Set up the min profile. We use complemented intervals to maintain the + # complement of the work load, and fixed intervals to enforce the min + # number of active workers per time period. + # + # Note that this works only if the max load cumulative is also added to the + # model. + time_period_min_intervals = model.new_fixed_size_interval_var_series( + name="time_period_min_intervals", + index=min_load_df.index, + starts=min_load_df.start_hour * minutes_per_hour, + sizes=minutes_per_hour * 2, + ) + time_period_min_heights = min_load_df.min_load + + # We take into account optional intervals. The actual capacity of the min load + # cumulative is the sum of all the active demands. + sum_of_demands = sum(tasks_df.load) + complement_capacity = model.new_int_var( + 0, sum_of_demands, "complement_capacity" + ) + model.add(complement_capacity == performed.dot(tasks_df.load)) + + # Cumulative constraint for the min profile. + model.add_cumulative( + prefix_intervals.to_list() + + suffix_intervals.to_list() + + time_period_min_intervals.to_list(), + tasks_df.load.to_list() + + tasks_df.load.to_list() + + time_period_min_heights.to_list(), + complement_capacity, + ) + + # Objective: maximize the value of performed intervals. + # 1 is the max priority. + max_priority = max(tasks_df.priority) + model.maximize(sum(performed * (max_priority + 1 - tasks_df.priority))) + + # Create the solver and solve the model. + solver = cp_model.CpSolver() + # solver.parameters.log_search_progress = True # Uncomment to see the logs. + solver.parameters.num_workers = 16 + solver.parameters.max_time_in_seconds = 30.0 + status = solver.solve(model) + + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + start_values = solver.values(starts) + performed_values = solver.boolean_values(performed) + tasks: list[tuple[int, int, int]] = [] + for task in tasks_df.index: + if performed_values[task]: + print( + f'task {task} duration={tasks_df["duration"][task]} ' + f'load={tasks_df["load"][task]} starts at {start_values[task]}' ) - elif status == cp_model.INFEASIBLE: - print("No solution found") - else: - print("Something is wrong, check the status and the log of the solve") + tasks.append( + (start_values[task], tasks_df.duration[task], tasks_df.load[task]) + ) + else: + print(f"task {task} is not performed") + assert check_solution( + tasks=tasks, + min_load_df=min_load_df, + max_load_df=max_load_df, + period_length=2 * minutes_per_hour, + horizon=horizon, + ) + elif status == cp_model.INFEASIBLE: + print("No solution found") + else: + print("Something is wrong, check the status and the log of the solve") if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/sat/samples/earliness_tardiness_cost_sample_sat.py b/ortools/sat/samples/earliness_tardiness_cost_sample_sat.py index 2d90faf45ae..a2e0303d4e1 100644 --- a/ortools/sat/samples/earliness_tardiness_cost_sample_sat.py +++ b/ortools/sat/samples/earliness_tardiness_cost_sample_sat.py @@ -19,69 +19,71 @@ class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables + def __init__(self, variables: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables - def on_solution_callback(self) -> None: - for v in self.__variables: - print(f"{v}={self.value(v)}", end=" ") - print() + def on_solution_callback(self) -> None: + for v in self.__variables: + print(f"{v}={self.value(v)}", end=" ") + print() def earliness_tardiness_cost_sample_sat(): - """Encode the piecewise linear expression.""" - - earliness_date = 5 # ed. - earliness_cost = 8 - lateness_date = 15 # ld. - lateness_cost = 12 - - # Model. - model = cp_model.CpModel() - - # Declare our primary variable. - x = model.new_int_var(0, 20, "x") - - # Create the expression variable and implement the piecewise linear function. - # - # \ / - # \______/ - # ed ld - # - large_constant = 1000 - expr = model.new_int_var(0, large_constant, "expr") - - # First segment. - s1 = model.new_int_var(-large_constant, large_constant, "s1") - model.add(s1 == earliness_cost * (earliness_date - x)) - - # Second segment. - s2 = 0 - - # Third segment. - s3 = model.new_int_var(-large_constant, large_constant, "s3") - model.add(s3 == lateness_cost * (x - lateness_date)) - - # Link together expr and x through s1, s2, and s3. - model.add_max_equality(expr, [s1, s2, s3]) - - # Search for x values in increasing order. - model.add_decision_strategy([x], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE) - - # Create a solver and solve with a fixed search. - solver = cp_model.CpSolver() - - # Force the solver to follow the decision strategy exactly. - solver.parameters.search_branching = cp_model.FIXED_SEARCH - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - - # Search and print out all solutions. - solution_printer = VarArraySolutionPrinter([x, expr]) - solver.solve(model, solution_printer) + """Encode the piecewise linear expression.""" + + earliness_date = 5 # ed. + earliness_cost = 8 + lateness_date = 15 # ld. + lateness_cost = 12 + + # Model. + model = cp_model.CpModel() + + # Declare our primary variable. + x = model.new_int_var(0, 20, "x") + + # Create the expression variable and implement the piecewise linear function. + # + # \ / + # \______/ + # ed ld + # + large_constant = 1000 + expr = model.new_int_var(0, large_constant, "expr") + + # First segment. + s1 = model.new_int_var(-large_constant, large_constant, "s1") + model.add(s1 == earliness_cost * (earliness_date - x)) + + # Second segment. + s2 = 0 + + # Third segment. + s3 = model.new_int_var(-large_constant, large_constant, "s3") + model.add(s3 == lateness_cost * (x - lateness_date)) + + # Link together expr and x through s1, s2, and s3. + model.add_max_equality(expr, [s1, s2, s3]) + + # Search for x values in increasing order. + model.add_decision_strategy( + [x], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE + ) + + # Create a solver and solve with a fixed search. + solver = cp_model.CpSolver() + + # Force the solver to follow the decision strategy exactly. + solver.parameters.search_branching = cp_model.FIXED_SEARCH + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + + # Search and print out all solutions. + solution_printer = VarArraySolutionPrinter([x, expr]) + solver.solve(model, solution_printer) earliness_tardiness_cost_sample_sat() diff --git a/ortools/sat/samples/index_first_boolvar_true_sample_sat.py b/ortools/sat/samples/index_first_boolvar_true_sample_sat.py index 25676cbbc10..abdb22aa5e1 100644 --- a/ortools/sat/samples/index_first_boolvar_true_sample_sat.py +++ b/ortools/sat/samples/index_first_boolvar_true_sample_sat.py @@ -18,56 +18,56 @@ class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, index: cp_model.IntVar, boolvars: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__index = index - self.__boolvars = boolvars + def __init__(self, index: cp_model.IntVar, boolvars: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__index = index + self.__boolvars = boolvars - def on_solution_callback(self) -> None: - line = "" - for v in self.__boolvars: - line += f"{self.value(v)}" - line += f" -> {self.value(self.__index)}" - print(line) + def on_solution_callback(self) -> None: + line = "" + for v in self.__boolvars: + line += f"{self.value(v)}" + line += f" -> {self.value(self.__index)}" + print(line) def index_of_first_bool_at_true_sample_sat(): - """Compute the index of the first Boolean variable set to true.""" - - # Model. - model = cp_model.CpModel() - - # Variables - num_bool_vars = 5 - bool_vars = [model.new_bool_var(f"{i}") for i in range(num_bool_vars)] - index = model.new_int_var(0, num_bool_vars, "index") - - # Channeling between the index and the Boolean variables. - model.add_min_equality( - index, - [ - num_bool_vars - bool_vars[i] * (num_bool_vars - i) - for i in range(num_bool_vars) - ], - ) - - # Flip bool_vars in increasing order. - model.add_decision_strategy( - bool_vars, cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE - ) - - # Create a solver and solve with a fixed search. - solver = cp_model.CpSolver() - - # Force the solver to follow the decision strategy exactly. - solver.parameters.search_branching = cp_model.FIXED_SEARCH - - # Search and print out all solutions. - solver.parameters.enumerate_all_solutions = True - solution_printer = VarArraySolutionPrinter(index, bool_vars) - solver.solve(model, solution_printer) + """Compute the index of the first Boolean variable set to true.""" + + # Model. + model = cp_model.CpModel() + + # Variables + num_bool_vars = 5 + bool_vars = [model.new_bool_var(f"{i}") for i in range(num_bool_vars)] + index = model.new_int_var(0, num_bool_vars, "index") + + # Channeling between the index and the Boolean variables. + model.add_min_equality( + index, + [ + num_bool_vars - bool_vars[i] * (num_bool_vars - i) + for i in range(num_bool_vars) + ], + ) + + # Flip bool_vars in increasing order. + model.add_decision_strategy( + bool_vars, cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE + ) + + # Create a solver and solve with a fixed search. + solver = cp_model.CpSolver() + + # Force the solver to follow the decision strategy exactly. + solver.parameters.search_branching = cp_model.FIXED_SEARCH + + # Search and print out all solutions. + solver.parameters.enumerate_all_solutions = True + solution_printer = VarArraySolutionPrinter(index, bool_vars) + solver.solve(model, solution_printer) index_of_first_bool_at_true_sample_sat() diff --git a/ortools/sat/samples/interval_relations_sample_sat.py b/ortools/sat/samples/interval_relations_sample_sat.py index 140242596c9..02746c5e8de 100644 --- a/ortools/sat/samples/interval_relations_sample_sat.py +++ b/ortools/sat/samples/interval_relations_sample_sat.py @@ -18,62 +18,64 @@ def interval_relations_sample_sat(): - """Showcases how to build temporal relations between intervals.""" - model = cp_model.CpModel() - horizon = 100 + """Showcases how to build temporal relations between intervals.""" + model = cp_model.CpModel() + horizon = 100 - # An interval can be created from three 1-var affine expressions. - start_var = model.new_int_var(0, horizon, "start") - duration = 10 # Python CP-SAT code accept integer variables or constants. - end_var = model.new_int_var(0, horizon, "end") - interval_var = model.new_interval_var(start_var, duration, end_var, "interval") + # An interval can be created from three 1-var affine expressions. + start_var = model.new_int_var(0, horizon, "start") + duration = 10 # Python CP-SAT code accept integer variables or constants. + end_var = model.new_int_var(0, horizon, "end") + interval_var = model.new_interval_var( + start_var, duration, end_var, "interval" + ) - # If the size is fixed, a simpler version uses the start expression and the - # size. - fixed_size_start_var = model.new_int_var(0, horizon, "fixed_start") - fixed_size_duration = 10 - fixed_size_interval_var = model.new_fixed_size_interval_var( - fixed_size_start_var, - fixed_size_duration, - "fixed_size_interval_var", - ) + # If the size is fixed, a simpler version uses the start expression and the + # size. + fixed_size_start_var = model.new_int_var(0, horizon, "fixed_start") + fixed_size_duration = 10 + fixed_size_interval_var = model.new_fixed_size_interval_var( + fixed_size_start_var, + fixed_size_duration, + "fixed_size_interval_var", + ) - # An optional interval can be created from three 1-var affine expressions and - # a literal. - opt_start_var = model.new_int_var(0, horizon, "opt_start") - opt_duration = model.new_int_var(2, 6, "opt_size") - opt_end_var = model.new_int_var(0, horizon, "opt_end") - opt_presence_var = model.new_bool_var("opt_presence") - opt_interval_var = model.new_optional_interval_var( - opt_start_var, opt_duration, opt_end_var, opt_presence_var, "opt_interval" - ) + # An optional interval can be created from three 1-var affine expressions and + # a literal. + opt_start_var = model.new_int_var(0, horizon, "opt_start") + opt_duration = model.new_int_var(2, 6, "opt_size") + opt_end_var = model.new_int_var(0, horizon, "opt_end") + opt_presence_var = model.new_bool_var("opt_presence") + opt_interval_var = model.new_optional_interval_var( + opt_start_var, opt_duration, opt_end_var, opt_presence_var, "opt_interval" + ) - # If the size is fixed, a simpler version uses the start expression, the - # size, and the presence literal. - opt_fixed_size_start_var = model.new_int_var(0, horizon, "opt_fixed_start") - opt_fixed_size_duration = 10 - opt_fixed_size_presence_var = model.new_bool_var("opt_fixed_presence") - opt_fixed_size_interval_var = model.new_optional_fixed_size_interval_var( - opt_fixed_size_start_var, - opt_fixed_size_duration, - opt_fixed_size_presence_var, - "opt_fixed_size_interval_var", - ) + # If the size is fixed, a simpler version uses the start expression, the + # size, and the presence literal. + opt_fixed_size_start_var = model.new_int_var(0, horizon, "opt_fixed_start") + opt_fixed_size_duration = 10 + opt_fixed_size_presence_var = model.new_bool_var("opt_fixed_presence") + opt_fixed_size_interval_var = model.new_optional_fixed_size_interval_var( + opt_fixed_size_start_var, + opt_fixed_size_duration, + opt_fixed_size_presence_var, + "opt_fixed_size_interval_var", + ) - # Simple precedence between two non optional intervals. - model.add(interval_var.start_expr() >= fixed_size_interval_var.end_expr()) + # Simple precedence between two non optional intervals. + model.add(interval_var.start_expr() >= fixed_size_interval_var.end_expr()) - # Synchronize start between two intervals (one optional, one not) - model.add( - interval_var.start_expr() == opt_interval_var.start_expr() - ).only_enforce_if(opt_presence_var) + # Synchronize start between two intervals (one optional, one not) + model.add( + interval_var.start_expr() == opt_interval_var.start_expr() + ).only_enforce_if(opt_presence_var) - # Exact delay between two optional intervals. - exact_delay: int = 5 - model.add( - opt_interval_var.start_expr() - == opt_fixed_size_interval_var.end_expr() + exact_delay - ).only_enforce_if(opt_presence_var, opt_fixed_size_presence_var) + # Exact delay between two optional intervals. + exact_delay: int = 5 + model.add( + opt_interval_var.start_expr() + == opt_fixed_size_interval_var.end_expr() + exact_delay + ).only_enforce_if(opt_presence_var, opt_fixed_size_presence_var) interval_relations_sample_sat() diff --git a/ortools/sat/samples/interval_sample_sat.py b/ortools/sat/samples/interval_sample_sat.py index d4ffb590db5..d134fdea289 100644 --- a/ortools/sat/samples/interval_sample_sat.py +++ b/ortools/sat/samples/interval_sample_sat.py @@ -19,28 +19,30 @@ def interval_sample_sat(): - """Showcases how to build interval variables.""" - model = cp_model.CpModel() - horizon = 100 - - # An interval can be created from three affine expressions. - start_var = model.new_int_var(0, horizon, "start") - duration = 10 # Python cp/sat code accept integer variables or constants. - end_var = model.new_int_var(0, horizon, "end") - interval_var = model.new_interval_var(start_var, duration, end_var + 2, "interval") - - print(f"interval = {repr(interval_var)}") - - # If the size is fixed, a simpler version uses the start expression and the - # size. - fixed_size_interval_var = model.new_fixed_size_interval_var( - start_var, 10, "fixed_size_interval_var" - ) - print(f"fixed_size_interval_var = {repr(fixed_size_interval_var)}") - - # A fixed interval can be created using the same API. - fixed_interval = model.new_fixed_size_interval_var(5, 10, "fixed_interval") - print(f"fixed_interval = {repr(fixed_interval)}") + """Showcases how to build interval variables.""" + model = cp_model.CpModel() + horizon = 100 + + # An interval can be created from three affine expressions. + start_var = model.new_int_var(0, horizon, "start") + duration = 10 # Python cp/sat code accept integer variables or constants. + end_var = model.new_int_var(0, horizon, "end") + interval_var = model.new_interval_var( + start_var, duration, end_var + 2, "interval" + ) + + print(f"interval = {repr(interval_var)}") + + # If the size is fixed, a simpler version uses the start expression and the + # size. + fixed_size_interval_var = model.new_fixed_size_interval_var( + start_var, 10, "fixed_size_interval_var" + ) + print(f"fixed_size_interval_var = {repr(fixed_size_interval_var)}") + + # A fixed interval can be created using the same API. + fixed_interval = model.new_fixed_size_interval_var(5, 10, "fixed_interval") + print(f"fixed_interval = {repr(fixed_interval)}") interval_sample_sat() diff --git a/ortools/sat/samples/literal_sample_sat.py b/ortools/sat/samples/literal_sample_sat.py index a3d3c9e485a..811f5ac4ce6 100644 --- a/ortools/sat/samples/literal_sample_sat.py +++ b/ortools/sat/samples/literal_sample_sat.py @@ -19,11 +19,11 @@ def literal_sample_sat(): - model = cp_model.CpModel() - x = model.new_bool_var("x") - not_x = ~x - print(x) - print(not_x) + model = cp_model.CpModel() + x = model.new_bool_var("x") + not_x = ~x + print(x) + print(not_x) literal_sample_sat() diff --git a/ortools/sat/samples/minimal_jobshop_sat.py b/ortools/sat/samples/minimal_jobshop_sat.py index a79406febd4..46f5e93ae16 100644 --- a/ortools/sat/samples/minimal_jobshop_sat.py +++ b/ortools/sat/samples/minimal_jobshop_sat.py @@ -22,139 +22,142 @@ def main() -> None: - """Minimal jobshop problem.""" - # Data. - # [START data] - jobs_data = [ # task = (machine_id, processing_time). - [(0, 3), (1, 2), (2, 2)], # Job0 - [(0, 2), (2, 1), (1, 4)], # Job1 - [(1, 4), (2, 3)], # Job2 - ] - - machines_count = 1 + max(task[0] for job in jobs_data for task in job) - all_machines = range(machines_count) - # Computes horizon dynamically as the sum of all durations. - horizon = sum(task[1] for job in jobs_data for task in job) - # [END data] - - # Create the model. - # [START model] - model = cp_model.CpModel() - # [END model] - - # [START variables] - # Named tuple to store information about created variables. - task_type = collections.namedtuple("task_type", "start end interval") - # Named tuple to manipulate solution information. - assigned_task_type = collections.namedtuple( - "assigned_task_type", "start job index duration" - ) - - # Creates job intervals and add to the corresponding machine lists. - all_tasks = {} - machine_to_intervals = collections.defaultdict(list) - + """Minimal jobshop problem.""" + # Data. + # [START data] + jobs_data = [ # task = (machine_id, processing_time). + [(0, 3), (1, 2), (2, 2)], # Job0 + [(0, 2), (2, 1), (1, 4)], # Job1 + [(1, 4), (2, 3)], # Job2 + ] + + machines_count = 1 + max(task[0] for job in jobs_data for task in job) + all_machines = range(machines_count) + # Computes horizon dynamically as the sum of all durations. + horizon = sum(task[1] for job in jobs_data for task in job) + # [END data] + + # Create the model. + # [START model] + model = cp_model.CpModel() + # [END model] + + # [START variables] + # Named tuple to store information about created variables. + task_type = collections.namedtuple("task_type", "start end interval") + # Named tuple to manipulate solution information. + assigned_task_type = collections.namedtuple( + "assigned_task_type", "start job index duration" + ) + + # Creates job intervals and add to the corresponding machine lists. + all_tasks = {} + machine_to_intervals = collections.defaultdict(list) + + for job_id, job in enumerate(jobs_data): + for task_id, task in enumerate(job): + machine, duration = task + suffix = f"_{job_id}_{task_id}" + start_var = model.new_int_var(0, horizon, "start" + suffix) + end_var = model.new_int_var(0, horizon, "end" + suffix) + interval_var = model.new_interval_var( + start_var, duration, end_var, "interval" + suffix + ) + all_tasks[job_id, task_id] = task_type( + start=start_var, end=end_var, interval=interval_var + ) + machine_to_intervals[machine].append(interval_var) + # [END variables] + + # [START constraints] + # Create and add disjunctive constraints. + for machine in all_machines: + model.add_no_overlap(machine_to_intervals[machine]) + + # Precedences inside a job. + for job_id, job in enumerate(jobs_data): + for task_id in range(len(job) - 1): + model.add( + all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end + ) + # [END constraints] + + # [START objective] + # Makespan objective. + obj_var = model.new_int_var(0, horizon, "makespan") + model.add_max_equality( + obj_var, + [ + all_tasks[job_id, len(job) - 1].end + for job_id, job in enumerate(jobs_data) + ], + ) + model.minimize(obj_var) + # [END objective] + + # Creates the solver and solve. + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] + + # [START print_solution] + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print("Solution:") + # Create one list of assigned tasks per machine. + assigned_jobs = collections.defaultdict(list) for job_id, job in enumerate(jobs_data): - for task_id, task in enumerate(job): - machine, duration = task - suffix = f"_{job_id}_{task_id}" - start_var = model.new_int_var(0, horizon, "start" + suffix) - end_var = model.new_int_var(0, horizon, "end" + suffix) - interval_var = model.new_interval_var( - start_var, duration, end_var, "interval" + suffix - ) - all_tasks[job_id, task_id] = task_type( - start=start_var, end=end_var, interval=interval_var + for task_id, task in enumerate(job): + machine = task[0] + assigned_jobs[machine].append( + assigned_task_type( + start=solver.value(all_tasks[job_id, task_id].start), + job=job_id, + index=task_id, + duration=task[1], ) - machine_to_intervals[machine].append(interval_var) - # [END variables] + ) - # [START constraints] - # Create and add disjunctive constraints. + # Create per machine output lines. + output = "" for machine in all_machines: - model.add_no_overlap(machine_to_intervals[machine]) - - # Precedences inside a job. - for job_id, job in enumerate(jobs_data): - for task_id in range(len(job) - 1): - model.add( - all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end - ) - # [END constraints] - - # [START objective] - # Makespan objective. - obj_var = model.new_int_var(0, horizon, "makespan") - model.add_max_equality( - obj_var, - [all_tasks[job_id, len(job) - 1].end for job_id, job in enumerate(jobs_data)], - ) - model.minimize(obj_var) - # [END objective] - - # Creates the solver and solve. - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] - - # [START print_solution] - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print("Solution:") - # Create one list of assigned tasks per machine. - assigned_jobs = collections.defaultdict(list) - for job_id, job in enumerate(jobs_data): - for task_id, task in enumerate(job): - machine = task[0] - assigned_jobs[machine].append( - assigned_task_type( - start=solver.value(all_tasks[job_id, task_id].start), - job=job_id, - index=task_id, - duration=task[1], - ) - ) - - # Create per machine output lines. - output = "" - for machine in all_machines: - # Sort by starting time. - assigned_jobs[machine].sort() - sol_line_tasks = "Machine " + str(machine) + ": " - sol_line = " " - - for assigned_task in assigned_jobs[machine]: - name = f"job_{assigned_task.job}_task_{assigned_task.index}" - # add spaces to output to align columns. - sol_line_tasks += f"{name:15}" - - start = assigned_task.start - duration = assigned_task.duration - sol_tmp = f"[{start},{start + duration}]" - # add spaces to output to align columns. - sol_line += f"{sol_tmp:15}" - - sol_line += "\n" - sol_line_tasks += "\n" - output += sol_line_tasks - output += sol_line - - # Finally print the solution found. - print(f"Optimal Schedule Length: {solver.objective_value}") - print(output) - else: - print("No solution found.") - # [END print_solution] - - # Statistics. - # [START statistics] - print("\nStatistics") - print(f" - conflicts: {solver.num_conflicts}") - print(f" - branches : {solver.num_branches}") - print(f" - wall time: {solver.wall_time}s") - # [END statistics] + # Sort by starting time. + assigned_jobs[machine].sort() + sol_line_tasks = "Machine " + str(machine) + ": " + sol_line = " " + + for assigned_task in assigned_jobs[machine]: + name = f"job_{assigned_task.job}_task_{assigned_task.index}" + # add spaces to output to align columns. + sol_line_tasks += f"{name:15}" + + start = assigned_task.start + duration = assigned_task.duration + sol_tmp = f"[{start},{start + duration}]" + # add spaces to output to align columns. + sol_line += f"{sol_tmp:15}" + + sol_line += "\n" + sol_line_tasks += "\n" + output += sol_line_tasks + output += sol_line + + # Finally print the solution found. + print(f"Optimal Schedule Length: {solver.objective_value}") + print(output) + else: + print("No solution found.") + # [END print_solution] + + # Statistics. + # [START statistics] + print("\nStatistics") + print(f" - conflicts: {solver.num_conflicts}") + print(f" - branches : {solver.num_branches}") + print(f" - wall time: {solver.wall_time}s") + # [END statistics] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/multiple_knapsack_sat.py b/ortools/sat/samples/multiple_knapsack_sat.py index 3f3b3e567a7..e8f930af3b5 100644 --- a/ortools/sat/samples/multiple_knapsack_sat.py +++ b/ortools/sat/samples/multiple_knapsack_sat.py @@ -21,85 +21,85 @@ def main() -> None: - # [START data] - data = {} - data["weights"] = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36] - data["values"] = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25] - assert len(data["weights"]) == len(data["values"]) - num_items = len(data["weights"]) - all_items = range(num_items) + # [START data] + data = {} + data["weights"] = [48, 30, 42, 36, 36, 48, 42, 42, 36, 24, 30, 30, 42, 36, 36] + data["values"] = [10, 30, 25, 50, 35, 30, 15, 40, 30, 35, 45, 10, 20, 30, 25] + assert len(data["weights"]) == len(data["values"]) + num_items = len(data["weights"]) + all_items = range(num_items) - data["bin_capacities"] = [100, 100, 100, 100, 100] - num_bins = len(data["bin_capacities"]) - all_bins = range(num_bins) - # [END data] + data["bin_capacities"] = [100, 100, 100, 100, 100] + num_bins = len(data["bin_capacities"]) + all_bins = range(num_bins) + # [END data] - # [START model] - model = cp_model.CpModel() - # [END model] + # [START model] + model = cp_model.CpModel() + # [END model] - # Variables. - # [START variables] - # x[i, b] = 1 if item i is packed in bin b. - x = {} - for i in all_items: - for b in all_bins: - x[i, b] = model.new_bool_var(f"x_{i}_{b}") - # [END variables] + # Variables. + # [START variables] + # x[i, b] = 1 if item i is packed in bin b. + x = {} + for i in all_items: + for b in all_bins: + x[i, b] = model.new_bool_var(f"x_{i}_{b}") + # [END variables] - # Constraints. - # [START constraints] - # Each item is assigned to at most one bin. - for i in all_items: - model.add_at_most_one(x[i, b] for b in all_bins) + # Constraints. + # [START constraints] + # Each item is assigned to at most one bin. + for i in all_items: + model.add_at_most_one(x[i, b] for b in all_bins) - # The amount packed in each bin cannot exceed its capacity. - for b in all_bins: - model.add( - sum(x[i, b] * data["weights"][i] for i in all_items) - <= data["bin_capacities"][b] - ) - # [END constraints] + # The amount packed in each bin cannot exceed its capacity. + for b in all_bins: + model.add( + sum(x[i, b] * data["weights"][i] for i in all_items) + <= data["bin_capacities"][b] + ) + # [END constraints] - # Objective. - # [START objective] - # maximize total value of packed items. - objective = [] - for i in all_items: - for b in all_bins: - objective.append(cp_model.LinearExpr.term(x[i, b], data["values"][i])) - model.maximize(cp_model.LinearExpr.sum(objective)) - # [END objective] + # Objective. + # [START objective] + # maximize total value of packed items. + objective = [] + for i in all_items: + for b in all_bins: + objective.append(cp_model.LinearExpr.term(x[i, b], data["values"][i])) + model.maximize(cp_model.LinearExpr.sum(objective)) + # [END objective] - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] - # [START print_solution] - if status == cp_model.OPTIMAL: - print(f"Total packed value: {solver.objective_value}") - total_weight = 0 - for b in all_bins: - print(f"Bin {b}") - bin_weight = 0 - bin_value = 0 - for i in all_items: - if solver.value(x[i, b]) > 0: - print( - f'Item:{i} weight:{data["weights"][i]} value:{data["values"][i]}' - ) - bin_weight += data["weights"][i] - bin_value += data["values"][i] - print(f"Packed bin weight: {bin_weight}") - print(f"Packed bin value: {bin_value}\n") - total_weight += bin_weight - print(f"Total packed weight: {total_weight}") - else: - print("The problem does not have an optimal solution.") - # [END print_solution] + # [START print_solution] + if status == cp_model.OPTIMAL: + print(f"Total packed value: {solver.objective_value}") + total_weight = 0 + for b in all_bins: + print(f"Bin {b}") + bin_weight = 0 + bin_value = 0 + for i in all_items: + if solver.value(x[i, b]) > 0: + print( + f'Item:{i} weight:{data["weights"][i]} value:{data["values"][i]}' + ) + bin_weight += data["weights"][i] + bin_value += data["values"][i] + print(f"Packed bin weight: {bin_weight}") + print(f"Packed bin value: {bin_value}\n") + total_weight += bin_weight + print(f"Total packed weight: {total_weight}") + else: + print("The problem does not have an optimal solution.") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/no_overlap_sample_sat.py b/ortools/sat/samples/no_overlap_sample_sat.py index 51fcac9135d..429f8813dee 100644 --- a/ortools/sat/samples/no_overlap_sample_sat.py +++ b/ortools/sat/samples/no_overlap_sample_sat.py @@ -18,52 +18,54 @@ def no_overlap_sample_sat(): - """No overlap sample with fixed activities.""" - model = cp_model.CpModel() - horizon = 21 # 3 weeks. + """No overlap sample with fixed activities.""" + model = cp_model.CpModel() + horizon = 21 # 3 weeks. - # Task 0, duration 2. - start_0 = model.new_int_var(0, horizon, "start_0") - duration_0 = 2 # Python cp/sat code accepts integer variables or constants. - end_0 = model.new_int_var(0, horizon, "end_0") - task_0 = model.new_interval_var(start_0, duration_0, end_0, "task_0") - # Task 1, duration 4. - start_1 = model.new_int_var(0, horizon, "start_1") - duration_1 = 4 # Python cp/sat code accepts integer variables or constants. - end_1 = model.new_int_var(0, horizon, "end_1") - task_1 = model.new_interval_var(start_1, duration_1, end_1, "task_1") + # Task 0, duration 2. + start_0 = model.new_int_var(0, horizon, "start_0") + duration_0 = 2 # Python cp/sat code accepts integer variables or constants. + end_0 = model.new_int_var(0, horizon, "end_0") + task_0 = model.new_interval_var(start_0, duration_0, end_0, "task_0") + # Task 1, duration 4. + start_1 = model.new_int_var(0, horizon, "start_1") + duration_1 = 4 # Python cp/sat code accepts integer variables or constants. + end_1 = model.new_int_var(0, horizon, "end_1") + task_1 = model.new_interval_var(start_1, duration_1, end_1, "task_1") - # Task 2, duration 3. - start_2 = model.new_int_var(0, horizon, "start_2") - duration_2 = 3 # Python cp/sat code accepts integer variables or constants. - end_2 = model.new_int_var(0, horizon, "end_2") - task_2 = model.new_interval_var(start_2, duration_2, end_2, "task_2") + # Task 2, duration 3. + start_2 = model.new_int_var(0, horizon, "start_2") + duration_2 = 3 # Python cp/sat code accepts integer variables or constants. + end_2 = model.new_int_var(0, horizon, "end_2") + task_2 = model.new_interval_var(start_2, duration_2, end_2, "task_2") - # Weekends. - weekend_0 = model.new_interval_var(5, 2, 7, "weekend_0") - weekend_1 = model.new_interval_var(12, 2, 14, "weekend_1") - weekend_2 = model.new_interval_var(19, 2, 21, "weekend_2") + # Weekends. + weekend_0 = model.new_interval_var(5, 2, 7, "weekend_0") + weekend_1 = model.new_interval_var(12, 2, 14, "weekend_1") + weekend_2 = model.new_interval_var(19, 2, 21, "weekend_2") - # No Overlap constraint. - model.add_no_overlap([task_0, task_1, task_2, weekend_0, weekend_1, weekend_2]) + # No Overlap constraint. + model.add_no_overlap( + [task_0, task_1, task_2, weekend_0, weekend_1, weekend_2] + ) - # Makespan objective. - obj = model.new_int_var(0, horizon, "makespan") - model.add_max_equality(obj, [end_0, end_1, end_2]) - model.minimize(obj) + # Makespan objective. + obj = model.new_int_var(0, horizon, "makespan") + model.add_max_equality(obj, [end_0, end_1, end_2]) + model.minimize(obj) - # Solve model. - solver = cp_model.CpSolver() - status = solver.solve(model) + # Solve model. + solver = cp_model.CpSolver() + status = solver.solve(model) - if status == cp_model.OPTIMAL: - # Print out makespan and the start times for all tasks. - print(f"Optimal Schedule Length: {solver.objective_value}") - print(f"Task 0 starts at {solver.value(start_0)}") - print(f"Task 1 starts at {solver.value(start_1)}") - print(f"Task 2 starts at {solver.value(start_2)}") - else: - print(f"Solver exited with nonoptimal status: {status}") + if status == cp_model.OPTIMAL: + # Print out makespan and the start times for all tasks. + print(f"Optimal Schedule Length: {solver.objective_value}") + print(f"Task 0 starts at {solver.value(start_0)}") + print(f"Task 1 starts at {solver.value(start_1)}") + print(f"Task 2 starts at {solver.value(start_2)}") + else: + print(f"Solver exited with nonoptimal status: {status}") no_overlap_sample_sat() diff --git a/ortools/sat/samples/non_linear_sat.py b/ortools/sat/samples/non_linear_sat.py index 27448eb3686..f71db16de93 100644 --- a/ortools/sat/samples/non_linear_sat.py +++ b/ortools/sat/samples/non_linear_sat.py @@ -22,30 +22,30 @@ def non_linear_sat(): - """Non linear sample.""" - perimeter = 20 + """Non linear sample.""" + perimeter = 20 - model = cp_model.CpModel() + model = cp_model.CpModel() - x = model.new_int_var(0, perimeter, "x") - y = model.new_int_var(0, perimeter, "y") - model.add(2 * (x + y) == perimeter) + x = model.new_int_var(0, perimeter, "x") + y = model.new_int_var(0, perimeter, "y") + model.add(2 * (x + y) == perimeter) - area = model.new_int_var(0, perimeter * perimeter, "s") - model.add_multiplication_equality(area, x, y) + area = model.new_int_var(0, perimeter * perimeter, "s") + model.add_multiplication_equality(area, x, y) - model.maximize(area) + model.maximize(area) - solver = cp_model.CpSolver() + solver = cp_model.CpSolver() - status = solver.solve(model) + status = solver.solve(model) - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print(f"x = {solver.value(x)}") - print(f"y = {solver.value(y)}") - print(f"s = {solver.value(area)}") - else: - print("No solution found.") + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print(f"x = {solver.value(x)}") + print(f"y = {solver.value(y)}") + print(f"s = {solver.value(area)}") + else: + print("No solution found.") non_linear_sat() diff --git a/ortools/sat/samples/nqueens_sat.py b/ortools/sat/samples/nqueens_sat.py index 770df0aea31..837fc3e17d8 100644 --- a/ortools/sat/samples/nqueens_sat.py +++ b/ortools/sat/samples/nqueens_sat.py @@ -24,86 +24,88 @@ # [START solution_printer] class NQueenSolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" - - def __init__(self, queens: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__queens = queens - self.__solution_count = 0 - self.__start_time = time.time() - - @property - def solution_count(self) -> int: - return self.__solution_count - - def on_solution_callback(self): - current_time = time.time() - print( - f"Solution {self.__solution_count}, " - f"time = {current_time - self.__start_time} s" - ) - self.__solution_count += 1 - - all_queens = range(len(self.__queens)) - for i in all_queens: - for j in all_queens: - if self.value(self.__queens[j]) == i: - # There is a queen in column j, row i. - print("Q", end=" ") - else: - print("_", end=" ") - print() - print() + """Print intermediate solutions.""" + + def __init__(self, queens: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__queens = queens + self.__solution_count = 0 + self.__start_time = time.time() + + @property + def solution_count(self) -> int: + return self.__solution_count + + def on_solution_callback(self): + current_time = time.time() + print( + f"Solution {self.__solution_count}, " + f"time = {current_time - self.__start_time} s" + ) + self.__solution_count += 1 + + all_queens = range(len(self.__queens)) + for i in all_queens: + for j in all_queens: + if self.value(self.__queens[j]) == i: + # There is a queen in column j, row i. + print("Q", end=" ") + else: + print("_", end=" ") + print() + print() # [END solution_printer] def main(board_size: int) -> None: - # Creates the solver. - # [START model] - model = cp_model.CpModel() - # [END model] - - # Creates the variables. - # [START variables] - # There are `board_size` number of variables, one for a queen in each column - # of the board. The value of each variable is the row that the queen is in. - queens = [model.new_int_var(0, board_size - 1, f"x_{i}") for i in range(board_size)] - # [END variables] - - # Creates the constraints. - # [START constraints] - # All rows must be different. - model.add_all_different(queens) - - # No two queens can be on the same diagonal. - model.add_all_different(queens[i] + i for i in range(board_size)) - model.add_all_different(queens[i] - i for i in range(board_size)) - # [END constraints] - - # Solve the model. - # [START solve] - solver = cp_model.CpSolver() - solution_printer = NQueenSolutionPrinter(queens) - solver.parameters.enumerate_all_solutions = True - solver.solve(model, solution_printer) - # [END solve] - - # Statistics. - # [START statistics] - print("\nStatistics") - print(f" conflicts : {solver.num_conflicts}") - print(f" branches : {solver.num_branches}") - print(f" wall time : {solver.wall_time} s") - print(f" solutions found: {solution_printer.solution_count}") - # [END statistics] + # Creates the solver. + # [START model] + model = cp_model.CpModel() + # [END model] + + # Creates the variables. + # [START variables] + # There are `board_size` number of variables, one for a queen in each column + # of the board. The value of each variable is the row that the queen is in. + queens = [ + model.new_int_var(0, board_size - 1, f"x_{i}") for i in range(board_size) + ] + # [END variables] + + # Creates the constraints. + # [START constraints] + # All rows must be different. + model.add_all_different(queens) + + # No two queens can be on the same diagonal. + model.add_all_different(queens[i] + i for i in range(board_size)) + model.add_all_different(queens[i] - i for i in range(board_size)) + # [END constraints] + + # Solve the model. + # [START solve] + solver = cp_model.CpSolver() + solution_printer = NQueenSolutionPrinter(queens) + solver.parameters.enumerate_all_solutions = True + solver.solve(model, solution_printer) + # [END solve] + + # Statistics. + # [START statistics] + print("\nStatistics") + print(f" conflicts : {solver.num_conflicts}") + print(f" branches : {solver.num_branches}") + print(f" wall time : {solver.wall_time} s") + print(f" solutions found: {solution_printer.solution_count}") + # [END statistics] if __name__ == "__main__": - # By default, solve the 8x8 problem. - size = 8 - if len(sys.argv) > 1: - size = int(sys.argv[1]) - main(size) + # By default, solve the 8x8 problem. + size = 8 + if len(sys.argv) > 1: + size = int(sys.argv[1]) + main(size) # [END program] diff --git a/ortools/sat/samples/nurses_sat.py b/ortools/sat/samples/nurses_sat.py index 16fae1af178..c72023caf7f 100644 --- a/ortools/sat/samples/nurses_sat.py +++ b/ortools/sat/samples/nurses_sat.py @@ -21,126 +21,126 @@ def main() -> None: - # Data. - # [START data] - num_nurses = 4 - num_shifts = 3 - num_days = 3 - all_nurses = range(num_nurses) - all_shifts = range(num_shifts) - all_days = range(num_days) - # [END data] - - # Creates the model. - # [START model] - model = cp_model.CpModel() - # [END model] - - # Creates shift variables. - # shifts[(n, d, s)]: nurse 'n' works shift 's' on day 'd'. - # [START variables] - shifts = {} - for n in all_nurses: - for d in all_days: - for s in all_shifts: - shifts[(n, d, s)] = model.new_bool_var(f"shift_n{n}_d{d}_s{s}") - # [END variables] - - # Each shift is assigned to exactly one nurse in the schedule period. - # [START exactly_one_nurse] + # Data. + # [START data] + num_nurses = 4 + num_shifts = 3 + num_days = 3 + all_nurses = range(num_nurses) + all_shifts = range(num_shifts) + all_days = range(num_days) + # [END data] + + # Creates the model. + # [START model] + model = cp_model.CpModel() + # [END model] + + # Creates shift variables. + # shifts[(n, d, s)]: nurse 'n' works shift 's' on day 'd'. + # [START variables] + shifts = {} + for n in all_nurses: for d in all_days: - for s in all_shifts: - model.add_exactly_one(shifts[(n, d, s)] for n in all_nurses) - # [END exactly_one_nurse] - - # Each nurse works at most one shift per day. - # [START at_most_one_shift] - for n in all_nurses: - for d in all_days: - model.add_at_most_one(shifts[(n, d, s)] for s in all_shifts) - # [END at_most_one_shift] - - # [START assign_nurses_evenly] - # Try to distribute the shifts evenly, so that each nurse works - # min_shifts_per_nurse shifts. If this is not possible, because the total - # number of shifts is not divisible by the number of nurses, some nurses will - # be assigned one more shift. - min_shifts_per_nurse = (num_shifts * num_days) // num_nurses - if num_shifts * num_days % num_nurses == 0: - max_shifts_per_nurse = min_shifts_per_nurse - else: - max_shifts_per_nurse = min_shifts_per_nurse + 1 - for n in all_nurses: - shifts_worked = [] - for d in all_days: - for s in all_shifts: - shifts_worked.append(shifts[(n, d, s)]) - model.add(min_shifts_per_nurse <= sum(shifts_worked)) - model.add(sum(shifts_worked) <= max_shifts_per_nurse) - # [END assign_nurses_evenly] - - # Creates the solver and solve. - # [START parameters] - solver = cp_model.CpSolver() - solver.parameters.linearization_level = 0 - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - # [END parameters] - - # [START solution_printer] - class NursesPartialSolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" - - def __init__(self, shifts, num_nurses, num_days, num_shifts, limit): - cp_model.CpSolverSolutionCallback.__init__(self) - self._shifts = shifts - self._num_nurses = num_nurses - self._num_days = num_days - self._num_shifts = num_shifts - self._solution_count = 0 - self._solution_limit = limit - - def on_solution_callback(self): - self._solution_count += 1 - print(f"Solution {self._solution_count}") - for d in range(self._num_days): - print(f"Day {d}") - for n in range(self._num_nurses): - is_working = False - for s in range(self._num_shifts): - if self.value(self._shifts[(n, d, s)]): - is_working = True - print(f" Nurse {n} works shift {s}") - if not is_working: - print(f" Nurse {n} does not work") - if self._solution_count >= self._solution_limit: - print(f"Stop search after {self._solution_limit} solutions") - self.stop_search() - - def solutionCount(self): - return self._solution_count - - # Display the first five solutions. - solution_limit = 5 - solution_printer = NursesPartialSolutionPrinter( - shifts, num_nurses, num_days, num_shifts, solution_limit - ) - # [END solution_printer] - - # [START solve] - solver.solve(model, solution_printer) - # [END solve] - - # Statistics. - # [START statistics] - print("\nStatistics") - print(f" - conflicts : {solver.num_conflicts}") - print(f" - branches : {solver.num_branches}") - print(f" - wall time : {solver.wall_time} s") - print(f" - solutions found: {solution_printer.solutionCount()}") - # [END statistics] + for s in all_shifts: + shifts[(n, d, s)] = model.new_bool_var(f"shift_n{n}_d{d}_s{s}") + # [END variables] + + # Each shift is assigned to exactly one nurse in the schedule period. + # [START exactly_one_nurse] + for d in all_days: + for s in all_shifts: + model.add_exactly_one(shifts[(n, d, s)] for n in all_nurses) + # [END exactly_one_nurse] + + # Each nurse works at most one shift per day. + # [START at_most_one_shift] + for n in all_nurses: + for d in all_days: + model.add_at_most_one(shifts[(n, d, s)] for s in all_shifts) + # [END at_most_one_shift] + + # [START assign_nurses_evenly] + # Try to distribute the shifts evenly, so that each nurse works + # min_shifts_per_nurse shifts. If this is not possible, because the total + # number of shifts is not divisible by the number of nurses, some nurses will + # be assigned one more shift. + min_shifts_per_nurse = (num_shifts * num_days) // num_nurses + if num_shifts * num_days % num_nurses == 0: + max_shifts_per_nurse = min_shifts_per_nurse + else: + max_shifts_per_nurse = min_shifts_per_nurse + 1 + for n in all_nurses: + shifts_worked = [] + for d in all_days: + for s in all_shifts: + shifts_worked.append(shifts[(n, d, s)]) + model.add(min_shifts_per_nurse <= sum(shifts_worked)) + model.add(sum(shifts_worked) <= max_shifts_per_nurse) + # [END assign_nurses_evenly] + + # Creates the solver and solve. + # [START parameters] + solver = cp_model.CpSolver() + solver.parameters.linearization_level = 0 + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + # [END parameters] + + # [START solution_printer] + class NursesPartialSolutionPrinter(cp_model.CpSolverSolutionCallback): + """Print intermediate solutions.""" + + def __init__(self, shifts, num_nurses, num_days, num_shifts, limit): + cp_model.CpSolverSolutionCallback.__init__(self) + self._shifts = shifts + self._num_nurses = num_nurses + self._num_days = num_days + self._num_shifts = num_shifts + self._solution_count = 0 + self._solution_limit = limit + + def on_solution_callback(self): + self._solution_count += 1 + print(f"Solution {self._solution_count}") + for d in range(self._num_days): + print(f"Day {d}") + for n in range(self._num_nurses): + is_working = False + for s in range(self._num_shifts): + if self.value(self._shifts[(n, d, s)]): + is_working = True + print(f" Nurse {n} works shift {s}") + if not is_working: + print(f" Nurse {n} does not work") + if self._solution_count >= self._solution_limit: + print(f"Stop search after {self._solution_limit} solutions") + self.stop_search() + + def solutionCount(self): + return self._solution_count + + # Display the first five solutions. + solution_limit = 5 + solution_printer = NursesPartialSolutionPrinter( + shifts, num_nurses, num_days, num_shifts, solution_limit + ) + # [END solution_printer] + + # [START solve] + solver.solve(model, solution_printer) + # [END solve] + + # Statistics. + # [START statistics] + print("\nStatistics") + print(f" - conflicts : {solver.num_conflicts}") + print(f" - branches : {solver.num_branches}") + print(f" - wall time : {solver.wall_time} s") + print(f" - solutions found: {solution_printer.solutionCount()}") + # [END statistics] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/optional_interval_sample_sat.py b/ortools/sat/samples/optional_interval_sample_sat.py index fa43a96fba9..6e485e1c7b5 100644 --- a/ortools/sat/samples/optional_interval_sample_sat.py +++ b/ortools/sat/samples/optional_interval_sample_sat.py @@ -18,33 +18,33 @@ def optional_interval_sample_sat(): - """Showcases how to build optional interval variables.""" - model = cp_model.CpModel() - horizon = 100 - - # An interval can be created from three affine expressions. - start_var = model.new_int_var(0, horizon, "start") - duration = 10 # Python cp/sat code accept integer variables or constants. - end_var = model.new_int_var(0, horizon, "end") - presence_var = model.new_bool_var("presence") - interval_var = model.new_optional_interval_var( - start_var, duration, end_var + 2, presence_var, "interval" - ) - - print(f"interval = {repr(interval_var)}") - - # If the size is fixed, a simpler version uses the start expression and the - # size. - fixed_size_interval_var = model.new_optional_fixed_size_interval_var( - start_var, 10, presence_var, "fixed_size_interval_var" - ) - print(f"fixed_size_interval_var = {repr(fixed_size_interval_var)}") - - # A fixed interval can be created using the same API. - fixed_interval = model.new_optional_fixed_size_interval_var( - 5, 10, presence_var, "fixed_interval" - ) - print(f"fixed_interval = {repr(fixed_interval)}") + """Showcases how to build optional interval variables.""" + model = cp_model.CpModel() + horizon = 100 + + # An interval can be created from three affine expressions. + start_var = model.new_int_var(0, horizon, "start") + duration = 10 # Python cp/sat code accept integer variables or constants. + end_var = model.new_int_var(0, horizon, "end") + presence_var = model.new_bool_var("presence") + interval_var = model.new_optional_interval_var( + start_var, duration, end_var + 2, presence_var, "interval" + ) + + print(f"interval = {repr(interval_var)}") + + # If the size is fixed, a simpler version uses the start expression and the + # size. + fixed_size_interval_var = model.new_optional_fixed_size_interval_var( + start_var, 10, presence_var, "fixed_size_interval_var" + ) + print(f"fixed_size_interval_var = {repr(fixed_size_interval_var)}") + + # A fixed interval can be created using the same API. + fixed_interval = model.new_optional_fixed_size_interval_var( + 5, 10, presence_var, "fixed_interval" + ) + print(f"fixed_interval = {repr(fixed_interval)}") optional_interval_sample_sat() diff --git a/ortools/sat/samples/overlapping_intervals_sample_sat.py b/ortools/sat/samples/overlapping_intervals_sample_sat.py index 7ac771e2422..1a7924f2ae5 100644 --- a/ortools/sat/samples/overlapping_intervals_sample_sat.py +++ b/ortools/sat/samples/overlapping_intervals_sample_sat.py @@ -18,79 +18,81 @@ class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables + def __init__(self, variables: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables - def on_solution_callback(self) -> None: - for v in self.__variables: - print(f"{v}={self.value(v)}", end=" ") - print() + def on_solution_callback(self) -> None: + for v in self.__variables: + print(f"{v}={self.value(v)}", end=" ") + print() def overlapping_interval_sample_sat(): - """Create the overlapping Boolean variables and enumerate all states.""" - model = cp_model.CpModel() - - horizon = 7 - - # First interval. - start_var_a = model.new_int_var(0, horizon, "start_a") - duration_a = 3 - end_var_a = model.new_int_var(0, horizon, "end_a") - unused_interval_var_a = model.new_interval_var( - start_var_a, duration_a, end_var_a, "interval_a" - ) - - # Second interval. - start_var_b = model.new_int_var(0, horizon, "start_b") - duration_b = 2 - end_var_b = model.new_int_var(0, horizon, "end_b") - unused_interval_var_b = model.new_interval_var( - start_var_b, duration_b, end_var_b, "interval_b" - ) - - # a_after_b Boolean variable. - a_after_b = model.new_bool_var("a_after_b") - model.add(start_var_a >= end_var_b).only_enforce_if(a_after_b) - model.add(start_var_a < end_var_b).only_enforce_if(~a_after_b) - - # b_after_a Boolean variable. - b_after_a = model.new_bool_var("b_after_a") - model.add(start_var_b >= end_var_a).only_enforce_if(b_after_a) - model.add(start_var_b < end_var_a).only_enforce_if(~b_after_a) - - # Result Boolean variable. - a_overlaps_b = model.new_bool_var("a_overlaps_b") - - # Option a: using only clauses - model.add_bool_or(a_after_b, b_after_a, a_overlaps_b) - model.add_implication(a_after_b, ~a_overlaps_b) - model.add_implication(b_after_a, ~a_overlaps_b) - - # Option b: using an exactly one constraint. - # model.add_exactly_one(a_after_b, b_after_a, a_overlaps_b) - - # Search for start values in increasing order for the two intervals. - model.add_decision_strategy( - [start_var_a, start_var_b], - cp_model.CHOOSE_FIRST, - cp_model.SELECT_MIN_VALUE, - ) - - # Create a solver and solve with a fixed search. - solver = cp_model.CpSolver() - - # Force the solver to follow the decision strategy exactly. - solver.parameters.search_branching = cp_model.FIXED_SEARCH - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - - # Search and print out all solutions. - solution_printer = VarArraySolutionPrinter([start_var_a, start_var_b, a_overlaps_b]) - solver.solve(model, solution_printer) + """Create the overlapping Boolean variables and enumerate all states.""" + model = cp_model.CpModel() + + horizon = 7 + + # First interval. + start_var_a = model.new_int_var(0, horizon, "start_a") + duration_a = 3 + end_var_a = model.new_int_var(0, horizon, "end_a") + unused_interval_var_a = model.new_interval_var( + start_var_a, duration_a, end_var_a, "interval_a" + ) + + # Second interval. + start_var_b = model.new_int_var(0, horizon, "start_b") + duration_b = 2 + end_var_b = model.new_int_var(0, horizon, "end_b") + unused_interval_var_b = model.new_interval_var( + start_var_b, duration_b, end_var_b, "interval_b" + ) + + # a_after_b Boolean variable. + a_after_b = model.new_bool_var("a_after_b") + model.add(start_var_a >= end_var_b).only_enforce_if(a_after_b) + model.add(start_var_a < end_var_b).only_enforce_if(~a_after_b) + + # b_after_a Boolean variable. + b_after_a = model.new_bool_var("b_after_a") + model.add(start_var_b >= end_var_a).only_enforce_if(b_after_a) + model.add(start_var_b < end_var_a).only_enforce_if(~b_after_a) + + # Result Boolean variable. + a_overlaps_b = model.new_bool_var("a_overlaps_b") + + # Option a: using only clauses + model.add_bool_or(a_after_b, b_after_a, a_overlaps_b) + model.add_implication(a_after_b, ~a_overlaps_b) + model.add_implication(b_after_a, ~a_overlaps_b) + + # Option b: using an exactly one constraint. + # model.add_exactly_one(a_after_b, b_after_a, a_overlaps_b) + + # Search for start values in increasing order for the two intervals. + model.add_decision_strategy( + [start_var_a, start_var_b], + cp_model.CHOOSE_FIRST, + cp_model.SELECT_MIN_VALUE, + ) + + # Create a solver and solve with a fixed search. + solver = cp_model.CpSolver() + + # Force the solver to follow the decision strategy exactly. + solver.parameters.search_branching = cp_model.FIXED_SEARCH + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + + # Search and print out all solutions. + solution_printer = VarArraySolutionPrinter( + [start_var_a, start_var_b, a_overlaps_b] + ) + solver.solve(model, solution_printer) overlapping_interval_sample_sat() diff --git a/ortools/sat/samples/rabbits_and_pheasants_sat.py b/ortools/sat/samples/rabbits_and_pheasants_sat.py index f7a91050b26..aea367b0f1f 100644 --- a/ortools/sat/samples/rabbits_and_pheasants_sat.py +++ b/ortools/sat/samples/rabbits_and_pheasants_sat.py @@ -18,23 +18,23 @@ def rabbits_and_pheasants_sat(): - """Solves the rabbits + pheasants problem.""" - model = cp_model.CpModel() + """Solves the rabbits + pheasants problem.""" + model = cp_model.CpModel() - r = model.new_int_var(0, 100, "r") - p = model.new_int_var(0, 100, "p") + r = model.new_int_var(0, 100, "r") + p = model.new_int_var(0, 100, "p") - # 20 heads. - model.add(r + p == 20) - # 56 legs. - model.add(4 * r + 2 * p == 56) + # 20 heads. + model.add(r + p == 20) + # 56 legs. + model.add(4 * r + 2 * p == 56) - # Solves and prints out the solution. - solver = cp_model.CpSolver() - status = solver.solve(model) + # Solves and prints out the solution. + solver = cp_model.CpSolver() + status = solver.solve(model) - if status == cp_model.OPTIMAL: - print(f"{solver.value(r)} rabbits and {solver.value(p)} pheasants") + if status == cp_model.OPTIMAL: + print(f"{solver.value(r)} rabbits and {solver.value(p)} pheasants") rabbits_and_pheasants_sat() diff --git a/ortools/sat/samples/ranking_circuit_sample_sat.py b/ortools/sat/samples/ranking_circuit_sample_sat.py index 0e91333ba01..a6f645457d7 100644 --- a/ortools/sat/samples/ranking_circuit_sample_sat.py +++ b/ortools/sat/samples/ranking_circuit_sample_sat.py @@ -27,153 +27,155 @@ def rank_tasks_with_circuit( presences: Sequence[cp_model.IntVar], ranks: Sequence[cp_model.IntVar], ) -> None: - """This method uses a circuit constraint to rank tasks. - - This method assumes that all starts are disjoint, meaning that all tasks have - a strictly positive duration, and they appear in the same NoOverlap - constraint. - - To implement this ranking, we will create a dense graph with num_tasks + 1 - nodes. - The extra node (with id 0) will be used to decide which task is first with - its only outgoing arc, and which task is last with its only incoming arc. - Each task i will be associated with id i + 1, and an arc between i + 1 and j + - 1 indicates that j is the immediate successor of i. - - The circuit constraint ensures there is at most 1 hamiltonian cycle of - length > 1. If no such path exists, then no tasks are active. - We also need to enforce that any hamiltonian cycle of size > 1 must contain - the node 0. And thus, there is a self loop on node 0 iff the circuit is empty. - - Args: - model: The CpModel to add the constraints to. - starts: The array of starts variables of all tasks. - durations: the durations of all tasks. - presences: The array of presence variables of all tasks. - ranks: The array of rank variables of all tasks. - """ - - num_tasks = len(starts) - all_tasks = range(num_tasks) - - arcs: List[cp_model.ArcT] = [] - for i in all_tasks: - # if node i is first. - start_lit = model.new_bool_var(f"start_{i}") - arcs.append((0, i + 1, start_lit)) - model.add(ranks[i] == 0).only_enforce_if(start_lit) - - # As there are no other constraints on the problem, we can add this - # redundant constraint. - model.add(starts[i] == 0).only_enforce_if(start_lit) - - # if node i is last. - end_lit = model.new_bool_var(f"end_{i}") - arcs.append((i + 1, 0, end_lit)) - - for j in all_tasks: - if i == j: - arcs.append((i + 1, i + 1, ~presences[i])) - model.add(ranks[i] == -1).only_enforce_if(~presences[i]) - else: - literal = model.new_bool_var(f"arc_{i}_to_{j}") - arcs.append((i + 1, j + 1, literal)) - model.add(ranks[j] == ranks[i] + 1).only_enforce_if(literal) - - # To perform the transitive reduction from precedences to successors, - # we need to tie the starts of the tasks with 'literal'. - # In a pure problem, the following inequality could be an equality. - # It is not true in general. - # - # Note that we could use this literal to penalize the transition, add an - # extra delay to the precedence. - model.add(starts[j] >= starts[i] + durations[i]).only_enforce_if( - literal - ) - - # Manage the empty circuit - empty = model.new_bool_var("empty") - arcs.append((0, 0, empty)) - - for i in all_tasks: - model.add_implication(empty, ~presences[i]) - - # Add the circuit constraint. - model.add_circuit(arcs) - - -def ranking_sample_sat() -> None: - """Ranks tasks in a NoOverlap constraint.""" - - model = cp_model.CpModel() - horizon = 100 - num_tasks = 4 - all_tasks = range(num_tasks) - - starts = [] - durations = [] - intervals = [] - presences = [] - ranks = [] - - # Creates intervals, half of them are optional. - for t in all_tasks: - start = model.new_int_var(0, horizon, f"start[{t}]") - duration = t + 1 - presence = model.new_bool_var(f"presence[{t}]") - interval = model.new_optional_fixed_size_interval_var( - start, duration, presence, f"opt_interval[{t}]" + """This method uses a circuit constraint to rank tasks. + + This method assumes that all starts are disjoint, meaning that all tasks have + a strictly positive duration, and they appear in the same NoOverlap + constraint. + + To implement this ranking, we will create a dense graph with num_tasks + 1 + nodes. + The extra node (with id 0) will be used to decide which task is first with + its only outgoing arc, and which task is last with its only incoming arc. + Each task i will be associated with id i + 1, and an arc between i + 1 and j + + 1 indicates that j is the immediate successor of i. + + The circuit constraint ensures there is at most 1 hamiltonian cycle of + length > 1. If no such path exists, then no tasks are active. + We also need to enforce that any hamiltonian cycle of size > 1 must contain + the node 0. And thus, there is a self loop on node 0 iff the circuit is empty. + + Args: + model: The CpModel to add the constraints to. + starts: The array of starts variables of all tasks. + durations: the durations of all tasks. + presences: The array of presence variables of all tasks. + ranks: The array of rank variables of all tasks. + """ + + num_tasks = len(starts) + all_tasks = range(num_tasks) + + arcs: List[cp_model.ArcT] = [] + for i in all_tasks: + # if node i is first. + start_lit = model.new_bool_var(f"start_{i}") + arcs.append((0, i + 1, start_lit)) + model.add(ranks[i] == 0).only_enforce_if(start_lit) + + # As there are no other constraints on the problem, we can add this + # redundant constraint. + model.add(starts[i] == 0).only_enforce_if(start_lit) + + # if node i is last. + end_lit = model.new_bool_var(f"end_{i}") + arcs.append((i + 1, 0, end_lit)) + + for j in all_tasks: + if i == j: + arcs.append((i + 1, i + 1, ~presences[i])) + model.add(ranks[i] == -1).only_enforce_if(~presences[i]) + else: + literal = model.new_bool_var(f"arc_{i}_to_{j}") + arcs.append((i + 1, j + 1, literal)) + model.add(ranks[j] == ranks[i] + 1).only_enforce_if(literal) + + # To perform the transitive reduction from precedences to successors, + # we need to tie the starts of the tasks with 'literal'. + # In a pure problem, the following inequality could be an equality. + # It is not true in general. + # + # Note that we could use this literal to penalize the transition, add an + # extra delay to the precedence. + model.add(starts[j] >= starts[i] + durations[i]).only_enforce_if( + literal ) - if t < num_tasks // 2: - model.add(presence == 1) - - starts.append(start) - durations.append(duration) - intervals.append(interval) - presences.append(presence) - # Ranks = -1 if and only if the tasks is not performed. - ranks.append(model.new_int_var(-1, num_tasks - 1, f"rank[{t}]")) + # Manage the empty circuit + empty = model.new_bool_var("empty") + arcs.append((0, 0, empty)) - # Adds NoOverlap constraint. - model.add_no_overlap(intervals) + for i in all_tasks: + model.add_implication(empty, ~presences[i]) - # Adds ranking constraint. - rank_tasks_with_circuit(model, starts, durations, presences, ranks) + # Add the circuit constraint. + model.add_circuit(arcs) - # Adds a constraint on ranks. - model.add(ranks[0] < ranks[1]) - # Creates makespan variable. - makespan = model.new_int_var(0, horizon, "makespan") +def ranking_sample_sat() -> None: + """Ranks tasks in a NoOverlap constraint.""" + + model = cp_model.CpModel() + horizon = 100 + num_tasks = 4 + all_tasks = range(num_tasks) + + starts = [] + durations = [] + intervals = [] + presences = [] + ranks = [] + + # Creates intervals, half of them are optional. + for t in all_tasks: + start = model.new_int_var(0, horizon, f"start[{t}]") + duration = t + 1 + presence = model.new_bool_var(f"presence[{t}]") + interval = model.new_optional_fixed_size_interval_var( + start, duration, presence, f"opt_interval[{t}]" + ) + if t < num_tasks // 2: + model.add(presence == 1) + + starts.append(start) + durations.append(duration) + intervals.append(interval) + presences.append(presence) + + # Ranks = -1 if and only if the tasks is not performed. + ranks.append(model.new_int_var(-1, num_tasks - 1, f"rank[{t}]")) + + # Adds NoOverlap constraint. + model.add_no_overlap(intervals) + + # Adds ranking constraint. + rank_tasks_with_circuit(model, starts, durations, presences, ranks) + + # Adds a constraint on ranks. + model.add(ranks[0] < ranks[1]) + + # Creates makespan variable. + makespan = model.new_int_var(0, horizon, "makespan") + for t in all_tasks: + model.add(starts[t] + durations[t] <= makespan).only_enforce_if( + presences[t] + ) + + # Minimizes makespan - fixed gain per tasks performed. + # As the fixed cost is less that the duration of the last interval, + # the solver will not perform the last interval. + model.minimize(2 * makespan - 7 * sum(presences[t] for t in all_tasks)) + + # Solves the model model. + solver = cp_model.CpSolver() + status = solver.solve(model) + + if status == cp_model.OPTIMAL: + # Prints out the makespan and the start times and ranks of all tasks. + print(f"Optimal cost: {solver.objective_value}") + print(f"Makespan: {solver.value(makespan)}") for t in all_tasks: - model.add(starts[t] + durations[t] <= makespan).only_enforce_if(presences[t]) - - # Minimizes makespan - fixed gain per tasks performed. - # As the fixed cost is less that the duration of the last interval, - # the solver will not perform the last interval. - model.minimize(2 * makespan - 7 * sum(presences[t] for t in all_tasks)) - - # Solves the model model. - solver = cp_model.CpSolver() - status = solver.solve(model) - - if status == cp_model.OPTIMAL: - # Prints out the makespan and the start times and ranks of all tasks. - print(f"Optimal cost: {solver.objective_value}") - print(f"Makespan: {solver.value(makespan)}") - for t in all_tasks: - if solver.value(presences[t]): - print( - f"Task {t} starts at {solver.value(starts[t])} " - f"with rank {solver.value(ranks[t])}" - ) - else: - print( - f"Task {t} in not performed and ranked at {solver.value(ranks[t])}" - ) - else: - print(f"Solver exited with nonoptimal status: {status}") + if solver.value(presences[t]): + print( + f"Task {t} starts at {solver.value(starts[t])} " + f"with rank {solver.value(ranks[t])}" + ) + else: + print( + f"Task {t} in not performed and ranked at {solver.value(ranks[t])}" + ) + else: + print(f"Solver exited with nonoptimal status: {status}") ranking_sample_sat() diff --git a/ortools/sat/samples/ranking_sample_sat.py b/ortools/sat/samples/ranking_sample_sat.py index 13bd9bbb980..cacaa5bdf37 100644 --- a/ortools/sat/samples/ranking_sample_sat.py +++ b/ortools/sat/samples/ranking_sample_sat.py @@ -23,138 +23,138 @@ def rank_tasks( presences: list[cp_model.BoolVarT], ranks: list[cp_model.IntVar], ) -> None: - """This method adds constraints and variables to links tasks and ranks. - - This method assumes that all starts are disjoint, meaning that all tasks have - a strictly positive duration, and they appear in the same NoOverlap - constraint. - - Args: - model: The CpModel to add the constraints to. - starts: The array of starts variables of all tasks. - presences: The array of presence variables or constants of all tasks. - ranks: The array of rank variables of all tasks. - """ - - num_tasks = len(starts) - all_tasks = range(num_tasks) - - # Creates precedence variables between pairs of intervals. - precedences: dict[tuple[int, int], cp_model.BoolVarT] = {} - for i in all_tasks: - for j in all_tasks: - if i == j: - precedences[(i, j)] = presences[i] - else: - prec = model.new_bool_var(f"{i} before {j}") - precedences[(i, j)] = prec - model.add(starts[i] < starts[j]).only_enforce_if(prec) - - # Treats optional intervals. - for i in range(num_tasks - 1): - for j in range(i + 1, num_tasks): - tmp_array: list[cp_model.BoolVarT] = [ - precedences[(i, j)], - precedences[(j, i)], - ] - if not cp_model.object_is_a_true_literal(presences[i]): - tmp_array.append(~presences[i]) - # Makes sure that if i is not performed, all precedences are false. - model.add_implication(~presences[i], ~precedences[(i, j)]) - model.add_implication(~presences[i], ~precedences[(j, i)]) - if not cp_model.object_is_a_true_literal(presences[j]): - tmp_array.append(~presences[j]) - # Makes sure that if j is not performed, all precedences are false. - model.add_implication(~presences[j], ~precedences[(i, j)]) - model.add_implication(~presences[j], ~precedences[(j, i)]) - # The following bool_or will enforce that for any two intervals: - # i precedes j or j precedes i or at least one interval is not - # performed. - model.add_bool_or(tmp_array) - # Redundant constraint: it propagates early that at most one precedence - # is true. - model.add_implication(precedences[(i, j)], ~precedences[(j, i)]) - model.add_implication(precedences[(j, i)], ~precedences[(i, j)]) - - # Links precedences and ranks. - for i in all_tasks: - model.add(ranks[i] == sum(precedences[(j, i)] for j in all_tasks) - 1) + """This method adds constraints and variables to links tasks and ranks. + + This method assumes that all starts are disjoint, meaning that all tasks have + a strictly positive duration, and they appear in the same NoOverlap + constraint. + + Args: + model: The CpModel to add the constraints to. + starts: The array of starts variables of all tasks. + presences: The array of presence variables or constants of all tasks. + ranks: The array of rank variables of all tasks. + """ + + num_tasks = len(starts) + all_tasks = range(num_tasks) + + # Creates precedence variables between pairs of intervals. + precedences: dict[tuple[int, int], cp_model.BoolVarT] = {} + for i in all_tasks: + for j in all_tasks: + if i == j: + precedences[(i, j)] = presences[i] + else: + prec = model.new_bool_var(f"{i} before {j}") + precedences[(i, j)] = prec + model.add(starts[i] < starts[j]).only_enforce_if(prec) + + # Treats optional intervals. + for i in range(num_tasks - 1): + for j in range(i + 1, num_tasks): + tmp_array: list[cp_model.BoolVarT] = [ + precedences[(i, j)], + precedences[(j, i)], + ] + if not cp_model.object_is_a_true_literal(presences[i]): + tmp_array.append(~presences[i]) + # Makes sure that if i is not performed, all precedences are false. + model.add_implication(~presences[i], ~precedences[(i, j)]) + model.add_implication(~presences[i], ~precedences[(j, i)]) + if not cp_model.object_is_a_true_literal(presences[j]): + tmp_array.append(~presences[j]) + # Makes sure that if j is not performed, all precedences are false. + model.add_implication(~presences[j], ~precedences[(i, j)]) + model.add_implication(~presences[j], ~precedences[(j, i)]) + # The following bool_or will enforce that for any two intervals: + # i precedes j or j precedes i or at least one interval is not + # performed. + model.add_bool_or(tmp_array) + # Redundant constraint: it propagates early that at most one precedence + # is true. + model.add_implication(precedences[(i, j)], ~precedences[(j, i)]) + model.add_implication(precedences[(j, i)], ~precedences[(i, j)]) + + # Links precedences and ranks. + for i in all_tasks: + model.add(ranks[i] == sum(precedences[(j, i)] for j in all_tasks) - 1) def ranking_sample_sat() -> None: - """Ranks tasks in a NoOverlap constraint.""" - - model = cp_model.CpModel() - horizon = 100 - num_tasks = 4 - all_tasks = range(num_tasks) - - starts = [] - ends = [] - intervals = [] - presences: list[cp_model.BoolVarT] = [] - ranks = [] - - # Creates intervals, half of them are optional. - for t in all_tasks: - start = model.new_int_var(0, horizon, f"start[{t}]") - duration = t + 1 - end = model.new_int_var(0, horizon, f"end[{t}]") - if t < num_tasks // 2: - interval = model.new_interval_var(start, duration, end, f"interval[{t}]") - presence = model.new_constant(1) - else: - presence = model.new_bool_var(f"presence[{t}]") - interval = model.new_optional_interval_var( - start, duration, end, presence, f"o_interval[{t}]" - ) - starts.append(start) - ends.append(end) - intervals.append(interval) - presences.append(presence) - - # Ranks = -1 if and only if the tasks is not performed. - ranks.append(model.new_int_var(-1, num_tasks - 1, f"rank[{t}]")) - - # Adds NoOverlap constraint. - model.add_no_overlap(intervals) - - # Adds ranking constraint. - rank_tasks(model, starts, presences, ranks) - - # Adds a constraint on ranks. - model.add(ranks[0] < ranks[1]) - - # Creates makespan variable. - makespan = model.new_int_var(0, horizon, "makespan") - for t in all_tasks: - model.add(ends[t] <= makespan).only_enforce_if(presences[t]) - - # Minimizes makespan - fixed gain per tasks performed. - # As the fixed cost is less that the duration of the last interval, - # the solver will not perform the last interval. - model.minimize(2 * makespan - 7 * sum(presences[t] for t in all_tasks)) - - # Solves the model model. - solver = cp_model.CpSolver() - status = solver.solve(model) - - if status == cp_model.OPTIMAL: - # Prints out the makespan and the start times and ranks of all tasks. - print(f"Optimal cost: {solver.objective_value}") - print(f"Makespan: {solver.value(makespan)}") - for t in all_tasks: - if solver.value(presences[t]): - print( - f"Task {t} starts at {solver.value(starts[t])} " - f"with rank {solver.value(ranks[t])}" - ) - else: - print( - f"Task {t} in not performed and ranked at {solver.value(ranks[t])}" - ) + """Ranks tasks in a NoOverlap constraint.""" + + model = cp_model.CpModel() + horizon = 100 + num_tasks = 4 + all_tasks = range(num_tasks) + + starts = [] + ends = [] + intervals = [] + presences: list[cp_model.BoolVarT] = [] + ranks = [] + + # Creates intervals, half of them are optional. + for t in all_tasks: + start = model.new_int_var(0, horizon, f"start[{t}]") + duration = t + 1 + end = model.new_int_var(0, horizon, f"end[{t}]") + if t < num_tasks // 2: + interval = model.new_interval_var(start, duration, end, f"interval[{t}]") + presence = model.new_constant(1) else: - print(f"Solver exited with nonoptimal status: {status}") + presence = model.new_bool_var(f"presence[{t}]") + interval = model.new_optional_interval_var( + start, duration, end, presence, f"o_interval[{t}]" + ) + starts.append(start) + ends.append(end) + intervals.append(interval) + presences.append(presence) + + # Ranks = -1 if and only if the tasks is not performed. + ranks.append(model.new_int_var(-1, num_tasks - 1, f"rank[{t}]")) + + # Adds NoOverlap constraint. + model.add_no_overlap(intervals) + + # Adds ranking constraint. + rank_tasks(model, starts, presences, ranks) + + # Adds a constraint on ranks. + model.add(ranks[0] < ranks[1]) + + # Creates makespan variable. + makespan = model.new_int_var(0, horizon, "makespan") + for t in all_tasks: + model.add(ends[t] <= makespan).only_enforce_if(presences[t]) + + # Minimizes makespan - fixed gain per tasks performed. + # As the fixed cost is less that the duration of the last interval, + # the solver will not perform the last interval. + model.minimize(2 * makespan - 7 * sum(presences[t] for t in all_tasks)) + + # Solves the model model. + solver = cp_model.CpSolver() + status = solver.solve(model) + + if status == cp_model.OPTIMAL: + # Prints out the makespan and the start times and ranks of all tasks. + print(f"Optimal cost: {solver.objective_value}") + print(f"Makespan: {solver.value(makespan)}") + for t in all_tasks: + if solver.value(presences[t]): + print( + f"Task {t} starts at {solver.value(starts[t])} " + f"with rank {solver.value(ranks[t])}" + ) + else: + print( + f"Task {t} in not performed and ranked at {solver.value(ranks[t])}" + ) + else: + print(f"Solver exited with nonoptimal status: {status}") ranking_sample_sat() diff --git a/ortools/sat/samples/reified_sample_sat.py b/ortools/sat/samples/reified_sample_sat.py index 48d95cd5779..c2473c5a8b4 100644 --- a/ortools/sat/samples/reified_sample_sat.py +++ b/ortools/sat/samples/reified_sample_sat.py @@ -18,23 +18,23 @@ def reified_sample_sat(): - """Showcase creating a reified constraint.""" - model = cp_model.CpModel() + """Showcase creating a reified constraint.""" + model = cp_model.CpModel() - x = model.new_bool_var("x") - y = model.new_bool_var("y") - b = model.new_bool_var("b") + x = model.new_bool_var("x") + y = model.new_bool_var("y") + b = model.new_bool_var("b") - # First version using a half-reified bool and. - model.add_bool_and(x, ~y).only_enforce_if(b) + # First version using a half-reified bool and. + model.add_bool_and(x, ~y).only_enforce_if(b) - # Second version using implications. - model.add_implication(b, x) - model.add_implication(b, ~y) + # Second version using implications. + model.add_implication(b, x) + model.add_implication(b, ~y) - # Third version using bool or. - model.add_bool_or(~b, x) - model.add_bool_or(~b, ~y) + # Third version using bool or. + model.add_bool_or(~b, x) + model.add_bool_or(~b, ~y) reified_sample_sat() diff --git a/ortools/sat/samples/schedule_requests_sat.py b/ortools/sat/samples/schedule_requests_sat.py index f89e5475a98..0cfd501dc11 100644 --- a/ortools/sat/samples/schedule_requests_sat.py +++ b/ortools/sat/samples/schedule_requests_sat.py @@ -23,121 +23,161 @@ def main() -> None: - # This program tries to find an optimal assignment of nurses to shifts - # (3 shifts per day, for 7 days), subject to some constraints (see below). - # Each nurse can request to be assigned to specific shifts. - # The optimal assignment maximizes the number of fulfilled shift requests. - # [START data] - num_nurses = 5 - num_shifts = 3 - num_days = 7 - all_nurses = range(num_nurses) - all_shifts = range(num_shifts) - all_days = range(num_days) - shift_requests = [ - [[0, 0, 1], [0, 0, 0], [0, 0, 0], [0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 1]], - [[0, 0, 0], [0, 0, 0], [0, 1, 0], [0, 1, 0], [1, 0, 0], [0, 0, 0], [0, 0, 1]], - [[0, 1, 0], [0, 1, 0], [0, 0, 0], [1, 0, 0], [0, 0, 0], [0, 1, 0], [0, 0, 0]], - [[0, 0, 1], [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 0], [1, 0, 0], [0, 0, 0]], - [[0, 0, 0], [0, 0, 1], [0, 1, 0], [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 0]], - ] - # [END data] - - # Creates the model. - # [START model] - model = cp_model.CpModel() - # [END model] - - # Creates shift variables. - # shifts[(n, d, s)]: nurse 'n' works shift 's' on day 'd'. - # [START variables] - shifts = {} - for n in all_nurses: - for d in all_days: - for s in all_shifts: - shifts[(n, d, s)] = model.new_bool_var(f"shift_n{n}_d{d}_s{s}") - # [END variables] - - # Each shift is assigned to exactly one nurse in . - # [START exactly_one_nurse] + # This program tries to find an optimal assignment of nurses to shifts + # (3 shifts per day, for 7 days), subject to some constraints (see below). + # Each nurse can request to be assigned to specific shifts. + # The optimal assignment maximizes the number of fulfilled shift requests. + # [START data] + num_nurses = 5 + num_shifts = 3 + num_days = 7 + all_nurses = range(num_nurses) + all_shifts = range(num_shifts) + all_days = range(num_days) + shift_requests = [ + [ + [0, 0, 1], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 1], + [0, 1, 0], + [0, 0, 1], + ], + [ + [0, 0, 0], + [0, 0, 0], + [0, 1, 0], + [0, 1, 0], + [1, 0, 0], + [0, 0, 0], + [0, 0, 1], + ], + [ + [0, 1, 0], + [0, 1, 0], + [0, 0, 0], + [1, 0, 0], + [0, 0, 0], + [0, 1, 0], + [0, 0, 0], + ], + [ + [0, 0, 1], + [0, 0, 0], + [1, 0, 0], + [0, 1, 0], + [0, 0, 0], + [1, 0, 0], + [0, 0, 0], + ], + [ + [0, 0, 0], + [0, 0, 1], + [0, 1, 0], + [0, 0, 0], + [1, 0, 0], + [0, 1, 0], + [0, 0, 0], + ], + ] + # [END data] + + # Creates the model. + # [START model] + model = cp_model.CpModel() + # [END model] + + # Creates shift variables. + # shifts[(n, d, s)]: nurse 'n' works shift 's' on day 'd'. + # [START variables] + shifts = {} + for n in all_nurses: for d in all_days: + for s in all_shifts: + shifts[(n, d, s)] = model.new_bool_var(f"shift_n{n}_d{d}_s{s}") + # [END variables] + + # Each shift is assigned to exactly one nurse in . + # [START exactly_one_nurse] + for d in all_days: + for s in all_shifts: + model.add_exactly_one(shifts[(n, d, s)] for n in all_nurses) + # [END exactly_one_nurse] + + # Each nurse works at most one shift per day. + # [START at_most_one_shift] + for n in all_nurses: + for d in all_days: + model.add_at_most_one(shifts[(n, d, s)] for s in all_shifts) + # [END at_most_one_shift] + + # [START assign_nurses_evenly] + # Try to distribute the shifts evenly, so that each nurse works + # min_shifts_per_nurse shifts. If this is not possible, because the total + # number of shifts is not divisible by the number of nurses, some nurses will + # be assigned one more shift. + min_shifts_per_nurse = (num_shifts * num_days) // num_nurses + if num_shifts * num_days % num_nurses == 0: + max_shifts_per_nurse = min_shifts_per_nurse + else: + max_shifts_per_nurse = min_shifts_per_nurse + 1 + for n in all_nurses: + num_shifts_worked: Union[cp_model.LinearExpr, int] = 0 + for d in all_days: + for s in all_shifts: + num_shifts_worked += shifts[(n, d, s)] + model.add(min_shifts_per_nurse <= num_shifts_worked) + model.add(num_shifts_worked <= max_shifts_per_nurse) + # [END assign_nurses_evenly] + + # [START objective] + model.maximize( + sum( + shift_requests[n][d][s] * shifts[(n, d, s)] + for n in all_nurses + for d in all_days + for s in all_shifts + ) + ) + # [END objective] + + # Creates the solver and solve. + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] + + # [START print_solution] + if status == cp_model.OPTIMAL: + print("Solution:") + for d in all_days: + print("Day", d) + for n in all_nurses: for s in all_shifts: - model.add_exactly_one(shifts[(n, d, s)] for n in all_nurses) - # [END exactly_one_nurse] - - # Each nurse works at most one shift per day. - # [START at_most_one_shift] - for n in all_nurses: - for d in all_days: - model.add_at_most_one(shifts[(n, d, s)] for s in all_shifts) - # [END at_most_one_shift] - - # [START assign_nurses_evenly] - # Try to distribute the shifts evenly, so that each nurse works - # min_shifts_per_nurse shifts. If this is not possible, because the total - # number of shifts is not divisible by the number of nurses, some nurses will - # be assigned one more shift. - min_shifts_per_nurse = (num_shifts * num_days) // num_nurses - if num_shifts * num_days % num_nurses == 0: - max_shifts_per_nurse = min_shifts_per_nurse - else: - max_shifts_per_nurse = min_shifts_per_nurse + 1 - for n in all_nurses: - num_shifts_worked: Union[cp_model.LinearExpr, int] = 0 - for d in all_days: - for s in all_shifts: - num_shifts_worked += shifts[(n, d, s)] - model.add(min_shifts_per_nurse <= num_shifts_worked) - model.add(num_shifts_worked <= max_shifts_per_nurse) - # [END assign_nurses_evenly] - - # [START objective] - model.maximize( - sum( - shift_requests[n][d][s] * shifts[(n, d, s)] - for n in all_nurses - for d in all_days - for s in all_shifts - ) + if solver.value(shifts[(n, d, s)]) == 1: + if shift_requests[n][d][s] == 1: + print("Nurse", n, "works shift", s, "(requested).") + else: + print("Nurse", n, "works shift", s, "(not requested).") + print() + print( + f"Number of shift requests met = {solver.objective_value}", + f"(out of {num_nurses * min_shifts_per_nurse})", ) - # [END objective] - - # Creates the solver and solve. - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] - - # [START print_solution] - if status == cp_model.OPTIMAL: - print("Solution:") - for d in all_days: - print("Day", d) - for n in all_nurses: - for s in all_shifts: - if solver.value(shifts[(n, d, s)]) == 1: - if shift_requests[n][d][s] == 1: - print("Nurse", n, "works shift", s, "(requested).") - else: - print("Nurse", n, "works shift", s, "(not requested).") - print() - print( - f"Number of shift requests met = {solver.objective_value}", - f"(out of {num_nurses * min_shifts_per_nurse})", - ) - else: - print("No optimal solution found !") - # [END print_solution] - - # Statistics. - # [START statistics] - print("\nStatistics") - print(f" - conflicts: {solver.num_conflicts}") - print(f" - branches : {solver.num_branches}") - print(f" - wall time: {solver.wall_time}s") - # [END statistics] + else: + print("No optimal solution found !") + # [END print_solution] + + # Statistics. + # [START statistics] + print("\nStatistics") + print(f" - conflicts: {solver.num_conflicts}") + print(f" - branches : {solver.num_branches}") + print(f" - wall time: {solver.wall_time}s") + # [END statistics] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/sat/samples/scheduling_with_calendar_sample_sat.py b/ortools/sat/samples/scheduling_with_calendar_sample_sat.py index c74a2fe01e5..a1b7eb88875 100644 --- a/ortools/sat/samples/scheduling_with_calendar_sample_sat.py +++ b/ortools/sat/samples/scheduling_with_calendar_sample_sat.py @@ -18,63 +18,63 @@ class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables + def __init__(self, variables: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables - def on_solution_callback(self) -> None: - for v in self.__variables: - print(f"{v}={self.value(v)}", end=" ") - print() + def on_solution_callback(self) -> None: + for v in self.__variables: + print(f"{v}={self.value(v)}", end=" ") + print() def scheduling_with_calendar_sample_sat(): - """Interval spanning across a lunch break.""" - model = cp_model.CpModel() - - # The data is the following: - # Work starts at 8h, ends at 18h, with a lunch break between 13h and 14h. - # We need to schedule a task that needs 3 hours of processing time. - # Total duration can be 3 or 4 (if it spans the lunch break). - # - # Because the duration is at least 3 hours, work cannot start after 15h. - # Because of the break, work cannot start at 13h. - - start = model.new_int_var_from_domain( - cp_model.Domain.from_intervals([(8, 12), (14, 15)]), "start" - ) - duration = model.new_int_var(3, 4, "duration") - end = model.new_int_var(8, 18, "end") - unused_interval = model.new_interval_var(start, duration, end, "interval") - - # We have 2 states (spanning across lunch or not) - across = model.new_bool_var("across") - non_spanning_hours = cp_model.Domain.from_values([8, 9, 10, 14, 15]) - model.add_linear_expression_in_domain(start, non_spanning_hours).only_enforce_if( - ~across - ) - model.add_linear_constraint(start, 11, 12).only_enforce_if(across) - model.add(duration == 3).only_enforce_if(~across) - model.add(duration == 4).only_enforce_if(across) - - # Search for x values in increasing order. - model.add_decision_strategy( - [start], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE - ) - - # Create a solver and solve with a fixed search. - solver = cp_model.CpSolver() - - # Force the solver to follow the decision strategy exactly. - solver.parameters.search_branching = cp_model.FIXED_SEARCH - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - - # Search and print all solutions. - solution_printer = VarArraySolutionPrinter([start, duration, across]) - solver.solve(model, solution_printer) + """Interval spanning across a lunch break.""" + model = cp_model.CpModel() + + # The data is the following: + # Work starts at 8h, ends at 18h, with a lunch break between 13h and 14h. + # We need to schedule a task that needs 3 hours of processing time. + # Total duration can be 3 or 4 (if it spans the lunch break). + # + # Because the duration is at least 3 hours, work cannot start after 15h. + # Because of the break, work cannot start at 13h. + + start = model.new_int_var_from_domain( + cp_model.Domain.from_intervals([(8, 12), (14, 15)]), "start" + ) + duration = model.new_int_var(3, 4, "duration") + end = model.new_int_var(8, 18, "end") + unused_interval = model.new_interval_var(start, duration, end, "interval") + + # We have 2 states (spanning across lunch or not) + across = model.new_bool_var("across") + non_spanning_hours = cp_model.Domain.from_values([8, 9, 10, 14, 15]) + model.add_linear_expression_in_domain( + start, non_spanning_hours + ).only_enforce_if(~across) + model.add_linear_constraint(start, 11, 12).only_enforce_if(across) + model.add(duration == 3).only_enforce_if(~across) + model.add(duration == 4).only_enforce_if(across) + + # Search for x values in increasing order. + model.add_decision_strategy( + [start], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE + ) + + # Create a solver and solve with a fixed search. + solver = cp_model.CpSolver() + + # Force the solver to follow the decision strategy exactly. + solver.parameters.search_branching = cp_model.FIXED_SEARCH + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + + # Search and print all solutions. + solution_printer = VarArraySolutionPrinter([start, duration, across]) + solver.solve(model, solution_printer) scheduling_with_calendar_sample_sat() diff --git a/ortools/sat/samples/search_for_all_solutions_sample_sat.py b/ortools/sat/samples/search_for_all_solutions_sample_sat.py index 37fb756a53d..9c43de0867e 100644 --- a/ortools/sat/samples/search_for_all_solutions_sample_sat.py +++ b/ortools/sat/samples/search_for_all_solutions_sample_sat.py @@ -20,57 +20,57 @@ # [START print_solution] class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables - self.__solution_count = 0 + def __init__(self, variables: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables + self.__solution_count = 0 - def on_solution_callback(self) -> None: - self.__solution_count += 1 - for v in self.__variables: - print(f"{v}={self.value(v)}", end=" ") - print() + def on_solution_callback(self) -> None: + self.__solution_count += 1 + for v in self.__variables: + print(f"{v}={self.value(v)}", end=" ") + print() - @property - def solution_count(self) -> int: - return self.__solution_count - # [END print_solution] + @property + def solution_count(self) -> int: + return self.__solution_count + # [END print_solution] def search_for_all_solutions_sample_sat(): - """Showcases calling the solver to search for all solutions.""" - # Creates the model. - # [START model] - model = cp_model.CpModel() - # [END model] + """Showcases calling the solver to search for all solutions.""" + # Creates the model. + # [START model] + model = cp_model.CpModel() + # [END model] - # Creates the variables. - # [START variables] - num_vals = 3 - x = model.new_int_var(0, num_vals - 1, "x") - y = model.new_int_var(0, num_vals - 1, "y") - z = model.new_int_var(0, num_vals - 1, "z") - # [END variables] + # Creates the variables. + # [START variables] + num_vals = 3 + x = model.new_int_var(0, num_vals - 1, "x") + y = model.new_int_var(0, num_vals - 1, "y") + z = model.new_int_var(0, num_vals - 1, "z") + # [END variables] - # Create the constraints. - # [START constraints] - model.add(x != y) - # [END constraints] + # Create the constraints. + # [START constraints] + model.add(x != y) + # [END constraints] - # Create a solver and solve. - # [START solve] - solver = cp_model.CpSolver() - solution_printer = VarArraySolutionPrinter([x, y, z]) - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - # Solve. - status = solver.solve(model, solution_printer) - # [END solve] + # Create a solver and solve. + # [START solve] + solver = cp_model.CpSolver() + solution_printer = VarArraySolutionPrinter([x, y, z]) + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + # Solve. + status = solver.solve(model, solution_printer) + # [END solve] - print(f"Status = {solver.status_name(status)}") - print(f"Number of solutions found: {solution_printer.solution_count}") + print(f"Status = {solver.status_name(status)}") + print(f"Number of solutions found: {solution_printer.solution_count}") search_for_all_solutions_sample_sat() diff --git a/ortools/sat/samples/sequences_in_no_overlap_sample_sat.py b/ortools/sat/samples/sequences_in_no_overlap_sample_sat.py index 7e1ff86c739..2d94043e54d 100644 --- a/ortools/sat/samples/sequences_in_no_overlap_sample_sat.py +++ b/ortools/sat/samples/sequences_in_no_overlap_sample_sat.py @@ -29,270 +29,273 @@ def sequence_constraints_with_circuit( sequence_length_constraints: Dict[str, Tuple[int, int]], sequence_cumul_constraints: Dict[str, Tuple[int, int, int]], ) -> Sequence[Tuple[cp_model.IntVar, int]]: - """This method enforces constraints on sequences of tasks of the same type. - - This method assumes that all durations are strictly positive. - - The extra node (with id 0) will be used to decide which task is first with - its only outgoing arc, and which task is last with its only incoming arc. - Each task i will be associated with id i + 1, and an arc between i + 1 and j + - 1 indicates that j is the immediate successor of i. - - The circuit constraint ensures there is at most 1 hamiltonian cycle of - length > 1. If no such path exists, then no tasks are active. - In this simplified model, all tasks must be performed. - - Args: - model: The CpModel to add the constraints to. - starts: The array of starts variables of all tasks. - durations: the durations of all tasks. - task_types: The type of all tasks. - lengths: the number of tasks of the same type in the current sequence. - cumuls: The computed cumul of the current sequence for each task. - sequence_length_constraints: the array of tuple (`task_type`, (`length_min`, - `length_max`)) that specifies the minimum and maximum length of the - sequence of tasks of type `task_type`. - sequence_cumul_constraints: the array of tuple (`task_type`, (`soft_max`, - `linear_penalty`, `hard_max`)) that specifies that if the cumul of the - sequence of tasks of type `task_type` is greater than `soft_max`, then - `linear_penalty * (cumul - soft_max)` is added to the cost - - Returns: - The list of pairs (integer variables, penalty) to be added to the objective. - """ - - num_tasks = len(starts) - all_tasks = range(num_tasks) - - arcs: List[cp_model.ArcT] = [] - for i in all_tasks: - # if node i is first. - start_lit = model.new_bool_var(f"start_{i}") - arcs.append((0, i + 1, start_lit)) - model.add(lengths[i] == 1).only_enforce_if(start_lit) - model.add(cumuls[i] == durations[i]).only_enforce_if(start_lit) - - # As there are no other constraints on the problem, we can add this - # redundant constraint. This is not valid in general. - model.add(starts[i] == 0).only_enforce_if(start_lit) - - # if node i is last. - end_lit = model.new_bool_var(f"end_{i}") - arcs.append((i + 1, 0, end_lit)) + """This method enforces constraints on sequences of tasks of the same type. + + This method assumes that all durations are strictly positive. + + The extra node (with id 0) will be used to decide which task is first with + its only outgoing arc, and which task is last with its only incoming arc. + Each task i will be associated with id i + 1, and an arc between i + 1 and j + + 1 indicates that j is the immediate successor of i. + + The circuit constraint ensures there is at most 1 hamiltonian cycle of + length > 1. If no such path exists, then no tasks are active. + In this simplified model, all tasks must be performed. + + Args: + model: The CpModel to add the constraints to. + starts: The array of starts variables of all tasks. + durations: the durations of all tasks. + task_types: The type of all tasks. + lengths: the number of tasks of the same type in the current sequence. + cumuls: The computed cumul of the current sequence for each task. + sequence_length_constraints: the array of tuple (`task_type`, (`length_min`, + `length_max`)) that specifies the minimum and maximum length of the + sequence of tasks of type `task_type`. + sequence_cumul_constraints: the array of tuple (`task_type`, (`soft_max`, + `linear_penalty`, `hard_max`)) that specifies that if the cumul of the + sequence of tasks of type `task_type` is greater than `soft_max`, then + `linear_penalty * (cumul - soft_max)` is added to the cost + + Returns: + The list of pairs (integer variables, penalty) to be added to the objective. + """ + + num_tasks = len(starts) + all_tasks = range(num_tasks) + + arcs: List[cp_model.ArcT] = [] + for i in all_tasks: + # if node i is first. + start_lit = model.new_bool_var(f"start_{i}") + arcs.append((0, i + 1, start_lit)) + model.add(lengths[i] == 1).only_enforce_if(start_lit) + model.add(cumuls[i] == durations[i]).only_enforce_if(start_lit) + + # As there are no other constraints on the problem, we can add this + # redundant constraint. This is not valid in general. + model.add(starts[i] == 0).only_enforce_if(start_lit) + + # if node i is last. + end_lit = model.new_bool_var(f"end_{i}") + arcs.append((i + 1, 0, end_lit)) + + # Make sure the previous length is within bounds. + type_length_min = sequence_length_constraints[task_types[i]][0] + model.add(lengths[i] >= type_length_min).only_enforce_if(end_lit) + + for j in all_tasks: + if i == j: + continue + lit = model.new_bool_var(f"arc_{i}_to_{j}") + arcs.append((i + 1, j + 1, lit)) + + # The circuit constraint is use to enforce the consistency between the + # precedences relations and the successor arcs. This is implemented by + # adding the constraint that force the implication task j is the next of + # task i implies that start(j) is greater or equal than the end(i). + # + # In the majority of problems, the following equality must be an + # inequality. In that particular case, as there are no extra constraints, + # we can keep the equality between start(j) and end(i). + model.add(starts[j] == starts[i] + durations[i]).only_enforce_if(lit) + + # We add the constraints to incrementally maintain the length and the + # cumul variables of the sequence. + if task_types[i] == task_types[j]: # Same task type. + # Increase the length of the sequence by 1. + model.add(lengths[j] == lengths[i] + 1).only_enforce_if(lit) + + # Increase the cumul of the sequence by the duration of the task. + model.add(cumuls[j] == cumuls[i] + durations[j]).only_enforce_if(lit) + + else: + # Switching task type. task[i] is the last task of the previous + # sequence, task[j] is the first task of the new sequence. + # + # Reset the length to 1. + model.add(lengths[j] == 1).only_enforce_if(lit) # Make sure the previous length is within bounds. type_length_min = sequence_length_constraints[task_types[i]][0] - model.add(lengths[i] >= type_length_min).only_enforce_if(end_lit) - - for j in all_tasks: - if i == j: - continue - lit = model.new_bool_var(f"arc_{i}_to_{j}") - arcs.append((i + 1, j + 1, lit)) - - # The circuit constraint is use to enforce the consistency between the - # precedences relations and the successor arcs. This is implemented by - # adding the constraint that force the implication task j is the next of - # task i implies that start(j) is greater or equal than the end(i). - # - # In the majority of problems, the following equality must be an - # inequality. In that particular case, as there are no extra constraints, - # we can keep the equality between start(j) and end(i). - model.add(starts[j] == starts[i] + durations[i]).only_enforce_if(lit) - - # We add the constraints to incrementally maintain the length and the - # cumul variables of the sequence. - if task_types[i] == task_types[j]: # Same task type. - # Increase the length of the sequence by 1. - model.add(lengths[j] == lengths[i] + 1).only_enforce_if(lit) - - # Increase the cumul of the sequence by the duration of the task. - model.add(cumuls[j] == cumuls[i] + durations[j]).only_enforce_if(lit) - - else: - # Switching task type. task[i] is the last task of the previous - # sequence, task[j] is the first task of the new sequence. - # - # Reset the length to 1. - model.add(lengths[j] == 1).only_enforce_if(lit) - - # Make sure the previous length is within bounds. - type_length_min = sequence_length_constraints[task_types[i]][0] - model.add(lengths[i] >= type_length_min).only_enforce_if(lit) - - # Reset the cumul to the duration of the task. - model.add(cumuls[j] == durations[j]).only_enforce_if(lit) - - # Add the circuit constraint. - model.add_circuit(arcs) - - # Create the penalty terms. We can penalize each cumul locally. - penalty_terms = [] - for i in all_tasks: - # Penalize the cumul of the last task w.r.t. the soft max - soft_max, linear_penalty, hard_max = sequence_cumul_constraints[task_types[i]] - - # To make it separable per task, and avoid double counting, we use the - # following trick: - # reduced_excess = min(durations[i], max(0, cumul[i] - soft_max)) - if soft_max < hard_max: - excess = model.new_int_var(0, hard_max - soft_max, f"excess+_{i}") - model.add_max_equality(excess, [0, cumuls[i] - soft_max]) - reduced_excess = model.new_int_var(0, durations[i], f"reduced_excess_{i}") - model.add_min_equality(reduced_excess, [durations[i], excess]) - penalty_terms.append((reduced_excess, linear_penalty)) - - return penalty_terms + model.add(lengths[i] >= type_length_min).only_enforce_if(lit) + # Reset the cumul to the duration of the task. + model.add(cumuls[j] == durations[j]).only_enforce_if(lit) -def sequences_in_no_overlap_sample_sat(): - """Implement cumul and length constraints in a NoOverlap constraint.""" - - # Tasks (duration, type). - tasks = [ - (5, "A"), - (6, "A"), - (7, "A"), - (2, "A"), - (3, "A"), - (5, "B"), - (2, "B"), - (3, "B"), - (1, "B"), - (4, "B"), - (3, "B"), - (6, "B"), - (2, "B"), + # Add the circuit constraint. + model.add_circuit(arcs) + + # Create the penalty terms. We can penalize each cumul locally. + penalty_terms = [] + for i in all_tasks: + # Penalize the cumul of the last task w.r.t. the soft max + soft_max, linear_penalty, hard_max = sequence_cumul_constraints[ + task_types[i] ] - # Sequence length constraints per task_types: (hard_min, hard_max) - # - # Note that this constraint is very tight for task type B and will fail with - # an odd number of tasks of type B. - sequence_length_constraints = { - "A": (1, 3), - "B": (2, 2), - } - - # Sequence accumulated durations constraints per task_types: - # (soft_max, linear_penalty, hard_max) - sequence_cumul_constraints = { - "A": (6, 1, 10), - "B": (7, 0, 7), - } - - model: cp_model.CpModel = cp_model.CpModel() - horizon: int = sum(t[0] for t in tasks) - - num_tasks = len(tasks) - all_tasks = range(num_tasks) - - starts = [] - durations = [] - intervals = [] - task_types = [] - - # Creates intervals for each task. - for duration, task_type in tasks: - index = len(starts) - start = model.new_int_var(0, horizon - duration, f"start[{index}]") - interval = model.new_fixed_size_interval_var( - start, duration, f"interval[{index}]" - ) + # To make it separable per task, and avoid double counting, we use the + # following trick: + # reduced_excess = min(durations[i], max(0, cumul[i] - soft_max)) + if soft_max < hard_max: + excess = model.new_int_var(0, hard_max - soft_max, f"excess+_{i}") + model.add_max_equality(excess, [0, cumuls[i] - soft_max]) + reduced_excess = model.new_int_var(0, durations[i], f"reduced_excess_{i}") + model.add_min_equality(reduced_excess, [durations[i], excess]) + penalty_terms.append((reduced_excess, linear_penalty)) + + return penalty_terms + - starts.append(start) - durations.append(duration) - intervals.append(interval) - task_types.append(task_type) - - # Create length variables for each task. - lengths = [] - for i in all_tasks: - max_hard_length = sequence_length_constraints[task_types[i]][1] - lengths.append(model.new_int_var(1, max_hard_length, f"length_{i}")) - - # Create cumul variables for each task. - cumuls = [] - for i in all_tasks: - max_hard_cumul = sequence_cumul_constraints[task_types[i]][2] - cumuls.append(model.new_int_var(durations[i], max_hard_cumul, f"cumul_{i}")) - - # Adds NoOverlap constraint. - model.add_no_overlap(intervals) - - # Adds the constraints on the lengths and cumuls of maximal sequences of - # tasks of the same type. - penalty_terms = sequence_constraints_with_circuit( - model, - starts, - durations, - task_types, - lengths, - cumuls, - sequence_length_constraints, - sequence_cumul_constraints, +def sequences_in_no_overlap_sample_sat(): + """Implement cumul and length constraints in a NoOverlap constraint.""" + + # Tasks (duration, type). + tasks = [ + (5, "A"), + (6, "A"), + (7, "A"), + (2, "A"), + (3, "A"), + (5, "B"), + (2, "B"), + (3, "B"), + (1, "B"), + (4, "B"), + (3, "B"), + (6, "B"), + (2, "B"), + ] + + # Sequence length constraints per task_types: (hard_min, hard_max) + # + # Note that this constraint is very tight for task type B and will fail with + # an odd number of tasks of type B. + sequence_length_constraints = { + "A": (1, 3), + "B": (2, 2), + } + + # Sequence accumulated durations constraints per task_types: + # (soft_max, linear_penalty, hard_max) + sequence_cumul_constraints = { + "A": (6, 1, 10), + "B": (7, 0, 7), + } + + model: cp_model.CpModel = cp_model.CpModel() + horizon: int = sum(t[0] for t in tasks) + + num_tasks = len(tasks) + all_tasks = range(num_tasks) + + starts = [] + durations = [] + intervals = [] + task_types = [] + + # Creates intervals for each task. + for duration, task_type in tasks: + index = len(starts) + start = model.new_int_var(0, horizon - duration, f"start[{index}]") + interval = model.new_fixed_size_interval_var( + start, duration, f"interval[{index}]" ) - # Minimize the sum of penalties, - model.minimize(sum(var * penalty for var, penalty in penalty_terms)) - - # Solves the model model. - solver = cp_model.CpSolver() - status = solver.solve(model) - - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - # Prints out the makespan and the start times and lengths, cumuls at each - # step. - if status == cp_model.OPTIMAL: - print(f"Optimal cost: {solver.objective_value}") - else: - print(f"Feasible cost: {solver.objective_value}") - - to_sort = [] - for t in all_tasks: - to_sort.append((solver.value(starts[t]), t)) - to_sort.sort() - - sum_of_penalties = 0 - for i, (start, t) in enumerate(to_sort): - # Check length constraints. - length: int = solver.value(lengths[t]) - hard_min_length, hard_max_length = sequence_length_constraints[ - task_types[t] - ] - assert length >= 0 - assert length <= hard_max_length - if ( - i + 1 == len(to_sort) or task_types[t] != task_types[to_sort[i + 1][1]] - ): # End of sequence. - assert length >= hard_min_length - - # Check cumul constraints. - cumul: int = solver.value(cumuls[t]) - soft_max_cumul, penalty, hard_max_cumul = sequence_cumul_constraints[ - task_types[t] - ] - assert cumul >= 0 - assert cumul <= hard_max_cumul - - if cumul > soft_max_cumul: - penalty = penalty * (cumul - soft_max_cumul) - sum_of_penalties += penalty - print( - f"Task {t} of type {task_types[t]} with" - f" duration={durations[t]} starts at {start}, length={length}," - f" cumul={cumul} penalty={penalty}" - ) - else: - print( - f"Task {t} of type {task_types[t]} with duration" - f" {durations[t]} starts at {start}, length =" - f" {length}, cumul = {cumul} " - ) - - assert int(solver.objective_value) == sum_of_penalties + starts.append(start) + durations.append(duration) + intervals.append(interval) + task_types.append(task_type) + + # Create length variables for each task. + lengths = [] + for i in all_tasks: + max_hard_length = sequence_length_constraints[task_types[i]][1] + lengths.append(model.new_int_var(1, max_hard_length, f"length_{i}")) + + # Create cumul variables for each task. + cumuls = [] + for i in all_tasks: + max_hard_cumul = sequence_cumul_constraints[task_types[i]][2] + cumuls.append(model.new_int_var(durations[i], max_hard_cumul, f"cumul_{i}")) + + # Adds NoOverlap constraint. + model.add_no_overlap(intervals) + + # Adds the constraints on the lengths and cumuls of maximal sequences of + # tasks of the same type. + penalty_terms = sequence_constraints_with_circuit( + model, + starts, + durations, + task_types, + lengths, + cumuls, + sequence_length_constraints, + sequence_cumul_constraints, + ) + + # Minimize the sum of penalties, + model.minimize(sum(var * penalty for var, penalty in penalty_terms)) + + # Solves the model model. + solver = cp_model.CpSolver() + status = solver.solve(model) + + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + # Prints out the makespan and the start times and lengths, cumuls at each + # step. + if status == cp_model.OPTIMAL: + print(f"Optimal cost: {solver.objective_value}") else: - print(f"Solver exited with the following status: {status}") + print(f"Feasible cost: {solver.objective_value}") + + to_sort = [] + for t in all_tasks: + to_sort.append((solver.value(starts[t]), t)) + to_sort.sort() + + sum_of_penalties = 0 + for i, (start, t) in enumerate(to_sort): + # Check length constraints. + length: int = solver.value(lengths[t]) + hard_min_length, hard_max_length = sequence_length_constraints[ + task_types[t] + ] + assert length >= 0 + assert length <= hard_max_length + if ( + i + 1 == len(to_sort) + or task_types[t] != task_types[to_sort[i + 1][1]] + ): # End of sequence. + assert length >= hard_min_length + + # Check cumul constraints. + cumul: int = solver.value(cumuls[t]) + soft_max_cumul, penalty, hard_max_cumul = sequence_cumul_constraints[ + task_types[t] + ] + assert cumul >= 0 + assert cumul <= hard_max_cumul + + if cumul > soft_max_cumul: + penalty = penalty * (cumul - soft_max_cumul) + sum_of_penalties += penalty + print( + f"Task {t} of type {task_types[t]} with" + f" duration={durations[t]} starts at {start}, length={length}," + f" cumul={cumul} penalty={penalty}" + ) + else: + print( + f"Task {t} of type {task_types[t]} with duration" + f" {durations[t]} starts at {start}, length =" + f" {length}, cumul = {cumul} " + ) + + assert int(solver.objective_value) == sum_of_penalties + else: + print(f"Solver exited with the following status: {status}") sequences_in_no_overlap_sample_sat() diff --git a/ortools/sat/samples/simple_sat_program.py b/ortools/sat/samples/simple_sat_program.py index 3c2041c6cf9..c00bcd67b1a 100644 --- a/ortools/sat/samples/simple_sat_program.py +++ b/ortools/sat/samples/simple_sat_program.py @@ -21,39 +21,39 @@ def simple_sat_program(): - """Minimal CP-SAT example to showcase calling the solver.""" - # Creates the model. - # [START model] - model = cp_model.CpModel() - # [END model] - - # Creates the variables. - # [START variables] - num_vals = 3 - x = model.new_int_var(0, num_vals - 1, "x") - y = model.new_int_var(0, num_vals - 1, "y") - z = model.new_int_var(0, num_vals - 1, "z") - # [END variables] - - # Creates the constraints. - # [START constraints] - model.add(x != y) - # [END constraints] - - # Creates a solver and solves the model. - # [START solve] - solver = cp_model.CpSolver() - status = solver.solve(model) - # [END solve] - - # [START print_solution] - if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: - print(f"x = {solver.value(x)}") - print(f"y = {solver.value(y)}") - print(f"z = {solver.value(z)}") - else: - print("No solution found.") - # [END print_solution] + """Minimal CP-SAT example to showcase calling the solver.""" + # Creates the model. + # [START model] + model = cp_model.CpModel() + # [END model] + + # Creates the variables. + # [START variables] + num_vals = 3 + x = model.new_int_var(0, num_vals - 1, "x") + y = model.new_int_var(0, num_vals - 1, "y") + z = model.new_int_var(0, num_vals - 1, "z") + # [END variables] + + # Creates the constraints. + # [START constraints] + model.add(x != y) + # [END constraints] + + # Creates a solver and solves the model. + # [START solve] + solver = cp_model.CpSolver() + status = solver.solve(model) + # [END solve] + + # [START print_solution] + if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE: + print(f"x = {solver.value(x)}") + print(f"y = {solver.value(y)}") + print(f"z = {solver.value(z)}") + else: + print("No solution found.") + # [END print_solution] simple_sat_program() diff --git a/ortools/sat/samples/solution_hinting_sample_sat.py b/ortools/sat/samples/solution_hinting_sample_sat.py index 11cc3c52636..ae86a9b9f7a 100644 --- a/ortools/sat/samples/solution_hinting_sample_sat.py +++ b/ortools/sat/samples/solution_hinting_sample_sat.py @@ -19,42 +19,42 @@ def solution_hinting_sample_sat(): - """Showcases solution hinting.""" - # Creates the model. - # [START model] - model = cp_model.CpModel() - # [END model] - - # Creates the variables. - # [START variables] - num_vals = 3 - x = model.new_int_var(0, num_vals - 1, "x") - y = model.new_int_var(0, num_vals - 1, "y") - z = model.new_int_var(0, num_vals - 1, "z") - # [END variables] - - # Creates the constraints. - # [START constraints] - model.add(x != y) - # [END constraints] - - # [START objective] - model.maximize(x + 2 * y + 3 * z) - # [END objective] - - # Solution hinting: x <- 1, y <- 2 - model.add_hint(x, 1) - model.add_hint(y, 2) - - # Creates a solver and solves. - # [START solve] - solver = cp_model.CpSolver() - solution_printer = cp_model.VarArrayAndObjectiveSolutionPrinter([x, y, z]) - status = solver.solve(model, solution_printer) - # [END solve] - - print(f"Status = {solver.status_name(status)}") - print(f"Number of solutions found: {solution_printer.solution_count}") + """Showcases solution hinting.""" + # Creates the model. + # [START model] + model = cp_model.CpModel() + # [END model] + + # Creates the variables. + # [START variables] + num_vals = 3 + x = model.new_int_var(0, num_vals - 1, "x") + y = model.new_int_var(0, num_vals - 1, "y") + z = model.new_int_var(0, num_vals - 1, "z") + # [END variables] + + # Creates the constraints. + # [START constraints] + model.add(x != y) + # [END constraints] + + # [START objective] + model.maximize(x + 2 * y + 3 * z) + # [END objective] + + # Solution hinting: x <- 1, y <- 2 + model.add_hint(x, 1) + model.add_hint(y, 2) + + # Creates a solver and solves. + # [START solve] + solver = cp_model.CpSolver() + solution_printer = cp_model.VarArrayAndObjectiveSolutionPrinter([x, y, z]) + status = solver.solve(model, solution_printer) + # [END solve] + + print(f"Status = {solver.status_name(status)}") + print(f"Number of solutions found: {solution_printer.solution_count}") solution_hinting_sample_sat() diff --git a/ortools/sat/samples/solve_and_print_intermediate_solutions_sample_sat.py b/ortools/sat/samples/solve_and_print_intermediate_solutions_sample_sat.py index d22922954f1..492203d474c 100644 --- a/ortools/sat/samples/solve_and_print_intermediate_solutions_sample_sat.py +++ b/ortools/sat/samples/solve_and_print_intermediate_solutions_sample_sat.py @@ -21,60 +21,60 @@ # You need to subclass the cp_model.CpSolverSolutionCallback class. # [START print_solution] class VarArrayAndObjectiveSolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables - self.__solution_count = 0 + def __init__(self, variables: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables + self.__solution_count = 0 - def on_solution_callback(self) -> None: - print(f"Solution {self.__solution_count}") - print(f" objective value = {self.objective_value}") - for v in self.__variables: - print(f" {v}={self.value(v)}", end=" ") - print() - self.__solution_count += 1 + def on_solution_callback(self) -> None: + print(f"Solution {self.__solution_count}") + print(f" objective value = {self.objective_value}") + for v in self.__variables: + print(f" {v}={self.value(v)}", end=" ") + print() + self.__solution_count += 1 - @property - def solution_count(self) -> int: - return self.__solution_count - # [END print_solution] + @property + def solution_count(self) -> int: + return self.__solution_count + # [END print_solution] def solve_and_print_intermediate_solutions_sample_sat(): - """Showcases printing intermediate solutions found during search.""" - # Creates the model. - # [START model] - model = cp_model.CpModel() - # [END model] - - # Creates the variables. - # [START variables] - num_vals = 3 - x = model.new_int_var(0, num_vals - 1, "x") - y = model.new_int_var(0, num_vals - 1, "y") - z = model.new_int_var(0, num_vals - 1, "z") - # [END variables] - - # Creates the constraints. - # [START constraints] - model.add(x != y) - # [END constraints] - - # [START objective] - model.maximize(x + 2 * y + 3 * z) - # [END objective] - - # Creates a solver and solves. - # [START solve] - solver = cp_model.CpSolver() - solution_printer = VarArrayAndObjectiveSolutionPrinter([x, y, z]) - status = solver.solve(model, solution_printer) - # [END solve] - - print(f"Status = {solver.status_name(status)}") - print(f"Number of solutions found: {solution_printer.solution_count}") + """Showcases printing intermediate solutions found during search.""" + # Creates the model. + # [START model] + model = cp_model.CpModel() + # [END model] + + # Creates the variables. + # [START variables] + num_vals = 3 + x = model.new_int_var(0, num_vals - 1, "x") + y = model.new_int_var(0, num_vals - 1, "y") + z = model.new_int_var(0, num_vals - 1, "z") + # [END variables] + + # Creates the constraints. + # [START constraints] + model.add(x != y) + # [END constraints] + + # [START objective] + model.maximize(x + 2 * y + 3 * z) + # [END objective] + + # Creates a solver and solves. + # [START solve] + solver = cp_model.CpSolver() + solution_printer = VarArrayAndObjectiveSolutionPrinter([x, y, z]) + status = solver.solve(model, solution_printer) + # [END solve] + + print(f"Status = {solver.status_name(status)}") + print(f"Number of solutions found: {solution_printer.solution_count}") solve_and_print_intermediate_solutions_sample_sat() diff --git a/ortools/sat/samples/solve_with_time_limit_sample_sat.py b/ortools/sat/samples/solve_with_time_limit_sample_sat.py index 3f52f6209f0..bdfc60fbfab 100644 --- a/ortools/sat/samples/solve_with_time_limit_sample_sat.py +++ b/ortools/sat/samples/solve_with_time_limit_sample_sat.py @@ -19,29 +19,29 @@ def solve_with_time_limit_sample_sat(): - """Minimal CP-SAT example to showcase calling the solver.""" - # Creates the model. - model = cp_model.CpModel() - # Creates the variables. - num_vals = 3 - x = model.new_int_var(0, num_vals - 1, "x") - y = model.new_int_var(0, num_vals - 1, "y") - z = model.new_int_var(0, num_vals - 1, "z") - # Adds an all-different constraint. - model.add(x != y) - - # Creates a solver and solves the model. - solver = cp_model.CpSolver() - - # Sets a time limit of 10 seconds. - solver.parameters.max_time_in_seconds = 10.0 - - status = solver.solve(model) - - if status == cp_model.OPTIMAL: - print(f"x = {solver.value(x)}") - print(f"y = {solver.value(y)}") - print(f"z = {solver.value(z)}") + """Minimal CP-SAT example to showcase calling the solver.""" + # Creates the model. + model = cp_model.CpModel() + # Creates the variables. + num_vals = 3 + x = model.new_int_var(0, num_vals - 1, "x") + y = model.new_int_var(0, num_vals - 1, "y") + z = model.new_int_var(0, num_vals - 1, "z") + # Adds an all-different constraint. + model.add(x != y) + + # Creates a solver and solves the model. + solver = cp_model.CpSolver() + + # Sets a time limit of 10 seconds. + solver.parameters.max_time_in_seconds = 10.0 + + status = solver.solve(model) + + if status == cp_model.OPTIMAL: + print(f"x = {solver.value(x)}") + print(f"y = {solver.value(y)}") + print(f"z = {solver.value(z)}") solve_with_time_limit_sample_sat() diff --git a/ortools/sat/samples/step_function_sample_sat.py b/ortools/sat/samples/step_function_sample_sat.py index 5098bc66192..389601882a6 100644 --- a/ortools/sat/samples/step_function_sample_sat.py +++ b/ortools/sat/samples/step_function_sample_sat.py @@ -18,74 +18,76 @@ class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar]): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables + def __init__(self, variables: list[cp_model.IntVar]): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables - def on_solution_callback(self) -> None: - for v in self.__variables: - print(f"{v}={self.value(v)}", end=" ") - print() + def on_solution_callback(self) -> None: + for v in self.__variables: + print(f"{v}={self.value(v)}", end=" ") + print() def step_function_sample_sat(): - """Encode the step function.""" - - # Model. - model = cp_model.CpModel() - - # Declare our primary variable. - x = model.new_int_var(0, 20, "x") - - # Create the expression variable and implement the step function - # Note it is not defined for x == 2. - # - # - 3 - # -- -- --------- 2 - # 1 - # -- --- 0 - # 0 ================ 20 - # - expr = model.new_int_var(0, 3, "expr") - - # expr == 0 on [5, 6] U [8, 10] - b0 = model.new_bool_var("b0") - model.add_linear_expression_in_domain( - x, cp_model.Domain.from_intervals([(5, 6), (8, 10)]) - ).only_enforce_if(b0) - model.add(expr == 0).only_enforce_if(b0) - - # expr == 2 on [0, 1] U [3, 4] U [11, 20] - b2 = model.new_bool_var("b2") - model.add_linear_expression_in_domain( - x, cp_model.Domain.from_intervals([(0, 1), (3, 4), (11, 20)]) - ).only_enforce_if(b2) - model.add(expr == 2).only_enforce_if(b2) - - # expr == 3 when x == 7 - b3 = model.new_bool_var("b3") - model.add(x == 7).only_enforce_if(b3) - model.add(expr == 3).only_enforce_if(b3) - - # At least one bi is true. (we could use an exactly one constraint). - model.add_bool_or(b0, b2, b3) - - # Search for x values in increasing order. - model.add_decision_strategy([x], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE) - - # Create a solver and solve with a fixed search. - solver = cp_model.CpSolver() - - # Force the solver to follow the decision strategy exactly. - solver.parameters.search_branching = cp_model.FIXED_SEARCH - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - - # Search and print out all solutions. - solution_printer = VarArraySolutionPrinter([x, expr]) - solver.solve(model, solution_printer) + """Encode the step function.""" + + # Model. + model = cp_model.CpModel() + + # Declare our primary variable. + x = model.new_int_var(0, 20, "x") + + # Create the expression variable and implement the step function + # Note it is not defined for x == 2. + # + # - 3 + # -- -- --------- 2 + # 1 + # -- --- 0 + # 0 ================ 20 + # + expr = model.new_int_var(0, 3, "expr") + + # expr == 0 on [5, 6] U [8, 10] + b0 = model.new_bool_var("b0") + model.add_linear_expression_in_domain( + x, cp_model.Domain.from_intervals([(5, 6), (8, 10)]) + ).only_enforce_if(b0) + model.add(expr == 0).only_enforce_if(b0) + + # expr == 2 on [0, 1] U [3, 4] U [11, 20] + b2 = model.new_bool_var("b2") + model.add_linear_expression_in_domain( + x, cp_model.Domain.from_intervals([(0, 1), (3, 4), (11, 20)]) + ).only_enforce_if(b2) + model.add(expr == 2).only_enforce_if(b2) + + # expr == 3 when x == 7 + b3 = model.new_bool_var("b3") + model.add(x == 7).only_enforce_if(b3) + model.add(expr == 3).only_enforce_if(b3) + + # At least one bi is true. (we could use an exactly one constraint). + model.add_bool_or(b0, b2, b3) + + # Search for x values in increasing order. + model.add_decision_strategy( + [x], cp_model.CHOOSE_FIRST, cp_model.SELECT_MIN_VALUE + ) + + # Create a solver and solve with a fixed search. + solver = cp_model.CpSolver() + + # Force the solver to follow the decision strategy exactly. + solver.parameters.search_branching = cp_model.FIXED_SEARCH + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + + # Search and print out all solutions. + solution_printer = VarArraySolutionPrinter([x, expr]) + solver.solve(model, solution_printer) step_function_sample_sat() diff --git a/ortools/sat/samples/stop_after_n_solutions_sample_sat.py b/ortools/sat/samples/stop_after_n_solutions_sample_sat.py index 464a4bbc013..284d1de4f99 100644 --- a/ortools/sat/samples/stop_after_n_solutions_sample_sat.py +++ b/ortools/sat/samples/stop_after_n_solutions_sample_sat.py @@ -19,48 +19,48 @@ class VarArraySolutionPrinterWithLimit(cp_model.CpSolverSolutionCallback): - """Print intermediate solutions.""" + """Print intermediate solutions.""" - def __init__(self, variables: list[cp_model.IntVar], limit: int): - cp_model.CpSolverSolutionCallback.__init__(self) - self.__variables = variables - self.__solution_count = 0 - self.__solution_limit = limit + def __init__(self, variables: list[cp_model.IntVar], limit: int): + cp_model.CpSolverSolutionCallback.__init__(self) + self.__variables = variables + self.__solution_count = 0 + self.__solution_limit = limit - def on_solution_callback(self) -> None: - self.__solution_count += 1 - for v in self.__variables: - print(f"{v}={self.value(v)}", end=" ") - print() - if self.__solution_count >= self.__solution_limit: - print(f"Stop search after {self.__solution_limit} solutions") - self.stop_search() + def on_solution_callback(self) -> None: + self.__solution_count += 1 + for v in self.__variables: + print(f"{v}={self.value(v)}", end=" ") + print() + if self.__solution_count >= self.__solution_limit: + print(f"Stop search after {self.__solution_limit} solutions") + self.stop_search() - @property - def solution_count(self) -> int: - return self.__solution_count + @property + def solution_count(self) -> int: + return self.__solution_count def stop_after_n_solutions_sample_sat(): - """Showcases calling the solver to search for small number of solutions.""" - # Creates the model. - model = cp_model.CpModel() - # Creates the variables. - num_vals = 3 - x = model.new_int_var(0, num_vals - 1, "x") - y = model.new_int_var(0, num_vals - 1, "y") - z = model.new_int_var(0, num_vals - 1, "z") + """Showcases calling the solver to search for small number of solutions.""" + # Creates the model. + model = cp_model.CpModel() + # Creates the variables. + num_vals = 3 + x = model.new_int_var(0, num_vals - 1, "x") + y = model.new_int_var(0, num_vals - 1, "y") + z = model.new_int_var(0, num_vals - 1, "z") - # Create a solver and solve. - solver = cp_model.CpSolver() - solution_printer = VarArraySolutionPrinterWithLimit([x, y, z], 5) - # Enumerate all solutions. - solver.parameters.enumerate_all_solutions = True - # Solve. - status = solver.solve(model, solution_printer) - print(f"Status = {solver.status_name(status)}") - print(f"Number of solutions found: {solution_printer.solution_count}") - assert solution_printer.solution_count == 5 + # Create a solver and solve. + solver = cp_model.CpSolver() + solution_printer = VarArraySolutionPrinterWithLimit([x, y, z], 5) + # Enumerate all solutions. + solver.parameters.enumerate_all_solutions = True + # Solve. + status = solver.solve(model, solution_printer) + print(f"Status = {solver.status_name(status)}") + print(f"Number of solutions found: {solution_printer.solution_count}") + assert solution_printer.solution_count == 5 stop_after_n_solutions_sample_sat() diff --git a/ortools/sat/samples/transitions_in_no_overlap_sample_sat.py b/ortools/sat/samples/transitions_in_no_overlap_sample_sat.py index 5cbf236b377..eab8abaff71 100644 --- a/ortools/sat/samples/transitions_in_no_overlap_sample_sat.py +++ b/ortools/sat/samples/transitions_in_no_overlap_sample_sat.py @@ -27,177 +27,177 @@ def transitive_reduction_with_circuit_delays_and_penalties( penalties: Dict[Tuple[int, int], int], delays: Dict[Tuple[int, int], int], ) -> Sequence[Tuple[cp_model.IntVar, int]]: - """This method uses a circuit constraint to rank tasks. - - This method assumes that all starts are disjoint, meaning that all tasks have - a strictly positive duration, and they appear in the same NoOverlap - constraint. - - The extra node (with id 0) will be used to decide which task is first with - its only outgoing arc, and which task is last with its only incoming arc. - Each task i will be associated with id i + 1, and an arc between i + 1 and j + - 1 indicates that j is the immediate successor of i. - - The circuit constraint ensures there is at most 1 hamiltonian cycle of - length > 1. If no such path exists, then no tasks are active. - We also need to enforce that any hamiltonian cycle of size > 1 must contain - the node 0. And thus, there is a self loop on node 0 iff the circuit is empty. - - Args: - model: The CpModel to add the constraints to. - starts: The array of starts variables of all tasks. - durations: the durations of all tasks. - presences: The array of presence variables of all tasks. - penalties: the array of tuple (`tail_index`, `head_index`, `penalty`) that - specifies that if task `tail_index` is the successor of the task - `head_index`, then `penalty` must be added to the cost. - delays: the array of tuple (`tail_index`, `head_index`, `delay`) that - specifies that if task `tail_index` is the successor of the task - `head_index`, then an extra `delay` must be added between the end of the - first task and the start of the second task. - - Returns: - The list of pairs (Boolean variables, penalty) to be added to the objective. - """ - - num_tasks = len(starts) - all_tasks = range(num_tasks) - - arcs: List[cp_model.ArcT] = [] - penalty_terms = [] - for i in all_tasks: - # if node i is first. - start_lit = model.new_bool_var(f"start_{i}") - arcs.append((0, i + 1, start_lit)) - - # As there are no other constraints on the problem, we can add this - # redundant constraint. - model.add(starts[i] == 0).only_enforce_if(start_lit) - - # if node i is last. - end_lit = model.new_bool_var(f"end_{i}") - arcs.append((i + 1, 0, end_lit)) - - for j in all_tasks: - if i == j: - arcs.append((i + 1, i + 1, ~presences[i])) - else: - literal = model.new_bool_var(f"arc_{i}_to_{j}") - arcs.append((i + 1, j + 1, literal)) - - # To perform the transitive reduction from precedences to successors, - # we need to tie the starts of the tasks with 'literal'. - # In a pure problem, the following inequality could be an equality. - # It is not true in general. - # - # Note that we could use this literal to penalize the transition, add an - # extra delay to the precedence. - min_delay = 0 - key = (i, j) - if key in delays: - min_delay = delays[key] - model.add( - starts[j] >= starts[i] + durations[i] + min_delay - ).only_enforce_if(literal) - - # Create the penalties. - if key in penalties: - penalty_terms.append((literal, penalties[key])) - - # Manage the empty circuit - empty = model.new_bool_var("empty") - arcs.append((0, 0, empty)) - - for i in all_tasks: - model.add_implication(empty, ~presences[i]) - - # Add the circuit constraint. - model.add_circuit(arcs) - - return penalty_terms + """This method uses a circuit constraint to rank tasks. + + This method assumes that all starts are disjoint, meaning that all tasks have + a strictly positive duration, and they appear in the same NoOverlap + constraint. + + The extra node (with id 0) will be used to decide which task is first with + its only outgoing arc, and which task is last with its only incoming arc. + Each task i will be associated with id i + 1, and an arc between i + 1 and j + + 1 indicates that j is the immediate successor of i. + + The circuit constraint ensures there is at most 1 hamiltonian cycle of + length > 1. If no such path exists, then no tasks are active. + We also need to enforce that any hamiltonian cycle of size > 1 must contain + the node 0. And thus, there is a self loop on node 0 iff the circuit is empty. + + Args: + model: The CpModel to add the constraints to. + starts: The array of starts variables of all tasks. + durations: the durations of all tasks. + presences: The array of presence variables of all tasks. + penalties: the array of tuple (`tail_index`, `head_index`, `penalty`) that + specifies that if task `tail_index` is the successor of the task + `head_index`, then `penalty` must be added to the cost. + delays: the array of tuple (`tail_index`, `head_index`, `delay`) that + specifies that if task `tail_index` is the successor of the task + `head_index`, then an extra `delay` must be added between the end of the + first task and the start of the second task. + + Returns: + The list of pairs (Boolean variables, penalty) to be added to the objective. + """ + + num_tasks = len(starts) + all_tasks = range(num_tasks) + + arcs: List[cp_model.ArcT] = [] + penalty_terms = [] + for i in all_tasks: + # if node i is first. + start_lit = model.new_bool_var(f"start_{i}") + arcs.append((0, i + 1, start_lit)) + + # As there are no other constraints on the problem, we can add this + # redundant constraint. + model.add(starts[i] == 0).only_enforce_if(start_lit) + + # if node i is last. + end_lit = model.new_bool_var(f"end_{i}") + arcs.append((i + 1, 0, end_lit)) + + for j in all_tasks: + if i == j: + arcs.append((i + 1, i + 1, ~presences[i])) + else: + literal = model.new_bool_var(f"arc_{i}_to_{j}") + arcs.append((i + 1, j + 1, literal)) + + # To perform the transitive reduction from precedences to successors, + # we need to tie the starts of the tasks with 'literal'. + # In a pure problem, the following inequality could be an equality. + # It is not true in general. + # + # Note that we could use this literal to penalize the transition, add an + # extra delay to the precedence. + min_delay = 0 + key = (i, j) + if key in delays: + min_delay = delays[key] + model.add( + starts[j] >= starts[i] + durations[i] + min_delay + ).only_enforce_if(literal) + + # Create the penalties. + if key in penalties: + penalty_terms.append((literal, penalties[key])) + + # Manage the empty circuit + empty = model.new_bool_var("empty") + arcs.append((0, 0, empty)) + + for i in all_tasks: + model.add_implication(empty, ~presences[i]) + + # Add the circuit constraint. + model.add_circuit(arcs) + + return penalty_terms def transitions_in_no_overlap_sample_sat(): - """Implement transitions in a NoOverlap constraint.""" - - model = cp_model.CpModel() - horizon = 40 - num_tasks = 4 - - # Breaking the natural sequence induces a fixed penalty. - penalties = { - (1, 0): 10, - (2, 0): 10, - (3, 0): 10, - (2, 1): 10, - (3, 1): 10, - (3, 2): 10, - } - - # Switching from an odd to even or even to odd task indices induces a delay. - delays = { - (1, 0): 10, - (0, 1): 10, - (3, 0): 10, - (0, 3): 10, - (1, 2): 10, - (2, 1): 10, - (3, 2): 10, - (2, 3): 10, - } - - all_tasks = range(num_tasks) - - starts = [] - durations = [] - intervals = [] - presences = [] - - # Creates intervals, all present. But the cost is robust w.r.t. optional - # intervals. - for t in all_tasks: - start = model.new_int_var(0, horizon, f"start[{t}]") - duration = 5 - presence = True - interval = model.new_optional_fixed_size_interval_var( - start, duration, presence, f"opt_interval[{t}]" - ) - - starts.append(start) - durations.append(duration) - intervals.append(interval) - presences.append(presence) - - # Adds NoOverlap constraint. - model.add_no_overlap(intervals) - - # Adds ranking constraint. - penalty_terms = transitive_reduction_with_circuit_delays_and_penalties( - model, starts, durations, presences, penalties, delays + """Implement transitions in a NoOverlap constraint.""" + + model = cp_model.CpModel() + horizon = 40 + num_tasks = 4 + + # Breaking the natural sequence induces a fixed penalty. + penalties = { + (1, 0): 10, + (2, 0): 10, + (3, 0): 10, + (2, 1): 10, + (3, 1): 10, + (3, 2): 10, + } + + # Switching from an odd to even or even to odd task indices induces a delay. + delays = { + (1, 0): 10, + (0, 1): 10, + (3, 0): 10, + (0, 3): 10, + (1, 2): 10, + (2, 1): 10, + (3, 2): 10, + (2, 3): 10, + } + + all_tasks = range(num_tasks) + + starts = [] + durations = [] + intervals = [] + presences = [] + + # Creates intervals, all present. But the cost is robust w.r.t. optional + # intervals. + for t in all_tasks: + start = model.new_int_var(0, horizon, f"start[{t}]") + duration = 5 + presence = True + interval = model.new_optional_fixed_size_interval_var( + start, duration, presence, f"opt_interval[{t}]" ) - # Minimize the sum of penalties, - model.minimize(sum(var * penalty for var, penalty in penalty_terms)) - - # In practise, only one penalty can happen. Thus the two even tasks are - # together, same for the two odd tasks. - # Because of the penalties, the optimal sequence is 0 -> 2 -> 1 -> 3 - # which induces one penalty and one delay. - - # Solves the model model. - solver = cp_model.CpSolver() - status = solver.solve(model) - - if status == cp_model.OPTIMAL: - # Prints out the makespan and the start times and ranks of all tasks. - print(f"Optimal cost: {solver.objective_value}") - for t in all_tasks: - if solver.value(presences[t]): - print(f"Task {t} starts at {solver.value(starts[t])} ") - else: - print(f"Task {t} in not performed") - else: - print(f"Solver exited with nonoptimal status: {status}") + starts.append(start) + durations.append(duration) + intervals.append(interval) + presences.append(presence) + + # Adds NoOverlap constraint. + model.add_no_overlap(intervals) + + # Adds ranking constraint. + penalty_terms = transitive_reduction_with_circuit_delays_and_penalties( + model, starts, durations, presences, penalties, delays + ) + + # Minimize the sum of penalties, + model.minimize(sum(var * penalty for var, penalty in penalty_terms)) + + # In practise, only one penalty can happen. Thus the two even tasks are + # together, same for the two odd tasks. + # Because of the penalties, the optimal sequence is 0 -> 2 -> 1 -> 3 + # which induces one penalty and one delay. + + # Solves the model model. + solver = cp_model.CpSolver() + status = solver.solve(model) + + if status == cp_model.OPTIMAL: + # Prints out the makespan and the start times and ranks of all tasks. + print(f"Optimal cost: {solver.objective_value}") + for t in all_tasks: + if solver.value(presences[t]): + print(f"Task {t} starts at {solver.value(starts[t])} ") + else: + print(f"Task {t} in not performed") + else: + print(f"Solver exited with nonoptimal status: {status}") transitions_in_no_overlap_sample_sat() diff --git a/ortools/sat/sat_decision.h b/ortools/sat/sat_decision.h index acce6c12922..371c98223dc 100644 --- a/ortools/sat/sat_decision.h +++ b/ortools/sat/sat_decision.h @@ -108,12 +108,20 @@ class SatDecisionPolicy { // Like SetAssignmentPreference() but it can be overridden by phase-saving. void SetTargetPolarity(Literal l) { - var_polarity_[l.Variable()] = l.IsPositive(); + has_target_polarity_[l.Variable()] = true; + target_polarity_[l.Variable()] = var_polarity_[l.Variable()] = + l.IsPositive(); + best_partial_assignment_.push_back(l); + target_length_++; } absl::Span GetBestPartialAssignment() const { return best_partial_assignment_; } - void ClearBestPartialAssignment() { best_partial_assignment_.clear(); } + void ClearBestPartialAssignment() { + target_length_ = 0; + has_target_polarity_.assign(has_target_polarity_.size(), false); + best_partial_assignment_.clear(); + } private: // Computes an initial variable ordering. diff --git a/ortools/sat/sat_decision_test.cc b/ortools/sat/sat_decision_test.cc index 104a4a566d1..ff90d707728 100644 --- a/ortools/sat/sat_decision_test.cc +++ b/ortools/sat/sat_decision_test.cc @@ -95,6 +95,47 @@ TEST(SatDecisionPolicyTest, ErwaHeuristic) { EXPECT_EQ(Literal(BooleanVariable(2), true), decision->NextBranch()); } +TEST(SatDecisionPolicyTest, SetTargetPolarityInStablePhase) { + Model model; + Trail* trail = model.GetOrCreate(); + SatDecisionPolicy* decision = model.GetOrCreate(); + const int num_variables = 100; + trail->Resize(num_variables); + decision->IncreaseNumVariables(num_variables); + + for (int i = 0; i < num_variables; ++i) { + decision->SetTargetPolarity(Literal(BooleanVariable(i), i % 2)); + } + + decision->SetStablePhase(true); + for (int i = 0; i < num_variables; ++i) { + const Literal literal = decision->NextBranch(); + EXPECT_EQ(literal, Literal(BooleanVariable(literal.Variable()), + literal.Variable().value() % 2)); + trail->EnqueueSearchDecision(literal); + } +} + +TEST(SatDecisionPolicyTest, SetTargetPolarity) { + Model model; + Trail* trail = model.GetOrCreate(); + SatDecisionPolicy* decision = model.GetOrCreate(); + const int num_variables = 100; + trail->Resize(num_variables); + decision->IncreaseNumVariables(num_variables); + + for (int i = 0; i < num_variables; ++i) { + decision->SetTargetPolarity(Literal(BooleanVariable(i), i % 2)); + } + + decision->SetStablePhase(false); + for (int i = 0; i < num_variables; ++i) { + const Literal literal = decision->NextBranch(); + EXPECT_EQ(literal, Literal(BooleanVariable(literal.Variable()), + literal.Variable().value() % 2)); + trail->EnqueueSearchDecision(literal); + } +} } // namespace } // namespace sat } // namespace operations_research diff --git a/ortools/sat/sat_parameters.proto b/ortools/sat/sat_parameters.proto index fb7d23f541d..60901fc1c0f 100644 --- a/ortools/sat/sat_parameters.proto +++ b/ortools/sat/sat_parameters.proto @@ -24,7 +24,7 @@ option java_multiple_files = true; // Contains the definitions for all the sat algorithm parameters and their // default values. // -// NEXT TAG: 325 +// NEXT TAG: 327 message SatParameters { // In some context, like in a portfolio of search, it makes sense to name a // given parameters set for logging purpose. @@ -703,6 +703,13 @@ message SatParameters { // Allows sharing of the bounds of modified variables at level 0. optional bool share_level_zero_bounds = 114 [default = true]; + // Allows sharing of the bounds on linear2 discovered at level 0. This is + // mainly interesting on scheduling type of problems when we branch on + // precedences. + // + // Warning: This currently non-deterministic. + optional bool share_linear2_bounds = 326 [default = false]; + // Allows sharing of new learned binary clause between workers. optional bool share_binary_clauses = 203 [default = true]; @@ -1336,10 +1343,15 @@ message SatParameters { optional bool use_lns_only = 101 [default = false]; // Size of the top-n different solutions kept by the solver. - // This parameter must be > 0. - // Currently this only impact the "base" solution chosen for a LNS fragment. + // This parameter must be > 0. Currently, having this larger than one mainly + // impact the "base" solution chosen for a LNS/LS fragment. optional int32 solution_pool_size = 193 [default = 3]; + // In order to not get stuck in local optima, when this is non-zero, we try to + // also work on "older" solutions with a worse objective value so we get a + // chance to follow a different LS/LNS trajectory. + optional int32 alternative_pool_size = 325 [default = 1]; + // Turns on relaxation induced neighborhood generator. optional bool use_rins_lns = 129 [default = true]; diff --git a/ortools/sat/sat_runner.cc b/ortools/sat/sat_runner.cc index c31a0e2b27e..c1dceb038b4 100644 --- a/ortools/sat/sat_runner.cc +++ b/ortools/sat/sat_runner.cc @@ -16,9 +16,11 @@ #include #include #include +#include #include #include +#include "absl/base/thread_annotations.h" #include "absl/flags/flag.h" #include "absl/flags/parse.h" #include "absl/flags/usage.h" @@ -30,6 +32,8 @@ #include "absl/strings/str_format.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" +#include "absl/synchronization/mutex.h" +#include "absl/types/span.h" #include "google/protobuf/arena.h" #include "google/protobuf/text_format.h" #include "ortools/base/helpers.h" @@ -45,6 +49,7 @@ #include "ortools/sat/synchronization.h" #include "ortools/util/file_util.h" #include "ortools/util/logging.h" +#include "ortools/util/sigint.h" #include "ortools/util/sorted_interval_list.h" ABSL_FLAG( @@ -102,8 +107,69 @@ std::string ExtractName(absl::string_view full_filename) { return filename; } -void LogInPbCompetitionFormat(int num_variables, bool has_objective, - Model* model, SatParameters* parameters) { +class LastSolutionPrinter { + public: + // Note that is prints the solution in the PB competition format. + void MaybePrintLastSolution() { + absl::MutexLock lock(&mutex_); + if (last_solution_printed_) return; + last_solution_printed_ = true; + + if (last_solution_.empty()) { + std::cout << "s UNKNOWN" << std::endl; + } else { + std::cout << "s SATISFIABLE" << std::endl; + std::string line; + for (int i = 0; i < num_variables_; ++i) { + if (last_solution_[i]) { + absl::StrAppend(&line, "x", i + 1, " "); + } else { + absl::StrAppend(&line, "-x", i + 1, " "); + } + if (line.size() >= 75) { + std::cout << "v " << line << std::endl; + line.clear(); + } + } + if (!line.empty()) { + std::cout << "v " << line << std::endl; + } + } + } + + void set_num_variables(int num_variables) { num_variables_ = num_variables; } + + void set_last_solution(absl::Span solution) { + absl::MutexLock lock(&mutex_); + if (last_solution_printed_) return; + last_solution_.assign(solution.begin(), solution.end()); + } + + // Returns false if the solution has already been printed, else mark it as + // printed by caller code. + bool mark_last_solution_printed() { + const absl::MutexLock lock(&mutex_); + if (last_solution_printed_) { + return false; + } + last_solution_printed_ = true; + return true; + } + + private: + int num_variables_ = 0; + std::vector last_solution_ ABSL_GUARDED_BY(mutex_); + bool last_solution_printed_ ABSL_GUARDED_BY(mutex_) = false; + absl::Mutex mutex_; +}; + +void LogInPbCompetitionFormat( + int num_variables, bool has_objective, Model* model, + SatParameters* parameters, + std::shared_ptr last_solution_printer) { + CHECK(last_solution_printer != nullptr); + last_solution_printer->set_num_variables(num_variables); + const auto log_callback = [](const std::string& multi_line_input) { if (multi_line_input.empty()) { std::cout << "c" << std::endl; @@ -118,55 +184,60 @@ void LogInPbCompetitionFormat(int num_variables, bool has_objective, model->GetOrCreate()->AddInfoLoggingCallback(log_callback); parameters->set_log_to_stdout(false); - const auto response_callback = [](const CpSolverResponse& r) { + const auto response_callback = [last_solution_printer]( + const CpSolverResponse& r) { std::cout << "o " << static_cast(r.objective_value()) << std::endl; + last_solution_printer->set_last_solution(r.solution()); }; model->Add(NewFeasibleSolutionObserver(response_callback)); - const auto final_response_callback = [num_variables, - has_objective](CpSolverResponse* r) { - switch (r->status()) { - case CpSolverStatus::OPTIMAL: - if (has_objective) { - std::cout << "s OPTIMUM FOUND " << std::endl; - } else { - std::cout << "s SATISFIABLE" << std::endl; + const auto final_response_callback = + [num_variables, has_objective, + last_solution_printer](CpSolverResponse* r) { + if (!last_solution_printer->mark_last_solution_printed()) return; + + switch (r->status()) { + case CpSolverStatus::OPTIMAL: + if (has_objective) { + std::cout << "s OPTIMUM FOUND " << std::endl; + } else { + std::cout << "s SATISFIABLE" << std::endl; + } + break; + case CpSolverStatus::FEASIBLE: + std::cout << "s SATISFIABLE" << std::endl; + break; + case CpSolverStatus::INFEASIBLE: + std::cout << "s UNSATISFIABLE" << std::endl; + break; + case CpSolverStatus::MODEL_INVALID: + std::cout << "s UNSUPPORTED" << std::endl; + break; + case CpSolverStatus::UNKNOWN: + std::cout << "s UNKNOWN" << std::endl; + break; + default: + break; } - break; - case CpSolverStatus::FEASIBLE: - std::cout << "s SATISFIABLE" << std::endl; - break; - case CpSolverStatus::INFEASIBLE: - std::cout << "s UNSATISFIABLE" << std::endl; - break; - case CpSolverStatus::MODEL_INVALID: - std::cout << "s UNSUPPORTED" << std::endl; - break; - case CpSolverStatus::UNKNOWN: - std::cout << "s UNKNOWN" << std::endl; - break; - default: - break; - } - if (r->status() == CpSolverStatus::OPTIMAL || - r->status() == CpSolverStatus::FEASIBLE) { - std::string line; - for (int i = 0; i < num_variables; ++i) { - if (r->solution(i)) { - absl::StrAppend(&line, "x", i + 1, " "); - } else { - absl::StrAppend(&line, "-x", i + 1, " "); + if (r->status() == CpSolverStatus::OPTIMAL || + r->status() == CpSolverStatus::FEASIBLE) { + std::string line; + for (int i = 0; i < num_variables; ++i) { + if (r->solution(i)) { + absl::StrAppend(&line, "x", i + 1, " "); + } else { + absl::StrAppend(&line, "-x", i + 1, " "); + } + if (line.size() >= 75) { + std::cout << "v " << line << std::endl; + line.clear(); + } + } + if (!line.empty()) { + std::cout << "v " << line << std::endl; + } } - if (line.size() >= 75) { - std::cout << "v " << line << std::endl; - line.clear(); - } - } - if (!line.empty()) { - std::cout << "v " << line << std::endl; - } - } - }; + }; model->GetOrCreate()->AddFinalResponsePostprocessor( final_response_callback); } @@ -186,7 +257,8 @@ void SetInterleavedWorkers(SatParameters* parameters) { bool LoadProblem(const std::string& filename, absl::string_view hint_file, absl::string_view domain_file, CpModelProto* cp_model, - Model* model, SatParameters* parameters) { + Model* model, SatParameters* parameters, + std::shared_ptr last_solution_printer) { if (absl::EndsWith(filename, ".opb") || absl::EndsWith(filename, ".opb.bz2") || absl::EndsWith(filename, ".opb.gz") || absl::EndsWith(filename, ".wbo") || @@ -217,7 +289,7 @@ bool LoadProblem(const std::string& filename, absl::string_view hint_file, const int num_variables = reader.model_is_supported() ? reader.num_variables() : 1; LogInPbCompetitionFormat(num_variables, cp_model->has_objective(), model, - parameters); + parameters, last_solution_printer); } if (absl::GetFlag(FLAGS_force_interleave_search)) { SetInterleavedWorkers(parameters); @@ -310,9 +382,13 @@ int Run() { google::protobuf::Arena arena; CpModelProto* cp_model = google::protobuf::Arena::Create(&arena); + std::shared_ptr last_solution_printer; + if (absl::GetFlag(FLAGS_competition_mode)) { + last_solution_printer = std::make_shared(); + } if (!LoadProblem(absl::GetFlag(FLAGS_input), absl::GetFlag(FLAGS_hint_file), absl::GetFlag(FLAGS_domain_file), cp_model, &model, - ¶meters)) { + ¶meters, last_solution_printer)) { if (!absl::GetFlag(FLAGS_competition_mode)) { LOG(FATAL) << "Cannot load file '" << absl::GetFlag(FLAGS_input) << "'."; } @@ -329,6 +405,14 @@ int Run() { FingerprintRepeatedField(r.solution(), kDefaultFingerprintSeed)); })); } + + if (absl::GetFlag(FLAGS_competition_mode)) { + model.GetOrCreate()->Register([last_solution_printer]() { + last_solution_printer->MaybePrintLastSolution(); + exit(EXIT_SUCCESS); + }); + } + const CpSolverResponse response = SolveCpModel(*cp_model, &model); if (!absl::GetFlag(FLAGS_output).empty()) { diff --git a/ortools/sat/scheduling_cuts.cc b/ortools/sat/scheduling_cuts.cc index e51929b16d2..4c62279d7b3 100644 --- a/ortools/sat/scheduling_cuts.cc +++ b/ortools/sat/scheduling_cuts.cc @@ -338,9 +338,9 @@ std::vector FindPossibleDemands(const EnergyEvent& event, void GenerateCumulativeEnergeticCutsWithMakespanAndFixedCapacity( absl::string_view cut_name, const util_intops::StrongVector& lp_values, - std::vector events, IntegerValue capacity, + absl::Span events, IntegerValue capacity, AffineExpression makespan, TimeLimit* time_limit, Model* model, - LinearConstraintManager* manager) { + TopNCuts& top_n_cuts) { // Checks the precondition of the code. IntegerTrail* integer_trail = model->GetOrCreate(); DCHECK(integer_trail->IsFixed(capacity)); @@ -408,7 +408,6 @@ void GenerateCumulativeEnergeticCutsWithMakespanAndFixedCapacity( const double makespan_lp = makespan.LpValue(lp_values); const double makespan_min_lp = ToDouble(makespan_min); LinearConstraintBuilder temp_builder(model); - TopNCuts top_n_cuts(5); for (int i = 0; i + 1 < num_time_points; ++i) { // Checks the time limit if the problem is too big. if (events.size() > 50 && time_limit->LimitReached()) return; @@ -510,15 +509,13 @@ void GenerateCumulativeEnergeticCutsWithMakespanAndFixedCapacity( } } } - - top_n_cuts.TransferToManager(manager); } void GenerateCumulativeEnergeticCuts( absl::string_view cut_name, const util_intops::StrongVector& lp_values, - std::vector events, const AffineExpression& capacity, - TimeLimit* time_limit, Model* model, LinearConstraintManager* manager) { + absl::Span events, const AffineExpression& capacity, + TimeLimit* time_limit, Model* model, TopNCuts& top_n_cuts) { double max_possible_energy_lp = 0.0; for (const EnergyEvent& event : events) { max_possible_energy_lp += event.linearized_energy_lp_value; @@ -549,7 +546,6 @@ void GenerateCumulativeEnergeticCuts( const int num_time_points = time_points.size(); LinearConstraintBuilder temp_builder(model); - TopNCuts top_n_cuts(5); for (int i = 0; i + 1 < num_time_points; ++i) { // Checks the time limit if the problem is too big. if (events.size() > 50 && time_limit->LimitReached()) return; @@ -602,8 +598,6 @@ void GenerateCumulativeEnergeticCuts( } } } - - top_n_cuts.TransferToManager(manager); } CutGenerator CreateCumulativeEnergyCutGenerator( @@ -664,16 +658,24 @@ CutGenerator CreateCumulativeEnergyCutGenerator( events.push_back(e); } - if (makespan.has_value() && integer_trail->IsFixed(capacity)) { - GenerateCumulativeEnergeticCutsWithMakespanAndFixedCapacity( - "CumulativeEnergyM", lp_values, events, - integer_trail->FixedValue(capacity), makespan.value(), time_limit, - model, manager); + TopNCuts top_n_cuts(5); + std::vector> disjoint_events = + SplitEventsInIndendentSets(absl::MakeSpan(events)); + // Can we pass cluster as const. It would mean sorting before. + for (const absl::Span cluster : disjoint_events) { + if (makespan.has_value() && integer_trail->IsFixed(capacity)) { + GenerateCumulativeEnergeticCutsWithMakespanAndFixedCapacity( + "CumulativeEnergyM", lp_values, cluster, + integer_trail->FixedValue(capacity), makespan.value(), time_limit, + model, top_n_cuts); - } else { - GenerateCumulativeEnergeticCuts("CumulativeEnergy", lp_values, events, - capacity, time_limit, model, manager); + } else { + GenerateCumulativeEnergeticCuts("CumulativeEnergy", lp_values, cluster, + capacity, time_limit, model, + top_n_cuts); + } } + top_n_cuts.TransferToManager(manager); return true; }; @@ -716,16 +718,22 @@ CutGenerator CreateNoOverlapEnergyCutGenerator( events.push_back(e); } - if (makespan.has_value()) { - GenerateCumulativeEnergeticCutsWithMakespanAndFixedCapacity( - "NoOverlapEnergyM", lp_values, events, - /*capacity=*/IntegerValue(1), makespan.value(), time_limit, model, - manager); - } else { - GenerateCumulativeEnergeticCuts("NoOverlapEnergy", lp_values, events, - /*capacity=*/IntegerValue(1), time_limit, - model, manager); + TopNCuts top_n_cuts(5); + std::vector> disjoint_events = + SplitEventsInIndendentSets(absl::MakeSpan(events)); + for (const absl::Span cluster : disjoint_events) { + if (makespan.has_value()) { + GenerateCumulativeEnergeticCutsWithMakespanAndFixedCapacity( + "NoOverlapEnergyM", lp_values, cluster, + /*capacity=*/IntegerValue(1), makespan.value(), time_limit, model, + top_n_cuts); + } else { + GenerateCumulativeEnergeticCuts("NoOverlapEnergy", lp_values, cluster, + /*capacity=*/IntegerValue(1), + time_limit, model, top_n_cuts); + } } + top_n_cuts.TransferToManager(manager); return true; }; return result; @@ -889,9 +897,8 @@ struct CachedIntervalData { void GenerateCutsBetweenPairOfNonOverlappingTasks( absl::string_view cut_name, bool ignore_zero_size_intervals, const util_intops::StrongVector& lp_values, - std::vector events, IntegerValue capacity_max, - Model* model, LinearConstraintManager* manager) { - TopNCuts top_n_cuts(5); + absl::Span events, IntegerValue capacity_max, + Model* model, TopNCuts& top_n_cuts) { const int num_events = events.size(); if (num_events <= 1) return; @@ -984,8 +991,6 @@ void GenerateCutsBetweenPairOfNonOverlappingTasks( } } } - - top_n_cuts.TransferToManager(manager); } CutGenerator CreateCumulativePrecedenceCutGenerator( @@ -1014,9 +1019,16 @@ CutGenerator CreateCumulativePrecedenceCutGenerator( } const IntegerValue capacity_max = integer_trail->UpperBound(capacity); - GenerateCutsBetweenPairOfNonOverlappingTasks( - "Cumulative", /* ignore_zero_size_intervals= */ true, - manager->LpValues(), std::move(events), capacity_max, model, manager); + + TopNCuts top_n_cuts(5); + std::vector> disjoint_events = + SplitEventsInIndendentSets(absl::MakeSpan(events)); + for (const absl::Span cluster : disjoint_events) { + GenerateCutsBetweenPairOfNonOverlappingTasks( + "Cumulative", /* ignore_zero_size_intervals= */ true, + manager->LpValues(), cluster, capacity_max, model, top_n_cuts); + } + top_n_cuts.TransferToManager(manager); return true; }; return result; @@ -1042,10 +1054,15 @@ CutGenerator CreateNoOverlapPrecedenceCutGenerator( events.push_back(event); } - GenerateCutsBetweenPairOfNonOverlappingTasks( - "NoOverlap", /* ignore_zero_size_intervals= */ false, - manager->LpValues(), std::move(events), IntegerValue(1), model, - manager); + TopNCuts top_n_cuts(5); + std::vector> disjoint_events = + SplitEventsInIndendentSets(absl::MakeSpan(events)); + for (const absl::Span cluster : disjoint_events) { + GenerateCutsBetweenPairOfNonOverlappingTasks( + "NoOverlap", /* ignore_zero_size_intervals= */ false, + manager->LpValues(), cluster, IntegerValue(1), model, top_n_cuts); + } + top_n_cuts.TransferToManager(manager); return true; }; @@ -1102,33 +1119,45 @@ std::string CompletionTimeEvent::DebugString() const { void CtExhaustiveHelper::Init( const absl::Span events, Model* model) { - BinaryRelationsMaps* binary_relations = - model->GetOrCreate(); max_task_index_ = 0; + if (events.empty()) return; + + // We compute the max_task_index_ from the events early to avoid sorting + // the events if there are too many of them. for (const auto& event : events) { max_task_index_ = std::max(max_task_index_, event.task_index); } - predecessors_.reserve(max_task_index_ + 1); - for (const auto& e1 : events) { - CHECK_LE(predecessors_.size(), e1.task_index); - while (predecessors_.size() <= e1.task_index) { - predecessors_.Add({}); - } + BuildPredecessors(events, model); + VLOG(2) << "num_tasks:" << max_task_index_ + 1 + << " num_precedences:" << predecessors_.num_entries() + << " predecessors size:" << predecessors_.size(); +} + +void CtExhaustiveHelper::BuildPredecessors( + const absl::Span events, Model* model) { + predecessors_.clear(); + if (events.size() > 100) return; + + ReifiedLinear2Bounds* binary_relations = + model->GetOrCreate(); - // Cap the number of precedences to avoid O(n^2) time complexity. - if (predecessors_.num_entries() > 20000) break; + std::vector sorted_events(events.begin(), events.end()); + std::sort(sorted_events.begin(), sorted_events.end(), + [](const CompletionTimeEvent& a, const CompletionTimeEvent& b) { + return a.task_index < b.task_index; + }); - for (const auto& e2 : events) { + predecessors_.reserve(max_task_index_ + 1); + for (const auto& e1 : sorted_events) { + for (const auto& e2 : sorted_events) { if (e2.task_index == e1.task_index) continue; if (binary_relations->GetLevelZeroPrecedenceStatus(e2.end, e1.start) == RelationStatus::IS_TRUE) { + while (predecessors_.size() <= e1.task_index) predecessors_.Add({}); predecessors_.AppendToLastVector(e2.task_index); } } } - VLOG(2) << "num_tasks:" << max_task_index_ + 1 - << " num_precedences:" << predecessors_.num_entries() - << " predecessors size:" << predecessors_.size(); } bool CtExhaustiveHelper::PermutationIsCompatibleWithPrecedences( @@ -1138,6 +1167,7 @@ bool CtExhaustiveHelper::PermutationIsCompatibleWithPrecedences( visited_.assign(max_task_index_ + 1, false); for (int i = permutation.size() - 1; i >= 0; --i) { const CompletionTimeEvent& event = events[permutation[i]]; + if (event.task_index >= predecessors_.size()) continue; for (const int predecessor : predecessors_[event.task_index]) { if (visited_[predecessor]) return false; } @@ -1328,9 +1358,11 @@ CompletionTimeExplorationStatus ComputeMinSumOfWeightedEndMins( helper.task_to_index_[events[i].task_index] = i; } helper.valid_permutation_iterator_.Reset(events.size()); + const auto& predecessors = helper.predecessors(); for (int i = 0; i < events.size(); ++i) { const int task_i = events[i].task_index; - for (const int task_j : helper.predecessors()[task_i]) { + if (task_i >= predecessors.size()) continue; + for (const int task_j : predecessors[task_i]) { const int j = helper.task_to_index_[task_j]; if (j != -1) { helper.valid_permutation_iterator_.AddArc(j, i); @@ -1384,11 +1416,10 @@ CompletionTimeExplorationStatus ComputeMinSumOfWeightedEndMins( // - detect disjoint tasks (no need to crossover to the second part) // - better caching of explored states ABSL_MUST_USE_RESULT bool GenerateShortCompletionTimeCutsWithExactBound( - absl::string_view cut_name, std::vector events, - IntegerValue capacity_max, CtExhaustiveHelper& helper, Model* model, - LinearConstraintManager* manager) { - TopNCuts top_n_cuts(5); - + absl::string_view cut_name, + const util_intops::StrongVector& lp_values, + absl::Span events, IntegerValue capacity_max, + CtExhaustiveHelper& helper, Model* model, TopNCuts& top_n_cuts) { // Sort by start min to bucketize by start_min. std::sort( events.begin(), events.end(), @@ -1456,6 +1487,7 @@ ABSL_MUST_USE_RESULT bool GenerateShortCompletionTimeCutsWithExactBound( helper, min_sum_of_ends, min_sum_of_weighted_ends, cut_use_precedences, exploration_limit); if (status == CompletionTimeExplorationStatus::NO_VALID_PERMUTATION) { + // TODO(user): We should return false here but there is a bug. break; } else if (status == CompletionTimeExplorationStatus::ABORTED) { break; @@ -1480,7 +1512,7 @@ ABSL_MUST_USE_RESULT bool GenerateShortCompletionTimeCutsWithExactBound( std::string full_name(cut_name); if (cut_use_precedences) full_name.append("_prec"); if (is_lifted) full_name.append("_lifted"); - top_n_cuts.AddCut(cut.Build(), full_name, manager->LpValues()); + top_n_cuts.AddCut(cut.Build(), full_name, lp_values); } // Weighted cuts. @@ -1498,11 +1530,10 @@ ABSL_MUST_USE_RESULT bool GenerateShortCompletionTimeCutsWithExactBound( if (is_lifted) full_name.append("_lifted"); if (cut_use_precedences) full_name.append("_prec"); full_name.append("_weighted"); - top_n_cuts.AddCut(cut.Build(), full_name, manager->LpValues()); + top_n_cuts.AddCut(cut.Build(), full_name, lp_values); } } } - top_n_cuts.TransferToManager(manager); return true; } @@ -1619,9 +1650,10 @@ void AddEventDemandsToCapacitySubsetSum( // - second loop, we add tasks that must contribute after this start time // ordered by increasing end time in the LP relaxation. void GenerateCompletionTimeCutsWithEnergy( - absl::string_view cut_name, std::vector events, - IntegerValue capacity_max, Model* model, LinearConstraintManager* manager) { - TopNCuts top_n_cuts(5); + absl::string_view cut_name, + const util_intops::StrongVector& lp_values, + absl::Span events, IntegerValue capacity_max, + Model* model, TopNCuts& top_n_cuts) { const VariablesAssignment& assignment = model->GetOrCreate()->Assignment(); std::vector tmp_possible_demands; @@ -1772,10 +1804,9 @@ void GenerateCompletionTimeCutsWithEnergy( if (add_energy_to_name) full_name.append("_energy"); if (is_lifted) full_name.append("_lifted"); if (best_uses_subset_sum) full_name.append("_subsetsum"); - top_n_cuts.AddCut(cut.Build(), full_name, manager->LpValues()); + top_n_cuts.AddCut(cut.Build(), full_name, lp_values); } } - top_n_cuts.TransferToManager(manager); } CutGenerator CreateNoOverlapCompletionTimeCutGenerator( @@ -1809,15 +1840,21 @@ CutGenerator CreateNoOverlapCompletionTimeCutGenerator( CtExhaustiveHelper helper; helper.Init(events, model); - if (!GenerateShortCompletionTimeCutsWithExactBound( - "NoOverlapCompletionTimeExhaustive", events, - /*capacity_max=*/IntegerValue(1), helper, model, manager)) { - return false; - } + TopNCuts top_n_cuts(5); + std::vector> disjoint_events = + SplitEventsInIndendentSets(absl::MakeSpan(events)); + for (const absl::Span cluster : disjoint_events) { + if (!GenerateShortCompletionTimeCutsWithExactBound( + "NoOverlapCompletionTimeExhaustive", lp_values, cluster, + /*capacity_max=*/IntegerValue(1), helper, model, top_n_cuts)) { + return false; + } - GenerateCompletionTimeCutsWithEnergy( - "NoOverlapCompletionTimeQueyrane", std::move(events), - /*capacity_max=*/IntegerValue(1), model, manager); + GenerateCompletionTimeCutsWithEnergy( + "NoOverlapCompletionTimeQueyrane", lp_values, cluster, + /*capacity_max=*/IntegerValue(1), model, top_n_cuts); + } + top_n_cuts.TransferToManager(manager); return true; }; if (!generate_cuts(/*time_is_forward=*/true)) return false; @@ -1846,6 +1883,7 @@ CutGenerator CreateCumulativeCompletionTimeCutGenerator( auto generate_cuts = [integer_trail, sat_solver, model, manager, helper, demands_helper, capacity](bool time_is_forward) -> bool { + DCHECK_EQ(sat_solver->CurrentDecisionLevel(), 0); if (!helper->SynchronizeAndSetTimeDirection(time_is_forward)) { return false; } @@ -1872,15 +1910,21 @@ CutGenerator CreateCumulativeCompletionTimeCutGenerator( helper.Init(events, model); const IntegerValue capacity_max = integer_trail->UpperBound(capacity); - if (!GenerateShortCompletionTimeCutsWithExactBound( - "CumulativeCompletionTimeExhaustive", events, capacity_max, - helper, model, manager)) { - return false; - } + TopNCuts top_n_cuts(5); + std::vector> disjoint_events = + SplitEventsInIndendentSets(absl::MakeSpan(events)); + for (const absl::Span cluster : disjoint_events) { + if (!GenerateShortCompletionTimeCutsWithExactBound( + "CumulativeCompletionTimeExhaustive", lp_values, cluster, + capacity_max, helper, model, top_n_cuts)) { + return false; + } - GenerateCompletionTimeCutsWithEnergy("CumulativeCompletionTimeQueyrane", - std::move(events), capacity_max, - model, manager); + GenerateCompletionTimeCutsWithEnergy("CumulativeCompletionTimeQueyrane", + lp_values, cluster, capacity_max, + model, top_n_cuts); + } + top_n_cuts.TransferToManager(manager); return true; }; diff --git a/ortools/sat/scheduling_cuts.h b/ortools/sat/scheduling_cuts.h index 920f5a23e68..8b493eefa38 100644 --- a/ortools/sat/scheduling_cuts.h +++ b/ortools/sat/scheduling_cuts.h @@ -174,6 +174,9 @@ class CtExhaustiveHelper { absl::Span permutation); private: + void BuildPredecessors(absl::Span events, + Model* model); + CompactVectorVector predecessors_; int max_task_index_ = 0; std::vector visited_; @@ -215,6 +218,37 @@ CompletionTimeExplorationStatus ComputeMinSumOfWeightedEndMins( double& min_sum_of_weighted_ends, bool& cut_use_precedences, int& exploration_credit); +// Split the list of events in connected components. Two intervals are connected +// if they overlap. It expects the events to have the start_min and end_max +// fields. Note that events are semi-open intervals [start_min, end_max). This +// will filter out components of size one. +template +std::vector> SplitEventsInIndendentSets(absl::Span events) { + if (events.empty()) return {}; + + std::sort(events.begin(), events.end(), [](const E& a, const E& b) { + return std::tie(a.start_min, a.end_max) < std::tie(b.start_min, b.end_max); + }); + const int size = events.size(); + std::vector> result; + IntegerValue max_end_max = events[0].end_max; + int start = 0; + for (int i = 1; i < size; ++i) { + const E& event = events[i]; + if (event.start_min >= max_end_max) { + if (i - start > 1) { + result.push_back(absl::MakeSpan(events.data() + start, i - start)); + } + start = i; + } + max_end_max = std::max(max_end_max, event.end_max); + } + if (size - start > 1) { + result.push_back(absl::MakeSpan(events.data() + start, size - start)); + } + return result; +} + } // namespace sat } // namespace operations_research diff --git a/ortools/sat/scheduling_cuts_test.cc b/ortools/sat/scheduling_cuts_test.cc index 543bafd0197..5a51c9b5350 100644 --- a/ortools/sat/scheduling_cuts_test.cc +++ b/ortools/sat/scheduling_cuts_test.cc @@ -15,8 +15,8 @@ #include +#include #include -#include #include #include "absl/base/log_severity.h" @@ -587,7 +587,7 @@ double ExactMakespan(absl::Span sizes, std::vector& demands, } builder.Minimize(obj); const CpSolverResponse response = - SolveWithParameters(builder.Build(), "num_search_workers:8"); + SolveWithParameters(builder.Build(), "num_workers:8"); EXPECT_EQ(response.status(), CpSolverStatus::OPTIMAL); return response.objective_value(); } @@ -657,6 +657,35 @@ TEST(ComputeMinSumOfEndMinsTest, RandomCases) { } } +struct SimpleEvent { + IntegerValue start_min; + IntegerValue end_max; + bool operator==(const SimpleEvent& other) const { + return start_min == other.start_min && end_max == other.end_max; + } +}; + +SimpleEvent ConvexHull(absl::Span events) { + SimpleEvent result = events[0]; + for (int i = 1; i < events.size(); ++i) { + result.start_min = std::min(result.start_min, events[i].start_min); + result.end_max = std::max(result.end_max, events[i].end_max); + } + return result; +} + +TEST(SplitEventsInIndendentSetsTest, BasicTest) { + std::vector events = {{0, 10}, {2, 12}, {3, 5}, + {15, 20}, {12, 21}, {30, 35}}; + const std::vector> sets = + SplitEventsInIndendentSets(absl::MakeSpan(events)); + EXPECT_EQ(sets.size(), 2); + EXPECT_EQ(sets[0].size(), 3); + EXPECT_EQ(ConvexHull(sets[0]), SimpleEvent({0, 12})); + EXPECT_EQ(sets[1].size(), 2); + EXPECT_EQ(ConvexHull(sets[1]), SimpleEvent({12, 21})); +} + } // namespace } // namespace sat } // namespace operations_research diff --git a/ortools/sat/scheduling_helpers.cc b/ortools/sat/scheduling_helpers.cc index 69d0bad2562..9d25b17a7f0 100644 --- a/ortools/sat/scheduling_helpers.cc +++ b/ortools/sat/scheduling_helpers.cc @@ -48,7 +48,8 @@ SchedulingConstraintHelper::SchedulingConstraintHelper( assignment_(sat_solver_->Assignment()), integer_trail_(model->GetOrCreate()), watcher_(model->GetOrCreate()), - precedence_relations_(model->GetOrCreate()), + linear2_bounds_(model->GetOrCreate()), + root_level_lin2_bounds_(model->GetOrCreate()), starts_(std::move(starts)), ends_(std::move(ends)), sizes_(std::move(sizes)), @@ -86,7 +87,8 @@ SchedulingConstraintHelper::SchedulingConstraintHelper(int num_tasks, sat_solver_(model->GetOrCreate()), assignment_(sat_solver_->Assignment()), integer_trail_(model->GetOrCreate()), - precedence_relations_(model->GetOrCreate()), + linear2_bounds_(model->GetOrCreate()), + root_level_lin2_bounds_(model->GetOrCreate()), capacity_(num_tasks), cached_size_min_(new IntegerValue[capacity_]), cached_start_min_(new IntegerValue[capacity_]), @@ -340,27 +342,16 @@ bool SchedulingConstraintHelper::SynchronizeAndSetTimeDirection( return true; } -// TODO(user): be more precise when we know a and b are in disjunction. -// we really just need start_b > start_a, or even >= if duration is non-zero. IntegerValue SchedulingConstraintHelper::GetCurrentMinDistanceBetweenTasks( - int a, int b, bool add_reason_if_after) { + int a, int b) { const AffineExpression before = ends_[a]; const AffineExpression after = starts_[b]; - LinearExpression2 expr(before.var, after.var, before.coeff, -after.coeff); - - // We take the min of the level zero (end_a - start_b) and the one coming from - // a conditional precedence at true. - const IntegerValue conditional_ub = precedence_relations_->UpperBound(expr); - const IntegerValue level_zero_ub = integer_trail_->LevelZeroUpperBound(expr); - const IntegerValue expr_ub = std::min(conditional_ub, level_zero_ub); - + const LinearExpression2 expr(before.var, after.var, before.coeff, + -after.coeff); + const IntegerValue expr_ub = linear2_bounds_->UpperBound(expr); const IntegerValue needed_offset = before.constant - after.constant; const IntegerValue ub_of_end_minus_start = expr_ub + needed_offset; const IntegerValue distance = -ub_of_end_minus_start; - if (add_reason_if_after && distance >= 0 && level_zero_ub > conditional_ub) { - precedence_relations_->AddReasonForUpperBoundLowerThan( - expr, conditional_ub, MutableLiteralReason(), MutableIntegerReason()); - } return distance; } @@ -368,41 +359,31 @@ IntegerValue SchedulingConstraintHelper::GetCurrentMinDistanceBetweenTasks( // associated to task a before task b. However we only call this for task that // are in detectable precedence, which means the normal precedence or linear // propagator should have already propagated that Boolean too. -bool SchedulingConstraintHelper::PropagatePrecedence(int a, int b) { +bool SchedulingConstraintHelper::NotifyLevelZeroPrecedence(int a, int b) { CHECK(IsPresent(a)); CHECK(IsPresent(b)); CHECK_EQ(sat_solver_->CurrentDecisionLevel(), 0); - const AffineExpression before = ends_[a]; - const AffineExpression after = starts_[b]; - if (after.coeff != 1) return true; - if (before.coeff != 1) return true; - if (after.var == kNoIntegerVariable) return true; - if (before.var == kNoIntegerVariable) return true; - if (before.var == after.var) { - if (before.constant <= after.constant) { - return true; - } else { + // Convert ends_[a] <= starts[b] to linear2 <= rhs and canonicalize. + const auto [expr, rhs] = EncodeDifferenceLowerThan(ends_[a], starts_[b], 0); + + // Trivial case. + if (expr.coeffs[0] == 0 && expr.coeffs[1] == 0) { + if (rhs < 0) { sat_solver_->NotifyThatModelIsUnsat(); return false; } + return true; } - const IntegerValue offset = before.constant - after.constant; - const LinearExpression2 expr = - LinearExpression2::Difference(before.var, after.var); - if (precedence_relations_->AddUpperBound(expr, -offset)) { + + if (root_level_lin2_bounds_->AddUpperBound(expr, rhs)) { VLOG(2) << "new relation " << TaskDebugString(a) << " <= " << TaskDebugString(b); - if (before.var == NegationOf(after.var)) { - AddWeightedSumLowerOrEqual({}, {before.var}, {int64_t{2}}, - -offset.value(), model_); - } else { - // TODO(user): Adding new constraint during propagation might not be the - // best idea as it can create some complication. - AddWeightedSumLowerOrEqual({}, {before.var, after.var}, - {int64_t{1}, int64_t{-1}}, -offset.value(), - model_); - } + // TODO(user): Adding new constraint during propagation might not be the + // best idea as it can create some complication. + AddWeightedSumLowerOrEqual({}, {expr.vars[0], expr.vars[1]}, + {expr.coeffs[0].value(), expr.coeffs[1].value()}, + rhs.value(), model_); if (sat_solver_->ModelIsUnsat()) return false; } return true; @@ -496,12 +477,27 @@ SchedulingConstraintHelper::GetEnergyProfile() { return energy_profile_; } -// Produces a relaxed reason for StartMax(before) < EndMin(after). -void SchedulingConstraintHelper::AddReasonForBeingBefore(int before, - int after) { +void SchedulingConstraintHelper::AddReasonForBeingBeforeAssumingNoOverlap( + int before, int after) { AddOtherReason(before); AddOtherReason(after); + // Prefer the linear2 explanation as it is more likely this comes from + // level zero or a single enforcement literal. + // We need Start(after) >= End(before) - SizeMin(before). + // we rewrite as "End(before) - Start(after) <= SizeMin(before). + const auto [expr, ub] = + EncodeDifferenceLowerThan(ends_[before], starts_[after], SizeMin(before)); + if (linear2_bounds_->UpperBound(expr) <= ub) { + AddSizeMinReason(before); + linear2_bounds_->AddReasonForUpperBoundLowerThan(expr, ub, &literal_reason_, + &integer_reason_); + return; + } + + // We will explain StartMax(before) < EndMin(after); + DCHECK_LT(StartMax(before), EndMin(after)); + // The reason will be a linear expression greater than a value. Note that all // coeff must be positive, and we will use the variable lower bound. std::vector vars; diff --git a/ortools/sat/scheduling_helpers.h b/ortools/sat/scheduling_helpers.h index def922023e0..2d1daa38768 100644 --- a/ortools/sat/scheduling_helpers.h +++ b/ortools/sat/scheduling_helpers.h @@ -205,18 +205,22 @@ class SchedulingConstraintHelper : public PropagatorInterface { bool IsPresent(LiteralIndex lit) const; bool IsAbsent(LiteralIndex lit) const; - // Return a value so that End(a) + dist <= Start(b). - // Returns kMinInterValue if we don't have any such relation. - IntegerValue GetCurrentMinDistanceBetweenTasks( - int a, int b, bool add_reason_if_after = false); - - // We detected a precedence between two tasks. - // If we are at level zero, we might want to add the constraint. - // If we are at positive level, we might want to propagate the associated - // precedence literal if it exists. - bool PropagatePrecedence(int a, int b); - - // Return the minimum overlap of interval i with the time window [start..end]. + // Returns a value so that End(a) + dist <= Start(b). + // + // TODO(user): we use this to optimize some reason, but ideally we only want + // to use linear2 bounds here, not bounds coming from trivial bounds. Make + // sure we have the best possible reason. + IntegerValue GetCurrentMinDistanceBetweenTasks(int a, int b); + + // We detected a precedence between two tasks at level zero. + // This register a new constraint and notify the linear2 root level bounds + // repository. Returns false on conflict. + // + // TODO(user): We could also call this at positive decision level, but it is a + // bit harder to exploit as we will also need to store the reasons. + bool NotifyLevelZeroPrecedence(int a, int b); + + // Return the minimum overlap of task t with the time window [start..end]. // // Note: this is different from the mandatory part of an interval. IntegerValue GetMinOverlap(int t, IntegerValue start, IntegerValue end) const; @@ -273,9 +277,22 @@ class SchedulingConstraintHelper : public PropagatorInterface { void AddEnergyAfterReason(int t, IntegerValue energy_min, IntegerValue time); void AddEnergyMinInIntervalReason(int t, IntegerValue min, IntegerValue max); - // Adds the reason why task "before" must be before task "after". - // That is StartMax(before) < EndMin(after). - void AddReasonForBeingBefore(int before, int after); + // Adds the reason why the task "before" must be before task "after", in + // the sense that "after" can only start at the same time or later than the + // task "before" ends. + // + // Important: this assumes that the two task cannot overlap. So we can have + // a more relaxed reason than Start(after) >= Ends(before). + // + // There are actually many possibilities to explain such relation: + // - StartMax(before) < EndMin(after). + // - We have a linear2: Start(after) >= End(before) - SizeMin(before); + // - etc... + // We try to pick the best one. + // + // TODO(user): Refine the heuritic. Also consider other reason for the + // complex cases where Start() and End() do not use the same integer variable. + void AddReasonForBeingBeforeAssumingNoOverlap(int before, int after); // It is also possible to directly manipulates the underlying reason vectors // that will be used when pushing something. @@ -397,7 +414,8 @@ class SchedulingConstraintHelper : public PropagatorInterface { const VariablesAssignment& assignment_; IntegerTrail* integer_trail_; GenericLiteralWatcher* watcher_; - PrecedenceRelations* precedence_relations_; + Linear2Bounds* linear2_bounds_; + RootLevelLinear2Bounds* root_level_lin2_bounds_; // The current direction of time, true for forward, false for backward. bool current_time_direction_ = true; diff --git a/ortools/sat/shaving_solver.cc b/ortools/sat/shaving_solver.cc index cb35ef56771..488ce0228e2 100644 --- a/ortools/sat/shaving_solver.cc +++ b/ortools/sat/shaving_solver.cc @@ -633,9 +633,9 @@ bool VariablesShavingSolver::ResetAndSolveModel(int64_t task_id, State* state, // Use the current best solution as hint. { - auto sols = shared_->response->SolutionsRepository().GetBestNSolutions(1); - if (!sols.empty()) { - const std::vector& solution = sols[0]->variable_values; + auto sol = shared_->response->SolutionPool().BestSolutions().GetSolution(0); + if (sol != nullptr) { + const std::vector& solution = sol->variable_values; auto* hint = shaving_proto->mutable_solution_hint(); hint->clear_vars(); hint->clear_values(); diff --git a/ortools/sat/synchronization.cc b/ortools/sat/synchronization.cc index a4272ca3603..18f37e7cfbc 100644 --- a/ortools/sat/synchronization.cc +++ b/ortools/sat/synchronization.cc @@ -30,9 +30,6 @@ #include #include -#include "absl/hash/hash.h" -#include "absl/log/log.h" -#include "absl/time/time.h" #include "ortools/base/logging.h" #include "ortools/base/timer.h" #if !defined(__PORTABLE_PLATFORM__) @@ -40,11 +37,17 @@ #include "ortools/base/options.h" #endif // __PORTABLE_PLATFORM__ #include "absl/algorithm/container.h" +#include "absl/base/thread_annotations.h" #include "absl/container/btree_map.h" #include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/flags/flag.h" +#include "absl/hash/hash.h" #include "absl/log/check.h" +#include "absl/log/log.h" +#include "absl/numeric/int128.h" +#include "absl/random/bit_gen_ref.h" +#include "absl/random/distributions.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" @@ -74,6 +77,144 @@ ABSL_FLAG(bool, cp_model_dump_tightened_models, false, namespace operations_research { namespace sat { +std::shared_ptr::Solution> +SharedSolutionPool::Add(SharedSolutionRepository::Solution solution) { + // Only add to the alternative path if it has the correct source id. + if (alternative_path_.num_solutions_to_keep() > 0 && + solution.source_id == alternative_path_.source_id()) { + alternative_path_.Add(solution); + if (solution.rank < best_solutions_.GetBestRank()) { + VLOG(2) << "ALTERNATIVE WIN !"; + } + } + + // For now we only return a solution if it was stored in best_solutions_. + return best_solutions_.Add(std::move(solution)); +} + +void SharedSolutionPool::Synchronize(absl::BitGenRef random) { + // Update the "seeds" for the aternative path. + if (alternative_path_.num_solutions_to_keep() > 0) { + absl::MutexLock mutex_lock(&mutex_); + + auto process_solution = + [this](const SharedSolutionRepository::Solution& solution) + ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_) { + if (solution.variable_values.empty()) return; + if (solution.rank < min_rank_ || solution.rank > max_rank_) { + // Recompute buckets. + min_rank_ = std::min(min_rank_, solution.rank); + max_rank_ = std::max(max_rank_, solution.rank); + + // We want to store around 100 MB max. + int num_solutions = std::max( + 10, 100'000'000 / solution.variable_values.size()); + const int64_t range = max_rank_ - min_rank_ + 1; + if (num_solutions > range) { + num_solutions = range; + } + + // But if the number of variables is low, we do not want + // to use a lot of space/time just iterating over num_solutions. + // + // TODO(user): Rework the algo to be in + // O(num_different_solutions) rather than initializing the + // maximum amount right away. + num_solutions = std::min(num_solutions, 1'000); + + // Resize and recompute rank_. + // + // seeds_[i] should contains solution in [ranks_[i], + // rank_[i+1]). rank_[0] is always min_rank_. As long as we have + // room, we should have exactly one bucket per rank. + ranks_.resize(num_solutions); + seeds_.resize(num_solutions); + + int64_t offset = (max_rank_ - min_rank_ + 1) / num_solutions; + CHECK_GT(offset, 0); + for (int i = 0; i < num_solutions; ++i) { + ranks_[i] = min_rank_ + + static_cast(absl::int128(i) * + absl::int128(range) / + absl::int128(num_solutions)); + } + + // Move existing solutions to their new bucket. + int to_index = seeds_.size() - 1; + for (int i = seeds_.size(); --i >= 0;) { + if (seeds_[i] == nullptr) continue; + while (to_index >= 0 && ranks_[to_index] > seeds_[i]->rank) { + --to_index; + } + seeds_[to_index] = std::move(seeds_[i]); + } + } + + // rank[limit] is the first > solution.rank. + const int limit = std::upper_bound(ranks_.begin(), ranks_.end(), + solution.rank) - + ranks_.begin(); + CHECK_GT(limit, 0); + seeds_[limit - 1] = + std::make_shared::Solution>( + solution); + }; + + // All solution go through best_solutions_.Add(), so we only need + // to process these here. + best_solutions_.Synchronize(process_solution); + } else { + best_solutions_.Synchronize(); + } + alternative_path_.Synchronize(); + + // If we try to improve the alternate path without success, reset it + // from a random path_seeds_. + // + // TODO(user): find a way to generate random solution and update the seeds + // with them. Shall we do that in a continuous way or only when needed? + if (alternative_path_.num_solutions_to_keep() > 0) { + // Restart the alternative path ? + const int threshold = std::max( + 100, static_cast(std::sqrt(best_solutions_.num_queried()))); + if (alternative_path_.NumRecentlyNonImproving() > threshold) { + VLOG(2) << "Done. num_non_improving: " + << alternative_path_.NumRecentlyNonImproving() + << " achieved: " << alternative_path_.GetBestRank() << " / " + << best_solutions_.GetBestRank(); + alternative_path_.ClearSolutionsAndIncreaseSourceId(); + } + + // If we restarted, or we are at the beginning, pick a seed for the path. + if (alternative_path_.NumSolutions() == 0) { + absl::MutexLock mutex_lock(&mutex_); + + // Pick random bucket with bias. If the bucket is empty, we will scan + // "worse" bucket until we find a solution. We never pick bucket 0. + if (seeds_.size() > 1) { + // Note that LogUniform() is always inclusive. + // TODO(user): Shall we bias even more? + int index = 1 + absl::LogUniform(random, 0, seeds_.size() - 2); + for (; index < seeds_.size(); ++index) { + if (seeds_[index] != nullptr) { + alternative_path_.Add(*seeds_[index]); + alternative_path_.Synchronize(); + VLOG(2) << "RESTART bucket=" << index << "/" << seeds_.size() + << " rank=" << alternative_path_.GetSolution(0)->rank + << " from_optimal=" + << alternative_path_.GetSolution(0)->rank - min_rank_; + break; + } + } + + // The last bucket should never be empty. + CHECK(seeds_.back() != nullptr); + CHECK_LT(index, seeds_.size()); + } + } + } +} + void SharedLPSolutionRepository::NewLPSolution( std::vector lp_solution) { if (lp_solution.empty()) return; @@ -119,7 +260,8 @@ SharedResponseManager::SharedResponseManager(Model* model) : parameters_(*model->GetOrCreate()), wall_timer_(*model->GetOrCreate()), shared_time_limit_(model->GetOrCreate()), - solutions_(parameters_.solution_pool_size(), "feasible solutions"), + random_(model->GetOrCreate()), + solution_pool_(parameters_), logger_(model->GetOrCreate()) { bounds_logging_id_ = logger_->GetNewThrottledId(); } @@ -397,13 +539,15 @@ IntegerValue SharedResponseManager::GetInnerObjectiveUpperBound() { } void SharedResponseManager::Synchronize() { + solution_pool_.Synchronize(*random_); + absl::MutexLock mutex_lock(&mutex_); synchronized_inner_objective_lower_bound_ = IntegerValue(inner_objective_lower_bound_); synchronized_inner_objective_upper_bound_ = IntegerValue(inner_objective_upper_bound_); synchronized_best_status_ = best_status_; - if (solutions_.NumSolutions() > 0) { + if (solution_pool_.BestSolutions().NumSolutions() > 0) { first_solution_solvers_should_stop_ = true; } logger_->FlushPendingThrottledLogs(); @@ -502,7 +646,7 @@ void SharedResponseManager::UnregisterBestBoundCallback(int callback_id) { CpSolverResponse SharedResponseManager::GetResponseInternal( absl::Span variable_values, - const std::string& solution_info) { + absl::string_view solution_info) { CpSolverResponse result; result.set_status(best_status_); if (!unsat_cores_.empty()) { @@ -551,19 +695,19 @@ CpSolverResponse SharedResponseManager::GetResponseInternal( CpSolverResponse SharedResponseManager::GetResponse() { absl::MutexLock mutex_lock(&mutex_); CpSolverResponse result; - if (solutions_.NumSolutions() == 0) { + if (solution_pool_.BestSolutions().NumSolutions() == 0) { result = GetResponseInternal({}, ""); } else { std::shared_ptr::Solution> - solution = solutions_.GetSolution(0); + solution = solution_pool_.BestSolutions().GetSolution(0); result = GetResponseInternal(solution->variable_values, solution->info); } // If this is true, we postsolve and copy all of our solutions. if (parameters_.fill_additional_solutions_in_response()) { std::vector temp; - for (int i = 0; i < solutions_.NumSolutions(); ++i) { - std::shared_ptr::Solution> - solution = solutions_.GetSolution(i); + const int size = solution_pool_.BestSolutions().NumSolutions(); + for (int i = 0; i < size; ++i) { + const auto solution = solution_pool_.BestSolutions().GetSolution(i); temp = solution->variable_values; for (int i = solution_postprocessors_.size(); --i >= 0;) { solution_postprocessors_[i](&temp); @@ -623,7 +767,7 @@ void SharedResponseManager::FillObjectiveValuesInResponse( std::shared_ptr::Solution> SharedResponseManager::NewSolution(absl::Span solution_values, const std::string& solution_info, - Model* model) { + Model* model, int source_id) { absl::MutexLock mutex_lock(&mutex_); std::shared_ptr::Solution> ret; @@ -634,7 +778,8 @@ SharedResponseManager::NewSolution(absl::Span solution_values, solution.variable_values.assign(solution_values.begin(), solution_values.end()); solution.info = solution_info; - ret = solutions_.Add(solution); + solution.source_id = source_id; + ret = solution_pool_.Add(solution); } else { const int64_t objective_value = ComputeInnerObjective(*objective_or_null_, solution_values); @@ -645,7 +790,8 @@ SharedResponseManager::NewSolution(absl::Span solution_values, solution_values.end()); solution.rank = objective_value; solution.info = solution_info; - ret = solutions_.Add(solution); + solution.source_id = source_id; + ret = solution_pool_.Add(solution); // Ignore any non-strictly improving solution. if (objective_value > inner_objective_upper_bound_) return ret; @@ -666,7 +812,7 @@ SharedResponseManager::NewSolution(absl::Span solution_values, // In single thread, no one is synchronizing the solution manager, so we // should do it from here. if (always_synchronize_) { - solutions_.Synchronize(); + solution_pool_.Synchronize(*random_); first_solution_solvers_should_stop_ = true; } @@ -1240,25 +1386,32 @@ int UniqueClauseStream::NumLiteralsOfSize(int size) const { SharedClausesManager::SharedClausesManager(bool always_synchronize) : always_synchronize_(always_synchronize) {} -int SharedClausesManager::RegisterNewId(bool may_terminate_early) { +int SharedClausesManager::RegisterNewId(absl::string_view worker_name, + bool may_terminate_early) { absl::MutexLock mutex_lock(&mutex_); num_full_workers_ += may_terminate_early ? 0 : 1; const int id = id_to_last_processed_binary_clause_.size(); id_to_last_processed_binary_clause_.resize(id + 1, 0); id_to_last_returned_batch_.resize(id + 1, -1); id_to_last_finished_batch_.resize(id + 1, -1); - id_to_clauses_exported_.resize(id + 1, 0); + id_to_num_exported_.resize(id + 1, 0); + id_to_worker_name_.resize(id + 1); + id_to_worker_name_[id] = worker_name; return id; } -bool SharedClausesManager::ShouldReadBatch(int reader_id, int writer_id) { - return reader_id != writer_id; -} - -void SharedClausesManager::SetWorkerNameForId(int id, - absl::string_view worker_name) { +int SharedLinear2Bounds::RegisterNewId(std::string worker_name) { absl::MutexLock mutex_lock(&mutex_); + const int id = id_to_worker_name_.size(); + + id_to_stats_.resize(id + 1); + id_to_worker_name_.resize(id + 1); id_to_worker_name_[id] = worker_name; + return id; +} + +bool SharedClausesManager::ShouldReadBatch(int reader_id, int writer_id) { + return reader_id != writer_id; } void SharedClausesManager::AddBinaryClause(int id, int lit1, int lit2) { @@ -1270,7 +1423,7 @@ void SharedClausesManager::AddBinaryClause(int id, int lit1, int lit2) { if (inserted) { added_binary_clauses_.push_back(p); if (always_synchronize_) ++last_visible_binary_clause_; - id_to_clauses_exported_[id]++; + id_to_num_exported_[id]++; // Small optim. If the worker is already up to date with clauses to import, // we can mark this new clause as already seen. @@ -1283,7 +1436,7 @@ void SharedClausesManager::AddBinaryClause(int id, int lit1, int lit2) { void SharedClausesManager::AddBatch(int id, CompactVectorVector batch) { absl::MutexLock mutex_lock(&mutex_); - id_to_clauses_exported_[id] += batch.size(); + id_to_num_exported_[id] += batch.size(); pending_batches_.push_back(std::move(batch)); } @@ -1317,16 +1470,44 @@ void SharedClausesManager::GetUnseenBinaryClauses( void SharedClausesManager::LogStatistics(SolverLogger* logger) { absl::MutexLock mutex_lock(&mutex_); - absl::btree_map name_to_clauses; - for (int id = 0; id < id_to_clauses_exported_.size(); ++id) { - if (id_to_clauses_exported_[id] == 0) continue; - name_to_clauses[id_to_worker_name_[id]] = id_to_clauses_exported_[id]; + absl::btree_map name_to_table_line; + for (int id = 0; id < id_to_num_exported_.size(); ++id) { + if (id_to_num_exported_[id] == 0) continue; + name_to_table_line[id_to_worker_name_[id]] = id_to_num_exported_[id]; } - if (!name_to_clauses.empty()) { + if (!name_to_table_line.empty()) { std::vector> table; table.push_back({"Clauses shared", "Num"}); - for (const auto& entry : name_to_clauses) { - table.push_back({FormatName(entry.first), FormatCounter(entry.second)}); + for (const auto& [name, count] : name_to_table_line) { + table.push_back({FormatName(name), FormatCounter(count)}); + } + SOLVER_LOG(logger, FormatTable(table)); + } +} + +// TODO(user): Add some library to simplify this "transposition". Ideally we +// could merge small table with few columns. I am thinking list (row_name, +// col_name, count) + function that create table? +void SharedLinear2Bounds::LogStatistics(SolverLogger* logger) { + absl::MutexLock mutex_lock(&mutex_); + absl::btree_map name_to_table_line; + for (int id = 0; id < id_to_stats_.size(); ++id) { + const Stats stats = id_to_stats_[id]; + if (!stats.empty()) { + name_to_table_line[id_to_worker_name_[id]] = stats; + } + } + for (int import_id = 0; import_id < import_id_to_index_.size(); ++import_id) { + name_to_table_line[import_id_to_name_[import_id]].num_imported = + import_id_to_num_imported_[import_id]; + } + if (!name_to_table_line.empty()) { + std::vector> table; + table.push_back({"Linear2 shared", "New", "Updated", "Imported"}); + for (const auto& [name, stats] : name_to_table_line) { + table.push_back({FormatName(name), FormatCounter(stats.num_new), + FormatCounter(stats.num_update), + FormatCounter(stats.num_imported)}); } SOLVER_LOG(logger, FormatTable(table)); } @@ -1376,6 +1557,69 @@ void SharedClausesManager::Synchronize() { } } +void SharedLinear2Bounds::Add(int id, Key expr, IntegerValue lb, + IntegerValue ub) { + DCHECK(expr.IsCanonicalized()); + + absl::MutexLock mutex_lock(&mutex_); + auto [it, inserted] = shared_bounds_.insert({expr, {lb, ub}}); + if (inserted) { + // It is new. + id_to_stats_[id].num_new++; + newly_updated_keys_.push_back(expr); + } else { + // Update the individual bounds if the new ones are better. + auto& bounds = it->second; + const bool update_lb = lb > bounds.first; + if (update_lb) bounds.first = lb; + const bool update_ub = ub < bounds.second; + if (update_ub) bounds.second = ub; + if (update_lb || update_ub) { + id_to_stats_[id].num_update++; + newly_updated_keys_.push_back(expr); + } + } +} + +int SharedLinear2Bounds::RegisterNewImportId(std::string name) { + absl::MutexLock mutex_lock(&mutex_); + const int import_id = import_id_to_index_.size(); + import_id_to_name_.push_back(name); + import_id_to_index_.push_back(0); + import_id_to_num_imported_.push_back(0); + return import_id; +} + +std::vector< + std::pair>> +SharedLinear2Bounds::NewlyUpdatedBounds(int import_id) { + std::vector>> result; + + absl::MutexLock mutex_lock(&mutex_); + MaybeCompressNewlyUpdateKeys(); + const int size = newly_updated_keys_.size(); + for (int i = import_id_to_index_[import_id]; i < size; ++i) { + const auto& key = newly_updated_keys_[i]; + result.push_back({key, shared_bounds_[key]}); + } + import_id_to_index_[import_id] = size; + return result; +} + +void SharedLinear2Bounds::MaybeCompressNewlyUpdateKeys() { + int min_index = 0; + for (const int index : import_id_to_index_) { + min_index = std::min(index, min_index); + } + if (min_index == 0) return; + + newly_updated_keys_.erase(newly_updated_keys_.begin(), + newly_updated_keys_.begin() + min_index); + for (int& index_ref : import_id_to_index_) { + index_ref -= min_index; + } +} + void SharedStatistics::AddStats( absl::Span> stats) { absl::MutexLock mutex_lock(&mutex_); diff --git a/ortools/sat/synchronization.h b/ortools/sat/synchronization.h index c6eadff0804..a9cd377fdb2 100644 --- a/ortools/sat/synchronization.h +++ b/ortools/sat/synchronization.h @@ -61,8 +61,11 @@ template class SharedSolutionRepository { public: explicit SharedSolutionRepository(int num_solutions_to_keep, - absl::string_view name = "") - : name_(name), num_solutions_to_keep_(num_solutions_to_keep) {} + absl::string_view name = "", + int source_id = -1) + : name_(name), + num_solutions_to_keep_(num_solutions_to_keep), + source_id_(source_id) {} // The solution format used by this class. struct Solution { @@ -84,6 +87,8 @@ class SharedSolutionRepository { // Should be private: only SharedSolutionRepository should modify this. mutable int num_selected = 0; + int source_id; // Internal information. + bool operator==(const Solution& other) const { return rank == other.rank && variable_values == other.variable_values; } @@ -100,10 +105,11 @@ class SharedSolutionRepository { int NumSolutions() const; // Returns the solution #i where i must be smaller than NumSolutions(). + // Returns nullptr if i is out of range. std::shared_ptr GetSolution(int index) const; - // Returns the rank of the best known solution. - // You shouldn't call this if NumSolutions() is zero. + // Returns the rank of the best known solution. If there is no solution, this + // will return std::numeric_limits::max(). int64_t GetBestRank() const; std::vector> GetBestNSolutions(int n) const; @@ -131,7 +137,9 @@ class SharedSolutionRepository { // set of added solutions is the same. // // Works in O(num_solutions_to_keep_). - void Synchronize(); + // + // If f() is provided, it will be called on all new solutions. + void Synchronize(std::function f = nullptr); std::vector TableLineStats() const { absl::MutexLock mutex_lock(&mutex_); @@ -139,20 +147,52 @@ class SharedSolutionRepository { FormatCounter(num_queried_), FormatCounter(num_synchronization_)}; } + int64_t NumRecentlyNonImproving() const { + absl::MutexLock mutex_lock(&mutex_); + return num_non_improving_; + } + + void ClearSolutionsAndIncreaseSourceId() { + absl::MutexLock mutex_lock(&mutex_); + new_solutions_.clear(); + solutions_.clear(); + ++source_id_; + } + + int source_id() const { + absl::MutexLock mutex_lock(&mutex_); + return source_id_; + } + + int num_queried() const { + absl::MutexLock mutex_lock(&mutex_); + return num_queried_; + } + + int num_solutions_to_keep() const { return num_solutions_to_keep_; } + protected: const std::string name_; const int num_solutions_to_keep_; mutable absl::Mutex mutex_; + int source_id_ ABSL_GUARDED_BY(mutex_); int64_t num_added_ ABSL_GUARDED_BY(mutex_) = 0; mutable int64_t num_queried_ ABSL_GUARDED_BY(mutex_) = 0; int64_t num_synchronization_ ABSL_GUARDED_BY(mutex_) = 0; + mutable int64_t num_queried_at_last_sync_ ABSL_GUARDED_BY(mutex_) = 0; + mutable int64_t num_non_improving_ ABSL_GUARDED_BY(mutex_) = 0; + // Our two solutions pools, the current one and the new one that will be // merged into the current one on each Synchronize() calls. mutable std::vector tmp_indices_ ABSL_GUARDED_BY(mutex_); std::vector> solutions_ ABSL_GUARDED_BY(mutex_); std::vector> new_solutions_ ABSL_GUARDED_BY(mutex_); + + // For computing orthogonality. + std::vector ABSL_GUARDED_BY(mutex_) distances_; + std::vector ABSL_GUARDED_BY(mutex_) buffer_; }; // Solutions coming from the LP. @@ -165,6 +205,74 @@ class SharedLPSolutionRepository : public SharedSolutionRepository { void NewLPSolution(std::vector lp_solution); }; +// This stores all the feasible solutions the solver know about. +// Moreover, for meta-heuristics, we keep them in different buckets. +class SharedSolutionPool { + public: + explicit SharedSolutionPool(const SatParameters& parameters_) + : best_solutions_(parameters_.solution_pool_size(), "best_solutions"), + alternative_path_(parameters_.alternative_pool_size(), + "alternative_path", /*source_id=*/0) {} + + const SharedSolutionRepository& BestSolutions() const { + return best_solutions_; + } + + // Note that the given random generator is likely local to the thread calling + // this. + std::shared_ptr::Solution> + GetSolutionToImprove(absl::BitGenRef random) const { + // If we seems to have trouble making progress, work on the alternative + // path too. + if (alternative_path_.num_solutions_to_keep() > 0 && + best_solutions_.NumRecentlyNonImproving() > 100 && + absl::Bernoulli(random, 0.5) && alternative_path_.NumSolutions() > 0) { + // Tricky: We might clear the alternative_path_ between NumSolutions() + // and this call. + auto result = alternative_path_.GetRandomBiasedSolution(random); + if (result != nullptr) return result; + } + + if (best_solutions_.NumSolutions() > 0) { + return best_solutions_.GetRandomBiasedSolution(random); + } + return nullptr; + } + + std::shared_ptr::Solution> Add( + SharedSolutionRepository::Solution solution); + + void Synchronize(absl::BitGenRef random); + + void AddTableStats(std::vector>* table) const { + table->push_back(best_solutions_.TableLineStats()); + table->push_back(alternative_path_.TableLineStats()); + } + + private: + // Currently we only have two "pools" of solutions. + SharedSolutionRepository best_solutions_; + SharedSolutionRepository alternative_path_; + + // We also keep a list of possible "path seeds" in n buckets defined according + // to the objective value of the solution. These are updated on Synchronize(). + // Bucket i will only contain the last seen solution in the internal objective + // range [ranks_[i], ranks_[i + 1]). + // + // ranks_[0] should always be min_rank_, and seeds_[0] should be one of the + // best known solution. We usually never select seeds_[0] but keep it around + // for later in case new best solutions are found. + absl::Mutex mutex_; + int64_t max_rank_ ABSL_GUARDED_BY(mutex_) = + std::numeric_limits::min(); + int64_t min_rank_ ABSL_GUARDED_BY(mutex_) = + std::numeric_limits::max(); + std::vector ranks_; + std::vector< + std::shared_ptr::Solution>> + ABSL_GUARDED_BY(mutex_) seeds_; +}; + // Set of best solution from the feasibility jump workers. // // We store (solution, num_violated_constraints), so we have a list of solutions @@ -316,6 +424,13 @@ class SharedResponseManager { void Synchronize(); IntegerValue GetInnerObjectiveLowerBound(); IntegerValue GetInnerObjectiveUpperBound(); + IntegerValue GetBestSolutionObjective() { + if (solution_pool_.BestSolutions().NumSolutions() > 0) { + return solution_pool_.BestSolutions().GetBestRank(); + } else { + return GetInnerObjectiveUpperBound(); + } + } // Returns the current best solution inner objective value or kInt64Max if // there is no solution. @@ -361,7 +476,8 @@ class SharedResponseManager { // stored in the repository. std::shared_ptr::Solution> NewSolution(absl::Span solution_values, - const std::string& solution_info, Model* model = nullptr); + const std::string& solution_info, Model* model = nullptr, + int source_id = -1); // Changes the solution to reflect the fact that the "improving" problem is // infeasible. This means that if we have a solution, we have proven @@ -380,14 +496,13 @@ class SharedResponseManager { // OPTIMAL and consider the problem solved. bool ProblemIsSolved() const; + bool HasFeasibleSolution() const { + return solution_pool_.BestSolutions().NumSolutions() > 0; + } + // Returns the underlying solution repository where we keep a set of best // solutions. - const SharedSolutionRepository& SolutionsRepository() const { - return solutions_; - } - SharedSolutionRepository* MutableSolutionsRepository() { - return &solutions_; - } + const SharedSolutionPool& SolutionPool() const { return solution_pool_; } // Debug only. Set dump prefix for solutions written to file. void set_dump_prefix(absl::string_view dump_prefix) { @@ -433,11 +548,12 @@ class SharedResponseManager { // Generates a response for callbacks and GetResponse(). CpSolverResponse GetResponseInternal( absl::Span variable_values, - const std::string& solution_info) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + absl::string_view solution_info) ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); const SatParameters& parameters_; const WallTimer& wall_timer_; ModelSharedTimeLimit* shared_time_limit_; + ModelRandomGenerator* random_; CpObjectiveProto const* objective_or_null_ = nullptr; mutable absl::Mutex mutex_; @@ -450,7 +566,7 @@ class SharedResponseManager { CpSolverStatus synchronized_best_status_ ABSL_GUARDED_BY(mutex_) = CpSolverStatus::UNKNOWN; std::vector unsat_cores_ ABSL_GUARDED_BY(mutex_); - SharedSolutionRepository solutions_; // Thread-safe. + SharedSolutionPool solution_pool_; // Thread-safe. int num_solutions_ ABSL_GUARDED_BY(mutex_) = 0; int64_t inner_objective_lower_bound_ ABSL_GUARDED_BY(mutex_) = @@ -732,8 +848,7 @@ class SharedClausesManager { std::vector>* new_clauses); // Ids are used to identify which worker is exporting/importing clauses. - int RegisterNewId(bool may_terminate_early); - void SetWorkerNameForId(int id, absl::string_view worker_name); + int RegisterNewId(absl::string_view worker_name, bool may_terminate_early); // Search statistics. void LogStatistics(SolverLogger* logger); @@ -777,8 +892,100 @@ class SharedClausesManager { const bool always_synchronize_ = true; // Stats: - std::vector id_to_clauses_exported_; - absl::flat_hash_map id_to_worker_name_; + std::vector id_to_num_exported_ ABSL_GUARDED_BY(mutex_); + std::vector id_to_num_updated_ ABSL_GUARDED_BY(mutex_); + std::vector id_to_worker_name_ ABSL_GUARDED_BY(mutex_); +}; + +// A class that allows to exchange root level bounds on linear2. +// +// TODO(user): Add Synchronize() support and only publish new bounds when this +// is called. +class SharedLinear2Bounds { + public: + int RegisterNewId(std::string worker_name); + void LogStatistics(SolverLogger* logger); + + // This should only contain canonicalized expression. + // See the code for IsCanonicalized() for the definition. + struct Key { + int vars[2]; + IntegerValue coeffs[2]; + + bool IsCanonicalized() { + return coeffs[0] > 0 && coeffs[1] != 0 && vars[0] < vars[1] && + std::gcd(coeffs[0].value(), coeffs[1].value()) == 1; + } + + bool operator==(const Key& o) const { + return vars[0] == o.vars[0] && vars[1] == o.vars[1] && + coeffs[0] == o.coeffs[0] && coeffs[1] == o.coeffs[1]; + } + + template + friend H AbslHashValue(H h, const Key& k) { + return H::combine(std::move(h), k.vars[0], k.vars[1], k.coeffs[0], + k.coeffs[1]); + } + }; + + // Exports new bounds on the given expr (should be canonicalized). + void Add(int id, Key expr, IntegerValue lb, IntegerValue ub); + + // This is called less often, and maybe not every-worker that exports want to + // export, so we use a separate id space. Because we rely on hash map to + // check if a bound is new, it is not such a big deal that a worker re-read + // once the bounds it exported. + int RegisterNewImportId(std::string name); + + // Returns the linear2 and their bounds. + // We only return changes since the last call with the same id. + std::vector>> + NewlyUpdatedBounds(int import_id); + + // This is not filled by NewlyUpdatedBounds() because we want to track the + // bounds that were not already known by the worker at the time of the import, + // and we don't have this information here. + void NotifyNumImported(int import_id, int num) { + absl::MutexLock mutex_lock(&mutex_); + import_id_to_num_imported_[import_id] += num; + } + + private: + void MaybeCompressNewlyUpdateKeys() ABSL_EXCLUSIVE_LOCKS_REQUIRED(mutex_); + + absl::Mutex mutex_; + + // The best known bounds for each key. + absl::flat_hash_map> shared_bounds_ + ABSL_GUARDED_BY(mutex_); + + // Ever growing list of updated position in shared_bounds_. + // Note that we do reduce it in MaybeCompressNewlyUpdateKeys(), but that + // requires all registered workers to have at least imported some bounds. + // + // TODO(user): use indirect addressing so that newly_updated_keys_ can just + // deal with indices, and it is a bit tighter memory wise? We also avoid + // hash-lookups on NewlyUpdatedBounds(). But since this is only called at + // level zero on new bounds, I don't think we care. + std::vector newly_updated_keys_; + + // For import. + std::vector import_id_to_name_ ABSL_GUARDED_BY(mutex_); + std::vector import_id_to_index_ ABSL_GUARDED_BY(mutex_); + std::vector import_id_to_num_imported_ ABSL_GUARDED_BY(mutex_); + + // Just for reporting at the end of the solve. + struct Stats { + int64_t num_new = 0; + int64_t num_update = 0; + int64_t num_imported = 0; // Copy of import_id_to_num_imported_. + bool empty() const { + return num_new == 0 && num_update == 0 && num_imported == 0; + } + }; + std::vector id_to_stats_ ABSL_GUARDED_BY(mutex_); + std::vector id_to_worker_name_ ABSL_GUARDED_BY(mutex_); }; // Simple class to add statistics by name and print them at the end. @@ -807,6 +1014,7 @@ template std::shared_ptr::Solution> SharedSolutionRepository::GetSolution(int i) const { absl::MutexLock mutex_lock(&mutex_); + if (i >= solutions_.size()) return nullptr; ++num_queried_; return solutions_[i]; } @@ -814,7 +1022,7 @@ SharedSolutionRepository::GetSolution(int i) const { template int64_t SharedSolutionRepository::GetBestRank() const { absl::MutexLock mutex_lock(&mutex_); - CHECK_GT(solutions_.size(), 0); + if (solutions_.empty()) return std::numeric_limits::max(); return solutions_[0]->rank; } @@ -823,11 +1031,12 @@ std::vector::Solution>> SharedSolutionRepository::GetBestNSolutions(int n) const { absl::MutexLock mutex_lock(&mutex_); - // Sorted and unique. - DCHECK(absl::c_is_sorted( - solutions_, - [](const std::shared_ptr& a, - const std::shared_ptr& b) { return *a < *b; })); + // Sorted by rank and unique. + DCHECK(absl::c_is_sorted(solutions_, + [](const std::shared_ptr& a, + const std::shared_ptr& b) { + return a->rank < b->rank; + })); DCHECK(absl::c_adjacent_find(solutions_, [](const std::shared_ptr& a, const std::shared_ptr& b) { @@ -855,34 +1064,41 @@ std::shared_ptr::Solution> SharedSolutionRepository::GetRandomBiasedSolution( absl::BitGenRef random) const { absl::MutexLock mutex_lock(&mutex_); + if (solutions_.empty()) return nullptr; ++num_queried_; - const int64_t best_rank = solutions_[0]->rank; + int index = 0; - // As long as we have solution with the best objective that haven't been - // explored too much, we select one uniformly. Otherwise, we select a solution - // from the pool uniformly. - // - // Note(user): Because of the increase of num_selected, this is dependent on - // the order of call. It should be fine for "determinism" because we do - // generate the task of a batch always in the same order. - const int kExplorationThreshold = 100; - - // Select all the best solution with a low enough selection count. - tmp_indices_.clear(); - for (int i = 0; i < solutions_.size(); ++i) { - std::shared_ptr solution = solutions_[i]; - if (solution->rank == best_rank && - solution->num_selected <= kExplorationThreshold) { - tmp_indices_.push_back(i); + if (solutions_.size() > 1) { + const int64_t best_rank = solutions_[0]->rank; + + // As long as we have solution with the best objective that haven't been + // explored too much, we select one uniformly. Otherwise, we select a + // solution from the pool uniformly. + // + // Note(user): Because of the increase of num_selected, this is dependent on + // the order of call. It should be fine for "determinism" because we do + // generate the task of a batch always in the same order. + const int kExplorationThreshold = 100; + + // Select all the best solution with a low enough selection count. + tmp_indices_.clear(); + for (int i = 0; i < solutions_.size(); ++i) { + std::shared_ptr solution = solutions_[i]; + if (solution->rank == best_rank && + solution->num_selected <= kExplorationThreshold) { + tmp_indices_.push_back(i); + } } - } - int index = 0; - if (tmp_indices_.empty()) { - index = absl::Uniform(random, 0, solutions_.size()); - } else { - index = tmp_indices_[absl::Uniform(random, 0, tmp_indices_.size())]; + if (tmp_indices_.empty()) { + index = absl::Uniform(random, 0, solutions_.size()); + } else { + index = tmp_indices_[absl::Uniform(random, 0, tmp_indices_.size())]; + } } + + CHECK_GE(index, 0); + CHECK_LT(index, solutions_.size()); solutions_[index]->num_selected++; return solutions_[index]; } @@ -896,38 +1112,147 @@ SharedSolutionRepository::Add(Solution solution) { { absl::MutexLock mutex_lock(&mutex_); ++num_added_; + solution_ptr->source_id = source_id_; new_solutions_.push_back(solution_ptr); } return solution_ptr; } template -void SharedSolutionRepository::Synchronize() { +void SharedSolutionRepository::Synchronize( + std::function f) { absl::MutexLock mutex_lock(&mutex_); - if (new_solutions_.empty()) return; + if (new_solutions_.empty()) { + const int64_t diff = num_queried_ - num_queried_at_last_sync_; + num_non_improving_ += diff; + num_queried_at_last_sync_ = num_queried_; + return; + } + + if (f != nullptr) { + gtl::STLStableSortAndRemoveDuplicates( + &new_solutions_, + [](const std::shared_ptr& a, + const std::shared_ptr& b) { return *a < *b; }); + for (const auto& ptr : new_solutions_) { + f(*ptr); + } + } + + const int64_t old_best_rank = solutions_.empty() + ? std::numeric_limits::max() + : solutions_[0]->rank; solutions_.insert(solutions_.end(), new_solutions_.begin(), new_solutions_.end()); new_solutions_.clear(); // We use a stable sort to keep the num_selected count for the already - // existing solutions. - // - // TODO(user): Introduce a notion of orthogonality to diversify the pool? + // existing solutions (in case of duplicates). gtl::STLStableSortAndRemoveDuplicates( &solutions_, [](const std::shared_ptr& a, const std::shared_ptr& b) { return *a < *b; }); + const int64_t new_best_rank = solutions_[0]->rank; + + // If we have more than num_solutions_to_keep_ solutions with the best rank, + // select them via orthogonality. + if (solutions_.size() > num_solutions_to_keep_ && + num_solutions_to_keep_ > 1) { + int num_best = 1; + while (num_best < solutions_.size() && + solutions_[num_best]->rank == new_best_rank) { + ++num_best; + } + + if (num_best > num_solutions_to_keep_ && num_solutions_to_keep_ < 10) { + // We should only be here if a new solution (not in our current set) was + // found. It could be one we saw before but forgot about. We put one + // first. + for (auto& solution : solutions_) { + if (solution->num_selected == 0) { + // TODO(user): randomize amongst new solution? + std::swap(solutions_[0], solution); + break; + } + } + + // We are going to be in O(n^2 * solution_size), so keep n <= 10. + solutions_.resize(std::min(10, num_best)); + + // Fill the pairwise distances. + const int n = solutions_.size(); + distances_.resize(n * n); + const int size = solutions_[0]->variable_values.size(); + for (int i = 0; i < n; ++i) { + for (int j = i + 1; j < n; ++j) { + int64_t dist = 0; + for (int k = 0; k < size; ++k) { + if (solutions_[i]->variable_values[k] != + solutions_[j]->variable_values[k]) { + ++dist; + } + } + distances_[i * n + j] = distances_[j * n + i] = dist; + } + } + + // In order to not get stuck on a subset that always maximize the sum of + // orthogonality, we pick the first element (which should be a new one + // thanks to the swap above), and we maximize the sum of orthogonality + // with the rest. + // + // This way, as we find new solution, the set changes slowly. + const std::vector selected = + FindMostDiverseSubset(num_solutions_to_keep_, n, distances_, buffer_, + /*always_pick_mask = */ 1); + + DCHECK(std::is_sorted(selected.begin(), selected.end())); + int new_size = 0; + for (const int s : selected) { + solutions_[new_size++] = std::move(solutions_[s]); + } + solutions_.resize(new_size); + + if (VLOG_IS_ON(3)) { + int min_count = std::numeric_limits::max(); + int max_count = 0; + for (const auto& s : solutions_) { + CHECK(s != nullptr); + min_count = std::min(s->num_selected, min_count); + max_count = std::max(s->num_selected, max_count); + } + int64_t score = 0; + for (const int i : selected) { + for (const int j : selected) { + if (i > j) score += distances_[i * n + j]; + } + } + LOG(INFO) << name_ << " rank=" << new_best_rank + << " num=" << num_solutions_to_keep_ << "/" << num_best + << " orthogonality=" << score << " count=[" << min_count + << ", " << max_count << "]"; + } + } + } + if (solutions_.size() > num_solutions_to_keep_) { solutions_.resize(num_solutions_to_keep_); } - + CHECK(!solutions_.empty()); if (!solutions_.empty()) { - VLOG(2) << "Solution pool update:" << " num_solutions=" << solutions_.size() + VLOG(4) << "Solution pool update:" << " num_solutions=" << solutions_.size() << " min_rank=" << solutions_[0]->rank << " max_rank=" << solutions_.back()->rank; } num_synchronization_++; + if (new_best_rank < old_best_rank) { + num_non_improving_ = 0; + } else { + const int64_t diff = num_queried_ - num_queried_at_last_sync_; + num_non_improving_ += diff; + } + num_queried_at_last_sync_ = num_queried_; } } // namespace sat diff --git a/ortools/sat/synchronization_test.cc b/ortools/sat/synchronization_test.cc index 00dd4a25504..1ab19d6cbcc 100644 --- a/ortools/sat/synchronization_test.cc +++ b/ortools/sat/synchronization_test.cc @@ -834,8 +834,8 @@ TEST(SharedResponseManagerTest, Callback) { TEST(SharedClausesManagerTest, SyncApi) { SharedClausesManager manager(/*always_synchronize=*/true); - EXPECT_EQ(0, manager.RegisterNewId(/*may_terminate_early=*/false)); - EXPECT_EQ(1, manager.RegisterNewId(/*may_terminate_early=*/false)); + EXPECT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); + EXPECT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); manager.AddBinaryClause(/*id=*/0, 1, 2); std::vector> new_clauses; @@ -922,8 +922,8 @@ TEST(UniqueClauseStreamTest, DropsClauses) { TEST(SharedClausesManagerTest, NonSyncApi) { SharedClausesManager manager(/*always_synchronize=*/false); - EXPECT_EQ(0, manager.RegisterNewId(/*may_terminate_early=*/false)); - EXPECT_EQ(1, manager.RegisterNewId(/*may_terminate_early=*/false)); + EXPECT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); + EXPECT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); manager.AddBinaryClause(/*id=*/0, 1, 2); std::vector> new_clauses; @@ -971,8 +971,8 @@ TEST(SharedClausesManagerTest, NonSyncApi) { TEST(SharedClausesManagerTest, ShareGlueClauses) { SharedClausesManager manager(/*always_synchronize=*/true); - ASSERT_EQ(0, manager.RegisterNewId(/*may_terminate_early=*/false)); - ASSERT_EQ(1, manager.RegisterNewId(/*may_terminate_early=*/false)); + ASSERT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); + ASSERT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); UniqueClauseStream stream0; UniqueClauseStream stream1; // Add a bunch of clauses that will be skipped batch. @@ -999,8 +999,8 @@ TEST(SharedClausesManagerTest, ShareGlueClauses) { TEST(SharedClausesManagerTest, LbdThresholdIncrease) { SharedClausesManager manager(/*always_synchronize=*/true); - ASSERT_EQ(0, manager.RegisterNewId(/*may_terminate_early=*/false)); - ASSERT_EQ(1, manager.RegisterNewId(/*may_terminate_early=*/false)); + ASSERT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); + ASSERT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); UniqueClauseStream stream0; UniqueClauseStream stream1; const int kExpectedClauses = UniqueClauseStream::kMaxLiteralsPerBatch / 5; @@ -1027,8 +1027,8 @@ TEST(SharedClausesManagerTest, LbdThresholdIncrease) { TEST(SharedClausesManagerTest, LbdThresholdDecrease) { SharedClausesManager manager(/*always_synchronize=*/true); - ASSERT_EQ(0, manager.RegisterNewId(/*may_terminate_early=*/false)); - ASSERT_EQ(1, manager.RegisterNewId(/*may_terminate_early=*/false)); + ASSERT_EQ(0, manager.RegisterNewId("", /*may_terminate_early=*/false)); + ASSERT_EQ(1, manager.RegisterNewId("", /*may_terminate_early=*/false)); UniqueClauseStream stream0; UniqueClauseStream stream1; diff --git a/ortools/sat/util.cc b/ortools/sat/util.cc index 9d047a4f6bd..dd90fbbcc19 100644 --- a/ortools/sat/util.cc +++ b/ortools/sat/util.cc @@ -25,6 +25,7 @@ #include "absl/algorithm/container.h" #include "absl/container/btree_set.h" #include "absl/log/check.h" +#include "absl/numeric/bits.h" #include "absl/numeric/int128.h" #include "absl/random/bit_gen_ref.h" #include "absl/random/distributions.h" @@ -1008,5 +1009,48 @@ int64_t MaxBoundedSubsetSumExact::MaxSubsetSum( return result; } +std::vector FindMostDiverseSubset(int k, int n, + absl::Span distances, + std::vector& buffer, + int always_pick_mask) { + CHECK_LE(n, 20); + const int limit = 1 << n; + buffer.assign(limit, 0); + int best_mask; + int best_value = -1; + for (unsigned int mask = 1; mask < limit; ++mask) { + const int hamming_weight = absl::popcount(mask); + + // TODO(user): Increase mask by more than one ? but counting to 1k is fast + // anyway. + if (hamming_weight > k) continue; + int low_bit = -1; + int64_t sum = 0; + for (int i = 0; i < n; ++i) { + if ((mask >> i) & 1) { + if (low_bit == -1) { + low_bit = i; + } else { + sum += distances[low_bit * n + i]; + } + } + } + buffer[mask] = buffer[mask ^ (1 << low_bit)] + sum; + if (hamming_weight == k && buffer[mask] > best_value) { + if ((mask & always_pick_mask) != always_pick_mask) continue; + best_value = buffer[mask]; + best_mask = mask; + } + } + std::vector result; + result.reserve(k); + for (int i = 0; i < n; ++i) { + if ((best_mask >> i) & 1) { + result.push_back(i); + } + } + return result; +} + } // namespace sat } // namespace operations_research diff --git a/ortools/sat/util.h b/ortools/sat/util.h index 88a5b927d30..3ec89ce7a91 100644 --- a/ortools/sat/util.h +++ b/ortools/sat/util.h @@ -391,6 +391,21 @@ int MoveOneUnprocessedLiteralLast( const absl::btree_set& processed, int relevant_prefix_size, std::vector* literals); +// Selects k out of n such that the sum of pairwise distances is maximal. +// distances[i * n + j] = distances[j * n + j] = distances between i and j. +// +// This shall only be called with small n, we CHECK_LE(n, 20). +// Complexity is in O(2 ^ n + n_choose_k * n). +// Memory is in O(2 ^ n). +// +// In case of tie, this will choose deterministically, so one can randomize the +// order first to get a random subset. The returned subset will always be +// sorted. +std::vector FindMostDiverseSubset(int k, int n, + absl::Span distances, + std::vector& buffer, + int always_pick_mask = 0); + // Simple DP to compute the maximum reachable value of a "subset sum" under // a given bound (inclusive). Note that we abort as soon as the computation // become too important. @@ -1005,7 +1020,6 @@ inline void CompactVectorVector::ResetFromTranspose( // // Note 2: adding an arc during an iteration is not supported and the behavior // is undefined. - class DagTopologicalSortIterator { public: DagTopologicalSortIterator() = default; diff --git a/ortools/sat/util_test.cc b/ortools/sat/util_test.cc index 1b2be2db49a..9aaef75c603 100644 --- a/ortools/sat/util_test.cc +++ b/ortools/sat/util_test.cc @@ -29,6 +29,7 @@ #include "absl/container/btree_set.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" +#include "absl/numeric/bits.h" #include "absl/numeric/int128.h" #include "absl/random/random.h" #include "absl/strings/str_join.h" @@ -1160,6 +1161,94 @@ TEST(DagTopologicalSortIteratorTest, RandomTest) { } } +TEST(FindMostDiverseSubsetTest, Random) { + const int k = 4; + const int n = 10; + absl::BitGen random; + std::vector distances(n * n); + std::vector buffer; + for (int i = 0; i < n; ++i) { + for (int j = i + 1; j < n; ++j) { + distances[i * n + j] = distances[j * n + i] = + absl::Uniform(random, 0, 1000); + } + } + + const std::vector result = + FindMostDiverseSubset(k, n, distances, buffer); + CHECK(std::is_sorted(result.begin(), result.end())); + int64_t result_value = 0; + for (const int i : result) { + for (const int j : result) { + if (i < j) result_value += distances[i * n + j]; + } + } + + int64_t best_seen = 0; + std::vector subset; + const int limit = 1 << n; + for (unsigned int mask = 0; mask < limit; ++mask) { + if (absl::popcount(mask) != k) continue; + subset.clear(); + for (int i = 0; i < n; ++i) { + if ((mask >> i) & 1) subset.push_back(i); + } + int64_t value = 0; + for (const int i : subset) { + for (const int j : subset) { + if (i < j) value += distances[i * n + j]; + } + } + ASSERT_LE(value, result_value); + best_seen = std::max(best_seen, value); + } + EXPECT_EQ(best_seen, result_value); +} + +TEST(FindMostDiverseSubsetTest, RandomButAlwaysPickZero) { + const int k = 5; + const int n = 10; + absl::BitGen random; + std::vector distances(n * n); + std::vector buffer; + for (int i = 0; i < n; ++i) { + for (int j = i + 1; j < n; ++j) { + distances[i * n + j] = distances[j * n + i] = + absl::Uniform(random, 0, 1000); + } + } + + const std::vector result = + FindMostDiverseSubset(k, n, distances, buffer, /*always_pick_mask=*/1); + CHECK(std::is_sorted(result.begin(), result.end())); + int64_t result_value = 0; + for (const int i : result) { + for (const int j : result) { + if (i < j) result_value += distances[i * n + j]; + } + } + + int64_t best_seen = 0; + std::vector subset; + const int limit = 1 << n; + for (unsigned int mask = 1; mask < limit; mask += 2) { // bit 1 always set. + if (absl::popcount(mask) != k) continue; + subset.clear(); + for (int i = 0; i < n; ++i) { + if ((mask >> i) & 1) subset.push_back(i); + } + int64_t value = 0; + for (const int i : subset) { + for (const int j : subset) { + if (i < j) value += distances[i * n + j]; + } + } + ASSERT_LE(value, result_value); + best_seen = std::max(best_seen, value); + } + EXPECT_EQ(best_seen, result_value); +} + } // namespace } // namespace sat } // namespace operations_research diff --git a/ortools/scheduling/python/rcpsp_test.py b/ortools/scheduling/python/rcpsp_test.py index 76780840834..669549e5b7c 100644 --- a/ortools/scheduling/python/rcpsp_test.py +++ b/ortools/scheduling/python/rcpsp_test.py @@ -22,22 +22,22 @@ class RcpspTest(absltest.TestCase): - def testParseAndAccess(self): - parser = rcpsp.RcpspParser() - data = "ortools/scheduling/testdata/j301_1.sm" - try: - filename = f"{FLAGS.test_srcdir}/_main/{data}" - except flags._exceptions.UnparsedFlagAccessError: - filename = f"../../../{data}" - self.assertTrue(parser.parse_file(filename)) - problem = parser.problem() - self.assertLen(problem.resources, 4) - self.assertLen(problem.tasks, 32) + def testParseAndAccess(self): + parser = rcpsp.RcpspParser() + data = "ortools/scheduling/testdata/j301_1.sm" + try: + filename = f"{FLAGS.test_srcdir}/_main/{data}" + except flags._exceptions.UnparsedFlagAccessError: + filename = f"../../../{data}" + self.assertTrue(parser.parse_file(filename)) + problem = parser.problem() + self.assertLen(problem.resources, 4) + self.assertLen(problem.tasks, 32) def main(unused_argv): - absltest.main() + absltest.main() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/set_cover/python/set_cover_test.py b/ortools/set_cover/python/set_cover_test.py index ee30263878c..5984c2f3589 100644 --- a/ortools/set_cover/python/set_cover_test.py +++ b/ortools/set_cover/python/set_cover_test.py @@ -19,218 +19,220 @@ def create_initial_cover_model(): - model = set_cover.SetCoverModel() - model.add_empty_subset(1.0) - model.add_element_to_last_subset(0) - model.add_empty_subset(1.0) - model.add_element_to_last_subset(1) - model.add_element_to_last_subset(2) - model.add_empty_subset(1.0) - model.add_element_to_last_subset(1) - model.add_empty_subset(1.0) - model.add_element_to_last_subset(2) - return model - - -def create_knights_cover_model(num_rows: int, num_cols: int) -> set_cover.SetCoverModel: - model = set_cover.SetCoverModel() - knight_row_move = [2, 1, -1, -2, -2, -1, 1, 2] - knight_col_move = [1, 2, 2, 1, -1, -2, -2, -1] - - for row in range(num_rows): - for col in range(num_cols): - model.add_empty_subset(1.0) - model.add_element_to_last_subset(row * num_cols + col) - - for i in range(8): - new_row = row + knight_row_move[i] - new_col = col + knight_col_move[i] - if 0 <= new_row < num_rows and 0 <= new_col < num_cols: - model.add_element_to_last_subset(new_row * num_cols + new_col) - - return model + model = set_cover.SetCoverModel() + model.add_empty_subset(1.0) + model.add_element_to_last_subset(0) + model.add_empty_subset(1.0) + model.add_element_to_last_subset(1) + model.add_element_to_last_subset(2) + model.add_empty_subset(1.0) + model.add_element_to_last_subset(1) + model.add_empty_subset(1.0) + model.add_element_to_last_subset(2) + return model + + +def create_knights_cover_model( + num_rows: int, num_cols: int +) -> set_cover.SetCoverModel: + model = set_cover.SetCoverModel() + knight_row_move = [2, 1, -1, -2, -2, -1, 1, 2] + knight_col_move = [1, 2, 2, 1, -1, -2, -2, -1] + + for row in range(num_rows): + for col in range(num_cols): + model.add_empty_subset(1.0) + model.add_element_to_last_subset(row * num_cols + col) + + for i in range(8): + new_row = row + knight_row_move[i] + new_col = col + knight_col_move[i] + if 0 <= new_row < num_rows and 0 <= new_col < num_cols: + model.add_element_to_last_subset(new_row * num_cols + new_col) + + return model # This test case is mostly a Python port of set_cover_test.cc. class SetCoverTest(absltest.TestCase): - def test_save_reload(self): - model = create_knights_cover_model(10, 10) - model.sort_elements_in_subsets() - proto = model.export_model_as_proto() - reloaded = set_cover.SetCoverModel() - reloaded.import_model_from_proto(proto) - - self.assertEqual(model.num_subsets, reloaded.num_subsets) - self.assertEqual(model.num_elements, reloaded.num_elements) - self.assertEqual(model.subset_costs, reloaded.subset_costs) - self.assertEqual(model.columns, reloaded.columns) - if model.row_view_is_valid and reloaded.row_view_is_valid: - self.assertEqual(model.rows, reloaded.rows) - - def test_save_reload_twice(self): - model = create_knights_cover_model(3, 3) - inv = set_cover.SetCoverInvariant(model) - - greedy = set_cover.GreedySolutionGenerator(inv) - self.assertTrue(greedy.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - greedy_proto = inv.export_solution_as_proto() - - steepest = set_cover.SteepestSearch(inv) - steepest.set_max_iterations(500) - self.assertTrue(steepest.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - steepest_proto = inv.export_solution_as_proto() - - inv.import_solution_from_proto(greedy_proto) - steepest.set_max_iterations(500) - self.assertTrue(steepest.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - reloaded_proto = inv.export_solution_as_proto() - self.assertEqual(str(steepest_proto), str(reloaded_proto)) - - def test_initial_values(self): - model = create_initial_cover_model() - self.assertTrue(model.compute_feasibility()) - - inv = set_cover.SetCoverInvariant(model) - trivial = set_cover.TrivialSolutionGenerator(inv) - self.assertTrue(trivial.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) - ) - - greedy = set_cover.GreedySolutionGenerator(inv) - self.assertTrue(greedy.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - - self.assertEqual(inv.num_uncovered_elements(), 0) - steepest = set_cover.SteepestSearch(inv) - steepest.set_max_iterations(500) - self.assertTrue(steepest.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) - ) - - def test_infeasible(self): - model = set_cover.SetCoverModel() - model.add_empty_subset(1.0) - model.add_element_to_last_subset(0) - model.add_empty_subset(1.0) - model.add_element_to_last_subset(3) - self.assertFalse(model.compute_feasibility()) - - def test_knights_cover_creation(self): - model = create_knights_cover_model(16, 16) - self.assertTrue(model.compute_feasibility()) - - def test_knights_cover_greedy(self): - model = create_knights_cover_model(16, 16) - self.assertTrue(model.compute_feasibility()) - inv = set_cover.SetCoverInvariant(model) - - greedy = set_cover.GreedySolutionGenerator(inv) - self.assertTrue(greedy.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - - steepest = set_cover.SteepestSearch(inv) - steepest.set_max_iterations(500) - self.assertTrue(steepest.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - - def test_knights_cover_degree(self): - model = create_knights_cover_model(16, 16) - self.assertTrue(model.compute_feasibility()) - inv = set_cover.SetCoverInvariant(model) - - degree = set_cover.ElementDegreeSolutionGenerator(inv) - self.assertTrue(degree.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) - ) - - steepest = set_cover.SteepestSearch(inv) - steepest.set_max_iterations(500) - self.assertTrue(steepest.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - - def test_knights_cover_gls(self): - model = create_knights_cover_model(16, 16) - self.assertTrue(model.compute_feasibility()) - inv = set_cover.SetCoverInvariant(model) - - greedy = set_cover.GreedySolutionGenerator(inv) - self.assertTrue(greedy.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - - gls = set_cover.GuidedLocalSearch(inv) - gls.set_max_iterations(500) - self.assertTrue(gls.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - - def test_knights_cover_random(self): - model = create_knights_cover_model(16, 16) - self.assertTrue(model.compute_feasibility()) - inv = set_cover.SetCoverInvariant(model) - - random = set_cover.RandomSolutionGenerator(inv) - self.assertTrue(random.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) - ) - - steepest = set_cover.SteepestSearch(inv) - steepest.set_max_iterations(500) - self.assertTrue(steepest.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - - def test_knights_cover_trivial(self): - model = create_knights_cover_model(16, 16) - self.assertTrue(model.compute_feasibility()) - inv = set_cover.SetCoverInvariant(model) - - trivial = set_cover.TrivialSolutionGenerator(inv) - self.assertTrue(trivial.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) - ) - - steepest = set_cover.SteepestSearch(inv) - steepest.set_max_iterations(500) - self.assertTrue(steepest.next_solution()) - self.assertTrue( - inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) - ) - - # TODO(user): KnightsCoverGreedyAndTabu, KnightsCoverGreedyRandomClear, - # KnightsCoverElementDegreeRandomClear, KnightsCoverRandomClearMip, - # KnightsCoverMip + def test_save_reload(self): + model = create_knights_cover_model(10, 10) + model.sort_elements_in_subsets() + proto = model.export_model_as_proto() + reloaded = set_cover.SetCoverModel() + reloaded.import_model_from_proto(proto) + + self.assertEqual(model.num_subsets, reloaded.num_subsets) + self.assertEqual(model.num_elements, reloaded.num_elements) + self.assertEqual(model.subset_costs, reloaded.subset_costs) + self.assertEqual(model.columns, reloaded.columns) + if model.row_view_is_valid and reloaded.row_view_is_valid: + self.assertEqual(model.rows, reloaded.rows) + + def test_save_reload_twice(self): + model = create_knights_cover_model(3, 3) + inv = set_cover.SetCoverInvariant(model) + + greedy = set_cover.GreedySolutionGenerator(inv) + self.assertTrue(greedy.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + greedy_proto = inv.export_solution_as_proto() + + steepest = set_cover.SteepestSearch(inv) + steepest.set_max_iterations(500) + self.assertTrue(steepest.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + steepest_proto = inv.export_solution_as_proto() + + inv.import_solution_from_proto(greedy_proto) + steepest.set_max_iterations(500) + self.assertTrue(steepest.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + reloaded_proto = inv.export_solution_as_proto() + self.assertEqual(str(steepest_proto), str(reloaded_proto)) + + def test_initial_values(self): + model = create_initial_cover_model() + self.assertTrue(model.compute_feasibility()) + + inv = set_cover.SetCoverInvariant(model) + trivial = set_cover.TrivialSolutionGenerator(inv) + self.assertTrue(trivial.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + greedy = set_cover.GreedySolutionGenerator(inv) + self.assertTrue(greedy.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + self.assertEqual(inv.num_uncovered_elements(), 0) + steepest = set_cover.SteepestSearch(inv) + steepest.set_max_iterations(500) + self.assertTrue(steepest.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + def test_infeasible(self): + model = set_cover.SetCoverModel() + model.add_empty_subset(1.0) + model.add_element_to_last_subset(0) + model.add_empty_subset(1.0) + model.add_element_to_last_subset(3) + self.assertFalse(model.compute_feasibility()) + + def test_knights_cover_creation(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + + def test_knights_cover_greedy(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + greedy = set_cover.GreedySolutionGenerator(inv) + self.assertTrue(greedy.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + steepest = set_cover.SteepestSearch(inv) + steepest.set_max_iterations(500) + self.assertTrue(steepest.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + def test_knights_cover_degree(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + degree = set_cover.ElementDegreeSolutionGenerator(inv) + self.assertTrue(degree.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + steepest = set_cover.SteepestSearch(inv) + steepest.set_max_iterations(500) + self.assertTrue(steepest.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + def test_knights_cover_gls(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + greedy = set_cover.GreedySolutionGenerator(inv) + self.assertTrue(greedy.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + gls = set_cover.GuidedLocalSearch(inv) + gls.set_max_iterations(500) + self.assertTrue(gls.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + def test_knights_cover_random(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + random = set_cover.RandomSolutionGenerator(inv) + self.assertTrue(random.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + steepest = set_cover.SteepestSearch(inv) + steepest.set_max_iterations(500) + self.assertTrue(steepest.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + def test_knights_cover_trivial(self): + model = create_knights_cover_model(16, 16) + self.assertTrue(model.compute_feasibility()) + inv = set_cover.SetCoverInvariant(model) + + trivial = set_cover.TrivialSolutionGenerator(inv) + self.assertTrue(trivial.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.COST_AND_COVERAGE) + ) + + steepest = set_cover.SteepestSearch(inv) + steepest.set_max_iterations(500) + self.assertTrue(steepest.next_solution()) + self.assertTrue( + inv.check_consistency(set_cover.consistency_level.FREE_AND_UNCOVERED) + ) + + # TODO(user): KnightsCoverGreedyAndTabu, KnightsCoverGreedyRandomClear, + # KnightsCoverElementDegreeRandomClear, KnightsCoverRandomClearMip, + # KnightsCoverMip def main(_): - absltest.main() + absltest.main() if __name__ == "__main__": - app.run(main) + app.run(main) diff --git a/ortools/set_cover/samples/set_cover.py b/ortools/set_cover/samples/set_cover.py index c6d5b48aef3..e38973da739 100755 --- a/ortools/set_cover/samples/set_cover.py +++ b/ortools/set_cover/samples/set_cover.py @@ -21,36 +21,36 @@ def main(): - # [START data] - model = set_cover.SetCoverModel() - model.add_empty_subset(2.0) - model.add_element_to_last_subset(0) - model.add_empty_subset(2.0) - model.add_element_to_last_subset(1) - model.add_empty_subset(1.0) - model.add_element_to_last_subset(0) - model.add_element_to_last_subset(1) - # [END data] - - # [START solve] - inv = set_cover.SetCoverInvariant(model) - greedy = set_cover.GreedySolutionGenerator(inv) - has_found = greedy.next_solution() - if not has_found: - print("No solution found by the greedy heuristic.") - return - solution = inv.export_solution_as_proto() - # [END solve] - - # [START print_solution] - print(f"Total cost: {solution.cost}") # == inv.cost() - print(f"Total number of selected subsets: {solution.num_subsets}") - print("Chosen subsets:") - for subset in solution.subset: - print(f" {subset}") - # [END print_solution] + # [START data] + model = set_cover.SetCoverModel() + model.add_empty_subset(2.0) + model.add_element_to_last_subset(0) + model.add_empty_subset(2.0) + model.add_element_to_last_subset(1) + model.add_empty_subset(1.0) + model.add_element_to_last_subset(0) + model.add_element_to_last_subset(1) + # [END data] + + # [START solve] + inv = set_cover.SetCoverInvariant(model) + greedy = set_cover.GreedySolutionGenerator(inv) + has_found = greedy.next_solution() + if not has_found: + print("No solution found by the greedy heuristic.") + return + solution = inv.export_solution_as_proto() + # [END solve] + + # [START print_solution] + print(f"Total cost: {solution.cost}") # == inv.cost() + print(f"Total number of selected subsets: {solution.num_subsets}") + print("Chosen subsets:") + for subset in solution.subset: + print(f" {subset}") + # [END print_solution] if __name__ == "__main__": - main() + main() # [END program] diff --git a/ortools/third_party_solvers/BUILD.bazel b/ortools/third_party_solvers/BUILD.bazel new file mode 100644 index 00000000000..7b2ee8fefba --- /dev/null +++ b/ortools/third_party_solvers/BUILD.bazel @@ -0,0 +1,89 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +package(default_visibility = ["//visibility:public"]) + +config_setting( + name = "on_linux", + constraint_values = ["@platforms//os:linux"], +) + +config_setting( + name = "on_macos", + constraint_values = ["@platforms//os:macos"], +) + +config_setting( + name = "on_windows", + constraint_values = ["@platforms//os:windows"], +) + +cc_library( + name = "dynamic_library", + hdrs = ["dynamic_library.h"], + linkopts = select({ + "on_linux": ["-Wl,--no-as-needed -ldl"], + "on_macos": [], + "on_windows": [], + "//conditions:default": [], + }), + deps = [ + "//ortools/base", + "//ortools/base:logging", + "@abseil-cpp//absl/strings", + ], +) + +cc_library( + name = "gurobi_environment", + srcs = [ + "gurobi_environment.cc", + ], + hdrs = [ + "gurobi_environment.h", + ], + deps = [ + ":dynamic_library", + "//ortools/base", + "//ortools/base:file", + "//ortools/base:status_macros", + "@abseil-cpp//absl/base:core_headers", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/synchronization", + "@abseil-cpp//absl/types:optional", + ], +) + +cc_library( + name = "xpress_environment", + srcs = [ + "xpress_environment.cc", + ], + hdrs = [ + "xpress_environment.h", + ], + deps = [ + ":dynamic_library", + "//ortools/base", + "//ortools/base:file", + "//ortools/base:status_macros", + "@abseil-cpp//absl/flags:flag", + "@abseil-cpp//absl/status", + "@abseil-cpp//absl/status:statusor", + "@abseil-cpp//absl/strings", + "@abseil-cpp//absl/synchronization", + "@abseil-cpp//absl/types:optional", + ], +) diff --git a/ortools/xpress/CMakeLists.txt b/ortools/third_party_solvers/CMakeLists.txt similarity index 89% rename from ortools/xpress/CMakeLists.txt rename to ortools/third_party_solvers/CMakeLists.txt index add1ace6ac7..97232c4000a 100644 --- a/ortools/xpress/CMakeLists.txt +++ b/ortools/third_party_solvers/CMakeLists.txt @@ -12,7 +12,7 @@ # limitations under the License. file(GLOB _SRCS "*.h" "*.cc") -set(NAME ${PROJECT_NAME}_xpress) +set(NAME ${PROJECT_NAME}_third_party_solvers) add_library(${NAME} OBJECT ${_SRCS}) set_target_properties(${NAME} PROPERTIES @@ -29,10 +29,8 @@ target_include_directories(${NAME} PRIVATE ${PROJECT_SOURCE_DIR} ${PROJECT_BINARY_DIR}) target_link_libraries(${NAME} PRIVATE - absl::hash - absl::meta - absl::memory + absl::status absl::strings absl::str_format - protobuf::libprotobuf - ${PROJECT_NAMESPACE}::ortools_proto) + absl::synchronization) + diff --git a/ortools/base/dynamic_library.h b/ortools/third_party_solvers/dynamic_library.h similarity index 100% rename from ortools/base/dynamic_library.h rename to ortools/third_party_solvers/dynamic_library.h diff --git a/ortools/gurobi/environment.cc b/ortools/third_party_solvers/gurobi_environment.cc similarity index 91% rename from ortools/gurobi/environment.cc rename to ortools/third_party_solvers/gurobi_environment.cc index 65485c7e270..007e7665271 100644 --- a/ortools/gurobi/environment.cc +++ b/ortools/third_party_solvers/gurobi_environment.cc @@ -11,39 +11,27 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "ortools/gurobi/environment.h" +#include "ortools/third_party_solvers/gurobi_environment.h" -#include +#include +#include #include +#include -#include "absl/flags/flag.h" +#include "absl/base/no_destructor.h" #include "absl/status/status.h" -#include "absl/status/statusor.h" -#include "absl/strings/match.h" #include "absl/strings/str_cat.h" -#include "absl/strings/str_format.h" #include "absl/strings/str_join.h" -#include "absl/synchronization/mutex.h" -#include "ortools/base/file.h" +#include "absl/strings/string_view.h" #include "ortools/base/logging.h" -#include "ortools/base/status_macros.h" +#include "ortools/third_party_solvers/dynamic_library.h" namespace operations_research { -bool GurobiIsCorrectlyInstalled() { - absl::StatusOr status = GetGurobiEnv(); - if (!status.ok() || status.value() == nullptr) { - LOG(WARNING) << status.status(); - return false; - } - - GRBfreeenv(status.value()); - - return true; -} - // This was generated with the parse_header.py script. // See the comment at the top of the script. +// Let's not reformat the rest of the file. +// clang-format off // This is the 'define' section. std::function GRBisattravailable = @@ -344,6 +332,8 @@ void LoadGurobiFunctions(DynamicLibrary* gurobi_dynamic_library) { gurobi_dynamic_library->GetFunction(&GRBplatform, "GRBplatform"); } +// clang-format on + std::vector GurobiDynamicLibraryPotentialPaths() { std::vector potential_paths; const std::vector kGurobiVersions = { @@ -369,7 +359,7 @@ std::vector GurobiDynamicLibraryPotentialPaths() { potential_paths.push_back( absl::StrCat(gurobi_home_from_env, "/lib64/libgurobi", lib, ".so")); #else - LOG(ERROR) << "OS Not recognized by gurobi/environment.cc." + LOG(ERROR) << "OS Not recognized by gurobi_environment.cc." << " You won't be able to use Gurobi."; #endif } @@ -401,7 +391,7 @@ std::vector GurobiDynamicLibraryPotentialPaths() { potential_paths.push_back( absl::StrCat("/opt/gurobi/linux64/lib64/libgurobi", lib, ".so")); #else - LOG(ERROR) << "OS Not recognized by gurobi/environment.cc." + LOG(ERROR) << "OS Not recognized by gurobi_environment.cc." << " You won't be able to use Gurobi."; #endif } @@ -419,51 +409,47 @@ std::vector GurobiDynamicLibraryPotentialPaths() { absl::Status LoadGurobiDynamicLibrary( std::vector potential_paths) { - static std::once_flag gurobi_loading_done; - static absl::Status gurobi_load_status; - static DynamicLibrary gurobi_library; - static absl::Mutex mutex; - - absl::MutexLock lock(&mutex); + struct GurobiLibraryStruct { + absl::Status gurobi_load_status; + DynamicLibrary gurobi_library; + }; - std::call_once(gurobi_loading_done, [&potential_paths]() { - const std::vector canonical_paths = - GurobiDynamicLibraryPotentialPaths(); - potential_paths.insert(potential_paths.end(), canonical_paths.begin(), - canonical_paths.end()); + static absl::NoDestructor loaded([&potential_paths]() { + GurobiLibraryStruct result; + // Try to load the library from the potential paths. for (const absl::string_view path : potential_paths) { - if (gurobi_library.TryToLoad(path)) { - LOG(INFO) << "Found the Gurobi library in '" << path << "."; + if (result.gurobi_library.TryToLoad(path)) { + VLOG(1) << "Found the Gurobi library in '" << path << "."; break; } } - if (gurobi_library.LibraryIsLoaded()) { - LoadGurobiFunctions(&gurobi_library); - gurobi_load_status = absl::OkStatus(); + // Fallback to the canonical paths. + if (!result.gurobi_library.LibraryIsLoaded()) { + const std::vector canonical_paths = + GurobiDynamicLibraryPotentialPaths(); + for (const absl::string_view path : canonical_paths) { + if (result.gurobi_library.TryToLoad(path)) { + VLOG(1) << "Found the Gurobi library in '" << path << "."; + break; + } + } + } + + if (result.gurobi_library.LibraryIsLoaded()) { + LoadGurobiFunctions(&result.gurobi_library); + result.gurobi_load_status = absl::OkStatus(); } else { - gurobi_load_status = absl::NotFoundError(absl::StrCat( + result.gurobi_load_status = absl::NotFoundError(absl::StrCat( "Could not find the Gurobi shared library. Looked in: [", absl::StrJoin(potential_paths, "', '"), "]. If you know where it" " is, pass the full path to 'LoadGurobiDynamicLibrary()'.")); } - }); - return gurobi_load_status; -} + return result; + }()); -absl::StatusOr GetGurobiEnv() { - RETURN_IF_ERROR(LoadGurobiDynamicLibrary({})); - - GRBenv* env = nullptr; - - if (GRBloadenv(&env, nullptr) != 0 || env == nullptr) { - return absl::FailedPreconditionError( - absl::StrCat("Found the Gurobi shared library, but could not create " - "Gurobi environment: is Gurobi licensed on this machine?", - GRBgeterrormsg(env))); - } - return env; + return loaded->gurobi_load_status; } } // namespace operations_research diff --git a/ortools/gurobi/environment.h b/ortools/third_party_solvers/gurobi_environment.h similarity index 98% rename from ortools/gurobi/environment.h rename to ortools/third_party_solvers/gurobi_environment.h index ab3bd46e765..c1985daf3ff 100644 --- a/ortools/gurobi/environment.h +++ b/ortools/third_party_solvers/gurobi_environment.h @@ -11,16 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef OR_TOOLS_GUROBI_ENVIRONMENT_H_ -#define OR_TOOLS_GUROBI_ENVIRONMENT_H_ +#ifndef THIRD_PARTY_ORTOOLS_ORTOOLS_THIRD_PARTY_SOLVERS_GUROBI_ENVIRONMENT_H_ +#define THIRD_PARTY_ORTOOLS_ORTOOLS_THIRD_PARTY_SOLVERS_GUROBI_ENVIRONMENT_H_ + +#include +#include -#include "absl/flags/declare.h" -#include "absl/flags/flag.h" #include "absl/status/status.h" -#include "absl/status/statusor.h" #include "absl/strings/string_view.h" -#include "ortools/base/dynamic_library.h" -#include "ortools/base/logging.h" #if defined(_MSC_VER) #define GUROBI_STDCALL __stdcall @@ -40,20 +38,15 @@ typedef struct _GRBsvec { namespace operations_research { -absl::StatusOr GetGurobiEnv(); - -// This returns true if the Gurobi shared library is properly loaded (otherwise, -// tries to find it and load it) and if a Gurobi license can be obtained (it -// does that by trying to grab a license and then release it). -bool GurobiIsCorrectlyInstalled(); - -// clang-format off // Force the loading of the gurobi dynamic library. It returns true if the // library was successfully loaded. This method can only be called once. // Successive calls are no-op. // // Note that it does not check if a token license can be grabbed. -absl::Status LoadGurobiDynamicLibrary(std::vector potential_paths); +absl::Status LoadGurobiDynamicLibrary( + std::vector potential_paths); + +// clang-format off // The list of #define and extern std::function<> below is generated directly // from gurobi_c.h via parse_header.py @@ -735,6 +728,8 @@ extern std::function GRBplatform; #define GRB_BATCH_ABORTED 3 #define GRB_BATCH_FAILED 4 #define GRB_BATCH_COMPLETED 5 + +// clang-format on } // namespace operations_research -#endif // OR_TOOLS_GUROBI_ENVIRONMENT_H_ +#endif // THIRD_PARTY_ORTOOLS_ORTOOLS_THIRD_PARTY_SOLVERS_GUROBI_ENVIRONMENT_H_ diff --git a/ortools/third_party_solvers/gurobi_parse_header.py b/ortools/third_party_solvers/gurobi_parse_header.py new file mode 100644 index 00000000000..f20c2856340 --- /dev/null +++ b/ortools/third_party_solvers/gurobi_parse_header.py @@ -0,0 +1,280 @@ +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +r"""Gurobi header parser script to generate code for the environment.{cc|h}. + +To use, run the script + copy gurobi_c.h somewhere. + edit the file and add the signature for the GRBisqp: + int __stdcall + GRBisqp(GRBenv**, const char*, const char*, const char*, int, const char*); + blaze run \ + ortools/third_party_solvers/gurobi_parse_header \ + -- + +It will output all methods defined in the EXPORTED_FUNCTIONS field, and all +defined symbols. + +The list of symbols to export is found by the following command: + grep -oh -e "GRB[A-Za-z0-9_]*" /gurobi_interface.cc \ + /gurobi_proto_solver.* /math_opt/solvers/gurobi* \ + /math_opt/solvers/gurobi/g_gurobi* | sort -u + +This will printout on the console 3 sections: + +------------------- header ------------------- + +to copy paste in environment.h + +------------------- define ------------------- + +to copy in the define part of environment.cc + +------------------- assign ------------------- + +to copy in the assign part of environment.cc +""" + +import re +from typing import Sequence +from absl import app + +EXPORTED_FUNCTIONS = frozenset([ + "GRBaddconstr", + "GRBaddconstrs", + "GRBaddgenconstrAbs", + "GRBaddgenconstrAnd", + "GRBaddgenconstrIndicator", + "GRBaddgenconstrMax", + "GRBaddgenconstrMin", + "GRBaddgenconstrOr", + "GRBaddqconstr", + "GRBaddqpterms", + "GRBaddrangeconstr", + "GRBaddsos", + "GRBaddvar", + "GRBaddvars", + "GRBcbcut", + "GRBcbget", + "GRBcblazy", + "GRBcbsolution", + "GRBchgcoeffs", + "GRBcopyparams", + "GRBdelconstrs", + "GRBdelgenconstrs", + "GRBdelq", + "GRBdelqconstrs", + "GRBdelsos", + "GRBdelvars", + "GRBenv", + "GRBenvUniquePtr", + "GRBemptyenv", + "GRBgetnumparams", + "GRBgetparamtype", + "GRBgetparamname", + "GRBgetintparaminfo", + "GRBgetdblparaminfo", + "GRBgetstrparaminfo", + "GRBfreeenv", + "GRBfreemodel", + "GRBgetcharattrarray", + "GRBgetcharattrelement", + "GRBgetdblattr", + "GRBgetdblattrarray", + "GRBgetdblattrelement", + "GRBgetdblparam", + "GRBgetenv", + "GRBgeterrormsg", + "GRBgetintattr", + "GRBgetintattrarray", + "GRBgetintattrelement", + "GRBgetintparam", + "GRBgetstrattr", + "GRBgetstrparam", + "GRBgetvars", + "GRBisattravailable", + "GRBisqp", + "GRBloadenv", + "GRBmodel", + "GRBnewmodel", + "GRBoptimize", + "GRBcomputeIIS", + "GRBplatform", + "GRBresetparams", + "GRBsetcallbackfunc", + "GRBsetcharattrarray", + "GRBsetcharattrelement", + "GRBsetcharattrlist", + "GRBsetdblattr", + "GRBsetdblattrarray", + "GRBsetdblattrelement", + "GRBsetdblattrlist", + "GRBsetdblparam", + "GRBsetintattr", + "GRBsetintattrarray", + "GRBsetintattrelement", + "GRBsetintattrlist", + "GRBsetintparam", + "GRBsetobjectiven", + "GRBsetparam", + "GRBsetstrattr", + "GRBsetstrparam", + "GRBterminate", + "GRBupdatemodel", + "GRBversion", + "GRBwrite", +]) + +# TODO(user): Filter #define too. + + +class GurobiHeaderParser: + """Converts gurobi_c.h to something pastable in ./environment.h|.cc.""" + + def __init__(self): + self.__header = "" + self.__define = "" + self.__assign = "" + self.__state = 0 + self.__return_type = "" + self.__args = "" + self.__fun_name = "" + + def should_be_exported(self, name: str) -> bool: + return name in EXPORTED_FUNCTIONS + + def write_define(self, symbol: str, value: str) -> None: + self.__header += f"#define {symbol} {value}\n" + + def write_fun(self, name: str, return_type: str, args: str) -> None: + if not self.should_be_exported(name): + print("skipping " + name) + return + + self.__header += f"extern std::function<{return_type}({args})> {name};\n" + self.__define += f"std::function<{return_type}({args})> {name} = nullptr;\n" + self.__assign += f" gurobi_dynamic_library->GetFunction(&{name}, " + self.__assign += f'"{name}");\n' + + def parse(self, filepath: str) -> None: + """Main method to parser the gurobi header.""" + + with open(filepath) as fp: + all_lines = fp.read() + + for line in all_lines.splitlines(): + if not line: # Ignore empty lines. + continue + if re.fullmatch(r"/\*", line, re.M): # Ignore comments. + continue + + if self.__state == 0: + # Note: fullmatch does not work. + match_def = re.match(r"#define ([A-Z0-9_]*)\s+([^/]+)", line, re.M) + if match_def: + self.write_define(match_def.group(1), match_def.group(2)) + continue + + # Single line function definition. + match_fun = re.fullmatch( + r"([a-z]+) __stdcall (GRB[A-Za-z_]*)\(([^;]*)\);", line, re.M + ) + if match_fun: + self.write_fun( + match_fun.group(1), match_fun.group(2), match_fun.group(3) + ) + continue + + # Simple type declaration (i.e. int __stdcall). + match_fun = re.fullmatch(r"([a-z]+) __stdcall\s*$", line, re.M) + if match_fun: + self.__return_type = match_fun.group(1) + self.__state = 1 + continue + + # Complex type declaration with pointer (i.e. GRBModel* __stdcall). + match_fun = re.fullmatch(r"([A-Za-z ]+)\*\s*__stdcall\s*$", line, re.M) + if match_fun: + self.__return_type = match_fun.group(1) + "*" + self.__state = 1 + continue + + elif self.__state == 1: # The return type was defined at the line before. + # Function definition terminates in this line. + match_fun = re.fullmatch(r"\s*(GRB[A-Za-z_]*)\(([^;]+)\);", line, re.M) + if match_fun: + self.write_fun( + match_fun.group(1), self.__return_type, match_fun.group(2) + ) + self.__state = 0 + self.__return_type = "" + continue + + # Function definition does not terminate in this line. + match_fun = re.fullmatch(r"\s*(GRB[A-Za-z_]*)\(([^;]+)$", line, re.M) + if match_fun: + self.__fun_name = match_fun.group(1) + self.__args = match_fun.group(2) + self.__state = 2 + continue + + elif self.__state == 2: # Extra arguments. + # Arguments end in this line. + match_fun = re.fullmatch(r"\s*([^;]+)\);", line, re.M) + if match_fun: + self.__args += match_fun.group(1) + self.write_fun(self.__fun_name, self.__return_type, self.__args) + self.__args = "" + self.__fun_name = "" + self.__return_type = "" + self.__state = 0 + continue + + # Arguments do not end in this line. + match_fun = re.fullmatch(r"\s*([^;]+)$", line, re.M) + if match_fun: + self.__args += match_fun.group(1) + continue + + def output(self) -> None: + """Output the 3 generated code on standard out.""" + + # replace __stdcall by GUROBI_STDCALL. + self.__header = self.__header.replace("__stdcall", "GUROBI_STDCALL") + self.__define = self.__define.replace("__stdcall", "GUROBI_STDCALL") + + print("------------------- header -------------------") + print(self.__header) + + print("------------------- define -------------------") + print(self.__define) + + print("------------------- assign -------------------") + print(self.__assign) + + +def main(argv: Sequence[str]) -> None: + if len(argv) > 2: + raise app.UsageError("Too many command-line arguments.") + if len(argv) == 1: + raise app.UsageError( + "Please supply path to gurobi_c.h on the command line." + ) + + parser = GurobiHeaderParser() + parser.parse(argv[1]) + parser.output() + + +if __name__ == "__main__": + app.run(main) diff --git a/ortools/xpress/environment.cc b/ortools/third_party_solvers/xpress_environment.cc similarity index 98% rename from ortools/xpress/environment.cc rename to ortools/third_party_solvers/xpress_environment.cc index 5e628099e25..04d872d7a6b 100644 --- a/ortools/xpress/environment.cc +++ b/ortools/third_party_solvers/xpress_environment.cc @@ -1,4 +1,3 @@ -// Copyright 2019-2023 RTE // Copyright 2010-2025 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +13,7 @@ // Initial version of this code was provided by RTE -#include "ortools/xpress/environment.h" +#include "ortools/third_party_solvers/xpress_environment.h" #include // NOLINTNEXTLINE(build/c++17) @@ -29,8 +28,8 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_join.h" #include "absl/synchronization/mutex.h" -#include "ortools/base/dynamic_library.h" #include "ortools/base/logging.h" +#include "ortools/third_party_solvers/dynamic_library.h" namespace operations_research { @@ -217,7 +216,7 @@ std::vector XpressDynamicLibraryPotentialPaths() { potential_paths.push_back( absl::StrCat(xpressdir_from_env, "/lib/libxprs.so")); #else - LOG(ERROR) << "OS Not recognized by xpress/environment.cc." + LOG(ERROR) << "OS Not recognized by xpress_environment.cc." << " You won't be able to use Xpress."; #endif } else { @@ -235,7 +234,7 @@ std::vector XpressDynamicLibraryPotentialPaths() { #elif defined(__GNUC__) // Linux potential_paths.push_back(absl::StrCat("/opt/xpressmp/lib/libxprs.so")); #else - LOG(ERROR) << "OS Not recognized by xpress/environment.cc." + LOG(ERROR) << "OS Not recognized by xpress_environment.cc." << " You won't be able to use Xpress."; #endif return potential_paths; diff --git a/ortools/xpress/environment.h b/ortools/third_party_solvers/xpress_environment.h similarity index 98% rename from ortools/xpress/environment.h rename to ortools/third_party_solvers/xpress_environment.h index 9a1fe558be1..aafecf3c4a6 100644 --- a/ortools/xpress/environment.h +++ b/ortools/third_party_solvers/xpress_environment.h @@ -1,4 +1,3 @@ -// Copyright 2019-2023 RTE // Copyright 2010-2025 Google LLC // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,8 +13,8 @@ // Initial version of this code was provided by RTE -#ifndef OR_TOOLS_XPRESS_ENVIRONMENT_H_ -#define OR_TOOLS_XPRESS_ENVIRONMENT_H_ +#ifndef THIRD_PARTY_ORTOOLS_ORTOOLS_THIRD_PARTY_SOLVERS_XPRESS_ENVIRONMENT_H_ +#define THIRD_PARTY_ORTOOLS_ORTOOLS_THIRD_PARTY_SOLVERS_XPRESS_ENVIRONMENT_H_ #include #include @@ -548,4 +547,4 @@ extern std::function::View BitsetView() { return bitset_.view(); } + typename Bitset64::ConstView BitsetConstView() { + return bitset_.const_view(); + } void SetUnsafe(typename Bitset64::View view, IntegerType index) { view.Set(index); to_clear_.push_back(index); diff --git a/ortools/util/file_util.cc b/ortools/util/file_util.cc index 6ee86f6e526..62d0aaa314e 100644 --- a/ortools/util/file_util.cc +++ b/ortools/util/file_util.cc @@ -166,7 +166,11 @@ absl::Status WriteProtoToFile(absl::string_view filename, case ProtoWriteFormat::kJson: { google::protobuf::util::JsonPrintOptions options; options.add_whitespace = true; +#if PROTOBUF_VERSION >= 5026000 // Version 26.0.0 options.always_print_fields_with_no_presence = true; +#else + options.always_print_primitive_fields = true; +#endif options.preserve_proto_field_names = true; if (!google::protobuf::util::MessageToJsonString(proto, &output_string, options) diff --git a/ortools/util/python/sorted_interval_list_test.py b/ortools/util/python/sorted_interval_list_test.py index 1a83f623b55..e45cd67366f 100755 --- a/ortools/util/python/sorted_interval_list_test.py +++ b/ortools/util/python/sorted_interval_list_test.py @@ -18,76 +18,76 @@ class SortedIntervalListTest(absltest.TestCase): - def testCtorAndGetter(self): - bool_domain = sorted_interval_list.Domain(0, 1) - self.assertEqual(2, bool_domain.size()) - self.assertEqual(0, bool_domain.min()) - self.assertEqual(1, bool_domain.max()) - self.assertFalse(bool_domain.is_empty()) - self.assertEqual(str(bool_domain), "[0,1]") + def testCtorAndGetter(self): + bool_domain = sorted_interval_list.Domain(0, 1) + self.assertEqual(2, bool_domain.size()) + self.assertEqual(0, bool_domain.min()) + self.assertEqual(1, bool_domain.max()) + self.assertFalse(bool_domain.is_empty()) + self.assertEqual(str(bool_domain), "[0,1]") - def testFromValues(self): - domain = sorted_interval_list.Domain.FromValues([1, 3, -5, 5]) - self.assertEqual(4, domain.size()) - self.assertEqual(-5, domain.min()) - self.assertEqual(5, domain.max()) - self.assertEqual([-5, -5, 1, 1, 3, 3, 5, 5], domain.flattened_intervals()) - self.assertTrue(domain.contains(1)) - self.assertFalse(domain.contains(0)) + def testFromValues(self): + domain = sorted_interval_list.Domain.FromValues([1, 3, -5, 5]) + self.assertEqual(4, domain.size()) + self.assertEqual(-5, domain.min()) + self.assertEqual(5, domain.max()) + self.assertEqual([-5, -5, 1, 1, 3, 3, 5, 5], domain.flattened_intervals()) + self.assertTrue(domain.contains(1)) + self.assertFalse(domain.contains(0)) - def testFromIntervals(self): - domain = sorted_interval_list.Domain.from_intervals([[2, 4], [-2, 0]]) - self.assertEqual(6, domain.size()) - self.assertEqual(-2, domain.min()) - self.assertEqual(4, domain.max()) - self.assertEqual([-2, 0, 2, 4], domain.flattened_intervals()) + def testFromIntervals(self): + domain = sorted_interval_list.Domain.from_intervals([[2, 4], [-2, 0]]) + self.assertEqual(6, domain.size()) + self.assertEqual(-2, domain.min()) + self.assertEqual(4, domain.max()) + self.assertEqual([-2, 0, 2, 4], domain.flattened_intervals()) - def testFromFlatIntervals(self): - domain = sorted_interval_list.Domain.from_flat_intervals([2, 4, -2, 0]) - self.assertEqual(6, domain.size()) - self.assertEqual(-2, domain.min()) - self.assertEqual(4, domain.max()) - self.assertEqual([-2, 0, 2, 4], domain.flattened_intervals()) + def testFromFlatIntervals(self): + domain = sorted_interval_list.Domain.from_flat_intervals([2, 4, -2, 0]) + self.assertEqual(6, domain.size()) + self.assertEqual(-2, domain.min()) + self.assertEqual(4, domain.max()) + self.assertEqual([-2, 0, 2, 4], domain.flattened_intervals()) - def testNegation(self): - domain = sorted_interval_list.Domain(5, 20) - self.assertEqual([-20, -5], domain.negation().flattened_intervals()) + def testNegation(self): + domain = sorted_interval_list.Domain(5, 20) + self.assertEqual([-20, -5], domain.negation().flattened_intervals()) - def testUnion(self): - d1 = sorted_interval_list.Domain(0, 5) - d2 = sorted_interval_list.Domain(10, 15) - d3 = d1.union_with(d2) - self.assertEqual([0, 5], d1.flattened_intervals()) - self.assertEqual([10, 15], d2.flattened_intervals()) - self.assertEqual([0, 5, 10, 15], d3.flattened_intervals()) + def testUnion(self): + d1 = sorted_interval_list.Domain(0, 5) + d2 = sorted_interval_list.Domain(10, 15) + d3 = d1.union_with(d2) + self.assertEqual([0, 5], d1.flattened_intervals()) + self.assertEqual([10, 15], d2.flattened_intervals()) + self.assertEqual([0, 5, 10, 15], d3.flattened_intervals()) - def testIntersection(self): - d1 = sorted_interval_list.Domain(0, 10) - d2 = sorted_interval_list.Domain(5, 15) - d3 = d1.intersection_with(d2) - self.assertEqual([0, 10], d1.flattened_intervals()) - self.assertEqual([5, 15], d2.flattened_intervals()) - self.assertEqual([5, 10], d3.flattened_intervals()) + def testIntersection(self): + d1 = sorted_interval_list.Domain(0, 10) + d2 = sorted_interval_list.Domain(5, 15) + d3 = d1.intersection_with(d2) + self.assertEqual([0, 10], d1.flattened_intervals()) + self.assertEqual([5, 15], d2.flattened_intervals()) + self.assertEqual([5, 10], d3.flattened_intervals()) - def testAddition(self): - d1 = sorted_interval_list.Domain(0, 5) - d2 = sorted_interval_list.Domain(10, 15) - d3 = d1.addition_with(d2) - self.assertEqual([0, 5], d1.flattened_intervals()) - self.assertEqual([10, 15], d2.flattened_intervals()) - self.assertEqual([10, 20], d3.flattened_intervals()) + def testAddition(self): + d1 = sorted_interval_list.Domain(0, 5) + d2 = sorted_interval_list.Domain(10, 15) + d3 = d1.addition_with(d2) + self.assertEqual([0, 5], d1.flattened_intervals()) + self.assertEqual([10, 15], d2.flattened_intervals()) + self.assertEqual([10, 20], d3.flattened_intervals()) - def testComplement(self): - d1 = sorted_interval_list.Domain(-9223372036854775808, 5) - d2 = d1.complement() - self.assertEqual([-9223372036854775808, 5], d1.flattened_intervals()) - self.assertEqual([6, 9223372036854775807], d2.flattened_intervals()) + def testComplement(self): + d1 = sorted_interval_list.Domain(-9223372036854775808, 5) + d2 = d1.complement() + self.assertEqual([-9223372036854775808, 5], d1.flattened_intervals()) + self.assertEqual([6, 9223372036854775807], d2.flattened_intervals()) - def testStr(self): - d1 = sorted_interval_list.Domain(0, 5) - self.assertEqual(str(d1), "[0,5]") - self.assertEqual(repr(d1), "Domain([0,5])") + def testStr(self): + d1 = sorted_interval_list.Domain(0, 5) + self.assertEqual(str(d1), "[0,5]") + self.assertEqual(repr(d1), "Domain([0,5])") if __name__ == "__main__": - absltest.main() + absltest.main() diff --git a/ortools/util/sigint.cc b/ortools/util/sigint.cc index 601f4983ccd..bd4f40cfac7 100644 --- a/ortools/util/sigint.cc +++ b/ortools/util/sigint.cc @@ -23,29 +23,47 @@ namespace operations_research { void SigintHandler::Register(const std::function& f) { handler_ = [this, f]() -> void { - const int num_sigint_calls = ++num_sigint_calls_; - if (num_sigint_calls < 3) { + const int num_calls = ++num_calls_; + if (num_calls < 3) { LOG(INFO) - << "^C pressed " << num_sigint_calls << " times. " + << "^C pressed " << num_calls << " times. " << "Interrupting the solver. Press 3 times to force termination."; - if (num_sigint_calls == 1) f(); - } else if (num_sigint_calls == 3) { + if (num_calls == 1) f(); + } else if (num_calls == 3) { LOG(INFO) << "^C pressed 3 times. Forcing termination."; exit(EXIT_FAILURE); } else { // Another thread is already running exit(), do nothing. } }; - signal(SIGINT, &ControlCHandler); + signal(SIGINT, &SigHandler); } // This method will be called by the system after the SIGINT signal. // The parameter is the signal received. -void SigintHandler::ControlCHandler(int sig) { handler_(); } +void SigintHandler::SigHandler(int) { handler_(); } -// Unregister the SIGINT handler. -SigintHandler::~SigintHandler() { signal(SIGINT, SIG_DFL); } +// Unregister the signal handlers. +SigintHandler::~SigintHandler() { + if (handler_ != nullptr) signal(SIGINT, SIG_DFL); +} thread_local std::function SigintHandler::handler_; +void SigtermHandler::Register(const std::function& f) { + handler_ = [f]() -> void { f(); }; + signal(SIGTERM, &SigHandler); +} + +// This method will be called by the system after the SIGTERM signal. +// The parameter is the signal received. +void SigtermHandler::SigHandler(int) { handler_(); } + +// Unregister the signal handlers. +SigtermHandler::~SigtermHandler() { + if (handler_ != nullptr) signal(SIGTERM, SIG_DFL); +} + +thread_local std::function SigtermHandler::handler_; + } // namespace operations_research diff --git a/ortools/util/sigint.h b/ortools/util/sigint.h index 7b3098033e0..1d9fcd1b814 100644 --- a/ortools/util/sigint.h +++ b/ortools/util/sigint.h @@ -21,7 +21,7 @@ namespace operations_research { class SigintHandler { public: - SigintHandler() {} + SigintHandler() = default; ~SigintHandler(); // Catches ^C and call f() the first time this happen. If ^C is pressed 3 @@ -29,9 +29,23 @@ class SigintHandler { void Register(const std::function& f); private: - static void ControlCHandler(int s); + std::atomic num_calls_ = 0; - std::atomic num_sigint_calls_ = 0; + static void SigHandler(int s); + thread_local static std::function handler_; +}; + +class SigtermHandler { + public: + SigtermHandler() = default; + ~SigtermHandler(); + + // Catches SIGTERM and call f(). It is recommended that f() calls exit() to + // terminate the program. + void Register(const std::function& f); + + private: + static void SigHandler(int s); thread_local static std::function handler_; }; diff --git a/ortools/util/sorted_interval_list.h b/ortools/util/sorted_interval_list.h index f07dca7c71a..fb62e30d271 100644 --- a/ortools/util/sorted_interval_list.h +++ b/ortools/util/sorted_interval_list.h @@ -724,7 +724,9 @@ class ClosedInterval::Iterator { // arithmetic. uint64_t current_; }; - +#if __cplusplus >= 202002L +static_assert(std::input_iterator); +#endif // begin()/end() are required for iteration over ClosedInterval in a range for // loop. inline ClosedInterval::Iterator begin(ClosedInterval interval) { diff --git a/ortools/xpress/BUILD.bazel b/ortools/xpress/BUILD.bazel deleted file mode 100644 index c5fb93d522e..00000000000 --- a/ortools/xpress/BUILD.bazel +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2010-2025 Google LLC -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -package(default_visibility = ["//visibility:public"]) - -cc_library( - name = "environment", - srcs = [ - "environment.cc", - ], - hdrs = [ - "environment.h", - ], - deps = [ - "//ortools/base", - "//ortools/base:dynamic_library", - "//ortools/base:file", - "//ortools/base:status_macros", - "@abseil-cpp//absl/status", - "@abseil-cpp//absl/status:statusor", - "@abseil-cpp//absl/strings", - "@abseil-cpp//absl/synchronization", - "@abseil-cpp//absl/types:optional", - ], -) diff --git a/patches/BUILD.bazel b/patches/BUILD.bazel index 22f2795c84e..f73a6d5b4bc 100644 --- a/patches/BUILD.bazel +++ b/patches/BUILD.bazel @@ -13,8 +13,7 @@ exports_files([ "abseil-cpp-20250512.0.patch", - "highs-v1.10.patch", - "protobuf-v31.0.patch", + "protobuf-v31.1.patch", "pybind11_bazel.patch", "pybind11_abseil.patch", "pybind11_protobuf.patch", diff --git a/patches/bzip2.patch b/patches/bzip2.patch index ace48522907..ee1caf8d531 100644 --- a/patches/bzip2.patch +++ b/patches/bzip2.patch @@ -1,5 +1,5 @@ diff --git a/CMakeLists.txt b/CMakeLists.txt -index c4b0b6e..30f7652 100644 +index c4b0b6e..ee39341 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,10 @@ @@ -24,7 +24,7 @@ index c4b0b6e..30f7652 100644 # Windows resource file set(BZ2_RES "") -@@ -299,21 +304,30 @@ endif() +@@ -299,21 +304,32 @@ endif() if(ENABLE_SHARED_LIB) # The libbz2 shared library. @@ -59,13 +59,15 @@ index c4b0b6e..30f7652 100644 + ) + install(TARGETS BZip2 + EXPORT ${PROJECT_NAME}Targets -+ DESTINATION ${CMAKE_INSTALL_LIBDIR}) ++ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} # For Windows DLLs and executables ++ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} # For shared libraries on UNIX ++ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) # For static libs or import libs install(FILES bzlib.h DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) + add_library(BZip2::BZip2 ALIAS BZip2) if(USE_OLD_SONAME) # Hack to support the old libbz2.so.1.0 version by including an extra copy. -@@ -323,16 +337,22 @@ if(ENABLE_SHARED_LIB) +@@ -323,16 +339,22 @@ if(ENABLE_SHARED_LIB) add_library(bz2_old_soname SHARED ${BZ2_RES}) target_sources(bz2_old_soname PRIVATE ${BZ2_SOURCES} @@ -92,7 +94,7 @@ index c4b0b6e..30f7652 100644 endif() endif() endif() -@@ -341,9 +361,13 @@ if(ENABLE_STATIC_LIB) +@@ -341,9 +363,13 @@ if(ENABLE_STATIC_LIB) # The libbz2 static library. add_library(bz2_static STATIC) target_sources(bz2_static @@ -109,7 +111,7 @@ index c4b0b6e..30f7652 100644 # Use '-fPIC'/'-fPIE' option for static libraries by default. # You may build with ENABLE_STATIC_LIB_IS_PIC=OFF to disable PIC for the static library. -@@ -357,8 +381,13 @@ if(ENABLE_STATIC_LIB) +@@ -357,8 +383,13 @@ if(ENABLE_STATIC_LIB) SOVERSION ${LT_SOVERSION} ARCHIVE_OUTPUT_NAME bz2_static) target_compile_definitions(bz2_static PUBLIC BZ2_STATICLIB) @@ -124,7 +126,7 @@ index c4b0b6e..30f7652 100644 endif() if(ENABLE_APP) -@@ -373,7 +402,9 @@ if(ENABLE_APP) +@@ -373,7 +404,9 @@ if(ENABLE_APP) else() target_compile_definitions(bzip2 PUBLIC BZ_LCCWIN32=0 BZ_UNIX) endif() @@ -135,7 +137,7 @@ index c4b0b6e..30f7652 100644 # Create bzip2 copies bzcat and bunzip. # The default behavior is altered in bzip2.c code by checking the program name. -@@ -391,7 +422,9 @@ if(ENABLE_APP) +@@ -391,7 +424,9 @@ if(ENABLE_APP) else() target_compile_definitions(bzip2recover PUBLIC BZ_LCCWIN32=0 BZ_UNIX) endif() @@ -146,7 +148,7 @@ index c4b0b6e..30f7652 100644 if(ENABLE_EXAMPLES) if(ENABLE_SHARED_LIB) -@@ -399,8 +432,10 @@ if(ENABLE_APP) +@@ -399,8 +434,10 @@ if(ENABLE_APP) add_executable(dlltest) target_sources(dlltest PRIVATE dlltest.c) @@ -159,7 +161,7 @@ index c4b0b6e..30f7652 100644 endif() endif() -@@ -419,6 +454,10 @@ if(ENABLE_APP) +@@ -419,6 +456,10 @@ if(ENABLE_APP) endif() diff --git a/patches/fuzztest-2025-02-14.patch b/patches/fuzztest-2025-02-14.patch index 053736fbb79..d288eb54188 100644 --- a/patches/fuzztest-2025-02-14.patch +++ b/patches/fuzztest-2025-02-14.patch @@ -36,7 +36,7 @@ index 1f4f08d..cc4d0ba 100644 set(proto_URL https://github.com/protocolbuffers/protobuf.git) -set(proto_TAG v28.2) -+set(proto_TAG v31.0) ++set(proto_TAG v31.1) set(nlohmann_json_URL https://github.com/nlohmann/json.git) set(nlohmann_json_TAG v3.11.2) diff --git a/patches/highs-v1.10.0.patch b/patches/highs-v1.10.0.patch deleted file mode 100644 index e7f58d19626..00000000000 --- a/patches/highs-v1.10.0.patch +++ /dev/null @@ -1,169 +0,0 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index ffaf5290..bf7d1f56 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -194,11 +194,11 @@ if (BUILD_CXX) - set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}) - # for multi-config build system (e.g. xcode) -- foreach(OUTPUTCONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) -- string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG) -- set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OUTPUTCONFIG}/${CMAKE_INSTALL_LIBDIR}) -- set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OUTPUTCONFIG}/${CMAKE_INSTALL_LIBDIR}) -- set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OUTPUTCONFIG}/${CMAKE_INSTALL_BINDIR}) -+ foreach(OutputConfig IN LISTS CMAKE_CONFIGURATION_TYPES) -+ string(TOUPPER ${OutputConfig} OUTPUTCONFIG) -+ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OutputConfig}/${CMAKE_INSTALL_LIBDIR}) -+ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OutputConfig}/${CMAKE_INSTALL_LIBDIR}) -+ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OutputConfig}/${CMAKE_INSTALL_BINDIR}) - endforeach() - else() - option(BUILD_SHARED_LIBS "Build shared libraries (.dll)." OFF) -@@ -206,14 +206,11 @@ if (BUILD_CXX) - set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}) - set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}) - # for multi-config builds (e.g. msvc) -- foreach(OUTPUTCONFIG IN LISTS CMAKE_CONFIGURATION_TYPES) -- string(TOLOWER ${OUTPUTCONFIG} OUTPUTCONFIG) -- set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}/${OUTPUTCONFIG}) -- set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}/${OUTPUTCONFIG}) -- set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}/${OUTPUTCONFIG}) -- # set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OUTPUTCONFIG}/${CMAKE_INSTALL_BINDIR}) -- # set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OUTPUTCONFIG}/${CMAKE_INSTALL_BINDIR}) -- # set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OUTPUTCONFIG}/${CMAKE_INSTALL_BINDIR}) -+ foreach(OutputConfig IN LISTS CMAKE_CONFIGURATION_TYPES) -+ string(TOUPPER ${OutputConfig} OUTPUTCONFIG) -+ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OutputConfig}/${CMAKE_INSTALL_BINDIR}) -+ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OutputConfig}/${CMAKE_INSTALL_BINDIR}) -+ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OutputConfig}/${CMAKE_INSTALL_BINDIR}) - endforeach() - endif() - -diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt -index e390ac4b..0e2c470c 100644 ---- a/src/CMakeLists.txt -+++ b/src/CMakeLists.txt -@@ -1,7 +1,7 @@ - if (NOT BUILD_CXX) - return() - endif() -- -+ - # Define library. - include(sources) - set(sources ${highs_sources} ${cupdlp_sources} ${ipx_sources} ${basiclu_sources}) -@@ -84,7 +84,7 @@ if(NOT FAST_BUILD) - # target_compile_options(libipx PRIVATE "-Wno-sign-compare") - # target_compile_options(libipx PRIVATE "-Wno-logical-op-parentheses") - endif() -- -+ - install(TARGETS libhighs EXPORT highs-targets - LIBRARY - ARCHIVE -@@ -154,8 +154,6 @@ else() - # $ - ) - -- target_include_directories(highs PUBLIC "${CMAKE_CUDA_PATH}/include") -- - # target_include_directories(highs PRIVATE - # $ - # $ -@@ -180,8 +178,8 @@ else() - # $) - - target_sources(highs PRIVATE ${sources} ${headers} ${win_version_file}) -- -- # Optional Cuda -+ -+ # Optional Cuda - if (CUPDLP_GPU) - # enable_language(CXX CUDA) - # target_sources(highs PRIVATE ${cuda_sources}) -@@ -189,9 +187,11 @@ else() - # set_target_properties(highs PROPERTIES CUDA_SEPARABLE_COMPILATION ON) - - # target_link_libraries(highs ${CUDA_LIBRARY} m) -- -+ - # target_include_directories(highs PUBLIC "/usr/local/include") - -+ target_include_directories(highs PUBLIC -+ $) - set(CUPDLP_INCLUDE_DIR "${PROJECT_SOURCE_DIR}/src/pdlp/cupdlp/") - - add_subdirectory(pdlp/cupdlp/cuda) -@@ -201,7 +201,7 @@ else() - else() - target_link_libraries(highs cudalin ${CUDA_LIBRARY} m) - endif() -- -+ - set_target_properties(highs PROPERTIES CUDA_SEPARABLE_COMPILATION ON) - - endif() -@@ -257,13 +257,13 @@ else() - $ - ) - target_link_libraries(highs ZLIB::ZLIB) -- set(CONF_DEPS -+ set(CONF_DEPS - "include(CMakeFindDependencyMacro)\nfind_dependency(Threads)\nfind_dependency(ZLIB)") - set(CONF_DEPENDENCIES ${CONF_DEPS}) -- else() -+ else() - set(CONF_DEPENDENCIES "include(CMakeFindDependencyMacro)\nfind_dependency(Threads)") - endif() -- -+ - - # # on UNIX system the 'lib' prefix is automatically added - # set_target_properties(highs PROPERTIES -@@ -274,7 +274,7 @@ else() - # set_target_properties(highs PROPERTIES - # LIBRARY_OUTPUT_DIRECTORY "${HIGHS_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}") - # endif() -- -+ - # set_target_properties(highs PROPERTIES PUBLIC_HEADER "src/Highs.h;src/lp_data/HighsLp.h;src/lp_data/HighsLpSolverObject.h") - - # install the header files of highs -@@ -291,7 +291,7 @@ else() - - # target_compile_options(highs PRIVATE "-Wall") - # target_compile_options(highs PRIVATE "-Wunused") -- -+ - if (UNIX) - target_compile_options(highs PRIVATE "-Wno-unused-variable") - target_compile_options(highs PRIVATE "-Wno-unused-const-variable") -@@ -324,7 +324,7 @@ else() - - - if (BUILD_DOTNET) -- -+ - # see: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog - if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)") - set(DOTNET_PLATFORM arm64) -@@ -355,8 +355,8 @@ else() - set(TARGET_FILE_NAME "highs.dll") - endif() - -- add_custom_command(TARGET highs POST_BUILD -- COMMAND "${CMAKE_COMMAND}" -E copy -+ add_custom_command(TARGET highs POST_BUILD -+ COMMAND "${CMAKE_COMMAND}" -E copy - "$" - ${DOTNET_PROJECT_DIR}/runtimes/${DOTNET_RID}/native/${TARGET_FILE_NAME} - COMMENT "Copying to output directory") -@@ -375,7 +375,7 @@ if(FORTRAN_FOUND) - target_link_libraries(FortranHighs PUBLIC highs) - endif() - -- install(TARGETS FortranHighs -+ install(TARGETS FortranHighs - LIBRARY - ARCHIVE - RUNTIME diff --git a/patches/highs-v1.11.0.patch b/patches/highs-v1.11.0.patch new file mode 100644 index 00000000000..ce021010764 --- /dev/null +++ b/patches/highs-v1.11.0.patch @@ -0,0 +1,276 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 661aa078..2606e08d 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -57,7 +57,7 @@ endif() + # message("CMAKE_CXX_COMPILER_ID is ${CMAKE_CXX_COMPILER_ID}") + if (CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM") + message(STATUS "Compiler is IntelLLVM") +- if (CMAKE_HOST_WIN32 AND CMAKE_VERSION VERSION_LESS "3.23.0") ++ if (CMAKE_HOST_WIN32 AND CMAKE_VERSION VERSION_LESS "3.23.0") + message(FATAL_ERROR "Need at least CMake 3.23 for IntelLLVM support of IntelDPCPP package on Windows") + elseif(CMAKE_VERSION VERSION_LESS "3.23.0") + message(FATAL_ERROR "CMake 3.20.5 is the minimum recommended for IntelLLVM on Linux") +@@ -121,9 +121,9 @@ endif() + + option(HIGHS_COVERAGE "Activate the code coverage compilation" OFF) + +-# Address | Thread | Leak ++# Address | Thread | Leak + # Linux atm +-# Only Debug is theted atm ++# Only Debug is theted atm + # See below for RelWithDeb info, todo test wip + set(DEBUG_MEMORY "Off" CACHE STRING "Sanitizers") + +@@ -137,7 +137,7 @@ message(STATUS "Build pdlp with GPU: ${CUPDLP_GPU}") + option(CUPDLP_FIND_CUDA "Build pdlp with GPU" OFF) + message(STATUS "Use FindCUDAConf: ${CUPDLP_FIND_CUDA}") + +-if(CUPDLP_GPU AND CMAKE_VERSION VERSION_LESS "3.25.0") ++if(CUPDLP_GPU AND CMAKE_VERSION VERSION_LESS "3.25.0") + message("CUPDLP FindCUDAConf requires CMake version minumum 3.24. Please use a higher version of CMake.") + endif() + +@@ -158,11 +158,11 @@ if (CUPDLP_GPU) + # With FindCUDAConf.cmake + # Need to have the CUDA_HOME environment variable set. + include(FindCUDAConf) +- else() ++ else() + # Without FindCUDAConf.cmake + enable_language(CUDA) + find_package(CUDAToolkit REQUIRED) +- ++ + set(CUDA_LIBRARY-NOTFOUND, OFF) + set(CUDA_LIBRARY CUDA::cudart CUDA::cublas CUDA::cusparse) + endif() +@@ -205,7 +205,7 @@ if (BUILD_CXX) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}) +- # for multi-config build system (e.g. xcode) ++ # for multi-config build system (e.g. xcode) + foreach(OutputConfig IN LISTS CMAKE_CONFIGURATION_TYPES) + string(TOUPPER ${OutputConfig} OUTPUTCONFIG) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY_${OUTPUTCONFIG} ${CMAKE_BINARY_DIR}/${OutputConfig}/${CMAKE_INSTALL_LIBDIR}) +@@ -244,14 +244,14 @@ if (BUILD_CXX) + option(STDCALL "Build highs with the __stdcall convention" OFF) + endif() + +- if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR +- CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR +- CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") ++ if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR ++ CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR ++ CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11") +- # elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") ++ # elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + # not recognised by cl +- # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /std:c++11") +- endif() ++ # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /std:c++11") ++ endif() + + # Basic type + include(CMakePushCheckState) +@@ -275,7 +275,7 @@ if (BUILD_CXX) + check_type_size("int *" SIZEOF_INT_P LANGUAGE CXX) + message(STATUS "Found int * size: ${SIZEOF_INT_P}") + cmake_pop_check_state() +- ++ + # Use current CMAKE_C_FLAGS and CMAKE_CXX_FLAGS when checking for IPO support, + # instead of defaults: https://cmake.org/cmake/help/latest/policy/CMP0138.html + if(MSVC AND BUILD_SHARED_LIBS) +@@ -293,7 +293,7 @@ if (BUILD_CXX) + set(ipo_supported NO) + message(STATUS "IPO / LTO not currently supported building HiGHS on MinGW") + else() +- if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") ++ if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") + cmake_policy(SET CMP0138 NEW) + endif() + +@@ -371,19 +371,8 @@ else() + HIGHS_HAVE_BUILTIN_CLZ) + endif() + +-set(CMAKE_MACOSX_RPATH ON) +- +-if (BUILD_DOTNET) +- set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE) +-else() +- # use, i.e. don't skip the full RPATH for the build tree +- set(CMAKE_SKIP_BUILD_RPATH FALSE) +- +- # when building, don't use the install RPATH already +- # (but later on when installing) +- set(CMAKE_BUILD_WITH_INSTALL_RPATH FALSE) +- set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +-endif() ++# set the correct rpath for OS X ++set(CMAKE_MACOSX_RPATH TRUE) + + if(NOT FAST_BUILD) + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${HIGHS_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}) +@@ -428,7 +417,7 @@ endif() + + # For debug of cuda locally + +-# does not work with older CMake ++# does not work with older CMake + # add_compile_options("$<$,$>:-G>") + + # add_compile_options("$<$:-G>") +@@ -453,7 +442,7 @@ if(MSVC) + add_compile_options("$<$:-D_CRT_SECURE_NO_WARNINGS>") + add_compile_options("$<$:/MP>") + +- # Try to split large pdb files into objects. ++ # Try to split large pdb files into objects. + # https://github.com/tensorflow/tensorflow/issues/31610 + # add_compile_options("/Z7") + # add_link_options("/DEBUG:FASTLINK") +@@ -611,11 +600,11 @@ if(FAST_BUILD AND HIGHS_COVERAGE) + message(STATUS "Building in coverage mode") + + # Enable coverage flags +- add_compile_options(-O0) +- add_compile_options(--coverage) +- add_compile_options(-fprofile-update=atomic) ++ add_compile_options(-O0) ++ add_compile_options(--coverage) ++ add_compile_options(-fprofile-update=atomic) + +- add_link_options(-O0) ++ add_link_options(-O0) + add_link_options(--coverage) # Ensure coverage data is linked correctly + + find_program(GCOV_PATH gcov) +diff --git a/highs/CMakeLists.txt b/highs/CMakeLists.txt +index 50301433..f7b982fb 100644 +--- a/highs/CMakeLists.txt ++++ b/highs/CMakeLists.txt +@@ -1,7 +1,7 @@ + if (NOT BUILD_CXX) + return() + endif() +- ++ + # Define library. + include(sources) + set(sources ${highs_sources} ${cupdlp_sources} ${ipx_sources} ${basiclu_sources}) +@@ -43,7 +43,7 @@ if(NOT FAST_BUILD) + set_target_properties(libhighs PROPERTIES + OUTPUT_NAME "highs" + PDB_NAME "libhighs" +- MACOSX_RPATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") ++ ) + + if(ZLIB AND ZLIB_FOUND) + target_link_libraries(libhighs ZLIB::ZLIB) +@@ -51,8 +51,11 @@ if(NOT FAST_BUILD) + endif() + + # set the install rpath to the installed destination +- set_target_properties(libhighs PROPERTIES INSTALL_RPATH +- "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") ++ if(APPLE) ++ set_target_properties(libhighs PROPERTIES INSTALL_RPATH "@loader_path") ++ elseif (UNIX) ++ set_target_properties(libhighs PROPERTIES INSTALL_RPATH "$ORIGIN") ++ endif() + + # install the header files of highs + foreach(file ${headers}) +@@ -84,7 +87,7 @@ if(NOT FAST_BUILD) + # target_compile_options(libipx PRIVATE "-Wno-sign-compare") + # target_compile_options(libipx PRIVATE "-Wno-logical-op-parentheses") + endif() +- ++ + install(TARGETS libhighs EXPORT highs-targets + LIBRARY + ARCHIVE +@@ -150,8 +153,8 @@ else() + + + target_sources(highs PRIVATE ${sources} ${headers} ${win_version_file}) +- +- # Optional Cuda ++ ++ # Optional Cuda + if (CUPDLP_GPU) + + target_include_directories(highs PUBLIC "$") +@@ -164,7 +167,7 @@ else() + else() + target_link_libraries(highs cudalin ${CUDA_LIBRARY} m) + endif() +- ++ + set_target_properties(highs PROPERTIES CUDA_SEPARABLE_COMPILATION ON) + + endif() +@@ -221,7 +224,7 @@ else() + ) + target_link_libraries(highs ZLIB::ZLIB) + endif() +- ++ + # install the header files of highs + foreach(file ${headers}) + get_filename_component(dir ${file} DIRECTORY) +@@ -236,9 +239,9 @@ else() + + # target_compile_options(highs PRIVATE "-Wall") + # target_compile_options(highs PRIVATE "-Wunused") +- ++ + if (UNIX) +- if ( CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") ++ if ( CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(highs PRIVATE "-Wall") + target_compile_options(highs PRIVATE "-Wreturn-type") + target_compile_options(highs PRIVATE "-Wmissing-declarations") +@@ -248,7 +251,7 @@ else() + target_compile_options(highs PRIVATE "-Wno-comment") + target_compile_options(highs PRIVATE "-Wno-unused-label") + +- if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") ++ if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + target_compile_options(highs PRIVATE "-Wno-unused-lambda-capture") + endif() + +@@ -267,7 +270,7 @@ else() + endif() + + if (BUILD_DOTNET) +- ++ + # see: https://docs.microsoft.com/en-us/dotnet/core/rid-catalog + if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)") + set(DOTNET_PLATFORM arm64) +@@ -298,8 +301,8 @@ else() + set(TARGET_FILE_NAME "highs.dll") + endif() + +- add_custom_command(TARGET highs POST_BUILD +- COMMAND "${CMAKE_COMMAND}" -E copy ++ add_custom_command(TARGET highs POST_BUILD ++ COMMAND "${CMAKE_COMMAND}" -E copy + "$" + ${DOTNET_PROJECT_DIR}/runtimes/${DOTNET_RID}/native/${TARGET_FILE_NAME} + COMMENT "Copying to output directory") +@@ -318,7 +321,7 @@ if(FORTRAN_FOUND) + target_link_libraries(FortranHighs PUBLIC highs) + endif() + +- install(TARGETS FortranHighs ++ install(TARGETS FortranHighs + LIBRARY + ARCHIVE + RUNTIME diff --git a/patches/protobuf-v31.0.patch b/patches/protobuf-v31.1.patch similarity index 100% rename from patches/protobuf-v31.0.patch rename to patches/protobuf-v31.1.patch diff --git a/patches/scip-v922.patch b/patches/scip-v922.patch index 7a92254118b..b1093c9e341 100644 --- a/patches/scip-v922.patch +++ b/patches/scip-v922.patch @@ -1,5 +1,5 @@ diff --git a/CMakeLists.txt b/CMakeLists.txt -index 8492dc75..4c12a9bf 100644 +index 38ac7845..9b0d4fcb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,9 +38,11 @@ set(CPACK_PACKAGE_VENDOR "Zuse Institute Berlin") @@ -17,6 +17,15 @@ index 8492dc75..4c12a9bf 100644 if(SCIPOptSuite_BINARY_DIR) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${SCIPOptSuite_BINARY_DIR}/bin) +@@ -239,7 +241,7 @@ if(DEBUGSOL) + endif() + + #set the correct rpath for OS X +-set(CMAKE_MACOSX_RPATH ON) ++set(CMAKE_MACOSX_RPATH TRUE) + + #set defines for Windows + if(WIN32) @@ -412,22 +414,11 @@ endif() #search the selected LP solver library message(STATUS "Finding Solver \"${LPS}\"") @@ -96,10 +105,35 @@ index 559552f9..682ac40a 100644 set(SCIP_INCLUDE_DIRS "@CONF_INCLUDE_DIRS@") set(SCIP_FOUND TRUE) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt -index e6fda2d5..2d04b845 100644 +index d6dd3acf..a146ddec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt -@@ -1149,17 +1149,8 @@ install(TARGETS scip libscip EXPORT scip-targets +@@ -5,8 +5,8 @@ include(GNUInstallDirs) + + function(setLibProperties targetname outputname) + set_target_properties(${targetname} PROPERTIES +- OUTPUT_NAME ${outputname} +- MACOSX_RPATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") ++ OUTPUT_NAME ${outputname} ++ ) + endfunction(setLibProperties) + + set(CMAKE_C_STANDARD 99) +@@ -1112,6 +1112,13 @@ target_link_libraries(scip + add_dependencies(libscip scip_update_githash) + add_dependencies(scip scip_update_githash) + ++if(APPLE) ++ set_target_properties(libscip PROPERTIES ++ INSTALL_RPATH "@loader_path") ++elseif(UNIX) ++ set_target_properties(libscip PROPERTIES ++ INSTALL_RPATH "$ORIGIN") ++endif() + set_target_properties(libscip PROPERTIES + VERSION ${SCIP_VERSION_MAJOR}.${SCIP_VERSION_MINOR}.${SCIP_VERSION_PATCH}.${SCIP_VERSION_SUB} + SOVERSION ${SCIP_VERSION_MAJOR}.${SCIP_VERSION_MINOR} +@@ -1150,17 +1157,8 @@ install(TARGETS scip libscip EXPORT scip-targets INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) # Add all targets to the build-tree export set @@ -119,7 +153,7 @@ index e6fda2d5..2d04b845 100644 # configure the config file for the build tree set(CONF_INCLUDE_DIRS "${PROJECT_SOURCE_DIR}/src" "${PROJECT_BINARY_DIR}") -@@ -1175,18 +1166,16 @@ ${PROJECT_BINARY_DIR}/scip-config-version.cmake +@@ -1176,18 +1174,16 @@ ${PROJECT_BINARY_DIR}/scip-config-version.cmake #configure the config file for the install set(CONF_INCLUDE_DIRS "\${CMAKE_CURRENT_LIST_DIR}/../../../include") diff --git a/patches/soplex-v7.1.3.patch b/patches/soplex-v7.1.3.patch index 06b629ec98f..2df6a368417 100644 --- a/patches/soplex-v7.1.3.patch +++ b/patches/soplex-v7.1.3.patch @@ -1,5 +1,5 @@ diff --git a/CMakeLists.txt b/CMakeLists.txt -index 0b21f5a..ddf1536 100644 +index 0b21f5a..6f08341 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,10 @@ set(CPACK_PACKAGE_VERSION_PATCH "${SOPLEX_VERSION_PATCH}") @@ -34,7 +34,12 @@ index 0b21f5a..ddf1536 100644 # for colorized output if(NOT WIN32) -@@ -69,6 +79,8 @@ set(CMAKE_MACOSX_RPATH ON) +@@ -65,10 +75,12 @@ if(NOT CMAKE_BUILD_TYPE) + endif() + + # set the correct rpath for OS X +-set(CMAKE_MACOSX_RPATH ON) ++set(CMAKE_MACOSX_RPATH TRUE) # use C++14 standard set(CMAKE_CXX_STANDARD 14) @@ -131,9 +136,20 @@ index 0b21f5a..ddf1536 100644 + add_subdirectory(check) +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt -index 84ec5a5..6f5d4ef 100644 +index 84ec5a5..4552300 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt +@@ -3,8 +3,8 @@ + # + function(setLibProperties targetname outputname) + set_target_properties(${targetname} PROPERTIES +- OUTPUT_NAME ${outputname} +- MACOSX_RPATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") ++ OUTPUT_NAME ${outputname} ++ ) + endfunction(setLibProperties) + + include(GNUInstallDirs) @@ -193,24 +193,28 @@ target_link_libraries(libsoplexshared libsoplex ${libs}) set_target_properties(libsoplexshared PROPERTIES CXX_VISIBILITY_PRESET default) @@ -143,11 +159,11 @@ index 84ec5a5..6f5d4ef 100644 +if(SOPLEX_SOPLEX) + add_executable(soplex EXCLUDE_FROM_ALL soplexmain.cpp) + target_link_libraries(soplex PRIVATE libsoplex ${Boost_LIBRARIES}) - --if(EMSCRIPTEN AND EMSCRIPTEN_HTML) ++ + # set the install rpath to the installed destination + set_target_properties(soplex PROPERTIES INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_LIBDIR}") -+ + +-if(EMSCRIPTEN AND EMSCRIPTEN_HTML) + if(EMSCRIPTEN AND EMSCRIPTEN_HTML) set_target_properties(soplex PROPERTIES LINK_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/soplex_webdemo_shell.html) set(CMAKE_EXECUTABLE_SUFFIX ".html") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000000..fdc8c729091 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.pyink] +line-length = 80 +unstable = true +target-version = [] +pyink-indentation = 2 +pyink-use-majority-quotes = true +pyink-annotation-pragmas = [ + "noqa", + "pylint:", + "type: ignore", + "pytype:", + "mypy:", + "pyright:", + "pyre-", +] diff --git a/tools/check_python_deps.py b/tools/check_python_deps.py index deeb558abd1..533330db440 100644 --- a/tools/check_python_deps.py +++ b/tools/check_python_deps.py @@ -20,24 +20,20 @@ # try to import setuptools try: - from setuptools import setup # pylint: disable=g-import-not-at-top,unused-import - from setuptools import ( - Extension, - ) # pylint: disable=g-import-not-at-top,unused-import - from setuptools.command import ( - easy_install, - ) # pylint: disable=g-import-not-at-top,unused-import + from setuptools import setup # pylint: disable=g-import-not-at-top,unused-import + from setuptools import ( + Extension, + ) # pylint: disable=g-import-not-at-top,unused-import + from setuptools.command import ( + easy_install, + ) # pylint: disable=g-import-not-at-top,unused-import except ImportError: - raise ImportError( - """setuptools is not installed for \"""" - + sys.executable - + """\" + raise ImportError( + """setuptools is not installed for \"""" + sys.executable + """\" Follow this link for installing instructions : https://pypi.python.org/pypi/setuptools -make sure you use \"""" - + sys.executable - + """\" during the installation""" - ) +make sure you use \"""" + sys.executable + """\" during the installation""" + ) from pkg_resources import ( parse_version, @@ -48,161 +44,157 @@ def notinstalled(modulename): - return ( - modulename - + """ is not installed for \"""" - + sys.executable - + """\" -Run \"""" - + sys.executable - + """ setup.py install --user\" to install it""" - ) + return modulename + """ is not installed for \"""" + sys.executable + """\" +Run \"""" + sys.executable + """ setup.py install --user\" to install it""" def absent_version(module, modulename): - return ( - """You are using a """ - + modulename - + """ module that doesn't have a __version__ attribute : """ - + inspect.getfile(module) - + """\" + return ( + """You are using a """ + + modulename + + """ module that doesn't have a __version__ attribute : """ + + inspect.getfile(module) + + """\" Run \"""" - + sys.executable - + """ setup.py install --user\" to upgrade. + + sys.executable + + """ setup.py install --user\" to upgrade. If the problem persists, remove the site-package that contains \"""" - + inspect.getfile(module) - + """\". You can do so either manually or by using pip.""" - ) + + inspect.getfile(module) + + """\". You can do so either manually or by using pip.""" + ) def wrong_version(module, modulename, required_version, installed_version): - return ( - """You are using """ - + modulename - + """-""" - + installed_version - + """ : """ - + inspect.getfile(module) - + """, while the required version is : """ - + required_version - + """ + return ( + """You are using """ + + modulename + + """-""" + + installed_version + + """ : """ + + inspect.getfile(module) + + """, while the required version is : """ + + required_version + + """ Run \"""" - + sys.executable - + """ setup.py install --user\" to upgrade. + + sys.executable + + """ setup.py install --user\" to upgrade. If the problem persists, remove the site-package that contains \"""" - + inspect.getfile(module) - + """\". You can do so either manually or by using pip.""" - ) + + inspect.getfile(module) + + """\". You can do so either manually or by using pip.""" + ) def log_error_and_exit(error_message): - logging.error(error_message) - raise SystemExit + logging.error(error_message) + raise SystemExit def check_absent_version(module, modulename): - if not hasattr(module, "__version__"): - log_error_and_exit(absent_version(module, modulename)) + if not hasattr(module, "__version__"): + log_error_and_exit(absent_version(module, modulename)) if __name__ == "__main__": - parser = optparse.OptionParser("Log level") - parser.add_option( - "-l", - "--log", - type="string", - help="Available levels are CRITICAL (3), ERROR (2), WARNING (1), INFO (0), DEBUG (-1)", - default="INFO", - ) - options, args = parser.parse_args() - - # Create the logger - try: - loglevel = getattr(logging, options.log.upper()) - except AttributeError: - loglevel = { - 3: logging.CRITICAL, - 2: logging.ERROR, - 1: logging.WARNING, - 0: logging.INFO, - -1: logging.DEBUG, - }[int(options.log)] - - logging.basicConfig( - format="[%(levelname)s] %(message)s", stream=sys.stdout, level=loglevel + parser = optparse.OptionParser("Log level") + parser.add_option( + "-l", + "--log", + type="string", + help=( + "Available levels are CRITICAL (3), ERROR (2), WARNING (1), INFO (0)," + " DEBUG (-1)" + ), + default="INFO", + ) + options, args = parser.parse_args() + + # Create the logger + try: + loglevel = getattr(logging, options.log.upper()) + except AttributeError: + loglevel = { + 3: logging.CRITICAL, + 2: logging.ERROR, + 1: logging.WARNING, + 0: logging.INFO, + -1: logging.DEBUG, + }[int(options.log)] + + logging.basicConfig( + format="[%(levelname)s] %(message)s", stream=sys.stdout, level=loglevel + ) + + # Display Python Version and path + logging.info("Python path : %s", sys.executable) + logging.info("Python version : %s", sys.version) + + # Choose the pypi package + ortools_name = "ortools" + + # try to import ortools + try: + import ortools # pylint: disable=g-import-not-at-top + except ImportError: + log_error_and_exit(notinstalled(ortools_name)) + + # try to import protobuf + try: + import google.protobuf # pylint: disable=g-import-not-at-top + except ImportError: + log_error_and_exit(notinstalled("protobuf")) + + # check ortools version + try: + check_absent_version(ortools, "ortools") + if required_ortools_version != ortools.__version__: + raise Exception + logging.info( + "or-tools version : " + + ortools.__version__ + + "\n" + + inspect.getfile(ortools) ) - - # Display Python Version and path - logging.info("Python path : %s", sys.executable) - logging.info("Python version : %s", sys.version) - - # Choose the pypi package - ortools_name = "ortools" - - # try to import ortools - try: - import ortools # pylint: disable=g-import-not-at-top - except ImportError: - log_error_and_exit(notinstalled(ortools_name)) - - # try to import protobuf - try: - import google.protobuf # pylint: disable=g-import-not-at-top - except ImportError: - log_error_and_exit(notinstalled("protobuf")) - - # check ortools version - try: - check_absent_version(ortools, "ortools") - if required_ortools_version != ortools.__version__: - raise Exception - logging.info( - "or-tools version : " - + ortools.__version__ - + "\n" - + inspect.getfile(ortools) - ) - except (AttributeError, Exception): # pylint: disable=broad-except - log_error_and_exit( - wrong_version( - ortools, ortools_name, required_ortools_version, ortools.__version__ - ) + except (AttributeError, Exception): # pylint: disable=broad-except + log_error_and_exit( + wrong_version( + ortools, ortools_name, required_ortools_version, ortools.__version__ ) + ) - # check protobuf version - try: - check_absent_version(google.protobuf, "protobuf") - if required_protobuf_version != google.protobuf.__version__: - raise Exception - logging.info( - "protobuf version : " - + google.protobuf.__version__ - + "\n" - + inspect.getfile(google.protobuf) - ) - except (AttributeError, Exception): # pylint: disable=broad-except - log_error_and_exit( - wrong_version( - google.protobuf, - "protobuf", - required_protobuf_version, - google.protobuf.__version__, - ) + # check protobuf version + try: + check_absent_version(google.protobuf, "protobuf") + if required_protobuf_version != google.protobuf.__version__: + raise Exception + logging.info( + "protobuf version : " + + google.protobuf.__version__ + + "\n" + + inspect.getfile(google.protobuf) + ) + except (AttributeError, Exception): # pylint: disable=broad-except + log_error_and_exit( + wrong_version( + google.protobuf, + "protobuf", + required_protobuf_version, + google.protobuf.__version__, ) + ) - # Check if python can load the libraries' modules - # this is useful when the library architecture is not compatible with the - # python executable, or when the library's dependencies are not available or - # not compatible. - from ortools.constraint_solver import ( - _pywrapcp, - ) # pylint: disable=g-import-not-at-top,unused-import - from ortools.linear_solver import ( - _pywraplp, - ) # pylint: disable=g-import-not-at-top,unused-import - from ortools.algorithms.python import ( - knapsack_solver, - ) # pylint: disable=g-import-not-at-top,unused-import - from ortools.graph import ( - _pywrapgraph, - ) # pylint: disable=g-import-not-at-top,unused-import + # Check if python can load the libraries' modules + # this is useful when the library architecture is not compatible with the + # python executable, or when the library's dependencies are not available or + # not compatible. + from ortools.constraint_solver import ( + _pywrapcp, + ) # pylint: disable=g-import-not-at-top,unused-import + from ortools.linear_solver import ( + _pywraplp, + ) # pylint: disable=g-import-not-at-top,unused-import + from ortools.algorithms.python import ( + knapsack_solver, + ) # pylint: disable=g-import-not-at-top,unused-import + from ortools.graph import ( + _pywrapgraph, + ) # pylint: disable=g-import-not-at-top,unused-import diff --git a/tools/doc/doxygen_filter.py b/tools/doc/doxygen_filter.py index f9271df7a7b..26b2279e7aa 100644 --- a/tools/doc/doxygen_filter.py +++ b/tools/doc/doxygen_filter.py @@ -69,41 +69,37 @@ def CompileExpressions(self): self.substitutions = [ # Remove copyright lines. (re.compile(r'^\s*//\s*[Cc]opyright.*Google.*'), r'', self.ANYWHERE), - # Remove any comment lines that consist of only punctuation (banners). # We only allow a maximum of two spaces before the punctuation so we # don't accidentally get rid of code examples with bare braces and # whatnot. (re.compile(r'(^\s*)//\s{0,2}[-=#/]+$'), r'\1//\n', self.ANYWHERE), - # If we find something that looks like a list item that is indented four # or more spaces, pull it back to the left so doxygen's Markdown engine # doesn't treat it like a code block. (re.compile(r'(^\s*)//\s{4,}([-\d*].*)'), r'\1 \2', self.COMMENT), - # Replace TODO(user) in a comment with @todo (someone) (re.compile(r'TODO'), r'@todo ', self.COMMENT), - # Replace leading 'Note:' or 'Note that' in a comment with @note - (re.compile(r'(\/\/\s+)Note(?:\:| that)', re.I), r'\1@note', - self.COMMENT), - + ( + re.compile(r'(\/\/\s+)Note(?:\:| that)', re.I), + r'\1@note', + self.COMMENT, + ), # Replace leading 'Warning:' in a comment with @warning (re.compile(r'(\/\/\s+)Warning:', re.I), r'\1@warning', self.COMMENT), - # Replace leading 'Deprecated' in a comment with @deprecated - (re.compile(r'(\/\/\s+)Deprecated[^\w\s]*', re.I), r'\1@deprecated', - self.COMMENT), - + ( + re.compile(r'(\/\/\s+)Deprecated[^\w\s]*', re.I), + r'\1@deprecated', + self.COMMENT, + ), # Replace pipe-delimited parameter names with backtick-delimiters (re.compile(r'\|(\w+)\|'), r'`\1`', self.COMMENT), - # Convert standalone comment lines to Doxygen style. (re.compile(r'(^\s*)//(?=[^/])'), r'\1///', self.ANYWHERE), - # Strip trailing comments from preprocessor directives. (re.compile(r'(^#.*)//.*'), r'\1', self.ANYWHERE), - # Convert remaining trailing comments to doxygen style, unless they are # documenting the end of a block. (re.compile(r'([^} ]\s+)//(?=[^/])'), r'\1///<', self.ANYWHERE), @@ -118,7 +114,7 @@ def Transform(self, line): Returns: The resulting line. """ - for (regex, repl, where) in self.substitutions: + for regex, repl, where in self.substitutions: if where is self.COMMENT and not self.comment_regex.match(line): return line line = regex.sub(repl, line) diff --git a/tools/doc/gen_ref_doc.py b/tools/doc/gen_ref_doc.py index 303c43ab21b..57d592a74ac 100755 --- a/tools/doc/gen_ref_doc.py +++ b/tools/doc/gen_ref_doc.py @@ -56,8 +56,7 @@ def main(version): f = open(headerfile, 'r') g = open(header_tmp, 'w') filedata = f.read() - filedata = re.sub('Banner Text', 'Google OR-Tools ' + version, - filedata) + filedata = re.sub('Banner Text', 'Google OR-Tools ' + version, filedata) filedata = re.sub('Page Title', title, filedata) # Write filedata. g.write(filedata) @@ -95,161 +94,147 @@ def main(version): def create_section_data(): """Generate each section configuration.""" - sections = [{ - 'output_dir': - 'cpp_algorithms', - 'project name': - 'Algorithms', - 'title': - 'C++ Reference: Algorithms', - 'doxyfile': - 'cpp.doxy.in', - 'headerfile': - 'cpp.header.html.in', - 'footerfile': - 'all.footer.html.in', - 'styleSheetfile': - 'all.styleSheet.css.in', - 'input_files': - 'ortools/algorithms/dense_doubly_linked_list.h ' + - 'ortools/algorithms/dynamic_partition.h ' + - 'ortools/algorithms/dynamic_permutation.h ' + - 'ortools/algorithms/find_graph_symmetries.h ' + - 'ortools/algorithms/hungarian.h ' + - 'ortools/algorithms/knapsack_solver.h ' + - 'ortools/algorithms/sparse_permutation.h' - }, { - 'output_dir': - 'cpp_sat', - 'project name': - 'CP-SAT', - 'title': - 'C++ Reference: CP-SAT', - 'doxyfile': - 'cpp.doxy.in', - 'headerfile': - 'cpp.header.html.in', - 'footerfile': - 'all.footer.html.in', - 'styleSheetfile': - 'all.styleSheet.css.in', - 'input_files': - 'ortools/sat/cp_model.h ' + 'ortools/sat/cp_model_solver.h ' + - 'ortools/sat/model.h ' + 'ortools/util/sorted_interval_list.h ' + - 'ortools/util/time_limit.h ' + - 'ortools/gen/ortools/sat/boolean_problem.pb.h ' + - 'ortools/gen/ortools/sat/cp_model.pb.h ' + - 'ortools/gen/ortools/sat/sat_parameters.pb.h' - }, { - 'output_dir': - 'cpp_graph', - 'project name': - 'Graph', - 'title': - 'C++ Reference: Graph', - 'doxyfile': - 'cpp.doxy.in', - 'headerfile': - 'cpp.header.html.in', - 'footerfile': - 'all.footer.html.in', - 'styleSheetfile': - 'all.styleSheet.css.in', - 'input_files': - 'ortools/graph/christofides.h ' + 'ortools/graph/cliques.h ' + - 'ortools/graph/connected_components.h ' + - 'ortools/graph/connectivity.h ' + - 'ortools/graph/eulerian_path.h ' + 'ortools/graph/graph.h ' + - 'ortools/graph/graphs.h ' + 'ortools/graph/hamiltonian_path.h ' + - 'ortools/graph/graph_io.h ' + 'ortools/graph/iterators.h ' + - 'ortools/graph/linear_assignment.h ' + 'ortools/graph/max_flow.h ' + - 'ortools/graph/min_cost_flow.h ' + - 'ortools/graph/minimum_spanning_tree.h ' + - 'ortools/graph/one_tree_lower_bound.h ' + - 'ortools/graph/shortestpaths.h ' + - 'ortools/graph/strongly_connected_components.h ' + - 'ortools/graph/util.h ' + - 'ortools/gen/ortools/graph/flow_problem.pb.h ' - }, { - 'output_dir': - 'cpp_linear', - 'project name': - 'Linear solver', - 'title': - 'C++ Reference: Linear solver', - 'doxyfile': - 'cpp.doxy.in', - 'headerfile': - 'cpp.header.html.in', - 'footerfile': - 'all.footer.html.in', - 'styleSheetfile': - 'all.styleSheet.css.in', - 'input_files': - 'ortools/linear_solver/linear_expr.h ' + - 'ortools/linear_solver/linear_solver.h ' + - 'ortools/linear_solver/model_exporter.h ' + - 'ortools/linear_solver/model_exporter_swig_helper.h ' + - 'ortools/linear_solver/model_validator.h ' + - 'ortools/gen/ortools/linear_solver/linear_solver.pb.h ' - }, { - 'output_dir': - 'cpp_routing', - 'project name': - 'Routing', - 'title': - 'C++ Reference: Routing', - 'doxyfile': - 'cpp.doxy.in', - 'headerfile': - 'cpp.header.html.in', - 'footerfile': - 'all.footer.html.in', - 'styleSheetfile': - 'all.styleSheet.css.in', - 'input_files': - 'ortools/constraint_solver/constraint_solver.h ' + - 'ortools/constraint_solver/constraint_solveri.h ' + - 'ortools/constraint_solver/routing.h ' + - 'ortools/constraint_solver/routing_index_manager.h ' + - 'ortools/constraint_solver/routing_lp_scheduling.h ' + - 'ortools/constraint_solver/routing_neighborhoods.h ' + - 'ortools/constraint_solver/routing_parameters.h ' + - 'ortools/constraint_solver/routing_types.h ' + - 'ortools/gen/ortools/constraint_solver/assignment.pb.h ' + - 'ortools/gen/ortools/constraint_solver/demon_profiler.pb.h ' + - 'ortools/gen/ortools/constraint_solver/routing_enums.pb.h ' + - 'ortools/gen/ortools/constraint_solver/routing_parameters.pb.h ' + - 'ortools/gen/ortools/constraint_solver/search_limit.pb.h ' + - 'ortools/gen/ortools/constraint_solver/solver_parameters.pb.h ' - }, { - 'output_dir': 'cpp', - 'project name': 'OR-Tools', - 'title': 'C++ Reference', - 'doxyfile': 'cpp.doxy.in', - 'headerfile': 'default.header.html.in', - 'footerfile': 'default.footer.html.in', - 'styleSheetfile': 'default.styleSheet.css.in', - 'input_files': 'ortools ' + 'tools/doc' - }, { - 'output_dir': 'dotnet', - 'project name': 'OR-Tools', - 'title': '.Net Reference', - 'doxyfile': 'dotnet.doxy.in', - 'headerfile': 'dotnet.header.html.in', - 'footerfile': 'all.footer.html.in', - 'styleSheetfile': 'all.styleSheet.css.in', - 'input_files': 'ortools ' + 'tools/doc' - }, { - 'output_dir': 'java', - 'project name': 'OR-Tools', - 'title': 'Java Reference', - 'doxyfile': 'java.doxy.in', - 'headerfile': 'java.header.html.in', - 'footerfile': 'all.footer.html.in', - 'styleSheetfile': 'all.styleSheet.css.in', - 'input_files': 'ortools ' + 'tools/doc' - }] + sections = [ + { + 'output_dir': 'cpp_algorithms', + 'project name': 'Algorithms', + 'title': 'C++ Reference: Algorithms', + 'doxyfile': 'cpp.doxy.in', + 'headerfile': 'cpp.header.html.in', + 'footerfile': 'all.footer.html.in', + 'styleSheetfile': 'all.styleSheet.css.in', + 'input_files': ( + 'ortools/algorithms/dense_doubly_linked_list.h ' + + 'ortools/algorithms/dynamic_partition.h ' + + 'ortools/algorithms/dynamic_permutation.h ' + + 'ortools/algorithms/find_graph_symmetries.h ' + + 'ortools/algorithms/hungarian.h ' + + 'ortools/algorithms/knapsack_solver.h ' + + 'ortools/algorithms/sparse_permutation.h' + ), + }, + { + 'output_dir': 'cpp_sat', + 'project name': 'CP-SAT', + 'title': 'C++ Reference: CP-SAT', + 'doxyfile': 'cpp.doxy.in', + 'headerfile': 'cpp.header.html.in', + 'footerfile': 'all.footer.html.in', + 'styleSheetfile': 'all.styleSheet.css.in', + 'input_files': ( + 'ortools/sat/cp_model.h ' + + 'ortools/sat/cp_model_solver.h ' + + 'ortools/sat/model.h ' + + 'ortools/util/sorted_interval_list.h ' + + 'ortools/util/time_limit.h ' + + 'ortools/gen/ortools/sat/boolean_problem.pb.h ' + + 'ortools/gen/ortools/sat/cp_model.pb.h ' + + 'ortools/gen/ortools/sat/sat_parameters.pb.h' + ), + }, + { + 'output_dir': 'cpp_graph', + 'project name': 'Graph', + 'title': 'C++ Reference: Graph', + 'doxyfile': 'cpp.doxy.in', + 'headerfile': 'cpp.header.html.in', + 'footerfile': 'all.footer.html.in', + 'styleSheetfile': 'all.styleSheet.css.in', + 'input_files': ( + 'ortools/graph/christofides.h ' + + 'ortools/graph/cliques.h ' + + 'ortools/graph/connected_components.h ' + + 'ortools/graph/connectivity.h ' + + 'ortools/graph/eulerian_path.h ' + + 'ortools/graph/graph.h ' + + 'ortools/graph/graphs.h ' + + 'ortools/graph/hamiltonian_path.h ' + + 'ortools/graph/graph_io.h ' + + 'ortools/graph/iterators.h ' + + 'ortools/graph/linear_assignment.h ' + + 'ortools/graph/max_flow.h ' + + 'ortools/graph/min_cost_flow.h ' + + 'ortools/graph/minimum_spanning_tree.h ' + + 'ortools/graph/one_tree_lower_bound.h ' + + 'ortools/graph/shortestpaths.h ' + + 'ortools/graph/strongly_connected_components.h ' + + 'ortools/graph/util.h ' + + 'ortools/gen/ortools/graph/flow_problem.pb.h ' + ), + }, + { + 'output_dir': 'cpp_linear', + 'project name': 'Linear solver', + 'title': 'C++ Reference: Linear solver', + 'doxyfile': 'cpp.doxy.in', + 'headerfile': 'cpp.header.html.in', + 'footerfile': 'all.footer.html.in', + 'styleSheetfile': 'all.styleSheet.css.in', + 'input_files': ( + 'ortools/linear_solver/linear_expr.h ' + + 'ortools/linear_solver/linear_solver.h ' + + 'ortools/linear_solver/model_exporter.h ' + + 'ortools/linear_solver/model_exporter_swig_helper.h ' + + 'ortools/linear_solver/model_validator.h ' + + 'ortools/gen/ortools/linear_solver/linear_solver.pb.h ' + ), + }, + { + 'output_dir': 'cpp_routing', + 'project name': 'Routing', + 'title': 'C++ Reference: Routing', + 'doxyfile': 'cpp.doxy.in', + 'headerfile': 'cpp.header.html.in', + 'footerfile': 'all.footer.html.in', + 'styleSheetfile': 'all.styleSheet.css.in', + 'input_files': ( + 'ortools/constraint_solver/constraint_solver.h ' + + 'ortools/constraint_solver/constraint_solveri.h ' + + 'ortools/constraint_solver/routing.h ' + + 'ortools/constraint_solver/routing_index_manager.h ' + + 'ortools/constraint_solver/routing_lp_scheduling.h ' + + 'ortools/constraint_solver/routing_neighborhoods.h ' + + 'ortools/constraint_solver/routing_parameters.h ' + + 'ortools/constraint_solver/routing_types.h ' + + 'ortools/gen/ortools/constraint_solver/assignment.pb.h ' + + 'ortools/gen/ortools/constraint_solver/demon_profiler.pb.h ' + + 'ortools/gen/ortools/constraint_solver/routing_enums.pb.h ' + + 'ortools/gen/ortools/constraint_solver/routing_parameters.pb.h ' + + 'ortools/gen/ortools/constraint_solver/search_limit.pb.h ' + + 'ortools/gen/ortools/constraint_solver/solver_parameters.pb.h ' + ), + }, + { + 'output_dir': 'cpp', + 'project name': 'OR-Tools', + 'title': 'C++ Reference', + 'doxyfile': 'cpp.doxy.in', + 'headerfile': 'default.header.html.in', + 'footerfile': 'default.footer.html.in', + 'styleSheetfile': 'default.styleSheet.css.in', + 'input_files': 'ortools ' + 'tools/doc', + }, + { + 'output_dir': 'dotnet', + 'project name': 'OR-Tools', + 'title': '.Net Reference', + 'doxyfile': 'dotnet.doxy.in', + 'headerfile': 'dotnet.header.html.in', + 'footerfile': 'all.footer.html.in', + 'styleSheetfile': 'all.styleSheet.css.in', + 'input_files': 'ortools ' + 'tools/doc', + }, + { + 'output_dir': 'java', + 'project name': 'OR-Tools', + 'title': 'Java Reference', + 'doxyfile': 'java.doxy.in', + 'headerfile': 'java.header.html.in', + 'footerfile': 'all.footer.html.in', + 'styleSheetfile': 'all.styleSheet.css.in', + 'input_files': 'ortools ' + 'tools/doc', + }, + ] return sections diff --git a/tools/docker/images/almalinux-9.Dockerfile b/tools/docker/images/almalinux-9.Dockerfile index 4a6102159ce..cf1d51d85f6 100644 --- a/tools/docker/images/almalinux-9.Dockerfile +++ b/tools/docker/images/almalinux-9.Dockerfile @@ -62,12 +62,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/alpine-edge.Dockerfile b/tools/docker/images/alpine-edge.Dockerfile index 3e57be822e6..9bb26f931ba 100644 --- a/tools/docker/images/alpine-edge.Dockerfile +++ b/tools/docker/images/alpine-edge.Dockerfile @@ -40,12 +40,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/archlinux.Dockerfile b/tools/docker/images/archlinux.Dockerfile index 5ad355cfe56..92aec56c97a 100644 --- a/tools/docker/images/archlinux.Dockerfile +++ b/tools/docker/images/archlinux.Dockerfile @@ -41,12 +41,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/debian-11.Dockerfile b/tools/docker/images/debian-11.Dockerfile index 2c514de3f8d..1315560940c 100644 --- a/tools/docker/images/debian-11.Dockerfile +++ b/tools/docker/images/debian-11.Dockerfile @@ -55,12 +55,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/debian-12.Dockerfile b/tools/docker/images/debian-12.Dockerfile index 424fd312824..da883df0ff5 100644 --- a/tools/docker/images/debian-12.Dockerfile +++ b/tools/docker/images/debian-12.Dockerfile @@ -49,12 +49,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/debian-13.Dockerfile b/tools/docker/images/debian-13.Dockerfile index 5bb761afed5..7c1e9fc1a9c 100644 --- a/tools/docker/images/debian-13.Dockerfile +++ b/tools/docker/images/debian-13.Dockerfile @@ -50,12 +50,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/debian-sid.Dockerfile b/tools/docker/images/debian-sid.Dockerfile index 27fc2bb075c..613a715a3e9 100644 --- a/tools/docker/images/debian-sid.Dockerfile +++ b/tools/docker/images/debian-sid.Dockerfile @@ -52,12 +52,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/fedora-40.Dockerfile b/tools/docker/images/fedora-40.Dockerfile index 0fd6b0c905a..7c487641d78 100644 --- a/tools/docker/images/fedora-40.Dockerfile +++ b/tools/docker/images/fedora-40.Dockerfile @@ -48,12 +48,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/fedora-41.Dockerfile b/tools/docker/images/fedora-41.Dockerfile index 9db9337a666..cc95fe40182 100644 --- a/tools/docker/images/fedora-41.Dockerfile +++ b/tools/docker/images/fedora-41.Dockerfile @@ -50,12 +50,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/fedora-42.Dockerfile b/tools/docker/images/fedora-42.Dockerfile index 485022bca27..be16996329d 100644 --- a/tools/docker/images/fedora-42.Dockerfile +++ b/tools/docker/images/fedora-42.Dockerfile @@ -50,12 +50,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/opensuse-leap.Dockerfile b/tools/docker/images/opensuse-leap.Dockerfile index 300efe9555a..52168a1c6af 100644 --- a/tools/docker/images/opensuse-leap.Dockerfile +++ b/tools/docker/images/opensuse-leap.Dockerfile @@ -48,12 +48,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/rockylinux-9.Dockerfile b/tools/docker/images/rockylinux-9.Dockerfile index c885bb3d870..86be1c55d50 100644 --- a/tools/docker/images/rockylinux-9.Dockerfile +++ b/tools/docker/images/rockylinux-9.Dockerfile @@ -62,12 +62,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/ubuntu-20.04.Dockerfile b/tools/docker/images/ubuntu-20.04.Dockerfile index 01564cf3ff4..187e76077ed 100644 --- a/tools/docker/images/ubuntu-20.04.Dockerfile +++ b/tools/docker/images/ubuntu-20.04.Dockerfile @@ -66,12 +66,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/ubuntu-22.04.Dockerfile b/tools/docker/images/ubuntu-22.04.Dockerfile index 1b32512b268..cd063522a44 100644 --- a/tools/docker/images/ubuntu-22.04.Dockerfile +++ b/tools/docker/images/ubuntu-22.04.Dockerfile @@ -64,12 +64,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/ubuntu-24.04.Dockerfile b/tools/docker/images/ubuntu-24.04.Dockerfile index 9cc69b1380a..6cf39fa36e6 100644 --- a/tools/docker/images/ubuntu-24.04.Dockerfile +++ b/tools/docker/images/ubuntu-24.04.Dockerfile @@ -58,12 +58,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/images/ubuntu-24.10.Dockerfile b/tools/docker/images/ubuntu-24.10.Dockerfile index 290a38decb6..5e6b972a16a 100644 --- a/tools/docker/images/ubuntu-24.10.Dockerfile +++ b/tools/docker/images/ubuntu-24.10.Dockerfile @@ -58,12 +58,12 @@ COPY or-tools.snk /root/or-tools.snk ENV DOTNET_SNK=/root/or-tools.snk ARG SRC_GIT_BRANCH -ENV SRC_GIT_BRANCH ${SRC_GIT_BRANCH:-main} +ENV SRC_GIT_BRANCH=${SRC_GIT_BRANCH:-main} ARG SRC_GIT_SHA1 ENV SRC_GIT_SHA1 ${SRC_GIT_SHA1:-unknown} ARG OR_TOOLS_PATCH -ENV OR_TOOLS_PATCH ${OR_TOOLS_PATCH:-9999} +ENV OR_TOOLS_PATCH=${OR_TOOLS_PATCH:-9999} # Download sources # use SRC_GIT_SHA1 to modify the command diff --git a/tools/docker/python/amd64/manylinux.Dockerfile b/tools/docker/python/amd64/manylinux.Dockerfile index 1933738a409..415bcc52ce1 100644 --- a/tools/docker/python/amd64/manylinux.Dockerfile +++ b/tools/docker/python/amd64/manylinux.Dockerfile @@ -22,12 +22,12 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ## OR-TOOLS ## ################ FROM env AS devel -ENV GIT_URL https://github.com/google/or-tools +ENV GIT_URL=https://github.com/google/or-tools ARG GIT_BRANCH -ENV GIT_BRANCH ${GIT_BRANCH:-main} +ENV GIT_BRANCH=${GIT_BRANCH:-main} ARG GIT_SHA1 -ENV GIT_SHA1 ${GIT_SHA1:-unknown} +ENV GIT_SHA1=${GIT_SHA1:-unknown} # Download sources # use GIT_SHA1 to modify the command @@ -38,9 +38,9 @@ RUN git clone -b "${GIT_BRANCH}" --single-branch "$GIT_URL" /project \ WORKDIR /project # Copy build script and setup env -ENV PLATFORM x86_64 +ENV PLATFORM=x86_64 ARG PYTHON_VERSION -ENV PYTHON_VERSION ${PYTHON_VERSION:-3} +ENV PYTHON_VERSION=${PYTHON_VERSION:-3} COPY build-manylinux.sh . RUN chmod a+x "build-manylinux.sh" diff --git a/tools/docker/python/amd64/musllinux.Dockerfile b/tools/docker/python/amd64/musllinux.Dockerfile index 4c2f64fdbfb..4cc17069832 100644 --- a/tools/docker/python/amd64/musllinux.Dockerfile +++ b/tools/docker/python/amd64/musllinux.Dockerfile @@ -17,12 +17,12 @@ CMD ["/bin/sh"] ## OR-TOOLS ## ################ FROM env AS devel -ENV GIT_URL https://github.com/google/or-tools +ENV GIT_URL=https://github.com/google/or-tools ARG GIT_BRANCH -ENV GIT_BRANCH ${GIT_BRANCH:-main} +ENV GIT_BRANCH=${GIT_BRANCH:-main} ARG GIT_SHA1 -ENV GIT_SHA1 ${GIT_SHA1:-unknown} +ENV GIT_SHA1=${GIT_SHA1:-unknown} # Download sources # use GIT_SHA1 to modify the command @@ -33,9 +33,9 @@ RUN git clone -b "${GIT_BRANCH}" --single-branch "$GIT_URL" /project \ WORKDIR /project # Copy build script and setup env -ENV PLATFORM x86_64 +ENV PLATFORM=x86_64 ARG PYTHON_VERSION -ENV PYTHON_VERSION ${PYTHON_VERSION:-3} +ENV PYTHON_VERSION=${PYTHON_VERSION:-3} COPY build-musllinux.sh . RUN chmod a+x "build-musllinux.sh" diff --git a/tools/docker/python/arm64/manylinux.Dockerfile b/tools/docker/python/arm64/manylinux.Dockerfile index 116fe68de2b..8edd33f3734 100644 --- a/tools/docker/python/arm64/manylinux.Dockerfile +++ b/tools/docker/python/arm64/manylinux.Dockerfile @@ -24,12 +24,12 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone ## OR-TOOLS ## ################ FROM env AS devel -ENV GIT_URL https://github.com/google/or-tools +ENV GIT_URL=https://github.com/google/or-tools ARG GIT_BRANCH -ENV GIT_BRANCH ${GIT_BRANCH:-main} +ENV GIT_BRANCH=${GIT_BRANCH:-main} ARG GIT_SHA1 -ENV GIT_SHA1 ${GIT_SHA1:-unknown} +ENV GIT_SHA1=${GIT_SHA1:-unknown} # Download sources # use GIT_SHA1 to modify the command @@ -40,9 +40,9 @@ RUN git clone -b "${GIT_BRANCH}" --single-branch "$GIT_URL" /project \ WORKDIR /project # Copy build script and setup env -ENV PLATFORM aarch64 +ENV PLATFORM=aarch64 ARG PYTHON_VERSION -ENV PYTHON_VERSION ${PYTHON_VERSION:-3} +ENV PYTHON_VERSION=${PYTHON_VERSION:-3} COPY build-manylinux.sh . RUN chmod a+x "build-manylinux.sh" diff --git a/tools/docker/python/arm64/musllinux.Dockerfile b/tools/docker/python/arm64/musllinux.Dockerfile index 719dc6417ee..bbe93685f1b 100644 --- a/tools/docker/python/arm64/musllinux.Dockerfile +++ b/tools/docker/python/arm64/musllinux.Dockerfile @@ -17,12 +17,12 @@ CMD ["/bin/sh"] ## OR-TOOLS ## ################ FROM env AS devel -ENV GIT_URL https://github.com/google/or-tools +ENV GIT_URL=https://github.com/google/or-tools ARG GIT_BRANCH -ENV GIT_BRANCH ${GIT_BRANCH:-main} +ENV GIT_BRANCH=${GIT_BRANCH:-main} ARG GIT_SHA1 -ENV GIT_SHA1 ${GIT_SHA1:-unknown} +ENV GIT_SHA1=${GIT_SHA1:-unknown} # Download sources # use GIT_SHA1 to modify the command @@ -33,9 +33,9 @@ RUN git clone -b "${GIT_BRANCH}" --single-branch "${GIT_URL}" /project \ WORKDIR /project # Copy build script and setup env -ENV PLATFORM aarch64 +ENV PLATFORM=aarch64 ARG PYTHON_VERSION -ENV PYTHON_VERSION ${PYTHON_VERSION:-3} +ENV PYTHON_VERSION=${PYTHON_VERSION:-3} COPY build-musllinux.sh . RUN chmod a+x "build-musllinux.sh" diff --git a/tools/docker/python/build-manylinux.sh b/tools/docker/python/build-manylinux.sh index 25b96541892..3bbf69e7c71 100755 --- a/tools/docker/python/build-manylinux.sh +++ b/tools/docker/python/build-manylinux.sh @@ -196,9 +196,9 @@ function test_wheel() { "ortools/linear_solver/samples/simple_lp_program.py" "ortools/linear_solver/samples/simple_mip_program.py" "ortools/sat/samples/simple_sat_program.py" - "ortools/constraint_solver/samples/tsp.py" - "ortools/constraint_solver/samples/vrp.py" - "ortools/constraint_solver/samples/cvrptw_break.py" + "ortools/routing/samples/tsp.py" + "ortools/routing/samples/vrp.py" + "ortools/routing/samples/cvrptw_break.py" ) # Run all the specified test scripts using the current environment. diff --git a/tools/docker/python/build-musllinux.sh b/tools/docker/python/build-musllinux.sh index a3c3fb9e71e..846e636c3ff 100755 --- a/tools/docker/python/build-musllinux.sh +++ b/tools/docker/python/build-musllinux.sh @@ -187,9 +187,9 @@ function test_wheel() { "ortools/linear_solver/samples/simple_lp_program.py" "ortools/linear_solver/samples/simple_mip_program.py" "ortools/sat/samples/simple_sat_program.py" - "ortools/constraint_solver/samples/tsp.py" - "ortools/constraint_solver/samples/vrp.py" - "ortools/constraint_solver/samples/cvrptw_break.py" + "ortools/routing/samples/tsp.py" + "ortools/routing/samples/vrp.py" + "ortools/routing/samples/cvrptw_break.py" ) # Run all the specified test scripts using the current environment. diff --git a/tools/export_to_ipynb.py b/tools/export_to_ipynb.py index e46659225f9..68ce446b680 100755 --- a/tools/export_to_ipynb.py +++ b/tools/export_to_ipynb.py @@ -24,15 +24,19 @@ input_file = sys.argv[1] print(f"reading {input_file}") with open(input_file, encoding="utf-8") as fpin: - text = fpin.read() + text = fpin.read() # Compute output file path. output_file = input_file output_file = output_file.replace(".py", ".ipynb") # For example/python/foo.py -> example/notebook/examples/foo.ipynb -output_file = output_file.replace("examples/python", "examples/notebook/examples") +output_file = output_file.replace( + "examples/python", "examples/notebook/examples" +) # For example/contrib/foo.py -> example/notebook/contrib/foo.ipynb -output_file = output_file.replace("examples/contrib", "examples/notebook/contrib") +output_file = output_file.replace( + "examples/contrib", "examples/notebook/contrib" +) # For ortools/*/samples/foo.py -> example/notebook/*/foo.ipynb output_file = output_file.replace("ortools", "examples/notebook") output_file = output_file.replace("samples/", "") @@ -67,17 +71,14 @@ nbook["cells"].append(v4.new_markdown_cell(source=basename, id="basename")) print("Adding link cell...") -GITHUB_LOGO = ( - "https://raw.githubusercontent.com/google/or-tools/main/tools/github_32px.png" -) +GITHUB_LOGO = "https://raw.githubusercontent.com/google/or-tools/main/tools/github_32px.png" GITHUB_PATH = "https://github.com/google/or-tools/blob/main/" + input_file COLAB_PATH = ( - "https://colab.research.google.com/github/google/or-tools/blob/main/" + output_file -) -COLAB_LOGO = ( - "https://raw.githubusercontent.com/google/or-tools/main/tools/colab_32px.png" + "https://colab.research.google.com/github/google/or-tools/blob/main/" + + output_file ) +COLAB_LOGO = "https://raw.githubusercontent.com/google/or-tools/main/tools/colab_32px.png" link = f"""
Run in Google Colab @@ -109,87 +110,92 @@ for idx, (c_block, s, e) in enumerate( zip(all_blocks, line_start, line_start[1:] + [len(lines)]) ): - print(f"block[{idx}]: {c_block}") - c_text = "\n".join(lines[s:e]) - # Clean boilerplate header and description - if ( - idx == 0 - and isinstance(c_block, ast.Expr) - and isinstance(c_block.value, ast.Constant) - ): - print("Adding description cell...") - filtered_lines = lines[s:e] - # filtered_lines = list( - # filter(lambda l: not l.startswith('#!'), lines[s:e])) - filtered_lines = list( - filter(lambda l: not re.search(r"^#!", l), filtered_lines) - ) - filtered_lines = list( - filter(lambda l: not re.search(r"# \[START .*\]$", l), filtered_lines) - ) - filtered_lines = list( - filter(lambda l: not re.search(r"# \[END .*\]$", l), filtered_lines) - ) - # TODO(user): Remove only copyright not all line with '^#' - filtered_lines = list(filter(lambda l: not l.startswith(r"#"), filtered_lines)) - filtered_lines = [s.replace(r'"""', "") for s in filtered_lines] - filtered_text = "\n".join(filtered_lines) - nbook["cells"].append( - v4.new_markdown_cell(source=filtered_text, id="description") - ) - # Remove absl app import - elif ( - isinstance(c_block, ast.ImportFrom) - and c_block.module == "absl" - and c_block.names[0].name == "app" - ): - print(f"Removing import {c_block.module}.{c_block.names[0].name}...") - # rewrite absl flag import - elif ( - isinstance(c_block, ast.ImportFrom) - and c_block.module == "absl" - and c_block.names[0].name == "flags" - ): - print(f"Rewrite import {c_block.module}.{c_block.names[0].name}...") - FULL_TEXT += "from ortools.sat.colab import flags\n" - # Unwrap __main__ function - elif isinstance(c_block, ast.If) and c_block.test.comparators[0].s == "__main__": - print("Unwrapping main function...") - c_lines = lines[s + 1 : e] - # remove start and de-indent lines - spaces_to_delete = c_block.body[0].col_offset - fixed_lines = [ - ( - n_line[spaces_to_delete:] - if n_line.startswith(" " * spaces_to_delete) - else n_line - ) - for n_line in c_lines - ] - filtered_lines = fixed_lines - filtered_lines = list( - filter(lambda l: not re.search(r"# \[START .*\]$", l), filtered_lines) - ) - filtered_lines = list( - filter(lambda l: not re.search(r"# \[END .*\]$", l), filtered_lines) - ) - filtered_lines = [ - re.sub(r"app.run\((.*)\)$", r"\1()", s) for s in filtered_lines - ] - FULL_TEXT += "\n".join(filtered_lines) + "\n" - # Others - else: - print("Appending block...") - filtered_lines = lines[s:e] - for i, line in enumerate(filtered_lines): - filtered_lines[i] = line.replace("DEFINE_", "define_") - filtered_lines = list( - filter(lambda l: not re.search(r"# \[START .*\]$", l), filtered_lines) - ) - filtered_lines = list( - filter(lambda l: not re.search(r"# \[END .*\]$", l), filtered_lines) + print(f"block[{idx}]: {c_block}") + c_text = "\n".join(lines[s:e]) + # Clean boilerplate header and description + if ( + idx == 0 + and isinstance(c_block, ast.Expr) + and isinstance(c_block.value, ast.Constant) + ): + print("Adding description cell...") + filtered_lines = lines[s:e] + # filtered_lines = list( + # filter(lambda l: not l.startswith('#!'), lines[s:e])) + filtered_lines = list( + filter(lambda l: not re.search(r"^#!", l), filtered_lines) + ) + filtered_lines = list( + filter(lambda l: not re.search(r"# \[START .*\]$", l), filtered_lines) + ) + filtered_lines = list( + filter(lambda l: not re.search(r"# \[END .*\]$", l), filtered_lines) + ) + # TODO(user): Remove only copyright not all line with '^#' + filtered_lines = list( + filter(lambda l: not l.startswith(r"#"), filtered_lines) + ) + filtered_lines = [s.replace(r'"""', "") for s in filtered_lines] + filtered_text = "\n".join(filtered_lines) + nbook["cells"].append( + v4.new_markdown_cell(source=filtered_text, id="description") + ) + # Remove absl app import + elif ( + isinstance(c_block, ast.ImportFrom) + and c_block.module == "absl" + and c_block.names[0].name == "app" + ): + print(f"Removing import {c_block.module}.{c_block.names[0].name}...") + # rewrite absl flag import + elif ( + isinstance(c_block, ast.ImportFrom) + and c_block.module == "absl" + and c_block.names[0].name == "flags" + ): + print(f"Rewrite import {c_block.module}.{c_block.names[0].name}...") + FULL_TEXT += "from ortools.sat.colab import flags\n" + # Unwrap __main__ function + elif ( + isinstance(c_block, ast.If) + and c_block.test.comparators[0].s == "__main__" + ): + print("Unwrapping main function...") + c_lines = lines[s + 1 : e] + # remove start and de-indent lines + spaces_to_delete = c_block.body[0].col_offset + fixed_lines = [ + ( + n_line[spaces_to_delete:] + if n_line.startswith(" " * spaces_to_delete) + else n_line ) - FULL_TEXT += "\n".join(filtered_lines) + "\n" + for n_line in c_lines + ] + filtered_lines = fixed_lines + filtered_lines = list( + filter(lambda l: not re.search(r"# \[START .*\]$", l), filtered_lines) + ) + filtered_lines = list( + filter(lambda l: not re.search(r"# \[END .*\]$", l), filtered_lines) + ) + filtered_lines = [ + re.sub(r"app.run\((.*)\)$", r"\1()", s) for s in filtered_lines + ] + FULL_TEXT += "\n".join(filtered_lines) + "\n" + # Others + else: + print("Appending block...") + filtered_lines = lines[s:e] + for i, line in enumerate(filtered_lines): + filtered_lines[i] = line.replace("DEFINE_", "define_") + filtered_lines = list( + filter(lambda l: not re.search(r"# \[START .*\]$", l), filtered_lines) + ) + filtered_lines = list( + filter(lambda l: not re.search(r"# \[END .*\]$", l), filtered_lines) + ) + FULL_TEXT += "\n".join(filtered_lines) + "\n" nbook["cells"].append(v4.new_code_cell(source=FULL_TEXT, id="code")) @@ -197,4 +203,4 @@ print(f"writing {output_file}") with open(output_file, mode="w", encoding="utf-8") as fpout: - fpout.write(jsonform) + fpout.write(jsonform) diff --git a/tools/release/amd64.Dockerfile b/tools/release/amd64.Dockerfile index b95ad2405ed..1622d7368fa 100644 --- a/tools/release/amd64.Dockerfile +++ b/tools/release/amd64.Dockerfile @@ -36,10 +36,10 @@ RUN dnf -y update \ ENV JAVA_HOME=/usr/lib/jvm/java # Update maven -ADD https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz /usr/local +ADD https://dlcdn.apache.org/maven/maven-3/3.9.10/binaries/apache-maven-3.9.10-bin.tar.gz /usr/local RUN mkdir -p /usr/local/maven \ - && tar xzvf /usr/local/apache-maven-3.9.9-bin.tar.gz --strip-components=1 -C /usr/local/maven \ - && rm /usr/local/apache-maven-3.9.9-bin.tar.gz + && tar xzvf /usr/local/apache-maven-3.9.10-bin.tar.gz --strip-components=1 -C /usr/local/maven \ + && rm /usr/local/apache-maven-3.9.10-bin.tar.gz ENV PATH=/usr/local/maven/bin:$PATH ENV TZ=America/Los_Angeles diff --git a/tools/release/arm64.Dockerfile b/tools/release/arm64.Dockerfile index b19b71c8fc2..138c653bb59 100644 --- a/tools/release/arm64.Dockerfile +++ b/tools/release/arm64.Dockerfile @@ -41,10 +41,10 @@ RUN dnf -y update \ ENV JAVA_HOME=/usr/lib/jvm/java # Update maven -ADD https://dlcdn.apache.org/maven/maven-3/3.9.9/binaries/apache-maven-3.9.9-bin.tar.gz /usr/local +ADD https://dlcdn.apache.org/maven/maven-3/3.9.10/binaries/apache-maven-3.9.10-bin.tar.gz /usr/local RUN mkdir -p /usr/local/maven \ - && tar xzvf /usr/local/apache-maven-3.9.9-bin.tar.gz --strip-components=1 -C /usr/local/maven \ - && rm /usr/local/apache-maven-3.9.9-bin.tar.gz + && tar xzvf /usr/local/apache-maven-3.9.10-bin.tar.gz --strip-components=1 -C /usr/local/maven \ + && rm /usr/local/apache-maven-3.9.10-bin.tar.gz ENV PATH=/usr/local/maven/bin:$PATH ENV TZ=America/Los_Angeles diff --git a/tools/release/build_delivery_linux.sh b/tools/release/build_delivery_linux.sh index 1a9431662aa..6c6558f880b 100755 --- a/tools/release/build_delivery_linux.sh +++ b/tools/release/build_delivery_linux.sh @@ -30,6 +30,7 @@ ${BOLD}DESCRIPTION${RESET} ${BOLD}OPTIONS${RESET} \t-h --help: display this help text +\tarchive: build all (C++, .Net, Java) archives \tdotnet: build all .Net packages \tjava: build all Java packages \tpython: build all Pyhon packages @@ -208,7 +209,7 @@ function build_python() { command -v python3 | xargs echo "python3: " | tee -a build.log python3 -c "import platform as p; print(p.platform())" | tee -a build.log python3 -m pip install --upgrade --user --break-system-package pip - python3 -m pip install --upgrade --user --break-system-package wheel absl-py mypy mypy-protobuf virtualenv + python3 -m pip install --upgrade --user --break-system-package wheel absl-py mypy mypy-protobuf virtualenv "typing-extensions>=4.12" echo "check protoc-gen-mypy..." command -v protoc-gen-mypy | xargs echo "protoc-gen-mypy: " | tee -a build.log protoc-gen-mypy --version | xargs echo "protoc-gen-mypy version: " | tee -a build.log diff --git a/tools/release/build_delivery_macos.sh b/tools/release/build_delivery_macos.sh index 31aa8d1d470..f03ddf3577c 100755 --- a/tools/release/build_delivery_macos.sh +++ b/tools/release/build_delivery_macos.sh @@ -30,6 +30,7 @@ ${BOLD}DESCRIPTION${RESET} ${BOLD}OPTIONS${RESET} \t-h --help: display this help text +\tarchive: build all (C++, .Net, Java) archives \tdotnet: build all .Net packages \tjava: build all Java packages \tpython: build all Pyhon packages @@ -63,12 +64,15 @@ function build_dotnet() { fi cd "${ROOT_DIR}" || exit 2 - echo "check swig..." + echo -n "check swig..." command -v swig command -v swig | xargs echo "swig: " | tee -a build.log - echo "check dotnet..." + echo "DONE" | tee -a build.log + + echo -n "check dotnet..." command -v dotnet command -v dotnet | xargs echo "dotnet: " | tee -a build.log + echo "DONE" | tee -a build.log # Install .Net SNK echo -n "Install .Net SNK..." | tee -a build.log @@ -76,7 +80,8 @@ function build_dotnet() { if [[ -x $(command -v openssl11) ]]; then OPENSSL_PRG=openssl11 fi - echo "check ${OPENSSL_PRG}..." + echo "DONE" | tee -a build.log + echo -n "check ${OPENSSL_PRG}..." command -v ${OPENSSL_PRG} | xargs echo "openssl: " | tee -a build.log $OPENSSL_PRG aes-256-cbc -iter 42 -pass pass:"$ORTOOLS_TOKEN" \ @@ -91,9 +96,12 @@ function build_dotnet() { rm -rf "${ROOT_DIR}/temp_dotnet" echo "DONE" | tee -a build.log - echo -n "Build .Net..." | tee -a build.log + echo "Build .Net..." | tee -a build.log cmake -S. -Btemp_dotnet -DBUILD_SAMPLES=OFF -DBUILD_EXAMPLES=OFF -DBUILD_DOTNET=ON cmake --build temp_dotnet -j8 -v + echo -n " Check libortools.dylib..." | tee -a build.log + otool -L temp_dotnet/lib/libortools.dylib | grep -vqz "/Users" + echo "DONE" | tee -a build.log echo "DONE" | tee -a build.log #cmake --build temp_dotnet --target test #echo "cmake test: DONE" | tee -a build.log @@ -111,9 +119,11 @@ function build_java() { fi cd "${ROOT_DIR}" || exit 2 - echo "check swig..." + echo -n "check swig..." command -v swig command -v swig | xargs echo "swig: " | tee -a build.log + echo "DONE" | tee -a build.log + # maven require JAVA_HOME if [[ -z "${JAVA_HOME}" ]]; then echo "JAVA_HOME: not found !" | tee -a build.log @@ -169,18 +179,19 @@ function build_java() { rm -rf "${ROOT_DIR}/temp_java" echo "DONE" | tee -a build.log - echo -n "Build Java..." | tee -a build.log - + echo "Build Java..." | tee -a build.log if [[ ! -v GPG_ARGS ]]; then GPG_EXTRA="" else GPG_EXTRA="-DGPG_ARGS=${GPG_ARGS}" fi - # shellcheck disable=SC2086 # cmake fail to parse empty string "" cmake -S. -Btemp_java -DBUILD_SAMPLES=OFF -DBUILD_EXAMPLES=OFF \ -DBUILD_JAVA=ON -DSKIP_GPG=OFF ${GPG_EXTRA} cmake --build temp_java -j8 -v + echo -n " Check libortools.dylib..." | tee -a build.log + otool -L temp_java/lib/libortools.dylib | grep -vqz "/Users" + echo "DONE" | tee -a build.log echo "DONE" | tee -a build.log #cmake --build temp_java --target test #echo "cmake test: DONE" | tee -a build.log @@ -208,9 +219,10 @@ function build_python() { PATH_BCKP=${PATH} cd "${ROOT_DIR}" || exit 2 - echo "check swig..." + echo -n "check swig..." command -v swig command -v swig | xargs echo "swig: " | tee -a build.log + echo "DONE" | tee -a build.log if [[ ${PLATFORM} == "arm64" ]]; then local -r PY=(3.9 3.10 3.11 3.12 3.13) @@ -269,10 +281,36 @@ function build_python() { echo -n "Cleaning Python ${PY_VERSION}..." | tee -a build.log rm -rf "temp_python${PY_VERSION}" echo "DONE" | tee -a build.log - echo -n "Build Python ${PY_VERSION}..." | tee -a build.log + + echo "Build Python ${PY_VERSION}..." | tee -a build.log + echo -n " CMake configure..." | tee -a build.log cmake -S. -B"temp_python${PY_VERSION}" -DBUILD_SAMPLES=OFF -DBUILD_EXAMPLES=OFF -DBUILD_PYTHON=ON -DPython3_ROOT_DIR="$PY_PATH" - cmake --build "temp_python${PY_VERSION}" -j8 -v echo "DONE" | tee -a build.log + + echo -n " Build libortools..." | tee -a build.log + cmake --build "temp_python${PY_VERSION}" --target ortools -j8 -v + echo "DONE" | tee -a build.log + + if [[ ${PLATFORM} == "x86_64" ]]; then + echo -n " Build all few times..." | tee -a build.log + # on macos X86_64 stubgen will timeout -> need to build few times + cmake --build "temp_python${PY_VERSION}" -j4 -v || true + sleep 10 + cmake --build "temp_python${PY_VERSION}" -v || true + echo "DONE" | tee -a build.log + echo -n " ReBuild all..." | tee -a build.log + cmake --build "temp_python${PY_VERSION}" -j4 -v + echo "DONE" | tee -a build.log + else + echo -n " Build all..." | tee -a build.log + cmake --build "temp_python${PY_VERSION}" -j8 -v + echo "DONE" | tee -a build.log + fi + + echo -n " Check libortools.dylib..." | tee -a build.log + otool -L "temp_python${PY_VERSION}/lib/libortools.dylib" | grep -vqz "/Users" + echo "DONE" | tee -a build.log + echo "Build Python ${PY_VERSION}...DONE" | tee -a build.log #cmake --build temp_python${PY_VERSION} --target test #echo "cmake test_python${PY_VERSION}: DONE" | tee -a build.log @@ -318,17 +356,27 @@ function build_archive() { echo -n "Clean previous archive..." | tee -a build.log make clean_archive + echo "DONE" | tee -a build.log - echo -n "Make cpp archive..." | tee -a build.log + echo "Make cpp archive..." | tee -a build.log make archive_cpp + echo -n " Check libortools.dylib..." | tee -a build.log + otool -L "build_make/lib/libortools.dylib" | grep -vqz "/Users" + echo "DONE" | tee -a build.log echo "DONE" | tee -a build.log - echo -n "Make dotnet archive..." | tee -a build.log + echo "Make dotnet archive..." | tee -a build.log make archive_dotnet + echo -n " Check libortools.dylib..." | tee -a build.log + otool -L "build_make/lib/libortools.dylib" | grep -vqz "/Users" + echo "DONE" | tee -a build.log echo "DONE" | tee -a build.log - echo -n "Make java archive..." | tee -a build.log + echo "Make java archive..." | tee -a build.log make archive_java + echo -n " Check libortools.dylib..." | tee -a build.log + otool -L "build_make/lib/libortools.dylib" | grep -vqz "/Users" + echo "DONE" | tee -a build.log echo "DONE" | tee -a build.log # move archive to export @@ -350,16 +398,26 @@ function build_examples() { echo "Check Sed version..." sed --version 2>&1 | head -n 1 | grep "GNU sed.*\b4" + echo -n "Clean previous example archives..." | tee -a build.log rm -rf temp ./*.tar.gz - echo -n "Build examples archives..." | tee -a build.log + echo "DONE" | tee -a build.log + + echo "Build examples archives..." | tee -a build.log + echo -n " Python examples archive..." | tee -a build.log make python_examples_archive UNIX_PYTHON_VER=3 + echo "DONE" | tee -a build.log + echo -n " Java examples archive..." | tee -a build.log make java_examples_archive UNIX_PYTHON_VER=3 + echo "DONE" | tee -a build.log + echo -n " .Net examples archive..." | tee -a build.log make dotnet_examples_archive UNIX_PYTHON_VER=3 echo "DONE" | tee -a build.log + echo "DONE" | tee -a build.log + # move example to export/ mv or-tools_*_examples_*.tar.gz export/ echo "${ORTOOLS_BRANCH} ${ORTOOLS_SHA1}" > "${ROOT_DIR}/export/examples_build" @@ -372,6 +430,7 @@ function reset() { cd "${ROOT_DIR}" || exit 2 make clean + rm -rf temp_cpp rm -rf temp_dotnet rm -rf temp_java rm -rf temp_python* diff --git a/tools/release/build_delivery_manylinux_amd64.sh b/tools/release/build_delivery_manylinux_amd64.sh index ab0a580856a..892745755e5 100755 --- a/tools/release/build_delivery_manylinux_amd64.sh +++ b/tools/release/build_delivery_manylinux_amd64.sh @@ -30,6 +30,7 @@ ${BOLD}DESCRIPTION${RESET} ${BOLD}OPTIONS${RESET} \t-h --help: display this help text +\tarchive: build all (C++, .Net, Java) archives \tdotnet: build all .Net packages \tjava: build all Java packages \tpython: build all Pyhon packages diff --git a/tools/release/build_delivery_manylinux_arm64.sh b/tools/release/build_delivery_manylinux_arm64.sh index d87d4b7bdfc..81961b19d43 100755 --- a/tools/release/build_delivery_manylinux_arm64.sh +++ b/tools/release/build_delivery_manylinux_arm64.sh @@ -30,6 +30,7 @@ ${BOLD}DESCRIPTION${RESET} ${BOLD}OPTIONS${RESET} \t-h --help: display this help text +\tarchive: build all (C++, .Net, Java) archives \tdotnet: build all .Net packages \tjava: build all Java packages \tpython: build all Pyhon packages diff --git a/tools/release/build_delivery_win.cmd b/tools/release/build_delivery_win.cmd index d50a83f8319..940e0ff7bd4 100644 --- a/tools/release/build_delivery_win.cmd +++ b/tools/release/build_delivery_win.cmd @@ -93,7 +93,7 @@ echo help: show this help text (default) echo dotnet: Build dotnet packages echo java: Build java packages echo python: Build python packages -echo archive: Build archive +echo archive: Build all (C++, .Net, Java) archives echo examples: Build examples archives echo all: build everything echo reset: delete all artifacts and suppress cache file @@ -284,7 +284,7 @@ FOR %%v IN (9 10 11 12 13) DO ( echo Check python3.%%v... | tee.exe -a build.log which.exe "C:\python3%%v-64\python.exe" || exit 1 echo "C:\python3%%v-64\python.exe: FOUND" | tee.exe -a build.log - C:\python3%%v-64\python.exe -m pip install --upgrade --user absl-py mypy mypy-protobuf protobuf numpy pandas + C:\python3%%v-64\python.exe -m pip install --upgrade --user absl-py mypy mypy-protobuf protobuf numpy pandas "typing-extensions>=4.12" call :subroutine %%v diff --git a/tools/release/publish_delivery_linux.sh b/tools/release/publish_delivery_linux.sh index fcd132729e1..a082bb3dc20 100755 --- a/tools/release/publish_delivery_linux.sh +++ b/tools/release/publish_delivery_linux.sh @@ -76,9 +76,9 @@ function publish_java() { if [[ -x "$(command -v openssl11)" ]]; then OPENSSL_PRG=openssl11 fi - command -v $OPENSSL_PRG | xargs echo "openssl: " | tee -a build.log + command -v $OPENSSL_PRG | xargs echo "openssl: " | tee -a publish.log command -v gpg - command -v gpg | xargs echo "gpg: " | tee -a build.log + command -v gpg | xargs echo "gpg: " | tee -a publish.log echo -n "Publish native Java..." | tee -a publish.log cmake --build temp_java --target java_native_deploy -v diff --git a/tools/release/publish_delivery_manylinux_amd64.sh b/tools/release/publish_delivery_manylinux_amd64.sh index 9f67999de11..17236e7688b 100755 --- a/tools/release/publish_delivery_manylinux_amd64.sh +++ b/tools/release/publish_delivery_manylinux_amd64.sh @@ -109,7 +109,7 @@ function main() { local -r RELEASE_DIR="$(cd -P -- "$(dirname -- "$0")" && pwd -P)" echo "RELEASE_DIR: '${RELEASE_DIR}'" | tee -a publish.log - (cd "${ROOT_DIR}" && make print-OR_TOOLS_VERSION | tee -a build.log) + (cd "${ROOT_DIR}" && make print-OR_TOOLS_VERSION | tee -a publish.log) local -r ORTOOLS_BRANCH=$(git rev-parse --abbrev-ref HEAD) local -r ORTOOLS_SHA1=$(git rev-parse --verify HEAD) @@ -124,8 +124,9 @@ function main() { "publish_$1" exit ;; all) + #publish_dotnet publish_java - publish_python + #publish_python exit ;; *) >&2 echo "Target '${1}' unknown" diff --git a/tools/release/publish_delivery_manylinux_arm64.sh b/tools/release/publish_delivery_manylinux_arm64.sh index 5c444b23565..7c1e655f322 100755 --- a/tools/release/publish_delivery_manylinux_arm64.sh +++ b/tools/release/publish_delivery_manylinux_arm64.sh @@ -109,7 +109,7 @@ function main() { local -r RELEASE_DIR="$(cd -P -- "$(dirname -- "$0")" && pwd -P)" echo "RELEASE_DIR: '${RELEASE_DIR}'" | tee -a publish.log - (cd "${ROOT_DIR}" && make print-OR_TOOLS_VERSION | tee -a build.log) + (cd "${ROOT_DIR}" && make print-OR_TOOLS_VERSION | tee -a publish.log) local -r ORTOOLS_BRANCH=$(git rev-parse --abbrev-ref HEAD) local -r ORTOOLS_SHA1=$(git rev-parse --verify HEAD) @@ -124,8 +124,9 @@ function main() { "publish_$1" exit ;; all) + #publish_dotnet publish_java - publish_python + #publish_python exit ;; *) >&2 echo "Target '${1}' unknown" diff --git a/tools/release/publish_delivery_meta.sh b/tools/release/publish_delivery_meta.sh new file mode 100755 index 00000000000..14df6f295f6 --- /dev/null +++ b/tools/release/publish_delivery_meta.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# Copyright 2010-2025 Google LLC +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -eo pipefail + +function help() { + local -r NAME=$(basename "$0") + local -r BOLD="\e[1m" + local -r RESET="\e[0m" + local -r help=$(cat << EOF +${BOLD}NAME${RESET} +\t$NAME - Publish delivery using the ${BOLD}local host system${RESET}. +${BOLD}SYNOPSIS${RESET} +\t$NAME [-h|--help] [java] +${BOLD}DESCRIPTION${RESET} +\tPublish Google OR-Tools deliveries. +\tYou ${BOLD}MUST${RESET} define the following variables before running this script: +\t* ORTOOLS_TOKEN: secret use to decrypt key to sign Java package. + +${BOLD}OPTIONS${RESET} +\t-h --help: display this help text +\tjava: publish the Java runtime packages +\tall: publish everything (default) + +${BOLD}EXAMPLES${RESET} +Using export to define the ${BOLD}ORTOOLS_TOKEN${RESET} env and only publishing the Java packages: +export ORTOOLS_TOKEN=SECRET +$0 java + +note: the 'export ORTOOLS_TOKEN=...' should be placed in your bashrc to avoid any leak +of the secret in your bash history +EOF +) + echo -e "$help" +} + +function assert_defined(){ + if [[ -z "${!1}" ]]; then + >&2 echo "Variable '${1}' must be defined" + exit 1 + fi +} + +# Java publish +function publish_java() { + if echo "${ORTOOLS_BRANCH} ${ORTOOLS_SHA1}" | cmp --silent "${ROOT_DIR}/export_meta/meta_java_publish" -; then + echo "publish Java up to date!" + return 0 + fi + + # maven require JAVA_HOME + if [[ -z "${JAVA_HOME}" ]]; then + echo "JAVA_HOME: not found !" | tee publish.log + exit 1 + else + echo "JAVA_HOME: ${JAVA_HOME}" | tee -a publish.log + command -v mvn + command -v mvn | xargs echo "mvn: " | tee -a publish.log + java -version 2>&1 | tee -a publish.log + java -version 2>&1 | head -n 1 | grep -q "1.8" + fi + # Maven central need gpg sign and we store the release key encoded using openssl + local OPENSSL_PRG=openssl + if [[ -x "$(command -v openssl11)" ]]; then + OPENSSL_PRG=openssl11 + fi + command -v $OPENSSL_PRG | xargs echo "openssl: " | tee -a publish.log + command -v gpg + command -v gpg | xargs echo "gpg: " | tee -a publish.log + + echo -n "Publish native Java..." | tee -a publish.log + cmake --build temp_meta_java --config Release --target java_deploy -v + echo "DONE" | tee -a publish.log + + echo "${ORTOOLS_BRANCH} ${ORTOOLS_SHA1}" > "${ROOT_DIR}/export_meta/meta_java_publish" +} + +# Main +function main() { + case ${1} in + -h | --help) + help; exit ;; + esac + + assert_defined ORTOOLS_TOKEN + echo "ORTOOLS_TOKEN: FOUND" | tee publish.log + make print-OR_TOOLS_VERSION | tee -a publish.log + + local -r ROOT_DIR="$(cd -P -- "$(dirname -- "$0")/../.." && pwd -P)" + echo "ROOT_DIR: '${ROOT_DIR}'" + + local -r RELEASE_DIR="$(cd -P -- "$(dirname -- "$0")" && pwd -P)" + echo "RELEASE_DIR: '${RELEASE_DIR}'" + + local -r ORTOOLS_BRANCH=$(git rev-parse --abbrev-ref HEAD) + local -r ORTOOLS_SHA1=$(git rev-parse --verify HEAD) + local -r PLATFORM=$(uname -m) + + mkdir -p export + case ${1} in + java) + "publish_$1" + exit ;; + all) + publish_java + exit ;; + *) + >&2 echo "Target '${1}' unknown" + exit 1 + esac + exit 0 +} + +main "${1:-all}" + diff --git a/tools/release/publish_delivery_win.cmd b/tools/release/publish_delivery_win.cmd index 4af40864b23..644871aba25 100644 --- a/tools/release/publish_delivery_win.cmd +++ b/tools/release/publish_delivery_win.cmd @@ -83,9 +83,9 @@ which.exe mvn || exit 1 which.exe mvn | tee.exe -a publish.log which.exe gpg || exit 1 -which.exe gpg | tee.exe -a build.log +which.exe gpg | tee.exe -a publish.log which.exe openssl || exit 1 -which.exe openssl | tee.exe -a build.log +which.exe openssl | tee.exe -a publish.log echo Publish native Java... | tee.exe -a publish.log cmake --build temp_java --config Release --target java_native_deploy -v