Skip to content

Examples

In this section, we show additional examples for defining different constraint families inheriting from the Constraint abstract base class.

Constraints

Cube

from gadgetocompiler.constraints import Constraint

class Cube(Constraint):
    """
    Represents a conjunction of literals.

    Example: Cube([-var1,var2,var3]) represents cube (not var1 and var2 and var3)
    """

    def __init__(self, relation):
        relation = frozenset(relation)
        if any(l == 0 for l in relation):
            raise ValueError("Clause has literal 0")
        super().__init__(relation)

    def __str__(self):
        return " ".join([str(item) for item in self.relation])

    def __repr__(self):
        return str(self)

    def get_arity(self) -> int:
        """Returns the arity of the constraint."""
        return len(self.relation)

    def get_vars(self):
        """Returns the vars in the constraint."""
        return [abs(lit) for lit in self.relation]

    def reify(self, var_mapping, model, assignment):
        pass

    def get_truth_value(self, assignment) -> int:
        """
        Given a full assignment to the vars of the constraint, return 1 if
        the constraint is satisfied, otherwise return 0.

        Args:
            assignment: a map of variables to their assigned value

        Returns:
            1 if the constraint is satisfied, 0 if not.
        """
        true_counter = 0
        for lit in self.relation:
            var = abs(lit)
            if var not in assignment:
                continue
            tv = Cube.get_lit_truth_value(lit, assignment[var])
            if tv == 0:
                return 0
            if tv == 1:
                true_counter += 1
        return 1 if true_counter == len(self.relation) else 2

    @staticmethod
    def get_lit_truth_value(lit: int, value: int):
        """Evaluates a literal under an assignment."""
        if value == 2:
            return 2
        elif (lit < 0 and value == 0) or (lit > 0 and value == 1):
            return 1
        else:
            return 0

    @classmethod
    def gen_constraints_from_vars(cls, vars, arity: int):
        """
        Generate all the possible constraints with
        the specified arity on the given set of variables.

        Args:
            vars: The variables to use when generating constraints.
            arity: The arity of the resulting constraints

        Returns:
            All the possible constraints.
        """
        constraints = []
        for indexs in itertools.combinations(range(len(vars)), arity):
            for value_tuple in itertools.product(*[[0, 1] for _ in range(arity)]):
                lits = []
                for i in range(len(value_tuple)):
                    (
                        lits.append(-vars[indexs[i]])
                        if value_tuple[i] == 0
                        else lits.append(vars[indexs[i]])
                    )
                constraints.append(cls(lits))
        return constraints

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.relation == other.relation

    def __hash__(self):
        return hash((self.__class__, self.relation))

PosClause

from gadgetocompiler.constraints import Clause
import itertools

class PosClause(Clause):
    """
    Represents a Clause with only positive literals.

    Example: PosClause([a,b,c]) represents Clause (a or b or c)

    """

    def __init__(self, relation):
        self.relation = frozenset(relation)
        if any(l <= 0 for l in relation):
            raise ValueError("PosClause has a negative literal")


    @classmethod
    def gen_constraints_from_vars(cls, vars, arity: int):
        """
        Generate all the possible constraints with
        the specified arity on the given set of variables.

        Args:
            vars: The variables to use when generating constraints.
            arity: The arity of the resulting constraints

        Returns:
            All the possible constraints.
        """
        constraints = []
        for indexs in itertools.combinations(range(len(vars)), arity):
            constraints.append(cls([vars[i] for i in indexs]))

        return constraints

CUT

from gadgetocompiler.constraints import Constraint
import itertools

