math_opt: export constraints tests

This commit is contained in:
Corentin Le Molgat
2024-02-26 17:13:24 +01:00
committed by Mizux Seiha
parent 0d8db7b56f
commit 5ffb246ccf
26 changed files with 2282 additions and 2 deletions

View File

@@ -238,6 +238,7 @@ cc_library(
name = "message_matchers",
hdrs = ["message_matchers.h"],
deps = [
"@com_google_absl//absl/strings",
"@com_google_googletest//:gtest",
"@com_google_protobuf//:protobuf",
],

View File

@@ -16,10 +16,83 @@
#include <memory>
#include "absl/strings/string_view.h"
#include "gmock/gmock.h"
#include "gmock/gmock-matchers.h"
#include "google/protobuf/message.h"
#include "google/protobuf/util/message_differencer.h"
namespace testing {
namespace internal {
// Utilities.
// How to compare two fields (equal vs. equivalent).
typedef ::google::protobuf::util::MessageDifferencer::MessageFieldComparison
ProtoFieldComparison;
// How to compare two floating-points (exact vs. approximate).
typedef ::google::protobuf::util::DefaultFieldComparator::FloatComparison
ProtoFloatComparison;
// How to compare repeated fields (whether the order of elements matters).
typedef ::google::protobuf::util::MessageDifferencer::RepeatedFieldComparison
RepeatedFieldComparison;
// Whether to compare all fields (full) or only fields present in the
// expected protobuf (partial).
typedef ::google::protobuf::util::MessageDifferencer::Scope ProtoComparisonScope;
const ProtoFieldComparison kProtoEqual =
::google::protobuf::util::MessageDifferencer::EQUAL;
const ProtoFieldComparison kProtoEquiv =
::google::protobuf::util::MessageDifferencer::EQUIVALENT;
const ProtoFloatComparison kProtoExact =
::google::protobuf::util::DefaultFieldComparator::EXACT;
const ProtoFloatComparison kProtoApproximate =
::google::protobuf::util::DefaultFieldComparator::APPROXIMATE;
const RepeatedFieldComparison kProtoCompareRepeatedFieldsRespectOrdering =
::google::protobuf::util::MessageDifferencer::AS_LIST;
const RepeatedFieldComparison kProtoCompareRepeatedFieldsIgnoringOrdering =
::google::protobuf::util::MessageDifferencer::AS_SET;
const ProtoComparisonScope kProtoFull = ::google::protobuf::util::MessageDifferencer::FULL;
const ProtoComparisonScope kProtoPartial =
::google::protobuf::util::MessageDifferencer::PARTIAL;
// Options for comparing two protobufs.
struct ProtoComparison {
ProtoComparison()
: field_comp(kProtoEqual),
float_comp(kProtoExact),
treating_nan_as_equal(false),
has_custom_margin(false),
has_custom_fraction(false),
repeated_field_comp(kProtoCompareRepeatedFieldsRespectOrdering),
scope(kProtoFull),
float_margin(0.0),
float_fraction(0.0),
ignore_debug_string_format(false),
fail_on_no_presence_default_values(false),
verified_presence_in_string(false) {}
ProtoFieldComparison field_comp;
ProtoFloatComparison float_comp;
bool treating_nan_as_equal;
bool has_custom_margin; // only used when float_comp = APPROXIMATE
bool has_custom_fraction; // only used when float_comp = APPROXIMATE
RepeatedFieldComparison repeated_field_comp;
ProtoComparisonScope scope;
double float_margin; // only used when has_custom_margin is set.
double float_fraction; // only used when has_custom_fraction is set.
std::vector<std::string> ignore_fields;
std::vector<std::string> ignore_field_paths;
std::vector<std::string> unordered_fields;
bool ignore_debug_string_format;
bool fail_on_no_presence_default_values;
bool verified_presence_in_string;
};
// Whether the protobuf must be initialized.
const bool kMustBeInitialized = true;
const bool kMayBeUninitialized = false;
class ProtoMatcher {
public:
@@ -29,6 +102,9 @@ class ProtoMatcher {
explicit ProtoMatcher(const MessageType& message)
: message_(CloneMessage(message)) {}
ProtoMatcher(const MessageType& message, bool, ProtoComparison&):
message_(CloneMessage(message)) {}
bool MatchAndExplain(const MessageType& m,
testing::MatchResultListener*) const {
return EqualsMessage(*message_, m);
@@ -69,8 +145,11 @@ class ProtoMatcher {
const std::shared_ptr<MessageType> message_;
};
inline ProtoMatcher EqualsProto(const ::google::protobuf::Message& message) {
return ProtoMatcher(message);
using PolymorphicProtoMatcher = PolymorphicMatcher<ProtoMatcher>;
} // namespace internal
inline internal::ProtoMatcher EqualsProto(const ::google::protobuf::Message& message) {
return internal::ProtoMatcher(message);
}
// for Pointwise
@@ -80,6 +159,16 @@ MATCHER(EqualsProto, "") {
return ::testing::ExplainMatchResult(EqualsProto(b), a, result_listener);
}
// Constructs a matcher that matches the argument if
// argument.Equivalent(x) or argument->Equivalent(x) returns true.
inline internal::PolymorphicProtoMatcher EquivToProto(
const ::google::protobuf::Message& x) {
internal::ProtoComparison comp;
comp.field_comp = internal::kProtoEquiv;
return MakePolymorphicMatcher(
internal::ProtoMatcher(x, internal::kMayBeUninitialized, comp));
}
} // namespace testing
#endif // OR_TOOLS_BASE_MESSAGE_MATCHERS_H_

View File

@@ -26,6 +26,21 @@ cc_library(
],
)
cc_test(
name = "indicator_constraint_test",
srcs = ["indicator_constraint_test.cc"],
deps = [
":indicator_constraint",
"//ortools/base:gmock",
"//ortools/base:gmock_main",
"//ortools/base:intops",
"//ortools/math_opt/cpp:math_opt",
"//ortools/math_opt/storage:model_storage",
"//ortools/math_opt/storage:sparse_coefficient_map",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "storage",
srcs = ["storage.cc"],
@@ -42,6 +57,20 @@ cc_library(
],
)
cc_test(
name = "storage_test",
srcs = ["storage_test.cc"],
deps = [
":storage",
"//ortools/base:gmock_main",
"//ortools/base:intops",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/cpp:math_opt",
"//ortools/math_opt/storage:sparse_coefficient_map",
],
)
cc_library(
name = "validator",
srcs = ["validator.cc"],
@@ -55,3 +84,18 @@ cc_library(
"@com_google_absl//absl/status",
],
)
cc_test(
name = "validator_test",
srcs = ["validator_test.cc"],
deps = [
":validator",
"//ortools/base:gmock_main",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:model_summary",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/types:span",
],
)

View File

@@ -15,6 +15,8 @@ set(NAME ${PROJECT_NAME}_math_opt_constraints_indicator)
add_library(${NAME} OBJECT)
file(GLOB _SRCS "*.h" "*.cc")
list(FILTER _SRCS EXCLUDE REGEX ".*_test.cc")
target_sources(${NAME} PRIVATE ${_SRCS})
set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(${NAME} PUBLIC

View File

@@ -0,0 +1,179 @@
// Copyright 2010-2024 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/math_opt/constraints/indicator/indicator_constraint.h"
#include <optional>
#include <sstream>
#include <string>
#include <utility>
#include "absl/strings/str_cat.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/cpp/math_opt.h"
#include "ortools/math_opt/storage/model_storage.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
namespace operations_research::math_opt {
namespace {
using ::testing::Optional;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
TEST(IndicatorConstraintTest, Accessors) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Variable z(&storage, storage.AddVariable("z"));
SparseCoefficientMap coeffs;
coeffs.set(x.typed_id(), 2.0);
coeffs.set(y.typed_id(), 3.0);
const IndicatorConstraintData data{
.lower_bound = -1.0,
.upper_bound = 1.0,
.linear_terms = std::move(coeffs),
.indicator = z.typed_id(),
.activate_on_zero = true,
.name = "c",
};
const IndicatorConstraint c(&storage, storage.AddAtomicConstraint(data));
EXPECT_EQ(c.name(), "c");
EXPECT_THAT(c.indicator_variable(), Optional(z));
const BoundedLinearExpression constraint = c.ImpliedConstraint();
EXPECT_EQ(constraint.lower_bound, -1.0);
EXPECT_EQ(constraint.upper_bound, 1.0);
EXPECT_EQ(constraint.expression.offset(), 0.0);
EXPECT_THAT(constraint.expression.terms(),
UnorderedElementsAre(Pair(x, 2.0), Pair(y, 3.0)));
EXPECT_TRUE(c.activate_on_zero());
}
TEST(IndicatorConstraintTest, Equality) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const IndicatorConstraint c(
&storage, storage.AddAtomicConstraint(
IndicatorConstraintData{.upper_bound = 1.0, .name = "c"}));
const IndicatorConstraint d(
&storage, storage.AddAtomicConstraint(
IndicatorConstraintData{.upper_bound = 2.0, .name = "d"}));
// `d2` is another `IndicatorConstraint` that points the same constraint in
// the indexed storage. It should compares == to `d`.
const IndicatorConstraint d2(d.storage(), d.typed_id());
// `e` has identical data as `d`. It should not compares equal to `d`, though.
const IndicatorConstraint e(
&storage, storage.AddAtomicConstraint(
IndicatorConstraintData{.upper_bound = 2.0, .name = "d"}));
EXPECT_TRUE(c == c);
EXPECT_FALSE(c == d);
EXPECT_TRUE(d == d2);
EXPECT_FALSE(d == e);
EXPECT_FALSE(c != c);
EXPECT_TRUE(c != d);
EXPECT_FALSE(d != d2);
EXPECT_TRUE(d != e);
}
TEST(IndicatorConstraintTest, NonzeroVariables) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Variable z(&storage, storage.AddVariable("z"));
SparseCoefficientMap coeffs;
coeffs.set(x.typed_id(), 2.0);
coeffs.set(y.typed_id(), 3.0);
const IndicatorConstraintData data{
.lower_bound = -1.0,
.upper_bound = 1.0,
.linear_terms = std::move(coeffs),
.indicator = z.typed_id(),
.name = "c",
};
const IndicatorConstraint c(&storage, storage.AddAtomicConstraint(data));
EXPECT_THAT(c.NonzeroVariables(), UnorderedElementsAre(x, y, z));
}
TEST(IndicatorConstraintTest, IndicatorGetter) {
Model model;
const Variable x = model.AddBinaryVariable("x");
const Variable y = model.AddVariable("y");
const IndicatorConstraint c = model.AddIndicatorConstraint(x, y <= 1);
EXPECT_THAT(c.indicator_variable(), Optional(x));
model.DeleteVariable(x);
EXPECT_EQ(c.indicator_variable(), std::nullopt);
}
TEST(IndicatorConstraintTest, ToString) {
Model model;
const Variable x = model.AddBinaryVariable("x");
const Variable y = model.AddVariable("y");
const IndicatorConstraint c = model.AddIndicatorConstraint(
x, -3.0 <= 2 * y + 1 <= 3.0, /*activate_on_zero=*/false, "c");
EXPECT_EQ(c.ToString(), "x = 1 ⇒ -4 ≤ 2*y ≤ 2");
const IndicatorConstraint d = model.AddIndicatorConstraint(
x, -2 * y == 3, /*activate_on_zero=*/true, "d");
EXPECT_EQ(d.ToString(), "x = 0 ⇒ -2*y = 3");
model.DeleteVariable(x);
EXPECT_EQ(c.ToString(), "[unset indicator variable] ⇒ -4 ≤ 2*y ≤ 2");
model.DeleteIndicatorConstraint(c);
EXPECT_EQ(c.ToString(), kDeletedConstraintDefaultDescription);
}
TEST(IndicatorConstraintTest, OutputStreaming) {
ModelStorage storage;
const IndicatorConstraint q(
&storage,
storage.AddAtomicConstraint(IndicatorConstraintData{.name = "q"}));
const IndicatorConstraint anonymous(
&storage,
storage.AddAtomicConstraint(IndicatorConstraintData{.name = ""}));
auto to_string = [](IndicatorConstraint c) {
std::ostringstream oss;
oss << c;
return oss.str();
};
EXPECT_EQ(to_string(q), "q");
EXPECT_EQ(to_string(anonymous),
absl::StrCat("__indic_con#", anonymous.id(), "__"));
}
TEST(IndicatorConstraintTest, NameAfterDeletion) {
Model model;
const Variable x = model.AddBinaryVariable("x");
const IndicatorConstraint c =
model.AddIndicatorConstraint(x, x >= 1, /*activate_on_zero=*/false, "c");
ASSERT_EQ(c.name(), "c");
model.DeleteIndicatorConstraint(c);
EXPECT_EQ(c.name(), kDeletedConstraintDefaultDescription);
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,126 @@
// Copyright 2010-2024 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/math_opt/constraints/indicator/storage.h"
#include <optional>
#include <string>
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/cpp/math_opt.h"
#include "ortools/math_opt/model.pb.h"
#include "ortools/math_opt/sparse_containers.pb.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
namespace operations_research::math_opt {
namespace {
using ::testing::EquivToProto;
using ::testing::Optional;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
IndicatorConstraintProto SimpleProto() {
IndicatorConstraintProto proto;
proto.set_lower_bound(-1.0);
proto.set_upper_bound(1.0);
proto.set_name("indicator");
proto.set_indicator_id(2);
proto.set_activate_on_zero(true);
proto.mutable_expression()->add_ids(1);
proto.mutable_expression()->add_values(2.0);
proto.mutable_expression()->add_ids(3);
proto.mutable_expression()->add_values(4.0);
proto.mutable_expression()->add_ids(5);
proto.mutable_expression()->add_values(6.0);
return proto;
}
IndicatorConstraintData SimpleData() {
IndicatorConstraintData data;
data.lower_bound = -1.0;
data.upper_bound = 1.0;
data.name = "indicator";
data.indicator = VariableId(2);
data.activate_on_zero = true;
data.linear_terms.set(VariableId(1), 2.0);
data.linear_terms.set(VariableId(3), 4.0);
data.linear_terms.set(VariableId(5), 6.0);
return data;
}
TEST(QuadraticStorageDataTest, RelatedVariables) {
EXPECT_THAT(SimpleData().RelatedVariables(),
UnorderedElementsAre(VariableId(1), VariableId(2), VariableId(3),
VariableId(5)));
}
TEST(IndicatorConstraintDataTest, DeleteVariable) {
IndicatorConstraintData data = SimpleData();
data.DeleteVariable(VariableId(1));
EXPECT_THAT(data.indicator, Optional(VariableId(2)));
EXPECT_THAT(
data.linear_terms.terms(),
UnorderedElementsAre(Pair(VariableId(3), 4.0), Pair(VariableId(5), 6.0)));
data.DeleteVariable(VariableId(2));
EXPECT_EQ(data.indicator, std::nullopt);
EXPECT_THAT(
data.linear_terms.terms(),
UnorderedElementsAre(Pair(VariableId(3), 4.0), Pair(VariableId(5), 6.0)));
}
TEST(IndicatorConstraintDataTest, FromProto) {
const auto data = IndicatorConstraintData::FromProto(SimpleProto());
EXPECT_EQ(data.lower_bound, -1.0);
EXPECT_EQ(data.upper_bound, 1.0);
EXPECT_EQ(data.name, "indicator");
EXPECT_THAT(data.indicator, Optional(VariableId(2)));
EXPECT_TRUE(data.activate_on_zero);
EXPECT_THAT(
data.linear_terms.terms(),
UnorderedElementsAre(Pair(VariableId(1), 2.0), Pair(VariableId(3), 4.0),
Pair(VariableId(5), 6.0)));
}
TEST(IndicatorConstraintDataTest, FromProtoUnsetIndicator) {
IndicatorConstraintProto proto = SimpleProto();
proto.clear_indicator_id();
const auto data = IndicatorConstraintData::FromProto(proto);
EXPECT_EQ(data.lower_bound, -1.0);
EXPECT_EQ(data.upper_bound, 1.0);
EXPECT_EQ(data.name, "indicator");
EXPECT_EQ(data.indicator, std::nullopt);
EXPECT_TRUE(data.activate_on_zero);
EXPECT_THAT(
data.linear_terms.terms(),
UnorderedElementsAre(Pair(VariableId(1), 2.0), Pair(VariableId(3), 4.0),
Pair(VariableId(5), 6.0)));
}
TEST(IndicatorConstraintDataTest, Proto) {
EXPECT_THAT(SimpleData().Proto(), EquivToProto(SimpleProto()));
}
TEST(IndicatorConstraintDataTest, ProtoUnsetIndicator) {
IndicatorConstraintData data = SimpleData();
data.indicator.reset();
IndicatorConstraintProto expected = SimpleProto();
expected.clear_indicator_id();
EXPECT_THAT(data.Proto(), EquivToProto(expected));
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,114 @@
// Copyright 2010-2024 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/math_opt/constraints/indicator/validator.h"
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <string>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/types/span.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/math_opt/core/model_summary.h"
#include "ortools/math_opt/model.pb.h"
#include "ortools/math_opt/sparse_containers.pb.h"
namespace operations_research::math_opt {
namespace {
using ::testing::HasSubstr;
using ::testing::status::StatusIs;
constexpr double kInf = std::numeric_limits<double>::infinity();
IdNameBiMap SimpleVariableUniverse(const std::initializer_list<int64_t> ids) {
IdNameBiMap universe;
CHECK_OK(universe.BulkUpdate({}, ids, {}));
return universe;
}
IndicatorConstraintProto SimpleIndicatorConstraintProto() {
IndicatorConstraintProto data;
data.set_indicator_id(1);
data.mutable_expression()->add_ids(2);
data.mutable_expression()->add_values(3.0);
data.set_lower_bound(-1.0);
data.set_upper_bound(1.0);
return data;
}
TEST(IndicatorConstraintValidatorTest, SimpleConstraintOk) {
EXPECT_OK(ValidateConstraint(SimpleIndicatorConstraintProto(),
SimpleVariableUniverse({1, 2})));
}
TEST(IndicatorConstraintValidatorTest, UnsetIndicatorId) {
IndicatorConstraintProto constraint = SimpleIndicatorConstraintProto();
constraint.clear_indicator_id();
EXPECT_OK(ValidateConstraint(constraint, SimpleVariableUniverse({2})));
}
TEST(IndicatorConstraintValidatorTest, InvalidIndicatorId) {
EXPECT_THAT(ValidateConstraint(SimpleIndicatorConstraintProto(),
SimpleVariableUniverse({2})),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("indicator variable id")));
}
TEST(IndicatorConstraintValidatorTest, InvalidTermInExpression) {
IndicatorConstraintProto constraint = SimpleIndicatorConstraintProto();
constraint.mutable_expression()->set_values(0, kInf);
EXPECT_THAT(ValidateConstraint(constraint, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("infinite value"),
HasSubstr("implied constraint"))));
}
TEST(IndicatorConstraintValidatorTest, InvalidIdInExpression) {
EXPECT_THAT(ValidateConstraint(SimpleIndicatorConstraintProto(),
SimpleVariableUniverse({1})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("variable id"),
HasSubstr("implied constraint"))));
}
TEST(IndicatorConstraintValidatorTest, InvalidLowerBound) {
IndicatorConstraintProto constraint = SimpleIndicatorConstraintProto();
constraint.set_lower_bound(kInf);
EXPECT_THAT(ValidateConstraint(constraint, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("invalid lower bound")));
constraint.set_lower_bound(std::numeric_limits<double>::quiet_NaN());
EXPECT_THAT(ValidateConstraint(constraint, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("invalid lower bound")));
}
TEST(IndicatorConstraintValidatorTest, InvalidUpperBound) {
IndicatorConstraintProto constraint = SimpleIndicatorConstraintProto();
constraint.set_upper_bound(-kInf);
EXPECT_THAT(ValidateConstraint(constraint, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("invalid upper bound")));
constraint.set_upper_bound(std::numeric_limits<double>::quiet_NaN());
EXPECT_THAT(ValidateConstraint(constraint, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("invalid upper bound")));
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -30,6 +30,22 @@ cc_library(
],
)
cc_test(
name = "quadratic_constraint_test",
srcs = ["quadratic_constraint_test.cc"],
deps = [
":quadratic_constraint",
"//ortools/base:gmock",
"//ortools/base:gmock_main",
"//ortools/base:intops",
"//ortools/math_opt/cpp:math_opt",
"//ortools/math_opt/storage:model_storage",
"//ortools/math_opt/storage:sparse_coefficient_map",
"//ortools/math_opt/storage:sparse_matrix",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "storage",
srcs = ["storage.cc"],
@@ -46,6 +62,21 @@ cc_library(
],
)
cc_test(
name = "storage_test",
srcs = ["storage_test.cc"],
deps = [
":storage",
"//ortools/base:gmock_main",
"//ortools/base:intops",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/storage:model_storage_types",
"//ortools/math_opt/storage:sparse_coefficient_map",
"//ortools/math_opt/storage:sparse_matrix",
],
)
cc_library(
name = "validator",
srcs = ["validator.cc"],
@@ -63,3 +94,18 @@ cc_library(
"@com_google_absl//absl/status",
],
)
cc_test(
name = "validator_test",
srcs = ["validator_test.cc"],
deps = [
":validator",
"//ortools/base:gmock_main",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:model_summary",
"@com_google_absl//absl/log:check",
"@com_google_absl//absl/status",
"@com_google_absl//absl/types:span",
],
)

View File

@@ -15,6 +15,8 @@ set(NAME ${PROJECT_NAME}_math_opt_constraints_quadratic)
add_library(${NAME} OBJECT)
file(GLOB _SRCS "*.h" "*.cc")
list(FILTER _SRCS EXCLUDE REGEX ".*_test.cc")
target_sources(${NAME} PRIVATE ${_SRCS})
set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(${NAME} PUBLIC

View File

@@ -0,0 +1,193 @@
// Copyright 2010-2024 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/math_opt/constraints/quadratic/quadratic_constraint.h"
#include <sstream>
#include <string>
#include "absl/strings/str_cat.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/cpp/math_opt.h"
#include "ortools/math_opt/storage/model_storage.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
#include "ortools/math_opt/storage/sparse_matrix.h"
namespace operations_research::math_opt {
namespace {
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
TEST(QuadraticConstraintTest, OutputStreaming) {
ModelStorage storage;
const QuadraticConstraint q(
&storage,
storage.AddAtomicConstraint(QuadraticConstraintData{.name = "q"}));
const QuadraticConstraint anonymous(
&storage, storage.AddAtomicConstraint(QuadraticConstraintData{}));
auto to_string = [](QuadraticConstraint c) {
std::ostringstream oss;
oss << c;
return oss.str();
};
EXPECT_EQ(to_string(q), "q");
EXPECT_EQ(to_string(anonymous),
absl::StrCat("__quad_con#", anonymous.id(), "__"));
}
TEST(QuadraticConstraintTest, Accessors) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
SparseSymmetricMatrix quadratic_terms;
quadratic_terms.set(x.typed_id(), y.typed_id(), 4.0);
quadratic_terms.set(y.typed_id(), y.typed_id(), 5.0);
const QuadraticConstraintData data{
.lower_bound = 1.0,
.upper_bound = 2.0,
.linear_terms = SparseCoefficientMap({{x.typed_id(), 3.0}}),
.quadratic_terms = quadratic_terms,
.name = "q",
};
const QuadraticConstraint c(&storage, storage.AddAtomicConstraint(data));
EXPECT_EQ(c.lower_bound(), 1.0);
EXPECT_EQ(c.upper_bound(), 2.0);
EXPECT_EQ(c.name(), "q");
EXPECT_TRUE(c.is_linear_coefficient_nonzero(x));
EXPECT_FALSE(c.is_linear_coefficient_nonzero(y));
EXPECT_EQ(c.linear_coefficient(x), 3.0);
EXPECT_EQ(c.linear_coefficient(y), 0.0);
EXPECT_FALSE(c.is_quadratic_coefficient_nonzero(x, x));
EXPECT_TRUE(c.is_quadratic_coefficient_nonzero(x, y));
EXPECT_TRUE(c.is_quadratic_coefficient_nonzero(y, x));
EXPECT_TRUE(c.is_quadratic_coefficient_nonzero(y, y));
EXPECT_EQ(c.quadratic_coefficient(x, x), 0.0);
EXPECT_EQ(c.quadratic_coefficient(x, y), 4.0);
EXPECT_EQ(c.quadratic_coefficient(y, x), 4.0);
EXPECT_EQ(c.quadratic_coefficient(y, y), 5.0);
}
TEST(QuadraticConstraintTest, Equality) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const QuadraticConstraint c(
&storage, storage.AddAtomicConstraint(QuadraticConstraintData{
.upper_bound = 1.5, .name = "upper_bounded"}));
const QuadraticConstraint d(
&storage, storage.AddAtomicConstraint(QuadraticConstraintData{
.lower_bound = 0.5, .name = "lower_bounded"}));
// `d2` is another `QuadraticConstraint` that points the same constraint in
// the indexed storage. It should compares == to `d`.
const QuadraticConstraint d2(d.storage(), d.typed_id());
// `e` has identical data as `d`. It should not compares equal to `d`, though.
const QuadraticConstraint e(
&storage, storage.AddAtomicConstraint(QuadraticConstraintData{
.lower_bound = 0.5, .name = "lower_bounded"}));
EXPECT_TRUE(c == c);
EXPECT_FALSE(c == d);
EXPECT_TRUE(d == d2);
EXPECT_FALSE(d == e);
EXPECT_FALSE(c != c);
EXPECT_TRUE(c != d);
EXPECT_FALSE(d != d2);
EXPECT_TRUE(d != e);
}
TEST(QuadraticConstraintTest, NonzeroVariables) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Variable z(&storage, storage.AddVariable("z"));
const Variable w(&storage, storage.AddVariable("w"));
SparseSymmetricMatrix quadratic_terms;
quadratic_terms.set(y.typed_id(), z.typed_id(), 5.0);
const QuadraticConstraintData data{
.lower_bound = 1.0,
.upper_bound = 2.0,
.linear_terms = SparseCoefficientMap({{x.typed_id(), 3.0}}),
.quadratic_terms = quadratic_terms,
.name = "q",
};
const QuadraticConstraint q(&storage, storage.AddAtomicConstraint(data));
EXPECT_THAT(q.NonzeroVariables(), UnorderedElementsAre(x, y, z));
}
TEST(QuadraticConstraintTest, AsBoundedQuadraticExpression) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Variable z(&storage, storage.AddVariable("z"));
const Variable w(&storage, storage.AddVariable("w"));
SparseSymmetricMatrix quadratic_terms;
quadratic_terms.set(y.typed_id(), z.typed_id(), 5.0);
const QuadraticConstraintData data{
.lower_bound = 1.0,
.upper_bound = 2.0,
.linear_terms = SparseCoefficientMap({{x.typed_id(), 3.0}}),
.quadratic_terms = quadratic_terms,
.name = "q",
};
const QuadraticConstraint q(&storage, storage.AddAtomicConstraint(data));
const BoundedQuadraticExpression expr = q.AsBoundedQuadraticExpression();
EXPECT_EQ(expr.lower_bound, 1.0);
EXPECT_EQ(expr.upper_bound, 2.0);
EXPECT_EQ(expr.expression.offset(), 0.0);
EXPECT_THAT(expr.expression.linear_terms(),
UnorderedElementsAre(Pair(x, 3.0)));
EXPECT_THAT(expr.expression.quadratic_terms(),
UnorderedElementsAre(Pair(QuadraticTermKey(y, z), 5.0)));
}
TEST(QuadraticConstraintTest, ToString) {
Model model;
const Variable x = model.AddVariable("x");
const Variable y = model.AddVariable("y");
const QuadraticConstraint c = model.AddQuadraticConstraint(
1.0 <= -2 * x + 3 * y - 4.0 * x * x + 5.0 * x * y + 6.0 <= 7.0);
EXPECT_EQ(c.ToString(), "-5 ≤ -4*x² + 5*x*y - 2*x + 3*y ≤ 1");
model.DeleteVariable(y);
EXPECT_EQ(c.ToString(), "-5 ≤ -4*x² - 2*x ≤ 1");
model.DeleteQuadraticConstraint(c);
EXPECT_EQ(c.ToString(), kDeletedConstraintDefaultDescription);
}
TEST(QuadraticConstraintTest, NameAfterDeletion) {
Model model;
const Variable x = model.AddVariable("x");
const QuadraticConstraint c =
model.AddQuadraticConstraint(2 * x * x >= 1, "c");
ASSERT_EQ(c.name(), "c");
model.DeleteQuadraticConstraint(c);
EXPECT_EQ(c.name(), kDeletedConstraintDefaultDescription);
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,103 @@
// Copyright 2010-2024 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/math_opt/constraints/quadratic/storage.h"
#include <string>
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/model.pb.h"
#include "ortools/math_opt/sparse_containers.pb.h"
#include "ortools/math_opt/storage/model_storage_types.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
#include "ortools/math_opt/storage/sparse_matrix.h"
namespace operations_research::math_opt {
namespace {
using ::testing::EquivToProto;
using ::testing::IsEmpty;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
using ::testing::no_adl::FieldsAre;
QuadraticConstraintProto SimpleProto() {
QuadraticConstraintProto proto;
proto.set_lower_bound(-1.0);
proto.set_upper_bound(1.0);
proto.set_name("q");
proto.mutable_quadratic_terms()->add_row_ids(1);
proto.mutable_quadratic_terms()->add_column_ids(2);
proto.mutable_quadratic_terms()->add_coefficients(3.0);
proto.mutable_quadratic_terms()->add_row_ids(4);
proto.mutable_quadratic_terms()->add_column_ids(4);
proto.mutable_quadratic_terms()->add_coefficients(5.0);
proto.mutable_linear_terms()->add_ids(6);
proto.mutable_linear_terms()->add_values(7.0);
return proto;
}
QuadraticConstraintData SimpleData() {
QuadraticConstraintData data;
data.lower_bound = -1.0;
data.upper_bound = 1.0;
data.name = "q";
data.linear_terms.set(VariableId(6), 7.0);
data.quadratic_terms.set(VariableId(1), VariableId(2), 3.0);
data.quadratic_terms.set(VariableId(4), VariableId(4), 5.0);
return data;
}
TEST(QuadraticStorageDataTest, RelatedVariables) {
EXPECT_THAT(SimpleData().RelatedVariables(),
UnorderedElementsAre(VariableId(1), VariableId(2), VariableId(4),
VariableId(6)));
}
TEST(QuadraticStorageDataTest, DeleteVariable) {
QuadraticConstraintData data = SimpleData();
data.DeleteVariable(VariableId(2));
EXPECT_THAT(data.linear_terms.terms(),
UnorderedElementsAre(Pair(VariableId(6), 7.0)));
EXPECT_THAT(
data.quadratic_terms.Terms(),
UnorderedElementsAre(FieldsAre(VariableId(4), VariableId(4), 5.0)));
data.DeleteVariable(VariableId(6));
EXPECT_THAT(data.linear_terms.terms(), IsEmpty());
EXPECT_THAT(
data.quadratic_terms.Terms(),
UnorderedElementsAre(FieldsAre(VariableId(4), VariableId(4), 5.0)));
}
TEST(QuadraticStorageDataTest, FromProto) {
const auto data = QuadraticConstraintData::FromProto(SimpleProto());
EXPECT_EQ(data.lower_bound, -1.0);
EXPECT_EQ(data.upper_bound, 1.0);
EXPECT_EQ(data.name, "q");
EXPECT_THAT(data.linear_terms.terms(),
UnorderedElementsAre(Pair(VariableId(6), 7.0)));
EXPECT_THAT(
data.quadratic_terms.Terms(),
UnorderedElementsAre(FieldsAre(VariableId(1), VariableId(2), 3.0),
FieldsAre(VariableId(4), VariableId(4), 5.0)));
}
TEST(QuadraticStorageDataTest, Proto) {
EXPECT_THAT(SimpleData().Proto(), EquivToProto(SimpleProto()));
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,199 @@
// Copyright 2010-2024 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/math_opt/constraints/quadratic/validator.h"
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <type_traits>
#include "absl/log/check.h"
#include "absl/status/status.h"
#include "absl/types/span.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/math_opt/core/model_summary.h"
#include "ortools/math_opt/model.pb.h"
#include "ortools/math_opt/sparse_containers.pb.h"
namespace operations_research::math_opt {
namespace {
using ::testing::AllOf;
using ::testing::HasSubstr;
using ::testing::status::StatusIs;
constexpr double kInf = std::numeric_limits<double>::infinity();
constexpr double kNan = std::numeric_limits<double>::quiet_NaN();
IdNameBiMap SimpleVariableUniverse(const std::initializer_list<int64_t> ids) {
IdNameBiMap universe;
CHECK_OK(universe.BulkUpdate({}, ids, {}));
return universe;
}
QuadraticConstraintProto SimpleQuadraticConstraintProto() {
QuadraticConstraintProto data;
data.mutable_linear_terms()->add_ids(2);
data.mutable_linear_terms()->add_values(2.0);
data.mutable_quadratic_terms()->add_row_ids(1);
data.mutable_quadratic_terms()->add_column_ids(1);
data.mutable_quadratic_terms()->add_coefficients(3.0);
data.set_lower_bound(4.0);
data.set_upper_bound(5.0);
data.set_name("c");
return data;
}
TEST(QuadraticConstraintDataValidatorTest, EmptyConstraintOk) {
EXPECT_OK(ValidateConstraint(QuadraticConstraintProto(),
SimpleVariableUniverse({})));
}
TEST(QuadraticConstraintDataValidatorTest, SimpleConstraintOk) {
EXPECT_OK(ValidateConstraint(SimpleQuadraticConstraintProto(),
SimpleVariableUniverse({1, 2})));
}
TEST(QuadraticConstraintDataValidatorTest, BadLinearTermIds) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.mutable_linear_terms()->add_ids(1);
data.mutable_linear_terms()->add_values(6.0);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Expected ids to be strictly increasing"),
HasSubstr("bad linear term in quadratic constraint"))));
}
TEST(QuadraticConstraintDataValidatorTest, PositiveInfLinearTermValue) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.mutable_linear_terms()->set_values(0, kInf);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Invalid positive infinite value"),
HasSubstr("bad linear term in quadratic constraint"))));
}
TEST(QuadraticConstraintDataValidatorTest, NegativeInfLinearTermValue) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.mutable_linear_terms()->set_values(0, -kInf);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Invalid negative infinite value"),
HasSubstr("bad linear term in quadratic constraint"))));
}
TEST(QuadraticConstraintDataValidatorTest, UnknownLinearTermId) {
EXPECT_THAT(
ValidateConstraint(SimpleQuadraticConstraintProto(),
SimpleVariableUniverse({1})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("id 2 not found"),
HasSubstr("bad linear term ID in quadratic constraint"))));
}
TEST(QuadraticConstraintDataValidatorTest, BadQuadraticTermIds) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.mutable_quadratic_terms()->set_row_ids(0, 2);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("lower triangular entry"),
HasSubstr("bad quadratic term in quadratic constraint"))));
}
TEST(QuadraticConstraintDataValidatorTest, PositiveInfQuadraticTermValue) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.mutable_quadratic_terms()->set_coefficients(0, kInf);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Expected finite coefficients"),
HasSubstr("bad quadratic term in quadratic constraint"))));
}
TEST(QuadraticConstraintDataValidatorTest, NegativeInfQuadraticTermValue) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.mutable_quadratic_terms()->set_coefficients(0, -kInf);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Expected finite coefficients"),
HasSubstr("bad quadratic term in quadratic constraint"))));
}
TEST(QuadraticConstraintDataValidatorTest, UnknownQuadraticTermId) {
EXPECT_THAT(
ValidateConstraint(SimpleQuadraticConstraintProto(),
SimpleVariableUniverse({2})),
StatusIs(
absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Unknown row_id"),
HasSubstr("bad quadratic term ID in quadratic constraint"))));
}
TEST(QuadraticConstraintDataValidatorTest, PositiveInfLowerBound) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.set_lower_bound(kInf);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Invalid positive infinite value"),
HasSubstr("bad quadratic constraint lower bound"))));
}
TEST(QuadraticConstraintDataValidatorTest, NanLowerBound) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.set_lower_bound(kNan);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Invalid NaN value"),
HasSubstr("bad quadratic constraint lower bound"))));
}
TEST(QuadraticConstraintDataValidatorTest, NegativeInfUpperBound) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.set_upper_bound(-kInf);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Invalid negative infinite value"),
HasSubstr("bad quadratic constraint upper bound"))));
}
TEST(QuadraticConstraintDataValidatorTest, NanUpperBound) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.set_upper_bound(kNan);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Invalid NaN value"),
HasSubstr("bad quadratic constraint upper bound"))));
}
TEST(QuadraticConstraintDataValidatorTest, InvertedBounds) {
QuadraticConstraintProto data = SimpleQuadraticConstraintProto();
data.set_upper_bound(data.lower_bound() - 1);
EXPECT_THAT(ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("Quadratic constraint"),
HasSubstr("bounds are inverted"))));
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -28,6 +28,21 @@ cc_library(
],
)
cc_test(
name = "second_order_cone_constraint_test",
srcs = ["second_order_cone_constraint_test.cc"],
deps = [
":second_order_cone_constraint",
"//ortools/base:gmock",
"//ortools/base:gmock_main",
"//ortools/base:intops",
"//ortools/math_opt/constraints/util:model_util",
"//ortools/math_opt/storage:model_storage",
"//ortools/math_opt/storage:sparse_coefficient_map",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "storage",
srcs = ["storage.cc"],
@@ -45,6 +60,21 @@ cc_library(
],
)
cc_test(
name = "storage_test",
srcs = ["storage_test.cc"],
deps = [
":storage",
"//ortools/base:gmock_main",
"//ortools/base:intops",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:sparse_coefficient_map",
"@com_google_absl//absl/container:flat_hash_map",
],
)
cc_library(
name = "validator",
srcs = ["validator.cc"],
@@ -59,3 +89,19 @@ cc_library(
"@com_google_absl//absl/status",
],
)
cc_test(
name = "validator_test",
srcs = ["validator_test.cc"],
deps = [
":validator",
"//ortools/base:gmock_main",
"//ortools/base:logging",
"//ortools/base:status_macros",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:model_summary",
"@com_google_absl//absl/status",
"@com_google_absl//absl/types:span",
],
)

View File

@@ -15,6 +15,8 @@ set(NAME ${PROJECT_NAME}_math_opt_constraints_second_order_cone)
add_library(${NAME} OBJECT)
file(GLOB _SRCS "*.h" "*.cc")
list(FILTER _SRCS EXCLUDE REGEX ".*_test.cc")
target_sources(${NAME} PRIVATE ${_SRCS})
set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(${NAME} PUBLIC

View File

@@ -0,0 +1,182 @@
// Copyright 2010-2024 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/math_opt/constraints/second_order_cone/second_order_cone_constraint.h"
#include <optional>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
#include "absl/strings/str_cat.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/constraints/util/model_util.h"
#include "ortools/math_opt/storage/model_storage.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
namespace operations_research::math_opt {
namespace {
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
TEST(SecondOrderConeConstraintTest, Accessors) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Variable z(&storage, storage.AddVariable("z"));
const SecondOrderConeConstraintData data{
.upper_bound = {.coeffs = SparseCoefficientMap({{x.typed_id(), 1.0}}),
.offset = 2.0},
.arguments_to_norm = {{.coeffs =
SparseCoefficientMap({{y.typed_id(), 3.0}}),
.offset = 4.0},
{.coeffs =
SparseCoefficientMap({{z.typed_id(), 5.0}}),
.offset = 6.0}},
.name = "soc",
};
const SecondOrderConeConstraint c(&storage,
storage.AddAtomicConstraint(data));
EXPECT_EQ(c.name(), "soc");
EXPECT_EQ(c.storage(), &storage);
{
const LinearExpression ub = c.UpperBound();
EXPECT_EQ(ub.offset(), 2.0);
EXPECT_THAT(ub.terms(), UnorderedElementsAre(Pair(x, 1.0)));
}
{
const std::vector<LinearExpression> args = c.ArgumentsToNorm();
ASSERT_EQ(args.size(), 2);
EXPECT_EQ(args[0].offset(), 4.0);
EXPECT_THAT(args[0].terms(), UnorderedElementsAre(Pair(y, 3.0)));
EXPECT_EQ(args[1].offset(), 6.0);
EXPECT_THAT(args[1].terms(), UnorderedElementsAre(Pair(z, 5.0)));
}
}
TEST(SecondOrderConeConstraintTest, Equality) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const SecondOrderConeConstraint c(
&storage, storage.AddAtomicConstraint(SecondOrderConeConstraintData{
.upper_bound = {.offset = 1.0}, .name = "c"}));
const SecondOrderConeConstraint d(
&storage, storage.AddAtomicConstraint(SecondOrderConeConstraintData{
.upper_bound = {.offset = 2.0}, .name = "d"}));
// `d2` is another `SecondOrderConstraint` that points the same constraint in
// the indexed storage. It should compares == to `d`.
const SecondOrderConeConstraint d2(d.storage(), d.typed_id());
// `e` has identical data as `d`. It should not compares equal to `d`, though.
const SecondOrderConeConstraint e(
&storage, storage.AddAtomicConstraint(SecondOrderConeConstraintData{
.upper_bound = {.offset = 2.0}, .name = "d"}));
EXPECT_TRUE(c == c);
EXPECT_FALSE(c == d);
EXPECT_TRUE(d == d2);
EXPECT_FALSE(d == e);
EXPECT_FALSE(c != c);
EXPECT_TRUE(c != d);
EXPECT_FALSE(d != d2);
EXPECT_TRUE(d != e);
}
TEST(SecondOrderConeConstraintTest, NonzeroVariables) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Variable z(&storage, storage.AddVariable("z"));
const SecondOrderConeConstraintData data{
.upper_bound = {.coeffs = SparseCoefficientMap({{x.typed_id(), 1.0}}),
.offset = 2.0},
.arguments_to_norm = {{.coeffs =
SparseCoefficientMap({{y.typed_id(), 3.0}}),
.offset = 4.0},
{.coeffs =
SparseCoefficientMap({{z.typed_id(), 5.0}}),
.offset = 6.0}},
.name = "soc",
};
const SecondOrderConeConstraint c(&storage,
storage.AddAtomicConstraint(data));
EXPECT_THAT(c.NonzeroVariables(), UnorderedElementsAre(x, y, z));
}
TEST(SecondOrderConeConstraintTest, ToString) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Variable z(&storage, storage.AddVariable("z"));
const SecondOrderConeConstraintData data{
.upper_bound = {.coeffs = SparseCoefficientMap({{x.typed_id(), 1.0}}),
.offset = 2.0},
.arguments_to_norm = {{.coeffs =
SparseCoefficientMap({{y.typed_id(), 3.0}}),
.offset = 4.0},
{.coeffs =
SparseCoefficientMap({{z.typed_id(), 5.0}}),
.offset = 6.0}},
.name = "soc",
};
const SecondOrderConeConstraint c(&storage,
storage.AddAtomicConstraint(data));
EXPECT_EQ(c.ToString(), "||{3*y + 4, 5*z + 6}||₂ ≤ x + 2");
storage.DeleteAtomicConstraint(c.typed_id());
EXPECT_EQ(c.ToString(), kDeletedConstraintDefaultDescription);
}
TEST(SecondOrderConeConstraintTest, OutputStreaming) {
ModelStorage storage;
const SecondOrderConeConstraint q(
&storage,
storage.AddAtomicConstraint(SecondOrderConeConstraintData{.name = "q"}));
const SecondOrderConeConstraint anonymous(
&storage,
storage.AddAtomicConstraint(SecondOrderConeConstraintData{.name = ""}));
auto to_string = [](SecondOrderConeConstraint c) {
std::ostringstream oss;
oss << c;
return oss.str();
};
EXPECT_EQ(to_string(q), "q");
EXPECT_EQ(to_string(anonymous),
absl::StrCat("__soc_con#", anonymous.id(), "__"));
}
TEST(SecondOrderConeConstraintTest, NameAfterDeletion) {
ModelStorage storage;
const SecondOrderConeConstraintData data{.name = "soc"};
const SecondOrderConeConstraint c(&storage,
storage.AddAtomicConstraint(data));
ASSERT_EQ(c.name(), "soc");
storage.DeleteAtomicConstraint(c.typed_id());
EXPECT_EQ(c.name(), kDeletedConstraintDefaultDescription);
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,130 @@
// Copyright 2010-2024 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/math_opt/constraints/second_order_cone/storage.h"
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/model.pb.h"
#include "ortools/math_opt/sparse_containers.pb.h"
#include "ortools/math_opt/storage/linear_expression_data.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
namespace operations_research::math_opt {
namespace {
using ::testing::AllOf;
using ::testing::ElementsAre;
using ::testing::Eq;
using ::testing::EquivToProto;
using ::testing::Field;
using ::testing::Matcher;
using ::testing::Property;
using ::testing::UnorderedElementsAre;
using ::testing::UnorderedElementsAreArray;
SecondOrderConeConstraintProto SimpleProto() {
SecondOrderConeConstraintProto proto;
proto.set_name("soc");
{
auto& expr = *proto.mutable_upper_bound();
expr.add_ids(0);
expr.add_coefficients(1.0);
}
{
auto& expr = *proto.add_arguments_to_norm();
expr.add_ids(3);
expr.add_ids(6);
expr.add_coefficients(2.0);
expr.add_coefficients(3.0);
expr.set_offset(4.0);
}
return proto;
}
SecondOrderConeConstraintData SimpleData() {
return {
.upper_bound = {.coeffs = SparseCoefficientMap({{VariableId(0), 1.0}}),
.offset = 0.0},
.arguments_to_norm = {{.coeffs = SparseCoefficientMap(
{{VariableId(3), 2.0}, {VariableId(6), 3.0}}),
.offset = 4.0}},
.name = "soc"};
}
Matcher<LinearExpressionData> LinearExprEquals(
const LinearExpressionData& other) {
return AllOf(
Field("offset", &LinearExpressionData::offset, Eq(other.offset)),
Field("coeffs", &LinearExpressionData::coeffs,
Property("terms", &SparseCoefficientMap::terms,
UnorderedElementsAreArray(other.coeffs.terms()))));
}
TEST(SecondOrderConeStorageDataTest, RelatedVariables) {
EXPECT_THAT(
SimpleData().RelatedVariables(),
UnorderedElementsAre(VariableId(0), VariableId(3), VariableId(6)));
}
TEST(SecondOrderConeStorageDataTest, DeleteVariable) {
SecondOrderConeConstraintData data = SimpleData();
data.DeleteVariable(VariableId(3));
EXPECT_THAT(
data.upper_bound,
LinearExprEquals({.coeffs = SparseCoefficientMap({{VariableId(0), 1.0}}),
.offset = 0.0}));
EXPECT_THAT(data.arguments_to_norm,
ElementsAre(LinearExprEquals(
{.coeffs = SparseCoefficientMap({{VariableId(6), 3.0}}),
.offset = 4.0})));
data.DeleteVariable(VariableId(0));
EXPECT_THAT(data.upper_bound,
LinearExprEquals({.coeffs = {}, .offset = 0.0}));
EXPECT_THAT(data.arguments_to_norm,
ElementsAre(LinearExprEquals(
{.coeffs = SparseCoefficientMap({{VariableId(6), 3.0}}),
.offset = 4.0})));
data.DeleteVariable(VariableId(6));
EXPECT_THAT(data.upper_bound,
LinearExprEquals({.coeffs = {}, .offset = 0.0}));
EXPECT_THAT(data.arguments_to_norm,
ElementsAre(LinearExprEquals({.coeffs = {}, .offset = 4.0})));
}
TEST(SecondOrderConeStorageDataTest, FromProto) {
const auto data = SecondOrderConeConstraintData::FromProto(SimpleProto());
EXPECT_EQ(data.name, "soc");
EXPECT_THAT(
data.upper_bound,
LinearExprEquals({.coeffs = SparseCoefficientMap({{VariableId(0), 1.0}}),
.offset = 0.0}));
EXPECT_THAT(data.arguments_to_norm,
ElementsAre(LinearExprEquals(
{.coeffs = SparseCoefficientMap(
{{VariableId(3), 2.0}, {VariableId(6), 3.0}}),
.offset = 4.0})));
}
TEST(SosStorageDataTest, Proto) {
EXPECT_THAT(SimpleData().Proto(), EquivToProto(SimpleProto()));
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,87 @@
// Copyright 2010-2024 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/math_opt/constraints/second_order_cone/validator.h"
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <string>
#include <type_traits>
#include "absl/status/status.h"
#include "absl/types/span.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/logging.h"
#include "ortools/math_opt/core/model_summary.h"
#include "ortools/math_opt/model.pb.h"
#include "ortools/math_opt/sparse_containers.pb.h"
namespace operations_research::math_opt {
namespace {
using ::testing::HasSubstr;
using ::testing::status::StatusIs;
IdNameBiMap SimpleVariableUniverse(const std::initializer_list<int64_t> ids) {
IdNameBiMap universe;
CHECK_OK(universe.BulkUpdate({}, ids, {}));
return universe;
}
SecondOrderConeConstraintProto SimpleSecondOrderConeConstraint() {
SecondOrderConeConstraintProto data;
{
LinearExpressionProto& expr = *data.mutable_upper_bound();
expr.add_ids(1);
expr.add_coefficients(2.0);
expr.set_offset(3.0);
}
{
LinearExpressionProto& expr = *data.add_arguments_to_norm();
expr.add_ids(2);
expr.add_coefficients(3.0);
expr.set_offset(4.0);
}
return data;
}
TEST(SecondOrderConeValidatorTest, EmptyConstraintOk) {
EXPECT_OK(ValidateConstraint(SecondOrderConeConstraintProto(),
SimpleVariableUniverse({})));
}
TEST(SecondOrderConeValidatorTest, SimpleConstraintOk) {
EXPECT_OK(ValidateConstraint(SimpleSecondOrderConeConstraint(),
SimpleVariableUniverse({1, 2})));
}
TEST(SosConstraintValidatorTest, InvalidUpperBound) {
SecondOrderConeConstraintProto data = SimpleSecondOrderConeConstraint();
data.mutable_upper_bound()->add_ids(2);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1, 2})),
StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("upper_bound")));
}
TEST(SosConstraintValidatorTest, InvalidArgumentsToNorm) {
SecondOrderConeConstraintProto data = SimpleSecondOrderConeConstraint();
data.mutable_arguments_to_norm(0)->add_ids(2);
EXPECT_THAT(ValidateConstraint(data, SimpleVariableUniverse({1})),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("arguments_to_norm")));
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -29,6 +29,23 @@ cc_library(
],
)
cc_test(
name = "sos1_constraint_test",
srcs = ["sos1_constraint_test.cc"],
deps = [
":sos1_constraint",
"//ortools/base:gmock",
"//ortools/base:gmock_main",
"//ortools/math_opt/constraints/util:model_util",
"//ortools/math_opt/cpp:math_opt",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:model_storage",
"//ortools/math_opt/storage:sparse_coefficient_map",
"//ortools/util:fp_roundtrip_conv_testing",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "sos2_constraint",
srcs = ["sos2_constraint.cc"],
@@ -45,6 +62,23 @@ cc_library(
],
)
cc_test(
name = "sos2_constraint_test",
srcs = ["sos2_constraint_test.cc"],
deps = [
":sos2_constraint",
"//ortools/base:gmock",
"//ortools/base:gmock_main",
"//ortools/math_opt/constraints/util:model_util",
"//ortools/math_opt/cpp:math_opt",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:model_storage",
"//ortools/math_opt/storage:sparse_coefficient_map",
"//ortools/util:fp_roundtrip_conv_testing",
"@com_google_absl//absl/strings",
],
)
cc_library(
name = "storage",
hdrs = ["storage.h"],
@@ -60,6 +94,21 @@ cc_library(
],
)
cc_test(
name = "storage_test",
srcs = ["storage_test.cc"],
deps = [
":storage",
"//ortools/base:gmock_main",
"//ortools/base:intops",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:sparse_coefficient_map",
"@com_google_absl//absl/container:flat_hash_map",
],
)
cc_library(
name = "util",
hdrs = ["util.h"],
@@ -85,3 +134,19 @@ cc_library(
"@com_google_absl//absl/status",
],
)
cc_test(
name = "validator_test",
srcs = ["validator_test.cc"],
deps = [
":validator",
"//ortools/base:gmock_main",
"//ortools/base:logging",
"//ortools/base:status_macros",
"//ortools/math_opt:model_cc_proto",
"//ortools/math_opt:sparse_containers_cc_proto",
"//ortools/math_opt/core:model_summary",
"@com_google_absl//absl/status",
"@com_google_absl//absl/types:span",
],
)

View File

@@ -15,6 +15,8 @@ set(NAME ${PROJECT_NAME}_math_opt_constraints_sos)
add_library(${NAME} OBJECT)
file(GLOB _SRCS "*.h" "*.cc")
list(FILTER _SRCS EXCLUDE REGEX ".*_test.cc")
target_sources(${NAME} PRIVATE ${_SRCS})
set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(${NAME} PUBLIC

View File

@@ -0,0 +1,150 @@
// Copyright 2010-2024 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/math_opt/constraints/sos/sos1_constraint.h"
#include <sstream>
#include <string>
#include <vector>
#include "absl/strings/str_cat.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/math_opt/constraints/util/model_util.h"
#include "ortools/math_opt/cpp/math_opt.h"
#include "ortools/math_opt/storage/linear_expression_data.h"
#include "ortools/math_opt/storage/model_storage.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
#include "ortools/util/fp_roundtrip_conv_testing.h"
namespace operations_research::math_opt {
namespace {
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
TEST(Sos1ConstraintTest, Accessors) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Sos1ConstraintData data(
{{.coeffs = SparseCoefficientMap({{x.typed_id(), 1.0}}), .offset = 0.0},
{.coeffs =
SparseCoefficientMap({{x.typed_id(), 0.5}, {y.typed_id(), 0.5}}),
.offset = -1.0}},
{3.0, 2.0}, "c");
const Sos1Constraint c(&storage, storage.AddAtomicConstraint(data));
EXPECT_EQ(c.name(), "c");
ASSERT_EQ(c.num_expressions(), 2);
EXPECT_THAT(c.Expression(0).terms(), UnorderedElementsAre(Pair(x, 1.0)));
EXPECT_EQ(c.Expression(0).offset(), 0.0);
EXPECT_TRUE(c.has_weights());
EXPECT_EQ(c.weight(0), 3.0);
EXPECT_THAT(c.Expression(1).terms(),
UnorderedElementsAre(Pair(x, 0.5), Pair(y, 0.5)));
EXPECT_EQ(c.Expression(1).offset(), -1.0);
EXPECT_EQ(c.weight(1), 2.0);
}
TEST(Sos1ConstraintTest, Equality) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Sos1Constraint c(
&storage, storage.AddAtomicConstraint(Sos1ConstraintData({}, {}, "c")));
const Sos1Constraint d(
&storage, storage.AddAtomicConstraint(Sos1ConstraintData({}, {}, "d")));
// `d2` is another `Sos1Constraint` that points the same constraint in the
// indexed storage. It should compares == to `d`.
const Sos1Constraint d2(d.storage(), d.typed_id());
// `e` has identical data as `d`. It should not compares equal to `d`, though.
const Sos1Constraint e(
&storage, storage.AddAtomicConstraint(Sos1ConstraintData({}, {}, "d")));
EXPECT_TRUE(c == c);
EXPECT_FALSE(c == d);
EXPECT_TRUE(d == d2);
EXPECT_FALSE(d == e);
EXPECT_FALSE(c != c);
EXPECT_TRUE(c != d);
EXPECT_FALSE(d != d2);
EXPECT_TRUE(d != e);
}
TEST(Sos1ConstraintTest, NonzeroVariables) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Variable z(&storage, storage.AddVariable("z"));
const Variable w(&storage, storage.AddVariable("w"));
const Sos1ConstraintData data(
{{.coeffs = SparseCoefficientMap({{z.typed_id(), 1.0}}), .offset = 0.0},
{.coeffs =
SparseCoefficientMap({{x.typed_id(), 0.5}, {y.typed_id(), 0.5}}),
.offset = -1.0}},
{3.0, 2.0}, "c");
const Sos1Constraint c(&storage, storage.AddAtomicConstraint(data));
EXPECT_THAT(c.NonzeroVariables(), UnorderedElementsAre(x, y, z));
}
TEST(Sos1ConstraintTest, ToString) {
Model model;
const Variable x = model.AddVariable("x");
const Variable y = model.AddVariable("y");
const Sos1Constraint c =
model.AddSos1Constraint({x, y}, {3.0, kRoundTripTestNumber}, "c");
EXPECT_EQ(c.ToString(), absl::StrCat("{x, y} is SOS1 with weights {3, ",
kRoundTripTestNumberStr, "}"));
const Sos1Constraint d = model.AddSos1Constraint({2 * y - 1, 1.0}, {}, "d");
EXPECT_EQ(d.ToString(), "{2*y - 1, 1} is SOS1");
model.DeleteSos1Constraint(c);
EXPECT_EQ(c.ToString(), kDeletedConstraintDefaultDescription);
}
TEST(Sos1ConstraintTest, OutputStreaming) {
ModelStorage storage;
const Sos1Constraint q(
&storage, storage.AddAtomicConstraint(Sos1ConstraintData({}, {}, "q")));
const Sos1Constraint anonymous(
&storage, storage.AddAtomicConstraint(Sos1ConstraintData({}, {}, "")));
auto to_string = [](Sos1Constraint c) {
std::ostringstream oss;
oss << c;
return oss.str();
};
EXPECT_EQ(to_string(q), "q");
EXPECT_EQ(to_string(anonymous),
absl::StrCat("__sos1_con#", anonymous.id(), "__"));
}
TEST(Sos1ConstraintTest, NameAfterDeletion) {
Model model;
const Variable x = model.AddVariable("x");
const Sos1Constraint c = model.AddSos1Constraint({x}, {}, "c");
ASSERT_EQ(c.name(), "c");
model.DeleteSos1Constraint(c);
EXPECT_EQ(c.name(), kDeletedConstraintDefaultDescription);
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,150 @@
// Copyright 2010-2024 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/math_opt/constraints/sos/sos2_constraint.h"
#include <sstream>
#include <string>
#include <vector>
#include "absl/strings/str_cat.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/math_opt/constraints/util/model_util.h"
#include "ortools/math_opt/cpp/math_opt.h"
#include "ortools/math_opt/storage/linear_expression_data.h"
#include "ortools/math_opt/storage/model_storage.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
#include "ortools/util/fp_roundtrip_conv_testing.h"
namespace operations_research::math_opt {
namespace {
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
TEST(Sos2ConstraintTest, Accessors) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Sos2ConstraintData data(
{{.coeffs = SparseCoefficientMap({{x.typed_id(), 1.0}}), .offset = 0.0},
{.coeffs =
SparseCoefficientMap({{x.typed_id(), 0.5}, {y.typed_id(), 0.5}}),
.offset = -1.0}},
{3.0, 2.0}, "c");
const Sos2Constraint c(&storage, storage.AddAtomicConstraint(data));
EXPECT_EQ(c.name(), "c");
ASSERT_EQ(c.num_expressions(), 2);
EXPECT_THAT(c.Expression(0).terms(), UnorderedElementsAre(Pair(x, 1.0)));
EXPECT_EQ(c.Expression(0).offset(), 0.0);
EXPECT_TRUE(c.has_weights());
EXPECT_EQ(c.weight(0), 3.0);
EXPECT_THAT(c.Expression(1).terms(),
UnorderedElementsAre(Pair(x, 0.5), Pair(y, 0.5)));
EXPECT_EQ(c.Expression(1).offset(), -1.0);
EXPECT_EQ(c.weight(1), 2.0);
}
TEST(Sos2ConstraintTest, Equality) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Sos2Constraint c(
&storage, storage.AddAtomicConstraint(Sos2ConstraintData({}, {}, "c")));
const Sos2Constraint d(
&storage, storage.AddAtomicConstraint(Sos2ConstraintData({}, {}, "d")));
// `d2` is another `Sos2Constraint` that points the same constraint in the
// indexed storage. It should compares == to `d`.
const Sos2Constraint d2(d.storage(), d.typed_id());
// `e` has identical data as `d`. It should not compares equal to `d`, though.
const Sos2Constraint e(
&storage, storage.AddAtomicConstraint(Sos2ConstraintData({}, {}, "d")));
EXPECT_TRUE(c == c);
EXPECT_FALSE(c == d);
EXPECT_TRUE(d == d2);
EXPECT_FALSE(d == e);
EXPECT_FALSE(c != c);
EXPECT_TRUE(c != d);
EXPECT_FALSE(d != d2);
EXPECT_TRUE(d != e);
}
TEST(Sos2ConstraintTest, NonzeroVariables) {
ModelStorage storage;
const Variable x(&storage, storage.AddVariable("x"));
const Variable y(&storage, storage.AddVariable("y"));
const Variable z(&storage, storage.AddVariable("z"));
const Variable w(&storage, storage.AddVariable("w"));
const Sos2ConstraintData data(
{{.coeffs = SparseCoefficientMap({{z.typed_id(), 1.0}}), .offset = 0.0},
{.coeffs =
SparseCoefficientMap({{x.typed_id(), 0.5}, {y.typed_id(), 0.5}}),
.offset = -1.0}},
{3.0, 2.0}, "c");
const Sos2Constraint c(&storage, storage.AddAtomicConstraint(data));
EXPECT_THAT(c.NonzeroVariables(), UnorderedElementsAre(x, y, z));
}
TEST(Sos2ConstraintTest, ToString) {
Model model;
const Variable x = model.AddVariable("x");
const Variable y = model.AddVariable("y");
const Sos2Constraint c =
model.AddSos2Constraint({x, y}, {3.0, kRoundTripTestNumber}, "c");
EXPECT_EQ(c.ToString(), absl::StrCat("{x, y} is SOS2 with weights {3, ",
kRoundTripTestNumberStr, "}"));
const Sos2Constraint d = model.AddSos2Constraint({2 * y - 1, 1.0}, {}, "d");
EXPECT_EQ(d.ToString(), "{2*y - 1, 1} is SOS2");
model.DeleteSos2Constraint(c);
EXPECT_EQ(c.ToString(), kDeletedConstraintDefaultDescription);
}
TEST(Sos2ConstraintTest, OutputStreaming) {
ModelStorage storage;
const Sos2Constraint q(
&storage, storage.AddAtomicConstraint(Sos2ConstraintData({}, {}, "q")));
const Sos2Constraint anonymous(
&storage, storage.AddAtomicConstraint(Sos2ConstraintData({}, {}, "")));
auto to_string = [](Sos2Constraint c) {
std::ostringstream oss;
oss << c;
return oss.str();
};
EXPECT_EQ(to_string(q), "q");
EXPECT_EQ(to_string(anonymous),
absl::StrCat("__sos2_con#", anonymous.id(), "__"));
}
TEST(Sos2ConstraintTest, NameAfterDeletion) {
Model model;
const Variable x = model.AddVariable("x");
const Sos2Constraint c = model.AddSos2Constraint({x}, {}, "c");
ASSERT_EQ(c.name(), "c");
model.DeleteSos2Constraint(c);
EXPECT_EQ(c.name(), kDeletedConstraintDefaultDescription);
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,156 @@
// Copyright 2010-2024 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/math_opt/constraints/sos/storage.h"
#include <vector>
#include "absl/container/flat_hash_map.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/model.pb.h"
#include "ortools/math_opt/sparse_containers.pb.h"
#include "ortools/math_opt/storage/linear_expression_data.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
namespace operations_research::math_opt {
namespace {
// We test with SOS1; there is no difference with SOS2 at the storage level.
using TestConstraintData = Sos1ConstraintData;
using ::testing::AllOf;
using ::testing::Eq;
using ::testing::EquivToProto;
using ::testing::Field;
using ::testing::Matcher;
using ::testing::Property;
using ::testing::UnorderedElementsAre;
using ::testing::UnorderedElementsAreArray;
SosConstraintProto SimpleProto(const bool with_weights = true) {
SosConstraintProto proto;
proto.set_name("q");
if (with_weights) {
proto.add_weights(2.0);
proto.add_weights(3.0);
}
{
auto& expr = *proto.add_expressions();
expr.add_ids(0);
expr.add_coefficients(1.0);
}
{
auto& expr = *proto.add_expressions();
expr.add_ids(3);
expr.add_ids(6);
expr.add_coefficients(2.0);
expr.add_coefficients(3.0);
expr.set_offset(4.0);
}
return proto;
}
TestConstraintData SimpleData(const bool with_weights = true) {
std::vector<double> weights;
if (with_weights) {
weights.push_back(2.0);
weights.push_back(3.0);
}
return TestConstraintData(
{{.coeffs = SparseCoefficientMap({{VariableId(0), 1.0}}), .offset = 0.0},
{.coeffs =
SparseCoefficientMap({{VariableId(3), 2.0}, {VariableId(6), 3.0}}),
.offset = 4.0}},
weights, "q");
}
Matcher<LinearExpressionData> LinearExprEquals(
const LinearExpressionData& other) {
return AllOf(
Field("offset", &LinearExpressionData::offset, Eq(other.offset)),
Field("coeffs", &LinearExpressionData::coeffs,
Property("terms", &SparseCoefficientMap::terms,
UnorderedElementsAreArray(other.coeffs.terms()))));
}
TEST(SosStorageDataTest, RelatedVariables) {
EXPECT_THAT(
SimpleData().RelatedVariables(),
UnorderedElementsAre(VariableId(0), VariableId(3), VariableId(6)));
}
TEST(SosStorageDataTest, DeleteVariable) {
TestConstraintData data = SimpleData();
data.DeleteVariable(VariableId(3));
ASSERT_THAT(data.num_expressions(), 2);
EXPECT_EQ(data.weight(0), 2.0);
EXPECT_THAT(
data.expression(0),
LinearExprEquals({.coeffs = SparseCoefficientMap({{VariableId(0), 1.0}}),
.offset = 0.0}));
EXPECT_EQ(data.weight(1), 3.0);
EXPECT_THAT(
data.expression(1),
LinearExprEquals({.coeffs = SparseCoefficientMap({{VariableId(6), 3.0}}),
.offset = 4.0}));
data.DeleteVariable(VariableId(0));
ASSERT_THAT(data.num_expressions(), 2);
EXPECT_EQ(data.weight(0), 2.0);
EXPECT_THAT(data.expression(0),
LinearExprEquals(LinearExpressionData{.offset = 0.0}));
EXPECT_EQ(data.weight(1), 3.0);
EXPECT_THAT(
data.expression(1),
LinearExprEquals({.coeffs = SparseCoefficientMap({{VariableId(6), 3.0}}),
.offset = 4.0}));
data.DeleteVariable(VariableId(6));
ASSERT_THAT(data.num_expressions(), 2);
EXPECT_EQ(data.weight(0), 2.0);
EXPECT_THAT(data.expression(0),
LinearExprEquals(LinearExpressionData{.offset = 0.0}));
EXPECT_EQ(data.weight(1), 3.0);
EXPECT_THAT(data.expression(1),
LinearExprEquals(LinearExpressionData{.offset = 4.0}));
}
TEST(SosStorageDataTest, FromProto) {
const auto data = TestConstraintData::FromProto(SimpleProto());
EXPECT_EQ(data.name(), "q");
ASSERT_THAT(data.num_expressions(), 2);
EXPECT_EQ(data.weight(0), 2.0);
EXPECT_THAT(
data.expression(0),
LinearExprEquals({.coeffs = SparseCoefficientMap({{VariableId(0), 1.0}}),
.offset = 0.0}));
EXPECT_EQ(data.weight(1), 3.0);
EXPECT_THAT(
data.expression(1),
LinearExprEquals({.coeffs = SparseCoefficientMap(
{{VariableId(3), 2.0}, {VariableId(6), 3.0}}),
.offset = 4.0}));
}
TEST(SosStorageDataTest, Proto) {
EXPECT_THAT(SimpleData().Proto(), EquivToProto(SimpleProto()));
}
TEST(SosStorageDataTest, ProtoUnsetWeights) {
EXPECT_THAT(SimpleData(/*with_weights=*/false).Proto(),
EquivToProto(SimpleProto(/*with_weights=*/false)));
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -0,0 +1,110 @@
// Copyright 2010-2024 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/math_opt/constraints/sos/validator.h"
#include <cstdint>
#include <initializer_list>
#include <limits>
#include <string>
#include <type_traits>
#include "absl/status/status.h"
#include "absl/types/span.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/logging.h"
#include "ortools/math_opt/core/model_summary.h"
#include "ortools/math_opt/model.pb.h"
#include "ortools/math_opt/sparse_containers.pb.h"
namespace operations_research::math_opt {
namespace {
using ::testing::AllOf;
using ::testing::HasSubstr;
using ::testing::status::StatusIs;
IdNameBiMap SimpleVariableUniverse(const std::initializer_list<int64_t> ids) {
IdNameBiMap universe;
CHECK_OK(universe.BulkUpdate({}, ids, {}));
return universe;
}
SosConstraintProto SimpleSosConstraintProto() {
SosConstraintProto data;
LinearExpressionProto& expr = *data.add_expressions();
expr.add_ids(1);
expr.add_coefficients(3.0);
expr.set_offset(4.0);
data.add_weights(2.0);
return data;
}
TEST(SosDataValidatorTest, EmptyConstraintOk) {
EXPECT_OK(
ValidateConstraint(SosConstraintProto(), SimpleVariableUniverse({})));
}
TEST(SosConstraintValidatorTest, SimpleConstraintOk) {
EXPECT_OK(ValidateConstraint(SimpleSosConstraintProto(),
SimpleVariableUniverse({1})));
}
TEST(SosConstraintValidatorTest, WeightsExpressionSizeMismatch) {
SosConstraintProto data = SimpleSosConstraintProto();
data.add_weights(3.0);
EXPECT_THAT(
ValidateConstraint(data, SimpleVariableUniverse({1})),
StatusIs(absl::StatusCode::kInvalidArgument, HasSubstr("mismatch")));
}
TEST(SosConstraintValidatorTest, InvalidLinearExpression) {
SosConstraintProto data = SimpleSosConstraintProto();
data.mutable_expressions(0)->add_ids(2);
EXPECT_THAT(ValidateConstraint(data, SimpleVariableUniverse({1})),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("linear expression")));
}
TEST(SosConstraintValidatorTest, InfWeight) {
SosConstraintProto data = SimpleSosConstraintProto();
data.set_weights(0, std::numeric_limits<double>::infinity());
EXPECT_THAT(ValidateConstraint(data, SimpleVariableUniverse({1})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("value: inf"),
HasSubstr("Invalid SOS weight"))));
}
TEST(SosConstraintValidatorTest, NanWeight) {
SosConstraintProto data = SimpleSosConstraintProto();
data.set_weights(0, std::numeric_limits<double>::quiet_NaN());
EXPECT_THAT(ValidateConstraint(data, SimpleVariableUniverse({1})),
StatusIs(absl::StatusCode::kInvalidArgument,
AllOf(HasSubstr("value: nan"),
HasSubstr("Invalid SOS weight"))));
}
TEST(SosConstraintValidatorTest, RepeatWeight) {
SosConstraintProto data;
data.add_expressions();
data.add_expressions();
data.add_weights(1.0);
data.add_weights(1.0);
EXPECT_THAT(ValidateConstraint(data, SimpleVariableUniverse({1})),
StatusIs(absl::StatusCode::kInvalidArgument,
HasSubstr("duplicate weight")));
}
} // namespace
} // namespace operations_research::math_opt

View File

@@ -27,3 +27,18 @@ cc_library(
"@com_google_absl//absl/strings",
],
)
cc_test(
name = "model_util_test",
srcs = ["model_util_test.cc"],
deps = [
":model_util",
"//ortools/base:gmock_main",
"//ortools/base:intops",
"//ortools/math_opt/constraints/quadratic:storage",
"//ortools/math_opt/cpp:math_opt",
"//ortools/math_opt/storage:linear_expression_data",
"//ortools/math_opt/storage:model_storage",
"//ortools/math_opt/storage:sparse_coefficient_map",
],
)

View File

@@ -15,6 +15,8 @@ set(NAME ${PROJECT_NAME}_math_opt_constraints_util)
add_library(${NAME} OBJECT)
file(GLOB _SRCS "*.h" "*.cc")
list(FILTER _SRCS EXCLUDE REGEX ".*_test.cc")
target_sources(${NAME} PRIVATE ${_SRCS})
set_target_properties(${NAME} PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(${NAME} PUBLIC

View File

@@ -0,0 +1,85 @@
// Copyright 2010-2024 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/math_opt/constraints/util/model_util.h"
#include "gtest/gtest.h"
#include "ortools/base/gmock.h"
#include "ortools/base/strong_int.h"
#include "ortools/math_opt/constraints/quadratic/storage.h"
#include "ortools/math_opt/cpp/math_opt.h"
#include "ortools/math_opt/storage/linear_expression_data.h"
#include "ortools/math_opt/storage/model_storage.h"
#include "ortools/math_opt/storage/sparse_coefficient_map.h"
namespace operations_research::math_opt {
namespace {
using ::testing::ElementsAre;
using ::testing::Pair;
using ::testing::UnorderedElementsAre;
using TestConstraint = QuadraticConstraint;
using TestData = QuadraticConstraintData;
using TestConstraintId = QuadraticConstraintId;
TEST(ModelUtilTest, ToLinearExpression) {
const VariableId u(0);
const VariableId v(1);
LinearExpressionData data{.offset = 4.0};
data.coeffs.set(u, 2.0);
data.coeffs.set(v, 3.0);
const ModelStorage storage;
const LinearExpression expr = ToLinearExpression(storage, data);
EXPECT_EQ(expr.storage(), &storage);
EXPECT_EQ(expr.offset(), 4.0);
EXPECT_THAT(expr.terms(),
UnorderedElementsAre(Pair(Variable(&storage, u), 2.0),
Pair(Variable(&storage, v), 3.0)));
}
TEST(ModelUtilTest, FromLinearExpression) {
Model model;
const Variable x = model.AddVariable();
const Variable y = model.AddVariable();
const LinearExpression expr = 2.0 * x + 3.0 * y + 4.0;
const auto [coeffs, offset] = FromLinearExpression(expr);
EXPECT_THAT(coeffs.terms(), UnorderedElementsAre(Pair(x.typed_id(), 2.0),
Pair(y.typed_id(), 3.0)));
EXPECT_EQ(offset, 4.0);
}
TEST(ModelUtilTest, AtomicConstraints) {
ModelStorage storage;
const TestConstraintId c = storage.AddAtomicConstraint(TestData());
const TestConstraintId d = storage.AddAtomicConstraint(TestData());
EXPECT_THAT(AtomicConstraints<TestConstraint>(storage),
UnorderedElementsAre(TestConstraint(&storage, c),
TestConstraint(&storage, d)));
}
TEST(ModelUtilTest, SortedAtomicConstraints) {
ModelStorage storage;
const TestConstraintId c = storage.AddAtomicConstraint(TestData());
const TestConstraintId d = storage.AddAtomicConstraint(TestData());
EXPECT_THAT(
SortedAtomicConstraints<TestConstraint>(storage),
ElementsAre(TestConstraint(&storage, c), TestConstraint(&storage, d)));
}
} // namespace
} // namespace operations_research::math_opt