Python Pseudo-Boolean library¶
This library offers a comprehensive set of utilites to work with pseudo-boolean formulas within Python.
This module provides bindings to the PBLib – A C++ Toolkit for Encoding Pseudo-Boolean Constraints into CNF.
Note
You can build the pypblib via:
python3 setup.py build
You can install the pypblib via:
python3 setup.py install
Overview¶
Pseudo-Boolean Constraint¶
PBLib provides classes that allows us to encode pseudo-Boolean constraints.
The first to know is the WeightedLit class.
Class WeightedLit¶
The WeightedLit class is used to represent the literals of a pseudo-Boolean constraint, including their weight.If we take the following pseudo-Boolean constraint as an example:2 x1 + 3 x2 + 1 x3 <= 3We can represent their literals as follows:>>> from pypblib import pblib >>> wl1 = pblib.WeightedLit(1, 2) >>> wl2 = pblib.WeightedLit(2, 3) >>> wl3 = pblib.WeightedLit(3, 1) >>> print(wl1, wl2, wl3) (1, 2) (2, 3) (3, 1)
An operator (inequality) is also required to represent the constraint.
Comparator¶
The Comparator represents the inequality symbols. It is a set of constants. You can treat the comparator as it were an enum: { LEQ, GEQ, BOTH }
- LEQ (less-equal)
- GEQ (greater-equal)
- BOTH (less-equal & greater-equal)
Now we have everything we need to instantiate a constraint.
Class PBConstraint¶
The PBConstraint class is used to represent a pseudo-Boolean constraint. Receives a list of WeightedLit, a comparator and a bound.Following the previous example we can generate a constraint as follows:>>> constr = pblib.PBConstraint([wl1, wl2, wl3], pblib.LEQ, 3) >>> constr +2x1+3x2+x3<=3However, depending on the comparator, two limits may be necessary. When using the BOTH comparator, a single limit is equivalent to strict equal. Therefore, in case working with a constraint such as the following,1<=+2x1+3x2+x3<=3it is necessary to use the BOTH comparator and two bounds. The Pblib also provides a constructor for the PBConstraint in these cases. This takes a list of WeightedLit, a comparator, the less-equal bound and the greater-equal bound.>>> constr_2 = pblib.PBConstraint([wl1, wl2, wl3], pblib.BOTH, 3, 1) >>> constr_2 1<=+2x1+3x2+x3<=3
class PBConfig¶
class Pb2cnf¶
The class Pb2cnf is the main interface to encode a pseudo-boolean constraint.The Pb2cnf constructor takes a PBConfig as a parameter. However, if PBConfig is omitted the default configuration will be used.For a simple interface you can use one of the following methods:encode_at_most_k (literals: [int], k: long, formula: [], first_free_variable: int) -> intencode_at_least_k (literals: [int], k: long, formula: [], first_free_variable: int) -> intencode_leq (weights: [long], literals: [int], k: long, formula: [], first_free_variable: int) -> intencode_geq (weights: [long], literals: [int], k: long, formula: [], first_free_variable: int) -> intencode_both (weights: [long], literals [int], leq: long, geq: long, formula: [], first_free_variable: int) -> intBut for a more sophisticated interface you should use, besides the PBConstraint, a VectorClauseDatabase and an AuxVarManager.If your encode has only one constraint, you can use one of the methods listed above. However, if you have several constraints, you will need to use the following method:encode (constr: PBConstraint, formula: VectorClauseDatabase, aux_var_manager: AuxVarManager)class AuxVarManagerThe constructor of AuxVarManager class takes an integer as parameter.The auxiliary variable manager, returns fresh variable to the encoder. Therefore it must be initialized with the first fresh variable.class VectorClauseDatabaseThe constructor of VectorClauseDatabase class takes an PBConfig as parameter.The clause database is a container of clauses. Every clause that is added is saved into a vector of clauses.If we take the followings pseudo-Boolean constraints as example (this is a dummy instance; only for demonstration purposes):1 x1 + 1 x2 + 2 x3 <= 21 x4 + 2 x5 - 1 x6 >= 1The following is a encoding full example:
>>> from pypblib import pblib >>> wl1 = pblib.WeightedLit(1, 1) >>> wl2 = pblib.WeightedLit(2, 1) >>> wl3 = pblib.WeightedLit(3, 2) >>> constr_1 = pblib.PBConstraint([wl1, wl2, wl3], pblib.LEQ, 2) >>> wl4 = pblib.WeightedLit(4, 1) >>> wl5 = pblib.WeightedLit(5, 2) >>> wl6 = pblib.WeightedLit(6, -1) >>> constr_2 = pblib.PBConstraint([wl4, wl5, wl6], pblib.GEQ, 1) >>> config = pblib.PBConfig() >>> pb2 = pblib.Pb2cnf() >>> aux_var = pblib.AuxVarManager(7) >>> formula = pblib.VectorClauseDatabase(config) >>> pb2.encode(constr_1, formula, aux_var) >>> formula Num. Causes: 12 ============================== 7 0 -7 8 0 -2 8 0 -7 -2 9 0 -1 8 0 -7 -1 9 0 -2 -1 9 0 -7 -2 -1 10 0 -3 11 0 -9 11 0 -3 -9 12 0 -12 0 ==============================>>> pb2.encode(constr_2, formula, aux_var) >>> formula Num. Causes: 24 ============================== 7 0 -7 8 0 -2 8 0 -7 -2 9 0 -1 8 0 -7 -1 9 0 -2 -1 9 0 -7 -2 -1 10 0 -3 11 0 -9 11 0 -3 -9 12 0 -12 0 13 0 -13 14 0 -6 14 0 -13 -6 15 0 4 14 0 -13 4 15 0 -6 4 15 0 -13 -6 4 16 0 5 17 0 -15 17 0 5 -15 18 0 -18 0 ==============================
Incremental Pseudo-Boolean Constraint¶
In this part we will assume that you already know some of the basic PBLib classes that are used to generate the encoding of a pseudo-Boolean constraint. These classes, WeightedLit , Comparator , PBConfig, Pb2cnf , AuxVarManager and VectorClauseDatabase , are also used to encoding incremental pseudo-boolean constraints. Hence, if you are not familiar with these classes, go to Pseudo-Boolean Constraint before continuing to read this part.
Class IncPBConstraint¶
The IncPBConstraint behaves very much like PBConstraint. The main difference is that it allows, when the coding is already generated, to make it more restrictive. To do this, instead of using the encode(…) method of Pb2cnf class, we will use this other method:encode_inc_initial (inc_constr: IncPBConstraint, formula: VectorClauseDatabase, aux_var_manager: AuxVarManager)When we already have the encoding, we can increase the restriction using the methods provided by the IncPBConstraint class, depending on the case:encode_new_geq (geq_bound: long, formula: VectorClauseDatabase, aux_var_manager: AuxVarManager)encode_new_leq (leq_bound: long, formula: VectorClauseDatabase, aux_var_manager: AuxVarManager)
Let’s see an example:
1 x1 + 1 x2 + 1 x3 <= 2
>>> from pypblib import pblib >>> wl1 = pblib.WeightedLit(1, 1) >>> wl2 = pblib.WeightedLit(2, 1) >>> wl3 = pblib.WeightedLit(3, 1) >>> i_constr = pblib.IncPBConstraint([wl1, wl2, wl3], pblib.LEQ, 2) >>> config = pblib.PBConfig() >>> pb2 = pblib.Pb2cnf() >>> aux_var = pblib.AuxVarManager(4) >>> formula = pblib.VectorClauseDatabase(config) >>> pb2.encode_inc_initial(i_constr, formula, aux_var) >>> formula Num. Causes: 8 ============================== -3 4 0 -2 4 0 -3 -2 5 0 -1 4 0 -3 -1 5 0 -2 -1 5 0 -3 -2 -1 6 0 -6 0 ============================== >>> >>> i_constr.encode_new_leq(1, formula, aux_var) >>> formula Num. Causes: 9 ============================== -3 4 0 -2 4 0 -3 -2 5 0 -1 4 0 -3 -1 5 0 -2 -1 5 0 -3 -2 -1 6 0 -6 0 -5 0 ==============================
Example, from OPB to CNF file¶
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import re
from pypblib import pblib
#
###############################################################################
PBLIB_OPS = {'<=': pblib.LEQ, '>=': pblib.GEQ, '=': pblib.BOTH}
HEADER_RE = re.compile(r'(.*)\s*#variable=\s*(\d+)\s*#constraint=\s*(\d+)\s*')
TERM_RE = re.compile(r'([+-]?\d)\s+x(\d+)')
IND_TERM_RE = re.compile(r'([>=|<=|=]+)\s+([+-]?\d+)')
#
###############################################################################
def transform_pb_to_cnf(pb_formula_path: str):
config = pblib.PBConfig()
aux = pblib.AuxVarManager(1)
pb2cnf = pblib.Pb2cnf(config)
formula = pblib.VectorClauseDatabase(config)
with open(pb_formula_path, 'r') as f:
m = HEADER_RE.match(f.readline())
aux.reset_aux_var_to(int(m.group(2)) + 1)
reader = (l for l in map(str.strip, f) if l and l[0] != '*')
for line in reader:
wl = [pblib.WeightedLit(int(l), int(w))
for w, l in TERM_RE.findall(line)]
op, ind_term = IND_TERM_RE.search(line).groups()
constraint = pblib.PBConstraint(wl, PBLIB_OPS[op], int(ind_term))
pb2cnf.encode(constraint, formula, aux)
return formula, aux
#
###############################################################################
if __name__ == '__main__':
f, aux = transform_pb_to_cnf(sys.argv[1])
f.print_formula("test.cnf", aux.get_biggest_returned_auxvar())
Example, from OPB to WCNF file¶
import sys
import re
from pypblib import pblib
#
###############################################################################
DEFAULT_FILE_NAME = "test_wcnf.cnf"
op = {'<=': pblib.LEQ, '>=': pblib.GEQ, '=': pblib.BOTH }
_sum_soft_weights = 1
#
###############################################################################
def read_opb(path):
config = pblib.PBConfig()
aux = pblib.AuxVarManager(1)
hard = pblib.VectorClauseDatabase(config)
soft = []
pb2 = pblib.Pb2cnf(config)
f = open(path,'r')
for line in f:
if len(line) == 0: continue
if '#variable=' in line:
aux.reset_aux_var_to(int(re.findall(r"\d+", line)[0]) + 1)
if line[0] == '*': continue
if 'min:' in line:
soft = re.findall(r"[-\d]+", line)
continue
# This is linear equation
lq = re.findall(r"[-\d|>=|<=|=]+", line)
wl = [pblib.WeightedLit(int(lq[i+1]), int(lq[i])) for i,e in enumerate(lq[:-2]) if(i%2 == 0)]
pb2.encode(pblib.PBConstraint(wl, op[lq[-2]], int(lq[-1])), hard, aux)
f.close()
return aux, soft, hard
#
###############################################################################
def write_cnf(file_path, aux, soft, hard):
with open(file_path, 'w') as f:
f.write("p wcnf " + str(aux.get_biggest_returned_auxvar()) + " " + str(hard.get_num_clauses() + int(len(soft)/2)) \
+ " " + str(sum(int(e) for i,e in enumerate(soft) if(i%2 != 0)) + 1) + '\n')
for i, e in enumerate(soft):
if(i%2 == 0):
global _sum_soft_weights
_sum_soft_weights += abs(int(soft[i]))
if(int(soft[i]) < 0):
f.write(str(abs(int(soft[i]))) + " " + str(int(soft[i+1])) + " 0\n")
else:
f.write(str(abs(int(soft[i]))) + " " + str(-int(soft[i+1])) + " 0\n")
v_form = hard.get_clauses()
for c in v_form:
tmp = ""
for i in c:
tmp += str(i);
f.write(str(_sum_soft_weights) + " " + tmp + " 0\n")
f.close()
#
###############################################################################
if __name__ == '__main__':
file_path = DEFAULT_FILE_NAME
if len(sys.argv) > 2:
file_path = argv[2]
aux, soft, hard = read_opb(sys.argv[1])
write_cnf(file_path, aux, soft, hard)