class CUT(Constraint):
    """
    Represents a CUT constraint.

    Example: CUT([a,b]) represents CUT(a,b) = a xor b

    """

    def __init__(self, relation):
        self.relation = frozenset(relation)
        if any(v <= 0 for v in relation):
            raise ValueError("CUT has a variable with index <= 0")

    def __str__(self):
        return " ".join([str(item) for item in self.relation])

    def __repr__(self):
        return str(self)

    def get_arity(self) -> int:
        """Returns the arity of the constraint."""
        return len(self.relation)

    def get_vars(self):
        """Returns the vars in the constraint."""
        return [abs(lit) for lit in self.relation]

    def reify(self, var_mapping, model, assignment):
        pass

    def get_truth_value(self, assignment) -> int:
        """
        Given a full assignment to the vars of the constraint, return 1 if
        the constraint is satisfied, otherwise return 0.

        Args:
            assignment: a map of variables to their assigned value

        Returns:
            1 if the constraint is satisfied, 0 if not.
        """

        lit_a,lit_b = self.relation

        tv_a = self.get_lit_truth_value(lit_a, assignment[abs(lit_a)])
        tv_b = self.get_lit_truth_value(lit_b, assignment[abs(lit_b)])

        if tv_a == 2 or tv_b == 2: return 2 
        return tv_a ^ tv_b

    @staticmethod
    def get_lit_truth_value(lit: int, value: int):
        """Evaluates a literal under an assignment."""
        if value == 2:
            return 2
        elif (lit < 0 and value == 0) or (lit > 0 and value == 1):
            return 1
        else:
            return 0

    @classmethod
    def gen_constraints_from_vars(cls, vars, arity: int):
        """
        Generate all the possible constraints with
        the specified arity on the given set of variables.

        Args:
            vars: The variables to use when generating constraints.
            arity: The arity of the resulting constraints

        Returns:
            All the possible constraints.
        """
        constraints = []
        for indexs in itertools.combinations(range(len(vars)), arity):
            constraints.append(cls([vars[indexs[0]], vars[indexs[1]]]))

            """
            for value_tuple in itertools.product(*[[0, 1] for _ in range(arity)]):
                lits = []
                for i in range(len(value_tuple)):
                    (
                        lits.append(-vars[indexs[i]])
                        if value_tuple[i] == 0
                        else lits.append(vars[indexs[i]])
                    )
                constraints.append(cls(lits))
            """
        return constraints


    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.relation == other.relation 

    def __hash__(self):
        return hash((self.__class__, self.relation))

DICUT

from gadgetocompiler.constraints import Constraint
import itertools

class DICUT(Constraint):
    """
    Represents DICUT constraint.

    Example: DICUT([a,b]) represents DICUT(a,b) = not a and b
    """

    def __init__(self, relation):
        self.relation = tuple(relation)
        if any(v <= 0 for v in relation):
            raise ValueError("DICUT has a variable index <= 0")

    def __str__(self):
        return " ".join([str(item) for item in self.relation])

    def __repr__(self):
        return str(self)

    def get_arity(self) -> int:
        """Returns the arity of the constraint."""
        return len(self.relation)

    def get_vars(self):
        """Returns the vars in the constraint."""
        return [abs(lit) for lit in self.relation]

    def reify(self, var_mapping, model, assignment):
        pass

    def get_truth_value(self, assignment) -> int:
        """
        Given a full assignment to the vars of the constraint, return 1 if
        the constraint is satisfied, otherwise return 0.

        Args:
            assignment: a map of variables to their assigned value

        Returns:
            1 if the constraint is satisfied, 0 if not.
        """

        lit_a,lit_b = self.relation

        tv_a = self.get_lit_truth_value(lit_a, assignment[abs(lit_a)])
        tv_b = self.get_lit_truth_value(lit_b, assignment[abs(lit_b)])

        if tv_b == 0: return 0
        if tv_a == 1: return 0
        if tv_a == 2 or tv_b == 2: return 2 
        return 1

    @staticmethod
    def get_lit_truth_value(lit: int, value: int):
        """Evaluates a literal under an assignment."""
        if value == 2:
            return 2
        elif (lit < 0 and value == 0) or (lit > 0 and value == 1):
            return 1
        else:
            return 0

    @classmethod
    def gen_constraints_from_vars(cls, vars, arity: int):
        """
        Generate all the possible constraints with
        the specified arity on the given set of variables.

        Args:
            vars: The variables to use when generating constraints.
            arity: The arity of the resulting constraints

        Returns:
            All the possible constraints.
        """
        constraints = []
        for indexs in itertools.permutations(range(len(vars)), arity):

            constraints.append(cls([vars[indexs[0]], vars[indexs[1]]]))

        return constraints


    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.relation == other.relation 

    def __hash__(self):
        return hash((self.__class__, self.relation))

