Adding a PbToCnfEncoder to Paramita: the naive pairwise encoder¶
In this tutorial we will add the pairwise encoder to Paramita by extending the C++ PbToCnfEncoder class.
The pairwise encoding enforces an At-Most-One rule by ensuring that no two variables can be true simultaneously. Given a set of \(n\) boolean variables, it creates a binary clause for every unique pair of variables, forcing at least one of them to be false:
Project setup¶
As in the cubing example using Propagators, we will use cmake to compile our PairwiseEncoder plugin.
The project structure that we will follow is this one:
pairwise/
├── cmake
│ ├── Finddoctest.cmake
│ ├── ParamitaInterfaces.cmake
│ └── plugin.cmake
├── CMakeLists.txt
├── encoder.cpp
├── encoder.hpp
└── test_pairwise.cpp
cmake/contains all the required CMakeLists files, which are provided by Paramita. You can find them here.CMakeLists.txtis thecmakefile that we will use to compile the plugin.encoder.cppandencoder.hppcontain the implementation and headers of our plugin.
Setting up CMake¶
The specification of the CMakeLists.txt file is shown below:
cmake_minimum_required(VERSION 3.16..3.30)
project(PairwiseEncoding
VERSION "1.0"
LANGUAGES CXX
)
set(TARGET_NAME pairwise-encoder)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake")
include(plugin)
# Extension definition
# ====================
# Declare the binding
add_library(${TARGET_NAME}
SHARED # compile as an *.so
encoder.cpp
encoder.hpp
)
set_target_properties(${TARGET_NAME}
PROPERTIES
LINKER_LANGUAGE CXX
POSITION_INDEPENDENT_CODE ON
)
target_link_libraries(${TARGET_NAME}
PUBLIC
Paramita::interfaces
)
# Testing
# =======
if (PARAMITA_TEST_PLUGINS)
set(TEST_TARGET "test-${TARGET_NAME}")
# Create test target
add_executable(${TEST_TARGET} test_pairwise.cpp)
target_link_libraries(${TEST_TARGET}
PRIVATE
${TARGET_NAME}
)
ParamitaInterfaces_discover_tests(${TEST_TARGET})
endif ()
# How to install
install(
TARGETS ${TARGET_NAME}
LIBRARY DESTINATION pb-to-cnf-encoders
)
For more information you can check the Setting up CMake section of the cubing Plugin tutorial.
Implementing the encoder¶
We will just need to implement the encode method of the PbToCnfEncoder C++ interface.
The header definition in encoder.hpp is the following:
#pragma once
#include <paramita/Exceptions.hpp>
#include <paramita/sat/PbToCnfEncoder.hpp>
using namespace Paramita;
class PARAMITA_EXPORT PairwiseEncoder : public PbToCnfEncoder {
public:
PairwiseEncoder() {};
~PairwiseEncoder() override {};
PairwiseEncoder(const PairwiseEncoder &) = delete;
PairwiseEncoder &operator=(const PairwiseEncoder &) = delete;
PairwiseEncoder(PairwiseEncoder &&) = delete;
PairwiseEncoder &operator=(PairwiseEncoder &&) = delete;
Context encode(
const PbConstraint &constraint, ClauseContainer &container,
Context context
) override;
};
And the implementation in encoder.cpp is this one:
#include "encoder.hpp"
#include <paramita/Exceptions.hpp>
#include <algorithm>
using namespace Paramita;
PbToCnfEncoder::Context PairwiseEncoder::encode(
const PbConstraint &constraint, ClauseContainer &container, Context context
) {
if (context) {
throw UnsupportedMethodException(
"Unsupported context. This is a non-incremental encoder."
);
}
if (constraint.op != PbConstraint::LTE || constraint.bound != 1) {
throw UnsupportedMethodException("Only '<= 1' is supported");
}
if (constraint.weights.has_value()) {
std::vector<int64_t> weights = constraint.weights.value();
if (!std::all_of(weights.begin(), weights.end(), [](int i) {
return i == 1;
})) {
throw UnsupportedMethodException("Only unweighted is supported");
}
}
for (int i = 0; i < (int)constraint.lits.size() - 1; ++i) {
for (int j = i + 1; j < (int)constraint.lits.size(); ++j) {
container.add_clause({-constraint.lits[i], -constraint.lits[j]});
}
}
return {}; // Empty context
};
PB_TO_CNF_ENCODER_C_INTERFACE(PairwiseEncoder, "", "", "", "", "");
- First, we ensure we have an empty
context, as the pairwise encoding in non-incremental (Contextis only used in incremental encoders). - Then, we ensure that the user provided us with a \(\leq 1\) Pseudo-Boolen constraint, as this is the only one that we will support.
- We also ensure that we have no weights (so all weights are 1) or that the user specified explicitly all weights set to 1.
- We then have the specific implementation for the encoder, and we add each of the resulting clauses to the
container. - Finally, we return an empty
Context.
Note
Notice that we have to call the PB_TO_CNF_ENCODER_C_INTERFACE(PairwiseEncoder); macro to register this implementation as a Paramita plugin.
Compiling the plugin¶
As we did with the CubingEngine compilation, we will compile the extension manually to generate an .so file that we can load in Python using the Propagator.dynamic_load(...) static method:
cmake -S . -B build/ -DCMAKE_BUILD_TYPE=Release -DPARAMITA_SKIP_LINTS=ON -DPARAMITA_TEST_PLUGINS=OFF
This would generate the build/libpairwise-encoder.so file, which we can then load in python using the PbToCnfEncoder.dynamic_load("build/libpairwise-encoder.so") method.