PySMS Reference

This is a full reference of the Python library PySMS that provides an interface for building encodings of graph problems recognizable by the SMS solvers smsg and smsd. The GraphEncodingBuilder class provides the interface to build SMS-ready encodings of graph problems, most importantly through var_edge(u, v), which maps graph edges to their propositional variables. In addition to this, GraphEncodingBuilder contains a ready-made library of common constraints, including cardinality constraints. The module can also be executed from the command line in order to generate or directly solve a formula built from the predifined constraints, which can be passed via command-line parameters.

This file can either directly be used for generating all graphs described by some predefined properties which can be selected with flags, or it can be use as starting point for constructing an encoding for graphs on your own.

GraphEncodingBuilder

Bases: IDPool, list

A class for building an encoding in the context of SMS. The IDPool gives the next free variable whilst the list contains the clauses

Source code in pysms/graph_builder.py
class GraphEncodingBuilder(IDPool, list):
    """A class for building an encoding in the context of SMS.
    The IDPool gives the next free variable whilst the list contains the clauses


    """

    def __init__(self, n, directed=False, multiGraph=None, staticInitialPartition=False, underlyingGraph=False, DEBUG=0):
        super().__init__()
        self.directed = directed
        self.V = list(range(n))
        self.n = n
        self.DEBUG = DEBUG
        self.varStaticInitialPartition = None

        self.paramsSMS = {"vertices": self.n, "print-stats": True, "frequency": 30}  # default params

        # order in which variables are assigned is import !!!
        if directed:
            self.varEdgeDirectedTable = [[None for _ in self.V] for _ in self.V]
            for v, u in permutations(self.V, 2):
                self.varEdgeDirectedTable[v][u] = self.id()
        elif multiGraph:
            self.paramsSMS["multi-graph"] = multiGraph
            self.varEdgeMultiTable = [[[None for _ in self.V] for _ in self.V] for _ in range(multiGraph)]
            for i in range(multiGraph):
                for v, u in combinations(self.V, 2):
                    self.varEdgeMultiTable[i][v][u] = self.varEdgeMultiTable[i][u][v] = self.id()
            self.varEdgeTable = self.varEdgeMultiTable[0]  # allows arguing over the first graph
        else:
            self.varEdgeTable = [[None for _ in self.V] for _ in self.V]
            for v, u in combinations(self.V, 2):
                self.varEdgeTable[v][u] = self.varEdgeTable[u][v] = self.id()

        if staticInitialPartition:
            self.varStaticInitialPartition = [[None for _ in self.V] for _ in self.V]
            for v, u in combinations(self.V, 2):
                self.varStaticInitialPartition[v][u] = self.varStaticInitialPartition[u][v] = self.id()

            for u, v, w in combinations(self.V, 3):
                # ensure that partition is well defined, i.e., elements in the middle are equal
                self.append([-self.var_partition(u, w), self.var_partition(u, v)])
                self.append([-self.var_partition(u, w), self.var_partition(v, w)])
                # transitive
                self.append([-self.var_partition(u, v), -self.var_partition(v, w), self.var_partition(u, w)])

        if underlyingGraph:
            self.varEdgeTable = [[None for _ in self.V] for _ in self.V]
            for v, u in combinations(self.V, 2):
                self.varEdgeTable[v][u] = self.varEdgeTable[u][v] = self.id()
                self.CNF_OR_APPEND(
                    [self.varEdgeDirectedTable[v][u], self.varEdgeDirectedTable[u][v]], self.varEdgeTable[v][u]
                )  # relelation between edge variables and directed edge variables, i.e., undirected is the underlying graph

    def var_edge(self, u, v) -> int:
        """Get the propositional variable associated with the undirected edge {u,v}

        :param u: a vertex
        :param v: a different vertex
        :returns: the undirected edge variable e_{u,v}

        """
        return self.varEdgeTable[u][v]

    def var_edge_multi(self, i, u, v) -> int:
        """Get the propositional variable associated with the undirected edge {u,v} of the i-th graph

        :param i: the graph index
        :param v: a vertex
        :param u: a different vertex
        :returns: the undirected multigraph edge variable e^i_{u,v}

        """
        return self.varEdgeMultiTable[i][u][v]

    def var_edge_dir(self, u, v) -> int:
        """Get the propositional variable associated with the directed edge (u,v)

        :param u: a vertex
        :param v: a different vertex
        :returns: the directed edge variable e_{u,v}

        """
        return self.varEdgeDirectedTable[u][v]

    def var_partition(self, u, v) -> int:
        """Get the propositional variable which holds whether u and v are in the same partition

        :param u: a vertex
        :param v: a different vertex
        :returns: the partition description variable

        """
        return self.varStaticInitialPartition[u][v]

    def solve(self, allGraphs=False, hideGraphs=False, cnfFile=None, args_SMS="", forwarding_args=[], graph6_format=False) -> None:
        """Solve the formula, given the encoding, using SMS.

        :param allGraphs: Enumerate all satisfying graphs. Default value = False
        :param hideGraphs: Count all satisfying graphs. Default value = False
        :param cnfFile: Write constraints here, use a temporary if None. Default value = None
        :param forwarding_args: Forward these arguments to SMS. Default value = []
        :param graph_format: Toggle output format. Choices are [graph6, edge_list]. Default value = edge-list
        :param args_SMS: Forward these arguments to SMS. Deprecated, use `forwarding_args` instead. Default value = ""

        """
        if cnfFile == None:
            import time

            cnfFile = f"./temp{os.getpid()}_t{time.time()}.enc"  # TODO use tempfile module
        with open(cnfFile, "w") as cnf_fh:
            self.print_dimacs(cnf_fh)  # write script to temporary file

        program = "smsd" if self.directed else "smsg"  # we expect these binaries to be on PATH

        # add arguments

        if allGraphs:
            self.paramsSMS["all-graphs"] = ""
        if hideGraphs:
            self.paramsSMS["hide-graphs"] = ""
        if self.varStaticInitialPartition:
            self.paramsSMS["combine-static-dynamic"] = ""

        python_args_SMS = " ".join(f"--{param} {value}" for param, value in self.paramsSMS.items())

        sms_command = "time " if self.DEBUG else ""
        sms_command += f"{program} {python_args_SMS} {args_SMS} --dimacs {cnfFile}"  # TODO eventually parse args_SMS to allow to override
        for arg in forwarding_args:
            sms_command += f" '{arg}'"

        if self.DEBUG:
            print("running the command: ", sms_command)

        if graph6_format:
            import networkx as nx

            for line in os.popen(sms_command).read().split("\n"):
                if line and line[0] == "[":
                    edges = literal_eval(line)
                    print(nx.to_graph6_bytes(nx.Graph(edges), header=False).decode(), end="")
                elif self.DEBUG:
                    print(line, end="\n")
        else:
            os.system(sms_command)

        os.system(f"rm {cnfFile}")  # cleanup

    def solveArgs(self, args, forwarding_args) -> None:
        """Wrapper for solving using arguments provided by argsParser

        :param args: Arguments for PySMS
        :param forwarding_args: Arguments to be forwarded to SMS

        """
        self.solve(allGraphs=args.all_graphs, hideGraphs=args.hide_graphs, cnfFile=args.cnf_file, args_SMS=args.args_SMS, forwarding_args=forwarding_args, graph6_format=args.graph6_format)

    # ------------------some utilies--------------------------

    def CNF_OR_APPEND(self, ins, out) -> None:
        """extends with an encoding of an or gate

        :param ins: input literal ids
        :param out: output gate variable id

        """
        self.extend(CNF_OR(ins, out))

    def CNF_AND_APPEND(self, ins, out) -> None:
        """extends with an encoding of an and gate

        :param ins: input literal ids
        :param out: output gate variable id

        """
        self.extend(CNF_AND(ins, out))

    def CNF_OR(self, ins) -> int:
        """returns a new variable which is true iff at least one of the literals in 'ins' is true

        :param ins: input literal ids
        :returns: a fresh Boolean variable equivalent to the disjunction of ins

        """
        out = self.id()
        self.extend(CNF_OR(ins, out))
        return out

    def CNF_AND(self, ins) -> int:
        """returns a new variable which is true iff all literals in 'ins' are true

        :param ins: input literal ids
        :returns: a fresh Boolean variable equivalent to the disjunction of ins

        """
        out = self.id()
        self.extend(CNF_AND(ins, out))
        return out

    def print_dimacs(self, outstream) -> None:
        """Print the current encoding to the given output stream

        :param outstream: print to here

        """
        print(f"p cnf {self.nextId-1} {len(self)}", file=outstream)
        for c in self:
            print(" ".join(str(x) for x in c), 0, file=outstream)

    def counterFunction(self, literals, countUpto, atMost=None, atLeast=None, counterType="sequential") -> list[int]:
        """A wrapper for `pysms.counters.counterFunction`: constraints are added to and the ids are provided by the builder object (self).

        :param literals: create a counter over these literals
        :param countUpto: extend the counter to this value
        :param atMost: enforce a cardinality upper bound. Default value = None
        :param atLeast: enforce a cardinality lower bound. Default value = None
        :param counterType: specify which counter encoding to use. Default value = "sequential"
        :returns: auxiliary counter variables

        """
        return counterFunction(literals, countUpto, self, self, atMost=atMost, atLeast=atLeast, type=counterType)

    # -------------------------ENCODINGS----------------------------------------

    def add_constraints_by_arguments(self, args) -> None:
        """Add constraints based on args given by default parser

        :param args: arguments parsed from the command line

        """
        g = self
        if g.DEBUG:
            print("Arguments:", args, file=stderr)
            stderr.flush()
        self.degreeBounds(self.V, args.delta_low, args.Delta_upp, encoding=args.counter)
        self.edgeBounds(self.V, args.num_edges_low, args.num_edges_upp, encoding=args.counter)
        if args.diam2_critical:
            self.diameter2critical()

        if args.Ck_free:
            self.ckFree(args.Ck_free)
        if args.girth:
            self.minGirth(args.girth)
        if args.girth_compact:
            self.minGirthCompact(args.girth_compact)

        if args.alpha_upp:
            self.maxIndependentSet(args.alpha_upp)
        if args.omega_upp:
            self.maxClique(args.omega_upp)
        if args.ramsey:
            self.maxIndependentSet(args.ramsey[0] - 1)
            self.maxClique(args.ramsey[1] - 1)

        if args.mtf:
            self.mtf()

        if args.no_subsuming_neighborhoods:
            self.noSubsumingNeighborhoods()
        if args.degree_partition:
            self.sort_vertices_by_degree()

        if args.chi_upp:
            self.maxChromaticNumber(args.chi_upp)

        if args.chi_low:
            self.paramsSMS["min-chromatic-number"] = args.chi_low

        if args.color_critical:
            self.colorCritical(args.color_critical)

        if args.connectivity_low:
            self.minConnectivity(args.connectivity_low)

        if args.planar_kuratowski:
            self.paramsSMS["planar"] = 5  # DEFAULT planarity frequency

        if args.even_degrees:
            for u in self.V:
                shouldBe([+self.var_edge(u, v) for v in self.V if v != u], [i for i in self.V if i % 2 == 0], self, self, type=DEFAULT_COUNTER)

        if args.fix_k_clique:
            k = args.fix_k_clique
            for u, v in combinations(range(k), 2):
                self.append([self.var_edge(u, v)])
            self.paramsSMS["initial-partition"] = f"{k} {self.n - k}"

        if args.circulant:
            self.circulant()
        if args.block_circulant:
            self.block_circulant(args.block_circulant)

        if args.contains_cliques:
            self.contains_cliques(args.contains_cliques[0], args.contains_cliques[1])

        if args.partial_sym_break:
            self.partialSymBreak()

        if args.exclude_balanced_bipartite_graph:
            self.excludeBalancedBipartiteGraph()

        if args.fixed_induced_subgraph:
            with open(args.fixed_induced_subgraph, "r") as f:
                for i, line in enumerate(f):
                    if i == args.fixed_induced_subgraph_line:
                        l = line.split()
                        n = int(l[0])
                        l = l[1:]
                        edges = []
                        for i in range(len(l) // 2):
                            edges.append((int(l[2 * i]), int(l[2 * i + 1])))

                        matrix = [[0 for _ in range(n)] for _ in range(n)]
                        for u, v in edges:
                            matrix[u][v] = matrix[v][u] = 1
                        for u, v in combinations(range(n), 2):
                            if matrix[u][v] == 0:
                                self.append([-self.var_edge(u, v)])
                            else:
                                self.append([self.var_edge(u, v)])
                        self.paramsSMS["fixed-subgraph-size"] = n
                        break

        if args.bcp:  # !!!!!!!! must be applied at the end
            print("Number of propagated literals:", self.bcp(), file=stderr)

    # ------------degree encodings--------------

    def minDegree(self, delta, countertype=DEFAULT_COUNTER) -> list[list[int]]:
        """Minimum degree at least delta

        :param delta: degree lower bound
        :param countertype: specify a cardinality encoding. Default value = DEFAULT_COUNTER

        """
        return self.degreeBounds(self.V, delta, None, encoding=countertype)

    def maxDegree(self, delta, countertype=DEFAULT_COUNTER) -> list[list[int]]:
        """Maximum degree at most delta

        :param delta: degree upper bound
        :param countertype: specify a cardinality encoding. Default value = DEFAULT_COUNTER

        """
        return self.degreeBounds(self.V, None, delta, encoding=countertype)

    def degreeBounds(self, verts, lower, upper, within=False, encoding=DEFAULT_COUNTER) -> list[list[int]]:
        """Enforce that each of verts has degree between lower and upper (either of which can be None)

        :param verts: the set of vertices on which to operate
        :param lower: a lower bound for the minimum degree
        :param upper: an upper bound for the maximum degree
        :param within: whether to count the degree only within verts, or to the whole graph (Default value = False)
        :param encoding: the cardinality encoding to be used (Default value = DEFAULT_COUNTER)
        :returns: A list of lists of auxiliary variables, which say that the `i`-th vertex has degree at least `j`,
        or `None` if the bounds are meaningless.

        """
        if lower == None and upper == None:
            return None
        if lower and upper and lower > upper:
            self.append([])
            return None
        aux = []
        target_set = verts if within else self.V
        count_limit = upper if upper else lower
        for u in verts:
            aux.append(self.counterFunction([self.var_edge(u, v) for v in target_set if v != u], count_limit, upper, lower, counterType=encoding))
        return aux

    # -------------------number of edges ------------------

    def numEdgesUpp(self, m, countertype=DEFAULT_COUNTER) -> list[int]:
        """An upper bound on the number of edges

        :param m: an upper bound on the number of edges
        :param countertype: the cardinality encoding to be used (Default value = DEFAULT_COUNTER)

        """
        return self.edgeBounds(self.V, None, m, encoding=countertype)

    def numEdgesLow(self, m, countertype=DEFAULT_COUNTER) -> list[int]:
        """A lower bound on the number of edges

        :param m: a lower bound on the number of edges
        :param countertype: the cardinality encoding to be used (Default value = DEFAULT_COUNTER)

        """
        return self.edgeBounds(self.V, m, None, encoding=countertype)

    def edgeBounds(self, verts, lower, upper, encoding=DEFAULT_COUNTER) -> list[int]:
        """Enforce that each of verts has degree between lower and upper (either of which can be None)

        :param verts: the set of vertices between which to enforce edge bounds
        :param lower: lower bound on the number of edges
        :param upper: upper bound on the number of edges
        :param encoding: the cardinality encoding to be used (default DEFAULT_COUNTER)
        :returns: A list of auxiliary variables, which say that the number of edges is at least i,
        or None if the bounds are meaningless.

        """
        if lower == None and upper == None:
            return None
        if lower and upper and lower > upper and verts:
            self.append([])
            return None
        count_limit = upper if upper else lower
        return self.counterFunction([self.var_edge(u, v) for u, v in combinations(verts, 2)], count_limit, upper, lower, counterType=encoding)

    # ------------------------------------------------------

    def mtf(self) -> None:
        """Make the graph maximal triangle-free (adding any further edges will create triangles)"""
        self.ckFree(3)  # forbid triangles
        commonNeighbor = {(i, j, k): self.id() for i, j in combinations(self.V, 2) for k in self.V if k not in [i, j]}
        for i, j in combinations(self.V, 2):
            for k in self.V:
                if k in [i, j]:
                    continue
                self.CNF_AND_APPEND([self.var_edge(i, k), self.var_edge(j, k)], commonNeighbor[(i, j, k)])
            self.append([self.var_edge(i, j)] + [commonNeighbor[(i, j, k)] for k in self.V if k not in [i, j]])

    def noSubsumingNeighborhoods(self) -> None:
        """No neighborhood of a vertex is contained in the neighborhood of another vertex"""
        # different neighborhood
        for i, j in permutations(self.V, 2):
            # There must be a vertex adjecent to i which is not adjacent to j
            adjacentOnlyToI = []
            for k in self.V:
                if k == i or k == j:
                    continue
                kIsAdjacentOnlyToI = self.id()
                self.append([+self.var_edge(i, k), -kIsAdjacentOnlyToI])
                self.append([-self.var_edge(j, k), -kIsAdjacentOnlyToI])
                adjacentOnlyToI.append(kIsAdjacentOnlyToI)
            self.append([+self.var_edge(i, j)] + adjacentOnlyToI)

    def minConnectivity(self, connectivity_low) -> None:
        """Make graphs with minimum required connectivity

        :param connectivity_low: a lower bound on connectivity

        """
        assert self.n > connectivity_low  # an k-connected graph has at least k+1 vertices
        V = self.V
        var_edge = self.var_edge
        reachable = {
            (v, t, I): self.id() for k in range(args.connectivity_low) for I in combinations(sorted(set(V)), k) for v in set(V) - {min(set(V) - set(I))} - set(I) for t in V
        }  # u can reach v without I in t steps
        reachable_via = {
            (v, w, t, I): self.id()
            for k in range(args.connectivity_low)
            for I in combinations(sorted(set(V)), k)
            for v in set(V) - {min(set(V) - set(I))} - set(I)
            for t in V
            for w in set(V) - {min(set(V) - set(I)), v} - set(I)
        }  # u can reach v via w without I in t steps

        def var_reachable(v, t, I):
            return reachable[(v, t, I)]

        def var_reachable_via(v, w, t, I):
            return reachable_via[(v, w, t, I)]

        for k in range(args.connectivity_low):
            for I in combinations(sorted(set(V)), k):  # remove I and check if still connected
                u = min(set(V) - set(I))
                for v in set(V) - {u} - set(I):
                    for t in V:
                        if t == 0:
                            # reachable in first step if adjacent
                            self.append([-var_edge(v, u), +var_reachable(v, 0, I)])
                            self.append([+var_edge(v, u), -var_reachable(v, 0, I)])

                        else:
                            self.append([-var_reachable(v, t, I), +var_reachable(v, t - 1, I)] + [+var_reachable_via(v, w, t, I) for w in set(V) - set(I) - {v, u}])
                            self.append([+var_reachable(v, t, I), -var_reachable(v, t - 1, I)])
                            for w in set(V) - set(I) - {v, u}:
                                self.append([+var_reachable(v, t, I), -var_reachable_via(v, w, t, I)])

                                self.append([+var_reachable_via(v, w, t, I), -var_reachable(w, t - 1, I), -var_edge(w, v)])
                                self.append([-var_reachable_via(v, w, t, I), +var_reachable(w, t - 1, I)])
                                self.append([-var_reachable_via(v, w, t, I), +var_edge(w, v)])
                    # must be reached
                    self.append([+var_reachable(v, max(V), I)])

    def diameter2critical(self) -> None:
        """Ensure that the graph has diameter two and removing any edge results in a graph with diameter > 2"""
        g = self
        V = g.V
        var_edge = g.var_edge
        commonNeighbor = {(i, j, k): g.id() for i, j in combinations(V, 2) for k in set(V) - {i, j}}

        for i, j in combinations(V, 2):
            for k in set(V) - {i, j}:
                L = (i, j, k)
                g.CNF_AND_APPEND([+var_edge(i, k), +var_edge(j, k)], commonNeighbor[L])

        noCommonNeighbor = {(i, j): g.id() for i, j in combinations(V, 2)}
        for i, j in combinations(V, 2):
            for k in set(V) - {i, j}:
                # if the have a common neighbor, noCommonNeighbor is false
                g.append([-commonNeighbor[(i, j, k)], -noCommonNeighbor[(i, j)]])

        for i, j in combinations(V, 2):
            g.append([+var_edge(i, j)] + [+commonNeighbor[(i, j, k)] for k in set(V) - {i, j}])  # adjacent or common neighbor

        for i, j in combinations(V, 2):
            # ensure that critical i.e. if edge ij is present removing will lead to diamter > 2
            clause = [-var_edge(i, j), +noCommonNeighbor[(i, j)]]
            for k in set(V) - {i, j}:
                for v1, v2 in [(i, j), (j, i)]:
                    # v2 and k have diameter > after removing ij
                    # v1 adjacent to k and v1 is the only common neighbor from v2 and k. And k not adjacent to v2
                    diameterIncreasing = g.id()
                    g.append([+var_edge(v1, k), -diameterIncreasing])
                    g.append([-var_edge(v2, k), -diameterIncreasing])
                    for l in set(V) - {i, j, k}:
                        g.append([-commonNeighbor[(min(v2, k), max(v2, k), l)], -diameterIncreasing])
                    clause.append(diameterIncreasing)
            g.append(clause)

    def maxIndependentSet(self, x) -> None:
        """No independent sets of size greater than x

        :param x: upper bound on the size of independent sets

        """
        g = self
        for S in combinations(g.V, x + 1):
            g.append([+g.var_edge(i, j) for i, j in combinations(S, 2)])

    def maxClique(self, x) -> None:
        """No cliques of size greater than x

        :param x: upper bound on the size of cliques

        """
        g = self
        for S in combinations(g.V, x + 1):
            g.append([-g.var_edge(i, j) for i, j in combinations(S, 2)])

    def ckFree(self, k) -> None:
        """Forbid k-cycles (C_k) as non-induced subgraphs

        :param k: length of the cycle

        """
        g = self
        for cycle in permutations(g.V, k):
            if cycle[0] != min(cycle):
                continue
            if cycle[1] > cycle[-1]:
                continue
            g.append([-g.var_edge(cycle[i], cycle[(i + 1) % k]) for i in range(k)])  # at least one edge absent from potential cycle

    def minGirth(self, k) -> None:
        """Basic encoding to ensure that the girth is at least k, i.e., no cycle with length < k

        :param k: girth lower bound

        """
        for l in range(3, k):  # forbid 3 cycles up to k-1 cycles
            self.ckFree(l)

    def minGirthCompact(self, minGirth) -> None:
        """
        Compact encoding to ensure that the girth is at least k, i.e., no cycle with length < k

        :param minGirth: girth lower bound
        """
        g = self
        # check distance of i,j without edge i,j.
        for i, j in combinations(g.V, 2):
            reached = [g.var_edge(i, k) if k not in [i, j] and k > i else None for k in g.V]

            for _ in range(minGirth - 4):  # if girth is 4 than no triangles so not in the loop
                reachedNew = [g.id() for _ in g.V]
                for k in g.V:
                    if k in [i, j] or k < i:
                        continue
                    g.append([-reached[k], +reachedNew[k]])  # already reached

                    # check if reached over l
                    for l in g.V:
                        if l in [i, j, k] or l < i:
                            continue
                        g.append([-g.var_edge(k, l), -reached[l], +reachedNew[k]])  # l reached in previous step and edge implies reached
                reached = reachedNew

            for k in g.V:
                if k in [i, j] or k < i:
                    continue
                # not reached, not adjacent to j, or edge not present
                g.append([-g.var_edge(i, j), -g.var_edge(j, k), -reached[k]])

    def maxChromaticNumber(self, chi) -> None:
        """The graph should be chi-colorable

        :param chi: nubmer of colors

        """
        g = self
        color = [[g.id() for _ in range(chi)] for _ in g.V]
        for v in g.V:
            g.append([color[v][i] for i in range(chi)])  # each vertex should have a color

        for v, w in combinations(g.V, 2):
            for i in range(chi):
                g.append([-g.var_edge(v, w), -color[v][i], -color[w][i]])  # adjacent vertices are not allowed to have the same color

    def colorCritical(self, chi):
        nColors = chi - 1
        for v in self.V:
            # check if G-v is args.critical - 1 colorable
            colors = [[self.id() for _ in self.V] for _ in range(nColors)]
            # at least one color
            for u in self.V:
                if u != v:
                    self.append([colors[r][u] for r in range(nColors)])
            # adjacent once cannot have the same color
            for u1, u2 in combinations(self.V, 2):
                if u1 == v or u2 == v:
                    continue
                for r in range(nColors):
                    self.append([-self.var_edge(u1, u2), -colors[r][u1], -colors[r][u2]])

            # basic symmetry breaking: there must be a smaller vertex for each smaller color not choosen
            for v in self.V:
                for c in range(nColors):
                    for cSmaller in range(c):
                        self.append([-colors[c][v]] + [colors[cSmaller][u] for u in range(v)])

    # --------------------------Encodings for initial static partition--------------------------------

    def sort_vertices_by_degree(self) -> None:
        """Create a static encoding of a partition, such that all vertices are sorted by degree.
        Currently only works for undirected graphs


        """
        g = self
        var_deg = []
        for v in g.V:
            counter_vars = g.counterFunction([g.var_edge(v, u) for u in g.V if u != v], g.n)
            var_deg.append(counter_vars)

        # compare consequent vertices
        for v1 in range(g.n - 1):
            v2 = v1 + 1

            # v2 is not allowed to have a lower degree
            for d in range(g.n - 1):
                g.append([-var_deg[v1][d], +var_deg[v2][d]])

            same_degree_options = []
            for d in range(g.n - 1):
                s = g.CNF_AND([+var_deg[v1][d], +var_deg[v2][d], -var_deg[v1][d + 1], -var_deg[v2][d + 1]])  # if s true then both have degree d - 1
                same_degree_options.append(s)
            g.CNF_OR_APPEND(same_degree_options, g.var_partition(v1, v2))

    def slimSingleStep(self, nOriginal, nReplaced, G, induced=False) -> None:
        """Apply single step of SLIM given the graph G as edgelist, where some random vertices are replaced"""
        import random

        VR = random.sample(range(nOriginal), k=nReplaced)

        nFixed = nOriginal - nReplaced
        self.paramsSMS["initial-partition"] = nFixed * "1 " + str(self.n - nFixed)

        remappingVertices = dict()
        cur = 0
        for v in set(range(nOriginal)) - set(VR):
            remappingVertices[v] = cur
            cur += 1
        r = remappingVertices

        remappedEdges = [(min(r[u], r[v]), max(r[u], r[v])) for u, v in G if u not in VR and v not in VR]
        # fix this subgraph in the encoding
        for u, v in remappedEdges:
            self.append([+self.var_edge(u, v)])
        if induced:
            for u, v in combinations(range(nFixed), 2):
                if (u, v) not in G and (v, u) not in G:
                    self.append([-self.var_edge(u, v)])

    def lex_smaller(self, seq1, seq2):
        """Ensure that seq1 is lexicographically smaller or equal than seq2"""
        assert len(seq1) == len(seq2)
        all_previous_equal = self.id()
        self.append([+all_previous_equal])
        for i in range(len(seq1)):
            self.append([-all_previous_equal, -seq1[i], +seq2[i]])  # all previous equal implies seq1[i] <= seq2[i]
            all_previous_equal_new = self.id()
            self.append([-all_previous_equal, -seq1[i], +all_previous_equal_new])
            self.append([-all_previous_equal, +seq2[i], +all_previous_equal_new])
            all_previous_equal = all_previous_equal_new

    def circulant(self, symmetryBreaking=True) -> None:
        self.paramsSMS["no-SMS"] = True
        for d in range(1, self.n // 2 + 1):  # math.ceil(self.n // 2) + 1): # math.ceil(self.n // 2) + 1
            for u in self.V:
                # must be equivalent
                u1 = 0
                u2 = d
                v1 = u
                v2 = (u + d) % self.n
                self.append([-self.var_edge(u1, u2), +self.var_edge(v1, v2)])
                self.append([+self.var_edge(u1, u2), -self.var_edge(v1, v2)])

        def edge2distance(u, v):
            return min(abs(u - v), self.n - abs(u - v))  # ensures that symmetric

        if False:
            # no-4-clique
            for potentialClique in combinations(range(1, self.n), 3):
                potentialClique = list(potentialClique) + [0]
                self.append([-self.var_edge(*e) for e in combinations(potentialClique, 2)])

        if False:
            deg = (self.n - 1) // 3 - 0
            self.counterFunction([self.var_edge(0, i) for i in range(1, self.n)], deg, atLeast=deg, counterType=DEFAULT_COUNTER)

        if False:
            # no-4-independent set
            for potentialClique in combinations(range(1, self.n), 3):
                potentialClique = list(potentialClique) + [0]
                self.append([+self.var_edge(*e) for e in combinations(potentialClique, 2)])

        # introducing any additional edge would result in an 4-clique
        if False:
            for u in range(1, self.n // 2 + 1):
                v = 0  # additional edge given by uv

                cliques = []
                for W in combinations(self.V, 2):
                    if u in W or v in W:
                        continue
                    clique = list(W) + [u, v]
                    cliquePairs = [p for p in combinations(clique, 2)]
                    distances = list(set(map(lambda p: edge2distance(p[0], p[1]), cliquePairs)))
                    distances.remove(edge2distance(v, u))  # remove the edges which would be introduced
                    # print(u, distances)
                    clique_var = self.CNF_AND([self.var_edge(0, d) for d in distances])
                    cliques.append(clique_var)
                # either edge is already present or it would introduce a 4-clique
                self.append([+self.var_edge(v, u)] + cliques)

        if symmetryBreaking:
            # symmetry breaking (only for primes)
            seq1 = [self.var_edge(0, i) for i in range(1, self.n // 2 + 1)]
            for first_vertex in [0]:  # don't need to check all because in same orbit
                for step_size in range(1, self.n // 2 + 1):
                    if first_vertex == 0 and step_size == 1:
                        continue

                    if math.gcd(step_size, self.n) != 1:
                        continue
                    reordering = [(first_vertex + step_size * i) % self.n for i in range(self.n)]
                    for i in range(self.n):
                        assert i in reordering
                    # print(reordering)

                    seq2 = [self.var_edge(reordering[0], reordering[i]) for i in range(1, self.n // 2 + 1)]
                    # seq2 = [ self.var_edge(0 , edge2distance(reordering[0], reordering[i])) for i in range(self.n // 2)]
                    self.lex_smaller(seq1, seq2)

    def block_circulant(self, nBlocks=None, symmetryBreaking=1) -> None:
        """if symmetry breaking = 0 then no sym breaking
        if symmetry brekaing = 1 then blocks
        if symmetry breaking = m then also permute m blocks
        # TODO also swap blocks
        """
        import sys

        print("Value of symmetry breaking", symmetryBreaking, file=sys.stderr)
        self.paramsSMS["no-SMS"] = True
        # Number of circulant graphs if not given then smallest prime factor
        if nBlocks == None:
            nBlocks = min([i for i in range(2, self.n) if self.n % i == 0])

        # ensure that the diagonal blocks are circulant
        blocksize = self.n // nBlocks
        for i in range(nBlocks):
            for j in range(i, nBlocks):
                for d in range(0 if i != j else 1, blocksize):  # also diagonal most be considered if i != j
                    for u in range(blocksize):
                        u1 = i * blocksize + 0
                        u2 = j * blocksize + d

                        v1 = i * blocksize + u
                        v2 = j * blocksize + (u + d) % blocksize

                        self.append([-self.var_edge(u1, u2), +self.var_edge(v1, v2)])
                        self.append([+self.var_edge(u1, u2), -self.var_edge(v1, v2)])

        if symmetryBreaking:
            # simple one only permuting each block and swap blocks; use vertex ordering such that blocks are sorted
            vertexPairOrdering = []
            print("Number of blocks:", nBlocks)
            for i in range(nBlocks):
                for u, v in combinations(range(i * blocksize, (i + 1) * blocksize), 2):
                    vertexPairOrdering.append((u, v))
            for u, v in combinations(self.V, 2):
                if (u, v) not in vertexPairOrdering:
                    vertexPairOrdering.append((u, v))
            symmetryCount = 0

            def checkGivenPermutation(p):
                seq1 = []
                seq2 = []
                for u, v in vertexPairOrdering:
                    up = p[u]
                    vp = p[v]
                    if {u, v} == {up, vp}:  # TODO eventuell consider duplicates
                        continue
                    seq1.append(self.var_edge(u, v))
                    seq2.append(self.var_edge(up, vp))
                self.lex_smaller(seq1, seq2)

            # swap blocks
            for i, j in combinations(range(nBlocks), 2):
                permutation = list(range(self.n))
                for k in range(blocksize):
                    permutation[blocksize * i + k] = blocksize * j + k
                    permutation[blocksize * j + k] = blocksize * i + k
                checkGivenPermutation(permutation)
                symmetryCount += 1

            print("Number of symmetries:", symmetryCount)

            # symmetry breaking on individual blocks
            for i in range(nBlocks):
                first_vertex = i * blocksize
                for step_size in range(2, blocksize // 2 + 1):
                    if math.gcd(step_size, blocksize) != 1:
                        continue
                    permutation = list(range(i * blocksize)) + [(step_size * i) % blocksize + first_vertex for i in range(blocksize)] + list(range((i + 1) * blocksize, self.n))
                    for i in range(self.n):
                        assert i in permutation
                    # print(reordering)
                    checkGivenPermutation(permutation)
                    symmetryCount += 1
            print("Number of symmetries:", symmetryCount)

        if symmetryBreaking and False:
            permutationsBlock = [[] for i in range(nBlocks)]  # for each block store the symmetries
            for i in range(nBlocks):
                first_vertex = i * blocksize
                for step_size in range(1, blocksize):
                    if math.gcd(step_size, blocksize) != 1:
                        continue
                    reordering = list(range(i * blocksize)) + [(step_size * i) % blocksize + first_vertex for i in range(blocksize)] + list(range((i + 1) * blocksize, self.n))
                    permutationsBlock[i].append(reordering)
            import sys

            # print(permutationsBlock, file=sys.stderr)
            counter = 0
            print("Value of symmetry breaking", symmetryBreaking, file=sys.stderr)
            for T in combinations(range(nBlocks), symmetryBreaking):
                print(T, file=sys.stderr)
                for P in product(*[permutationsBlock[i] for i in T]):
                    # print(P, file=sys.stderr)
                    counter += 1
                    seq1 = []
                    seq2 = []
                    for u, v in combinations(range(self.n), 2):
                        up = u
                        vp = v
                        for p in P:
                            up = p[up]
                            vp = p[vp]
                        if {u, v} == {up, vp}:
                            continue
                        seq1.append(self.var_edge(u, v))
                        seq2.append(self.var_edge(up, vp))
                    self.lex_smaller(seq1, seq2)
            print("Number of symmetries", counter, file=sys.stderr)
            # TODO also swap blocks

    def contains_cliques(self, s, k):
        """Ensures that the graph contains exactly s cliques of size k and each edge is part of such a clique"""
        cliqueVertices = [[self.id() for _ in self.V] for _ in range(s)]  # cliqueVertices[i][v] is true iff v is in clique i
        for i in range(s):
            self.counterFunction(cliqueVertices[i], k, k, k, counterType=DEFAULT_COUNTER)  # exactly k selected
            for u, v in combinations(self.V, 2):
                self.append([-cliqueVertices[i][u], -cliqueVertices[i][v], +self.var_edge(u, v)])
        # each edge is part of a clique
        for u, v in combinations(self.V, 2):
            self.append([-self.var_edge(u, v)] + [self.CNF_AND([cliqueVertices[i][u], cliqueVertices[i][v]]) for i in range(s)])

        # cliques can not be identical
        for i, j in combinations(range(s), 2):
            self.append([self.CNF_AND([+cliqueVertices[i][v], -cliqueVertices[j][v]]) for v in self.V])  # one in i but not in j

        # each vertex is in at most one clique
        for v in self.V:
            self.append([cliqueVertices[i][v] for i in range(s)])

        # symmetry breaking over cliques first clique is lex-smallest
        for i in range(1, s):
            self.lex_smaller(cliqueVertices[i - 1], cliqueVertices[i])

    def partialSymBreak(self):
        for u, v in combinations(self.V, 2):
            self.lex_smaller([self.var_edge(u, k) for k in range(self.n) if k not in [u, v]], [self.var_edge(v, k) for k in range(self.n) if k not in [u, v]])

    def bcp(self):
        """Perform unit propagation on the current encoding whilest ignoring the edge variables"""
        maxEdgeVar = self.n * (self.n - 1) // 2 if not self.directed else self.n**2 - self.n
        propagatedLiterals = [c[0] for c in self if len(c) == 1 if abs(c[0]) > maxEdgeVar]
        nPropagted = 0
        while len(propagatedLiterals) > 0:
            nPropagted += len(propagatedLiterals)
            # print("Propagated literals (inbetween):", len(propagatedLiterals))
            propagatedValues = [None for _ in range(self.nextId)]
            for l in propagatedLiterals:
                propagatedValues[abs(l)] = 1 if l > 0 else -1

            def sign(i):
                return 1 if i > 0 else -1

            def isSatisfied(c):
                return any([propagatedValues[abs(l)] and sign(l) == propagatedValues[abs(l)] for l in c])

            # already assumes that not satisfied
            def simplifyClause(c):
                return np.array([l for l in c if not propagatedValues[abs(l)]])

            # delete literals from clauses and delete satisfied clauses
            newClauses = [simplifyClause(c) for c in self if not isSatisfied(c)]
            self.clear()
            self.extend(newClauses)
            propagatedLiterals = [c[0] for c in self if len(c) == 1 if abs(c[0]) > maxEdgeVar]

        return nPropagted

    def excludeBalancedBipartiteGraph(self):
        nHalf = [self.n // 2] if self.n % 2 == 0 else [self.n // 2, self.n // 2 + 1]
        for h in nHalf:
            self.append([self.var_edge(u, v) for u, v in combinations(range(h), 2)] + [self.var_edge(u, v) for u, v in combinations(range(h, self.n), 2)])

CNF_AND(ins)

returns a new variable which is true iff all literals in 'ins' are true

Parameters:
  • ins

    input literal ids

Returns:
  • int

    a fresh Boolean variable equivalent to the disjunction of ins

Source code in pysms/graph_builder.py
def CNF_AND(self, ins) -> int:
    """returns a new variable which is true iff all literals in 'ins' are true

    :param ins: input literal ids
    :returns: a fresh Boolean variable equivalent to the disjunction of ins

    """
    out = self.id()
    self.extend(CNF_AND(ins, out))
    return out

CNF_AND_APPEND(ins, out)

extends with an encoding of an and gate

Parameters:
  • ins

    input literal ids

  • out

    output gate variable id

Source code in pysms/graph_builder.py
def CNF_AND_APPEND(self, ins, out) -> None:
    """extends with an encoding of an and gate

    :param ins: input literal ids
    :param out: output gate variable id

    """
    self.extend(CNF_AND(ins, out))

CNF_OR(ins)

returns a new variable which is true iff at least one of the literals in 'ins' is true

Parameters:
  • ins

    input literal ids

Returns:
  • int

    a fresh Boolean variable equivalent to the disjunction of ins

Source code in pysms/graph_builder.py
def CNF_OR(self, ins) -> int:
    """returns a new variable which is true iff at least one of the literals in 'ins' is true

    :param ins: input literal ids
    :returns: a fresh Boolean variable equivalent to the disjunction of ins

    """
    out = self.id()
    self.extend(CNF_OR(ins, out))
    return out

CNF_OR_APPEND(ins, out)

extends with an encoding of an or gate

Parameters:
  • ins

    input literal ids

  • out

    output gate variable id

Source code in pysms/graph_builder.py
def CNF_OR_APPEND(self, ins, out) -> None:
    """extends with an encoding of an or gate

    :param ins: input literal ids
    :param out: output gate variable id

    """
    self.extend(CNF_OR(ins, out))

add_constraints_by_arguments(args)

Add constraints based on args given by default parser

Parameters:
  • args

    arguments parsed from the command line

Source code in pysms/graph_builder.py
def add_constraints_by_arguments(self, args) -> None:
    """Add constraints based on args given by default parser

    :param args: arguments parsed from the command line

    """
    g = self
    if g.DEBUG:
        print("Arguments:", args, file=stderr)
        stderr.flush()
    self.degreeBounds(self.V, args.delta_low, args.Delta_upp, encoding=args.counter)
    self.edgeBounds(self.V, args.num_edges_low, args.num_edges_upp, encoding=args.counter)
    if args.diam2_critical:
        self.diameter2critical()

    if args.Ck_free:
        self.ckFree(args.Ck_free)
    if args.girth:
        self.minGirth(args.girth)
    if args.girth_compact:
        self.minGirthCompact(args.girth_compact)

    if args.alpha_upp:
        self.maxIndependentSet(args.alpha_upp)
    if args.omega_upp:
        self.maxClique(args.omega_upp)
    if args.ramsey:
        self.maxIndependentSet(args.ramsey[0] - 1)
        self.maxClique(args.ramsey[1] - 1)

    if args.mtf:
        self.mtf()

    if args.no_subsuming_neighborhoods:
        self.noSubsumingNeighborhoods()
    if args.degree_partition:
        self.sort_vertices_by_degree()

    if args.chi_upp:
        self.maxChromaticNumber(args.chi_upp)

    if args.chi_low:
        self.paramsSMS["min-chromatic-number"] = args.chi_low

    if args.color_critical:
        self.colorCritical(args.color_critical)

    if args.connectivity_low:
        self.minConnectivity(args.connectivity_low)

    if args.planar_kuratowski:
        self.paramsSMS["planar"] = 5  # DEFAULT planarity frequency

    if args.even_degrees:
        for u in self.V:
            shouldBe([+self.var_edge(u, v) for v in self.V if v != u], [i for i in self.V if i % 2 == 0], self, self, type=DEFAULT_COUNTER)

    if args.fix_k_clique:
        k = args.fix_k_clique
        for u, v in combinations(range(k), 2):
            self.append([self.var_edge(u, v)])
        self.paramsSMS["initial-partition"] = f"{k} {self.n - k}"

    if args.circulant:
        self.circulant()
    if args.block_circulant:
        self.block_circulant(args.block_circulant)

    if args.contains_cliques:
        self.contains_cliques(args.contains_cliques[0], args.contains_cliques[1])

    if args.partial_sym_break:
        self.partialSymBreak()

    if args.exclude_balanced_bipartite_graph:
        self.excludeBalancedBipartiteGraph()

    if args.fixed_induced_subgraph:
        with open(args.fixed_induced_subgraph, "r") as f:
            for i, line in enumerate(f):
                if i == args.fixed_induced_subgraph_line:
                    l = line.split()
                    n = int(l[0])
                    l = l[1:]
                    edges = []
                    for i in range(len(l) // 2):
                        edges.append((int(l[2 * i]), int(l[2 * i + 1])))

                    matrix = [[0 for _ in range(n)] for _ in range(n)]
                    for u, v in edges:
                        matrix[u][v] = matrix[v][u] = 1
                    for u, v in combinations(range(n), 2):
                        if matrix[u][v] == 0:
                            self.append([-self.var_edge(u, v)])
                        else:
                            self.append([self.var_edge(u, v)])
                    self.paramsSMS["fixed-subgraph-size"] = n
                    break

    if args.bcp:  # !!!!!!!! must be applied at the end
        print("Number of propagated literals:", self.bcp(), file=stderr)

bcp()

Perform unit propagation on the current encoding whilest ignoring the edge variables

Source code in pysms/graph_builder.py
def bcp(self):
    """Perform unit propagation on the current encoding whilest ignoring the edge variables"""
    maxEdgeVar = self.n * (self.n - 1) // 2 if not self.directed else self.n**2 - self.n
    propagatedLiterals = [c[0] for c in self if len(c) == 1 if abs(c[0]) > maxEdgeVar]
    nPropagted = 0
    while len(propagatedLiterals) > 0:
        nPropagted += len(propagatedLiterals)
        # print("Propagated literals (inbetween):", len(propagatedLiterals))
        propagatedValues = [None for _ in range(self.nextId)]
        for l in propagatedLiterals:
            propagatedValues[abs(l)] = 1 if l > 0 else -1

        def sign(i):
            return 1 if i > 0 else -1

        def isSatisfied(c):
            return any([propagatedValues[abs(l)] and sign(l) == propagatedValues[abs(l)] for l in c])

        # already assumes that not satisfied
        def simplifyClause(c):
            return np.array([l for l in c if not propagatedValues[abs(l)]])

        # delete literals from clauses and delete satisfied clauses
        newClauses = [simplifyClause(c) for c in self if not isSatisfied(c)]
        self.clear()
        self.extend(newClauses)
        propagatedLiterals = [c[0] for c in self if len(c) == 1 if abs(c[0]) > maxEdgeVar]

    return nPropagted

block_circulant(nBlocks=None, symmetryBreaking=1)

if symmetry breaking = 0 then no sym breaking if symmetry brekaing = 1 then blocks if symmetry breaking = m then also permute m blocks

TODO also swap blocks

Source code in pysms/graph_builder.py
def block_circulant(self, nBlocks=None, symmetryBreaking=1) -> None:
    """if symmetry breaking = 0 then no sym breaking
    if symmetry brekaing = 1 then blocks
    if symmetry breaking = m then also permute m blocks
    # TODO also swap blocks
    """
    import sys

    print("Value of symmetry breaking", symmetryBreaking, file=sys.stderr)
    self.paramsSMS["no-SMS"] = True
    # Number of circulant graphs if not given then smallest prime factor
    if nBlocks == None:
        nBlocks = min([i for i in range(2, self.n) if self.n % i == 0])

    # ensure that the diagonal blocks are circulant
    blocksize = self.n // nBlocks
    for i in range(nBlocks):
        for j in range(i, nBlocks):
            for d in range(0 if i != j else 1, blocksize):  # also diagonal most be considered if i != j
                for u in range(blocksize):
                    u1 = i * blocksize + 0
                    u2 = j * blocksize + d

                    v1 = i * blocksize + u
                    v2 = j * blocksize + (u + d) % blocksize

                    self.append([-self.var_edge(u1, u2), +self.var_edge(v1, v2)])
                    self.append([+self.var_edge(u1, u2), -self.var_edge(v1, v2)])

    if symmetryBreaking:
        # simple one only permuting each block and swap blocks; use vertex ordering such that blocks are sorted
        vertexPairOrdering = []
        print("Number of blocks:", nBlocks)
        for i in range(nBlocks):
            for u, v in combinations(range(i * blocksize, (i + 1) * blocksize), 2):
                vertexPairOrdering.append((u, v))
        for u, v in combinations(self.V, 2):
            if (u, v) not in vertexPairOrdering:
                vertexPairOrdering.append((u, v))
        symmetryCount = 0

        def checkGivenPermutation(p):
            seq1 = []
            seq2 = []
            for u, v in vertexPairOrdering:
                up = p[u]
                vp = p[v]
                if {u, v} == {up, vp}:  # TODO eventuell consider duplicates
                    continue
                seq1.append(self.var_edge(u, v))
                seq2.append(self.var_edge(up, vp))
            self.lex_smaller(seq1, seq2)

        # swap blocks
        for i, j in combinations(range(nBlocks), 2):
            permutation = list(range(self.n))
            for k in range(blocksize):
                permutation[blocksize * i + k] = blocksize * j + k
                permutation[blocksize * j + k] = blocksize * i + k
            checkGivenPermutation(permutation)
            symmetryCount += 1

        print("Number of symmetries:", symmetryCount)

        # symmetry breaking on individual blocks
        for i in range(nBlocks):
            first_vertex = i * blocksize
            for step_size in range(2, blocksize // 2 + 1):
                if math.gcd(step_size, blocksize) != 1:
                    continue
                permutation = list(range(i * blocksize)) + [(step_size * i) % blocksize + first_vertex for i in range(blocksize)] + list(range((i + 1) * blocksize, self.n))
                for i in range(self.n):
                    assert i in permutation
                # print(reordering)
                checkGivenPermutation(permutation)
                symmetryCount += 1
        print("Number of symmetries:", symmetryCount)

    if symmetryBreaking and False:
        permutationsBlock = [[] for i in range(nBlocks)]  # for each block store the symmetries
        for i in range(nBlocks):
            first_vertex = i * blocksize
            for step_size in range(1, blocksize):
                if math.gcd(step_size, blocksize) != 1:
                    continue
                reordering = list(range(i * blocksize)) + [(step_size * i) % blocksize + first_vertex for i in range(blocksize)] + list(range((i + 1) * blocksize, self.n))
                permutationsBlock[i].append(reordering)
        import sys

        # print(permutationsBlock, file=sys.stderr)
        counter = 0
        print("Value of symmetry breaking", symmetryBreaking, file=sys.stderr)
        for T in combinations(range(nBlocks), symmetryBreaking):
            print(T, file=sys.stderr)
            for P in product(*[permutationsBlock[i] for i in T]):
                # print(P, file=sys.stderr)
                counter += 1
                seq1 = []
                seq2 = []
                for u, v in combinations(range(self.n), 2):
                    up = u
                    vp = v
                    for p in P:
                        up = p[up]
                        vp = p[vp]
                    if {u, v} == {up, vp}:
                        continue
                    seq1.append(self.var_edge(u, v))
                    seq2.append(self.var_edge(up, vp))
                self.lex_smaller(seq1, seq2)
        print("Number of symmetries", counter, file=sys.stderr)

ckFree(k)

Forbid k-cycles (C_k) as non-induced subgraphs

Parameters:
  • k

    length of the cycle

Source code in pysms/graph_builder.py
def ckFree(self, k) -> None:
    """Forbid k-cycles (C_k) as non-induced subgraphs

    :param k: length of the cycle

    """
    g = self
    for cycle in permutations(g.V, k):
        if cycle[0] != min(cycle):
            continue
        if cycle[1] > cycle[-1]:
            continue
        g.append([-g.var_edge(cycle[i], cycle[(i + 1) % k]) for i in range(k)])  # at least one edge absent from potential cycle

contains_cliques(s, k)

Ensures that the graph contains exactly s cliques of size k and each edge is part of such a clique

Source code in pysms/graph_builder.py
def contains_cliques(self, s, k):
    """Ensures that the graph contains exactly s cliques of size k and each edge is part of such a clique"""
    cliqueVertices = [[self.id() for _ in self.V] for _ in range(s)]  # cliqueVertices[i][v] is true iff v is in clique i
    for i in range(s):
        self.counterFunction(cliqueVertices[i], k, k, k, counterType=DEFAULT_COUNTER)  # exactly k selected
        for u, v in combinations(self.V, 2):
            self.append([-cliqueVertices[i][u], -cliqueVertices[i][v], +self.var_edge(u, v)])
    # each edge is part of a clique
    for u, v in combinations(self.V, 2):
        self.append([-self.var_edge(u, v)] + [self.CNF_AND([cliqueVertices[i][u], cliqueVertices[i][v]]) for i in range(s)])

    # cliques can not be identical
    for i, j in combinations(range(s), 2):
        self.append([self.CNF_AND([+cliqueVertices[i][v], -cliqueVertices[j][v]]) for v in self.V])  # one in i but not in j

    # each vertex is in at most one clique
    for v in self.V:
        self.append([cliqueVertices[i][v] for i in range(s)])

    # symmetry breaking over cliques first clique is lex-smallest
    for i in range(1, s):
        self.lex_smaller(cliqueVertices[i - 1], cliqueVertices[i])

counterFunction(literals, countUpto, atMost=None, atLeast=None, counterType='sequential')

A wrapper for pysms.counters.counterFunction: constraints are added to and the ids are provided by the builder object (self).

Parameters:
  • literals

    create a counter over these literals

  • countUpto

    extend the counter to this value

  • atMost

    enforce a cardinality upper bound. Default value = None

  • atLeast

    enforce a cardinality lower bound. Default value = None

  • counterType

    specify which counter encoding to use. Default value = "sequential"

Returns:
  • list[int]

    auxiliary counter variables

Source code in pysms/graph_builder.py
def counterFunction(self, literals, countUpto, atMost=None, atLeast=None, counterType="sequential") -> list[int]:
    """A wrapper for `pysms.counters.counterFunction`: constraints are added to and the ids are provided by the builder object (self).

    :param literals: create a counter over these literals
    :param countUpto: extend the counter to this value
    :param atMost: enforce a cardinality upper bound. Default value = None
    :param atLeast: enforce a cardinality lower bound. Default value = None
    :param counterType: specify which counter encoding to use. Default value = "sequential"
    :returns: auxiliary counter variables

    """
    return counterFunction(literals, countUpto, self, self, atMost=atMost, atLeast=atLeast, type=counterType)

degreeBounds(verts, lower, upper, within=False, encoding=DEFAULT_COUNTER)

Enforce that each of verts has degree between lower and upper (either of which can be None)

Parameters:
  • verts

    the set of vertices on which to operate

  • lower

    a lower bound for the minimum degree

  • upper

    an upper bound for the maximum degree

  • within

    whether to count the degree only within verts, or to the whole graph (Default value = False)

  • encoding

    the cardinality encoding to be used (Default value = DEFAULT_COUNTER)

Returns:
  • list[list[int]]

    A list of lists of auxiliary variables, which say that the i-th vertex has degree at least j, or None if the bounds are meaningless.

Source code in pysms/graph_builder.py
def degreeBounds(self, verts, lower, upper, within=False, encoding=DEFAULT_COUNTER) -> list[list[int]]:
    """Enforce that each of verts has degree between lower and upper (either of which can be None)

    :param verts: the set of vertices on which to operate
    :param lower: a lower bound for the minimum degree
    :param upper: an upper bound for the maximum degree
    :param within: whether to count the degree only within verts, or to the whole graph (Default value = False)
    :param encoding: the cardinality encoding to be used (Default value = DEFAULT_COUNTER)
    :returns: A list of lists of auxiliary variables, which say that the `i`-th vertex has degree at least `j`,
    or `None` if the bounds are meaningless.

    """
    if lower == None and upper == None:
        return None
    if lower and upper and lower > upper:
        self.append([])
        return None
    aux = []
    target_set = verts if within else self.V
    count_limit = upper if upper else lower
    for u in verts:
        aux.append(self.counterFunction([self.var_edge(u, v) for v in target_set if v != u], count_limit, upper, lower, counterType=encoding))
    return aux

diameter2critical()

Ensure that the graph has diameter two and removing any edge results in a graph with diameter > 2

Source code in pysms/graph_builder.py
def diameter2critical(self) -> None:
    """Ensure that the graph has diameter two and removing any edge results in a graph with diameter > 2"""
    g = self
    V = g.V
    var_edge = g.var_edge
    commonNeighbor = {(i, j, k): g.id() for i, j in combinations(V, 2) for k in set(V) - {i, j}}

    for i, j in combinations(V, 2):
        for k in set(V) - {i, j}:
            L = (i, j, k)
            g.CNF_AND_APPEND([+var_edge(i, k), +var_edge(j, k)], commonNeighbor[L])

    noCommonNeighbor = {(i, j): g.id() for i, j in combinations(V, 2)}
    for i, j in combinations(V, 2):
        for k in set(V) - {i, j}:
            # if the have a common neighbor, noCommonNeighbor is false
            g.append([-commonNeighbor[(i, j, k)], -noCommonNeighbor[(i, j)]])

    for i, j in combinations(V, 2):
        g.append([+var_edge(i, j)] + [+commonNeighbor[(i, j, k)] for k in set(V) - {i, j}])  # adjacent or common neighbor

    for i, j in combinations(V, 2):
        # ensure that critical i.e. if edge ij is present removing will lead to diamter > 2
        clause = [-var_edge(i, j), +noCommonNeighbor[(i, j)]]
        for k in set(V) - {i, j}:
            for v1, v2 in [(i, j), (j, i)]:
                # v2 and k have diameter > after removing ij
                # v1 adjacent to k and v1 is the only common neighbor from v2 and k. And k not adjacent to v2
                diameterIncreasing = g.id()
                g.append([+var_edge(v1, k), -diameterIncreasing])
                g.append([-var_edge(v2, k), -diameterIncreasing])
                for l in set(V) - {i, j, k}:
                    g.append([-commonNeighbor[(min(v2, k), max(v2, k), l)], -diameterIncreasing])
                clause.append(diameterIncreasing)
        g.append(clause)

edgeBounds(verts, lower, upper, encoding=DEFAULT_COUNTER)

Enforce that each of verts has degree between lower and upper (either of which can be None)

Parameters:
  • verts

    the set of vertices between which to enforce edge bounds

  • lower

    lower bound on the number of edges

  • upper

    upper bound on the number of edges

  • encoding

    the cardinality encoding to be used (default DEFAULT_COUNTER)

Returns:
  • list[int]

    A list of auxiliary variables, which say that the number of edges is at least i, or None if the bounds are meaningless.

Source code in pysms/graph_builder.py
def edgeBounds(self, verts, lower, upper, encoding=DEFAULT_COUNTER) -> list[int]:
    """Enforce that each of verts has degree between lower and upper (either of which can be None)

    :param verts: the set of vertices between which to enforce edge bounds
    :param lower: lower bound on the number of edges
    :param upper: upper bound on the number of edges
    :param encoding: the cardinality encoding to be used (default DEFAULT_COUNTER)
    :returns: A list of auxiliary variables, which say that the number of edges is at least i,
    or None if the bounds are meaningless.

    """
    if lower == None and upper == None:
        return None
    if lower and upper and lower > upper and verts:
        self.append([])
        return None
    count_limit = upper if upper else lower
    return self.counterFunction([self.var_edge(u, v) for u, v in combinations(verts, 2)], count_limit, upper, lower, counterType=encoding)

lex_smaller(seq1, seq2)

Ensure that seq1 is lexicographically smaller or equal than seq2

Source code in pysms/graph_builder.py
def lex_smaller(self, seq1, seq2):
    """Ensure that seq1 is lexicographically smaller or equal than seq2"""
    assert len(seq1) == len(seq2)
    all_previous_equal = self.id()
    self.append([+all_previous_equal])
    for i in range(len(seq1)):
        self.append([-all_previous_equal, -seq1[i], +seq2[i]])  # all previous equal implies seq1[i] <= seq2[i]
        all_previous_equal_new = self.id()
        self.append([-all_previous_equal, -seq1[i], +all_previous_equal_new])
        self.append([-all_previous_equal, +seq2[i], +all_previous_equal_new])
        all_previous_equal = all_previous_equal_new

maxChromaticNumber(chi)

The graph should be chi-colorable

Parameters:
  • chi

    nubmer of colors

Source code in pysms/graph_builder.py
def maxChromaticNumber(self, chi) -> None:
    """The graph should be chi-colorable

    :param chi: nubmer of colors

    """
    g = self
    color = [[g.id() for _ in range(chi)] for _ in g.V]
    for v in g.V:
        g.append([color[v][i] for i in range(chi)])  # each vertex should have a color

    for v, w in combinations(g.V, 2):
        for i in range(chi):
            g.append([-g.var_edge(v, w), -color[v][i], -color[w][i]])  # adjacent vertices are not allowed to have the same color

maxClique(x)

No cliques of size greater than x

Parameters:
  • x

    upper bound on the size of cliques

Source code in pysms/graph_builder.py
def maxClique(self, x) -> None:
    """No cliques of size greater than x

    :param x: upper bound on the size of cliques

    """
    g = self
    for S in combinations(g.V, x + 1):
        g.append([-g.var_edge(i, j) for i, j in combinations(S, 2)])

maxDegree(delta, countertype=DEFAULT_COUNTER)

Maximum degree at most delta

Parameters:
  • delta

    degree upper bound

  • countertype

    specify a cardinality encoding. Default value = DEFAULT_COUNTER

Source code in pysms/graph_builder.py
def maxDegree(self, delta, countertype=DEFAULT_COUNTER) -> list[list[int]]:
    """Maximum degree at most delta

    :param delta: degree upper bound
    :param countertype: specify a cardinality encoding. Default value = DEFAULT_COUNTER

    """
    return self.degreeBounds(self.V, None, delta, encoding=countertype)

maxIndependentSet(x)

No independent sets of size greater than x

Parameters:
  • x

    upper bound on the size of independent sets

Source code in pysms/graph_builder.py
def maxIndependentSet(self, x) -> None:
    """No independent sets of size greater than x

    :param x: upper bound on the size of independent sets

    """
    g = self
    for S in combinations(g.V, x + 1):
        g.append([+g.var_edge(i, j) for i, j in combinations(S, 2)])

minConnectivity(connectivity_low)

Make graphs with minimum required connectivity

Parameters:
  • connectivity_low

    a lower bound on connectivity

Source code in pysms/graph_builder.py
def minConnectivity(self, connectivity_low) -> None:
    """Make graphs with minimum required connectivity

    :param connectivity_low: a lower bound on connectivity

    """
    assert self.n > connectivity_low  # an k-connected graph has at least k+1 vertices
    V = self.V
    var_edge = self.var_edge
    reachable = {
        (v, t, I): self.id() for k in range(args.connectivity_low) for I in combinations(sorted(set(V)), k) for v in set(V) - {min(set(V) - set(I))} - set(I) for t in V
    }  # u can reach v without I in t steps
    reachable_via = {
        (v, w, t, I): self.id()
        for k in range(args.connectivity_low)
        for I in combinations(sorted(set(V)), k)
        for v in set(V) - {min(set(V) - set(I))} - set(I)
        for t in V
        for w in set(V) - {min(set(V) - set(I)), v} - set(I)
    }  # u can reach v via w without I in t steps

    def var_reachable(v, t, I):
        return reachable[(v, t, I)]

    def var_reachable_via(v, w, t, I):
        return reachable_via[(v, w, t, I)]

    for k in range(args.connectivity_low):
        for I in combinations(sorted(set(V)), k):  # remove I and check if still connected
            u = min(set(V) - set(I))
            for v in set(V) - {u} - set(I):
                for t in V:
                    if t == 0:
                        # reachable in first step if adjacent
                        self.append([-var_edge(v, u), +var_reachable(v, 0, I)])
                        self.append([+var_edge(v, u), -var_reachable(v, 0, I)])

                    else:
                        self.append([-var_reachable(v, t, I), +var_reachable(v, t - 1, I)] + [+var_reachable_via(v, w, t, I) for w in set(V) - set(I) - {v, u}])
                        self.append([+var_reachable(v, t, I), -var_reachable(v, t - 1, I)])
                        for w in set(V) - set(I) - {v, u}:
                            self.append([+var_reachable(v, t, I), -var_reachable_via(v, w, t, I)])

                            self.append([+var_reachable_via(v, w, t, I), -var_reachable(w, t - 1, I), -var_edge(w, v)])
                            self.append([-var_reachable_via(v, w, t, I), +var_reachable(w, t - 1, I)])
                            self.append([-var_reachable_via(v, w, t, I), +var_edge(w, v)])
                # must be reached
                self.append([+var_reachable(v, max(V), I)])

minDegree(delta, countertype=DEFAULT_COUNTER)

Minimum degree at least delta

Parameters:
  • delta

    degree lower bound

  • countertype

    specify a cardinality encoding. Default value = DEFAULT_COUNTER

Source code in pysms/graph_builder.py
def minDegree(self, delta, countertype=DEFAULT_COUNTER) -> list[list[int]]:
    """Minimum degree at least delta

    :param delta: degree lower bound
    :param countertype: specify a cardinality encoding. Default value = DEFAULT_COUNTER

    """
    return self.degreeBounds(self.V, delta, None, encoding=countertype)

minGirth(k)

Basic encoding to ensure that the girth is at least k, i.e., no cycle with length < k

Parameters:
  • k

    girth lower bound

Source code in pysms/graph_builder.py
def minGirth(self, k) -> None:
    """Basic encoding to ensure that the girth is at least k, i.e., no cycle with length < k

    :param k: girth lower bound

    """
    for l in range(3, k):  # forbid 3 cycles up to k-1 cycles
        self.ckFree(l)

minGirthCompact(minGirth)

Compact encoding to ensure that the girth is at least k, i.e., no cycle with length < k

Parameters:
  • minGirth

    girth lower bound

Source code in pysms/graph_builder.py
def minGirthCompact(self, minGirth) -> None:
    """
    Compact encoding to ensure that the girth is at least k, i.e., no cycle with length < k

    :param minGirth: girth lower bound
    """
    g = self
    # check distance of i,j without edge i,j.
    for i, j in combinations(g.V, 2):
        reached = [g.var_edge(i, k) if k not in [i, j] and k > i else None for k in g.V]

        for _ in range(minGirth - 4):  # if girth is 4 than no triangles so not in the loop
            reachedNew = [g.id() for _ in g.V]
            for k in g.V:
                if k in [i, j] or k < i:
                    continue
                g.append([-reached[k], +reachedNew[k]])  # already reached

                # check if reached over l
                for l in g.V:
                    if l in [i, j, k] or l < i:
                        continue
                    g.append([-g.var_edge(k, l), -reached[l], +reachedNew[k]])  # l reached in previous step and edge implies reached
            reached = reachedNew

        for k in g.V:
            if k in [i, j] or k < i:
                continue
            # not reached, not adjacent to j, or edge not present
            g.append([-g.var_edge(i, j), -g.var_edge(j, k), -reached[k]])

mtf()

Make the graph maximal triangle-free (adding any further edges will create triangles)

Source code in pysms/graph_builder.py
def mtf(self) -> None:
    """Make the graph maximal triangle-free (adding any further edges will create triangles)"""
    self.ckFree(3)  # forbid triangles
    commonNeighbor = {(i, j, k): self.id() for i, j in combinations(self.V, 2) for k in self.V if k not in [i, j]}
    for i, j in combinations(self.V, 2):
        for k in self.V:
            if k in [i, j]:
                continue
            self.CNF_AND_APPEND([self.var_edge(i, k), self.var_edge(j, k)], commonNeighbor[(i, j, k)])
        self.append([self.var_edge(i, j)] + [commonNeighbor[(i, j, k)] for k in self.V if k not in [i, j]])

noSubsumingNeighborhoods()

No neighborhood of a vertex is contained in the neighborhood of another vertex

Source code in pysms/graph_builder.py
def noSubsumingNeighborhoods(self) -> None:
    """No neighborhood of a vertex is contained in the neighborhood of another vertex"""
    # different neighborhood
    for i, j in permutations(self.V, 2):
        # There must be a vertex adjecent to i which is not adjacent to j
        adjacentOnlyToI = []
        for k in self.V:
            if k == i or k == j:
                continue
            kIsAdjacentOnlyToI = self.id()
            self.append([+self.var_edge(i, k), -kIsAdjacentOnlyToI])
            self.append([-self.var_edge(j, k), -kIsAdjacentOnlyToI])
            adjacentOnlyToI.append(kIsAdjacentOnlyToI)
        self.append([+self.var_edge(i, j)] + adjacentOnlyToI)

numEdgesLow(m, countertype=DEFAULT_COUNTER)

A lower bound on the number of edges

Parameters:
  • m

    a lower bound on the number of edges

  • countertype

    the cardinality encoding to be used (Default value = DEFAULT_COUNTER)

Source code in pysms/graph_builder.py
def numEdgesLow(self, m, countertype=DEFAULT_COUNTER) -> list[int]:
    """A lower bound on the number of edges

    :param m: a lower bound on the number of edges
    :param countertype: the cardinality encoding to be used (Default value = DEFAULT_COUNTER)

    """
    return self.edgeBounds(self.V, m, None, encoding=countertype)

numEdgesUpp(m, countertype=DEFAULT_COUNTER)

An upper bound on the number of edges

Parameters:
  • m

    an upper bound on the number of edges

  • countertype

    the cardinality encoding to be used (Default value = DEFAULT_COUNTER)

Source code in pysms/graph_builder.py
def numEdgesUpp(self, m, countertype=DEFAULT_COUNTER) -> list[int]:
    """An upper bound on the number of edges

    :param m: an upper bound on the number of edges
    :param countertype: the cardinality encoding to be used (Default value = DEFAULT_COUNTER)

    """
    return self.edgeBounds(self.V, None, m, encoding=countertype)

print_dimacs(outstream)

Print the current encoding to the given output stream

Parameters:
  • outstream

    print to here

Source code in pysms/graph_builder.py
def print_dimacs(self, outstream) -> None:
    """Print the current encoding to the given output stream

    :param outstream: print to here

    """
    print(f"p cnf {self.nextId-1} {len(self)}", file=outstream)
    for c in self:
        print(" ".join(str(x) for x in c), 0, file=outstream)

slimSingleStep(nOriginal, nReplaced, G, induced=False)

Apply single step of SLIM given the graph G as edgelist, where some random vertices are replaced

Source code in pysms/graph_builder.py
def slimSingleStep(self, nOriginal, nReplaced, G, induced=False) -> None:
    """Apply single step of SLIM given the graph G as edgelist, where some random vertices are replaced"""
    import random

    VR = random.sample(range(nOriginal), k=nReplaced)

    nFixed = nOriginal - nReplaced
    self.paramsSMS["initial-partition"] = nFixed * "1 " + str(self.n - nFixed)

    remappingVertices = dict()
    cur = 0
    for v in set(range(nOriginal)) - set(VR):
        remappingVertices[v] = cur
        cur += 1
    r = remappingVertices

    remappedEdges = [(min(r[u], r[v]), max(r[u], r[v])) for u, v in G if u not in VR and v not in VR]
    # fix this subgraph in the encoding
    for u, v in remappedEdges:
        self.append([+self.var_edge(u, v)])
    if induced:
        for u, v in combinations(range(nFixed), 2):
            if (u, v) not in G and (v, u) not in G:
                self.append([-self.var_edge(u, v)])

solve(allGraphs=False, hideGraphs=False, cnfFile=None, args_SMS='', forwarding_args=[], graph6_format=False)

Solve the formula, given the encoding, using SMS.

Parameters:
  • allGraphs

    Enumerate all satisfying graphs. Default value = False

  • hideGraphs

    Count all satisfying graphs. Default value = False

  • cnfFile

    Write constraints here, use a temporary if None. Default value = None

  • forwarding_args

    Forward these arguments to SMS. Default value = []

  • graph_format

    Toggle output format. Choices are [graph6, edge_list]. Default value = edge-list

  • args_SMS

    Forward these arguments to SMS. Deprecated, use forwarding_args instead. Default value = ""

Source code in pysms/graph_builder.py
def solve(self, allGraphs=False, hideGraphs=False, cnfFile=None, args_SMS="", forwarding_args=[], graph6_format=False) -> None:
    """Solve the formula, given the encoding, using SMS.

    :param allGraphs: Enumerate all satisfying graphs. Default value = False
    :param hideGraphs: Count all satisfying graphs. Default value = False
    :param cnfFile: Write constraints here, use a temporary if None. Default value = None
    :param forwarding_args: Forward these arguments to SMS. Default value = []
    :param graph_format: Toggle output format. Choices are [graph6, edge_list]. Default value = edge-list
    :param args_SMS: Forward these arguments to SMS. Deprecated, use `forwarding_args` instead. Default value = ""

    """
    if cnfFile == None:
        import time

        cnfFile = f"./temp{os.getpid()}_t{time.time()}.enc"  # TODO use tempfile module
    with open(cnfFile, "w") as cnf_fh:
        self.print_dimacs(cnf_fh)  # write script to temporary file

    program = "smsd" if self.directed else "smsg"  # we expect these binaries to be on PATH

    # add arguments

    if allGraphs:
        self.paramsSMS["all-graphs"] = ""
    if hideGraphs:
        self.paramsSMS["hide-graphs"] = ""
    if self.varStaticInitialPartition:
        self.paramsSMS["combine-static-dynamic"] = ""

    python_args_SMS = " ".join(f"--{param} {value}" for param, value in self.paramsSMS.items())

    sms_command = "time " if self.DEBUG else ""
    sms_command += f"{program} {python_args_SMS} {args_SMS} --dimacs {cnfFile}"  # TODO eventually parse args_SMS to allow to override
    for arg in forwarding_args:
        sms_command += f" '{arg}'"

    if self.DEBUG:
        print("running the command: ", sms_command)

    if graph6_format:
        import networkx as nx

        for line in os.popen(sms_command).read().split("\n"):
            if line and line[0] == "[":
                edges = literal_eval(line)
                print(nx.to_graph6_bytes(nx.Graph(edges), header=False).decode(), end="")
            elif self.DEBUG:
                print(line, end="\n")
    else:
        os.system(sms_command)

    os.system(f"rm {cnfFile}")  # cleanup

solveArgs(args, forwarding_args)

Wrapper for solving using arguments provided by argsParser

Parameters:
  • args

    Arguments for PySMS

  • forwarding_args

    Arguments to be forwarded to SMS

Source code in pysms/graph_builder.py
def solveArgs(self, args, forwarding_args) -> None:
    """Wrapper for solving using arguments provided by argsParser

    :param args: Arguments for PySMS
    :param forwarding_args: Arguments to be forwarded to SMS

    """
    self.solve(allGraphs=args.all_graphs, hideGraphs=args.hide_graphs, cnfFile=args.cnf_file, args_SMS=args.args_SMS, forwarding_args=forwarding_args, graph6_format=args.graph6_format)

sort_vertices_by_degree()

Create a static encoding of a partition, such that all vertices are sorted by degree. Currently only works for undirected graphs

Source code in pysms/graph_builder.py
def sort_vertices_by_degree(self) -> None:
    """Create a static encoding of a partition, such that all vertices are sorted by degree.
    Currently only works for undirected graphs


    """
    g = self
    var_deg = []
    for v in g.V:
        counter_vars = g.counterFunction([g.var_edge(v, u) for u in g.V if u != v], g.n)
        var_deg.append(counter_vars)

    # compare consequent vertices
    for v1 in range(g.n - 1):
        v2 = v1 + 1

        # v2 is not allowed to have a lower degree
        for d in range(g.n - 1):
            g.append([-var_deg[v1][d], +var_deg[v2][d]])

        same_degree_options = []
        for d in range(g.n - 1):
            s = g.CNF_AND([+var_deg[v1][d], +var_deg[v2][d], -var_deg[v1][d + 1], -var_deg[v2][d + 1]])  # if s true then both have degree d - 1
            same_degree_options.append(s)
        g.CNF_OR_APPEND(same_degree_options, g.var_partition(v1, v2))

var_edge(u, v)

Get the propositional variable associated with the undirected edge {u,v}

Parameters:
  • u

    a vertex

  • v

    a different vertex

Returns:
  • int

    the undirected edge variable e_{u,v}

Source code in pysms/graph_builder.py
def var_edge(self, u, v) -> int:
    """Get the propositional variable associated with the undirected edge {u,v}

    :param u: a vertex
    :param v: a different vertex
    :returns: the undirected edge variable e_{u,v}

    """
    return self.varEdgeTable[u][v]

var_edge_dir(u, v)

Get the propositional variable associated with the directed edge (u,v)

Parameters:
  • u

    a vertex

  • v

    a different vertex

Returns:
  • int

    the directed edge variable e_{u,v}

Source code in pysms/graph_builder.py
def var_edge_dir(self, u, v) -> int:
    """Get the propositional variable associated with the directed edge (u,v)

    :param u: a vertex
    :param v: a different vertex
    :returns: the directed edge variable e_{u,v}

    """
    return self.varEdgeDirectedTable[u][v]

var_edge_multi(i, u, v)

Get the propositional variable associated with the undirected edge {u,v} of the i-th graph

Parameters:
  • i

    the graph index

  • v

    a vertex

  • u

    a different vertex

Returns:
  • int

    the undirected multigraph edge variable e^i_{u,v}

Source code in pysms/graph_builder.py
def var_edge_multi(self, i, u, v) -> int:
    """Get the propositional variable associated with the undirected edge {u,v} of the i-th graph

    :param i: the graph index
    :param v: a vertex
    :param u: a different vertex
    :returns: the undirected multigraph edge variable e^i_{u,v}

    """
    return self.varEdgeMultiTable[i][u][v]

var_partition(u, v)

Get the propositional variable which holds whether u and v are in the same partition

Parameters:
  • u

    a vertex

  • v

    a different vertex

Returns:
  • int

    the partition description variable

Source code in pysms/graph_builder.py
def var_partition(self, u, v) -> int:
    """Get the propositional variable which holds whether u and v are in the same partition

    :param u: a vertex
    :param v: a different vertex
    :returns: the partition description variable

    """
    return self.varStaticInitialPartition[u][v]

IDPool

A class for returning the next free id starting with 1

Source code in pysms/graph_builder.py
class IDPool:
    """A class for returning the next free id starting with 1"""

    def __init__(self, start_from=1) -> None:
        self.nextId = start_from

    def id(self):
        """Returns the next free variable"""
        x = self.nextId
        self.nextId += 1
        return x

id()

Returns the next free variable

Source code in pysms/graph_builder.py
def id(self):
    """Returns the next free variable"""
    x = self.nextId
    self.nextId += 1
    return x

CNF_AND(ins, out)

Returns a list of clauses ensuring that out is equivalent to the conjunction of the literals in ins

Parameters:
  • ins

    input variable ids

  • out

    a (fresh) variable id

Source code in pysms/graph_builder.py
def CNF_AND(ins, out) -> list[int]:
    """Returns a list of clauses ensuring that `out` is equivalent to the conjunction of the literals in `ins`

    :param ins: input variable ids
    :param out: a (fresh) variable id

    """
    return [[out] + [-x for x in ins]] + [[-out, x] for x in ins]

CNF_OR(ins, out)

Returns a list of clauses ensuring that out is equivalent to the disjunction of the literals in ins

Parameters:
  • ins

    input variable ids

  • out

    a (fresh) variable id

Source code in pysms/graph_builder.py
def CNF_OR(ins, out) -> list[int]:
    """Returns a list of clauses ensuring that `out` is equivalent to the disjunction of the literals in `ins`

    :param ins: input variable ids
    :param out: a (fresh) variable id

    """
    return [[-out] + ins] + [[out, -x] for x in ins]