PC

from gadgetocompiler.constraints import Constraint

class PC(Constraint):
    """
    Represents Partity Check constraints.

    Example: PC([a,b,c],i) represents PC_i(a,b,c) where
        PC_i(a,b,c) = 1 if a xor b xor c == i.
        PC_i(a,b,c) = 0 otherwise.
    """

    def __init__(self, relation, i):
        self.relation = list(relation)
        self.i = i
        if any(v <= 0 for v in relation):
            raise ValueError("PC constraints has a variable index <= 0")

    def __str__(self):
        return " ".join([str(item) for item in self.relation])+" "+str(self.i)

    def __repr__(self):
        return str(self)

    def get_arity(self) -> int:
        """Returns the arity of the constraint."""
        return len(self.relation)

    def get_vars(self):
        """Returns the vars in the constraint."""
        return [abs(lit) for lit in self.relation]

    def reify(self, var_mapping, model, assignment):
        pass

    def get_truth_value(self, assignment) -> int:
        """
        Given a full assignment to the vars of the constraint, return 1 if
        the constraint is satisfied, otherwise return 0.

        Args:
            assignment: a map of variables to their assigned value

        Returns:
            1 if the constraint is satisfied, 0 if not.
        """

        lit_a,lit_b,lit_c = self.relation

        tv_a = self.get_lit_truth_value(lit_a, assignment[abs(lit_a)])
        tv_b = self.get_lit_truth_value(lit_b, assignment[abs(lit_b)])
        tv_c = self.get_lit_truth_value(lit_c, assignment[abs(lit_c)])
        if tv_a == 2 or tv_b == 2 or tv_c ==2: return 2 


        if tv_a ^ tv_b ^ tv_c ==  self.i: return 1
        return 0

    @staticmethod
    def get_lit_truth_value(lit: int, value: int):
        """Evaluates a literal under an assignment."""
        if value == 2:
            return 2
        elif (lit < 0 and value == 0) or (lit > 0 and value == 1):
            return 1
        else:
            return 0

    @classmethod
    def gen_constraints_from_vars(cls, vars, arity: int):
        pass

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.relation == other.relation and self.i == other.i

    def __hash__(self):
        return hash((self.__class__, self.relation, self.i))

RMBC

from gadgetocompiler.constraints import Constraint

class RMBC(Constraint):
    """
    Represents Respect of Monomial Basis Check cosntraint.

    Example: RMC([a,b,c,d],i,j) represents RMBC_{ij}(a,b,c,d).
    if a==0 and b == c xor i, RMBC_{ij}(a,b,c,d) = 1
    if a==1 and b == d xor j, RMBC_{ij}(a,b,c,d) = 1
    otherwise RMBC_{ij}(a,b,c,d) = 0 
    """

    def __init__(self, relation, i, j):
        self.relation = list(relation)
        self.i = i
        self.j = j
        if any(l == 0 for l in relation):
            raise ValueError("Clause has literal 0")

    def __str__(self):
        return " ".join([str(item) for item in self.relation])+" "+str(self.i)+" "+str(self.j)

    def __repr__(self):
        return str(self)

    def get_arity(self) -> int:
        """Returns the arity of the constraint."""
        return len(self.relation)

    def get_vars(self):
        """Returns the vars in the constraint."""
        return [abs(lit) for lit in self.relation]

    def reify(self, var_mapping, model, assignment):
        pass

    def get_truth_value(self, assignment) -> int:
        """
        Given a full assignment to the vars of the constraint, return 1 if
        the constraint is satisfied, otherwise return 0.

        Args:
            assignment: a map of variables to their assigned value

        Returns:
            1 if the constraint is satisfied, 0 if not.
        """

        lit_a,lit_b,lit_c,lit_d = self.relation

        tv_a = RMBC.get_lit_truth_value(lit_a, assignment[abs(lit_a)])
        tv_b = RMBC.get_lit_truth_value(lit_b, assignment[abs(lit_b)])
        tv_c = RMBC.get_lit_truth_value(lit_c, assignment[abs(lit_c)])
        tv_d = RMBC.get_lit_truth_value(lit_d, assignment[abs(lit_d)])
        if tv_a == 2 or tv_b == 2 or tv_c ==2 or tv_d == 2: return 2 

        if tv_a == 1 and (tv_b == (tv_c ^ self.i)): return 1
        if tv_a == 0 and (tv_b == (tv_d ^ self.j)): return 1
        return 0

    @staticmethod
    def get_lit_truth_value(lit: int, value: int):
        """Evaluates a literal under an assignment."""
        if value == 2:
            return 2
        elif (lit < 0 and value == 0) or (lit > 0 and value == 1):
            return 1
        else:
            return 0

    @classmethod
    def gen_constraints_from_vars(cls, vars, arity: int):
        pass

    def __eq__(self, other):
        return isinstance(other, self.__class__) and self.relation == other.relation and self.i == other.i and self.j == other.j

    def __hash__(self):
        return hash((self.__class__, self.relation, self.i, self.j))

