From c8935fcfdf84dcf0de259c3edefec11765c96bae Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 5 Nov 2025 12:03:24 +0100 Subject: [PATCH] backport from main --- ortools/algorithms/BUILD.bazel | 2 - ortools/base/python-swig.h | 60 ++----- .../src/moi_wrapper/CPSat_wrapper.jl | 157 ++++++++++++++++++ ortools/sat/docs/README.md | 4 +- ortools/service/v1/BUILD.bazel | 6 +- 5 files changed, 171 insertions(+), 58 deletions(-) diff --git a/ortools/algorithms/BUILD.bazel b/ortools/algorithms/BUILD.bazel index 5f778ab22c..35566e85aa 100644 --- a/ortools/algorithms/BUILD.bazel +++ b/ortools/algorithms/BUILD.bazel @@ -131,7 +131,6 @@ cc_library( name = "hungarian", srcs = ["hungarian.cc"], hdrs = ["hungarian.h"], - visibility = ["//visibility:public"], deps = [ "//ortools/base", "@abseil-cpp//absl/container:flat_hash_map", @@ -158,7 +157,6 @@ cc_test( cc_library( name = "adjustable_k_ary_heap", hdrs = ["adjustable_k_ary_heap.h"], - visibility = ["//visibility:public"], deps = ["@abseil-cpp//absl/log:check"], ) diff --git a/ortools/base/python-swig.h b/ortools/base/python-swig.h index edc8236f86..a7d2a846c2 100644 --- a/ortools/base/python-swig.h +++ b/ortools/base/python-swig.h @@ -25,7 +25,6 @@ #ifndef ORTOOLS_BASE_PYTHON_SWIG_H_ #define ORTOOLS_BASE_PYTHON_SWIG_H_ -#if PY_VERSION_HEX >= 0x03030000 // Py3.3+ // Use Py3 unicode str() type for C++ strings. #ifdef PyString_FromStringAndSize #undef PyString_FromStringAndSize @@ -51,7 +50,6 @@ static inline int PyString_AsStringAndSize(PyObject* obj, char** buf, PyErr_SetString(PyExc_TypeError, "Expecting str or bytes"); return -1; } -#endif // Py3.3+ template inline bool PyObjAs(PyObject* pystr, T* cstr) { @@ -67,14 +65,12 @@ template <> inline bool PyObjAs(PyObject* pystr, ::std::string* cstr) { char* buf; Py_ssize_t len; -#if PY_VERSION_HEX >= 0x03030000 if (PyUnicode_Check(pystr)) { buf = PyUnicode_AsUTF8AndSize(pystr, &len); if (!buf) return false; - } else // NOLINT -#endif - if (PyBytes_AsStringAndSize(pystr, &buf, &len) == -1) + } else if (PyBytes_AsStringAndSize(pystr, &buf, &len) == -1) { // NOLINT return false; + } if (cstr) cstr->assign(buf, len); return true; } @@ -83,14 +79,12 @@ template inline bool PyObjAs(PyObject* pystr, std::string* cstr) { char* buf; Py_ssize_t len; -#if PY_VERSION_HEX >= 0x03030000 if (PyUnicode_Check(pystr)) { buf = const_cast(PyUnicode_AsUTF8AndSize(pystr, &len)); if (!buf) return false; - } else // NOLINT -#endif - if (PyBytes_AsStringAndSize(pystr, &buf, &len) == -1) + } else if (PyBytes_AsStringAndSize(pystr, &buf, &len) == -1) { // NOLINT return false; + } if (cstr) cstr->assign(buf, len); return true; } @@ -132,36 +126,18 @@ inline bool PyObjAs(PyObject* py, unsigned int* c) { template <> inline bool PyObjAs(PyObject* py, int64_t* c) { // NOLINT - int64_t i; // NOLINT -#if PY_MAJOR_VERSION < 3 - if (PyInt_Check(py)) { - i = PyInt_AsLong(py); - } else { - if (!PyLong_Check(py)) return false; // Not a Python long. -#else - { -#endif - i = PyLong_AsLongLong(py); - if (i == -1 && PyErr_Occurred()) return false; // Not a C long long. - } + const int64_t i = PyLong_AsLongLong(py); + if (i == -1 && PyErr_Occurred()) return false; // Not a C long long. if (c) *c = i; return true; } template <> inline bool PyObjAs(PyObject* py, uint64_t* c) { // NOLINT - uint64_t i; // NOLINT -#if PY_MAJOR_VERSION < 3 - if (PyInt_Check(py)) { - i = PyInt_AsUnsignedLongLongMask(py); - } else // NOLINT -#endif - { - if (!PyLong_Check(py)) return false; // Not a Python long. - i = PyLong_AsUnsignedLongLong(py); - if (i == (uint64_t)-1 && PyErr_Occurred()) // NOLINT - return false; - } + if (!PyLong_Check(py)) return false; // Not a Python long. + const uint64_t i = PyLong_AsUnsignedLongLong(py); + if (i == (uint64_t)-1 && PyErr_Occurred()) // NOLINT + return false; if (c) *c = i; return true; } @@ -171,12 +147,6 @@ inline bool PyObjAs(PyObject* py, double* c) { double d; if (PyFloat_Check(py)) { d = PyFloat_AsDouble(py); -#if PY_MAJOR_VERSION < 3 - } else if (PyInt_Check(py)) { - d = PyInt_AsLong(py); - } else if (!PyLong_Check(py)) { - return false; // float or int/long expected -#endif } else { d = PyLong_AsDouble(py); if (d == -1.0 && PyErr_Occurred()) { @@ -210,13 +180,7 @@ inline bool PyObjAs(PyObject* py, bool* c) { return true; } -inline int SwigPyIntOrLong_Check(PyObject* o) { - return (PyLong_Check(o) -#if PY_MAJOR_VERSION <= 2 - || PyInt_Check(o) -#endif - ); // NOLINT -} +inline int SwigPyIntOrLong_Check(PyObject* o) { return PyLong_Check(o); } inline int SwigString_Check(PyObject* o) { return PyUnicode_Check(o); } @@ -352,13 +316,11 @@ inline PyObject* vector_output_wrap_helper(const std::vector* vec, #endif } -#if PY_MAJOR_VERSION > 2 /* SWIG 2's own C preprocessor macro for this is too strict. * It requires a (x) parameter which doesn't work for the case where the * function is being passed by & as a converter into a helper such as * vector_output_helper above. */ #undef PyInt_FromLong #define PyInt_FromLong PyLong_FromLong -#endif #endif // ORTOOLS_BASE_PYTHON_SWIG_H_ diff --git a/ortools/julia/ORTools.jl/src/moi_wrapper/CPSat_wrapper.jl b/ortools/julia/ORTools.jl/src/moi_wrapper/CPSat_wrapper.jl index 63d9901a0d..0f7082349c 100644 --- a/ortools/julia/ORTools.jl/src/moi_wrapper/CPSat_wrapper.jl +++ b/ortools/julia/ORTools.jl/src/moi_wrapper/CPSat_wrapper.jl @@ -10,12 +10,17 @@ MOI implementation for the CP-Sat solver mutable struct CPSATOptimizer <: MOI.AbstractOptimizer model::Union{Nothing,CpModel} parameters::Union{Nothing,SatParameters} + # Set of constraints in variable indices + variables_constraints::Set{MOI.ConstraintIndex} + constraint_types_present::Set{Tuple{Type,Type}} # This structure is updated by the optimize! function. solve_response::Union{Nothing,CpSolverResponse} function CPSATOptimizer(; name::String = "") model = CpModel(name = name) parameters = SatParameters() + variables_constraints = Set{MOI.ConstraintIndex}() + constraint_types_present = Set{Tuple{Type,Type}}() new(model, parameters, nothing) end @@ -32,6 +37,8 @@ end function MOI.empty!(optimizer::CPSATOptimizer) optimizer.model = CpModel() + optimizer.variables_constraints = Set{MOI.ConstraintIndex}() + optimizer.constraint_types_present = Set{Tuple{Type,Type}}() optimizer.solve_response = nothing return nothing @@ -203,3 +210,153 @@ function MOI.get(optimizer::CPSATOptimizer, ::Type{MOI.VariableIndex}, name::Str return nothing end + + +""" + + Constraint overrides. + +""" + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{MOI.VariableIndex}, + ::Type{<:SCALAR_SET}, +) + return true +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::MOI.VariableIndex, + c::S, +) where {S<:SCALAR_SET} + if S <: MOI.LessThan + throw_if_upper_bound_is_already_set(optimizer, vi, c) + end + + if S <: MOI.GreaterThan + throw_if_lower_bound_is_already_set(optimizer, vi, c) + end + + if S <: MOI.Interval + throw_if_upper_bound_is_already_set(optimizer, vi, c) + throw_if_lower_bound_is_already_set(optimizer, vi, c) + end + + if S <: MOI.EqualTo + throw_if_upper_bound_is_already_set(optimizer, vi, c) + throw_if_lower_bound_is_already_set(optimizer, vi, c) + end + + variable_index = vi.value + + # retrieve the constraint bounds + lower_bound, upper_bound = bounds(c) + + # TODO: (b/452908268) - Update variable's domain instead of creating a new linear constraint. + linear_constraint = CPSatLinearConstraintProto() + + push!(linear_constraint.vars, variable_index) + # In this case, we set the coefficient to 1. + push!(linear_constraint.coeffs, 1) + # Update the domain + push!(linear_constraint.domain, lower_bound) + push!(linear_constraint.domain, upper_bound) + + constraint = CPSATConstraint() + constraint.name = :linear + constraint.value = linear_constraint + + push!(optimizer.model.constraints, constraint) + + push!( + optimizer.variables_constraints, + MOI.ConstraintIndex{MOI.VariableIndex,typeof(c)}(variable_index), + ) + push!(optimizer.constraint_types_present, (MOI.VariableIndex, typeof(c))) + + return MOI.ConstraintIndex{MOI.VariableIndex,typeof(c)}(variable_index) +end + +function throw_if_upper_bound_is_already_set( + optimizer::CPSATOptimizer, + vi::MOI.VariableIndex, + c::S, +) where {S<:SCALAR_SET} + # Assumes type consistency across all constraints. + T = typeof(c).parameters[1] + less_than_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.LessThan{T}}(vi.value) + interval_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.Interval{T}}(vi.value) + equal_to_idx = MOI.ConstraintIndex{MOI.VariableIndex,MOI.EqualTo{T}}(vi.value) + + if in(less_than_idx, optimizer.variables_constraints) + throw(MOI.UpperBoundAlreadySet{MOI.LessThan{T},S}(vi)) + end + + if in(interval_idx, optimizer.variables_constraints) + throw(MOI.UpperBoundAlreadySet{MOI.Interval{T},S}(vi)) + end + + if in(equal_to_idx, optimizer.variables_constraints) + throw(MOI.UpperBoundAlreadySet{MOI.EqualTo{T},S}(vi)) + end + + return nothing +end + +function throw_if_lower_bound_is_already_set( + optimizer::CPSATOptimizer, + vi::MOI.VariableIndex, + c::S, +) where {S<:SCALAR_SET} + # Assumes type consistency across all constraints. + T = typeof(c).parameters[1] + greater_than_idx = MOI.ConstraintIndex{typeof(vi),MOI.GreaterThan{T}}(vi.value) + interval_idx = MOI.ConstraintIndex{typeof(vi),MOI.Interval{T}}(vi.value) + equal_to_idx = MOI.ConstraintIndex{typeof(vi),MOI.EqualTo{T}}(vi.value) + + if in(greater_than_idx, optimizer.variables_constraints) + throw(MOI.LowerBoundAlreadySet{MOI.GreaterThan{T},S}(vi)) + end + + if in(interval_idx, optimizer.variables_constraints) + throw(MOI.LowerBoundAlreadySet{MOI.Interval{T},S}(vi)) + end + + if in(equal_to_idx, optimizer.variables_constraints) + throw(MOI.LowerBoundAlreadySet{MOI.EqualTo{T},S}(vi)) + end + + return nothing +end + +function MOI.supports_constraint( + ::CPSATOptimizer, + ::Type{MOI.VariableIndex}, + ::Type{MOI.ZeroOne}, +) + return true +end + +function MOI.add_constraint( + optimizer::CPSATOptimizer, + vi::MOI.VariableIndex, + c::MOI.ZeroOne, +) + # Get the int value of the variable index + index = vi.value + + # Set the variable bounds to [0, 1] (override the existing domain) + optimizer.model.variables[index].domain = [zero(Int), one(Int)] + + # Update the associated metadata. + # TODO: (b/452416646) - Fetch constraint types dynamically at runtime + push!(optimizer.constraint_types_present, (MOI.VariableIndex, MOI.ZeroOne)) + push!( + optimizer.variables_constraints, + MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(index), + ) + + return MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}(index) +end diff --git a/ortools/sat/docs/README.md b/ortools/sat/docs/README.md index 9e4116e4e4..2b35338cdd 100644 --- a/ortools/sat/docs/README.md +++ b/ortools/sat/docs/README.md @@ -66,8 +66,8 @@ simple_sat_program() The interface to the C++ CP-SAT solver is implemented through the **CpModelBuilder** class described in -*ortools/sat/cp_model.h*. This class is just a helper to fill -in the cp_model protobuf. +*ortools/sat/cp_model.h*. This class is just a helper to +fill in the cp_model protobuf. Calling Solve() method will return a CpSolverResponse protobuf that contains the solve status, the values for each variable in the model if solve was successful, diff --git a/ortools/service/v1/BUILD.bazel b/ortools/service/v1/BUILD.bazel index 69c6fbc45c..1d7a1dcf05 100644 --- a/ortools/service/v1/BUILD.bazel +++ b/ortools/service/v1/BUILD.bazel @@ -12,14 +12,10 @@ # 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") -package(default_visibility = [ - "//ortools/math_opt:__subpackages__", - "//ortools/service:__subpackages__", -]) +package(default_visibility = ["//ortools/math_opt:__subpackages__"]) proto_library( name = "optimization_proto",