diff --git a/cmake/python.cmake b/cmake/python.cmake index 6d4fbde01e..01d0e0b8f2 100644 --- a/cmake/python.cmake +++ b/cmake/python.cmake @@ -384,7 +384,6 @@ if(BUILD_MATH_OPT) ortools/math_opt/python/errors.py ortools/math_opt/python/expressions.py ortools/math_opt/python/from_model.py - ortools/math_opt/python/hash_model_storage.py ortools/math_opt/python/indicator_constraints.py ortools/math_opt/python/init_arguments.py ortools/math_opt/python/linear_constraints.py @@ -392,7 +391,6 @@ if(BUILD_MATH_OPT) ortools/math_opt/python/message_callback.py ortools/math_opt/python/model.py ortools/math_opt/python/model_parameters.py - ortools/math_opt/python/model_storage.py ortools/math_opt/python/normalized_inequality.py ortools/math_opt/python/normalize.py ortools/math_opt/python/objectives.py diff --git a/ortools/algorithms/hungarian.cc b/ortools/algorithms/hungarian.cc index f39d420cc3..b0b2fa109a 100644 --- a/ortools/algorithms/hungarian.cc +++ b/ortools/algorithms/hungarian.cc @@ -16,7 +16,6 @@ #include #include #include -#include #include #include diff --git a/ortools/algorithms/hungarian.h b/ortools/algorithms/hungarian.h index 538027b0c9..5ae49fbbae 100644 --- a/ortools/algorithms/hungarian.h +++ b/ortools/algorithms/hungarian.h @@ -11,18 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// -// IMPORTANT NOTE: we advise using the code in -// graph/linear_assignment.h whose complexity is -// usually much smaller. -// TODO(user): base this code on LinearSumAssignment. -// -// For each of the four functions declared in this file, in case the input -// parameter 'cost' contains NaN, the function will return without invoking the -// Hungarian algorithm, and the output parameters 'direct_assignment' and -// 'reverse_assignment' will be left unchanged. -// - // An O(n^4) implementation of the Kuhn-Munkres algorithm (a.k.a. the // Hungarian algorithm) for solving the assignment problem. // The assignment problem takes a set of agents, a set of tasks and a @@ -30,10 +18,6 @@ // an optimal (i.e., least cost) assignment of agents to tasks. // The code also enables computing a maximum assignment by changing the // input matrix. -// -// This code is based on (read: translated from) the Java version -// (read: translated from) the Python version at -// http://www.clapper.org/software/python/munkres/. #ifndef OR_TOOLS_ALGORITHMS_HUNGARIAN_H_ #define OR_TOOLS_ALGORITHMS_HUNGARIAN_H_ diff --git a/ortools/bop/bop_portfolio.h b/ortools/bop/bop_portfolio.h index 16f78a616f..085ddaaf9b 100644 --- a/ortools/bop/bop_portfolio.h +++ b/ortools/bop/bop_portfolio.h @@ -56,7 +56,6 @@ class OptimizerSelector; // - LP_FIRST_SOLUTION // - OBJECTIVE_FIRST_SOLUTION // - USER_GUIDED_FIRST_SOLUTION -// - FEASIBILITY_PUMP_FIRST_SOLUTION // - RANDOM_CONSTRAINT_LNS_GUIDED_BY_LP // - RANDOM_VARIABLE_LNS_GUIDED_BY_LP // - RELATION_GRAPH_LNS diff --git a/ortools/constraint_solver/alldiff_cst.cc b/ortools/constraint_solver/alldiff_cst.cc index 3a3c2aff9a..328d756c3b 100644 --- a/ortools/constraint_solver/alldiff_cst.cc +++ b/ortools/constraint_solver/alldiff_cst.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // AllDifferent constraints #include @@ -24,7 +23,6 @@ #include "absl/strings/str_format.h" #include "absl/strings/string_view.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/util/string_array.h" diff --git a/ortools/constraint_solver/constraint_solver.cc b/ortools/constraint_solver/constraint_solver.cc index de6ebe007f..0f4f56dfc0 100644 --- a/ortools/constraint_solver/constraint_solver.cc +++ b/ortools/constraint_solver/constraint_solver.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // This file implements the core objects of the constraint solver: // Solver, Search, Queue, ... along with the main resolution loop. diff --git a/ortools/constraint_solver/count_cst.cc b/ortools/constraint_solver/count_cst.cc index edab301b68..80ac0f9617 100644 --- a/ortools/constraint_solver/count_cst.cc +++ b/ortools/constraint_solver/count_cst.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Count constraints #include diff --git a/ortools/constraint_solver/expr_array.cc b/ortools/constraint_solver/expr_array.cc index 914c7e5e88..714909fd63 100644 --- a/ortools/constraint_solver/expr_array.cc +++ b/ortools/constraint_solver/expr_array.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Array Expression constraints #include diff --git a/ortools/constraint_solver/expr_cst.cc b/ortools/constraint_solver/expr_cst.cc index fb1b45bb97..c3d50056c7 100644 --- a/ortools/constraint_solver/expr_cst.cc +++ b/ortools/constraint_solver/expr_cst.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Expression constraints #include diff --git a/ortools/constraint_solver/range_cst.cc b/ortools/constraint_solver/range_cst.cc index 6637eeb40e..fa4b24ca66 100644 --- a/ortools/constraint_solver/range_cst.cc +++ b/ortools/constraint_solver/range_cst.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Range constraints #include diff --git a/ortools/constraint_solver/search_limit.proto b/ortools/constraint_solver/search_limit.proto index fc78a2e35b..43c3dd3be4 100644 --- a/ortools/constraint_solver/search_limit.proto +++ b/ortools/constraint_solver/search_limit.proto @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // This file contains a protocol buffer definition for search limits. syntax = "proto3"; diff --git a/ortools/constraint_solver/table.cc b/ortools/constraint_solver/table.cc index 40db3285f6..60003d8c2d 100644 --- a/ortools/constraint_solver/table.cc +++ b/ortools/constraint_solver/table.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // This file implements the table constraints. #include diff --git a/ortools/glop/preprocessor.h b/ortools/glop/preprocessor.h index dc171cb91d..4e77c5cdfe 100644 --- a/ortools/glop/preprocessor.h +++ b/ortools/glop/preprocessor.h @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // This file contains the presolving code for a LinearProgram. // // A classical reference is: diff --git a/ortools/glop/revised_simplex.cc b/ortools/glop/revised_simplex.cc index fcfe6ef92e..f1aeae58aa 100644 --- a/ortools/glop/revised_simplex.cc +++ b/ortools/glop/revised_simplex.cc @@ -18,7 +18,6 @@ #include #include #include -#include #include #include #include diff --git a/ortools/graph/cliques.h b/ortools/graph/cliques.h index 387c49a42c..6afe4a9f8a 100644 --- a/ortools/graph/cliques.h +++ b/ortools/graph/cliques.h @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Maximal clique algorithms, based on the Bron-Kerbosch algorithm. // See http://en.wikipedia.org/wiki/Bron-Kerbosch_algorithm // and diff --git a/ortools/graph/linear_assignment.h b/ortools/graph/linear_assignment.h index 188a46ddab..c77ad79b1f 100644 --- a/ortools/graph/linear_assignment.h +++ b/ortools/graph/linear_assignment.h @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // An implementation of a cost-scaling push-relabel algorithm for the // assignment problem (minimum-cost perfect bipartite matching), from // the paper of Goldberg and Kennedy (1995). diff --git a/ortools/graph/solve_flow_model.cc b/ortools/graph/solve_flow_model.cc index 31e98c162f..fd42f9fc6e 100644 --- a/ortools/graph/solve_flow_model.cc +++ b/ortools/graph/solve_flow_model.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // This code loads flow-graph models (as Dimacs file or binary FlowModel proto) // and solves them with the OR-tools flow algorithms. // diff --git a/ortools/linear_solver/cbc_interface.cc b/ortools/linear_solver/cbc_interface.cc index c3c8d45e4d..dc97d229eb 100644 --- a/ortools/linear_solver/cbc_interface.cc +++ b/ortools/linear_solver/cbc_interface.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// #if defined(USE_CBC) #include diff --git a/ortools/linear_solver/clp_interface.cc b/ortools/linear_solver/clp_interface.cc index 31220cefeb..d23df0eec5 100644 --- a/ortools/linear_solver/clp_interface.cc +++ b/ortools/linear_solver/clp_interface.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// #if defined(USE_CLP) || defined(USE_CBC) #include diff --git a/ortools/linear_solver/proto_solver/BUILD.bazel b/ortools/linear_solver/proto_solver/BUILD.bazel index eea9aa0f12..e7047ebc6f 100644 --- a/ortools/linear_solver/proto_solver/BUILD.bazel +++ b/ortools/linear_solver/proto_solver/BUILD.bazel @@ -72,6 +72,7 @@ cc_library( srcs = ["sat_solver_utils.cc"], hdrs = ["sat_solver_utils.h"], deps = [ + ":preprocessor", "//ortools/glop:parameters_cc_proto", "//ortools/glop:preprocessor", "//ortools/linear_solver:linear_solver_cc_proto", @@ -195,3 +196,22 @@ cc_library( "@highs", ], ) + +cc_library( + name = "preprocessor", + srcs = ["preprocessor.cc"], + hdrs = ["preprocessor.h"], + deps = [ + "//ortools/glop:preprocessor", + "//ortools/lp_data", + "//ortools/lp_data:base", + "//ortools/lp_data:lp_utils", + "//ortools/lp_data:sparse", + "//ortools/lp_data:sparse_column", + "//ortools/util:fp_utils", + "//ortools/util:return_macros", + "//ortools/util:stats", + "@abseil-cpp//absl/log", + "@abseil-cpp//absl/log:check", + ], +) diff --git a/ortools/linear_solver/proto_solver/preprocessor.cc b/ortools/linear_solver/proto_solver/preprocessor.cc new file mode 100644 index 0000000000..fa62477f66 --- /dev/null +++ b/ortools/linear_solver/proto_solver/preprocessor.cc @@ -0,0 +1,492 @@ +// 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/preprocessor.h" + +#include +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/log/log.h" +#include "ortools/lp_data/lp_data.h" +#include "ortools/lp_data/lp_types.h" +#include "ortools/lp_data/lp_utils.h" +#include "ortools/lp_data/sparse.h" +#include "ortools/lp_data/sparse_column.h" +#include "ortools/util/fp_utils.h" +#include "ortools/util/return_macros.h" +#include "ortools/util/stats.h" + +using ::operations_research::glop::ColIndex; +using ::operations_research::glop::ColToRowIndex; +using ::operations_research::glop::Fractional; +using ::operations_research::glop::kInfinity; +using ::operations_research::glop::LinearProgram; +using ::operations_research::glop::ProblemStatus; +using ::operations_research::glop::RowIndex; +using ::operations_research::glop::SparseColumn; +using ::operations_research::glop::SparseMatrix; +using ::operations_research::glop::StrictITIVector; +using ::operations_research::glop::SumWithNegativeInfiniteAndOneMissing; +using ::operations_research::glop::SumWithPositiveInfiniteAndOneMissing; + +namespace operations_research { + +// Helper function to check the bounds of the SetVariableBounds() and +// SetConstraintBounds() functions. +inline bool AreBoundsValid(Fractional lower_bound, Fractional upper_bound) { + if (std::isnan(lower_bound)) return false; + if (std::isnan(upper_bound)) return false; + if (lower_bound == kInfinity && upper_bound == kInfinity) return false; + if (lower_bound == -kInfinity && upper_bound == -kInfinity) return false; + if (lower_bound > upper_bound) return false; + return true; +} + +// -------------------------------------------------------- +// IntegerBoundsPreprocessor +// -------------------------------------------------------- + +bool IntegerBoundsPreprocessor::Run(LinearProgram* linear_program) { + SCOPED_INSTRUCTION_COUNT(time_limit_); + RETURN_VALUE_IF_NULL(linear_program, false); + const Fractional tolerance = integer_solution_tolerance_; + + // Make integer the bounds of integer variables. + // NOTE(user): it may happen that the new bound will be less strict (but at + // most it will be off by integer_solution_tolerance). + int64_t num_changed_bounds = 0; + for (ColIndex col : linear_program->IntegerVariablesList()) { + const Fractional lb = + ceil(linear_program->variable_lower_bounds()[col] - tolerance); + const Fractional ub = + floor(linear_program->variable_upper_bounds()[col] + tolerance); + if (!AreBoundsValid(lb, ub)) { + status_ = glop::ProblemStatus::PRIMAL_INFEASIBLE; + return false; + } + if (lb != linear_program->variable_lower_bounds()[col] || + ub != linear_program->variable_upper_bounds()[col]) { + num_changed_bounds++; + } + linear_program->SetVariableBounds(col, lb, ub); + } + VLOG(2) << "IntegerBoundsPreprocessor changed " << num_changed_bounds + << " variable bounds."; + + // Make integer the bounds of integer constraints, if it makes them stricter. + const SparseMatrix& transpose = linear_program->GetTransposeSparseMatrix(); + num_changed_bounds = 0; + for (RowIndex row = RowIndex(0); row < linear_program->num_constraints(); + ++row) { + bool integer_constraint = true; + for (const SparseColumn::Entry var : transpose.column(RowToColIndex(row))) { + // Don't affect the constraint if it has a non-integer variable. + if (!linear_program->IsVariableInteger(RowToColIndex(var.row()))) { + integer_constraint = false; + break; + } + // Don't affect the constraint if it has a non-integer coefficient. Note + // that we require each coefficient to be precisely an integer in order to + // avoid floating point errors. + // + // TODO(user): checking integer constraints can go further, e.g., + // x + 2 * y = 4 for binary x and y can never be satisfied. But this + // perhaps starts to resemble bound propagation, which might be too much + // for a lightweighted preprocessor like this one. + if (round(var.coefficient()) != var.coefficient()) { + integer_constraint = false; + break; + } + } + if (integer_constraint) { + const Fractional lb = + std::ceil(linear_program->constraint_lower_bounds()[row] - tolerance); + const Fractional ub = std::floor( + linear_program->constraint_upper_bounds()[row] + tolerance); + if (!AreBoundsValid(lb, ub)) { + status_ = ProblemStatus::PRIMAL_INFEASIBLE; + return false; + } + if (lb != linear_program->constraint_lower_bounds()[row] || + ub != linear_program->constraint_upper_bounds()[row]) { + num_changed_bounds++; + } + linear_program->SetConstraintBounds(row, lb, ub); + } + } + VLOG(2) << "IntegerBoundsPreprocessor changed " << num_changed_bounds + << " constraint bounds."; + DCHECK(linear_program->BoundsOfIntegerVariablesAreInteger(tolerance)); + DCHECK(linear_program->BoundsOfIntegerConstraintsAreInteger(tolerance)); + return false; +} + +// -------------------------------------------------------- +// BoundPropagationPreprocessor +// -------------------------------------------------------- +// TODO(user): This preprocessor is not as efficient as it could be because each +// time we process a constraint, we rescan all the involved variables. Make it +// more efficient if it becomes needed. Note that this kind of propagation is +// probably something we want to do each time we take a branch in the mip +// search, so probably an efficient class for this will be created at some +// point. +bool BoundPropagationPreprocessor::Run(LinearProgram* linear_program) { + SCOPED_INSTRUCTION_COUNT(time_limit_); + RETURN_VALUE_IF_NULL(linear_program, false); + const Fractional tolerance = integer_solution_tolerance_; + + // Starts by adding all the row in the 'to_process' queue. + StrictITIVector in_queue(linear_program->num_constraints(), + false); + std::deque to_process; + for (RowIndex row(0); row < linear_program->num_constraints(); ++row) { + to_process.push_back(row); + in_queue[row] = true; + } + + // This preprocessor will need to access the constraints row by row. + const SparseMatrix& transpose = linear_program->GetTransposeSparseMatrix(); + + // Now process all the rows until none are left, or a limit on the number of + // processed rows is reached. The limit is mainly here to prevent infinite + // loops on corner cases problems. It should not be reached often in practice. + const int kMaxNumberOfProcessedRows = + linear_program->num_constraints().value() * 10; + for (int i = 0; i < kMaxNumberOfProcessedRows && !to_process.empty(); ++i) { + const RowIndex row = to_process.front(); + in_queue[row] = false; + to_process.pop_front(); + + // For each variable of a constraint on n variables, we want the bound + // implied by the (n - 1) other variables and the constraint bounds. We use + // two handy utility classes that allow us to do that efficiently while + // dealing properly with infinite bounds. + SumWithNegativeInfiniteAndOneMissing lb_sum; + SumWithPositiveInfiniteAndOneMissing ub_sum; + + // Initialize the sums. + bool skip = false; + for (const SparseColumn::Entry e : transpose.column(RowToColIndex(row))) { + const ColIndex col = RowToColIndex(e.row()); + Fractional entry_lb = + e.coefficient() * linear_program->variable_lower_bounds()[col]; + Fractional entry_ub = + e.coefficient() * linear_program->variable_upper_bounds()[col]; + if (e.coefficient() < 0.0) std::swap(entry_lb, entry_ub); + if (entry_lb == kInfinity || entry_ub == -kInfinity) { + // TODO(user): our SumWithOneMissing does not deal well with infinity of + // the wrong sign. For now when this happen we skip this constraint. + // Note however than the other implied bounds could still be used. + skip = true; + break; + } + lb_sum.Add(entry_lb); + ub_sum.Add(entry_ub); + } + if (skip) continue; + + // The inequality + // constraint_lb <= sum(entries) <= constraint_ub + // can be rewritten as: + // sum(entries) + (-activity) = 0, + // where (-activity) has bounds [-constraint_ub, -constraint_lb]. + // We use this latter convention to simplify our code. + lb_sum.Add(-linear_program->constraint_upper_bounds()[row]); + ub_sum.Add(-linear_program->constraint_lower_bounds()[row]); + + // Process the variables one by one and check if the implied bounds are + // more restrictive. + for (const SparseColumn::Entry e : transpose.column(RowToColIndex(row))) { + const ColIndex col = RowToColIndex(e.row()); + const Fractional coeff = e.coefficient(); + const Fractional var_lb = linear_program->variable_lower_bounds()[col]; + const Fractional var_ub = linear_program->variable_upper_bounds()[col]; + Fractional entry_lb = coeff * var_lb; + Fractional entry_ub = coeff * var_ub; + if (coeff < 0.0) std::swap(entry_lb, entry_ub); + + // If X is the variable with index col and Y the sum of all the other + // variables and of (-activity), then coeff * X + Y = 0. Since Y's bounds + // are [lb_sum without X, ub_sum without X], it is easy to derive the + // implied bounds on X. + Fractional implied_lb = -ub_sum.SumWithout(entry_ub) / coeff; + Fractional implied_ub = -lb_sum.SumWithout(entry_lb) / coeff; + if (coeff < 0.0) std::swap(implied_lb, implied_ub); + + // If the variable is integer, make the implied bounds integer. + if (linear_program->IsVariableInteger(col)) { + implied_lb = std::ceil(implied_lb - tolerance); + implied_ub = std::floor(implied_ub + tolerance); + } + + // more restrictive? If yes, sets the bounds, and add all the impacted + // row back into to_process if they are not already there. + if (implied_lb > var_lb || implied_ub < var_ub) { + Fractional new_lb = std::max(implied_lb, var_lb); + Fractional new_ub = std::min(implied_ub, var_ub); + if (new_lb > new_ub) { + // TODO(user): Investigate what tolerance we should use here. + if (new_lb - tolerance > new_ub) { + status_ = ProblemStatus::PRIMAL_INFEASIBLE; + return false; + } else { + // We choose the nearest integer for an integer variable, or the + // middle value for a non-integer one. + if (linear_program->IsVariableInteger(col)) { + new_lb = new_ub = round(new_lb); + } else { + new_lb = new_ub = (new_lb + new_ub) / 2.0; + } + } + } + + // This extra test avoids reprocessing many rows for no reason. + // It can be false if we run into the case new_lb > new_ub above. + if (new_ub != var_ub || new_lb != var_lb) { + linear_program->SetVariableBounds(col, new_lb, new_ub); + for (SparseColumn::Entry e : linear_program->GetSparseColumn(col)) { + if (!in_queue[e.row()]) { + to_process.push_back(e.row()); + in_queue[e.row()] = true; + } + } + } + } + } + } + if (!to_process.empty()) { + LOG_FIRST_N(WARNING, 10) + << "Propagation limit reached in the BoundPropagationPreprocessor, " + << "maybe the limit should be increased."; + } + DCHECK(linear_program->BoundsOfIntegerVariablesAreInteger( + integer_solution_tolerance_)); + DCHECK(linear_program->BoundsOfIntegerConstraintsAreInteger( + integer_solution_tolerance_)); + return false; +} + +// -------------------------------------------------------- +// ImpliedIntegerPreprocessor +// -------------------------------------------------------- +bool ImpliedIntegerPreprocessor::Run(LinearProgram* linear_program) { + SCOPED_INSTRUCTION_COUNT(time_limit_); + RETURN_VALUE_IF_NULL(linear_program, false); + int64_t num_implied_integer_variables = 0; + const Fractional tolerance = integer_solution_tolerance_; + for (ColIndex col(0); col < linear_program->num_variables(); ++col) { + DCHECK_EQ(linear_program->GetFirstSlackVariable(), glop::kInvalidCol); + + // Skip the integer variables. + if (linear_program->GetVariableType(col) != + LinearProgram::VariableType::CONTINUOUS) { + continue; + } + + const bool is_implied_integer = + VariableOccursInAtLeastOneEqualityConstraint(*linear_program, col) + ? AnyEqualityConstraintImpliesIntegrality(*linear_program, col) + : AllInequalityConstraintsImplyIntegrality(*linear_program, col); + + if (is_implied_integer) { + linear_program->SetVariableType( + col, LinearProgram::VariableType::IMPLIED_INTEGER); + num_implied_integer_variables++; + VLOG(2) << "Marked col " << col << " implied integer."; + + // We need to tighten its bounds if they are not integer, otherwise + // other preprocessor complains. + const Fractional lb = + std::ceil(linear_program->variable_lower_bounds()[col] - tolerance); + const Fractional ub = + std::floor(linear_program->variable_upper_bounds()[col] + tolerance); + if (!AreBoundsValid(lb, ub)) { + status_ = ProblemStatus::PRIMAL_INFEASIBLE; + return false; + } + linear_program->SetVariableBounds(col, lb, ub); + } + } + VLOG(2) << "ImpliedIntegerPreprocessor detected " + << num_implied_integer_variables << " implied integer variables."; + + DCHECK(linear_program->BoundsOfIntegerVariablesAreInteger( + integer_solution_tolerance_)); + + // TODO(user): Because this presolve step detects new integer variables and + // does not tighten the bounds of a constraint if all its variables become + // integer, this invariant is currently not enforced: + // DCHECK(linear_program->BoundsOfIntegerConstraintsAreInteger( + // integer_solution_tolerance_)); + + return false; // Does not require postsolve. +} + +bool ImpliedIntegerPreprocessor::AnyEqualityConstraintImpliesIntegrality( + const LinearProgram& linear_program, ColIndex variable) { + for (const SparseColumn::Entry entry : + linear_program.GetSparseColumn(variable)) { + // Process only equality constraints. + if (linear_program.constraint_upper_bounds()[entry.row()] == + linear_program.constraint_lower_bounds()[entry.row()]) { + if (ConstraintSupportsImpliedIntegrality(linear_program, variable, + entry.row())) { + return true; + } + } + } + return false; +} + +bool ImpliedIntegerPreprocessor::AllInequalityConstraintsImplyIntegrality( + const LinearProgram& linear_program, ColIndex variable) { + // Check variable bounds. + Fractional lower_bound = linear_program.variable_lower_bounds()[variable]; + Fractional upper_bound = linear_program.variable_upper_bounds()[variable]; + if (!IsIntegerWithinTolerance(lower_bound, integer_solution_tolerance_) || + !IsIntegerWithinTolerance(upper_bound, integer_solution_tolerance_)) { + // The bounds are not integer. + // We cannot deduce anything if the variable as an objective. + // + // TODO(user): Actually we can if the bound that minimize the cost is + // integer but not the other. Improve the code. + if (linear_program.objective_coefficients()[variable] != 0.0) return false; + + // No objective. If the variable domain contains an integer point, then + // there is a chance for this variable to be integer. This is because if the + // condition on the constraints below is true, then the constraints will + // always imply the variable to be inside a [integer_lb, integer_ub] domain. + // And if the intersection of this domain with the variable domain is + // non-empty, then it contains one or more integer points and we can always + // set the variable to one of these integer values. + if (std::ceil(lower_bound) > std::floor(upper_bound)) return false; + } + + // Primal detection for each constraint containing variable. + for (const SparseColumn::Entry entry : + linear_program.GetSparseColumn(variable)) { + if (!ConstraintSupportsImpliedIntegrality(linear_program, variable, + entry.row())) { + return false; + } + } + return true; +} + +bool ImpliedIntegerPreprocessor::ConstraintSupportsImpliedIntegrality( + const LinearProgram& linear_program, ColIndex variable, RowIndex row) { + const SparseMatrix& coefficients_transpose = + linear_program.GetTransposeSparseMatrix(); + const Fractional variable_coefficient = coefficients_transpose.LookUpValue( + ColToRowIndex(variable), RowToColIndex(row)); + + for (const SparseColumn::Entry entry : + coefficients_transpose.column(RowToColIndex(row))) { + const ColIndex col = RowToColIndex(entry.row()); + if (col == variable) continue; + + // Check if the variables in the row are all integers. + if (!linear_program.IsVariableInteger(col)) { + return false; + } + + // Check if the coefficient ratios are all integers. + const Fractional coefficient_ratio = + entry.coefficient() / variable_coefficient; + if (!IsIntegerWithinTolerance(coefficient_ratio, + integer_solution_tolerance_)) { + return false; + } + } + + // Check if the constraint bound ratios are integers. + // Note that we ignore infinities. + if (linear_program.constraint_lower_bounds()[row] != -kInfinity) { + const Fractional constraint_lower_bound_ratio = + linear_program.constraint_lower_bounds()[row] / variable_coefficient; + if (!IsIntegerWithinTolerance(constraint_lower_bound_ratio, + integer_solution_tolerance_)) { + return false; + } + } + if (linear_program.constraint_upper_bounds()[row] != kInfinity) { + const Fractional constraint_upper_bound_ratio = + linear_program.constraint_upper_bounds()[row] / variable_coefficient; + if (!IsIntegerWithinTolerance(constraint_upper_bound_ratio, + integer_solution_tolerance_)) { + return false; + } + } + return true; +} + +bool ImpliedIntegerPreprocessor::VariableOccursInAtLeastOneEqualityConstraint( + const LinearProgram& linear_program, ColIndex variable) { + for (const SparseColumn::Entry entry : + linear_program.GetSparseColumn(variable)) { + // Check if the constraint is an equality. + if (linear_program.constraint_upper_bounds()[entry.row()] == + linear_program.constraint_lower_bounds()[entry.row()]) { + return true; + } + } + return false; +} + +// -------------------------------------------------------- +// ReduceCostOverExclusiveOrConstraintPreprocessor +// -------------------------------------------------------- + +bool ReduceCostOverExclusiveOrConstraintPreprocessor::Run( + LinearProgram* linear_program) { + SCOPED_INSTRUCTION_COUNT(time_limit_); + RETURN_VALUE_IF_NULL(linear_program, false); + const RowIndex num_constraints = linear_program->num_constraints(); + const SparseMatrix& transpose = linear_program->GetTransposeSparseMatrix(); + for (RowIndex row(0); row < num_constraints; ++row) { + if (linear_program->constraint_lower_bounds()[row] != 1.0) continue; + if (linear_program->constraint_upper_bounds()[row] != 1.0) continue; + Fractional min_cost = std::numeric_limits::infinity(); + bool constraint_is_exclusive_or = true; + for (const SparseColumn::Entry e : transpose.column(RowToColIndex(row))) { + const ColIndex var = RowToColIndex(e.row()); + if (!linear_program->IsVariableInteger(var) || + linear_program->variable_lower_bounds()[var] != 0.0 || + linear_program->variable_upper_bounds()[var] != 1.0 || + e.coefficient() != 1.0) { + constraint_is_exclusive_or = false; + break; + } + min_cost = + std::min(min_cost, linear_program->objective_coefficients()[var]); + } + if (constraint_is_exclusive_or && min_cost > 0.0 && + glop::IsFinite(min_cost)) { + for (const SparseColumn::Entry e : transpose.column(RowToColIndex(row))) { + const ColIndex var = RowToColIndex(e.row()); + const Fractional cost = linear_program->objective_coefficients()[var]; + linear_program->SetObjectiveCoefficient(var, cost - min_cost); + } + linear_program->SetObjectiveOffset(linear_program->objective_offset() + + min_cost); + } + } + return false; +} + +} // namespace operations_research diff --git a/ortools/linear_solver/proto_solver/preprocessor.h b/ortools/linear_solver/proto_solver/preprocessor.h new file mode 100644 index 0000000000..fcae6cc025 --- /dev/null +++ b/ortools/linear_solver/proto_solver/preprocessor.h @@ -0,0 +1,198 @@ +// 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_PREPROCESSOR_H_ +#define OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_PREPROCESSOR_H_ + +#include "ortools/glop/preprocessor.h" +#include "ortools/lp_data/lp_data.h" +#include "ortools/lp_data/lp_types.h" + +namespace operations_research { + +// -------------------------------------------------------- +// IntegerBoundsPreprocessor +// -------------------------------------------------------- + +// Makes the bounds of integer variables integer. Makes the bounds of +// constraints involving only integer variables with integer coefficients +// integer. +class IntegerBoundsPreprocessor : public glop::Preprocessor { + public: + IntegerBoundsPreprocessor(const glop::GlopParameters* parameters, + glop::Fractional integer_solution_tolerance) + : glop::Preprocessor(parameters), + integer_solution_tolerance_(integer_solution_tolerance) {} + + IntegerBoundsPreprocessor(const IntegerBoundsPreprocessor&) = delete; + IntegerBoundsPreprocessor& operator=(const IntegerBoundsPreprocessor&) = + delete; + ~IntegerBoundsPreprocessor() override = default; + + bool Run(glop::LinearProgram* linear_program) override; + void RecoverSolution(glop::ProblemSolution* /*solution*/) const override {} + + private: + const glop::Fractional integer_solution_tolerance_; +}; + +// -------------------------------------------------------- +// BoundPropagationPreprocessor +// -------------------------------------------------------- + +// It is possible to compute "implied" bounds on a variable from the bounds of +// all the other variables and the constraints in which this variable take +// place. These "implied" bounds can be used to restrict the variable bounds. +// This preprocessor just does that until no more bounds can be propagated or +// a given limit on the number of propagations is reached. +// +// Note(user): In particular, this preprocessor will remove any singleton row. +// +// Note(user): This seems like a general LP preprocessor but it is really +// difficult to postsolve it correctly in the LP context when one wants to have +// a basic optimal solution at the end. By contrast, in Mip context one is happy +// with any form of an optimal solution at the end, thus restoring the full +// solution is trivial. Consequently, bound propagation is implemented as a mip +// preprocessor. +class BoundPropagationPreprocessor : public glop::Preprocessor { + public: + BoundPropagationPreprocessor(const glop::GlopParameters* parameters, + glop::Fractional integer_solution_tolerance) + : glop::Preprocessor(parameters), + integer_solution_tolerance_(integer_solution_tolerance) {} + + BoundPropagationPreprocessor(const BoundPropagationPreprocessor&) = delete; + BoundPropagationPreprocessor& operator=(const BoundPropagationPreprocessor&) = + delete; + ~BoundPropagationPreprocessor() override = default; + + bool Run(glop::LinearProgram* linear_program) override; + void RecoverSolution(glop::ProblemSolution* /*solution*/) const override {} + + private: + const glop::Fractional integer_solution_tolerance_; +}; + +// -------------------------------------------------------- +// ImpliedIntegerPreprocessor +// -------------------------------------------------------- + +// In this preprocessor, we find continuous variables which can only take +// integer values and mark them as integer variables. +// +// There are two methods for detecting implied integer variables: 1) primal +// and 2) dual detection. If the variable appears in at least one equality +// constraint then we use primal detection otherwise we use dual detection. +class ImpliedIntegerPreprocessor : public glop::Preprocessor { + public: + explicit ImpliedIntegerPreprocessor( + const glop::GlopParameters* parameters, + glop::Fractional integer_solution_tolerance) + : glop::Preprocessor(parameters), + integer_solution_tolerance_(integer_solution_tolerance) {} + + ImpliedIntegerPreprocessor(const ImpliedIntegerPreprocessor&) = delete; + ImpliedIntegerPreprocessor& operator=(const ImpliedIntegerPreprocessor&) = + delete; + ~ImpliedIntegerPreprocessor() override = default; + + // TODO(user): When some variable are detected to be implied integer, other + // can in turn be detected as such. Change the code to reach a fixed point. + // Calling this multiple time has a similar effect, but is a lot less + // efficient and can require O(num_variables) calls to reach the fix point. + bool Run(glop::LinearProgram* linear_program) override; + void RecoverSolution(glop::ProblemSolution* /*solution*/) const override {} + + private: + // Returns true if the given variable is implied integer. This method is used + // for continuous variables appearing in at least one equality constraint. + // This is sometimes called "primal" detection in the literature. + // + // For each equality constraint s in which the given continuous variable x_j + // appears, this method checks the primal detection criteria by using + // ConstraintSupportsImpliedIntegrality() method. + bool AnyEqualityConstraintImpliesIntegrality( + const glop::LinearProgram& linear_program, glop::ColIndex variable); + + // Returns true if given variable is implied integer variable. This method is + // used for continuous variables for which primal detection is not applicable + // i.e. all constraints containing the given variable are inequalities. This + // is sometimes called "dual" detection in the literature. + // + // This method checks the following for the givan continuous variable x_j. + // a) The lower and upper bound of x_j are integers or the variable has no + // cost and its domain contains an integer point. + // b) For all constraint containing x_j, when treated as equality under primal + // detection, implies x_j as integer variable. + // If both conditions are satisfied then the variable x_j is implied integer + // variable. + bool AllInequalityConstraintsImplyIntegrality( + const glop::LinearProgram& linear_program, glop::ColIndex variable); + + // Returns true if the following conditions are satisfied. + // + // Let the constraint be lb_s <= \sum_{i=1..n}(a_si*x_i) + a_sj*x_j <= ub_s + // a) lb_s / a_sj and ub_s / a_sj are integers. + // b) a_si / a_sj is integer for all i. + // c) x_i are all integer variables. + bool ConstraintSupportsImpliedIntegrality( + const glop::LinearProgram& linear_program, glop::ColIndex variable, + glop::RowIndex row); + + // Returns true if the variable occurs in at least one equality constraint. + bool VariableOccursInAtLeastOneEqualityConstraint( + const glop::LinearProgram& linear_program, glop::ColIndex variable); + + private: + const glop::Fractional integer_solution_tolerance_; +}; + +// -------------------------------------------------------- +// ReduceCostOverExclusiveOrConstraintPreprocessor +// -------------------------------------------------------- + +// For an "exclusive or" constraint (sum Boolean = 1), if all the costs of the +// Boolean variables are positive, then we can subtract the cost of the one +// with minimum cost from the cost of all the others. We can do that for all +// such constraints one by one. +// +// ex: if x,y,z are Boolean variables with respective cost 1,2,1 and x+y+z=1, +// then we can change their costs to 0,1,0 and add 1 to the objective offset +// without changing the cost of any feasible solution. +// +// This seems pretty trivial, but can have a big impact depending on the +// technique we use to solve the MIP. It also makes the objective sparser which +// can only be a good thing. +// +// TODO(user): In more generality, in presence of an exclusive or constraint we +// can shift the cost by any value (even negative), so it may be good to +// maximize the number of coefficients at zero. To investigate. +class ReduceCostOverExclusiveOrConstraintPreprocessor + : public glop::Preprocessor { + public: + explicit ReduceCostOverExclusiveOrConstraintPreprocessor( + const glop::GlopParameters* mip_parameters) + : glop::Preprocessor(mip_parameters) {} + ReduceCostOverExclusiveOrConstraintPreprocessor( + const ReduceCostOverExclusiveOrConstraintPreprocessor&) = delete; + ReduceCostOverExclusiveOrConstraintPreprocessor& operator=( + const ReduceCostOverExclusiveOrConstraintPreprocessor&) = delete; + ~ReduceCostOverExclusiveOrConstraintPreprocessor() override = default; + + bool Run(glop::LinearProgram* linear_program) override; + void RecoverSolution(glop::ProblemSolution* /*solution*/) const override {} +}; + +} // namespace operations_research + +#endif // OR_TOOLS_LINEAR_SOLVER_PROTO_SOLVER_PREPROCESSOR_H_ diff --git a/ortools/linear_solver/proto_solver/sat_solver_utils.cc b/ortools/linear_solver/proto_solver/sat_solver_utils.cc index b986539ee9..5aabb5652e 100644 --- a/ortools/linear_solver/proto_solver/sat_solver_utils.cc +++ b/ortools/linear_solver/proto_solver/sat_solver_utils.cc @@ -21,6 +21,7 @@ #include "absl/log/check.h" #include "ortools/glop/parameters.pb.h" #include "ortools/glop/preprocessor.h" +#include "ortools/linear_solver/proto_solver/preprocessor.h" #include "ortools/lp_data/lp_data.h" #include "ortools/lp_data/lp_types.h" #include "ortools/lp_data/proto_utils.h" @@ -29,9 +30,10 @@ namespace operations_research { -#define ADD_LP_PREPROCESSOR(name) \ - names.push_back(#name); \ - lp_preprocessors.push_back(std::make_unique(&glop_params)); +#define ADD_LP_PREPROCESSOR(name, ...) \ + names.push_back(#name); \ + lp_preprocessors.push_back( \ + std::make_unique(&glop_params __VA_OPT__(, ) __VA_ARGS__)); glop::ProblemStatus ApplyMipPresolveSteps( const glop::GlopParameters& glop_params, MPModelProto* model, @@ -60,13 +62,13 @@ glop::ProblemStatus ApplyMipPresolveSteps( // These presolve might change the problem size. // // TODO(user): transform the hint instead of disabling presolve. + std::vector names; + std::vector> lp_preprocessors; + const std::string header = + "Running basic LP presolve, initial problem dimensions: "; if (!hint_is_present) { - const std::string header = - "Running basic LP presolve, initial problem dimensions: "; SOLVER_LOG(logger, ""); SOLVER_LOG(logger, header, lp.GetDimensionString()); - std::vector names; - std::vector> lp_preprocessors; ADD_LP_PREPROCESSOR(glop::FixedVariablePreprocessor); ADD_LP_PREPROCESSOR(glop::SingletonPreprocessor); ADD_LP_PREPROCESSOR(glop::ForcingAndImpliedFreeConstraintPreprocessor); @@ -77,19 +79,30 @@ glop::ProblemStatus ApplyMipPresolveSteps( // for the conversion, it is better to have tight bounds even if the bound // propagator is supposed to undo what this presolve would have done. ADD_LP_PREPROCESSOR(glop::UnconstrainedVariablePreprocessor); + } - for (int i = 0; i < lp_preprocessors.size(); ++i) { - if (time_limit->LimitReached()) break; - auto& preprocessor = lp_preprocessors[i]; - preprocessor->SetTimeLimit(time_limit.get()); - preprocessor->UseInMipContext(); - const bool need_postsolve = preprocessor->Run(&lp); - names[i].resize(header.size(), ' '); // padding. - SOLVER_LOG(logger, names[i], lp.GetDimensionString()); - const glop::ProblemStatus status = preprocessor->status(); - if (status != glop::ProblemStatus::INIT) return status; - if (need_postsolve) for_postsolve->push_back(std::move(preprocessor)); - } + // These preprocessors do not need postsolve. + ADD_LP_PREPROCESSOR(IntegerBoundsPreprocessor, 1e-6); + ADD_LP_PREPROCESSOR(BoundPropagationPreprocessor, 1e-6); + ADD_LP_PREPROCESSOR(ImpliedIntegerPreprocessor, 1e-6); + + // We need to re-run this after the ImpliedIntegerPreprocessor because the + // latter does not round the bounds of the constraints involving only + // integer variables and coefficients. + ADD_LP_PREPROCESSOR(IntegerBoundsPreprocessor, 1e-6); + ADD_LP_PREPROCESSOR(ReduceCostOverExclusiveOrConstraintPreprocessor); + + for (int i = 0; i < lp_preprocessors.size(); ++i) { + if (time_limit->LimitReached()) break; + auto& preprocessor = lp_preprocessors[i]; + preprocessor->SetTimeLimit(time_limit.get()); + preprocessor->UseInMipContext(); + const bool need_postsolve = preprocessor->Run(&lp); + names[i].resize(header.size(), ' '); // padding. + SOLVER_LOG(logger, names[i], lp.GetDimensionString()); + const glop::ProblemStatus status = preprocessor->status(); + if (status != glop::ProblemStatus::INIT) return status; + if (need_postsolve) for_postsolve->push_back(std::move(preprocessor)); } // Finally, we make sure all domains contain zero. diff --git a/ortools/linear_solver/python/model_builder_helper.cc b/ortools/linear_solver/python/model_builder_helper.cc index 8036828159..2855f6b2cb 100644 --- a/ortools/linear_solver/python/model_builder_helper.cc +++ b/ortools/linear_solver/python/model_builder_helper.cc @@ -586,8 +586,7 @@ PYBIND11_MODULE(model_builder_helper, m) { absl::StrCat("Evaluating a BoundedLinearExpression '", self.ToString(), "'instance as a Boolean is " - "not supported.") - .c_str()); + "not supported.")); return false; }) .def("__str__", &BoundedLinearExpression::ToString) diff --git a/ortools/lp_data/BUILD.bazel b/ortools/lp_data/BUILD.bazel index 6e3ad9bd97..619de54c9a 100644 --- a/ortools/lp_data/BUILD.bazel +++ b/ortools/lp_data/BUILD.bazel @@ -254,17 +254,6 @@ cc_library( ], ) -#cc_library( -# name = "lp_constraint_classifier", -# srcs = ["lp_constraint_classifier.cc"], -# hdrs = ["lp_constraint_classifier.h"], -# copts = SAFE_FP_CODE, -# deps = [ -# ":lp_data", -# "//ortools/util:fp_utils", -# ], -#) - cc_library( name = "lp_print_utils", srcs = ["lp_print_utils.cc"], diff --git a/ortools/lp_data/README.md b/ortools/lp_data/README.md index e69de29bb2..52d4764280 100644 --- a/ortools/lp_data/README.md +++ b/ortools/lp_data/README.md @@ -0,0 +1,86 @@ +# LP Data + +This directory contains a rich collection of C++ libraries for handling Linear +Programming (LP) data structures. + +It provides core components for representing, manipulating, and solving linear +programs, with a focus on efficient handling of sparse data and various utility +functions for pre-processing and analysis. + +## Core Data Structures + +This set of libraries provides the fundamental building blocks for representing +and working with linear programming problems. + +* [`lp_types.h`][lp_types_h]: Defines common types and constants used throughout + the linear programming solver. +* [`lp_data.h`][lp_data_h]: Provides the main `LinearProgram` class for storing + the complete data of a linear program, including the objective function, + constraint matrix, and variable bounds. +* [`lp_utils.h`][lp_utils_h]: Contains basic utility functions for operations on + fractional numbers and row/column vectors. + +## Sparse Data Representation + +Given that large-scale linear programs are often sparse, this directory offers a +suite of libraries for efficient sparse data handling. + +* [`sparse.h`][sparse_h]: Implements data structures for sparse matrices, based + on well-established references in the field of direct methods for sparse + matrices. +* [`sparse_vector.h`][sparse_vector_h]: Provides classes to represent sparse + vectors efficiently. +* [`sparse_column.h`][sparse_column_h] & [`sparse_row.h`][sparse_row_h]: + Specializations of sparse vectors for column-oriented and row-oriented matrix + storage schemes. +* [`scattered_vector.h`][scattered_vector_h]: Implements vectors that offer a + sparse interface to what is internally a dense storage, which can be useful + for certain computations. + +## LP Solvers and Utilities + +A collection of tools for preprocessing, analyzing, and manipulating linear +programs. + +* [`matrix_scaler.h`][matrix_scaler_h]: Provides the `SparseMatrixScaler` class, + which scales a `SparseMatrix` to improve numerical stability during the + solving process. +* [`lp_decomposer.h`][lp_decomposer_h]: Implements a tool to decompose a large + `LinearProgram` into several smaller, independent subproblems by identifying + disconnected components in the constraint matrix. +* [`permutation.h`][permutation_h]: Contains utilities for handling row and + column permutations on LP data structures. + +## Parsers and I/O Utilities + +This group of libraries handles reading and writing LP data in various formats. + +* [`lp_parser.h`][lp_parser_h]: A simple parser for creating a linear program + from a string representation. +* [`mps_reader.h`][mps_reader_h]: A reader for the industry-standard MPS file + format for mathematical programming problems. +* [`sol_reader.h`][sol_reader_h]: A reader for .sol files, which are used to + specify solution values for a given model. +* [`proto_utils.h`][proto_utils_h]: Provides utilities to convert + `LinearProgram` objects to and from the MPModelProto protobuf format. +* [`lp_print_utils.h`][lp_print_utils_h]: Contains utilities to display linear + expressions in a human-readable way, including rational approximations. + + + +[lp_types_h]: ../lp_data/lp_types.h +[lp_data_h]: ../lp_data/lp_data.h +[lp_utils_h]: ../lp_data/lp_utils.h +[sparse_h]: ../lp_data/sparse.h +[sparse_vector_h]: ../lp_data/sparse_vector.h +[sparse_column_h]: ../lp_data/sparse_column.h +[sparse_row_h]: ../lp_data/sparse_row.h +[scattered_vector_h]: ../lp_data/scattered_vector.h +[matrix_scaler_h]: ../lp_data/matrix_scaler.h +[lp_decomposer_h]: ../lp_data/lp_decomposer.h +[permutation_h]: ../lp_data/permutation.h +[lp_parser_h]: ../lp_data/lp_parser.h +[mps_reader_h]: ../lp_data/mps_reader.h +[sol_reader_h]: ../lp_data/sol_reader.h +[proto_utils_h]: ../lp_data/proto_utils.h +[lp_print_utils_h]: ../lp_data/lp_print_utils.h diff --git a/ortools/lp_data/lp_data.h b/ortools/lp_data/lp_data.h index 236423daa1..478a1a35be 100644 --- a/ortools/lp_data/lp_data.h +++ b/ortools/lp_data/lp_data.h @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Storage classes for Linear Programs. // // LinearProgram stores the complete data for a Linear Program: diff --git a/ortools/lp_data/sparse.h b/ortools/lp_data/sparse.h index fecc42a79c..cc856fc3a8 100644 --- a/ortools/lp_data/sparse.h +++ b/ortools/lp_data/sparse.h @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // The following are very good references for terminology, data structures, // and algorithms: // diff --git a/ortools/math_opt/cpp/solver_resources.cc b/ortools/math_opt/cpp/solver_resources.cc index 70ee369993..db6dee14f1 100644 --- a/ortools/math_opt/cpp/solver_resources.cc +++ b/ortools/math_opt/cpp/solver_resources.cc @@ -13,6 +13,7 @@ #include "ortools/math_opt/cpp/solver_resources.h" +#include #include #include @@ -23,6 +24,11 @@ namespace operations_research::math_opt { +std::ostream& operator<<(std::ostream& out, const SolverResources& resources) { + out << '{' << AbslUnparseFlag(resources) << '}'; + return out; +} + SolverResourcesProto SolverResources::Proto() const { SolverResourcesProto ret; if (cpu.has_value()) { diff --git a/ortools/math_opt/cpp/solver_resources.h b/ortools/math_opt/cpp/solver_resources.h index ffccb37db6..54ca852a84 100644 --- a/ortools/math_opt/cpp/solver_resources.h +++ b/ortools/math_opt/cpp/solver_resources.h @@ -18,6 +18,7 @@ #define OR_TOOLS_MATH_OPT_CPP_SOLVER_RESOURCES_H_ #include +#include #include #include "absl/status/statusor.h" @@ -74,6 +75,8 @@ struct SolverResources { const SolverResourcesProto& proto); }; +std::ostream& operator<<(std::ostream& out, const SolverResources& resources); + bool AbslParseFlag(absl::string_view text, SolverResources* solver_resources, std::string* error); diff --git a/ortools/math_opt/labs/BUILD.bazel b/ortools/math_opt/labs/BUILD.bazel index 5fd8f93eab..a786ffdb3a 100644 --- a/ortools/math_opt/labs/BUILD.bazel +++ b/ortools/math_opt/labs/BUILD.bazel @@ -88,3 +88,17 @@ cc_library( ], alwayslink = 1, ) + +cc_library( + name = "scaler_util", + srcs = ["scaler_util.cc"], + hdrs = ["scaler_util.h"], + visibility = ["//visibility:public"], + deps = [ + "//ortools/base", + "//ortools/base:types", + "//ortools/util:fp_utils", + "@abseil-cpp//absl/log:check", + "@abseil-cpp//absl/strings:str_format", + ], +) diff --git a/ortools/math_opt/python/BUILD.bazel b/ortools/math_opt/python/BUILD.bazel index facc0b7e87..ce37a84b4d 100644 --- a/ortools/math_opt/python/BUILD.bazel +++ b/ortools/math_opt/python/BUILD.bazel @@ -51,66 +51,6 @@ py_library( deps = ["//ortools/math_opt/python/elemental"], ) -py_library( - name = "model_storage", - srcs = ["model_storage.py"], - visibility = ["//ortools/math_opt/python:__subpackages__"], - deps = [ - "//ortools/math_opt:model_py_pb2", - "//ortools/math_opt:model_update_py_pb2", - ], -) - -py_library( - name = "hash_model_storage", - srcs = ["hash_model_storage.py"], - deps = [ - ":model_storage", - "//ortools/math_opt:model_py_pb2", - "//ortools/math_opt:model_update_py_pb2", - "//ortools/math_opt:sparse_containers_py_pb2", - ], -) - -py_test( - name = "hash_model_storage_test", - size = "small", - srcs = ["hash_model_storage_test.py"], - deps = [ - ":hash_model_storage", - requirement("absl-py"), - ], -) - -py_test( - name = "model_storage_test", - size = "small", - srcs = ["model_storage_test.py"], - deps = [ - ":hash_model_storage", - ":model_storage", - requirement("absl-py"), - "//ortools/math_opt:model_py_pb2", - "//ortools/math_opt:sparse_containers_py_pb2", - "//ortools/math_opt/python/testing:compare_proto", - ], -) - -py_test( - name = "model_storage_update_test", - size = "small", - srcs = ["model_storage_update_test.py"], - deps = [ - ":hash_model_storage", - ":model_storage", - requirement("absl-py"), - "//ortools/math_opt:model_py_pb2", - "//ortools/math_opt:model_update_py_pb2", - "//ortools/math_opt:sparse_containers_py_pb2", - "//ortools/math_opt/python/testing:compare_proto", - ], -) - py_library( name = "model", srcs = ["model.py"], diff --git a/ortools/math_opt/python/hash_model_storage.py b/ortools/math_opt/python/hash_model_storage.py deleted file mode 100644 index 96cad3aa77..0000000000 --- a/ortools/math_opt/python/hash_model_storage.py +++ /dev/null @@ -1,843 +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. - -"""A minimal pure python implementation of model_storage.ModelStorage.""" - -from typing import Dict, Iterable, Iterator, Optional, Set, Tuple -import weakref - -from ortools.math_opt import model_pb2 -from ortools.math_opt import model_update_pb2 -from ortools.math_opt import sparse_containers_pb2 -from ortools.math_opt.python import model_storage - -_QuadraticKey = model_storage.QuadraticTermIdKey - - -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, - ) - _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), - ) - 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() - - -class _VariableStorage: - """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() - - -class _LinearConstraintStorage: - """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() - - -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 - ) - - -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 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 - - -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 - - -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) - - -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) diff --git a/ortools/math_opt/python/hash_model_storage_test.py b/ortools/math_opt/python/hash_model_storage_test.py deleted file mode 100644 index 7c5feb37b3..0000000000 --- a/ortools/math_opt/python/hash_model_storage_test.py +++ /dev/null @@ -1,31 +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. - -"""Tests for hash_model_storage that cannot be covered by model_storage_(update)_test.""" - -from absl.testing import absltest -from ortools.math_opt.python import hash_model_storage - - -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))) - - -if __name__ == "__main__": - absltest.main() diff --git a/ortools/math_opt/python/model_storage.py b/ortools/math_opt/python/model_storage.py deleted file mode 100644 index cf67f6a87d..0000000000 --- a/ortools/math_opt/python/model_storage.py +++ /dev/null @@ -1,441 +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. - -"""An interface for in memory storage of optimization problems.""" - -import abc -import dataclasses -from typing import Iterator, Optional, Type, TypeVar - -from ortools.math_opt import model_pb2 -from ortools.math_opt import model_update_pb2 - - -# TODO(b/231426528): remove __slots__ and set slots=True when Python 3.10 is -# available. -@dataclasses.dataclass(frozen=True) -class LinearConstraintMatrixIdEntry: - __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 - - -# 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. - - QuadraticTermIdKey.id1 <= QuadraticTermIdKey.id2. - """ - - __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) - - -# 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.""" - - __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 - - -class UsedUpdateTrackerAfterRemovalError(RuntimeError): - - 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.""" - - 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.""" - - 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). - """ - - @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 export_model(self) -> model_pb2.ModelProto: - pass - - @abc.abstractmethod - def add_update_tracker(self) -> StorageUpdateTracker: - """Creates a StorageUpdateTracker registered with self to view model changes.""" - pass - - @abc.abstractmethod - def remove_update_tracker(self, tracker: StorageUpdateTracker): - """Stops tracker from getting updates on model changes in self. - - An error will be raised if tracker is not a StorageUpdateTracker created by - this Model that has not previously been removed. - - Using an UpdateTracker (via checkpoint or export_update) after it has been - removed will result in an error. - - 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) -ModelStorageImplClass = Type[ModelStorageImpl] diff --git a/ortools/math_opt/python/model_storage_test.py b/ortools/math_opt/python/model_storage_test.py deleted file mode 100644 index 64590af629..0000000000 --- a/ortools/math_opt/python/model_storage_test.py +++ /dev/null @@ -1,941 +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. - -import math -from typing import Any, Callable - -from absl.testing import absltest -from absl.testing import parameterized -from ortools.math_opt import model_pb2 -from ortools.math_opt import sparse_containers_pb2 -from ortools.math_opt.python import hash_model_storage -from ortools.math_opt.python import model_storage -from ortools.math_opt.python.testing import compare_proto - -_StorageClass = model_storage.ModelStorageImplClass -_MatEntry = model_storage.LinearConstraintMatrixIdEntry -_ObjEntry = model_storage.LinearObjectiveEntry - - -@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"], - ), - 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] - ), - quadratic_coefficients=sparse_containers_pb2.SparseDoubleMatrixProto( - row_ids=[0, 0, 0], - column_ids=[0, 1, 2], - coefficients=[3.0, 4.0, 5.0], - ), - ), - 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] - ), - 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] - ), - ) - 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] - ), - 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()) - - -if __name__ == "__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 deleted file mode 100644 index 9419fc1e28..0000000000 --- a/ortools/math_opt/python/model_storage_update_test.py +++ /dev/null @@ -1,1175 +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. - -from absl.testing import absltest -from absl.testing import parameterized -from ortools.math_opt import model_pb2 -from ortools.math_opt import model_update_pb2 -from ortools.math_opt import sparse_containers_pb2 -from ortools.math_opt.python import hash_model_storage -from ortools.math_opt.python import model_storage -from ortools.math_opt.python.testing import compare_proto - -_StorageClass = model_storage.ModelStorageImplClass - -_ModelUpdateProto = model_update_pb2.ModelUpdateProto -_VariableUpdatesProto = model_update_pb2.VariableUpdatesProto -_LinearConstraintUpdatesProto = model_update_pb2.LinearConstraintUpdatesProto -_SparseDoubleVectorProto = sparse_containers_pb2.SparseDoubleVectorProto -_SparseBoolVectorProto = sparse_containers_pb2.SparseBoolVectorProto -_SparseDoubleMatrixProto = sparse_containers_pb2.SparseDoubleMatrixProto -_VariablesProto = model_pb2.VariablesProto -_LinearConstraintsProto = model_pb2.LinearConstraintsProto -_ObjectiveUpdatesProto = model_update_pb2.ObjectiveUpdatesProto - - -@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() - ) - - 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"], - ) - ) - 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"], - ) - ) - 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"], - ) - ) - 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"] - ) - ) - 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"] - ) - ) - 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_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]) - ) - ) - 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]) - ) - ) - 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]) - ) - ) - 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"], - ) - ), - 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_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] - ) - ) - ) - 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] - ), - 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_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(), - ) - - 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(), - ) - - 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(), - ) - - 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], - ) - ), - ), - 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( - 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] - ), - 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_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] - ), - ), - 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] - ), - ), - 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_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], - ), - ), - 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]) - ) - ), - 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() diff --git a/ortools/routing/parsers/solomon_parser_test.cc b/ortools/routing/parsers/solomon_parser_test.cc index 0791d364e1..42070a6b04 100644 --- a/ortools/routing/parsers/solomon_parser_test.cc +++ b/ortools/routing/parsers/solomon_parser_test.cc @@ -24,7 +24,7 @@ #define ROOT_DIR "_main/" ABSL_FLAG(std::string, solomon_test_archive, - "ortools/bench/solomon/" + "ortools/routing/benchmarks/solomon/" "testdata/solomon.zip", "Solomon: testing archive"); ABSL_FLAG(std::string, solomon_test_instance, "google2.txt", diff --git a/ortools/routing/samples/cvrp_disjoint_tw.cc b/ortools/routing/samples/cvrp_disjoint_tw.cc index 18e51bd1ec..b23e515acc 100644 --- a/ortools/routing/samples/cvrp_disjoint_tw.cc +++ b/ortools/routing/samples/cvrp_disjoint_tw.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Capacitated Vehicle Routing Problem with Disjoint Time Windows (and optional // orders). // A description of the problem can be found here: diff --git a/ortools/routing/samples/cvrptw.cc b/ortools/routing/samples/cvrptw.cc index b1ab211b2e..16aa4f01c7 100644 --- a/ortools/routing/samples/cvrptw.cc +++ b/ortools/routing/samples/cvrptw.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Capacitated Vehicle Routing Problem with Time Windows (and optional orders). // A description of the problem can be found here: // http://en.wikipedia.org/wiki/Vehicle_routing_problem. diff --git a/ortools/routing/samples/cvrptw_with_breaks.cc b/ortools/routing/samples/cvrptw_with_breaks.cc index 791be7a200..5ee85a3129 100644 --- a/ortools/routing/samples/cvrptw_with_breaks.cc +++ b/ortools/routing/samples/cvrptw_with_breaks.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Capacitated Vehicle Routing Problem with Time Windows and Breaks. // A description of the Capacitated Vehicle Routing Problem with Time Windows // can be found here: diff --git a/ortools/routing/samples/cvrptw_with_precedences.cc b/ortools/routing/samples/cvrptw_with_precedences.cc index bbf3f40d56..f001b217f8 100644 --- a/ortools/routing/samples/cvrptw_with_precedences.cc +++ b/ortools/routing/samples/cvrptw_with_precedences.cc @@ -11,7 +11,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// // Capacitated Vehicle Routing Problem with Time Windows (and optional orders). // A description of the problem can be found here: // http://en.wikipedia.org/wiki/Vehicle_routing_problem. diff --git a/ortools/sat/BUILD.bazel b/ortools/sat/BUILD.bazel index a67fc0cd6f..7c4a6e67e8 100644 --- a/ortools/sat/BUILD.bazel +++ b/ortools/sat/BUILD.bazel @@ -1500,6 +1500,7 @@ cc_library( "//ortools/util:bitset", "//ortools/util:integer_pq", "//ortools/util:strong_integers", + "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/log:check", "@abseil-cpp//absl/types:span", ], @@ -4279,6 +4280,7 @@ cc_library( "//ortools/util:running_stat", "//ortools/util:strong_integers", "//ortools/util:time_limit", + "@abseil-cpp//absl/algorithm:container", "@abseil-cpp//absl/base:core_headers", "@abseil-cpp//absl/container:flat_hash_map", "@abseil-cpp//absl/container:flat_hash_set", diff --git a/ortools/sat/cp_model_solver_helpers.cc b/ortools/sat/cp_model_solver_helpers.cc index 3e22dabbf5..e64e37ae16 100644 --- a/ortools/sat/cp_model_solver_helpers.cc +++ b/ortools/sat/cp_model_solver_helpers.cc @@ -863,6 +863,10 @@ void RegisterLinear2BoundsImport(SharedLinear2Bounds* shared_linear2_bounds, for (const auto& [proto_expr, bounds] : new_bounds) { // Lets create the corresponding LinearExpression2. LinearExpression2 expr; + if (!cp_model_mapping->IsInteger(proto_expr.vars[0]) || + !cp_model_mapping->IsInteger(proto_expr.vars[1])) { + continue; + } for (const int i : {0, 1}) { expr.vars[i] = cp_model_mapping->Integer(proto_expr.vars[i]); expr.coeffs[i] = proto_expr.coeffs[i]; diff --git a/ortools/sat/sat_solver.cc b/ortools/sat/sat_solver.cc index ef554832d4..c27cc650f5 100644 --- a/ortools/sat/sat_solver.cc +++ b/ortools/sat/sat_solver.cc @@ -1013,9 +1013,9 @@ SatSolver::Status SatSolver::EnqueueDecisionAndBacktrackOnConflict( bool SatSolver::EnqueueDecisionIfNotConflicting(Literal true_literal) { SCOPED_TIME_STAT(&stats_); + if (model_is_unsat_) return kUnsatTrailIndex; DCHECK(PropagationIsDone()); - if (model_is_unsat_) return kUnsatTrailIndex; const int current_level = CurrentDecisionLevel(); EnqueueNewDecision(true_literal); if (Propagate()) { diff --git a/ortools/sat/synchronization.cc b/ortools/sat/synchronization.cc index 18f37e7cfb..f09a824c4d 100644 --- a/ortools/sat/synchronization.cc +++ b/ortools/sat/synchronization.cc @@ -1559,7 +1559,7 @@ void SharedClausesManager::Synchronize() { void SharedLinear2Bounds::Add(int id, Key expr, IntegerValue lb, IntegerValue ub) { - DCHECK(expr.IsCanonicalized()); + DCHECK(expr.IsCanonicalized()) << expr; absl::MutexLock mutex_lock(&mutex_); auto [it, inserted] = shared_bounds_.insert({expr, {lb, ub}}); diff --git a/ortools/sat/synchronization.h b/ortools/sat/synchronization.h index a9cd377fdb..83085ff27e 100644 --- a/ortools/sat/synchronization.h +++ b/ortools/sat/synchronization.h @@ -913,7 +913,7 @@ class SharedLinear2Bounds { IntegerValue coeffs[2]; bool IsCanonicalized() { - return coeffs[0] > 0 && coeffs[1] != 0 && vars[0] < vars[1] && + return vars[0] >= 0 && vars[1] >= 0 && vars[0] < vars[1] && std::gcd(coeffs[0].value(), coeffs[1].value()) == 1; } @@ -927,6 +927,12 @@ class SharedLinear2Bounds { return H::combine(std::move(h), k.vars[0], k.vars[1], k.coeffs[0], k.coeffs[1]); } + + template + friend void AbslStringify(Sink& sink, const Key& k) { + absl::Format(&sink, "%d X%d + %d X%d", k.coeffs[0].value(), k.vars[0], + k.coeffs[1].value(), k.vars[1]); + } }; // Exports new bounds on the given expr (should be canonicalized).