Gadgets Compilation

3ConjSAT to 2SAT

from gadgetocompiler.constraints import Clause
from gadgetocompiler.compilers.gurobi import GurobiGadgetoCompiler
from Cube import Cube
g = GurobiGadgetoCompiler.generate(
    Cube([1, 2, 3]),  # The input constraint
    [(Clause, 2), (Clause, 1)],  # The output family constraints
    alpha_ub=10,  # We must set an upper bound for the alpha
    max_auxs=1,  # As well as the maximum number of aux variables that we will use

)
g.show()
is_gadget, is_strict = g.show_assignment_values()
alpha, beta = g.bounds[1], g.beta
print(f"This is {'' if is_gadget else 'NOT '}a ({alpha}-{beta})-gadget")
print("Is strict?", is_strict)

PC to 2SAT

import argparse

from gadgetocompiler.constraints import Clause
from gadgetocompiler.compilers.gurobi import GurobiGadgetoCompiler
from PC import PC

parser = argparse.ArgumentParser()
parser.add_argument("--i", type=int, required=True)
parser.add_argument("--max-auxs", "-b", type=int, required=True)
args = parser.parse_args()

print(f"i: {args.i}")
print(f"max_auxs: {args.max_auxs}")

g = GurobiGadgetoCompiler.generate(
    PC([1, 2, 3], args.i),  # The input constraint
    [(Clause, 2), (Clause, 1)],  # The output family constraints
    alpha_ub=14,  # We must set an upper bound for the alpha
    max_auxs=args.max_auxs,  # As well as the maximum number of aux variables that we will use
)
g.show()
is_gadget, is_strict = g.show_assignment_values()
alpha, beta = g.bounds[1], g.beta
print(f"This is {'' if is_gadget else 'NOT '}a ({alpha}-{beta})-gadget")
print("Is strict?", is_strict)

PC to 3SAT

import argparse

from gadgetocompiler.constraints import Clause
from gadgetocompiler.compilers.gurobi import GurobiGadgetoCompiler
from PC import PC

parser = argparse.ArgumentParser()
parser.add_argument("--i", type=int, required=True)
parser.add_argument("--max-auxs", "-b", type=int, required=True)
args = parser.parse_args()

print(f"i: {args.i}")
print(f"max_auxs: {args.max_auxs}")

g = GurobiGadgetoCompiler.generate(
    PC([1, 2, 3],args.i),  # The input constraint
    [(Clause, 3), (Clause, 2), (Clause, 1)],  # The output family constraints
    alpha_ub=14,  # We must set an upper bound for the alpha
    max_auxs=args.max_auxs,  # As well as the maximum number of aux variables that we will use
)
g.show()
is_gadget, is_strict = g.show_assignment_values()
alpha, beta = g.bounds[1], g.beta
print(f"This is {'' if is_gadget else 'NOT '}a ({alpha}-{beta})-gadget")
print("Is strict?", is_strict)

PC to CUT/0

import argparse

from gadgetocompiler.constraints import Clause
from gadgetocompiler.compilers.gurobi import GurobiGadgetoCompiler
from PC import PC
from CUT import CUT
from PosClause import PosClause

parser = argparse.ArgumentParser()
parser.add_argument("--i", type=int, required=True)
parser.add_argument("--max-auxs", "-b", type=int, required=True)
args = parser.parse_args()

