Files
ortools-clone/ortools/sat/symmetry_test.cc

154 lines
5.8 KiB
C++

// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "ortools/sat/symmetry.h"
#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "absl/types/span.h"
#include "gtest/gtest.h"
#include "ortools/algorithms/sparse_permutation.h"
#include "ortools/base/gmock.h"
#include "ortools/sat/sat_base.h"
namespace operations_research {
namespace sat {
namespace {
using ::testing::ElementsAre;
TEST(SymmetryPropagatorTest, Permute) {
const int num_variables = 6;
const int num_literals = 2 * num_variables;
std::unique_ptr<SparsePermutation> perm(new SparsePermutation(num_literals));
perm->AddToCurrentCycle(Literal(+3).Index().value());
perm->AddToCurrentCycle(Literal(+2).Index().value());
perm->AddToCurrentCycle(Literal(-4).Index().value());
perm->CloseCurrentCycle();
// Note that the permutation 'p' must be compatible with the negation.
// That is negation(p(l)) = p(negation(l)). This is actually not required
// for this test though.
perm->AddToCurrentCycle(Literal(-3).Index().value());
perm->AddToCurrentCycle(Literal(-2).Index().value());
perm->AddToCurrentCycle(Literal(+4).Index().value());
perm->CloseCurrentCycle();
Trail trail;
SymmetryPropagator propagator;
propagator.AddSymmetry(std::move(perm));
trail.RegisterPropagator(&propagator);
std::vector<Literal> literals = Literals({+1, +2, -2, +3});
std::vector<Literal> output;
propagator.SetNumLiterals(num_literals);
propagator.Permute(0, literals, &output);
EXPECT_THAT(output,
ElementsAre(Literal(+1), Literal(-4), Literal(+4), Literal(+2)));
}
TEST(SymmetryPropagatorTest, BasicTest) {
const int num_variables = 6;
const int num_literals = 2 * num_variables;
std::unique_ptr<SparsePermutation> perm(new SparsePermutation(num_literals));
perm->AddToCurrentCycle(Literal(+3).Index().value());
perm->AddToCurrentCycle(Literal(+2).Index().value());
perm->AddToCurrentCycle(Literal(-4).Index().value());
perm->CloseCurrentCycle();
// Note that the permutation 'p' must be compatible with the negation.
// That is negation(p(l)) = p(negation(l)).
perm->AddToCurrentCycle(Literal(-3).Index().value());
perm->AddToCurrentCycle(Literal(-2).Index().value());
perm->AddToCurrentCycle(Literal(+4).Index().value());
perm->CloseCurrentCycle();
perm->AddToCurrentCycle(Literal(-5).Index().value());
perm->AddToCurrentCycle(Literal(+5).Index().value());
perm->CloseCurrentCycle();
Trail trail;
trail.Resize(num_variables);
SymmetryPropagator propagator;
propagator.AddSymmetry(std::move(perm));
trail.RegisterPropagator(&propagator);
propagator.SetNumLiterals(num_literals);
// We need a mock propagator to inject a reason.
struct MockPropagator : SatPropagator {
MockPropagator() : SatPropagator("MockPropagator") {}
bool Propagate(Trail* trail) final { return true; }
absl::Span<const Literal> Reason(const Trail& /*trail*/,
int /*trail_index*/,
int64_t /*conflict_id*/) const final {
return reason;
}
std::vector<Literal> reason;
};
MockPropagator mock_propagator;
trail.RegisterPropagator(&mock_propagator);
// With such a trail, nothing should propagate because the first non-symmetric
// literal +3 is a decision.
trail.Enqueue(Literal(+3), AssignmentType::kSearchDecision);
trail.Enqueue(Literal(-5), mock_propagator.PropagatorId());
while (!propagator.PropagationIsDone(trail)) {
EXPECT_TRUE(propagator.Propagate(&trail));
}
EXPECT_EQ(trail.Index(), 2);
// Now we take the decision +2 (which is the image of +3).
trail.Enqueue(Literal(+2), AssignmentType::kUnitReason);
// We need to initialize the reason for -5, because it will be needed during
// the conflict creation that the Propagate() below will trigger.
mock_propagator.reason = Literals({-3});
// Because -5 is now the first non-symmetric literal, a conflict is detected
// since +5 can then be propagated.
EXPECT_FALSE(propagator.PropagationIsDone(trail));
EXPECT_FALSE(propagator.Propagate(&trail));
// Let assume that the reason for -5 is the assignment +3 (which make sense
// since it was propagated). The expected conflict is as stated below because
// if -5 and +2 are true, by summetry since we had +3 => -5 we know that +2 =>
// 5.
//
// Note: by convention all the literals of a reason or a conflict are false.
EXPECT_THAT(trail.FailingClause(), ElementsAre(Literal(-2), Literal(+5)));
// Let backtrack to the trail to +3.
trail.Untrail(trail.Index() - 2);
propagator.Untrail(trail, trail.Index());
// Let now assume that +3 => +2, by symmetry we can also propagate -4!
while (!propagator.PropagationIsDone(trail)) {
EXPECT_TRUE(propagator.Propagate(&trail));
}
EXPECT_EQ(trail.Index(), 1);
trail.Enqueue(Literal(+2), mock_propagator.PropagatorId());
EXPECT_FALSE(propagator.PropagationIsDone(trail));
EXPECT_TRUE(propagator.Propagate(&trail));
EXPECT_EQ(trail.Index(), 3);
EXPECT_EQ(trail[2], Literal(-4));
// Once again, if the reason for +2 was the assignment +3, we can compute
// the reason for the assignment -4 (it is just the symmetric of the other).
EXPECT_THAT(trail.Reason(Literal(-4).Variable()), ElementsAre(Literal(-2)));
}
} // namespace
} // namespace sat
} // namespace operations_research