Files
ortools-clone/ortools/sat/opb_reader.h

391 lines
13 KiB
C
Raw Normal View History

2025-01-10 11:35:44 +01:00
// Copyright 2010-2025 Google LLC
2013-12-12 14:43:04 +00:00
// 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.
2025-11-05 11:34:49 +01:00
#ifndef ORTOOLS_SAT_OPB_READER_H_
#define ORTOOLS_SAT_OPB_READER_H_
2013-12-12 14:43:04 +00:00
#include <algorithm>
2021-04-23 14:55:51 +02:00
#include <cstdint>
#include <limits>
2025-07-21 17:25:33 +02:00
#include <optional>
2013-12-12 14:43:04 +00:00
#include <string>
#include <utility>
2013-12-12 14:43:04 +00:00
#include <vector>
2025-04-16 11:09:15 +02:00
#include "absl/base/attributes.h"
#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
2023-05-24 11:42:11 +02:00
#include "absl/log/check.h"
#include "absl/log/log.h"
#include "absl/strings/numbers.h"
dotnet: Remove reference to dotnet release command - Currently not implemented... Add abseil patch - Add patches/absl-config.cmake Makefile: Add abseil-cpp on unix - Force abseil-cpp SHA1 to 45221cc note: Just before the PR #136 which break all CMake Makefile: Add abseil-cpp on windows - Force abseil-cpp SHA1 to 45221cc note: Just before the PR #136 which break all CMake CMake: Add abseil-cpp - Force abseil-cpp SHA1 to 45221cc note: Just before the PR #136 which break all CMake port to absl: C++ Part - Fix warning with the use of ABSL_MUST_USE_RESULT > The macro must appear as the very first part of a function declaration or definition: ... Note: past advice was to place the macro after the argument list. src: dependencies/sources/abseil-cpp-master/absl/base/attributes.h:418 - Rename enum after windows clash - Remove non compact table constraints - Change index type from int64 to int in routing library - Fix file_nonport compilation on windows - Fix another naming conflict with windows (NO_ERROR is a macro) - Cleanup hash containers; work on sat internals - Add optional_boolean sub-proto Sync cpp examples with internal code - reenable issue173 after reducing number of loops port to absl: Python Part - Add back cp_model.INT32_MIN|MAX for examples Update Python examples - Add random_tsp.py - Run words_square example - Run magic_square in python tests port to absl: Java Part - Fix compilation of the new routing parameters in java - Protect some code from SWIG parsing Update Java Examples port to absl: .Net Part Update .Net examples work on sat internals; Add C++ CP-SAT CpModelBuilder API; update sample code and recipes to use the new API; sync with internal code Remove VS 2015 in Appveyor-CI - abseil-cpp does not support VS 2015... improve tables upgrade C++ sat examples to use the new API; work on sat internals update license dates rewrite jobshop_ft06_distance.py to use the CP-SAT solver rename last example revert last commit more work on SAT internals fix
2018-10-31 16:18:18 +01:00
#include "absl/strings/str_split.h"
2025-06-02 14:25:50 +02:00
#include "absl/strings/string_view.h"
#include "absl/types/span.h"
#include "ortools/base/logging.h"
#include "ortools/base/stl_util.h"
#include "ortools/sat/cp_model.pb.h"
#include "ortools/sat/cp_model_utils.h"
2022-01-19 10:31:35 +01:00
#include "ortools/util/filelineiter.h"
2013-12-12 14:43:04 +00:00
namespace operations_research {
namespace sat {
// This class loads a file in pbo file format into a LinearBooleanProblem.
// The format is described here:
// http://www.cril.univ-artois.fr/PB24/format.pdf
2013-12-12 14:43:04 +00:00
class OpbReader {
2020-10-22 23:36:58 +02:00
public:
2023-05-24 11:42:11 +02:00
OpbReader() = default;
// This type is neither copyable nor movable.
OpbReader(const OpbReader&) = delete;
OpbReader& operator=(const OpbReader&) = delete;
2013-12-12 14:43:04 +00:00
// Returns the number of variables in the problem.
int num_variables() const { return num_variables_; }
2025-04-03 16:12:45 +02:00
// Returns true if the model is supported. A model is not supported if it
// contains an integer that does not fit in int64_t.
bool model_is_supported() const { return model_is_supported_; }
2013-12-12 14:43:04 +00:00
// Loads the given opb filename into the given problem.
// Returns true on success.
2025-04-16 11:09:15 +02:00
ABSL_MUST_USE_RESULT bool LoadAndValidate(const std::string& filename,
CpModelProto* model) {
model->Clear();
model->set_name(ExtractProblemName(filename));
2013-12-12 14:43:04 +00:00
num_variables_ = 0;
int num_lines = 0;
2025-04-03 16:12:45 +02:00
model_is_supported_ = true;
// Read constraints line by line (1 constraint per line).
// We process into a temporary structure to support non linear constraints
// and weighted constraints.
2020-10-29 14:25:39 +01:00
for (const std::string& line : FileLines(filename)) {
2013-12-12 14:43:04 +00:00
++num_lines;
ProcessNewLine(line);
// Check if the model is supported. It is not supported if one constant
2025-04-16 11:09:15 +02:00
// contains an integer that does not fit in an int64_t.
if (!model_is_supported_) return false;
2013-12-12 14:43:04 +00:00
}
if (num_lines == 0) {
LOG(ERROR) << "File '" << filename << "' is empty or can't be read.";
return false;
}
LOG(INFO) << "Read " << num_lines << " lines from " << filename;
LOG(INFO) << "#variables: " << num_variables_;
LOG(INFO) << "#constraints: " << constraints_.size();
LOG(INFO) << "#objective: " << objective_.size();
2025-07-21 17:25:33 +02:00
if (top_cost_.has_value()) LOG(INFO) << "top_cost: " << top_cost_.value();
const std::string error_message = ValidateModel();
if (!error_message.empty()) {
LOG(ERROR) << "Error while trying to parse '" << filename
<< "': " << error_message;
return false;
2013-12-12 14:43:04 +00:00
}
BuildModel(model);
2013-12-12 14:43:04 +00:00
return true;
}
2020-10-22 23:36:58 +02:00
private:
// A term is coeff * Product(literals).
// Note that it is okay to have duplicate literals here, we will just merge
// them. Having a literal and its negation will always result in a product of
// zero.
struct PbTerm {
int64_t coeff;
std::vector<int> literals; // CpModelProto literals
};
enum PbConstraintType {
UNDEFINED_OPERATION,
GE_OPERATION,
EQ_OPERATION,
};
struct PbConstraint {
std::vector<PbTerm> terms;
PbConstraintType type = UNDEFINED_OPERATION;
int64_t rhs = std::numeric_limits<int64_t>::min();
int64_t soft_cost = std::numeric_limits<int64_t>::max();
};
2025-04-03 16:12:45 +02:00
// Since the problem name is not stored in the opb format, we infer it from
2013-12-12 14:43:04 +00:00
// the file name.
2020-10-29 14:25:39 +01:00
static std::string ExtractProblemName(const std::string& filename) {
2021-01-14 10:48:19 +01:00
const int found = filename.find_last_of('/');
2014-01-08 12:01:58 +00:00
const std::string problem_name =
found != std::string::npos ? filename.substr(found + 1) : filename;
2013-12-12 14:43:04 +00:00
return problem_name;
}
void ProcessNewLine(const std::string& line) {
2014-05-21 12:56:57 +00:00
const std::vector<std::string> words =
absl::StrSplit(line, absl::ByAnyChar(" ;"), absl::SkipEmpty());
2025-07-21 17:25:33 +02:00
if (words.empty() || words[0].empty() || words[0][0] == '*') return;
if (words[0] == "soft:") {
if (words.size() == 1) return;
int64_t top_cost;
if (!ParseInt64Into(words[1], &top_cost)) return;
top_cost_ = top_cost;
2013-12-12 14:43:04 +00:00
return;
}
if (words[0] == "min:") {
for (int i = 1; i < words.size(); ++i) {
2020-10-29 14:25:39 +01:00
const std::string& word = words[i];
2020-10-22 23:36:58 +02:00
if (word.empty() || word[0] == ';') continue;
2013-12-12 14:43:04 +00:00
if (word[0] == 'x') {
2025-04-03 16:12:45 +02:00
const int index = ParseIndex(word.substr(1));
num_variables_ = std::max(num_variables_, index);
objective_.back().literals.push_back(
PbLiteralToCpModelLiteral(index));
} else if (word[0] == '~' && word[1] == 'x') {
2025-04-03 16:12:45 +02:00
const int index = ParseIndex(word.substr(2));
num_variables_ = std::max(num_variables_, index);
objective_.back().literals.push_back(
NegatedRef(PbLiteralToCpModelLiteral(index)));
2013-12-12 14:43:04 +00:00
} else {
2025-04-03 16:12:45 +02:00
// Note that coefficient always appear before the variable/variables.
PbTerm term;
if (!ParseInt64Into(word, &term.coeff)) return;
objective_.emplace_back(std::move(term));
2013-12-12 14:43:04 +00:00
}
}
// Normalize objective literals.
for (PbTerm& term : objective_) {
if (term.literals.size() <= 1) continue;
gtl::STLSortAndRemoveDuplicates(&term.literals);
CHECK_GT(term.literals.size(), 1);
2013-12-12 14:43:04 +00:00
}
2013-12-12 14:43:04 +00:00
return;
}
PbConstraint constraint;
2013-12-12 14:43:04 +00:00
for (int i = 0; i < words.size(); ++i) {
2020-10-29 14:25:39 +01:00
const std::string& word = words[i];
2013-12-12 14:43:04 +00:00
CHECK(!word.empty());
if (word[0] == '[') { // Soft constraint.
2025-04-03 16:12:45 +02:00
if (!ParseInt64Into(word.substr(1, word.size() - 2),
&constraint.soft_cost)) {
return;
}
} else if (word == ">=") {
2013-12-12 14:43:04 +00:00
CHECK_LT(i + 1, words.size());
constraint.type = GE_OPERATION;
2025-04-03 16:12:45 +02:00
if (!ParseInt64Into(words[i + 1], &constraint.rhs)) return;
2013-12-12 14:43:04 +00:00
break;
} else if (word == "=") {
CHECK_LT(i + 1, words.size());
constraint.type = EQ_OPERATION;
2025-04-03 16:12:45 +02:00
if (!ParseInt64Into(words[i + 1], &constraint.rhs)) return;
2013-12-12 14:43:04 +00:00
break;
} else if (word[0] == 'x') {
2025-04-03 16:12:45 +02:00
const int index = ParseIndex(word.substr(1));
num_variables_ = std::max(num_variables_, index);
constraint.terms.back().literals.push_back(
PbLiteralToCpModelLiteral(index));
} else if (word[0] == '~' && word[1] == 'x') {
2025-04-03 16:12:45 +02:00
const int index = ParseIndex(word.substr(2));
num_variables_ = std::max(num_variables_, index);
constraint.terms.back().literals.push_back(
NegatedRef(PbLiteralToCpModelLiteral(index)));
2013-12-12 14:43:04 +00:00
} else {
// Note that coefficient always appear before the variable/variables.
2025-04-03 16:12:45 +02:00
PbTerm term;
if (!ParseInt64Into(word, &term.coeff)) return;
constraint.terms.emplace_back(std::move(term));
}
}
// Normalize literals.
for (PbTerm& term : constraint.terms) {
if (term.literals.size() <= 1) continue;
gtl::STLSortAndRemoveDuplicates(&term.literals);
CHECK_GT(term.literals.size(), 1);
}
constraints_.push_back(std::move(constraint));
}
std::string ValidateModel() {
// Normalize and validate constraints.
for (const PbConstraint& constraint : constraints_) {
if (constraint.rhs == std::numeric_limits<int64_t>::min()) {
return "constraint error: undefined rhs";
}
if (constraint.type == UNDEFINED_OPERATION) {
return "constraint error: undefined operation";
}
for (const PbTerm& term : constraint.terms) {
if (term.coeff == 0) {
return "constraint error: coefficient cannot be zero";
}
if (term.literals.empty()) return "constraint error: empty literals";
if (term.literals.size() == 1) {
if (!RefIsPositive(term.literals[0])) {
return "constraint error: linear terms must use positive literals";
}
}
}
}
// Normalize and validate objective.
if (objective_.empty()) return ""; // No objective.
for (const PbTerm& term : objective_) {
if (term.coeff == 0) return "objective error: coefficient cannot be zero";
if (term.literals.empty()) return "objective error: empty literals";
if (term.literals.size() == 1) {
if (!RefIsPositive(term.literals[0])) {
return "objective error: linear terms must use positive literals";
2013-12-12 14:43:04 +00:00
}
return "";
}
}
return "";
}
static int PbLiteralToCpModelLiteral(int pb_literal) {
return pb_literal > 0 ? pb_literal - 1 : -pb_literal;
}
2025-04-03 16:12:45 +02:00
bool ParseInt64Into(const std::string& word, int64_t* value) {
if (!absl::SimpleAtoi(word, value)) {
VLOG(1) << "Failed to parse int64_t: " << word;
2025-04-03 16:12:45 +02:00
model_is_supported_ = false;
return false;
}
return true;
}
2025-06-02 14:25:50 +02:00
static int ParseIndex(absl::string_view word) {
2025-04-03 16:12:45 +02:00
int index;
CHECK(absl::SimpleAtoi(word, &index));
return index;
}
int GetVariable(const PbTerm& term, CpModelProto* model) {
CHECK(!term.literals.empty());
if (term.literals.size() == 1) {
CHECK(RefIsPositive(term.literals[0]));
return term.literals[0];
}
const auto it = product_to_var_.find(term.literals);
if (it != product_to_var_.end()) {
return it->second;
}
const int var_index = model->variables_size();
IntegerVariableProto* var_proto = model->add_variables();
var_proto->add_domain(0);
var_proto->add_domain(1);
product_to_var_[term.literals] = var_index;
// Link the new variable to the terms.
// var_index => and(literals).
ConstraintProto* var_to_literals = model->add_constraints();
var_to_literals->add_enforcement_literal(var_index);
// and(literals) => var_index.
ConstraintProto* literals_to_var = model->add_constraints();
literals_to_var->mutable_bool_and()->add_literals(var_index);
for (const int proto_literal : term.literals) {
var_to_literals->mutable_bool_and()->add_literals(proto_literal);
literals_to_var->add_enforcement_literal(proto_literal);
}
return var_index;
}
void BuildModel(CpModelProto* model) {
// We know how many variables we have, so we can add them all.
for (int i = 0; i < num_variables_; ++i) {
IntegerVariableProto* var = model->add_variables();
var->add_domain(0);
var->add_domain(1);
}
for (const PbConstraint& constraint : constraints_) {
ConstraintProto* ct = model->add_constraints();
LinearConstraintProto* lin = ct->mutable_linear();
for (const PbTerm& term : constraint.terms) {
lin->add_vars(GetVariable(term, model));
lin->add_coeffs(term.coeff);
}
if (constraint.type == GE_OPERATION) {
lin->add_domain(constraint.rhs);
lin->add_domain(std::numeric_limits<int64_t>::max());
} else if (constraint.type == EQ_OPERATION) {
lin->add_domain(constraint.rhs);
lin->add_domain(constraint.rhs);
} else {
LOG(FATAL) << "Unsupported operation: " << constraint.type;
}
if (constraint.soft_cost != std::numeric_limits<int64_t>::max()) {
const int violation_var_index = model->variables_size();
IntegerVariableProto* violation_var = model->add_variables();
violation_var->add_domain(0);
violation_var->add_domain(1);
// Update the objective.
model->mutable_objective()->add_vars(violation_var_index);
model->mutable_objective()->add_coeffs(constraint.soft_cost);
// Add the enforcement literal to ct.
ct->add_enforcement_literal(NegatedRef(violation_var_index));
2013-12-12 14:43:04 +00:00
}
}
if (!objective_.empty()) {
CpObjectiveProto* obj = model->mutable_objective();
for (const PbTerm& term : objective_) {
obj->add_vars(GetVariable(term, model));
obj->add_coeffs(term.coeff);
}
2013-12-12 14:43:04 +00:00
}
2025-07-21 17:25:33 +02:00
if (top_cost_.has_value()) {
CpObjectiveProto* obj = model->mutable_objective();
obj->add_domain(std::numeric_limits<int64_t>::min());
obj->add_domain(top_cost_.value());
}
2013-12-12 14:43:04 +00:00
}
int num_variables_;
std::vector<PbTerm> objective_;
std::vector<PbConstraint> constraints_;
absl::flat_hash_map<absl::Span<const int>, int> product_to_var_;
2025-04-03 16:12:45 +02:00
bool model_is_supported_ = true;
2025-07-21 17:25:33 +02:00
std::optional<int64_t> top_cost_;
2013-12-12 14:43:04 +00:00
};
2020-10-22 23:36:58 +02:00
} // namespace sat
} // namespace operations_research
2013-12-12 14:43:04 +00:00
2025-11-05 11:34:49 +01:00
#endif // ORTOOLS_SAT_OPB_READER_H_