From 5ffb246ccf5cc85e92ecab0155608daea7de84e2 Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 26 Feb 2024 17:13:24 +0100 Subject: [PATCH] math_opt: export constraints tests --- ortools/base/BUILD.bazel | 1 + ortools/base/message_matchers.h | 93 +++++++- .../constraints/indicator/BUILD.bazel | 44 ++++ .../constraints/indicator/CMakeLists.txt | 2 + .../indicator/indicator_constraint_test.cc | 179 ++++++++++++++++ .../constraints/indicator/storage_test.cc | 126 +++++++++++ .../constraints/indicator/validator_test.cc | 114 ++++++++++ .../constraints/quadratic/BUILD.bazel | 46 ++++ .../constraints/quadratic/CMakeLists.txt | 2 + .../quadratic/quadratic_constraint_test.cc | 193 +++++++++++++++++ .../constraints/quadratic/storage_test.cc | 103 +++++++++ .../constraints/quadratic/validator_test.cc | 199 ++++++++++++++++++ .../constraints/second_order_cone/BUILD.bazel | 46 ++++ .../second_order_cone/CMakeLists.txt | 2 + .../second_order_cone_constraint_test.cc | 182 ++++++++++++++++ .../second_order_cone/storage_test.cc | 130 ++++++++++++ .../second_order_cone/validator_test.cc | 87 ++++++++ ortools/math_opt/constraints/sos/BUILD.bazel | 65 ++++++ .../math_opt/constraints/sos/CMakeLists.txt | 2 + .../constraints/sos/sos1_constraint_test.cc | 150 +++++++++++++ .../constraints/sos/sos2_constraint_test.cc | 150 +++++++++++++ .../math_opt/constraints/sos/storage_test.cc | 156 ++++++++++++++ .../constraints/sos/validator_test.cc | 110 ++++++++++ ortools/math_opt/constraints/util/BUILD.bazel | 15 ++ .../math_opt/constraints/util/CMakeLists.txt | 2 + .../constraints/util/model_util_test.cc | 85 ++++++++ 26 files changed, 2282 insertions(+), 2 deletions(-) create mode 100644 ortools/math_opt/constraints/indicator/indicator_constraint_test.cc create mode 100644 ortools/math_opt/constraints/indicator/storage_test.cc create mode 100644 ortools/math_opt/constraints/indicator/validator_test.cc create mode 100644 ortools/math_opt/constraints/quadratic/quadratic_constraint_test.cc create mode 100644 ortools/math_opt/constraints/quadratic/storage_test.cc create mode 100644 ortools/math_opt/constraints/quadratic/validator_test.cc create mode 100644 ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint_test.cc create mode 100644 ortools/math_opt/constraints/second_order_cone/storage_test.cc create mode 100644 ortools/math_opt/constraints/second_order_cone/validator_test.cc create mode 100644 ortools/math_opt/constraints/sos/sos1_constraint_test.cc create mode 100644 ortools/math_opt/constraints/sos/sos2_constraint_test.cc create mode 100644 ortools/math_opt/constraints/sos/storage_test.cc create mode 100644 ortools/math_opt/constraints/sos/validator_test.cc create mode 100644 ortools/math_opt/constraints/util/model_util_test.cc diff --git a/ortools/base/BUILD.bazel b/ortools/base/BUILD.bazel index 2a2a7c2eb3..1a754e05f9 100644 --- a/ortools/base/BUILD.bazel +++ b/ortools/base/BUILD.bazel @@ -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", ], diff --git a/ortools/base/message_matchers.h b/ortools/base/message_matchers.h index 3d28579505..9e82ccdaf3 100644 --- a/ortools/base/message_matchers.h +++ b/ortools/base/message_matchers.h @@ -16,10 +16,83 @@ #include +#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 ignore_fields; + std::vector ignore_field_paths; + std::vector 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 message_; }; -inline ProtoMatcher EqualsProto(const ::google::protobuf::Message& message) { - return ProtoMatcher(message); +using PolymorphicProtoMatcher = PolymorphicMatcher; +} // 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_ diff --git a/ortools/math_opt/constraints/indicator/BUILD.bazel b/ortools/math_opt/constraints/indicator/BUILD.bazel index 6c1c807c76..12fdf6dc1e 100644 --- a/ortools/math_opt/constraints/indicator/BUILD.bazel +++ b/ortools/math_opt/constraints/indicator/BUILD.bazel @@ -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", + ], +) diff --git a/ortools/math_opt/constraints/indicator/CMakeLists.txt b/ortools/math_opt/constraints/indicator/CMakeLists.txt index 8313fb4c4a..af47b4cb6a 100644 --- a/ortools/math_opt/constraints/indicator/CMakeLists.txt +++ b/ortools/math_opt/constraints/indicator/CMakeLists.txt @@ -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 diff --git a/ortools/math_opt/constraints/indicator/indicator_constraint_test.cc b/ortools/math_opt/constraints/indicator/indicator_constraint_test.cc new file mode 100644 index 0000000000..35dbfbc664 --- /dev/null +++ b/ortools/math_opt/constraints/indicator/indicator_constraint_test.cc @@ -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 +#include +#include +#include + +#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 diff --git a/ortools/math_opt/constraints/indicator/storage_test.cc b/ortools/math_opt/constraints/indicator/storage_test.cc new file mode 100644 index 0000000000..1f9862e130 --- /dev/null +++ b/ortools/math_opt/constraints/indicator/storage_test.cc @@ -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 +#include + +#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 diff --git a/ortools/math_opt/constraints/indicator/validator_test.cc b/ortools/math_opt/constraints/indicator/validator_test.cc new file mode 100644 index 0000000000..b7bd846f80 --- /dev/null +++ b/ortools/math_opt/constraints/indicator/validator_test.cc @@ -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 +#include +#include +#include + +#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::infinity(); + +IdNameBiMap SimpleVariableUniverse(const std::initializer_list 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::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::quiet_NaN()); + EXPECT_THAT(ValidateConstraint(constraint, SimpleVariableUniverse({1, 2})), + StatusIs(absl::StatusCode::kInvalidArgument, + HasSubstr("invalid upper bound"))); +} + +} // namespace +} // namespace operations_research::math_opt diff --git a/ortools/math_opt/constraints/quadratic/BUILD.bazel b/ortools/math_opt/constraints/quadratic/BUILD.bazel index e4e708b3b8..e4a0925098 100644 --- a/ortools/math_opt/constraints/quadratic/BUILD.bazel +++ b/ortools/math_opt/constraints/quadratic/BUILD.bazel @@ -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", + ], +) diff --git a/ortools/math_opt/constraints/quadratic/CMakeLists.txt b/ortools/math_opt/constraints/quadratic/CMakeLists.txt index 288d832524..0240bc5296 100644 --- a/ortools/math_opt/constraints/quadratic/CMakeLists.txt +++ b/ortools/math_opt/constraints/quadratic/CMakeLists.txt @@ -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 diff --git a/ortools/math_opt/constraints/quadratic/quadratic_constraint_test.cc b/ortools/math_opt/constraints/quadratic/quadratic_constraint_test.cc new file mode 100644 index 0000000000..c8dac00594 --- /dev/null +++ b/ortools/math_opt/constraints/quadratic/quadratic_constraint_test.cc @@ -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 +#include + +#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 diff --git a/ortools/math_opt/constraints/quadratic/storage_test.cc b/ortools/math_opt/constraints/quadratic/storage_test.cc new file mode 100644 index 0000000000..5a5303fde0 --- /dev/null +++ b/ortools/math_opt/constraints/quadratic/storage_test.cc @@ -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 + +#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 diff --git a/ortools/math_opt/constraints/quadratic/validator_test.cc b/ortools/math_opt/constraints/quadratic/validator_test.cc new file mode 100644 index 0000000000..d81b9300b2 --- /dev/null +++ b/ortools/math_opt/constraints/quadratic/validator_test.cc @@ -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 +#include +#include +#include + +#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::infinity(); +constexpr double kNan = std::numeric_limits::quiet_NaN(); + +IdNameBiMap SimpleVariableUniverse(const std::initializer_list 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 diff --git a/ortools/math_opt/constraints/second_order_cone/BUILD.bazel b/ortools/math_opt/constraints/second_order_cone/BUILD.bazel index 41144dc2ba..37ed646b57 100644 --- a/ortools/math_opt/constraints/second_order_cone/BUILD.bazel +++ b/ortools/math_opt/constraints/second_order_cone/BUILD.bazel @@ -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", + ], +) diff --git a/ortools/math_opt/constraints/second_order_cone/CMakeLists.txt b/ortools/math_opt/constraints/second_order_cone/CMakeLists.txt index a75ebd9717..c282fe1a7e 100644 --- a/ortools/math_opt/constraints/second_order_cone/CMakeLists.txt +++ b/ortools/math_opt/constraints/second_order_cone/CMakeLists.txt @@ -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 diff --git a/ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint_test.cc b/ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint_test.cc new file mode 100644 index 0000000000..757c302521 --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/second_order_cone_constraint_test.cc @@ -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 +#include +#include +#include +#include + +#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 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 diff --git a/ortools/math_opt/constraints/second_order_cone/storage_test.cc b/ortools/math_opt/constraints/second_order_cone/storage_test.cc new file mode 100644 index 0000000000..5d1f5a0b93 --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/storage_test.cc @@ -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 + +#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 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 diff --git a/ortools/math_opt/constraints/second_order_cone/validator_test.cc b/ortools/math_opt/constraints/second_order_cone/validator_test.cc new file mode 100644 index 0000000000..e57c68a7b1 --- /dev/null +++ b/ortools/math_opt/constraints/second_order_cone/validator_test.cc @@ -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 +#include +#include +#include +#include + +#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 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 diff --git a/ortools/math_opt/constraints/sos/BUILD.bazel b/ortools/math_opt/constraints/sos/BUILD.bazel index c97f4a4edf..fade5cb4d6 100644 --- a/ortools/math_opt/constraints/sos/BUILD.bazel +++ b/ortools/math_opt/constraints/sos/BUILD.bazel @@ -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", + ], +) diff --git a/ortools/math_opt/constraints/sos/CMakeLists.txt b/ortools/math_opt/constraints/sos/CMakeLists.txt index 520b847701..c0b0ff207a 100644 --- a/ortools/math_opt/constraints/sos/CMakeLists.txt +++ b/ortools/math_opt/constraints/sos/CMakeLists.txt @@ -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 diff --git a/ortools/math_opt/constraints/sos/sos1_constraint_test.cc b/ortools/math_opt/constraints/sos/sos1_constraint_test.cc new file mode 100644 index 0000000000..f6a8559efc --- /dev/null +++ b/ortools/math_opt/constraints/sos/sos1_constraint_test.cc @@ -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 +#include +#include + +#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 diff --git a/ortools/math_opt/constraints/sos/sos2_constraint_test.cc b/ortools/math_opt/constraints/sos/sos2_constraint_test.cc new file mode 100644 index 0000000000..b6f4446ad4 --- /dev/null +++ b/ortools/math_opt/constraints/sos/sos2_constraint_test.cc @@ -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 +#include +#include + +#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 diff --git a/ortools/math_opt/constraints/sos/storage_test.cc b/ortools/math_opt/constraints/sos/storage_test.cc new file mode 100644 index 0000000000..cedf94016f --- /dev/null +++ b/ortools/math_opt/constraints/sos/storage_test.cc @@ -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 + +#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 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 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 diff --git a/ortools/math_opt/constraints/sos/validator_test.cc b/ortools/math_opt/constraints/sos/validator_test.cc new file mode 100644 index 0000000000..b9c5c73bdd --- /dev/null +++ b/ortools/math_opt/constraints/sos/validator_test.cc @@ -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 +#include +#include +#include +#include + +#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 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::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::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 diff --git a/ortools/math_opt/constraints/util/BUILD.bazel b/ortools/math_opt/constraints/util/BUILD.bazel index e211640610..c3d0c067ee 100644 --- a/ortools/math_opt/constraints/util/BUILD.bazel +++ b/ortools/math_opt/constraints/util/BUILD.bazel @@ -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", + ], +) diff --git a/ortools/math_opt/constraints/util/CMakeLists.txt b/ortools/math_opt/constraints/util/CMakeLists.txt index 6a033b3b59..151755f284 100644 --- a/ortools/math_opt/constraints/util/CMakeLists.txt +++ b/ortools/math_opt/constraints/util/CMakeLists.txt @@ -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 diff --git a/ortools/math_opt/constraints/util/model_util_test.cc b/ortools/math_opt/constraints/util/model_util_test.cc new file mode 100644 index 0000000000..1b83a9025b --- /dev/null +++ b/ortools/math_opt/constraints/util/model_util_test.cc @@ -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(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(storage), + ElementsAre(TestConstraint(&storage, c), TestConstraint(&storage, d))); +} + +} // namespace +} // namespace operations_research::math_opt