Raw File
pwcnf.py
#!/usr/bin/python
#Title			: pwcnf.py
#Usage			: python pwcnf.py -h
#Author			: pmorvalho
#Date			: October 03, 2022
#Description		: Class for manipulating partition-based partial weighted CNF formulas (PWCNFs). 
#Notes			: Inspired in the WCNF class available on PySAT 0.1.7.DEV19
#Python Version: 3.8.5
# (C) Copyright 2022 Pedro Orvalho.
#==============================================================================

from pysat.formula import *

class PWCNF(object):
    """
        Class for manipulating partition-based partial weighted CNF formulas (PWCNF). 
        It can be used for creating formulas, reading them from a file, or writing them to a
        file. The ``comment_lead`` parameter can be helpful when one needs to
        parse specific comment lines starting not with character ``c`` but with
        another character or a string.
        :param from_file: a PWCNF filename to read from
        :param from_fp: a file pointer to read from
        :param from_string: a string storing a PPWCNF formula
        :param comment_lead: a list of characters leading comment lines
        :type from_file: str
        :type from_fp: file_pointer
        :type from_string: str
        :type comment_lead: list(str)
    """
    def __init__(self, from_file=None, from_fp=None, from_string=None,
                 comment_lead=['c'], wcnf=False):
        """
            Constructor.
        """

        self.wcnf = None
        if not wcnf:
            self.nv = 0 
            self.hard = []
            self.soft = []
            self.wght = []
            self.topw = 1
            self.comments = []
            self.parts = []
            self.parts_wghts = []

            if from_file:
                self.from_file(from_file, comment_lead, compressed_with='use_ext')
            elif from_fp:
                self.from_fp(from_fp, comment_lead)
            elif from_string:
                self.from_string(from_string, comment_lead)

            sorted(self.parts, key=len)
            sorted(self.parts_wghts, key=len)
            
        else: # when the user provides a WCNF 
            if from_file:
                self.wcnf = WCNF(from_file=from_file, comment_lead=comment_lead)
            elif from_fp:
                self.wcnf = WCNF(from_fp=from_fp, comment_lead=comment_lead)
            elif from_string:
                self.wcnf = WCNF(from_string=from_string, comment_lead=comment_lead)
            
            self.nv = self.wcnf.nv
            self.hard = self.wcnf.hard
            self.soft = self.wcnf.soft
            self.wght = self.wcnf.wght
            self.topw = self.wcnf.topw
            self.comments = self.wcnf.comments
            self.parts = []
            self.parts_wghts = []


    def get_num_variables(self):
        """
           Number of variables
        """
        return self.nv

    def get_hard(self):
        """
           Hard Clauses
        """
        return self.hard

    def get_soft(self):
        """
           Soft Clauses
        """
        return self.soft

    def get_soft_clause(self, c):
        """
           Soft clause c
        """
        return self.soft[c]

    def get_soft_weight(self, s):
        """
           Soft clause c's weight
        """
        return self.wght[s]

    def get_partitions(self):
        """
           Partitions
        """
        return self.parts

    def get_partition(self, p):
        """
           Partition P
        """
        return self.parts[p]

    def get_partition_weights(self, p):
        """
           Partition P's Weights
        """
        return self.parts_wghts[p]

    
    def from_file(self, fname, comment_lead=['c'], compressed_with='use_ext'):
        """
            Read a PWCNF formula from a file in the DIMACS format. A file name
            is expected as an argument. A default argument is ``comment_lead``
            for parsing comment lines. A given file can be compressed by either
            gzip, bzip2, or lzma.
            :param fname: name of a file to parse.
            :param comment_lead: a list of characters leading comment lines
            :param compressed_with: file compression algorithm
            :type fname: str
            :type comment_lead: list(str)
            :type compressed_with: str
            Note that the ``compressed_with`` parameter can be ``None`` (i.e.
            the file is uncompressed), ``'gzip'``, ``'bzip2'``, ``'lzma'``, or
            ``'use_ext'``. The latter value indicates that compression type
            should be automatically determined based on the file extension.
            Using ``'lzma'`` in Python 2 requires the ``backports.lzma``
            package to be additionally installed.
            Usage example:
            .. code-block:: python
                >>> from pysat.formula import PWCNF
                >>> cnf1 = PWCNF()
                >>> cnf1.from_file('some-file.wcnf.bz2', compressed_with='bzip2')
                >>>
                >>> cnf2 = PWCNF(from_file='another-file.wcnf')
        """

        with FileObject(fname, mode='r', compression=compressed_with) as fobj:
            self.from_fp(fobj.fp, comment_lead)

    def from_fp(self, file_pointer, comment_lead=['c']):
        """
            Read a PWCNF formula from a file pointer. A file pointer should be
            specified as an argument. The only default argument is
            ``comment_lead``, which can be used for parsing specific comment
            lines.
            :param file_pointer: a file pointer to read the formula from.
            :param comment_lead: a list of characters leading comment lines
            :type file_pointer: file pointer
            :type comment_lead: list(str)
            Usage example:
            .. code-block:: python
                >>> with open('some-file.cnf', 'r') as fp:
                ...     cnf1 = PWCNF()
                ...     cnf1.from_fp(fp)
                >>>
                >>> with open('another-file.cnf', 'r') as fp:
                ...     cnf2 = PWCNF(from_fp=fp)
        """

        def parse_wght(string):
            wght = float(string)
            return int(wght) if wght.is_integer() else decimal.Decimal(string)

        def parse_partition(string):
            p = float(string)
            return int(p) 

        
        self.nv = 0
        self.hard = []
        self.soft = []
        self.wght = []
        self.n_parts = 0
        self.parts = []
        self.parts_wghts = []
        self.topw = 1
        self.comments = []
        comment_lead = tuple('p') + tuple(comment_lead)

        # soft clauses with negative weights
        negs = []

        for line in file_pointer:
            line = line.strip()
            if line:
                if line[0] not in comment_lead:
                    items = line.split()[:-1]
                    p, w, cl = parse_partition(items[0]), parse_wght(items[1]), [int(l) for l in items[2:]]
                    self.nv = max([abs(l) for l in cl] + [self.nv])

                    if w <= 0:
                        # this clause has a negative weight
                        # it will be processed later
                        negs.append(tuple([cl, -w, p]))
                    elif w >= self.topw:
                        self.hard.append(cl)
                    else:
                        self.parts[p].append(cl)
                        self.parts_wghts[p].append(w)
                        self.soft.append(cl)
                        self.wght.append(w)
                        
                elif not line.startswith('p pwcnf '):
                    self.comments.append(line)
                else: # expecting the preamble
                    preamble = line.split(' ')
                    if len(preamble) == 6: # preamble should be "p pwcnf nvars nclauses topw n_part"
                        self.topw = parse_wght(preamble[-2])
                        self.n_part = parse_wght(preamble[-1])
                    else: # preamble should be "p pwcnf nvars nclauses n_parts", with topw omitted
                        self.topw = decimal.Decimal('+inf')
                        self.n_part = parse_wght(preamble[-1])
                    for p in range(self.n_part+1):
                        self.parts.append([])
                        self.parts_wghts.append([])
        # if there is any soft clause with negative weight
        # normalize it, i.e. transform into a set of clauses
        # with a positive weight
        if negs:
            self.normalize_negatives(negs)

        # if topw was unspecified and assigned to +infinity,
        # we will assign it to the sum of all soft clause weights plus one
        if type(self.topw) == decimal.Decimal and self.topw.is_infinite():
            self.topw = 1 + sum(self.wght)

    def normalize_negatives(self, negatives):
        """
            Iterate over all soft clauses with negative weights and add their
            negation either as a hard clause or a soft one.
            :param negatives: soft clauses with their negative weights.
            :type negatives: list(list(int))
        """

        for cl, w, p in negatives:
            selv = cl[0]

            # tseitin-encoding the clause if it is not unit-size
            if len(cl) > 1:
                self.nv += 1
                selv = self.nv

                for l in cl:
                    self.hard.append([selv, -l])
                self.hard.append([-selv] + cl)

            # adding the negation of the clause either as hard or soft
            if w >= self.topw:
                self.hard.append([-selv])
            else:
                self.soft.append([-selv])
                self.parts.append([-selv])
                self.parts_wghts[p].append(w)
                self.wght.append(w)

    def from_string(self, string, comment_lead=['c']):
        """
            Read a PWCNF formula from a string. The string should be specified
            as an argument and should be in the DIMACS CNF format. The only
            default argument is ``comment_lead``, which can be used for parsing
            specific comment lines.
            :param string: a string containing the formula in DIMACS.
            :param comment_lead: a list of characters leading comment lines
            :type string: str
            :type comment_lead: list(str)
            Example:
            .. code-block:: python
                >>> from pysat.formula import PWCNF
                >>> cnf1 = PWCNF()
                >>> cnf1.from_string(='p pwcnf 2 2 2\\n 2 -1 2 0\\n1 1 -2 0')
                >>> print(cnf1.hard)
                [[-1, 2]]
                >>> print(cnf1.soft)
                [[1, 2]]
                >>>
                >>> cnf2 = PWCNF(from_string='p pwcnf 3 3 2\\n2 -1 2 0\\n2 -2 3 0\\n1 -3 0\\n')
                >>> print(cnf2.hard)
                [[-1, 2], [-2, 3]]
                >>> print(cnf2.soft)
                [[-3]]
                >>> print(cnf2.nv)
                3
        """

        self.from_fp(StringIO(string), comment_lead)
        
    
back to top