print(f"i: {args.i}")
print(f"max_auxs: {args.max_auxs}")

g = GurobiGadgetoCompiler.generate(
    PC([1, 2, 3],args.i),  # The input constraint
    [(CUT, 2), (PosClause,1)],  # The output family constraints
    alpha_ub=14,  # We must set an upper bound for the alpha
    max_auxs=args.max_auxs,  # As well as the maximum number of aux variables that we will use
)
g.show()
is_gadget, is_strict = g.show_assignment_values()
alpha, beta = g.bounds[1], g.beta
print(f"This is {'' if is_gadget else 'NOT '}a ({alpha}-{beta})-gadget")
print("Is strict?", is_strict)

PC to DICUT

import argparse

from gadgetocompiler.compilers.z3 import Z3GadgetoCompiler
from gadgetocompiler.constraints import Clause
from gadgetocompiler.compilers.gurobi import GurobiGadgetoCompiler
from PC import PC
from DICUT import DICUT

parser = argparse.ArgumentParser()
parser.add_argument("--i", type=int, required=True)
parser.add_argument("--max-auxs", "-b", type=int, required=True)
args = parser.parse_args()

print(f"i: {args.i}")
print(f"max_auxs: {args.max_auxs}")

g = GurobiGadgetoCompiler.generate(
    PC([1, 2, 3], args.i),  # The input constraint
    [(DICUT, 2)],  # The output family constraints
    alpha_ub=14,  # We must set an upper bound for the alpha
    max_auxs=args.max_auxs,  # As well as the maximum number of aux variables that we will use
)
g.show()
is_gadget, is_strict = g.show_assignment_values()
alpha, beta = g.bounds[1], g.beta
print(f"This is {'' if is_gadget else 'NOT '}a ({alpha}-{beta})-gadget")
print("Is strict?", is_strict)

RMBC to 2SAT

import argparse

from gadgetocompiler.constraints import Clause
from gadgetocompiler.compilers.gurobi import GurobiGadgetoCompiler
from RMBC import RMBC

parser = argparse.ArgumentParser()
parser.add_argument("--i", type=int, required=True)
parser.add_argument("--j", type=int, required=True)
parser.add_argument("--max-auxs", "-b", type=int, required=True)
args = parser.parse_args()

print(f"i: {args.i}")
print(f"j: {args.j}")
print(f"max_auxs: {args.max_auxs}")

g = GurobiGadgetoCompiler.generate(
    RMBC([1, 2, 3, 4], args.i, args.j),  # The input constraint
    [(Clause, 2), (Clause, 1)],  # The output family constraints
    alpha_ub=12.5,  # We must set an upper bound for the alpha
    max_auxs=args.max_auxs,  # As well as the maximum number of aux variables that we will use
)
g.show()
is_gadget, is_strict = g.show_assignment_values()
alpha, beta = g.bounds[1], g.beta
print(f"This is {'' if is_gadget else 'NOT '}a ({alpha}-{beta})-gadget")
print("Is strict?", is_strict)

RMBC to 3SAT

import argparse

from gadgetocompiler.constraints import Clause
from gadgetocompiler.compilers.gurobi import GurobiGadgetoCompiler
from RMBC import RMBC

parser = argparse.ArgumentParser()
parser.add_argument("--i", type=int, required=True)
parser.add_argument("--j", type=int, required=True)
parser.add_argument("--max-auxs", "-b", type=int, required=True)
args = parser.parse_args()

print(f"i: {args.i}")
print(f"j: {args.j}")
print(f"max_auxs: {args.max_auxs}")

g = GurobiGadgetoCompiler.generate(
    RMBC([1, 2, 3, 4], args.i, args.j),  # The input constraint
    [(Clause, 3), (Clause, 2), (Clause, 1)],  # The output family constraints
    alpha_ub=12.5,  # We must set an upper bound for the alpha
    max_auxs=args.max_auxs,  # As well as the maximum number of aux variables that we will use
)
g.show()
is_gadget, is_strict = g.show_assignment_values()
alpha, beta = g.bounds[1], g.beta
print(f"This is {'' if is_gadget else 'NOT '}a ({alpha}-{beta})-gadget")
print("Is strict?", is_strict)

RMBC to CUT/0

import argparse

from gadgetocompiler.constraints import Clause
from gadgetocompiler.compilers.gurobi import GurobiGadgetoCompiler
from RMBC import RMBC
from CUT import CUT
from PosClause import PosClause

parser = argparse.ArgumentParser()
parser.add_argument("--i", type=int, required=True)
parser.add_argument("--j", type=int, required=True)
parser.add_argument("--max-auxs", "-b", type=int, required=True)
args = parser.parse_args()

print(f"i: {args.i}")
print(f"j: {args.j}")
print(f"max_auxs: {args.max_auxs}")

g = GurobiGadgetoCompiler.generate(
    RMBC([1, 2, 3, 4], args.i, args.j),  # The input constraint
    [(CUT, 2), (PosClause,1)],  # The output family constraints
    alpha_ub=12.5,  # We must set an upper bound for the alpha
    max_auxs=args.max_auxs,  # As well as the maximum number of aux variables that we will use
)
g.show()

RMBC to DICUT

import argparse

from gadgetocompiler.constraints import Clause
from gadgetocompiler.compilers.gurobi import GurobiGadgetoCompiler
from RMBC import RMBC
from DICUT import DICUT

parser = argparse.ArgumentParser()
parser.add_argument("--i", type=int, required=True)
parser.add_argument("--j", type=int, required=True)
parser.add_argument("--max-auxs", "-b", type=int, required=True)
args = parser.parse_args()

print(f"i: {args.i}")
print(f"j: {args.j}")
print(f"max_auxs: {args.max_auxs}")

g = GurobiGadgetoCompiler.generate(
    RMBC([1, 2, 3, 4], args.i, args.j),  # The input constraint
    [(DICUT, 2)],  # The output family constraints
    alpha_ub=12.5,  # We must set an upper bound for the alpha
    max_auxs=args.max_auxs,  # As well as the maximum number of aux variables that we will use
)
g.show()
is_gadget, is_strict = g.show_assignment_values()
alpha, beta = g.bounds[1], g.beta
print(f"This is {'' if is_gadget else 'NOT '}a ({alpha}-{beta})-gadget")
print("Is strict?", is_strict)

Optimal Gadgets

\(\alpha\) \(\beta\) # Auxs Vars Time (s) Gadget
3SAT -> 2SAT 3.5 4 1 5 details
3ConjSAT -> 2SAT 4 4 1 5 details
\(PC_0\) -> 3SAT 4 4 0 2 details
\(PC_1\) -> 3SAT 4 4 0 7 details
\(PC_0\) -> 2SAT 11 12 4 11 details
\(PC_1\) -> 2SAT 11 12 4 7 details
\(PC_0\) -> CUT/0 8 10 8 23101 details
\(PC_1\) -> CUT/0 9 14 1 5 details
\(PC_0\) -> DICUT 6.5 15.5 7 761 details
\(PC_1\) -> DICUT 6.5 15.5 7 876 details
\(RMBC_{00}\) -> 3SAT 4 4 0 2 details
\(RMBC_{01}\) -> 3SAT 4 4 0 2 details
\(RMBC_{10}\) -> 3SAT 4 4 0 2 details
\(RMBC_{11}\) -> 3SAT 4 4 0 2 details
\(RMBC_{00}\) -> 2SAT 11 12 4 32 details
\(RMBC_{01}\) -> 2SAT 11 12 4 59 details
\(RMBC_{10}\) -> 2SAT 11 12 4 25 details
\(RMBC_{11}\) -> 2SAT 11 12 4 55 details
\(RMBC_{00}\) -> CUT/0 8 11 5 285 details
\(RMBC_{11}\) -> CUT/0 9 13 3 3 details
\(RMBC_{00}\) -> DICUT 6 16 2 2 details
\(RMBC_{01}\) -> DICUT 6.5 15.5 7 11454* details
\(RMBC_{10}\) -> DICUT 6.5 15.5 7 5043* details
\(RMBC_{11}\) -> DICUT 7 20 2 2 details

For results with "*" we used Gurobi with 40 threads. Otherwise, only one thread.