diff --git a/.gitignore b/.gitignore index c02d15a4..0e1cd0b0 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,6 @@ __pycache__/ *.py[cod] *$py.class -# C extensions -*.so # Distribution / packaging .Python diff --git a/python2/pracmln/mln/inference/.gitignore b/python2/pracmln/mln/inference/.gitignore index 24fae75e..e22a709f 100644 --- a/python2/pracmln/mln/inference/.gitignore +++ b/python2/pracmln/mln/inference/.gitignore @@ -1 +1,6 @@ -*.pyo \ No newline at end of file +*.pyo +*.c +*.html +setup.py +build/* +pracmln/* diff --git a/python2/pracmln/mln/inference/exact.py b/python2/pracmln/mln/inference/exact.pyx similarity index 90% rename from python2/pracmln/mln/inference/exact.py rename to python2/pracmln/mln/inference/exact.pyx index 6568d1a8..7a6f5361 100644 --- a/python2/pracmln/mln/inference/exact.py +++ b/python2/pracmln/mln/inference/exact.pyx @@ -36,6 +36,7 @@ from numpy.ma.core import exp from pracmln.mln.inference.infer import Inference from pracmln.logic.common import Logic +from numpy import zeros logger = logs.getlogger(__name__) @@ -46,12 +47,12 @@ def eval_queries(world): - """ + ''' Evaluates the queries given a possible world. - """ - numerators = [0] * len(global_enumAsk.queries) + ''' + numerators = zeros(len(global_enumAsk.queries)) denominator = 0 - expsum = 0 + cdef expsum = 0 for gf in global_enumAsk.grounder.itergroundings(): if global_enumAsk.soft_evidence_formula(gf): expsum += gf.noisyor(world) * gf.weight @@ -67,6 +68,7 @@ def eval_queries(world): expsum += gf(world) * gf.weight expsum = exp(expsum) # update numerators + cdef int i for i, query in enumerate(global_enumAsk.queries): if query(world): numerators[i] += expsum @@ -115,13 +117,13 @@ def _run(self): for variable in self.mrf.variables: values = variable.valuecount(self.mrf.evidence) worlds *= values - numerators = [0.0 for i in range(len(self.queries))] - denominator = 0. + numerators = zeros(len(self.queries))#[0.0 for i in range(len(self.queries))] + cdef double denominator = 0. # start summing logger.debug("Summing over %d possible worlds..." % worlds) if worlds > 500000 and self.verbose: print colorize('!!! %d WORLDS WILL BE ENUMERATED !!!' % worlds, (None, 'red', True), True) - k = 0 + cdef int k = 0 self._watch.tag('enumerating worlds', verbose=self.verbose) global global_enumAsk global_enumAsk = self @@ -135,9 +137,11 @@ def _run(self): for num, denum in pool.imap(with_tracing(eval_queries), self.mrf.worlds()): denominator += denum k += 1 - for i, v in enumerate(num): - numerators[i] += v - if self.verbose: bar.inc() + numerators += num + #for i, v in enumerate(num): + # numerators[i] += v + if self.verbose: + bar.inc() except Exception as e: logger.error('Error in child process. Terminating pool...') pool.close() @@ -150,8 +154,9 @@ def _run(self): # compute exp. sum of weights for this world num, denom = eval_queries(world) denominator += denom - for i, _ in enumerate(self.queries): - numerators[i] += num[i] + numerators += num + #for i, _ in enumerate(self.queries): + # numerators[i] += num[i] k += 1 if self.verbose: bar.update(float(k) / worlds) @@ -163,7 +168,8 @@ def _run(self): raise SatisfiabilityException( 'MLN is unsatisfiable. All probability masses returned 0.') # normalize answers - dist = map(lambda x: float(x) / denominator, numerators) + dist = numerators / denominator + #dist = map(lambda x: float(x) / denominator, numerators) result = {} for q, p in zip(self.queries, dist): result[str(q)] = p diff --git a/python3/build-cython.sh b/python3/build-cython.sh new file mode 100644 index 00000000..ad3ea36b --- /dev/null +++ b/python3/build-cython.sh @@ -0,0 +1,21 @@ + +cd pracmln/logic/ +echo "======================LOGIC======================" +python3 setup.py build_ext --inplace +echo "=================================================" + +echo "=======================MLN=======================" +cd ../mln/ +python3 setup.py build_ext --inplace +echo "=================================================" + +echo "====================GROUNDING====================" +cd grounding/ +python3 setup.py build_ext --inplace +echo "=================================================" + +echo "====================INFERENCE====================" +cd ../inference/ +python3 setup.py build_ext --inplace +echo "=================================================" + diff --git a/python3/pracmln/__init__.py b/python3/pracmln/__init__.py index 508f8057..f1a7fc56 100644 --- a/python3/pracmln/__init__.py +++ b/python3/pracmln/__init__.py @@ -29,4 +29,4 @@ from .mlnlearn import QUERY_PREDS from .mlnlearn import EVIDENCE_PREDS from .utils.project import mlnpath -from .utils.project import PRACMLNConfig \ No newline at end of file +from .utils.project import PRACMLNConfig diff --git a/python3/pracmln/logic/.gitignore b/python3/pracmln/logic/.gitignore new file mode 100644 index 00000000..1ddd5cc6 --- /dev/null +++ b/python3/pracmln/logic/.gitignore @@ -0,0 +1,6 @@ +common.py +*.pyo +*.c +*.html +build/* +pracmln/* diff --git a/python3/pracmln/logic/common.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/logic/common.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..59273f2c --- /dev/null +++ b/python3/pracmln/logic/common.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/logic/common.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/logic/common.pxd b/python3/pracmln/logic/common.pxd new file mode 100644 index 00000000..db15b464 --- /dev/null +++ b/python3/pracmln/logic/common.pxd @@ -0,0 +1,88 @@ +from ..mln.base cimport MLN + + + +cdef class Constraint(): + cdef dict __dict__ + cpdef gndatoms(self, l=*) + +cdef class Formula(Constraint): + cdef MLN _mln + #cdef public list children + pass + +cdef class ComplexFormula(Formula): + pass + +cdef class Conjunction(ComplexFormula): + cpdef int maxtruth(self, world) + cpdef int mintruth(self, world) + +cdef class Disjunction(ComplexFormula): + cpdef int maxtruth(self, world) + cpdef int mintruth(self, world) + +cdef class Lit(Formula): + cdef int _negated + cdef str _predname + +cdef class LitGroup(Formula): + cdef int _negated + cdef str _predname + +cdef class GroundLit(Formula): + cdef GroundAtom _gndatom + cdef int _negated + cpdef truth(self, list world) + cpdef mintruth(self, list world) + cpdef maxtruth(self, list world) + +cdef class GroundAtom(): + cdef str _predname + cdef MLN mln + #cdef int _idx + cdef dict __dict__ + cpdef truth(self, list world) + cpdef mintruth(self, list world) + cpdef maxtruth(self, list world) + +cdef class Equality(ComplexFormula): + cdef int _negated + cdef str _argsA + cdef str _argsB + cpdef truth(self, world=*) + cpdef int maxtruth(self, world) + cpdef int mintruth(self, world) + +cdef class Implication(ComplexFormula): + pass + +cdef class Biimplication(ComplexFormula): + pass + +cdef class Negation(ComplexFormula): + cpdef truth(self, list world) + pass + +cdef class Exist(ComplexFormula): + pass + +cdef class TrueFalse(Formula): + cdef float _value + cpdef float truth(self, world=*) + cpdef mintruth(self, world=*) + cpdef maxtruth(self, world=*) + +cdef class NonLogicalConstraint(Constraint): + pass + +cdef class CountConstraint(NonLogicalConstraint): + pass + +cdef class GroundCountConstraint(NonLogicalConstraint): + pass + +cdef class Logic: + cdef dict __dict__ + #cdef class Constraint(): + # pass diff --git a/python3/pracmln/logic/common.py b/python3/pracmln/logic/common.py deleted file mode 100644 index 1b5ff7af..00000000 --- a/python3/pracmln/logic/common.py +++ /dev/null @@ -1,2543 +0,0 @@ -# LOGIC -- COMMON BASE CLASSES -# -# (C) 2012-2013 by Daniel Nyga (nyga@cs.uni-bremen.de) -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -import sys - -from dnutils import logs, ifnone - -from .grammar import StandardGrammar, PRACGrammar -from ..mln.util import fstr, dict_union, colorize -from ..mln.errors import NoSuchDomainError, NoSuchPredicateError -from ..mln.constants import HARD, predicate_color, inherit, auto -from collections import defaultdict -import itertools -from functools import reduce - -logger = logs.getlogger(__name__) - -def latexsym(sym): -# import re -# sym = re.sub(r'^\w+^[_]', '', sym) -# print sym -# sym = re.sub(r'_', r'\_', sym) -# if len(sym) == 1: -# return ' %s' % sym -# elif sym.startswith('?'): -# return ' ' - return r'\textit{%s}' % str(sym) - -class Logic(object): - """ - Abstract factory class for instantiating logical constructs like conjunctions, - disjunctions etc. Every specifc logic should implement the methods and return - an instance of the respective element. They also might override the respective - implementations and behavior of the logic. - """ - - def __init__(self, grammar, mln): - """ - Creates a new instance of a Logic factory class. - - :param grammar: an instance of grammar.Grammar - :param mln: the MLN instance that the logic shall be tied to. - """ - if grammar not in ('StandardGrammar', 'PRACGrammar'): - raise Exception('Invalid grammar: %s' % grammar) - self.grammar = eval(grammar)(self) - self.mln = mln - - - def __getstate__(self): - d = self.__dict__.copy() - d['grammar'] = type(self.grammar).__name__ - return d - - def __setstate__(self, d): - self.__dict__ = d - self.grammar = eval(d['grammar'])(self) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Constraint(object): - """ - Super class of every constraint. - """ - - - def template_variants(self, mln): - """ - Gets all the template variants of the constraint for the given mln/ground markov random field. - """ - raise Exception("%s does not implement getTemplateVariants" % str(type(self))) - - - def truth(self, world): - """ - Returns the truth value of the constraint in given a complete possible world - - - :param world: a possible world as a list of truth values - """ - raise Exception("%s does not implement truth" % str(type(self))) - - - def islogical(self): - """ - Returns whether this is a logical constraint, i.e. a logical formula - """ - raise Exception("%s does not implement islogical" % str(type(self))) - - - def itergroundings(self, mrf, simplify=False, domains=None): - """ - Iteratively yields the groundings of the formula for the given ground MRF - - simplify: If set to True, the grounded formulas will be simplified - according to the evidence set in the MRF. - - domains: If None, the default domains will be used for grounding. - If its a dict mapping the variable names to a list of values, - these values will be used instead. - """ - raise Exception("%s does not implement itergroundings" % str(type(self))) - - - def idx_gndatoms(self, l=None): - raise Exception("%s does not implement idxgndatoms" % str(type(self))) - - - def gndatoms(self, l=None): - raise Exception("%s does not implement gndatoms" % str(type(self))) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Formula(Constraint): - """ - The base class for all logical formulas. - """ - - def __init__(self, mln=None, idx=None): - self.mln = mln - if idx == auto and mln is not None: - self.idx = len(mln.formulas) - else: - self.idx = idx - - @property - def idx(self): - """ - The formula's weight. - """ -# if self._idx is None: -# try: return self.mln._formulas.index(self) -# except ValueError: -# return None - return self._idx - - - @idx.setter - def idx(self, idx): -# print 'setting idx to', idx - self._idx = idx - - - @property - def mln(self): - """ - Specifies whether the weight of this formula is fixed for learning. - """ - return self._mln - - - @mln.setter - def mln(self, mln): - if hasattr(self, 'children'): - for child in self.children: - child.mln = mln - self._mln = mln - - - @property - def weight(self): - return self.mln.weight(self.idx) - - - @weight.setter - def weight(self, w): - if self.idx is None: - raise Exception('%s does not have an index' % str(self)) - self.mln.weight(self.idx, w) - - - @property - def ishard(self): - return self.weight == HARD - - - def contains_gndatom(self, gndatomidx): - """ - Checks if this formula contains the ground atom with the given index. - """ - if not hasattr(self, "children"): - return False - for child in self.children: - if child.contains_gndatom(gndatomidx): - return True - return False - - - def gndatom_indices(self, l=None): - """ - Returns a list of the indices of all ground atoms that - are contained in this formula. - """ - if l == None: l = [] - if not hasattr(self, "children"): - return l - for child in self.children: - child.gndatom_indices(l) - return l - - - def gndatoms(self, l=None): - """ - Returns a list of all ground atoms that are contained - in this formula. - """ - if l is None: l = [] - if not hasattr(self, "children"): - return l - for child in self.children: - child.gndatoms(l) - return l - - - def templ_atoms(self): - """ - Returns a list of template variants of all atoms - that can be generated from this formula and the given mln. - - :Example: - - foo(?x, +?y) ^ bar(?x, +?z) --> [foo(?x, X1), foo(?x, X2), ..., - bar(?x, Z1), bar(?x, Z2), ...] - """ - templ_atoms = [] - for literal in self.literals(): - for templ in literal.template_variants(): - templ_atoms.append(templ) - return templ_atoms - - - def atomic_constituents(self, oftype=None): - """ - Returns a list of all atomic logical constituents, optionally filtered - by type. - - Example: f.atomic_constituents(oftype=Logic.Equality) - - returns a list of all equality constraints in this formula. - """ - const = list(self.literals()) - if oftype is None: return const - else: return [c for c in const if isinstance(c, oftype)] - - - def template_variants(self): - """ - Gets all the template variants of the formula for the given MLN - """ - uniqvars = list(self.mln._unique_templvars[self.idx]) - vardoms = self.template_variables() - # get the vars with the same domains that should not be expanded ambiguously - uniqvars_ = defaultdict(set) - for var in uniqvars: - dom = vardoms[var] - uniqvars_[dom].add(var) - assignments = [] - # create sets of admissible variable assignments for the groups of unique template variables - for domain, variables in uniqvars_.items(): - group = [] - domvalues = self.mln.domains[domain] - if not domvalues: - logger.warning('Template variants cannot be constructed since the domain "{}" is empty.'.format(domain)) - for values in itertools.combinations(domvalues, len(variables)): - group.append(dict([(var, val) for var, val in zip(variables, values)])) - assignments.append(group) - # add the non-unique variables - for variable, domain in vardoms.items(): - if variable in uniqvars: continue - group = [] - domvalues = self.mln.domains[domain] - if not domvalues: - logger.warning('Template variants cannot be constructed since the domain "{}" is empty.'.format(domain)) - for value in self.mln.domains[domain]: - group.append(dict([(variable, value)])) - assignments.append(group) - # generate the combinations of values - def product(assign, result=[]): - if len(assign) == 0: - yield result - return - for a in assign[0]: - for r in product(assign[1:], result+[a]): yield r - for assignment in product(assignments): - if assignment: - for t in self._ground_template(reduce(lambda x, y: dict_union(x, y), itertools.chain(assignment))): - yield t - else: - for t in self._ground_template({}): - yield t - - def template_variables(self, variable=None): - """ - Gets all variables of this formula that are required to be expanded - (i.e. variables to which a '+' was appended) and returns a - mapping (dict) from variable name to domain name. - """ - raise Exception("%s does not implement template_variables" % str(type(self))) - - - def _ground_template(self, assignment): - """ - Grounds this formula for the given assignment of template variables - and returns a list of formulas, the list of template variants - - assignment: a mapping from variable names to constants - """ - raise Exception("%s does not implement _ground_template" % str(type(self))) - - - def itervargroundings(self, mrf, partial=None): - """ - Yields dictionaries mapping variable names to values - this formula may be grounded with without grounding it. If there are not free - variables in the formula, returns an empty dict. - """ -# try: - variables = self.vardoms() - if partial is not None: - for v in [p for p in partial if p in variables]: del variables[v] -# except Exception, e: -# raise Exception("Error finding variable assignments '%s': %s" % (str(self), str(e))) - for assignment in self._itervargroundings(mrf, variables, {}): - yield assignment - - - def _itervargroundings(self, mrf, variables, assignment): - # if all variables have been assigned a value... - if variables == {}: - yield assignment - return - # ground the first variable... - variables = dict(variables) - varname, domname = variables.popitem() - domain = mrf.domains[domname] - assignment = dict(assignment) - for value in domain: # replacing it with one of the constants - assignment[varname] = value - # recursive descent to ground further variables - for assign in self._itervargroundings(mrf, dict(variables), assignment): - yield assign - - - def itergroundings(self, mrf, simplify=False, domains=None): - """ - Iteratively yields the groundings of the formula for the given grounder - - :param mrf: an object, such as an MRF instance, which - :param simplify: If set to True, the grounded formulas will be simplified - according to the evidence set in the MRF. - :param domains: If None, the default domains will be used for grounding. - If its a dict mapping the variable names to a list of values, - these values will be used instead. - :returns: a generator for all ground formulas - """ - try: - variables = self.vardoms() - except Exception as e: - raise Exception("Error grounding '%s': %s" % (str(self), str(e))) - for grounding in self._itergroundings(mrf, variables, {}, simplify, domains): - yield grounding - - - def iter_true_var_assignments(self, mrf, world=None, truth_thr=1.0, strict=False, unknown=False, partial=None): - """ - Iteratively yields the variable assignments (as a dict) for which this - formula exceeds the given truth threshold. - - Same as itergroundings, but returns variable mappings only for assignments rendering this formula true. - - :param mrf: the MRF instance to be used for the grounding. - :param world: the possible world values. if `None`, the evidence in the MRF is used. - :param thr: a truth threshold for this formula. Only variable assignments rendering this - formula true with at least this truth value will be returned. - :param strict: if `True`, the truth value of the formula must be strictly greater than the `thr`. - if `False`, it can be greater or equal. - :param unknown: If `True`, also groundings with the truth value `None` are returned - """ - if world is None: - world = list(mrf.evidence) - if partial is None: - partial = {} - try: - variables = self.vardoms() - for var in partial: - if var in variables: del variables[var] - except Exception as e: - raise Exception("Error grounding '%s': %s" % (str(self), str(e))) - for assignment in self._iter_true_var_assignments(mrf, variables, partial, world, - dict(variables), truth_thr=truth_thr, strict=strict, unknown=unknown): - yield assignment - - - def _iter_true_var_assignments(self, mrf, variables, assignment, world, allvars, truth_thr=1.0, strict=False, unknown=False): - # if all variables have been grounded... - if variables == {}: - gf = self.ground(mrf, assignment) - truth = gf(world) - if (((truth >= truth_thr) if not strict else (truth > truth_thr)) and truth is not None) or (truth is None and unknown): - true_assignment = {} - for v in allvars: - true_assignment[v] = assignment[v] - yield true_assignment - return - # ground the first variable... - varname, domname = variables.popitem() - assignment_ = dict(assignment) # copy for avoiding side effects - if domname not in mrf.domains: raise NoSuchDomainError('The domain %s does not exist, but is needed to ground the formula %s' % (domname, str(self))) - for value in mrf.domains[domname]: # replacing it with one of the constants - assignment_[varname] = value - # recursive descent to ground further variables - for ass in self._iter_true_var_assignments(mrf, dict(variables), assignment_, world, allvars, - truth_thr=truth_thr, strict=strict, unknown=unknown): - yield ass - - - def _itergroundings(self, mrf, variables, assignment, simplify=False, domains=None): - # if all variables have been grounded... - if not variables: - gf = self.ground(mrf, assignment, simplify, domains) - yield gf - return - # ground the first variable... - varname, domname = variables.popitem() - domain = domains[varname] if domains is not None else mrf.domains[domname] - for value in domain: # replacing it with one of the constants - assignment[varname] = value - # recursive descent to ground further variables - for gf in self._itergroundings(mrf, dict(variables), assignment, simplify, domains): - yield gf - - - def vardoms(self, variables=None, constants=None): - """ - Returns a dictionary mapping each variable name in this formula to - its domain name as specified in the associated MLN. - """ - raise Exception("%s does not implement vardoms()" % str(type(self))) - - - def prednames(self, prednames=None): - """ - Returns a list of all predicate names used in this formula. - """ - raise Exception('%s does not implement prednames()' % str(type(self))) - - - def ground(self, mrf, assignment, simplify=False, partial=False): - """ - Grounds the formula using the given assignment of variables to values/constants and, if given a list in referencedAtoms, - fills that list with indices of ground atoms that the resulting ground formula uses - - :param mrf: the :class:`mln.base.MRF` instance - :param assignment: mapping of variable names to values - :param simplify: whether or not the formula shall be simplified wrt, the evidence - :param partial: by default, only complete groundings are allowed. If `partial` is `True`, - the result formula may also contain free variables. - :returns: a new formula object instance representing the grounded formula - """ - raise Exception("%s does not implement ground" % str(type(self))) - - - def copy(self, mln=None, idx=inherit): - """ - Produces a deep copy of this formula. - - If `mln` is specified, the copied formula will be tied to `mln`. If not, it will be tied to the same - MLN as the original formula is. If `idx` is None, the index of the original formula will be used. - - :param mln: the MLN that the new formula shall be tied to. - :param idx: the index of the formula. - If `None`, the index of this formula will be erased to `None`. - if `idx` is `auto`, the formula will get a new index from the MLN. - if `idx` is :class:`mln.constants.inherit`, the index from this formula will be inherited to the copy (default). - """ - raise Exception('%s does not implement copy()' % str(type(self)))#self._copy(ifnone(mln, self.mln), ifnone(idx, self.idx)) - - - def vardom(self, varname): - """ - Returns the domain values of the variable with name `vardom`. - """ - return self.mln.domains.get(self.vardoms()[varname]) - - - def cnf(self, level=0): - """ - Convert to conjunctive normal form. - """ - return self - - - def nnf(self, level=0): - """ - Convert to negation normal form. - """ - return self.copy() - - - def print_structure(self, world=None, level=0, stream=sys.stdout): - """ - Prints the structure of the formula to the given `stream`. - """ - stream.write(''.rjust(level * 4, ' ')) - stream.write('%s: [idx=%s, weight=%s] %s = %s\n' % (repr(self), ifnone(self.idx, '?'), '?' if self.idx is None else self.weight, - str(self), ifnone(world, '?', lambda mrf: ifnone(self.truth(world), '?')))) - if hasattr(self, 'children'): - for child in self.children: - child.print_structure(world, level+1, stream) - - - def islogical(self): - return True - - - def simplify(self, mrf): - """ - Simplify the formula by evaluating it with respect to the ground atoms given - by the evidence in the mrf. - """ - raise Exception('%s does not implement simplify()' % str(type(self))) - - - def literals(self): - """ - Traverses the formula and returns a generator for the literals it contains. - """ - if not hasattr(self, 'children'): - yield self - return - else: - for child in self.children: - for lit in child.literals(): - yield lit - - - def expandgrouplits(self): - #returns list of formulas - for t in self._ground_template({}): - yield t - - - def truth(self, world): - """ - Evaluates the formula for its truth wrt. the truth values - of ground atoms in the possible world `world`. - - :param world: a vector of truth values representing a possible world. - :returns: the truth of the formula in `world` in [0,1] or None if - the truth value cannot be determined. - """ - raise Exception('%s does not implement truth()' % str(type(self))) - - - def countgroundings(self, mrf): - """ - Computes the number of ground formulas based on the domains of free - variables in this formula. (NB: this does _not_ generate the groundings.) - """ - gf_count = 1 - for _, dom in self.vardoms().items(): - domain = mrf.domains[dom] - gf_count *= len(domain) - return gf_count - - - def maxtruth(self, world): - """ - Returns the maximum truth value of this formula given the evidence. - For FOL, this is always 1 if the formula is not rendered false by evidence. - """ - raise Exception('%s does not implement maxtruth()' % self.__class__.__name__) - - - def mintruth(self, world): - """ - Returns the minimum truth value of this formula given the evidence. - For FOL, this is always 0 if the formula is not rendered true by evidence. - """ - raise Exception('%s does not implement mintruth()' % self.__class__.__name__) - - - def __call__(self, world): - return self.truth(world) - - - def __repr__(self): - return '<%s: %s>' % (self.__class__.__name__, str(self)) - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class ComplexFormula(Formula): - """ - A formula that has other formulas as subelements (children) - """ - - def __init__(self, mln, idx=None): - Formula.__init__(self, mln, idx) - - - def vardoms(self, variables=None, constants=None): - """ - Get the free (unquantified) variables of the formula in a dict that maps the variable name to the corresp. domain name - The vars and constants parameters can be omitted. - If vars is given, it must be a dictionary with already known variables. - If constants is given, then it must be a dictionary that is to be extended with all constants appearing in the formula; - it will be a dictionary mapping domain names to lists of constants - If constants is not given, then constants are not collected, only variables. - The dictionary of variables is returned. - """ - if variables is None: variables = defaultdict(set) - for child in self.children: - if not hasattr(child, "vardoms"): continue - variables = child.vardoms(variables, constants) - return variables - - - def constants(self, constants=None): - """ - Get the constants appearing in the formula in a dict that maps the constant - name to the domain name the constant belongs to. - """ - if constants == None: constants = defaultdict - for child in self.children: - if not hasattr(child, "constants"): continue - constants = child.constants(constants) - return constants - - - def ground(self, mrf, assignment, simplify=False, partial=False): - children = [] - for child in self.children: - gndchild = child.ground(mrf, assignment, simplify, partial) - children.append(gndchild) - gndformula = self.mln.logic.create(type(self), children, mln=self.mln, idx=self.idx) - if simplify: - gndformula = gndformula.simplify(mrf.evidence) - gndformula.idx = self.idx - return gndformula - - - def copy(self, mln=None, idx=inherit): - children = [] - for child in self.children: - child_ = child.copy(mln=ifnone(mln, self.mln), idx=None) - children.append(child_) - return type(self)(children, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) - - - def _ground_template(self, assignment): - variants = [[]] - for child in self.children: - child_variants = child._ground_template(assignment) - new_variants = [] - for variant in variants: - for child_variant in child_variants: - v = list(variant) - v.append(child_variant) - new_variants.append(v) - variants = new_variants - final_variants = [] - for variant in variants: - if isinstance(self, Logic.Exist): - final_variants.append(self.mln.logic.exist(self.vars, variant[0], mln=self.mln)) - else: - final_variants.append(self.mln.logic.create(type(self), variant, mln=self.mln)) - return final_variants - - - def template_variables(self, variables=None): - if variables == None: - variables = {} - for child in self.children: - child.template_variables(variables) - return variables - - - def prednames(self, prednames=None): - if prednames is None: - prednames = [] - for child in self.children: - if not hasattr(child, 'prednames'): continue - prednames = child.prednames(prednames) - return prednames - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class Conjunction(ComplexFormula): - """ - Represents a logical conjunction. - """ - - - def __init__(self, children, mln, idx=None): - Formula.__init__(self, mln, idx) - self.children = children - - @property - def children(self): - return self._children - - @children.setter - def children(self, children): - if len(children) < 2: - raise Exception('Conjunction needs at least 2 children.') - self._children = children - - - def __str__(self): - return ' ^ '.join([('(%s)' % str(c)) if isinstance(c, Logic.ComplexFormula) else str(c) for c in self.children]) - - - def cstr(self, color=False): - return ' ^ '.join([('(%s)' % c.cstr(color)) if isinstance(c, Logic.ComplexFormula) else c.cstr(color) for c in self.children]) - - - def latex(self): - return ' \land '.join([('(%s)' % c.latex()) if isinstance(c, Logic.ComplexFormula) else c.latex() for c in self.children]) - - - def maxtruth(self, world): - mintruth = 1 - for c in self.children: - truth = c.truth(world) - if truth is None: continue - if truth < mintruth: mintruth = truth - return mintruth - - - def mintruth(self, world): - mintruth = 1 - for c in self.children: - truth = c.truth(world) - if truth is None: return 0 - if truth < mintruth: mintruth = truth - return mintruth - - - def cnf(self, level=0): - clauses = [] - litSets = [] - for child in self.children: - c = child.cnf(level+1) - if isinstance(c, Logic.Conjunction): # flatten nested conjunction - l = c.children - else: - l = [c] - for clause in l: # (clause is either a disjunction, a literal or a constant) - # if the clause is always true, it can be ignored; if it's always false, then so is the conjunction - if isinstance(clause, Logic.TrueFalse): - if clause.truth() == 1: - continue - elif clause.truth() == 0: - return self.mln.logic.true_false(0, mln=self.mln, idx=self.idx) - # get the set of string literals - if hasattr(clause, "children"): - litSet = set(map(str, clause.children)) - else: # unit clause - litSet = set([str(clause)]) - # check if the clause is equivalent to another (subset/superset of the set of literals) -> always keep the smaller one - doAdd = True - i = 0 - while i < len(litSets): - s = litSets[i] - if len(litSet) < len(s): - if litSet.issubset(s): - del litSets[i] - del clauses[i] - continue - else: - if litSet.issuperset(s): - doAdd = False - break - i += 1 - if doAdd: - clauses.append(clause) - litSets.append(litSet) - if not clauses: - return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) - elif len(clauses) == 1: - return clauses[0].copy(idx=self.idx) - return self.mln.logic.conjunction(clauses, mln=self.mln, idx=self.idx) - - - def nnf(self, level = 0): - conjuncts = [] - for child in self.children: - c = child.nnf(level+1) - if isinstance(c, Logic.Conjunction): # flatten nested conjunction - conjuncts.extend(c.children) - else: - conjuncts.append(c) - return self.mln.logic.conjunction(conjuncts, mln=self.mln, idx=self.idx) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class Disjunction(ComplexFormula): - """ - Represents a disjunction of formulas. - """ - - - def __init__(self, children, mln, idx=None): - Formula.__init__(self, mln, idx) - self.children = children - - - @property - def children(self): - """ - A list of disjuncts. - """ - return self._children - - - @children.setter - def children(self, children): - if len(children) < 2: - raise Exception('Disjunction needs at least 2 children.') - self._children = children - - - def __str__(self): - return ' v '.join([('(%s)' % str(c)) if isinstance(c, Logic.ComplexFormula) else str(c) for c in self.children]) - - - def cstr(self, color=False): - return ' v '.join([('(%s)' % c.cstr(color)) if isinstance(c, Logic.ComplexFormula) else c.cstr(color) for c in self.children]) - - - def latex(self): - return ' \lor '.join([('(%s)' % c.latex()) if isinstance(c, Logic.ComplexFormula) else c.latex() for c in self.children]) - - def maxtruth(self, world): - maxtruth = 0 - for c in self.children: - truth = c.truth(world) - if truth is None: return 1 - if truth > maxtruth: maxtruth = truth - return maxtruth - - - def mintruth(self, world): - maxtruth = 0 - for c in self.children: - truth = c.truth(world) - if truth is None: continue - if truth > maxtruth: maxtruth = truth - return maxtruth - - - def cnf(self, level=0): - disj = [] - conj = [] - # convert children to CNF and group by disjunction/conjunction; flatten nested disjunction, remove duplicates, check for tautology - for child in self.children: - c = child.cnf(level+1) # convert child to CNF -> must be either conjunction of clauses, disjunction of literals, literal or boolean constant - if isinstance(c, Logic.Conjunction): - conj.append(c) - else: - if isinstance(c, Logic.Disjunction): - lits = c.children - else: # literal or boolean constant - lits = [c] - for l in lits: - # if the literal is always true, the disjunction is always true; if it's always false, it can be ignored - if isinstance(l, Logic.TrueFalse): - if l.truth(): - return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) - else: continue - # it's a regular literal: check if the negated literal is already among the disjuncts - l_ = l.copy() - l_.negated = True - if l_ in disj: - return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) - # check if the literal itself is not already there and if not, add it - if l not in disj: disj.append(l) - # if there are no conjunctions, this is a flat disjunction or unit clause - if not conj: - if len(disj) >= 2: - return self.mln.logic.disjunction(disj, mln=self.mln, idx=self.idx) - else: - return disj[0].copy() - # there are conjunctions among the disjuncts - # if there is only one conjunction and no additional disjuncts, we are done - if len(conj) == 1 and not disj: return conj[0].copy() - # otherwise apply distributivity - # use the first conjunction to distribute: (C_1 ^ ... ^ C_n) v RD = (C_1 v RD) ^ ... ^ (C_n v RD) - # - C_i = conjuncts[i] - conjuncts = conj[0].children - # - RD = disjunction of the elements in remaining_disjuncts (all the original disjuncts except the first conjunction) - remaining_disjuncts = disj + conj[1:] - # - create disjunctions - disj = [] - for c in conjuncts: - disj.append(self.mln.logic.disjunction([c] + remaining_disjuncts, mln=self.mln, idx=self.idx)) - return self.mln.logic.conjunction(disj, mln=self.mln, idx=self.idx).cnf(level + 1) - - - def nnf(self, level = 0): - disjuncts = [] - for child in self.children: - c = child.nnf(level+1) - if isinstance(c, Logic.Disjunction): # flatten nested disjunction - disjuncts.extend(c.children) - else: - disjuncts.append(c) - return self.mln.logic.disjunction(disjuncts, mln=self.mln, idx=self.idx) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Lit(Formula): - """ - Represents a literal. - """ - - def __init__(self, negated, predname, args, mln, idx=None): - Formula.__init__(self, mln, idx) - self.negated = negated - self.predname = predname - self.args = list(args) - - - @property - def negated(self): - return self._negated - - - @negated.setter - def negated(self, value): - self._negated = value - - - @property - def predname(self): - return self._predname - - - @predname.setter - def predname(self, predname): - if self.mln is not None and self.mln.predicate(predname) is None: - # if self.mln is not None and any(self.mln.predicate(p) is None for p in predname): - raise NoSuchPredicateError('Predicate %s is undefined.' % predname) - self._predname = predname - - - @property - def args(self): - return self._args - - - @args.setter - def args(self, args): - if self.mln is not None and len(args) != len(self.mln.predicate(self.predname).argdoms): - raise Exception('Illegal argument length: %s. %s requires %d arguments: %s' % (str(args), self.predname, - len(self.mln.predicate(self.predname).argdoms), - self.mln.predicate(self.predname).argdoms)) - self._args = args - - - def __str__(self): - return {True:'!', False:'', 2: '*'}[self.negated] + self.predname + "(" + ",".join(self.args) + ")" - - - def cstr(self, color=False): - return {True:"!", False:"", 2:'*'}[self.negated] + colorize(self.predname, predicate_color, color) + "(" + ",".join(self.args) + ")" - - - def latex(self): - return {True:r'\lnot ', False:'', 2: '*'}[self.negated] + latexsym(self.predname) + "(" + ",".join(map(latexsym, self.args)) + ")" - - - def vardoms(self, variables=None, constants=None): - if variables == None: - variables = {} - argdoms = self.mln.predicate(self.predname).argdoms - if len(argdoms) != len(self.args): - raise Exception("Wrong number of parameters in '%s'; expected %d!" % (str(self), len(argdoms))) - for i, arg in enumerate(self.args): - if self.mln.logic.isvar(arg): - varname = arg - domain = argdoms[i] - if varname in variables and variables[varname] != domain and variables[varname] is not None: - raise Exception("Variable '%s' bound to more than one domain: %s" % (varname, str((variables[varname], domain)))) - variables[varname] = domain - elif constants is not None: - domain = argdoms[i] - if domain not in constants: constants[domain] = [] - constants[domain].append(arg) - return variables - - - def template_variables(self, variables=None): - if variables == None: variables = {} - for i, arg in enumerate(self.args): - if self.mln.logic.istemplvar(arg): - varname = arg - pred = self.mln.predicate(self.predname) - domain = pred.argdoms[i] - if varname in variables and variables[varname] != domain: - raise Exception("Variable '%s' bound to more than one domain" % varname) - variables[varname] = domain - return variables - - - def prednames(self, prednames=None): - if prednames is None: - prednames = [] - if self.predname not in prednames: - prednames.append(self.predname) - return prednames - - - def ground(self, mrf, assignment, simplify=False, partial=False): - args = [assignment.get(x, x) for x in self.args] - if not any(map(self.mln.logic.isvar, args)): - atom = "%s(%s)" % (self.predname, ",".join(args)) - gndatom = mrf.gndatom(atom) - if gndatom is None: - raise Exception('Could not ground "%s". This atom is not among the ground atoms.' % atom) - # simplify if necessary - if simplify and gndatom.truth(mrf.evidence) is not None: - truth = gndatom.truth(mrf.evidence) - if self.negated: truth = 1 - truth - return self.mln.logic.true_false(truth, mln=self.mln, idx=self.idx) - gndformula = self.mln.logic.gnd_lit(gndatom, self.negated, mln=self.mln, idx=self.idx) - return gndformula - else: - if partial: - return self.mln.logic.lit(self.negated, self.predname, args, mln=self.mln, idx=self.idx) - if any([self.mln.logic.isvar(arg) for arg in args]): - raise Exception('Partial formula groundings are not allowed. Consider setting partial=True if desired.') - else: - print("\nground atoms:") - mrf.print_gndatoms() - raise Exception("Could not ground formula containing '%s' - this atom is not among the ground atoms (see above)." % self.predname) - - - def _ground_template(self, assignment): - args = [assignment.get(x, x) for x in self.args] - if self.negated == 2: # template - return [self.mln.logic.lit(False, self.predname, args, mln=self.mln), self.mln.logic.lit(True, self.predname, args, mln=self.mln)] - else: - return [self.mln.logic.lit(self.negated, self.predname, args, mln=self.mln)] - - - def copy(self, mln=None, idx=inherit): - return self.mln.logic.lit(self.negated, self.predname, self.args, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) - - - def truth(self, world): - return None -# raise Exception('Literals do not have a truth value. Ground the literal first.') - - - def mintruth(self, world): - raise Exception('Literals do not have a truth value. Ground the literal first.') - - - def maxtruth(self, world): - raise Exception('Literals do not have a truth value. Ground the literal first.') - - - def constants(self, constants=None): - if constants is None: constants = {} - for i, c in enumerate(self.params): - domname = self.mln.predicate(self.predname).argdoms[i] - values = constants.get(domname, None) - if values is None: - values = [] - constants[domname] = values - if not self.mln.logic.isvar(c) and not c in values: values.append(c) - return constants - - - def simplify(self, world): - return self.mln.logic.lit(self.negated, self.predname, self.args, mln=self.mln, idx=self.idx) - - - def __eq__(self, other): - return str(self) == str(other) - - - def __ne__(self, other): - return not self == other - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class LitGroup(Formula): - """ - Represents a group of literals with identical arguments. - """ - - def __init__(self, negated, predname, args, mln, idx=None): - Formula.__init__(self, mln, idx) - self.negated = negated - self.predname = predname - self.args = list(args) - - - @property - def negated(self): - return self._negated - - - @negated.setter - def negated(self, value): - self._negated = value - - - @property - def predname(self): - return self._predname - - - @predname.setter - def predname(self, prednames): - """ - predname is a list of predicate names, of which each is tested if it is None - """ - if self.mln is not None and any(self.mln.predicate(p) is None for p in prednames): - erroneouspreds = [p for p in prednames if self.mln.predicate(p) is None] - raise NoSuchPredicateError('Predicate{} {} is undefined.'.format('s' if len(erroneouspreds) > 1 else '', ', '.join(erroneouspreds))) - self._predname = prednames - - - @property - def lits(self): - return [Lit(self.negated, lit, self.args, self.mln) for lit in self.predname] - - - @property - def args(self): - return self._args - - - @args.setter - def args(self, args): - # arguments are identical for all predicates in group, so choose - # arbitrary predicate - predname = self.predname[0] - if self.mln is not None and len(args) != len(self.mln.predicate(predname).argdoms): - raise Exception('Illegal argument length: %s. %s requires %d arguments: %s' % (str(args), predname, - len(self.mln.predicate(predname).argdoms), - self.mln.predicate(predname).argdoms)) - self._args = args - - - def __str__(self): - return {True:'!', False:'', 2: '*'}[self.negated] + '|'.join(self.predname) + "(" + ",".join(self.args) + ")" - - - def cstr(self, color=False): - return {True:"!", False:"", 2:'*'}[self.negated] + colorize('|'.join(self.predname), predicate_color, color) + "(" + ",".join(self.args) + ")" - - - def latex(self): - return {True:r'\lnot ', False:'', 2: '*'}[self.negated] + latexsym('|'.join(self.predname)) + "(" + ",".join(map(latexsym, self.args)) + ")" - - - def vardoms(self, variables=None, constants=None): - if variables == None: - variables = {} - argdoms = self.mln.predicate(self.predname[0]).argdoms - if len(argdoms) != len(self.args): - raise Exception("Wrong number of parameters in '%s'; expected %d!" % (str(self), len(argdoms))) - for i, arg in enumerate(self.args): - if self.mln.logic.isvar(arg): - varname = arg - domain = argdoms[i] - if varname in variables and variables[varname] != domain and variables[varname] is not None: - raise Exception("Variable '%s' bound to more than one domain" % varname) - variables[varname] = domain - elif constants is not None: - domain = argdoms[i] - if domain not in constants: constants[domain] = [] - constants[domain].append(arg) - return variables - - - def template_variables(self, variables=None): - if variables == None: variables = {} - for i, arg in enumerate(self.args): - if self.mln.logic.istemplvar(arg): - varname = arg - pred = self.mln.predicate(self.predname[0]) - domain = pred.argdoms[i] - if varname in variables and variables[varname] != domain: - raise Exception("Variable '%s' bound to more than one domain" % varname) - variables[varname] = domain - return variables - - - def prednames(self, prednames=None): - if prednames is None: - prednames = [] - prednames.extend([p for p in self.predname if p not in prednames]) - return prednames - - - def _ground_template(self, assignment): - # args = map(lambda x: assignment.get(x, x), self.args) - if self.negated == 2: # template - return [self.mln.logic.lit(False, predname, self.args, mln=self.mln) for predname in self.predname] + \ - [self.mln.logic.lit(True, predname, self.args, mln=self.mln) for predname in self.predname] - else: - return [self.mln.logic.lit(self.negated, predname, self.args, mln=self.mln) for predname in self.predname] - - def copy(self, mln=None, idx=inherit): - return self.mln.logic.litgroup(self.negated, self.predname, self.args, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) - - - def truth(self, world): - return None - - - def mintruth(self, world): - raise Exception('LitGroups do not have a truth value. Ground the literal first.') - - - def maxtruth(self, world): - raise Exception('LitGroups do not have a truth value. Ground the literal first.') - - - def constants(self, constants=None): - if constants is None: constants = {} - for i, c in enumerate(self.params): - # domname = self.mln.predicate(self.predname).argdoms[i] - domname = self.mln.predicate(self.predname[0]).argdoms[i] - values = constants.get(domname, None) - if values is None: - values = [] - constants[domname] = values - if not self.mln.logic.isvar(c) and not c in values: values.append(c) - return constants - - - def simplify(self, world): - return self.mln.logic.litgroup(self.negated, self.predname, self.args, mln=self.mln, idx=self.idx) - - - def __eq__(self, other): - return str(self) == str(other) - - - def __ne__(self, other): - return not self == other - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundLit(Formula): - """ - Represents a ground literal. - """ - - - def __init__(self, gndatom, negated, mln, idx=None): - Formula.__init__(self, mln, idx) - self.gndatom = gndatom - self.negated = negated - - - @property - def gndatom(self): - return self._gndatom - - - @gndatom.setter - def gndatom(self, gndatom): - self._gndatom = gndatom - - - @property - def negated(self): - return self._negated - - - @negated.setter - def negated(self, negate): - self._negated = negate - - - @property - def predname(self): - return self.gndatom.predname - - @property - def args(self): - return self.gndatom.args - - - def truth(self, world): - tv = self.gndatom.truth(world) - if tv is None: return None - if self.negated: return (1. - tv) - return tv - - - def mintruth(self, world): - truth = self.truth(world) - if truth is None: return 0 - else: return truth - - - def maxtruth(self, world): - truth = self.truth(world) - if truth is None: return 1 - else: return truth - - - def __str__(self): - return {True:"!", False:""}[self.negated] + str(self.gndatom) - - - def cstr(self, color=False): - return {True:"!", False:""}[self.negated] + self.gndatom.cstr(color) - - - def contains_gndatom(self, atomidx): - return (self.gndatom.idx == atomidx) - - - def vardoms(self, variables=None, constants=None): - return self.gndatom.vardoms(variables, constants) - - - def constants(self, constants=None): - if constants is None: constants = {} - for i, c in enumerate(self.gndatom.args): - domname = self.mln.predicates[self.gndatom.predname][i] - values = constants.get(domname, None) - if values is None: - values = [] - constants[domname] = values - if not c in values: values.append(c) - return constants - - - def gndatom_indices(self, l=None): - if l == None: l = [] - if self.gndatom.idx not in l: l.append(self.gndatom.idx) - return l - - - def gndatoms(self, l=None): - if l == None: l = [] - if not self.gndatom in l: l.append(self.gndatom) - return l - - - def ground(self, mrf, assignment, simplify=False, partial=False): - # always get the gnd atom from the mrf, so that - # formulas can be transferred between different MRFs - return self.mln.logic.gnd_lit(mrf.gndatom(str(self.gndatom)), self.negated, mln=self.mln, idx=self.idx) - - - def copy(self, mln=None, idx=inherit): - mln = ifnone(mln, self.mln) - if mln is not self.mln: - raise Exception('GroundLit cannot be copied among MLNs.') - return self.mln.logic.gnd_lit(self.gndatom, self.negated, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) - - - def simplify(self, world): - truth = self.truth(world) - if truth is not None: - return self.mln.logic.true_false(truth, mln=self.mln, idx=self.idx) - return self.mln.logic.gnd_lit(self.gndatom, self.negated, mln=self.mln, idx=self.idx) - - - def prednames(self, prednames=None): - if prednames is None: - prednames = [] - if self.gndatom.predname not in prednames: - prednames.append(self.gndatom.predname) - return prednames - - - def template_variables(self, variables=None): - return {} - - - def _ground_template(self, assignment): - return [self.mln.logic.gnd_lit(self.gndatom, self.negated, mln=self.mln)] - - - def __eq__(self, other): - return str(self) == str(other)#self.negated == other.negated and self.gndAtom == other.gndAtom - - - def __ne__(self, other): - return not self == other - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundAtom: - """ - Represents a ground atom. - """ - - def __init__(self, predname, args, mln, idx=None): - self.predname = predname - self.args = args - self.idx = idx - self.mln = mln - - - @property - def predname(self): - return self._predname - - - @predname.setter - def predname(self, predname): - self._predname = predname - - - @property - def args(self): - return self._args - - - @args.setter - def args(self, args): - self._args = args - - - @property - def idx(self): - return self._idx - - - @idx.setter - def idx(self, idx): - self._idx = idx - - - def truth(self, world): - return world[self.idx] - - - def mintruth(self, world): - truth = self.truth(world) - if truth is None: return 0 - else: return truth - - - def maxtruth(self, world): - truth = self.truth(world) - if truth is None: return 1 - else: return truth - - - def __repr__(self): - return '' % str(self) - - - def __str__(self): - return "%s(%s)" % (self.predname, ",".join(self.args)) - - - def cstr(self, color=False): - return "%s(%s)" % (colorize(self.predname, predicate_color, color), ",".join(self.args)) - - - def prednames(self, prednames=None): - if prednames is None: - prednames = [] - if self.predname not in prednames: - prednames.append(self.predname) - return prednames - - - def vardoms(self, variables=None, constants=None): - if variables is None: - variables = {} - if constants is None: - constants = {} - for d, c in zip(self.args, self.mln.predicate(self.predname).argdoms): - if d not in constants: - constants[d] = [] - if c not in constants[d]: - constants[d].append(c) - return variables - - - def __eq__(self, other): - return str(self) == str(other) - - def __ne__(self, other): - return not self == other - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Equality(ComplexFormula): - """ - Represents (in)equality constraints between two symbols. - """ - - - def __init__(self, args, negated, mln, idx=None): - ComplexFormula.__init__(self, mln, idx) - self.args = args - self.negated = negated - - - @property - def args(self): - return self._args - - - @args.setter - def args(self, args): - if len(args) != 2: - raise Exception('Illegal number of aeguments of equality: %d' % len(args)) - self._args = args - - - @property - def negated(self): - return self._negated - - @negated.setter - def negated(self, negate): - self._negated = negate - - - def __str__(self): - return "%s%s%s" % (str(self.args[0]), '=/=' if self.negated else '=', str(self.args[1])) - - - def cstr(self, color=False): - return str(self) - - - def latex(self): - return "%s%s%s" % (latexsym(self.args[0]), r'\neq ' if self.negated else '=', latexsym(self.args[1])) - - - def ground(self, mrf, assignment, simplify=False, partial=False): - # if the parameter is a variable, do a lookup (it must be bound by now), - # otherwise it's a constant which we can use directly - args = [assignment.get(x, x) for x in self.args] - if self.mln.logic.isvar(args[0]) or self.mln.logic.isvar(args[1]): - if partial: - return self.mln.logic.equality(args, self.negated, mln=self.mln) - else: raise Exception("At least one variable was not grounded in '%s'!" % str(self)) - if simplify: - equal = (args[0] == args[1]) - return self.mln.logic.true_false(1 if {True: not equal, False: equal}[self.negated] else 0, mln=self.mln, idx=self.idx) - else: - return self.mln.logic.equality(args, self.negated, mln=self.mln, idx=self.idx) - - - def copy(self, mln=None, idx=inherit): - return self.mln.logic.equality(self.args, self.negated, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) - - - def _ground_template(self, assignment): - return [self.mln.logic.equality(self.args, negated=self.negated, mln=self.mln)] - - - def template_variables(self, variables=None): - return variables - - - def vardoms(self, variables=None, constants=None): - if variables is None: - variables = {} - if self.mln.logic.isvar(self.args[0]) and self.args[0] not in variables: variables[self.args[0]] = None - if self.mln.logic.isvar(self.args[1]) and self.args[1] not in variables: variables[self.args[1]] = None - return variables - - - def vardom(self, varname): - return None - - - def vardomain_from_formula(self, formula): - f_var_domains = formula.vardoms() - eq_vars = self.vardoms() - for var_ in eq_vars: - if var_ not in f_var_domains: - raise Exception('Variable %s not bound to a domain by formula %s' % (var_, fstr(formula))) - eq_vars[var_] = f_var_domains[var_] - return eq_vars - - - def prednames(self, prednames=None): - if prednames is None: - prednames = [] - return prednames - - - def truth(self, world=None): - if any(map(self.mln.logic.isvar, self.args)): - return None - equals = 1 if (self.args[0] == self.args[1]) else 0 - return (1 - equals) if self.negated else equals - - - def maxtruth(self, world): - truth = self.truth(world) - if truth is None: return 1 - else: return truth - - - def mintruth(self, world): - truth = self.truth(world) - if truth is None: return 0 - else: return truth - - - def simplify(self, world): - truth = self.truth(world) - if truth != None: return self.mln.logic.true_false(truth, mln=self.mln, idx=self.idx) - return self.mln.logic.equality(list(self.args), negated=self.negated, mln=self.mln, idx=self.idx) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Implication(ComplexFormula): - """ - Represents an implication - """ - - - def __init__(self, children, mln, idx=None): - Formula.__init__(self, mln, idx) - self.children = children - - @property - def children(self): - return self._children - - @children.setter - def children(self, children): - if len(children) != 2: - raise Exception('Implication needs exactly 2 children (antescedant and consequence)') - self._children = children - - - def __str__(self): - c1 = self.children[0] - c2 = self.children[1] - return (str(c1) if not isinstance(c1, Logic.ComplexFormula) \ - else '(%s)' % str(c1)) + " => " + (str(c2) if not isinstance(c2, Logic.ComplexFormula) else '(%s)' % str(c2)) - - - def cstr(self, color=False): - c1 = self.children[0] - c2 = self.children[1] - (s1, s2) = (c1.cstr(color), c2.cstr(color)) - (s1, s2) = (('(%s)' if isinstance(c1, Logic.ComplexFormula) else '%s') % s1, ('(%s)' if isinstance(c2, Logic.ComplexFormula) else '%s') % s2) - return '%s => %s' % (s1, s2) - - - def latex(self): - return self.children[0].latex() + r" \rightarrow " + self.children[1].latex() - - - def cnf(self, level=0): - return self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln, idx=self.idx), self.children[1]], mln=self.mln, idx=self.idx).cnf(level+1) - - - def nnf(self, level=0): - return self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln, idx=self.idx), self.children[1]], mln=self.mln, idx=self.idx).nnf(level+1) - - - def simplify(self, world): - return self.mln.logic.disjunction([Negation([self.children[0]], mln=self.mln, idx=self.idx), self.children[1]], mln=self.mln, idx=self.idx).simplify(world) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Biimplication(ComplexFormula): - """ - Represents a bi-implication. - """ - - - def __init__(self, children, mln, idx=None): - Formula.__init__(self, mln, idx) - self.children = children - - - @property - def children(self): - return self._children - - - @children.setter - def children(self, children): - if len(children) != 2: - raise Exception('Biimplication needs exactly 2 children') - self._children = children - - - def __str__(self): - c1 = self.children[0] - c2 = self.children[1] - return (str(c1) if not isinstance(c1, Logic.ComplexFormula) \ - else '(%s)' % str(c1)) + " <=> " + (str(c2) if not isinstance(c2, Logic.ComplexFormula) else str(c2)) - - - def cstr(self, color=False): - c1 = self.children[0] - c2 = self.children[1] - (s1, s2) = (c1.cstr(color), c2.cstr(color)) - (s1, s2) = (('(%s)' if isinstance(c1, Logic.ComplexFormula) else '%s') % s1, ('(%s)' if isinstance(c2, Logic.ComplexFormula) else '%s') % s2) - return '%s <=> %s' % (s1, s2) - - - def latex(self): - return r'%s \leftrightarrow %s' % (self.children[0].latex(), self.children[1].latex()) - - - def cnf(self, level=0): - cnf = self.mln.logic.conjunction([self.mln.logic.implication([self.children[0], self.children[1]], mln=self.mln, idx=self.idx), - self.mln.logic.implication([self.children[1], self.children[0]], mln=self.mln, idx=self.idx)], mln=self.mln, idx=self.idx) - return cnf.cnf(level+1) - - - def nnf(self, level = 0): - return self.mln.logic.conjunction([self.mln.logic.implication([self.children[0], self.children[1]], mln=self.mln, idx=self.idx), - self.mln.logic.implication([self.children[1], self.children[0]], mln=self.mln, idx=self.idx)], mln=self.mln, idx=self.idx).nnf(level+1) - - - def simplify(self, world): - c1 = self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln), self.children[1]], mln=self.mln) - c2 = self.mln.logic.disjunction([self.children[0], self.mln.logic.negation([self.children[1]], mln=self.mln)], mln=self.mln) - return self.mln.logic.conjunction([c1,c2], mln=self.mln, idx=self.idx).simplify(world) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Negation(ComplexFormula): - """ - Represents a negation of a complex formula. - """ - - def __init__(self, children, mln, idx=None): - ComplexFormula.__init__(self, mln, idx) - if hasattr(children, '__iter__'): - assert len(children) == 1 - else: - children = [children] - self.children = children - - - @property - def children(self): - return self._children - - @children.setter - def children(self, children): - if hasattr(children, '__iter__'): - if len(children) != 1: - raise Exception('Negation may have only 1 child.') - else: - children = [children] - self._children = children - - - def __str__(self): - return ('!(%s)' if isinstance(self.children[0], Logic.ComplexFormula) else '!%s') % str(self.children[0]) - - - def cstr(self, color=False): - return ('!(%s)' if isinstance(self.children[0], Logic.ComplexFormula) else '!%s') % self.children[0].cstr(color) - - - def latex(self): - return r'\lnot (%s)' % self.children[0].latex() - - - def truth(self, world): - childValue = self.children[0].truth(world) - if childValue is None: - return None - return 1 - childValue - - - def cnf(self, level=0): - # convert the formula that is negated to negation normal form (NNF), - # so that if it's a complex formula, it will be either a disjunction - # or conjunction, to which we can then apply De Morgan's law. - # Note: CNF conversion would be unnecessarily complex, and, - # when the children are negated below, most of it would be for nothing! - child = self.children[0].nnf(level+1) - # apply negation to child (pull inwards) - if hasattr(child, 'children'): - neg_children = [] - for c in child.children: - neg_children.append(self.mln.logic.negation([c], mln=self.mln, idx=None).cnf(level+1)) - if isinstance(child, Logic.Conjunction): - return self.mln.logic.disjunction(neg_children, mln=self.mln, idx=self.idx).cnf(level+1) - elif isinstance(child, Logic.Disjunction): - return self.mln.logic.conjunction(neg_children, mln=self.mln, idx=self.idx).cnf(level+1) - elif isinstance(child, Logic.Negation): - return c.cnf(level+1) - else: - raise Exception("Unexpected child type %s while converting '%s' to CNF!" % (str(type(child)), str(self))) - elif isinstance(child, Logic.Lit): - return self.mln.logic.lit(not child.negated, child.predname, child.args, mln=self.mln, idx=self.idx) - elif isinstance(child, Logic.LitGroup): - return self.mln.logic.litgroup(not child.negated, child.predname, child.args, mln=self.mln, idx=self.idx) - elif isinstance(child, Logic.GroundLit): - return self.mln.logic.gnd_lit(child.gndatom, not child.negated, mln=self.mln, idx=self.idx) - elif isinstance(child, Logic.TrueFalse): - return self.mln.logic.true_false(1 - child.value, mln=self.mln, idx=self.idx) - elif isinstance(child, Logic.Equality): - return self.mln.logic.equality(child.params, not child.negated, mln=self.mln, idx=self.idx) - else: - raise Exception("CNF conversion of '%s' failed (type:%s)" % (str(self), str(type(child)))) - - - def nnf(self, level = 0): - # child is the formula that is negated - child = self.children[0].nnf(level+1) - # apply negation to the children of the formula that is negated (pull inwards) - # - complex formula (should be disjunction or conjunction at this point), use De Morgan's law - if hasattr(child, 'children'): - neg_children = [] - for c in child.children: - neg_children.append(self.mln.logic.negation([c], mln=self.mln, idx=None).nnf(level+1)) - if isinstance(child, Logic.Conjunction): # !(A ^ B) = !A v !B - return self.mln.logic.disjunction(neg_children, mln=self.mln, idx=self.idx).nnf(level+1) - elif isinstance(child, Logic.Disjunction): # !(A v B) = !A ^ !B - return self.mln.logic.conjunction(neg_children, mln=self.mln, idx=self.idx).nnf(level+1) - elif isinstance(child, Logic.Negation): - return c.nnf(level+1) - # !(A => B) = A ^ !B - # !(A <=> B) = (A ^ !B) v (B ^ !A) - else: - raise Exception("Unexpected child type %s while converting '%s' to NNF!" % (str(type(child)), str(self))) - # - non-complex formula, i.e. literal or constant - elif isinstance(child, Logic.Lit): - return self.mln.logic.lit(not child.negated, child.predname, child.args, mln=self.mln, idx=self.idx) - elif isinstance(child, Logic.LitGroup): - return self.mln.logic.litgroup(not child.negated, child.predname, child.args, mln=self.mln, idx=self.idx) - elif isinstance(child, Logic.GroundLit): - return self.mln.logic.gnd_lit(child.gndatom, not child.negated, mln=self.mln, idx=self.idx) - elif isinstance(child, Logic.TrueFalse): - return self.mln.logic.true_false(1 - child.value, mln=self.mln, idx=self.idx) - elif isinstance(child, Logic.Equality): - return self.mln.logic.equality(child.args, not child.negated, mln=self.mln, idx=self.idx) - else: - raise Exception("NNF conversion of '%s' failed (type:%s)" % (str(self), str(type(child)))) - - - def simplify(self, world): - f = self.children[0].simplify(world) - if isinstance(f, Logic.TrueFalse): - return f.invert() - else: - return self.mln.logic.negation([f], mln=self.mln, idx=self.idx) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Exist(ComplexFormula): - """ - Existential quantifier. - """ - - - def __init__(self, variables, formula, mln, idx=None): - Formula.__init__(self, mln, idx) - self.formula = formula - self.vars = variables - - - @property - def children(self): - return self._children - - @children.setter - def children(self, children): - if len(children) != 1: - raise Exception('Illegal number of formulas in Exist: %s' % str(children)) - self._children = children - - - @property - def formula(self): - return self._children[0] - - @formula.setter - def formula(self, f): - self._children = [f] - - @property - def vars(self): - return self._vars - - @vars.setter - def vars(self, v): - self._vars = v - - - def __str__(self): - return 'EXIST %s (%s)' % (', '.join(self.vars), str(self.formula)) - - - def cstr(self, color=False): - return colorize('EXIST ', predicate_color, color) + ', '.join(self.vars) + ' (' + self.formula.cstr(color) + ')' - - - def latex(self): - return '\exists\ %s (%s)' % (', '.join(map(latexsym, self.vars)), self.formula.latex()) - - - def vardoms(self, variables=None, constants=None): - if variables == None: - variables = {} - # get the child's variables: - newvars = self.formula.vardoms(None, constants) - # remove the quantified variable(s) - for var in self.vars: - try: del newvars[var] - except: - raise Exception("Variable '%s' in '%s' not bound to a domain!" % (var, str(self))) - # add the remaining ones that are not None and return - variables.update(dict([(k, v) for k, v in newvars.items() if v is not None])) - return variables - - - def ground(self, mrf, assignment, partial=False, simplify=False): - # find out variable domains - vardoms = self.formula.vardoms() - if not set(self.vars).issubset(vardoms): - raise Exception('One or more variables do not appear in formula: %s' % str(set(self.vars).difference(vardoms))) - variables = dict([(k,v) for k,v in vardoms.items() if k in self.vars]) - # ground - gndings = [] - self._ground(self.children[0], variables, assignment, gndings, mrf, partial=partial) - if len(gndings) == 1: - return gndings[0] - if not gndings: - return self.mln.logic.true_false(0, mln=self.mln, idx=self.idx) - disj = self.mln.logic.disjunction(gndings, mln=self.mln, idx=self.idx) - if simplify: - return disj.simplify(mrf.evidence) - else: - return disj - - - def _ground(self, formula, variables, assignment, gndings, mrf, partial=False): - # if all variables have been grounded... - if variables == {}: - gndFormula = formula.ground(mrf, assignment, partial=partial) - gndings.append(gndFormula) - return - # ground the first variable... - varname,domname = variables.popitem() - for value in mrf.domains[domname]: # replacing it with one of the constants - assignment[varname] = value - # recursive descent to ground further variables - self._ground(formula, dict(variables), assignment, gndings, mrf, partial=partial) - - - def copy(self, mln=None, idx=inherit): - return self.mln.logic.exist(self.vars, self.formula, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) - - - def cnf(self,l=0): - raise Exception("'%s' cannot be converted to CNF. Ground this formula first!" % str(self)) - - - def truth(self, w): - raise Exception("'%s' does not implement truth()" % self.__class__.__name__) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class TrueFalse(Formula): - """ - Represents constant truth values. - """ - - def __init__(self, truth, mln, idx=None): - Formula.__init__(self, mln, idx) - self.value = truth - - @property - def value(self): - return self._value - - def cstr(self, color=False): - return str(self) - - def truth(self, world=None): - return self.value - - def mintruth(self, world=None): - return self.truth - - def maxtruth(self, world=None): - return self.truth - - def invert(self): - return self.mln.logic.true_false(1 - self.truth(), mln=self.mln, idx=self.idx) - - def simplify(self, world): - return self.copy() - - def vardoms(self, variables=None, constants=None): - if variables is None: - variables = {} - return variables - - def ground(self, mln, assignment, simplify=False, partial=False): - return self.mln.logic.true_false(self.value, mln=self.mln, idx=self.idx) - - def copy(self, mln=None, idx=inherit): - return self.mln.logic.true_false(self.value, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class NonLogicalConstraint(Constraint): - """ - A constraint that is not somehow made up of logical connectives and (ground) atoms. - """ - - def template_variants(self, mln): - # non logical constraints are never templates; therefore, there is just one variant, the constraint itself - return [self] - - def islogical(self): - return False - - def negate(self): - raise Exception("%s does not implement negate()" % str(type(self))) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class CountConstraint(NonLogicalConstraint): - """ - A constraint that tests the number of relation instances against an integer. - """ - - def __init__(self, predicate, predicate_params, fixed_params, op, count): - """op: an operator; one of "=", "<=", ">=" """ - self.literal = self.mln.logic.lit(False, predicate, predicate_params) - self.fixed_params = fixed_params - self.count = count - if op == "=": op = "==" - self.op = op - - def __str__(self): - op = self.op - if op == "==": op = "=" - return "count(%s | %s) %s %d" % (str(self.literal), ", ".join(self.fixed_params), op, self.count) - - def cstr(self, color=False): - return str(self) - - def iterGroundings(self, mrf, simplify=False): - a = {} - other_params = [] - for param in self.literal.params: - if param[0].isupper(): - a[param] = param - else: - if param not in self.fixed_params: - other_params.append(param) - #other_params = list(set(self.literal.params).difference(self.fixed_params)) - # for each assignment of the fixed parameters... - for assignment in self._iterAssignment(mrf, list(self.fixed_params), a): - gndAtoms = [] - # generate a count constraint with all the atoms we obtain by grounding the other params - for full_assignment in self._iterAssignment(mrf, list(other_params), assignment): - gndLit = self.literal.ground(mrf, full_assignment, None) - gndAtoms.append(gndLit.gndAtom) - yield self.mln.logic.gnd_count_constraint(gndAtoms, self.op, self.count), [] - - def _iterAssignment(self, mrf, variables, assignment): - """iterates over all possible assignments for the given variables of this constraint's literal - variables: the variables that are still to be grounded""" - # if all variables have been grounded, we have the complete assigment - if len(variables) == 0: - yield dict(assignment) - return - # otherwise one of the remaining variables in the list... - varname = variables.pop() - domName = self.literal.getVarDomain(varname, mrf.mln) - for value in mrf.domains[domName]: # replacing it with one of the constants - assignment[varname] = value - # recursive descent to ground further variables - for a in self._iterAssignment(mrf, variables, assignment): - yield a - - def getVariables(self, mln, variables = None, constants = None): - if constants is not None: - self.literal.getVariables(mln, variables, constants) - return variables - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundCountConstraint(NonLogicalConstraint): - def __init__(self, gndAtoms, op, count): - self.gndAtoms = gndAtoms - self.count = count - self.op = op - - def isTrue(self, world_values): - c = 0 - for ga in self.gndAtoms: - if(world_values[ga.idx]): - c += 1 - return eval("c %s self.count" % self.op) - - def __str__(self): - op = self.op - if op == "==": op = "=" - return "count(%s) %s %d" % (";".join(map(str, self.gndAtoms)), op, self.count) - - def cstr(self, color=False): - op = self.op - if op == "==": op = "=" - return "count(%s) %s %d" % (";".join([c.cstr(color) for c in self.gndAtoms]), op, self.count) - - def negate(self): - if self.op == "==": - self.op = "!=" - elif self.op == "!=": - self.op = "==" - elif self.op == ">=": - self.op = "<=" - self.count -= 1 - elif self.op == "<=": - self.op = ">=" - self.count += 1 - - def idxGroundAtoms(self, l = None): - if l is None: l = [] - for ga in self.gndAtoms: - l.append(ga.idx) - return l - - def getGroundAtoms(self, l = None): - if l is None: l = [] - for ga in self.gndAtoms: - l.append(ga) - return l - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - def isvar(self, identifier): - """ - Returns True if identifier is a logical variable according - to the used grammar, and False otherwise. - """ - return self.grammar.isvar(identifier) - - def isconstant(self, identifier): - """ - Returns True if identifier is a logical constant according - to the used grammar, and False otherwise. - """ - return self.grammar.isConstant(identifier) - - def istemplvar(self, s): - """ - Returns True if `s` is template variable or False otherwise. - """ - return self.grammar.istemplvar(s) - - def parse_formula(self, formula): - """ - Returns the Formula object parsed by the grammar. - """ - return self.grammar.parse_formula(formula) - - def parse_predicate(self, string): - return self.grammar.parse_predicate(string) - - def parse_atom(self, string): - return self.grammar.parse_atom(string) - - def parse_domain(self, decl): - return self.grammar.parse_domain(decl) - - def parse_literal(self, lit): - return self.grammar.parse_literal(lit) - - - def islit(self, f): - """ - Determines whether or not a formula is a literal. - """ - return isinstance(f, Logic.GroundLit) or isinstance(f, Logic.Lit) or isinstance(f, Logic.GroundAtom) - - - def iseq(self, f): - """ - Determines wheter or not a formula is an equality consttaint. - """ - return isinstance(f, Logic.Equality) - - - def islitconj(self, f): - """ - Returns true if the given formula is a conjunction of literals. - """ - if self.islit(f): return True - if not isinstance(f, Logic.Conjunction): - if not isinstance(f, Logic.Lit) and \ - not isinstance(f, Logic.GroundLit) and \ - not isinstance(f, Logic.Equality) and \ - not isinstance(f, Logic.TrueFalse): - return False - return True - for child in f.children: - if not isinstance(child, Logic.Lit) and \ - not isinstance(child, Logic.GroundLit) and \ - not isinstance(child, Logic.Equality) and \ - not isinstance(child, Logic.TrueFalse): - return False - return True - - - def isclause(self, f): - """ - Returns true if the given formula is a clause (a disjunction of literals) - """ - if self.islit(f): return True - if not isinstance(f, Logic.Disjunction): - if not isinstance(f, Logic.Lit) and \ - not isinstance(f, Logic.GroundLit) and \ - not isinstance(f, Logic.Equality) and \ - not isinstance(f, Logic.TrueFalse): - return False - return True - for child in f.children: - if not isinstance(child, Logic.Lit) and \ - not isinstance(child, Logic.GroundLit) and \ - not isinstance(child, Logic.Equality) and \ - not isinstance(child, Logic.TrueFalse): - return False - return True - - - def negate(self, formula): - """ - Returns a negation of the given formula. - - The original formula will be copied first. The resulting negation is tied - to the same mln and will have the same formula index. Also performs - a rudimentary simplification in case of `formula` is a (ground) literal - or equality. - """ - if isinstance(formula, Logic.Lit) or isinstance(formula, Logic.GroundLit): - ret = formula.copy() - ret.negated = not ret.negated - elif isinstance(formula, Logic.Equality): - ret = formula.copy() - ret.negated = not ret.negated - else: - ret = self.negation([formula.copy(mln=formula.mln, idx=None)], mln=formula.mln, idx=formula.idx) - return ret - - - def conjugate(self, children, mln=None, idx=inherit): - """ - Returns a conjunction of the given children. - - Performs rudimentary simplification in the sense that if children - has only one element, it returns this element (e.g. one literal) - """ - if not children: - return self.true_false(0, mln=ifnone(mln, self.mln), idx=idx) - elif len(children) == 1: - return children[0].copy(mln=ifnone(mln, self.mln), idx=idx) - else: - return self.conjunction(children, mln=ifnone(mln,self.mln), idx=idx) - - - def disjugate(self, children, mln=None, idx=inherit): - """ - Returns a conjunction of the given children. - - Performs rudimentary simplification in the sense that if children - has only one element, it returns this element (e.g. one literal) - """ - if not children: - return self.true_false(0, mln=ifnone(mln, self.mln), idx=idx) - elif len(children) == 1: - return children[0].copy(mln=ifnone(mln, self.mln), idx=idx) - else: - return self.disjunction(children, mln=ifnone(mln,self.mln), idx=idx) - - - @staticmethod - def iter_eq_varassignments(eq, f, mln): - """ - Iterates over all variable assignments of an (in)equality constraint. - - Needs a formula since variables in equality constraints are not typed per se. - """ - doms = f.vardoms() - eqVars_ = eq.vardoms() - if not set(eqVars_).issubset(doms): - raise Exception('Variable in (in)equality constraint not bound to a domain: %s' % eq) - eqVars = {} - for v in eqVars_: - eqVars[v] = doms[v] - for assignment in Logic._iter_eq_varassignments(mln, eqVars, {}): - yield assignment - - @staticmethod - def _iter_eq_varassignments(mrf, variables, assignment): - if len(variables) == 0: - yield assignment - return - variables = dict(variables) - variable, domName = variables.popitem() - domain = mrf.domains[domName] - for value in domain: - for assignment in Logic._iter_eq_varassignments(mrf, variables, dict_union(assignment, {variable: value})): - yield assignment - - - @staticmethod - def clauseset(cnf): - """ - Takes a formula in CNF and returns a set of clauses, i.e. a list of sets - containing literals. All literals are converted into strings. - """ - clauses = [] - if isinstance(cnf, Logic.Disjunction): - clauses.append(set(map(str, cnf.children))) - elif isinstance(cnf, Logic.Conjunction): - for disj in cnf.children: - clause = set() - clauses.append(clause) - if isinstance(disj, Logic.Disjunction): - for c in disj.children: - clause.add(str(c)) - else: - clause.add(str(disj)) - else: - clauses.append(set([str(cnf)])) - return clauses - - - @staticmethod - def cnf(gfs, formulas, logic, allpos=False): - """ - convert the given ground formulas to CNF - if allPositive=True, then formulas with negative weights are negated to make all weights positive - @return a new pair (gndformulas, formulas) - - .. warning:: - - If allpos is True, this might have side effects on the formula weights of the MLN. - - """ - # get list of formula indices which we must negate - formulas_ = [] - negated = [] - if allpos: - for f in formulas: - if f.weight < 0: - negated.append(f.idx) - f = logic.negate(f) - f.weight = -f.weight - formulas_.append(f) - # get CNF version of each ground formula - gfs_ = [] - for gf in gfs: - # non-logical constraint - if not gf.islogical(): # don't apply any transformations to non-logical constraints - if gf.idx in negated: - gf.negate() - gfs_.append(gf) - continue - # logical constraint - if gf.idx in negated: - cnf = logic.negate(gf).cnf() - else: - cnf = gf.cnf() - if isinstance(cnf, Logic.TrueFalse): # formulas that are always true or false can be ignored - continue - cnf.idx = gf.idx - gfs_.append(cnf) - # return modified formulas - return gfs_, formulas_ - - - def conjunction(self, *args, **kwargs): - """ - Returns a new instance of a Conjunction object. - """ - raise Exception('%s does not implement conjunction()' % str(type(self))) - - def disjunction(self, *args, **kwargs): - """ - Returns a new instance of a Disjunction object. - """ - raise Exception('%s does not implement disjunction()' % str(type(self))) - - def negation(self, *args, **kwargs): - """ - Returns a new instance of a Negation object. - """ - raise Exception('%s does not implement negation()' % str(type(self))) - - def implication(self, *args, **kwargs): - """ - Returns a new instance of a Implication object. - """ - raise Exception('%s does not implement implication()' % str(type(self))) - - def biimplication(self, *args, **kwargs): - """ - Returns a new instance of a Biimplication object. - """ - raise Exception('%s does not implement biimplication()' % str(type(self))) - - def equality(self, *args, **kwargs): - """ - Returns a new instance of a Equality object. - """ - raise Exception('%s does not implement equality()' % str(type(self))) - - def exist(self, *args, **kwargs): - """ - Returns a new instance of a Exist object. - """ - raise Exception('%s does not implement exist()' % str(type(self))) - - def gnd_atom(self, *args, **kwargs): - """ - Returns a new instance of a GndAtom object. - """ - raise Exception('%s does not implement gnd_atom()' % str(type(self))) - - def lit(self, *args, **kwargs): - """ - Returns a new instance of a Lit object. - """ - raise Exception('%s does not implement lit()' % str(type(self))) - - def litgroup(self, *args, **kwargs): - """ - Returns a new instance of a Lit object. - """ - raise Exception('%s does not implement litgroup()' % str(type(self))) - - def gnd_lit(self, *args, **kwargs): - """ - Returns a new instance of a GndLit object. - """ - raise Exception('%s does not implement gnd_lit()' % str(type(self))) - - def count_constraint(self, *args, **kwargs): - """ - Returns a new instance of a CountConstraint object. - """ - raise Exception('%s does not implement count_constraint()' % str(type(self))) - - def true_false(self, *args, **kwargs): - """ - Returns a new instance of a TrueFalse constant object. - """ - raise Exception('%s does not implement true_false()' % str(type(self))) - - def create(self, clazz, *args, **kwargs): - """ - Takes the type of a logical element (class type) and creates - a new instance of it. - """ - return clazz(*args, **kwargs) - - - - -# this is a little hack to make nested classes pickleable -Constraint = Logic.Constraint -Formula = Logic.Formula -ComplexFormula = Logic.ComplexFormula -Conjunction = Logic.Conjunction -Disjunction = Logic.Disjunction -Lit = Logic.Lit -LitGroup = Logic.LitGroup -GroundLit = Logic.GroundLit -GroundAtom = Logic.GroundAtom -Equality = Logic.Equality -Implication = Logic.Implication -Biimplication = Logic.Biimplication -Negation = Logic.Negation -Exist = Logic.Exist -TrueFalse = Logic.TrueFalse -NonLogicalConstraint = Logic.NonLogicalConstraint -CountConstraint = Logic.CountConstraint -GroundCountConstraint = Logic.GroundCountConstraint diff --git a/python3/pracmln/logic/common.pyx b/python3/pracmln/logic/common.pyx new file mode 100644 index 00000000..af8d2e50 --- /dev/null +++ b/python3/pracmln/logic/common.pyx @@ -0,0 +1,2585 @@ +# LOGIC -- COMMON BASE CLASSES +# +# (C) 2012-2013 by Daniel Nyga (nyga@cs.uni-bremen.de) +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +import sys + +from dnutils import logs, ifnone + +from .grammar import StandardGrammar, PRACGrammar +from ..mln.util import fstr, dict_union, colorize +from ..mln.errors import NoSuchDomainError, NoSuchPredicateError +from ..mln.constants import HARD, predicate_color, inherit, auto +from collections import defaultdict +import itertools +from functools import reduce + +'''from .misc import Constraint as misc_Constraint +from .misc import Formula as misc_Formula +from .misc import ComplexFormula as misc_ComplexFormula +from .misc import Conjunction as misc_Conjunction +from .misc import Disjunction as misc_Disjunction +from .misc import Lit as misc_Lit +from .misc import LitGroup as misc_LitGroup +from .misc import GroundLit as misc_GroundLit +from .misc import GroundAtom as misc_GroundAtom +from .misc import Equality as misc_Equality +from .misc import Implication as misc_Implication +from .misc import Biimplication as misc_Biimplication +from .misc import Negation as misc_Negation +from .misc import Exist as misc_Exist +from .misc import TrueFalse as misc_TrueFalse +from .misc import NonLogicalConstraint as misc_NonLogicalConstraint +from .misc import CountConstraint as misc_CountConstraint +from .misc import GroundCountConstraint as misc_GroundCountConstraint''' + + +logger = logs.getlogger(__name__) + + +def latexsym(sym): + return r'\textit{%s}' % str(sym) + + + + + + +cdef class Constraint(): + """ + Super class of every constraint. + """ + def template_variants(self, mln): + """ + Gets all the template variants of the constraint for the given mln/ground markov random field. + """ + raise Exception("%s does not implement getTemplateVariants" % str(type(self))) + + def truth(self, world): + """ + Returns the truth value of the constraint in given a complete possible world + + + :param world: a possible world as a list of truth values + """ + raise Exception("%s does not implement truth" % str(type(self))) + + def islogical(self): + """ + Returns whether this is a logical constraint, i.e. a logical formula + """ + raise Exception("%s does not implement islogical" % str(type(self))) + + def itergroundings(self, mrf, simplify=False, domains=None): + """ + Iteratively yields the groundings of the formula for the given ground MRF + - simplify: If set to True, the grounded formulas will be simplified + according to the evidence set in the MRF. + - domains: If None, the default domains will be used for grounding. + If its a dict mapping the variable names to a list of values, + these values will be used instead. + """ + raise Exception("%s does not implement itergroundings" % str(type(self))) + + def idx_gndatoms(self, l=None): + raise Exception("%s does not implement idxgndatoms" % str(type(self))) + + cpdef gndatoms(self, l=None): + raise Exception("%s does not implement gndatoms" % str(type(self))) + +cdef class Formula(Constraint): + """ + The base class for all logical formulas. + """ + + def __init__(self, mln=None, idx=None): + self.mln = mln + if idx == auto and mln is not None: + self.idx = len(mln.formulas) + else: + self.idx = idx + + @property + def idx(self): + """ + The formula's weight. + """ + # if self._idx is None: + # try: return self.mln._formulas.index(self) + # except ValueError: + # return None + return self._idx + + + @idx.setter + def idx(self, idx): + # print 'setting idx to', idx + self._idx = idx + + + @property + def mln(self): + """ + Specifies whether the weight of this formula is fixed for learning. + """ + return self._mln + + + @mln.setter + def mln(self, mln): + if hasattr(self, 'children'):# and self.children is not None: + for child in self.children: + child.mln = mln + self._mln = mln + + + @property + def weight(self): + return self.mln.weight(self.idx) + + + @weight.setter + def weight(self, double w): + if self.idx is None: + raise Exception('%s does not have an index' % str(self)) + self.mln.weight(self.idx, w) + + + @property + def ishard(self): + return self.weight == HARD + + + # Q(gsoc): the following code is never called + def contains_gndatom(self, gndatomidx): + """ + Checks if this formula contains the ground atom with the given index. + """ + if not hasattr(self, "children"): + return False + for child in self.children: + if child.contains_gndatom(gndatomidx): + return True + return False + + + def gndatom_indices(self, l=None): + """ + Returns a list of the indices of all ground atoms that + are contained in this formula. + """ + if l == None: l = [] + if not hasattr(self, "children"): + return l + for child in self.children: + child.gndatom_indices(l) + return l + + # Q(gsoc): needs speedup ... + cpdef gndatoms(self, l=None): + """ + Returns a list of all ground atoms that are contained + in this formula. + """ + if l is None: l = [] + #print('\nFormula:gndatoms - (common.pyx-203) l = {}'.format(l)) + if not hasattr(self, "children"): + return l + #print('Formula:gndatoms - (common.pyx-205) self.children = {}, is of type = {}, type of child[0] = {}'.format(self.children, type(self.children), type(self.children[0]))) + for child in self.children: + child.gndatoms(l) + #print('Formula:gndatoms - (common.pyx-209) l = {}, type of l[0] = {}'.format(l, type(l[0]))) + return l + + + def templ_atoms(self): + """ + Returns a list of template variants of all atoms + that can be generated from this formula and the given mln. + + :Example: + + foo(?x, +?y) ^ bar(?x, +?z) --> [foo(?x, X1), foo(?x, X2), ..., + bar(?x, Z1), bar(?x, Z2), ...] + """ + templ_atoms = [] + for literal in self.literals(): + for templ in literal.template_variants(): + templ_atoms.append(templ) + return templ_atoms + + + def atomic_constituents(self, oftype=None): + """ + Returns a list of all atomic logical constituents, optionally filtered + by type. + + Example: f.atomic_constituents(oftype=Equality) + + returns a list of all equality constraints in this formula. + """ + const = list(self.literals()) + if oftype is None: return const + else: return [c for c in const if isinstance(c, oftype)] + + + def template_variants(self): + """ + Gets all the template variants of the formula for the given MLN + """ + uniqvars = list(self.mln._unique_templvars[self.idx]) + vardoms = self.template_variables() + # get the vars with the same domains that should not be expanded ambiguously + uniqvars_ = defaultdict(set) + for var in uniqvars: + dom = vardoms[var] + uniqvars_[dom].add(var) + assignments = [] + # create sets of admissible variable assignments for the groups of unique template variables + for domain, variables in uniqvars_.items(): + group = [] + domvalues = self.mln.domains[domain] + if not domvalues: + logger.warning('Template variants cannot be constructed since the domain "{}" is empty.'.format(domain)) + for values in itertools.combinations(domvalues, len(variables)): + group.append(dict([(var, val) for var, val in zip(variables, values)])) + assignments.append(group) + # add the non-unique variables + for variable, domain in vardoms.items(): + if variable in uniqvars: continue + group = [] + domvalues = self.mln.domains[domain] + if not domvalues: + logger.warning('Template variants cannot be constructed since the domain "{}" is empty.'.format(domain)) + for value in self.mln.domains[domain]: + group.append(dict([(variable, value)])) + assignments.append(group) + # generate the combinations of values + def product(assign, result=[]): + if len(assign) == 0: + yield result + return + for a in assign[0]: + for r in product(assign[1:], result+[a]): yield r + for assignment in product(assignments): + if assignment: + for t in self._ground_template(reduce(lambda x, y: dict_union(x, y), itertools.chain(assignment))): + yield t + else: + for t in self._ground_template({}): + yield t + + def template_variables(self, variable=None): + """ + Gets all variables of this formula that are required to be expanded + (i.e. variables to which a '+' was appended) and returns a + mapping (dict) from variable name to domain name. + """ + raise Exception("%s does not implement template_variables" % str(type(self))) + + + def _ground_template(self, assignment): + """ + Grounds this formula for the given assignment of template variables + and returns a list of formulas, the list of template variants + - assignment: a mapping from variable names to constants + """ + raise Exception("%s does not implement _ground_template" % str(type(self))) + + + def itervargroundings(self, mrf, partial=None): + """ + Yields dictionaries mapping variable names to values + this formula may be grounded with without grounding it. If there are not free + variables in the formula, returns an empty dict. + """ + # try: + variables = self.vardoms() + if partial is not None: + for v in [p for p in partial if p in variables]: del variables[v] + # except Exception, e: + # raise Exception("Error finding variable assignments '%s': %s" % (str(self), str(e))) + for assignment in self._itervargroundings(mrf, variables, {}): + yield assignment + + + def _itervargroundings(self, mrf, variables, assignment): + # if all variables have been assigned a value... + if variables == {}: + yield assignment + return + # ground the first variable... + variables = dict(variables) + varname, domname = variables.popitem() + domain = mrf.domains[domname] + assignment = dict(assignment) + for value in domain: # replacing it with one of the constants + assignment[varname] = value + # recursive descent to ground further variables + for assign in self._itervargroundings(mrf, dict(variables), assignment): + yield assign + + + def itergroundings(self, mrf, simplify=False, domains=None): + """ + Iteratively yields the groundings of the formula for the given grounder + + :param mrf: an object, such as an MRF instance, which + :param simplify: If set to True, the grounded formulas will be simplified + according to the evidence set in the MRF. + :param domains: If None, the default domains will be used for grounding. + If its a dict mapping the variable names to a list of values, + these values will be used instead. + :returns: a generator for all ground formulas + """ + try: + variables = self.vardoms() + except Exception as e: + raise Exception("Error grounding '%s': %s" % (str(self), str(e))) + for grounding in self._itergroundings(mrf, variables, {}, simplify, domains): + yield grounding + + + def iter_true_var_assignments(self, mrf, world=None, truth_thr=1.0, strict=False, unknown=False, partial=None): + """ + Iteratively yields the variable assignments (as a dict) for which this + formula exceeds the given truth threshold. + + Same as itergroundings, but returns variable mappings only for assignments rendering this formula true. + + :param mrf: the MRF instance to be used for the grounding. + :param world: the possible world values. if `None`, the evidence in the MRF is used. + :param thr: a truth threshold for this formula. Only variable assignments rendering this + formula true with at least this truth value will be returned. + :param strict: if `True`, the truth value of the formula must be strictly greater than the `thr`. + if `False`, it can be greater or equal. + :param unknown: If `True`, also groundings with the truth value `None` are returned + """ + if world is None: + world = list(mrf.evidence) + if partial is None: + partial = {} + try: + variables = self.vardoms() + for var in partial: + if var in variables: del variables[var] + except Exception as e: + raise Exception("Error grounding '%s': %s" % (str(self), str(e))) + for assignment in self._iter_true_var_assignments(mrf, variables, partial, world, + dict(variables), truth_thr=truth_thr, strict=strict, unknown=unknown): + yield assignment + + + def _iter_true_var_assignments(self, mrf, variables, assignment, world, allvars, truth_thr=1.0, strict=False, unknown=False): + # if all variables have been grounded... + if variables == {}: + gf = self.ground(mrf, assignment) + truth = gf(world) + if (((truth >= truth_thr) if not strict else (truth > truth_thr)) and truth is not None) or (truth is None and unknown): + true_assignment = {} + for v in allvars: + true_assignment[v] = assignment[v] + yield true_assignment + return + # ground the first variable... + varname, domname = variables.popitem() + assignment_ = dict(assignment) # copy for avoiding side effects + if domname not in mrf.domains: raise NoSuchDomainError('The domain %s does not exist, but is needed to ground the formula %s' % (domname, str(self))) + for value in mrf.domains[domname]: # replacing it with one of the constants + assignment_[varname] = value + # recursive descent to ground further variables + for ass in self._iter_true_var_assignments(mrf, dict(variables), assignment_, world, allvars, + truth_thr=truth_thr, strict=strict, unknown=unknown): + yield ass + + + def _itergroundings(self, mrf, variables, assignment, simplify=False, domains=None): + # if all variables have been grounded... + if not variables: + gf = self.ground(mrf, assignment, simplify, domains) + yield gf + return + # ground the first variable... + varname, domname = variables.popitem() + domain = domains[varname] if domains is not None else mrf.domains[domname] + for value in domain: # replacing it with one of the constants + assignment[varname] = value + # recursive descent to ground further variables + for gf in self._itergroundings(mrf, dict(variables), assignment, simplify, domains): + yield gf + + + def vardoms(self, variables=None, constants=None): + """ + Returns a dictionary mapping each variable name in this formula to + its domain name as specified in the associated MLN. + """ + raise Exception("%s does not implement vardoms()" % str(type(self))) + + + def prednames(self, prednames=None): + """ + Returns a list of all predicate names used in this formula. + """ + raise Exception('%s does not implement prednames()' % str(type(self))) + + + def ground(self, mrf, assignment, simplify=False, partial=False): + """ + Grounds the formula using the given assignment of variables to values/constants and, if given a list in referencedAtoms, + fills that list with indices of ground atoms that the resulting ground formula uses + + :param mrf: the :class:`mln.base.MRF` instance + :param assignment: mapping of variable names to values + :param simplify: whether or not the formula shall be simplified wrt, the evidence + :param partial: by default, only complete groundings are allowed. If `partial` is `True`, + the result formula may also contain free variables. + :returns: a new formula object instance representing the grounded formula + """ + raise Exception("%s does not implement ground" % str(type(self))) + + + def copy(self, mln=None, idx=inherit): + """ + Produces a deep copy of this formula. + + If `mln` is specified, the copied formula will be tied to `mln`. If not, it will be tied to the same + MLN as the original formula is. If `idx` is None, the index of the original formula will be used. + + :param mln: the MLN that the new formula shall be tied to. + :param idx: the index of the formula. + If `None`, the index of this formula will be erased to `None`. + if `idx` is `auto`, the formula will get a new index from the MLN. + if `idx` is :class:`mln.constants.inherit`, the index from this formula will be inherited to the copy (default). + """ + raise Exception('%s does not implement copy()' % str(type(self)))#self._copy(ifnone(mln, self.mln), ifnone(idx, self.idx)) + + + def vardom(self, varname): + """ + Returns the domain values of the variable with name `vardom`. + """ + return self.mln.domains.get(self.vardoms()[varname]) + + + def cnf(self, level=0): + """ + Convert to conjunctive normal form. + """ + return self + + + def nnf(self, level=0): + """ + Convert to negation normal form. + """ + return self.copy() + + + def print_structure(self, world=None, level=0, stream=sys.stdout): + """ + Prints the structure of the formula to the given `stream`. + """ + stream.write(''.rjust(level * 4, ' ')) + stream.write('%s: [idx=%s, weight=%s] %s = %s\n' % (repr(self), ifnone(self.idx, '?'), '?' if self.idx is None else self.weight, + str(self), ifnone(world, '?', lambda mrf: ifnone(self.truth(world), '?')))) + if hasattr(self, 'children'): + for child in self.children: + child.print_structure(world, level+1, stream) + + + def islogical(self): + return True + + + def simplify(self, mrf): + """ + Simplify the formula by evaluating it with respect to the ground atoms given + by the evidence in the mrf. + """ + raise Exception('%s does not implement simplify()' % str(type(self))) + + + def literals(self): + """ + Traverses the formula and returns a generator for the literals it contains. + """ + if not hasattr(self, 'children'): + yield self + return + else: + for child in self.children: + for lit in child.literals(): + yield lit + + + def expandgrouplits(self): + #returns list of formulas + for t in self._ground_template({}): + yield t + + + def truth(self, world): + """ + Evaluates the formula for its truth wrt. the truth values + of ground atoms in the possible world `world`. + + :param world: a vector of truth values representing a possible world. + :returns: the truth of the formula in `world` in [0,1] or None if + the truth value cannot be determined. + """ + raise Exception('%s does not implement truth()' % str(type(self))) + + + def countgroundings(self, mrf): + """ + Computes the number of ground formulas based on the domains of free + variables in this formula. (NB: this does _not_ generate the groundings.) + """ + gf_count = 1 + for _, dom in self.vardoms().items(): + domain = mrf.domains[dom] + gf_count *= len(domain) + return gf_count + + + def maxtruth(self, world): + """ + Returns the maximum truth value of this formula given the evidence. + For FOL, this is always 1 if the formula is not rendered false by evidence. + """ + raise Exception('%s does not implement maxtruth()' % self.__class__.__name__) + + + def mintruth(self, world): + """ + Returns the minimum truth value of this formula given the evidence. + For FOL, this is always 0 if the formula is not rendered true by evidence. + """ + raise Exception('%s does not implement mintruth()' % self.__class__.__name__) + + + def __call__(self, world): + return self.truth(world) + + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, str(self)) + +cdef class ComplexFormula(Formula): + """ + A formula that has other formulas as subelements (children) + """ + + def __init__(self, mln, idx=None): + Formula.__init__(self, mln, idx) + + + def vardoms(self, variables=None, constants=None): + """ + Get the free (unquantified) variables of the formula in a dict that maps the variable name to the corresp. domain name + The vars and constants parameters can be omitted. + If vars is given, it must be a dictionary with already known variables. + If constants is given, then it must be a dictionary that is to be extended with all constants appearing in the formula; + it will be a dictionary mapping domain names to lists of constants + If constants is not given, then constants are not collected, only variables. + The dictionary of variables is returned. + """ + if variables is None: variables = defaultdict(set) + for child in self.children: + if not hasattr(child, "vardoms"): continue + variables = child.vardoms(variables, constants) + return variables + + + def constants(self, constants=None): + """ + Get the constants appearing in the formula in a dict that maps the constant + name to the domain name the constant belongs to. + """ + if constants == None: constants = defaultdict + for child in self.children: + if not hasattr(child, "constants"): continue + constants = child.constants(constants) + return constants + + + def ground(self, mrf, assignment, simplify=False, partial=False): + children = [] + for child in self.children: + gndchild = child.ground(mrf, assignment, simplify, partial) + children.append(gndchild) + gndformula = self.mln.logic.create(type(self), children, mln=self.mln, idx=self.idx) + if simplify: + gndformula = gndformula.simplify(mrf.evidence) + gndformula.idx = self.idx + return gndformula + + + def copy(self, mln=None, idx=inherit): + children = [] + for child in self.children: + child_ = child.copy(mln=ifnone(mln, self.mln), idx=None) + children.append(child_) + return type(self)(children, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) + + + def _ground_template(self, assignment): + variants = [[]] + for child in self.children: + child_variants = child._ground_template(assignment) + new_variants = [] + for variant in variants: + for child_variant in child_variants: + v = list(variant) + v.append(child_variant) + new_variants.append(v) + variants = new_variants + final_variants = [] + for variant in variants: + if isinstance(self, Exist): # Q(gsoc): replaced "Logic.Exist" with "Exist" + final_variants.append(self.mln.logic.exist(self.vars, variant[0], mln=self.mln)) + else: + final_variants.append(self.mln.logic.create(type(self), variant, mln=self.mln)) + return final_variants + + + def template_variables(self, variables=None): + if variables == None: + variables = {} + for child in self.children: + child.template_variables(variables) + return variables + + + def prednames(self, prednames=None): + if prednames is None: + prednames = [] + for child in self.children: + if not hasattr(child, 'prednames'): continue + prednames = child.prednames(prednames) + return prednames + +cdef class Conjunction(ComplexFormula): + """ + Represents a logical conjunction. + """ + + + def __init__(self, children, mln, idx=None): + Formula.__init__(self, mln, idx) + self.children = children + + @property + def children(self): + return self._children + + @children.setter + def children(self, children): + if len(children) < 2: + raise Exception('Conjunction needs at least 2 children.') + self._children = children + + + def __str__(self): + return ' ^ '.join([('(%s)' % str(c)) if isinstance(c, ComplexFormula) else str(c) for c in self.children]) + + + def cstr(self, color=False): + return ' ^ '.join([('(%s)' % c.cstr(color)) if isinstance(c, ComplexFormula) else c.cstr(color) for c in self.children]) + + + def latex(self): + return ' \land '.join([('(%s)' % c.latex()) if isinstance(c, ComplexFormula) else c.latex() for c in self.children]) + + + cpdef int maxtruth(self, world): + cdef int mintruth = 1 + for c in self.children: + truth = c.truth(world) + if truth is None: continue + if truth < mintruth: mintruth = truth + return mintruth + + + cpdef int mintruth(self, world): + cdef int mintruth = 1 + for c in self.children: + truth = c.truth(world) + if truth is None: return 0 + if truth < mintruth: mintruth = truth + return mintruth + + + def cnf(self, level=0): + clauses = [] + litSets = [] + for child in self.children: + c = child.cnf(level+1) + if isinstance(c, Conjunction): # flatten nested conjunction + l = c.children + else: + l = [c] + for clause in l: # (clause is either a disjunction, a literal or a constant) + # if the clause is always true, it can be ignored; if it's always false, then so is the conjunction + if isinstance(clause, TrueFalse): + if clause.truth() == 1: + continue + elif clause.truth() == 0: + return self.mln.logic.true_false(0, mln=self.mln, idx=self.idx) + # get the set of string literals + if hasattr(clause, "children"): + litSet = set(map(str, clause.children)) + else: # unit clause + litSet = set([str(clause)]) + # check if the clause is equivalent to another (subset/superset of the set of literals) -> always keep the smaller one + doAdd = True + i = 0 + while i < len(litSets): + s = litSets[i] + if len(litSet) < len(s): + if litSet.issubset(s): + del litSets[i] + del clauses[i] + continue + else: + if litSet.issuperset(s): + doAdd = False + break + i += 1 + if doAdd: + clauses.append(clause) + litSets.append(litSet) + if not clauses: + return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) + elif len(clauses) == 1: + return clauses[0].copy(idx=self.idx) + return self.mln.logic.conjunction(clauses, mln=self.mln, idx=self.idx) + + + def nnf(self, level = 0): + conjuncts = [] + for child in self.children: + c = child.nnf(level+1) + if isinstance(c, Conjunction): # flatten nested conjunction + conjuncts.extend(c.children) + else: + conjuncts.append(c) + return self.mln.logic.conjunction(conjuncts, mln=self.mln, idx=self.idx) + +cdef class Disjunction(ComplexFormula): + """ + Represents a disjunction of formulas. + """ + + + def __init__(self, children, mln, idx=None): + Formula.__init__(self, mln, idx) + self.children = children + + + @property + def children(self): + """ + A list of disjuncts. + """ + return self._children + + + @children.setter + def children(self, children): + if len(children) < 2: + raise Exception('Disjunction needs at least 2 children.') + self._children = children + + + def __str__(self): + return ' v '.join([('(%s)' % str(c)) if isinstance(c, ComplexFormula) else str(c) for c in self.children]) + + + def cstr(self, color=False): + return ' v '.join([('(%s)' % c.cstr(color)) if isinstance(c, ComplexFormula) else c.cstr(color) for c in self.children]) + + + def latex(self): + return ' \lor '.join([('(%s)' % c.latex()) if isinstance(c, ComplexFormula) else c.latex() for c in self.children]) + + cpdef int maxtruth(self, world): + cdef maxtruth = 0 + for c in self.children: + truth = c.truth(world) + if truth is None: return 1 + if truth > maxtruth: maxtruth = truth + return maxtruth + + + cpdef int mintruth(self, world): + cdef maxtruth = 0 + for c in self.children: + truth = c.truth(world) + if truth is None: continue + if truth > maxtruth: maxtruth = truth + return maxtruth + + + def cnf(self, level=0): + disj = [] + conj = [] + # convert children to CNF and group by disjunction/conjunction; flatten nested disjunction, remove duplicates, check for tautology + for child in self.children: + c = child.cnf(level+1) # convert child to CNF -> must be either conjunction of clauses, disjunction of literals, literal or boolean constant + if isinstance(c, Conjunction): + conj.append(c) + else: + if isinstance(c, Disjunction): + lits = c.children + else: # literal or boolean constant + lits = [c] + for l in lits: + # if the literal is always true, the disjunction is always true; if it's always false, it can be ignored + if isinstance(l, TrueFalse): + if l.truth(): + return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) + else: continue + # it's a regular literal: check if the negated literal is already among the disjuncts + l_ = l.copy() + l_.negated = True + if l_ in disj: + return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) + # check if the literal itself is not already there and if not, add it + if l not in disj: disj.append(l) + # if there are no conjunctions, this is a flat disjunction or unit clause + if not conj: + if len(disj) >= 2: + return self.mln.logic.disjunction(disj, mln=self.mln, idx=self.idx) + else: + return disj[0].copy() + # there are conjunctions among the disjuncts + # if there is only one conjunction and no additional disjuncts, we are done + if len(conj) == 1 and not disj: return conj[0].copy() + # otherwise apply distributivity + # use the first conjunction to distribute: (C_1 ^ ... ^ C_n) v RD = (C_1 v RD) ^ ... ^ (C_n v RD) + # - C_i = conjuncts[i] + conjuncts = conj[0].children + # - RD = disjunction of the elements in remaining_disjuncts (all the original disjuncts except the first conjunction) + remaining_disjuncts = disj + conj[1:] + # - create disjunctions + disj = [] + for c in conjuncts: + disj.append(self.mln.logic.disjunction([c] + remaining_disjuncts, mln=self.mln, idx=self.idx)) + return self.mln.logic.conjunction(disj, mln=self.mln, idx=self.idx).cnf(level + 1) + + + def nnf(self, level = 0): + disjuncts = [] + for child in self.children: + c = child.nnf(level+1) + if isinstance(c, Disjunction): # flatten nested disjunction + disjuncts.extend(c.children) + else: + disjuncts.append(c) + return self.mln.logic.disjunction(disjuncts, mln=self.mln, idx=self.idx) + +cdef class Lit(Formula): + """ + Represents a literal. + """ + + def __init__(self, negated, predname, args, mln, idx=None): + Formula.__init__(self, mln, idx) + self.negated = negated + self.predname = predname + self.args = list(args) + + + @property + def negated(self): + return self._negated + + + @negated.setter + def negated(self, value): + self._negated = value + + + @property + def predname(self): + return self._predname + + + @predname.setter + def predname(self, predname): + if self.mln is not None and self.mln.predicate(predname) is None: + # if self.mln is not None and any(self.mln.predicate(p) is None for p in predname): + raise NoSuchPredicateError('Predicate %s is undefined.' % predname) + self._predname = predname + + + @property + def args(self): + return self._args + + + @args.setter + def args(self, args): + if self.mln is not None and len(args) != len(self.mln.predicate(self.predname).argdoms): + raise Exception('Illegal argument length: %s. %s requires %d arguments: %s' % (str(args), self.predname, + len(self.mln.predicate(self.predname).argdoms), + self.mln.predicate(self.predname).argdoms)) + self._args = args + + + def __str__(self): + return {True:'!', False:'', 2: '*'}[self.negated] + self.predname + "(" + ",".join(self.args) + ")" + + + def cstr(self, color=False): + return {True:"!", False:"", 2:'*'}[self.negated] + colorize(self.predname, predicate_color, color) + "(" + ",".join(self.args) + ")" + + + def latex(self): + return {True:r'\lnot ', False:'', 2: '*'}[self.negated] + latexsym(self.predname) + "(" + ",".join(map(latexsym, self.args)) + ")" + + + def vardoms(self, variables=None, constants=None): + if variables == None: + variables = {} + argdoms = self.mln.predicate(self.predname).argdoms + if len(argdoms) != len(self.args): + raise Exception("Wrong number of parameters in '%s'; expected %d!" % (str(self), len(argdoms))) + for i, arg in enumerate(self.args): + if self.mln.logic.isvar(arg): + varname = arg + domain = argdoms[i] + if varname in variables and variables[varname] != domain and variables[varname] is not None: + raise Exception("Variable '%s' bound to more than one domain: %s" % (varname, str((variables[varname], domain)))) + variables[varname] = domain + elif constants is not None: + domain = argdoms[i] + if domain not in constants: constants[domain] = [] + constants[domain].append(arg) + return variables + + + def template_variables(self, variables=None): + if variables == None: variables = {} + for i, arg in enumerate(self.args): + if self.mln.logic.istemplvar(arg): + varname = arg + pred = self.mln.predicate(self.predname) + domain = pred.argdoms[i] + if varname in variables and variables[varname] != domain: + raise Exception("Variable '%s' bound to more than one domain" % varname) + variables[varname] = domain + return variables + + + def prednames(self, prednames=None): + if prednames is None: + prednames = [] + if self.predname not in prednames: + prednames.append(self.predname) + return prednames + + + def ground(self, mrf, assignment, simplify=False, partial=False): + args = [assignment.get(x, x) for x in self.args] + if not any(map(self.mln.logic.isvar, args)): + atom = "%s(%s)" % (self.predname, ",".join(args)) + gndatom = mrf.gndatom(atom) + if gndatom is None: + raise Exception('Could not ground "%s". This atom is not among the ground atoms.' % atom) + # simplify if necessary + if simplify and gndatom.truth(mrf.evidence) is not None: + truth = gndatom.truth(mrf.evidence) + if self.negated: truth = 1 - truth + return self.mln.logic.true_false(truth, mln=self.mln, idx=self.idx) + gndformula = self.mln.logic.gnd_lit(gndatom, self.negated, mln=self.mln, idx=self.idx) + return gndformula + else: + if partial: + return self.mln.logic.lit(self.negated, self.predname, args, mln=self.mln, idx=self.idx) + if any([self.mln.logic.isvar(arg) for arg in args]): + raise Exception('Partial formula groundings are not allowed. Consider setting partial=True if desired.') + else: + print("\nground atoms:") + mrf.print_gndatoms() + raise Exception("Could not ground formula containing '%s' - this atom is not among the ground atoms (see above)." % self.predname) + + + def _ground_template(self, assignment): + args = [assignment.get(x, x) for x in self.args] + if self.negated == 2: # template + return [self.mln.logic.lit(False, self.predname, args, mln=self.mln), self.mln.logic.lit(True, self.predname, args, mln=self.mln)] + else: + return [self.mln.logic.lit(self.negated, self.predname, args, mln=self.mln)] + + + def copy(self, mln=None, idx=inherit): + return self.mln.logic.lit(self.negated, self.predname, self.args, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) + + + def truth(self, world): + return None + # raise Exception('Literals do not have a truth value. Ground the literal first.') + + + def mintruth(self, world): + raise Exception('Literals do not have a truth value. Ground the literal first.') + + + def maxtruth(self, world): + raise Exception('Literals do not have a truth value. Ground the literal first.') + + + def constants(self, constants=None): + if constants is None: constants = {} + for i, c in enumerate(self.params): + domname = self.mln.predicate(self.predname).argdoms[i] + values = constants.get(domname, None) + if values is None: + values = [] + constants[domname] = values + if not self.mln.logic.isvar(c) and not c in values: values.append(c) + return constants + + + def simplify(self, world): + return self.mln.logic.lit(self.negated, self.predname, self.args, mln=self.mln, idx=self.idx) + + + def __eq__(self, other): + return str(self) == str(other) + + + def __ne__(self, other): + return not self == other + +cdef class LitGroup(Formula): + """ + Represents a group of literals with identical arguments. + """ + + def __init__(self, negated, predname, args, mln, idx=None): + Formula.__init__(self, mln, idx) + self.negated = negated + self.predname = predname + self.args = list(args) + + + @property + def negated(self): + return self._negated + + + @negated.setter + def negated(self, value): + self._negated = value + + + @property + def predname(self): + return self._predname + + + @predname.setter + def predname(self, prednames): + """ + predname is a list of predicate names, of which each is tested if it is None + """ + if self.mln is not None and any(self.mln.predicate(p) is None for p in prednames): + erroneouspreds = [p for p in prednames if self.mln.predicate(p) is None] + raise NoSuchPredicateError('Predicate{} {} is undefined.'.format('s' if len(erroneouspreds) > 1 else '', ', '.join(erroneouspreds))) + self._predname = prednames + + + @property + def lits(self): + return [Lit(self.negated, lit, self.args, self.mln) for lit in self.predname] + + + @property + def args(self): + return self._args + + + @args.setter + def args(self, args): + # arguments are identical for all predicates in group, so choose + # arbitrary predicate + predname = self.predname[0] + if self.mln is not None and len(args) != len(self.mln.predicate(predname).argdoms): + raise Exception('Illegal argument length: %s. %s requires %d arguments: %s' % (str(args), predname, + len(self.mln.predicate(predname).argdoms), + self.mln.predicate(predname).argdoms)) + self._args = args + + + def __str__(self): + return {True:'!', False:'', 2: '*'}[self.negated] + '|'.join(self.predname) + "(" + ",".join(self.args) + ")" + + + def cstr(self, color=False): + return {True:"!", False:"", 2:'*'}[self.negated] + colorize('|'.join(self.predname), predicate_color, color) + "(" + ",".join(self.args) + ")" + + + def latex(self): + return {True:r'\lnot ', False:'', 2: '*'}[self.negated] + latexsym('|'.join(self.predname)) + "(" + ",".join(map(latexsym, self.args)) + ")" + + + def vardoms(self, variables=None, constants=None): + if variables == None: + variables = {} + argdoms = self.mln.predicate(self.predname[0]).argdoms + if len(argdoms) != len(self.args): + raise Exception("Wrong number of parameters in '%s'; expected %d!" % (str(self), len(argdoms))) + for i, arg in enumerate(self.args): + if self.mln.logic.isvar(arg): + varname = arg + domain = argdoms[i] + if varname in variables and variables[varname] != domain and variables[varname] is not None: + raise Exception("Variable '%s' bound to more than one domain" % varname) + variables[varname] = domain + elif constants is not None: + domain = argdoms[i] + if domain not in constants: constants[domain] = [] + constants[domain].append(arg) + return variables + + + def template_variables(self, variables=None): + if variables == None: variables = {} + for i, arg in enumerate(self.args): + if self.mln.logic.istemplvar(arg): + varname = arg + pred = self.mln.predicate(self.predname[0]) + domain = pred.argdoms[i] + if varname in variables and variables[varname] != domain: + raise Exception("Variable '%s' bound to more than one domain" % varname) + variables[varname] = domain + return variables + + + def prednames(self, prednames=None): + if prednames is None: + prednames = [] + prednames.extend([p for p in self.predname if p not in prednames]) + return prednames + + + def _ground_template(self, assignment): + # args = map(lambda x: assignment.get(x, x), self.args) + if self.negated == 2: # template + return [self.mln.logic.lit(False, predname, self.args, mln=self.mln) for predname in self.predname] + \ + [self.mln.logic.lit(True, predname, self.args, mln=self.mln) for predname in self.predname] + else: + return [self.mln.logic.lit(self.negated, predname, self.args, mln=self.mln) for predname in self.predname] + + def copy(self, mln=None, idx=inherit): + return self.mln.logic.litgroup(self.negated, self.predname, self.args, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) + + + def truth(self, world): + return None + + + def mintruth(self, world): + raise Exception('LitGroups do not have a truth value. Ground the literal first.') + + + def maxtruth(self, world): + raise Exception('LitGroups do not have a truth value. Ground the literal first.') + + + def constants(self, constants=None): + if constants is None: constants = {} + for i, c in enumerate(self.params): + # domname = self.mln.predicate(self.predname).argdoms[i] + domname = self.mln.predicate(self.predname[0]).argdoms[i] + values = constants.get(domname, None) + if values is None: + values = [] + constants[domname] = values + if not self.mln.logic.isvar(c) and not c in values: values.append(c) + return constants + + + def simplify(self, world): + return self.mln.logic.litgroup(self.negated, self.predname, self.args, mln=self.mln, idx=self.idx) + + + def __eq__(self, other): + return str(self) == str(other) + + + def __ne__(self, other): + return not self == other + +cdef class GroundLit(Formula): + """ + Represents a ground literal. + """ + + + def __init__(self, gndatom, negated, mln, idx=None): + Formula.__init__(self, mln, idx) + self.gndatom = gndatom + #print('type={}, gndatom={}'.format(type(gndatom), gndatom)) + self.negated = negated + + + @property + def gndatom(self): + #print('retrieving gndatom {} of type {}'.format(self._gndatom, type(self._gndatom))) + return self._gndatom + + + @gndatom.setter + def gndatom(self, gndatom): + self._gndatom = gndatom + #print('setting gndatom {} of type {}'.format(self._gndatom, type(self._gndatom))) + + + @property + def negated(self): + #print('retrieving negated {} of type {}'.format(self._negated, type(self._negated))) + return self._negated + + + @negated.setter + def negated(self, negate): + self._negated = negate + #print('setting negated {} of type {}'.format(self._negated, type(self._negated))) + + + # Q(gsoc): is this property ever used? + @property + def predname(self): + return self.gndatom.predname + + # Q(gsoc): is this property ever used? + @property + def args(self): + return self.gndatom.args + + + #cdef float truth(self, list world): + cpdef truth(self, list world): + #print('\nworld is of type {} and world has length {}'.format(type(world), len(world))) + #for wi in world: + # print('\twi is of type {} and is {}'.format(type(wi), wi)) + #cdef float tv = self.gndatom.truth(world) + tv = self.gndatom.truth(world) + #print('tv is of type {} and tv is {}'.format(type(tv), tv)) + if tv is None: + return None + if self.negated: + return (1. - tv) + return tv + + + cpdef mintruth(self, list world): + truth = self.truth(world) + if truth is None: + return 0 + else: + return truth + + + cpdef maxtruth(self, list world): + truth = self.truth(world) + if truth is None: + return 1 + else: + return truth + + + def __str__(self): + return {True:"!", False:""}[self.negated] + str(self.gndatom) + + + def cstr(self, color=False): + return {True:"!", False:""}[self.negated] + self.gndatom.cstr(color) + + + def contains_gndatom(self, atomidx): + return (self.gndatom.idx == atomidx) + + + def vardoms(self, variables=None, constants=None): + return self.gndatom.vardoms(variables, constants) + + + def constants(self, constants=None): + if constants is None: constants = {} + for i, c in enumerate(self.gndatom.args): + domname = self.mln.predicates[self.gndatom.predname][i] + values = constants.get(domname, None) + if values is None: + values = [] + constants[domname] = values + if not c in values: values.append(c) + return constants + + + def gndatom_indices(self, l=None): + if l == None: l = [] + if self.gndatom.idx not in l: l.append(self.gndatom.idx) + return l + + + cpdef gndatoms(self, l=None): + if l == None: l = [] + if not self.gndatom in l: l.append(self.gndatom) + #print('GroundLit:gndatoms(common.pyx-1331) len={}'.format(len(l))) + return l + + + def ground(self, mrf, assignment, simplify=False, partial=False): + # always get the gnd atom from the mrf, so that + # formulas can be transferred between different MRFs + return self.mln.logic.gnd_lit(mrf.gndatom(str(self.gndatom)), self.negated, mln=self.mln, idx=self.idx) + + + def copy(self, mln=None, idx=inherit): + mln = ifnone(mln, self.mln) + if mln is not self.mln: + raise Exception('GroundLit cannot be copied among MLNs.') + return self.mln.logic.gnd_lit(self.gndatom, self.negated, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) + + + def simplify(self, world): + truth = self.truth(world) + if truth is not None: + return self.mln.logic.true_false(truth, mln=self.mln, idx=self.idx) + return self.mln.logic.gnd_lit(self.gndatom, self.negated, mln=self.mln, idx=self.idx) + + + def prednames(self, prednames=None): + if prednames is None: + prednames = [] + if self.gndatom.predname not in prednames: + prednames.append(self.gndatom.predname) + return prednames + + + def template_variables(self, variables=None): + return {} + + + def _ground_template(self, assignment): + return [self.mln.logic.gnd_lit(self.gndatom, self.negated, mln=self.mln)] + + + def __eq__(self, other): + return str(self) == str(other)#self.negated == other.negated and self.gndAtom == other.gndAtom + + + def __ne__(self, other): + return not self == other + +cdef class GroundAtom(): + """ + Represents a ground atom. + """ + + def __init__(self, predname, args, mln, idx=None): + self.predname = predname + #print("self.predname is {} of type {}".format(self.predname, type(self.predname))) + self.args = args + #print("self.args is {} of type {}".format(self.args, type(self.args))) + self.idx = idx + #print("self.idx is {} of type {}".format(self.idx, type(self.idx))) + self.mln = mln + #print("self.mln is {} of type {}".format(self.mln, type(self.mln))) + + + @property + def predname(self): + return self._predname + + + @predname.setter + def predname(self, predname): + self._predname = predname + + + @property + def args(self): + return self._args + + + @args.setter + def args(self, args): + self._args = args + + + @property + def idx(self): + return self._idx + + + @idx.setter + def idx(self, idx): + #print('idx is of type {} and has value {}'.format(type(idx), idx)) + self._idx = idx + + + cpdef truth(self, list world): + return world[self.idx] + + + cpdef mintruth(self, list world): + truth = self.truth(world) + if truth is None: + return 0 + else: + return truth + + + cpdef maxtruth(self, list world): + truth = self.truth(world) + if truth is None: + return 1 + else: + return truth + + + def __repr__(self): + return '' % str(self) + + + # Q(gsoc): needs speedup ... + def __str__(self): + return "%s(%s)" % (self.predname, ",".join(self.args)) + + + def cstr(self, color=False): + return "%s(%s)" % (colorize(self.predname, predicate_color, color), ",".join(self.args)) + + + def prednames(self, prednames=None): + if prednames is None: + prednames = [] + if self.predname not in prednames: + prednames.append(self.predname) + return prednames + + + def vardoms(self, variables=None, constants=None): + if variables is None: + variables = {} + if constants is None: + constants = {} + for d, c in zip(self.args, self.mln.predicate(self.predname).argdoms): + if d not in constants: + constants[d] = [] + if c not in constants[d]: + constants[d].append(c) + return variables + + + def __eq__(self, other): + return (self.predname == other.predname) and (self.args == other.args) + #return str(self) == str(other) + + def __ne__(self, other): + return not self == other + +cdef class Equality(ComplexFormula): + """ + Represents (in)equality constraints between two symbols. + """ + + + def __init__(self, args, negated, mln, idx=None): + ComplexFormula.__init__(self, mln, idx) + self.args = args + self.negated = negated + + + @property + def args(self): + return [self._argsA, self._argsB] + #return self._args + + + @args.setter + def args(self, args): + if len(args) != 2: + raise Exception('Illegal number of arguments of equality: %d' % len(args)) + #self._args = args + self._argsA = args[0] + self._argsB = args[1] + + + @property + def negated(self): + return self._negated + + @negated.setter + def negated(self, negate): + self._negated = negate + + + def __str__(self): + return "%s%s%s" % (str(self.args[0]), '=/=' if self.negated else '=', str(self.args[1])) + + + def cstr(self, color=False): + return str(self) + + + def latex(self): + return "%s%s%s" % (latexsym(self.args[0]), r'\neq ' if self.negated else '=', latexsym(self.args[1])) + + + def ground(self, mrf, assignment, simplify=False, partial=False): + # if the parameter is a variable, do a lookup (it must be bound by now), + # otherwise it's a constant which we can use directly + args = [assignment.get(x, x) for x in self.args] + if self.mln.logic.isvar(args[0]) or self.mln.logic.isvar(args[1]): + if partial: + return self.mln.logic.equality(args, self.negated, mln=self.mln) + else: raise Exception("At least one variable was not grounded in '%s'!" % str(self)) + if simplify: + equal = (args[0] == args[1]) + return self.mln.logic.true_false(1 if {True: not equal, False: equal}[self.negated] else 0, mln=self.mln, idx=self.idx) + else: + return self.mln.logic.equality(args, self.negated, mln=self.mln, idx=self.idx) + + + def copy(self, mln=None, idx=inherit): + return self.mln.logic.equality(self.args, self.negated, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) + + + def _ground_template(self, assignment): + return [self.mln.logic.equality(self.args, negated=self.negated, mln=self.mln)] + + + def template_variables(self, variables=None): + return variables + + + def vardoms(self, variables=None, constants=None): + if variables is None: + variables = {} + if self.mln.logic.isvar(self.args[0]) and self.args[0] not in variables: variables[self.args[0]] = None + if self.mln.logic.isvar(self.args[1]) and self.args[1] not in variables: variables[self.args[1]] = None + return variables + + + def vardom(self, varname): + return None + + + def vardomain_from_formula(self, formula): + f_var_domains = formula.vardoms() + eq_vars = self.vardoms() + for var_ in eq_vars: + if var_ not in f_var_domains: + raise Exception('Variable %s not bound to a domain by formula %s' % (var_, fstr(formula))) + eq_vars[var_] = f_var_domains[var_] + return eq_vars + + + def prednames(self, prednames=None): + if prednames is None: + prednames = [] + return prednames + + + cpdef truth(self, world=None): + #print('\nargs type={} , args={}'.format(type(self.args), self.args)) + #print('negated type={} , negated={}'.format(type(self.negated), self.negated)) + if any(map(self.mln.logic.isvar, self.args)): + return None + cdef int equals + equals = 1 if (self._argsA == self._argsB) else 0 + #print('equals is of type {} and has value {}'.format(type(equals), equals)) + return (1 - equals) if self.negated else equals + + cpdef int maxtruth(self, world): + if any(map(self.mln.logic.isvar, self.args)): + return 1 + cdef int equals + equals = 1 if (self._argsA == self._argsB) else 0 + return (1 - equals) if self.negated else equals + + cpdef int mintruth(self, world): + if any(map(self.mln.logic.isvar, self.args)): + return 0 + cdef int equals + equals = 1 if (self._argsA == self._argsB) else 0 + return (1 - equals) if self.negated else equals + + + def simplify(self, world): + truth = self.truth(world) + if truth != None: return self.mln.logic.true_false(truth, mln=self.mln, idx=self.idx) + return self.mln.logic.equality(list(self.args), negated=self.negated, mln=self.mln, idx=self.idx) + +cdef class Implication(ComplexFormula): + """ + Represents an implication + """ + + + def __init__(self, children, mln, idx=None): + Formula.__init__(self, mln, idx) + self.children = children + + @property + def children(self): + return self._children + + @children.setter + def children(self, children): + if len(children) != 2: + raise Exception('Implication needs exactly 2 children (antescedant and consequence)') + self._children = children + + + def __str__(self): + c1 = self.children[0] + c2 = self.children[1] + return (str(c1) if not isinstance(c1, ComplexFormula) \ + else '(%s)' % str(c1)) + " => " + (str(c2) if not isinstance(c2, ComplexFormula) else '(%s)' % str(c2)) + + + def cstr(self, color=False): + c1 = self.children[0] + c2 = self.children[1] + (s1, s2) = (c1.cstr(color), c2.cstr(color)) + (s1, s2) = (('(%s)' if isinstance(c1, ComplexFormula) else '%s') % s1, ('(%s)' if isinstance(c2, ComplexFormula) else '%s') % s2) + return '%s => %s' % (s1, s2) + + + def latex(self): + return self.children[0].latex() + r" \rightarrow " + self.children[1].latex() + + + def cnf(self, level=0): + return self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln, idx=self.idx), self.children[1]], mln=self.mln, idx=self.idx).cnf(level+1) + + + def nnf(self, level=0): + return self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln, idx=self.idx), self.children[1]], mln=self.mln, idx=self.idx).nnf(level+1) + + + def simplify(self, world): + return self.mln.logic.disjunction([Negation([self.children[0]], mln=self.mln, idx=self.idx), self.children[1]], mln=self.mln, idx=self.idx).simplify(world) + +cdef class Biimplication(ComplexFormula): + """ + Represents a bi-implication. + """ + + + def __init__(self, children, mln, idx=None): + Formula.__init__(self, mln, idx) + self.children = children + + + @property + def children(self): + return self._children + + + @children.setter + def children(self, children): + if len(children) != 2: + raise Exception('Biimplication needs exactly 2 children') + self._children = children + + + def __str__(self): + c1 = self.children[0] + c2 = self.children[1] + return (str(c1) if not isinstance(c1, ComplexFormula) \ + else '(%s)' % str(c1)) + " <=> " + (str(c2) if not isinstance(c2, ComplexFormula) else str(c2)) + + + def cstr(self, color=False): + c1 = self.children[0] + c2 = self.children[1] + (s1, s2) = (c1.cstr(color), c2.cstr(color)) + (s1, s2) = (('(%s)' if isinstance(c1, ComplexFormula) else '%s') % s1, ('(%s)' if isinstance(c2, ComplexFormula) else '%s') % s2) + return '%s <=> %s' % (s1, s2) + + + def latex(self): + return r'%s \leftrightarrow %s' % (self.children[0].latex(), self.children[1].latex()) + + + def cnf(self, level=0): + cnf = self.mln.logic.conjunction([self.mln.logic.implication([self.children[0], self.children[1]], mln=self.mln, idx=self.idx), + self.mln.logic.implication([self.children[1], self.children[0]], mln=self.mln, idx=self.idx)], mln=self.mln, idx=self.idx) + return cnf.cnf(level+1) + + + def nnf(self, level = 0): + return self.mln.logic.conjunction([self.mln.logic.implication([self.children[0], self.children[1]], mln=self.mln, idx=self.idx), + self.mln.logic.implication([self.children[1], self.children[0]], mln=self.mln, idx=self.idx)], mln=self.mln, idx=self.idx).nnf(level+1) + + + def simplify(self, world): + c1 = self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln), self.children[1]], mln=self.mln) + c2 = self.mln.logic.disjunction([self.children[0], self.mln.logic.negation([self.children[1]], mln=self.mln)], mln=self.mln) + return self.mln.logic.conjunction([c1,c2], mln=self.mln, idx=self.idx).simplify(world) + +cdef class Negation(ComplexFormula): + """ + Represents a negation of a complex formula. + """ + + def __init__(self, children, mln, idx=None): + ComplexFormula.__init__(self, mln, idx) + if hasattr(children, '__iter__'): + assert len(children) == 1 + else: + children = [children] + self.children = children + + + @property + def children(self): + return self._children + + @children.setter + def children(self, children): + if hasattr(children, '__iter__'): + if len(children) != 1: + raise Exception('Negation may have only 1 child.') + else: + children = [children] + self._children = children + + + def __str__(self): + return ('!(%s)' if isinstance(self.children[0], ComplexFormula) else '!%s') % str(self.children[0]) + + + def cstr(self, color=False): + return ('!(%s)' if isinstance(self.children[0], ComplexFormula) else '!%s') % self.children[0].cstr(color) + + + def latex(self): + return r'\lnot (%s)' % self.children[0].latex() + + + cpdef truth(self, list world): + childValue = self.children[0].truth(world) + if childValue is None: + return None + return 1 - childValue + + + def cnf(self, level=0): + # convert the formula that is negated to negation normal form (NNF), + # so that if it's a complex formula, it will be either a disjunction + # or conjunction, to which we can then apply De Morgan's law. + # Note: CNF conversion would be unnecessarily complex, and, + # when the children are negated below, most of it would be for nothing! + child = self.children[0].nnf(level+1) + # apply negation to child (pull inwards) + if hasattr(child, 'children'): + neg_children = [] + for c in child.children: + neg_children.append(self.mln.logic.negation([c], mln=self.mln, idx=None).cnf(level+1)) + if isinstance(child, Conjunction): + return self.mln.logic.disjunction(neg_children, mln=self.mln, idx=self.idx).cnf(level+1) + elif isinstance(child, Disjunction): + return self.mln.logic.conjunction(neg_children, mln=self.mln, idx=self.idx).cnf(level+1) + elif isinstance(child, Negation): + return c.cnf(level+1) + else: + raise Exception("Unexpected child type %s while converting '%s' to CNF!" % (str(type(child)), str(self))) + elif isinstance(child, Lit): + return self.mln.logic.lit(not child.negated, child.predname, child.args, mln=self.mln, idx=self.idx) + elif isinstance(child, LitGroup): + return self.mln.logic.litgroup(not child.negated, child.predname, child.args, mln=self.mln, idx=self.idx) + elif isinstance(child, GroundLit): + return self.mln.logic.gnd_lit(child.gndatom, not child.negated, mln=self.mln, idx=self.idx) + elif isinstance(child, TrueFalse): + return self.mln.logic.true_false(1 - child.value, mln=self.mln, idx=self.idx) + elif isinstance(child, Equality): + return self.mln.logic.equality(child.params, not child.negated, mln=self.mln, idx=self.idx) + else: + raise Exception("CNF conversion of '%s' failed (type:%s)" % (str(self), str(type(child)))) + + + def nnf(self, level = 0): + # child is the formula that is negated + child = self.children[0].nnf(level+1) + # apply negation to the children of the formula that is negated (pull inwards) + # - complex formula (should be disjunction or conjunction at this point), use De Morgan's law + if hasattr(child, 'children'): + neg_children = [] + for c in child.children: + neg_children.append(self.mln.logic.negation([c], mln=self.mln, idx=None).nnf(level+1)) + if isinstance(child, Conjunction): # !(A ^ B) = !A v !B + return self.mln.logic.disjunction(neg_children, mln=self.mln, idx=self.idx).nnf(level+1) + elif isinstance(child, Disjunction): # !(A v B) = !A ^ !B + return self.mln.logic.conjunction(neg_children, mln=self.mln, idx=self.idx).nnf(level+1) + elif isinstance(child, Negation): + return c.nnf(level+1) + # !(A => B) = A ^ !B + # !(A <=> B) = (A ^ !B) v (B ^ !A) + else: + raise Exception("Unexpected child type %s while converting '%s' to NNF!" % (str(type(child)), str(self))) + # - non-complex formula, i.e. literal or constant + elif isinstance(child, Lit): + return self.mln.logic.lit(not child.negated, child.predname, child.args, mln=self.mln, idx=self.idx) + elif isinstance(child, LitGroup): + return self.mln.logic.litgroup(not child.negated, child.predname, child.args, mln=self.mln, idx=self.idx) + elif isinstance(child, GroundLit): + return self.mln.logic.gnd_lit(child.gndatom, not child.negated, mln=self.mln, idx=self.idx) + elif isinstance(child, TrueFalse): + return self.mln.logic.true_false(1 - child.value, mln=self.mln, idx=self.idx) + elif isinstance(child, Equality): + return self.mln.logic.equality(child.args, not child.negated, mln=self.mln, idx=self.idx) + else: + raise Exception("NNF conversion of '%s' failed (type:%s)" % (str(self), str(type(child)))) + + + def simplify(self, world): + f = self.children[0].simplify(world) + if isinstance(f, TrueFalse): + return f.invert() + else: + return self.mln.logic.negation([f], mln=self.mln, idx=self.idx) + +cdef class Exist(ComplexFormula): + """ + Existential quantifier. + """ + + + def __init__(self, variables, formula, mln, idx=None): + Formula.__init__(self, mln, idx) + self.formula = formula + self.vars = variables + + + @property + def children(self): + return self._children + + @children.setter + def children(self, children): + if len(children) != 1: + raise Exception('Illegal number of formulas in Exist: %s' % str(children)) + self._children = children + + + @property + def formula(self): + return self._children[0] + + @formula.setter + def formula(self, f): + self._children = [f] + + @property + def vars(self): + return self._vars + + @vars.setter + def vars(self, v): + self._vars = v + + + def __str__(self): + return 'EXIST %s (%s)' % (', '.join(self.vars), str(self.formula)) + + + def cstr(self, color=False): + return colorize('EXIST ', predicate_color, color) + ', '.join(self.vars) + ' (' + self.formula.cstr(color) + ')' + + + def latex(self): + return '\exists\ %s (%s)' % (', '.join(map(latexsym, self.vars)), self.formula.latex()) + + + def vardoms(self, variables=None, constants=None): + if variables == None: + variables = {} + # get the child's variables: + newvars = self.formula.vardoms(None, constants) + # remove the quantified variable(s) + for var in self.vars: + try: del newvars[var] + except: + raise Exception("Variable '%s' in '%s' not bound to a domain!" % (var, str(self))) + # add the remaining ones that are not None and return + variables.update(dict([(k, v) for k, v in newvars.items() if v is not None])) + return variables + + + def ground(self, mrf, assignment, partial=False, simplify=False): + # find out variable domains + vardoms = self.formula.vardoms() + if not set(self.vars).issubset(vardoms): + raise Exception('One or more variables do not appear in formula: %s' % str(set(self.vars).difference(vardoms))) + variables = dict([(k,v) for k,v in vardoms.items() if k in self.vars]) + # ground + gndings = [] + self._ground(self.children[0], variables, assignment, gndings, mrf, partial=partial) + if len(gndings) == 1: + return gndings[0] + if not gndings: + return self.mln.logic.true_false(0, mln=self.mln, idx=self.idx) + disj = self.mln.logic.disjunction(gndings, mln=self.mln, idx=self.idx) + if simplify: + return disj.simplify(mrf.evidence) + else: + return disj + + + def _ground(self, formula, variables, assignment, gndings, mrf, partial=False): + # if all variables have been grounded... + if variables == {}: + gndFormula = formula.ground(mrf, assignment, partial=partial) + gndings.append(gndFormula) + return + # ground the first variable... + varname,domname = variables.popitem() + for value in mrf.domains[domname]: # replacing it with one of the constants + assignment[varname] = value + # recursive descent to ground further variables + self._ground(formula, dict(variables), assignment, gndings, mrf, partial=partial) + + + def copy(self, mln=None, idx=inherit): + return self.mln.logic.exist(self.vars, self.formula, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) + + + def cnf(self,l=0): + raise Exception("'%s' cannot be converted to CNF. Ground this formula first!" % str(self)) + + + def truth(self, w): + raise Exception("'%s' does not implement truth()" % self.__class__.__name__) + +cdef class TrueFalse(Formula): + """ + Represents constant truth values. + """ + + def __init__(self, truth, mln, idx=None): + Formula.__init__(self, mln, idx) + self.value = truth + + # Q(gsoc): where is this property used? GenericSystemTest doesn't lead to any print outputs + @property + def value(self): + #print('getting value {} of type {}'.format(self._value, type(self._value))) + return self._value + + # Q(gsoc): is this addition semantically correct? + @value.setter + def value(self, value): + self._value = value + #print('setting value {} of type {}'.format(self._value, type(self._value))) + + def cstr(self, color=False): + return str(self) + + cpdef float truth(self, world=None): + #print('getting value type {} = {}'.format(type(self.value), self.value)) + return self.value + + cpdef mintruth(self, world=None): + return self.truth + + cpdef maxtruth(self, world=None): + return self.truth + + def invert(self): + return self.mln.logic.true_false(1 - self.truth(), mln=self.mln, idx=self.idx) + + def simplify(self, world): + return self.copy() + + def vardoms(self, variables=None, constants=None): + if variables is None: + variables = {} + return variables + + def ground(self, mln, assignment, simplify=False, partial=False): + return self.mln.logic.true_false(self.value, mln=self.mln, idx=self.idx) + + def copy(self, mln=None, idx=inherit): + return self.mln.logic.true_false(self.value, mln=ifnone(mln, self.mln), idx=self.idx if idx is inherit else idx) + +cdef class NonLogicalConstraint(Constraint): + """ + A constraint that is not somehow made up of logical connectives and (ground) atoms. + """ + + def template_variants(self, mln): + # non logical constraints are never templates; therefore, there is just one variant, the constraint itself + return [self] + + def islogical(self): + return False + + def negate(self): + raise Exception("%s does not implement negate()" % str(type(self))) + +cdef class CountConstraint(NonLogicalConstraint): + """ + A constraint that tests the number of relation instances against an integer. + """ + + def __init__(self, predicate, predicate_params, fixed_params, op, count): + """op: an operator; one of "=", "<=", ">=" """ + self.literal = self.mln.logic.lit(False, predicate, predicate_params) + self.fixed_params = fixed_params + self.count = count + if op == "=": op = "==" + self.op = op + + def __str__(self): + op = self.op + if op == "==": op = "=" + return "count(%s | %s) %s %d" % (str(self.literal), ", ".join(self.fixed_params), op, self.count) + + def cstr(self, color=False): + return str(self) + + def iterGroundings(self, mrf, simplify=False): + a = {} + other_params = [] + for param in self.literal.params: + if param[0].isupper(): + a[param] = param + else: + if param not in self.fixed_params: + other_params.append(param) + #other_params = list(set(self.literal.params).difference(self.fixed_params)) + # for each assignment of the fixed parameters... + for assignment in self._iterAssignment(mrf, list(self.fixed_params), a): + gndAtoms = [] + # generate a count constraint with all the atoms we obtain by grounding the other params + for full_assignment in self._iterAssignment(mrf, list(other_params), assignment): + gndLit = self.literal.ground(mrf, full_assignment, None) + gndAtoms.append(gndLit.gndAtom) + yield self.mln.logic.gnd_count_constraint(gndAtoms, self.op, self.count), [] + + def _iterAssignment(self, mrf, variables, assignment): + """iterates over all possible assignments for the given variables of this constraint's literal + variables: the variables that are still to be grounded""" + # if all variables have been grounded, we have the complete assigment + if len(variables) == 0: + yield dict(assignment) + return + # otherwise one of the remaining variables in the list... + varname = variables.pop() + domName = self.literal.getVarDomain(varname, mrf.mln) + for value in mrf.domains[domName]: # replacing it with one of the constants + assignment[varname] = value + # recursive descent to ground further variables + for a in self._iterAssignment(mrf, variables, assignment): + yield a + + def getVariables(self, mln, variables = None, constants = None): + if constants is not None: + self.literal.getVariables(mln, variables, constants) + return variables + +cdef class GroundCountConstraint(NonLogicalConstraint): + def __init__(self, gndAtoms, op, count): + self.gndAtoms = gndAtoms + self.count = count + self.op = op + + def isTrue(self, world_values): + c = 0 + for ga in self.gndAtoms: + if(world_values[ga.idx]): + c += 1 + return eval("c %s self.count" % self.op) + + def __str__(self): + op = self.op + if op == "==": op = "=" + return "count(%s) %s %d" % (";".join(map(str, self.gndAtoms)), op, self.count) + + def cstr(self, color=False): + op = self.op + if op == "==": op = "=" + return "count(%s) %s %d" % (";".join([c.cstr(color) for c in self.gndAtoms]), op, self.count) + + def negate(self): + if self.op == "==": + self.op = "!=" + elif self.op == "!=": + self.op = "==" + elif self.op == ">=": + self.op = "<=" + self.count -= 1 + elif self.op == "<=": + self.op = ">=" + self.count += 1 + + def idxGroundAtoms(self, l = None): + if l is None: l = [] + for ga in self.gndAtoms: + l.append(ga.idx) + return l + + def getGroundAtoms(self, l = None): + if l is None: l = [] + for ga in self.gndAtoms: + l.append(ga) + return l + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +cdef class Logic(): + """ + Abstract factory class for instantiating logical constructs like conjunctions, + disjunctions etc. Every specifc logic should implement the methods and return + an instance of the respective element. They also might override the respective + implementations and behavior of the logic. + """ + + def __init__(self, grammar, mln): + """ + Creates a new instance of a Logic factory class. + + :param grammar: an instance of grammar.Grammar + :param mln: the MLN instance that the logic shall be tied to. + """ + if grammar not in ('StandardGrammar', 'PRACGrammar'): + raise Exception('Invalid grammar: %s' % grammar) + self.grammar = eval(grammar)(self) + self.mln = mln + #self.Constraint = Constraint(mln) + #self.Formula = Formula(mln) + #self.ComplexFormula = ComplexFormula(mln) + #self.Conjunction = Conjunction(mln) # children = ? + #self.Disjunction = Disjunction(mln) # children = ? + #self.Lit = Lit() + + + def __getstate__(self): + d = self.__dict__.copy() + d['grammar'] = type(self.grammar).__name__ + return d + + def __setstate__(self, d): + self.__dict__ = d + self.grammar = eval(d['grammar'])(self) + + def isvar(self, identifier): + """ + Returns True if identifier is a logical variable according + to the used grammar, and False otherwise. + """ + return self.grammar.isvar(identifier) + + def isconstant(self, identifier): + """ + Returns True if identifier is a logical constant according + to the used grammar, and False otherwise. + """ + return self.grammar.isConstant(identifier) + + def istemplvar(self, s): + """ + Returns True if `s` is template variable or False otherwise. + """ + return self.grammar.istemplvar(s) + + def parse_formula(self, formula): + """ + Returns the Formula object parsed by the grammar. + """ + return self.grammar.parse_formula(formula) + + def parse_predicate(self, string): + return self.grammar.parse_predicate(string) + + def parse_atom(self, string): + return self.grammar.parse_atom(string) + + def parse_domain(self, decl): + return self.grammar.parse_domain(decl) + + def parse_literal(self, lit): + return self.grammar.parse_literal(lit) + + # Q(gsoc): possible modifications required upon removing inner classes ... + def islit(self, f): + """ + Determines whether or not a formula is a literal. + """ + return isinstance(f, GroundLit) or isinstance(f, Lit) or isinstance(f, GroundAtom) + + # Q(gsoc): possible modifications required upon removing inner classes ... + def iseq(self, f): + """ + Determines wheter or not a formula is an equality consttaint. + """ + return isinstance(f, Equality) + + # Q(gsoc): possible modifications required upon removing inner classes ... + def islitconj(self, f): + """ + Returns true if the given formula is a conjunction of literals. + """ + if self.islit(f): return True + if not isinstance(f, Conjunction): + if not isinstance(f, Lit) and \ + not isinstance(f, GroundLit) and \ + not isinstance(f, Equality) and \ + not isinstance(f, TrueFalse): + return False + return True + for child in f.children: + if not isinstance(child, Lit) and \ + not isinstance(child, GroundLit) and \ + not isinstance(child, Equality) and \ + not isinstance(child, TrueFalse): + return False + return True + + # Q(gsoc): possible modifications required upon removing inner classes ... + def isclause(self, f): + """ + Returns true if the given formula is a clause (a disjunction of literals) + """ + if self.islit(f): return True + if not isinstance(f, Disjunction): + if not isinstance(f, Lit) and \ + not isinstance(f, GroundLit) and \ + not isinstance(f, Equality) and \ + not isinstance(f, TrueFalse): + return False + return True + for child in f.children: + if not isinstance(child, Lit) and \ + not isinstance(child, GroundLit) and \ + not isinstance(child, Equality) and \ + not isinstance(child, TrueFalse): + return False + return True + + # Q(gsoc): possible modifications required upon removing inner classes ... + def negate(self, formula): + """ + Returns a negation of the given formula. + + The original formula will be copied first. The resulting negation is tied + to the same mln and will have the same formula index. Also performs + a rudimentary simplification in case of `formula` is a (ground) literal + or equality. + """ + if isinstance(formula, Lit) or isinstance(formula, GroundLit): + ret = formula.copy() + ret.negated = not ret.negated + elif isinstance(formula, Equality): + ret = formula.copy() + ret.negated = not ret.negated + else: + ret = self.negation([formula.copy(mln=formula.mln, idx=None)], mln=formula.mln, idx=formula.idx) + return ret + + def conjugate(self, children, mln=None, idx=inherit): + """ + Returns a conjunction of the given children. + + Performs rudimentary simplification in the sense that if children + has only one element, it returns this element (e.g. one literal) + """ + if not children: + return self.true_false(0, mln=ifnone(mln, self.mln), idx=idx) + elif len(children) == 1: + return children[0].copy(mln=ifnone(mln, self.mln), idx=idx) + else: + return self.conjunction(children, mln=ifnone(mln,self.mln), idx=idx) + + def disjugate(self, children, mln=None, idx=inherit): + """ + Returns a conjunction of the given children. + + Performs rudimentary simplification in the sense that if children + has only one element, it returns this element (e.g. one literal) + """ + if not children: + return self.true_false(0, mln=ifnone(mln, self.mln), idx=idx) + elif len(children) == 1: + return children[0].copy(mln=ifnone(mln, self.mln), idx=idx) + else: + return self.disjunction(children, mln=ifnone(mln,self.mln), idx=idx) + + @staticmethod + def iter_eq_varassignments(eq, f, mln): + """ + Iterates over all variable assignments of an (in)equality constraint. + + Needs a formula since variables in equality constraints are not typed per se. + """ + doms = f.vardoms() + eqVars_ = eq.vardoms() + if not set(eqVars_).issubset(doms): + raise Exception('Variable in (in)equality constraint not bound to a domain: %s' % eq) + eqVars = {} + for v in eqVars_: + eqVars[v] = doms[v] + for assignment in Logic._iter_eq_varassignments(mln, eqVars, {}): + yield assignment + + @staticmethod + def _iter_eq_varassignments(mrf, variables, assignment): + if len(variables) == 0: + yield assignment + return + variables = dict(variables) + variable, domName = variables.popitem() + domain = mrf.domains[domName] + for value in domain: + for assignment in Logic._iter_eq_varassignments(mrf, variables, dict_union(assignment, {variable: value})): + yield assignment + + @staticmethod + def clauseset(cnf): + """ + Takes a formula in CNF and returns a set of clauses, i.e. a list of sets + containing literals. All literals are converted into strings. + """ + clauses = [] + if isinstance(cnf, Disjunction): + clauses.append(set(map(str, cnf.children))) + elif isinstance(cnf, Conjunction): + for disj in cnf.children: + clause = set() + clauses.append(clause) + if isinstance(disj, Disjunction): + for c in disj.children: + clause.add(str(c)) + else: + clause.add(str(disj)) + else: + clauses.append(set([str(cnf)])) + return clauses + + @staticmethod + def cnf(gfs, formulas, logic, allpos=False): + """ + convert the given ground formulas to CNF + if allPositive=True, then formulas with negative weights are negated to make all weights positive + @return a new pair (gndformulas, formulas) + + .. warning:: + + If allpos is True, this might have side effects on the formula weights of the MLN. + + """ + # get list of formula indices which we must negate + formulas_ = [] + negated = [] + if allpos: + for f in formulas: + if f.weight < 0: + negated.append(f.idx) + f = logic.negate(f) + f.weight = -f.weight + formulas_.append(f) + # get CNF version of each ground formula + gfs_ = [] + for gf in gfs: + # non-logical constraint + if not gf.islogical(): # don't apply any transformations to non-logical constraints + if gf.idx in negated: + gf.negate() + gfs_.append(gf) + continue + # logical constraint + if gf.idx in negated: + cnf = logic.negate(gf).cnf() + else: + cnf = gf.cnf() + if isinstance(cnf, TrueFalse): # formulas that are always true or false can be ignored + continue + cnf.idx = gf.idx + gfs_.append(cnf) + # return modified formulas + return gfs_, formulas_ + + def conjunction(self, *args, **kwargs): + """ + Returns a new instance of a Conjunction object. + """ + raise Exception('%s does not implement conjunction()' % str(type(self))) + + def disjunction(self, *args, **kwargs): + """ + Returns a new instance of a Disjunction object. + """ + raise Exception('%s does not implement disjunction()' % str(type(self))) + + def negation(self, *args, **kwargs): + """ + Returns a new instance of a Negation object. + """ + raise Exception('%s does not implement negation()' % str(type(self))) + + def implication(self, *args, **kwargs): + """ + Returns a new instance of a Implication object. + """ + raise Exception('%s does not implement implication()' % str(type(self))) + + def biimplication(self, *args, **kwargs): + """ + Returns a new instance of a Biimplication object. + """ + raise Exception('%s does not implement biimplication()' % str(type(self))) + + def equality(self, *args, **kwargs): + """ + Returns a new instance of a Equality object. + """ + raise Exception('%s does not implement equality()' % str(type(self))) + + def exist(self, *args, **kwargs): + """ + Returns a new instance of a Exist object. + """ + raise Exception('%s does not implement exist()' % str(type(self))) + + def gnd_atom(self, *args, **kwargs): + """ + Returns a new instance of a GndAtom object. + """ + raise Exception('%s does not implement gnd_atom()' % str(type(self))) + + def lit(self, *args, **kwargs): + """ + Returns a new instance of a Lit object. + """ + raise Exception('%s does not implement lit()' % str(type(self))) + + def litgroup(self, *args, **kwargs): + """ + Returns a new instance of a Lit object. + """ + raise Exception('%s does not implement litgroup()' % str(type(self))) + + def gnd_lit(self, *args, **kwargs): + """ + Returns a new instance of a GndLit object. + """ + raise Exception('%s does not implement gnd_lit()' % str(type(self))) + + def count_constraint(self, *args, **kwargs): + """ + Returns a new instance of a CountConstraint object. + """ + raise Exception('%s does not implement count_constraint()' % str(type(self))) + + def true_false(self, *args, **kwargs): + """ + Returns a new instance of a TrueFalse constant object. + """ + raise Exception('%s does not implement true_false()' % str(type(self))) + + def create(self, clazz, *args, **kwargs): + """ + Takes the type of a logical element (class type) and creates + a new instance of it. + """ + return clazz(*args, **kwargs) + + + + +# this is a little hack to make nested classes pickleable +# Constraint = Logic.Constraint +# Formula = Logic.Formula +# ComplexFormula = Logic.ComplexFormula +# Conjunction = Logic.Conjunction +# Disjunction = Logic.Disjunction +# Lit = Logic.Lit +# LitGroup = Logic.LitGroup +# GroundLit = Logic.GroundLit +# GroundAtom = Logic.GroundAtom +# Equality = Logic.Equality +# Implication = Logic.Implication +# Biimplication = Logic.Biimplication +# Negation = Logic.Negation +# Exist = Logic.Exist +# TrueFalse = Logic.TrueFalse +# NonLogicalConstraint = Logic.NonLogicalConstraint +# CountConstraint = Logic.CountConstraint +# GroundCountConstraint = Logic.GroundCountConstraint diff --git a/python3/pracmln/logic/fol.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/logic/fol.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..62fb75e8 --- /dev/null +++ b/python3/pracmln/logic/fol.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/logic/fol.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/logic/fol.pxd b/python3/pracmln/logic/fol.pxd new file mode 100644 index 00000000..b11a5532 --- /dev/null +++ b/python3/pracmln/logic/fol.pxd @@ -0,0 +1,79 @@ +from .common cimport Logic +from .common cimport Constraint as Super_Constraint +from .common cimport Formula as Super_Formula +from .common cimport ComplexFormula as Super_ComplexFormula +from .common cimport Conjunction as Super_Conjunction +from .common cimport Disjunction as Super_Disjunction +from .common cimport Lit as Super_Lit +from .common cimport LitGroup as Super_LitGroup +from .common cimport GroundLit as Super_GroundLit +from .common cimport GroundAtom as Super_GroundAtom +from .common cimport Equality as Super_Equality +from .common cimport Implication as Super_Implication +from .common cimport Biimplication as Super_Biimplication +from .common cimport Negation as Super_Negation +from .common cimport Exist as Super_Exist +from .common cimport TrueFalse as Super_TrueFalse +from .common cimport NonLogicalConstraint as Super_NonLogicalConstraint +from .common cimport CountConstraint as Super_CountConstraint +from .common cimport GroundCountConstraint as Super_GroundCountConstraint + + + +cdef class Constraint(Super_Constraint): + pass + +cdef class Formula(Super_Formula): + pass + +cdef class ComplexFormula(Super_ComplexFormula): + pass + +cdef class Lit(Super_Lit): + pass + +cdef class LitGroup(Super_LitGroup): + pass + +cdef class GroundAtom(Super_GroundAtom): + pass + +cdef class GroundLit(Super_GroundLit): + pass + +cdef class Disjunction(Super_Disjunction): + pass + +cdef class Conjunction(Super_Conjunction): + pass + +cdef class Implication(Super_Implication): + pass + +cdef class Biimplication(Super_Biimplication): + pass + +cdef class Negation(Super_Negation): + pass + +cdef class Exist(Super_Exist): + pass + +cdef class Equality(Super_Equality): + pass + +cdef class TrueFalse(Super_TrueFalse): + pass + +cdef class ProbabilityConstraint(): + pass + +cdef class PriorConstraint(ProbabilityConstraint): + pass + +cdef class PosteriorConstraint(ProbabilityConstraint): + pass + + +cdef class FirstOrderLogic(Logic): + pass diff --git a/python3/pracmln/logic/fol.py b/python3/pracmln/logic/fol.py deleted file mode 100644 index 6f73791b..00000000 --- a/python3/pracmln/logic/fol.py +++ /dev/null @@ -1,381 +0,0 @@ -# FIRST-ORDER LOGIC -- PROCESSING -# -# (C) 2013 by Daniel Nyga (nyga@cs.uni-bremen.de) -# (C) 2007-2012 by Dominik Jain -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -from dnutils import ifnone - -from .common import Logic -from ..mln.util import fstr - - -class FirstOrderLogic(Logic): - """ - Factory class for first-order logic. - """ - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Constraint(Logic.Constraint): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Formula(Logic.Formula, Constraint): - - def noisyor(self, world): - """ - Computes the noisy-or distribution of this formula. - """ - return self.cnf().noisyor(world) - - - def _getEvidenceTruthDegreeCW(self, gndAtom, worldValues): - """ - gets (soft or hard) evidence as a degree of belief from 0 to 1, making the closed world assumption, - soft evidence has precedence over hard evidence - """ - se = self._getSoftEvidence(gndAtom) - if se is not None: - return se if (True == worldValues[gndAtom.idx] or None == worldValues[gndAtom.idx]) else 1.0 - se # TODO allSoft currently unsupported - return 1.0 if worldValues[gndAtom.idx] else 0.0 - - - def _noisyOr(self, mln, worldValues, disj): - if isinstance(disj, FirstOrderLogic.GroundLit): - lits = [disj] - elif isinstance(disj, FirstOrderLogic.TrueFalse): - return disj.isTrue(worldValues) - else: - lits = disj.children - prod = 1.0 - for lit in lits: - p = mln._getEvidenceTruthDegreeCW(lit.gndAtom, worldValues) - if not lit.negated: - factor = p - else: - factor = 1.0 - p - prod *= 1.0 - factor - return 1.0 - prod - - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class ComplexFormula(Logic.ComplexFormula, Formula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Lit(Logic.Lit, Formula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Litgroup(Logic.LitGroup, Formula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundAtom(Logic.GroundAtom): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundLit(Logic.GroundLit, Formula): - - def noisyor(self, world): - truth = self(world) - if self.negated: truth = 1. - truth - return truth - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Disjunction(Logic.Disjunction, ComplexFormula): - - def truth(self, world): - dontKnow = False - for child in self.children: - childValue = child.truth(world) - if childValue == 1: - return 1 - if childValue is None: - dontKnow = True - if dontKnow: - return None - else: - return 0 - - - def simplify(self, world): - sf_children = [] - for child in self.children: - child = child.simplify(world) - t = child.truth(world) - if t == 1: - return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) - elif t == 0: continue - else: sf_children.append(child) - if len(sf_children) == 1: - return sf_children[0].copy(idx=self.idx) - elif len(sf_children) >= 2: - return self.mln.logic.disjunction(sf_children, mln=self.mln, idx=self.idx) - else: - return self.mln.logic.true_false(0, mln=self.mln, idx=self.idx) - - - def noisyor(self, world): - prod = 1.0 - for lit in self.children: - p = ifnone(lit(world), 1) - if not lit.negated: - factor = p - else: - factor = 1.0 - p - prod *= 1.0 - factor - return 1.0 - prod - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class Conjunction(Logic.Conjunction, ComplexFormula): - - def truth(self, world): - dontKnow = False - for child in self.children: - childValue = child.truth(world) - if childValue == 0: - return 0. - if childValue is None: - dontKnow = True - if dontKnow: - return None - else: - return 1. - - - def simplify(self, world): - sf_children = [] - for child in self.children: - child = child.simplify(world) - t = child.truth(world) - if t == 0: - return self.mln.logic.true_false(0, mln=self.mln, idx=self.idx) - elif t == 1: pass - else: sf_children.append(child) - if len(sf_children) == 1: - return sf_children[0].copy(idx=self.idx) - elif len(sf_children) >= 2: - return self.mln.logic.conjunction(sf_children, mln=self.mln, idx=self.idx) - else: - return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) - - - def noisyor(self, world): - cnf = self.cnf() - prod = 1.0 - if isinstance(cnf, FirstOrderLogic.Conjunction): - for disj in cnf.children: - prod *= disj.noisyor(world) - else: - prod *= cnf.noisyor(world) - return prod - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Implication(Logic.Implication, ComplexFormula): - - def truth(self, world): - ant = self.children[0].truth(world) - cons = self.children[1].truth(world) - if ant == 0 or cons == 1: - return 1 - if ant is None or cons is None: - return None - return 0 - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Biimplication(Logic.Biimplication, ComplexFormula): - - def truth(self, world): - c1 = self.children[0].truth(world) - c2 = self.children[1].truth(world) - if c1 is None or c2 is None: - return None - return 1 if (c1 == c2) else 0 - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Negation(Logic.Negation, ComplexFormula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Exist(Logic.Exist, ComplexFormula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Equality(Logic.Equality, ComplexFormula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class TrueFalse(Logic.TrueFalse, Formula): - - @property - def value(self): - return self._value - - - @value.setter - def value(self, truth): - if not truth == 0 and not truth == 1: - raise Exception('Truth values in first-order logic cannot be %s' % truth) - self._value = truth - - - def __str__(self): - return str(True if self.value == 1 else False) - - - def noisyor(self, world): - return self(world) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class ProbabilityConstraint(object): - """ - Base class for representing a prior/posterior probability constraint (soft evidence) - on a logical expression. - """ - - def __init__(self, formula, p): - self.formula = formula - self.p = p - - def __repr__(self): - return str(self) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class PriorConstraint(ProbabilityConstraint): - """ - Class representing a prior probability. - """ - - def __str__(self): - return 'P(%s) = %.2f' % (fstr(self.formula), self.p) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class PosteriorConstraint(ProbabilityConstraint): - """ - Class representing a posterior probability. - """ - - def __str__(self): - return 'P(%s|E) = %.2f' % (fstr(self.formula), self.p) - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - def conjunction(self, *args, **kwargs): - return FirstOrderLogic.Conjunction(*args, **kwargs) - - def disjunction(self, *args, **kwargs): - return FirstOrderLogic.Disjunction(*args, **kwargs) - - def negation(self, *args, **kwargs): - return FirstOrderLogic.Negation(*args, **kwargs) - - def implication(self, *args, **kwargs): - return FirstOrderLogic.Implication(*args, **kwargs) - - def biimplication(self, *args, **kwargs): - return FirstOrderLogic.Biimplication(*args, **kwargs) - - def equality(self, *args, **kwargs): - return FirstOrderLogic.Equality(*args, **kwargs) - - def exist(self, *args, **kwargs): - return FirstOrderLogic.Exist(*args, **kwargs) - - def gnd_atom(self, *args, **kwargs): - return FirstOrderLogic.GroundAtom(*args, **kwargs) - - def lit(self, *args, **kwargs): - return FirstOrderLogic.Lit(*args, **kwargs) - - def litgroup(self, *args, **kwargs): - return FirstOrderLogic.LitGroup(*args, **kwargs) - - def gnd_lit(self, *args, **kwargs): - return FirstOrderLogic.GroundLit(*args, **kwargs) - - def count_constraint(self, *args, **kwargs): - return FirstOrderLogic.CountConstraint(*args, **kwargs) - - def true_false(self, *args, **kwargs): - return FirstOrderLogic.TrueFalse(*args, **kwargs) - - -# this is a little hack to make nested classes pickleable -Constraint = FirstOrderLogic.Constraint -Formula = FirstOrderLogic.Formula -ComplexFormula = FirstOrderLogic.ComplexFormula -Conjunction = FirstOrderLogic.Conjunction -Disjunction = FirstOrderLogic.Disjunction -Lit = FirstOrderLogic.Lit -GroundLit = FirstOrderLogic.GroundLit -GroundAtom = FirstOrderLogic.GroundAtom -Equality = FirstOrderLogic.Equality -Implication = FirstOrderLogic.Implication -Biimplication = FirstOrderLogic.Biimplication -Negation = FirstOrderLogic.Negation -Exist = FirstOrderLogic.Exist -TrueFalse = FirstOrderLogic.TrueFalse -NonLogicalConstraint = FirstOrderLogic.NonLogicalConstraint -CountConstraint = FirstOrderLogic.CountConstraint -GroundCountConstraint = FirstOrderLogic.GroundCountConstraint diff --git a/python3/pracmln/logic/fol.pyx b/python3/pracmln/logic/fol.pyx new file mode 100644 index 00000000..d8634e2c --- /dev/null +++ b/python3/pracmln/logic/fol.pyx @@ -0,0 +1,359 @@ +# FIRST-ORDER LOGIC -- PROCESSING +# +# (C) 2013 by Daniel Nyga (nyga@cs.uni-bremen.de) +# (C) 2007-2012 by Dominik Jain +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +from dnutils import ifnone + +from .common import Logic +from .common import Constraint as Super_Constraint +from .common import Formula as Super_Formula +from .common import ComplexFormula as Super_ComplexFormula +from .common import Conjunction as Super_Conjunction +from .common import Disjunction as Super_Disjunction +from .common import Lit as Super_Lit +from .common import LitGroup as Super_LitGroup +from .common import GroundLit as Super_GroundLit +from .common import GroundAtom as Super_GroundAtom +from .common import Equality as Super_Equality +from .common import Implication as Super_Implication +from .common import Biimplication as Super_Biimplication +from .common import Negation as Super_Negation +from .common import Exist as Super_Exist +from .common import TrueFalse as Super_TrueFalse +from .common import NonLogicalConstraint as Super_NonLogicalConstraint +from .common import CountConstraint# as Super_CountConstraint +from .common import GroundCountConstraint as Super_GroundCountConstraint +from ..mln.util import fstr + + +cdef class Constraint(Super_Constraint): + pass + +#class Formula(Super_Formula, Constraint): +cdef class Formula(Super_Formula): + def noisyor(self, world): + """ + Computes the noisy-or distribution of this formula. + """ + return self.cnf().noisyor(world) + + + def _getEvidenceTruthDegreeCW(self, gndAtom, worldValues): + """ + gets (soft or hard) evidence as a degree of belief from 0 to 1, making the closed world assumption, + soft evidence has precedence over hard evidence + """ + se = self._getSoftEvidence(gndAtom) + if se is not None: + return se if (True == worldValues[gndAtom.idx] or None == worldValues[gndAtom.idx]) else 1.0 - se # TODO allSoft currently unsupported + return 1.0 if worldValues[gndAtom.idx] else 0.0 + + + def _noisyOr(self, mln, worldValues, disj): + if isinstance(disj, GroundLit): + lits = [disj] + elif isinstance(disj, TrueFalse): + return disj.isTrue(worldValues) + else: + lits = disj.children + prod = 1.0 + for lit in lits: + p = mln._getEvidenceTruthDegreeCW(lit.gndAtom, worldValues) + if not lit.negated: + factor = p + else: + factor = 1.0 - p + prod *= 1.0 - factor + return 1.0 - prod + +#class ComplexFormula(Super_ComplexFormula, Formula): +cdef class ComplexFormula(Super_ComplexFormula): + pass + +#class Lit(Super_Lit, Formula): +cdef class Lit(Super_Lit): + pass + +#class Litgroup(Super_LitGroup, Formula): +cdef class LitGroup(Super_LitGroup): + pass + +cdef class GroundAtom(Super_GroundAtom): + pass + +#class GroundLit(Super_GroundLit, Formula): +cdef class GroundLit(Super_GroundLit): + def noisyor(self, world): + truth = self(world) + if self.negated: truth = 1. - truth + return truth + +#class Disjunction(Super_Disjunction, ComplexFormula): +cdef class Disjunction(Super_Disjunction): + def truth(self, world): + cdef bint dontKnow = False + for child in self.children: + childValue = child.truth(world) + if childValue == 1: + return 1 + if childValue is None: + dontKnow = True + if dontKnow: + return None + else: + return 0 + + + def simplify(self, world): + sf_children = [] + for child in self.children: + child = child.simplify(world) + t = child.truth(world) + if t == 1: + return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) + elif t == 0: continue + else: sf_children.append(child) + if len(sf_children) == 1: + return sf_children[0].copy(idx=self.idx) + elif len(sf_children) >= 2: + return self.mln.logic.disjunction(sf_children, mln=self.mln, idx=self.idx) + else: + return self.mln.logic.true_false(0, mln=self.mln, idx=self.idx) + + + def noisyor(self, world): + prod = 1.0 + for lit in self.children: + p = ifnone(lit(world), 1) + if not lit.negated: + factor = p + else: + factor = 1.0 - p + prod *= 1.0 - factor + return 1.0 - prod + +#class Conjunction(Super_Conjunction, ComplexFormula): +cdef class Conjunction(Super_Conjunction): + def truth(self, world): + cdef bint dontKnow = False + for child in self.children: + childValue = child.truth(world) + if childValue == 0: + return 0. + if childValue is None: + dontKnow = True + if dontKnow: + return None + else: + return 1. + + + def simplify(self, world): + sf_children = [] + for child in self.children: + child = child.simplify(world) + t = child.truth(world) + if t == 0: + return self.mln.logic.true_false(0, mln=self.mln, idx=self.idx) + elif t == 1: pass + else: sf_children.append(child) + if len(sf_children) == 1: + return sf_children[0].copy(idx=self.idx) + elif len(sf_children) >= 2: + return self.mln.logic.conjunction(sf_children, mln=self.mln, idx=self.idx) + else: + return self.mln.logic.true_false(1, mln=self.mln, idx=self.idx) + + + def noisyor(self, world): + cnf = self.cnf() + prod = 1.0 + if isinstance(cnf, Conjunction): + for disj in cnf.children: + prod *= disj.noisyor(world) + else: + prod *= cnf.noisyor(world) + return prod + +#class Implication(Super_Implication, ComplexFormula): +cdef class Implication(Super_Implication): + def truth(self, world): + ant = self.children[0].truth(world) + cons = self.children[1].truth(world) + if ant == 0 or cons == 1: + return 1 + if ant is None or cons is None: + return None + return 0 + +#class Biimplication(Super_Biimplication, ComplexFormula): +cdef class Biimplication(Super_Biimplication): + def truth(self, world): + c1 = self.children[0].truth(world) + c2 = self.children[1].truth(world) + if c1 is None or c2 is None: + return None + return 1 if (c1 == c2) else 0 + +#class Negation(Super_Negation, ComplexFormula): +cdef class Negation(Super_Negation): + pass + +#class Exist(Super_Exist, ComplexFormula): +cdef class Exist(Super_Exist): + pass + +#class Equality(Super_Equality, ComplexFormula): +cdef class Equality(Super_Equality): + pass + +#class TrueFalse(Super_TrueFalse, Formula): +cdef class TrueFalse(Super_TrueFalse): + @property + def value(self): + return self._value + + + @value.setter + def value(self, truth): + if not truth == 0 and not truth == 1: + raise Exception('Truth values in first-order logic cannot be %s' % truth) + self._value = truth + + + def __str__(self): + return str(True if self.value == 1 else False) + + + def noisyor(self, world): + return self(world) + +#class ProbabilityConstraint(object): +cdef class ProbabilityConstraint(): + """ + Base class for representing a prior/posterior probability constraint (soft evidence) + on a logical expression. + """ + + def __init__(self, formula, p): + self.formula = formula + self.p = p + + def __repr__(self): + return str(self) + +cdef class PriorConstraint(ProbabilityConstraint): + """ + Class representing a prior probability. + """ + + def __str__(self): + return 'P(%s) = %.2f' % (fstr(self.formula), self.p) + +cdef class PosteriorConstraint(ProbabilityConstraint): + """ + Class representing a posterior probability. + """ + + def __str__(self): + return 'P(%s|E) = %.2f' % (fstr(self.formula), self.p) + + + + + + + + + + + + + + + + +cdef class FirstOrderLogic(Logic): + """ + Factory class for first-order logic. + """ + + def conjunction(self, *args, **kwargs): + return Conjunction(*args, **kwargs) + + def disjunction(self, *args, **kwargs): + return Disjunction(*args, **kwargs) + + def negation(self, *args, **kwargs): + return Negation(*args, **kwargs) + + def implication(self, *args, **kwargs): + return Implication(*args, **kwargs) + + def biimplication(self, *args, **kwargs): + return Biimplication(*args, **kwargs) + + def equality(self, *args, **kwargs): + return Equality(*args, **kwargs) + + def exist(self, *args, **kwargs): + return Exist(*args, **kwargs) + + def gnd_atom(self, *args, **kwargs): + return GroundAtom(*args, **kwargs) + + def lit(self, *args, **kwargs): + return Lit(*args, **kwargs) + + def litgroup(self, *args, **kwargs): + return LitGroup(*args, **kwargs) + + def gnd_lit(self, *args, **kwargs): + return GroundLit(*args, **kwargs) + + def count_constraint(self, *args, **kwargs): + return CountConstraint(*args, **kwargs) + + def true_false(self, *args, **kwargs): + return TrueFalse(*args, **kwargs) + + +# this is a little hack to make nested classes pickleable +# Constraint = FirstOrderLogic.Constraint +# Formula = FirstOrderLogic.Formula +# ComplexFormula = FirstOrderLogic.ComplexFormula +# Conjunction = FirstOrderLogic.Conjunction +# Disjunction = FirstOrderLogic.Disjunction +# Lit = FirstOrderLogic.Lit +# GroundLit = FirstOrderLogic.GroundLit +# GroundAtom = FirstOrderLogic.GroundAtom +# Equality = FirstOrderLogic.Equality +# Implication = FirstOrderLogic.Implication +# Biimplication = FirstOrderLogic.Biimplication +# Negation = FirstOrderLogic.Negation +# Exist = FirstOrderLogic.Exist +# TrueFalse = FirstOrderLogic.TrueFalse + +# the following attributes no longer exist (!): +# NonLogicalConstraint = FirstOrderLogic.NonLogicalConstraint +# CountConstraint = FirstOrderLogic.CountConstraint +# GroundCountConstraint = FirstOrderLogic.GroundCountConstraint diff --git a/python3/pracmln/logic/fuzzy.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/logic/fuzzy.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..8883d0f9 --- /dev/null +++ b/python3/pracmln/logic/fuzzy.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/logic/fuzzy.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/logic/fuzzy.pxd b/python3/pracmln/logic/fuzzy.pxd new file mode 100644 index 00000000..d0650dda --- /dev/null +++ b/python3/pracmln/logic/fuzzy.pxd @@ -0,0 +1,70 @@ +from .common cimport Logic +from .common cimport Constraint as Super_Constraint +from .common cimport Formula as Super_Formula +from .common cimport ComplexFormula as Super_ComplexFormula +from .common cimport Conjunction as Super_Conjunction +from .common cimport Disjunction as Super_Disjunction +from .common cimport Lit as Super_Lit +from .common cimport LitGroup as Super_LitGroup +from .common cimport GroundLit as Super_GroundLit +from .common cimport GroundAtom as Super_GroundAtom +from .common cimport Equality as Super_Equality +from .common cimport Implication as Super_Implication +from .common cimport Biimplication as Super_Biimplication +from .common cimport Negation as Super_Negation +from .common cimport Exist as Super_Exist +from .common cimport TrueFalse as Super_TrueFalse +from .common cimport NonLogicalConstraint as Super_NonLogicalConstraint +from .common cimport CountConstraint as Super_CountConstraint +from .common cimport GroundCountConstraint as Super_GroundCountConstraint + + + +cdef class Constraint(Super_Constraint): + pass + +cdef class Formula(Super_Formula): + pass + +cdef class ComplexFormula(Super_Formula): + pass + +cdef class Lit(Super_Lit): + pass + +cdef class LitGroup(Super_LitGroup): + pass + +cdef class GroundLit(Super_GroundLit): + pass + +cdef class GroundAtom(Super_GroundAtom): + cpdef truth(self, list world) + +cdef class Negation(Super_Negation): + cpdef truth(self, list world) + +cdef class Conjunction(Super_Conjunction): + pass + +cdef class Disjunction(Super_Disjunction): + pass + +cdef class Implication(Super_Implication): + pass + +cdef class Biimplication(Super_Biimplication): + pass + +cdef class Equality(Super_Equality): + pass + +cdef class TrueFalse(Super_TrueFalse): + pass + +cdef class Exist(Super_Exist): + pass + + +cdef class FuzzyLogic(Logic): + pass diff --git a/python3/pracmln/logic/fuzzy.py b/python3/pracmln/logic/fuzzy.py deleted file mode 100644 index f1508aeb..00000000 --- a/python3/pracmln/logic/fuzzy.py +++ /dev/null @@ -1,341 +0,0 @@ -# FUZZY LOGIC -# -# (C) 2012-2013 by Daniel Nyga (nyga@cs.tum.edu) -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -from .common import Logic -from functools import reduce - - -class FuzzyLogic(Logic): - """ - Implementation of fuzzy logic for MLNs. - """ - - - @staticmethod - def min_undef(*args): - """ - Custom minimum function return None if one of its arguments - is None and min(*args) otherwise. - """ - if len([x for x in args if x == 0]) > 0: - return 0 - return reduce(lambda x, y: None if (x is None or y is None) else min(x, y), args) - - - @staticmethod - def max_undef(*args): - """ - Custom maximum function return None if one of its arguments - is None and max(*args) otherwise. - """ - if len([x for x in args if x == 1]) > 0: - return 1 - return reduce(lambda x, y: None if x is None or y is None else max(x, y), args) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Constraint(Logic.Constraint): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Formula(Logic.Formula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class ComplexFormula(Logic.Formula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Lit(Logic.Lit): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class LitGroup(Logic.LitGroup): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundLit(Logic.GroundLit): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundAtom(Logic.GroundAtom): - - def truth(self, world): - return world[self.idx] - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Negation(Logic.Negation, ComplexFormula): - - def truth(self, world): - val = self.children[0].truth(world) - return None if val is None else 1. - val - - - def simplify(self, world): - f = self.children[0].simplify(world) - if isinstance(f, Logic.TrueFalse): - return f.invert() - else: - return self.mln.logic.negation([f], mln=self.mln, idx=self.idx) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Conjunction(Logic.Conjunction, ComplexFormula): - - - def truth(self, world): - truthChildren = [a.truth(world) for a in self.children] - return FuzzyLogic.min_undef(*truthChildren) - - - def simplify(self, world): - sf_children = [] - minTruth = None - for child_ in self.children: - child = child_.simplify(world) - if isinstance(child, Logic.TrueFalse): - truth = child.truth() - if truth == 0: - return self.mln.logic.true_false(0., mln=self.mln, idx=self.idx) - if minTruth is None or truth < minTruth: - minTruth = truth - else: - sf_children.append(child) - if minTruth is not None and minTruth < 1 or minTruth == 1 and len(sf_children) == 0: - sf_children.append(self.mln.logic.true_false(minTruth, mln=self.mln)) - if len(sf_children) > 1: - return self.mln.logic.conjunction(sf_children, mln=self.mln, idx=self.idx) - elif len(sf_children) == 1: - return sf_children[0].copy(idx=self.idx) - else: - assert False # should be unreachable - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Disjunction(Logic.Disjunction, ComplexFormula): - - - def truth(self, world): - return FuzzyLogic.max_undef(*[a.truth(world) for a in self.children]) - - - def simplify(self, world): - sf_children = [] - maxTruth = None - for child in self.children: - child = child.simplify(world) - if isinstance(child, Logic.TrueFalse): - truth = child.truth() - if truth == 1: - return self.mln.logic.true_false(1., mln=self.mln, idx=self.idx) - if maxTruth is None or truth > maxTruth: - maxTruth = truth - else: - sf_children.append(child) - if maxTruth is not None and maxTruth > 0 or (maxTruth == 0 and len(sf_children) == 0): - sf_children.append(self.mln.logic.true_false(maxTruth, mln=self.mln)) - if len(sf_children) > 1: - return self.mln.logic.disjunction(sf_children, mln=self.mln, idx=self.idx) - elif len(sf_children) == 1: - return sf_children[0].copy(idx=self.idx) - else: - assert False - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Implication(Logic.Implication, ComplexFormula): - - def truth(self, world): - ant = self.children[0].truth(world) - return FuzzyLogic.max_undef(None if ant is None else 1. - ant, self.children[1].truth(world)) - - def simplify(self, world): - return self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln, idx=self.idx), - self.children[1]], mln=self.mln, idx=self.idx).simplify(world) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Biimplication(Logic.Biimplication, ComplexFormula): - - def truth(self, world): - return FuzzyLogic.min_undef(self.children[0].truth(world), self.children[1].truth(world)) - - def simplify(self, world): - c1 = self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln, idx=self.idx), self.children[1]], mln=self.mln, idx=self.idx) - c2 = self.mln.logic.disjunction([self.children[0], self.mln.logic.negation([self.children[1]], mln=self.mln, idx=self.idx)], mln=self.mln, idx=self.idx) - return self.mln.logic.conjunction([c1,c2], mln=self.mln, idx=self.idx).simplify(world) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Equality(Logic.Equality): - - def truth(self, world=None): - if any(map(self.mln.logic.isvar, self.args)): - return None - equals = 1. if (self.args[0] == self.args[1]) else 0. - return (1. - equals) if self.negated else equals - - def simplify(self, world): - truth = self.truth(world) - if truth != None: return self.mln.logic.true_false(truth, mln=self.mln, idx=self.idx) - return self.mln.logic.equality(list(self.args), negated=self.negated, mln=self.mln, idx=self.idx) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class TrueFalse(Formula, Logic.TrueFalse): - - # def __init__(self, truth, mln, idx=None): - # if not (truth >= 0. and truth <= 1.): - # raise Exception('Illegal truth value: %s' % truth) - # Logic.TrueFalse(self, truth) - - @property - def value(self): - return self._value - - @value.setter - def value(self, truth): - if not (truth >= 0. and truth <= 1.): - raise Exception('Illegal truth value: %s' % truth) - self._value = truth - - def __str__(self): - return str(self.value) - - def cstr(self, color=False): - return str(self) - - def invert(self): - return self.mln.logic.true_false(1. - self.value, idx=self.idx, mln=self.mln) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Exist(Logic.Exist, Logic.ComplexFormula): - pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - def conjunction(self, *args, **kwargs): - return FuzzyLogic.Conjunction(*args, **kwargs) - - - def disjunction(self, *args, **kwargs): - return FuzzyLogic.Disjunction(*args, **kwargs) - - - def negation(self, *args, **kwargs): - return FuzzyLogic.Negation(*args, **kwargs) - - - def implication(self, *args, **kwargs): - return FuzzyLogic.Implication(*args, **kwargs) - - - def biimplication(self, *args, **kwargs): - return FuzzyLogic.Biimplication(*args, **kwargs) - - - def equality(self, *args, **kwargs): - return FuzzyLogic.Equality(*args, **kwargs) - - - def exist(self, *args, **kwargs): - return FuzzyLogic.Exist(*args, **kwargs) - - - def gnd_atom(self, *args, **kwargs): - return FuzzyLogic.GroundAtom(*args, **kwargs) - - - def lit(self, *args, **kwargs): - return FuzzyLogic.Lit(*args, **kwargs) - - - def litgroup(self, *args, **kwargs): - return FuzzyLogic.LitGroup(*args, **kwargs) - - - def gnd_lit(self, *args, **kwargs): - return FuzzyLogic.GroundLit(*args, **kwargs) - - - def count_constraint(self, *args, **kwargs): - return FuzzyLogic.CountConstraint(*args, **kwargs) - - - def true_false(self, *args, **kwargs): - return FuzzyLogic.TrueFalse(*args, **kwargs) - - -# this is a little hack to make nested classes pickleable -Constraint = FuzzyLogic.Constraint -Formula = FuzzyLogic.Formula -ComplexFormula = FuzzyLogic.ComplexFormula -Conjunction = FuzzyLogic.Conjunction -Disjunction = FuzzyLogic.Disjunction -Lit = FuzzyLogic.Lit -GroundLit = FuzzyLogic.GroundLit -GroundAtom = FuzzyLogic.GroundAtom -Equality = FuzzyLogic.Equality -Implication = FuzzyLogic.Implication -Biimplication = FuzzyLogic.Biimplication -Negation = FuzzyLogic.Negation -Exist = FuzzyLogic.Exist -TrueFalse = FuzzyLogic.TrueFalse -NonLogicalConstraint = FuzzyLogic.NonLogicalConstraint -CountConstraint = FuzzyLogic.CountConstraint -GroundCountConstraint = FuzzyLogic.GroundCountConstraint - diff --git a/python3/pracmln/logic/fuzzy.pyx b/python3/pracmln/logic/fuzzy.pyx new file mode 100644 index 00000000..b3fe8dc6 --- /dev/null +++ b/python3/pracmln/logic/fuzzy.pyx @@ -0,0 +1,298 @@ +# FUZZY LOGIC +# +# (C) 2012-2013 by Daniel Nyga (nyga@cs.tum.edu) +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + +from .common import Logic +from .common import Constraint as Super_Constraint +from .common import Formula as Super_Formula +from .common import ComplexFormula as Super_ComplexFormula +from .common import Conjunction as Super_Conjunction +from .common import Disjunction as Super_Disjunction +from .common import Lit as Super_Lit +from .common import LitGroup as Super_LitGroup +from .common import GroundLit as Super_GroundLit +from .common import GroundAtom as Super_GroundAtom +from .common import Equality as Super_Equality +from .common import Implication as Super_Implication +from .common import Biimplication as Super_Biimplication +from .common import Negation as Super_Negation +from .common import Exist as Super_Exist +from .common import TrueFalse as Super_TrueFalse +from .common import NonLogicalConstraint as Super_NonLogicalConstraint +from .common import CountConstraint# as Super_CountConstraint +from .common import GroundCountConstraint as Super_GroundCountConstraint +from functools import reduce + + +cdef class Constraint(Super_Constraint): + pass + +cdef class Formula(Super_Formula): + pass + +cdef class ComplexFormula(Super_Formula): + pass + +cdef class Lit(Super_Lit): + pass + +cdef class LitGroup(Super_LitGroup): + pass + +cdef class GroundLit(Super_GroundLit): + pass + +cdef class GroundAtom(Super_GroundAtom): + cpdef truth(self, list world): + return world[self.idx] + +cdef class Negation(Super_Negation): + cpdef truth(self, list world): + val = self.children[0].truth(world) + return None if val is None else 1. - val + + def simplify(self, world): + f = self.children[0].simplify(world) + if isinstance(f, Super_TrueFalse): + return f.invert() + else: + return self.mln.logic.negation([f], mln=self.mln, idx=self.idx) + +cdef class Conjunction(Super_Conjunction): + def truth(self, world): + truthChildren = [a.truth(world) for a in self.children] + return FuzzyLogic.min_undef(*truthChildren) + + def simplify(self, world): + sf_children = [] + minTruth = None + for child_ in self.children: + child = child_.simplify(world) + if isinstance(child, Super_TrueFalse): + truth = child.truth() + if truth == 0: + return self.mln.logic.true_false(0., mln=self.mln, idx=self.idx) + if minTruth is None or truth < minTruth: + minTruth = truth + else: + sf_children.append(child) + if minTruth is not None and minTruth < 1 or minTruth == 1 and len(sf_children) == 0: + sf_children.append(self.mln.logic.true_false(minTruth, mln=self.mln)) + if len(sf_children) > 1: + return self.mln.logic.conjunction(sf_children, mln=self.mln, idx=self.idx) + elif len(sf_children) == 1: + return sf_children[0].copy(idx=self.idx) + else: + assert False # should be unreachable + +cdef class Disjunction(Super_Disjunction): + def truth(self, world): + return FuzzyLogic.max_undef(*[a.truth(world) for a in self.children]) + + def simplify(self, world): + sf_children = [] + maxTruth = None + for child in self.children: + child = child.simplify(world) + if isinstance(child, Super_TrueFalse): + truth = child.truth() + if truth == 1: + return self.mln.logic.true_false(1., mln=self.mln, idx=self.idx) + if maxTruth is None or truth > maxTruth: + maxTruth = truth + else: + sf_children.append(child) + if maxTruth is not None and maxTruth > 0 or (maxTruth == 0 and len(sf_children) == 0): + sf_children.append(self.mln.logic.true_false(maxTruth, mln=self.mln)) + if len(sf_children) > 1: + return self.mln.logic.disjunction(sf_children, mln=self.mln, idx=self.idx) + elif len(sf_children) == 1: + return sf_children[0].copy(idx=self.idx) + else: + assert False + +cdef class Implication(Super_Implication): + def truth(self, world): + ant = self.children[0].truth(world) + return FuzzyLogic.max_undef(None if ant is None else 1. - ant, self.children[1].truth(world)) + + def simplify(self, world): + return self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln, idx=self.idx), + self.children[1]], mln=self.mln, idx=self.idx).simplify(world) + +cdef class Biimplication(Super_Biimplication): + def truth(self, world): + return FuzzyLogic.min_undef(self.children[0].truth(world), self.children[1].truth(world)) + + def simplify(self, world): + c1 = self.mln.logic.disjunction([self.mln.logic.negation([self.children[0]], mln=self.mln, idx=self.idx), self.children[1]], mln=self.mln, idx=self.idx) + c2 = self.mln.logic.disjunction([self.children[0], self.mln.logic.negation([self.children[1]], mln=self.mln, idx=self.idx)], mln=self.mln, idx=self.idx) + return self.mln.logic.conjunction([c1,c2], mln=self.mln, idx=self.idx).simplify(world) + +cdef class Equality(Super_Equality): + def truth(self, world=None): + if any(map(self.mln.logic.isvar, self.args)): + return None + equals = 1. if (self.args[0] == self.args[1]) else 0. + return (1. - equals) if self.negated else equals + + def simplify(self, world): + truth = self.truth(world) + if truth != None: return self.mln.logic.true_false(truth, mln=self.mln, idx=self.idx) + return self.mln.logic.equality(list(self.args), negated=self.negated, mln=self.mln, idx=self.idx) + +cdef class TrueFalse(Super_TrueFalse): + @property + def value(self): + return self._value + + @value.setter + def value(self, truth): + if not (truth >= 0. and truth <= 1.): + raise Exception('Illegal truth value: %s' % truth) + self._value = truth + + def __str__(self): + return str(self.value) + + def cstr(self, color=False): + return str(self) + + def invert(self): + return self.mln.logic.true_false(1. - self.value, idx=self.idx, mln=self.mln) + +cdef class Exist(Super_Exist): + pass + + + + + + + + + + + + + + + + +cdef class FuzzyLogic(Logic): + """ + Implementation of fuzzy logic for MLNs. + """ + + @staticmethod + def min_undef(*args): + """ + Custom minimum function return None if one of its arguments + is None and min(*args) otherwise. + """ + if len([x for x in args if x == 0]) > 0: + return 0 + return reduce(lambda x, y: None if (x is None or y is None) else min(x, y), args) + + @staticmethod + def max_undef(*args): + """ + Custom maximum function return None if one of its arguments + is None and max(*args) otherwise. + """ + if len([x for x in args if x == 1]) > 0: + return 1 + return reduce(lambda x, y: None if x is None or y is None else max(x, y), args) + + def conjunction(self, *args, **kwargs): + return Conjunction(*args, **kwargs) + + + def disjunction(self, *args, **kwargs): + return Disjunction(*args, **kwargs) + + + def negation(self, *args, **kwargs): + return Negation(*args, **kwargs) + + + def implication(self, *args, **kwargs): + return Implication(*args, **kwargs) + + + def biimplication(self, *args, **kwargs): + return Biimplication(*args, **kwargs) + + + def equality(self, *args, **kwargs): + return Equality(*args, **kwargs) + + + def exist(self, *args, **kwargs): + return Exist(*args, **kwargs) + + + def gnd_atom(self, *args, **kwargs): + return GroundAtom(*args, **kwargs) + + + def lit(self, *args, **kwargs): + return Lit(*args, **kwargs) + + + def litgroup(self, *args, **kwargs): + return LitGroup(*args, **kwargs) + + + def gnd_lit(self, *args, **kwargs): + return GroundLit(*args, **kwargs) + + + def count_constraint(self, *args, **kwargs): + return CountConstraint(*args, **kwargs) + + + def true_false(self, *args, **kwargs): + return TrueFalse(*args, **kwargs) + + +# this is a little hack to make nested classes pickleable +# Constraint = FuzzyLogic.Constraint +# Formula = FuzzyLogic.Formula +# ComplexFormula = FuzzyLogic.ComplexFormula +# Conjunction = FuzzyLogic.Conjunction +# Disjunction = FuzzyLogic.Disjunction +# Lit = FuzzyLogic.Lit +# GroundLit = FuzzyLogic.GroundLit +# GroundAtom = FuzzyLogic.GroundAtom +# Equality = FuzzyLogic.Equality +# Implication = FuzzyLogic.Implication +# Biimplication = FuzzyLogic.Biimplication +# Negation = FuzzyLogic.Negation +# Exist = FuzzyLogic.Exist +# TrueFalse = FuzzyLogic.TrueFalse + +# the following attributes no longer exist (!): +# NonLogicalConstraint = FuzzyLogic.NonLogicalConstraint +# CountConstraint = FuzzyLogic.CountConstraint +# GroundCountConstraint = FuzzyLogic.GroundCountConstraint diff --git a/python3/pracmln/logic/setup.py b/python3/pracmln/logic/setup.py new file mode 100644 index 00000000..68e8a682 --- /dev/null +++ b/python3/pracmln/logic/setup.py @@ -0,0 +1,6 @@ +from distutils.core import setup +from Cython.Build import cythonize + +setup( + ext_modules=cythonize("*.pyx")#, compiler_directives={'profile': True}) +) diff --git a/python3/pracmln/mln/.gitignore b/python3/pracmln/mln/.gitignore index 24fae75e..2db71cff 100644 --- a/python3/pracmln/mln/.gitignore +++ b/python3/pracmln/mln/.gitignore @@ -1 +1,6 @@ -*.pyo \ No newline at end of file +mini-test.py +*.pyo +*.c +*.html +build/* +pracmln/* diff --git a/python3/pracmln/mln/base.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/base.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..cda902f8 --- /dev/null +++ b/python3/pracmln/mln/base.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/base.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/base.pxd b/python3/pracmln/mln/base.pxd new file mode 100644 index 00000000..074a2fee --- /dev/null +++ b/python3/pracmln/mln/base.pxd @@ -0,0 +1,6 @@ +from ..logic.common cimport Logic + +cdef class MLN: + cdef public Logic logic + cdef dict __dict__ + cpdef weight(self, int idx, weight=*) diff --git a/python3/pracmln/mln/base.py b/python3/pracmln/mln/base.pyx similarity index 96% rename from python3/pracmln/mln/base.py rename to python3/pracmln/mln/base.pyx index 9da4b024..95e2c1c7 100644 --- a/python3/pracmln/mln/base.py +++ b/python3/pracmln/mln/base.pyx @@ -47,6 +47,10 @@ from ..utils.project import mlnpath from importlib import util as imputil +#from cpython cimport array +#import array + + logger = logs.getlogger(__name__) @@ -58,13 +62,13 @@ logger.warning("Note: Psyco (http://psyco.sourceforge.net) was not loaded. On 32bit systems, it is recommended to install it for improved performance.") -class MLN(object): +cdef class MLN(object): ''' Represents a Markov logic network. - + :member formulas: a list of :class:`logic.common.Formula` objects representing the formulas of the MLN. :member predicates: a dict mapping predicate names to :class:`mlnpreds.Predicate` objects. - + :param logic: (string) the type of logic to be used in this MLN. Possible values are `FirstOrderLogic` and `FuzzyLogic`. :param grammar: (string) the syntax to be used. Possible grammars are @@ -82,7 +86,7 @@ def __init__(self, logic='FirstOrderLogic', grammar='PRACGrammar', mlnfile=None) self.domains = {} # maps from domain names to list of values self._formulas = [] # list of MLNFormula instances self.domain_decls = [] - self.weights = [] + self.weights = [] #array.array('d', []) self.fixweights = [] self.vars = {} self._unique_templvars = [] @@ -101,15 +105,16 @@ def __init__(self, logic='FirstOrderLogic', grammar='PRACGrammar', mlnfile=None) @property def predicates(self): return list(self.iterpreds()) - + @property def formulas(self): return list(self._formulas) - + @property def weights(self): + #print('_weight is {} of type {}'.format(self._weights, type(self._weights))) return self._weights - + @weights.setter def weights(self, wts): if len(wts) != len(self._formulas): @@ -129,7 +134,7 @@ def fixweights(self, fw): @property def probreqs(self): return self._probreqs - + @property def weighted_formulas(self): return [f for f in self._formulas if f.weight is not HARD] @@ -163,25 +168,25 @@ def copy(self): def predicate(self, predicate): ''' Returns the predicate object with the given predicate name, or declares a new predicate. - - If predicate is a string, this method returns the predicate object + + If predicate is a string, this method returns the predicate object assiciated to the given predicate name. If it is a predicate instance, it declares the - new predicate in this MLN and returns the MLN instance. In the latter case, this is + new predicate in this MLN and returns the MLN instance. In the latter case, this is equivalent to `MLN.declare_predicate()`. - + :param predicate: name of the predicate to be returned or a `Predicate` instance specifying the predicate to be declared. :returns: the Predicate object or None if there is no predicate with this name. If a new predicate is declared, returns this MLN instance. - + :Example: - + >>> mln = MLN() >>> mln.predicate(Predicate(foo, [arg0, arg1])) .predicate(Predicate(bar, [arg1, arg2])) # this declares predicates foo and bar >>> mln.predicate('foo') - + ''' if isinstance(predicate, Predicate): return self.declare_predicate(predicate) @@ -191,29 +196,29 @@ def predicate(self, predicate): return predicate.asList() else: raise Exception('Illegal type of argument predicate: %s' % type(predicate)) - + def iterpreds(self): ''' Yields the predicates defined in this MLN alphabetically ordered. ''' for predname in sorted(self._predicates): yield self.predicate(predname) - + def update_predicates(self, mln): ''' Merges the predicate definitions of this MLN with the definitions of the given one. - + :param mln: an instance of an MLN object. ''' for pred in mln.iterpreds(): self.declare_predicate(pred) - + def declare_predicate(self, predicate): ''' Adds a predicate declaration to the MLN: - - :param predicate: an instance of a Predicate or one of its subclasses + + :param predicate: an instance of a Predicate or one of its subclasses specifying a predicate declaration. ''' pred = self._predicates.get(predicate.name) @@ -232,7 +237,7 @@ def formula(self, formula, weight=0., fixweight=False, unique_templvars=None): are updated, if necessary. If `formula` is an integer, returns the formula with the respective index or the formula object that has been created from formula. The formula will be automatically tied to this MLN. - + :param formula: a `Logic.Formula` object or a formula string :param weight: an optional weight. May be a mathematical expression as a string (e.g. log(0.1)), real-valued number @@ -246,13 +251,18 @@ def formula(self, formula, weight=0., fixweight=False, unique_templvars=None): formula = self.logic.parse_formula(formula) elif type(formula) is int: return self._formulas[formula] - constants = {} + cdef dict constants = {} + #cdef list constants = [] + #print("CONSTANTS is a {}".format(type(constants))) formula.vardoms(None, constants) - for domain, constants in constants.items(): - for c in constants: self.constant(domain, c) + #print("CONSTANTS is a {}\n".format(type(constants))) + for domain, constantslist in constants.items(): + for c in constantslist: self.constant(domain, c) formula.mln = self formula.idx = len(self._formulas) self._formulas.append(formula) + #print('accessing weights = {}'.format(self.weights)) + #print('append(weight) - weight is {} of type {}'.format(weight, type(weight))) self.weights.append(weight) self.fixweights.append(fixweight) self._unique_templvars.append(list(unique_templvars) if unique_templvars is not None else []) @@ -263,15 +273,15 @@ def _rmformulas(self): self.weights = [] self.fixweights = [] self._unique_templvars = [] - + def iterformulas(self): ''' Returns a generator yielding (idx, formula) tuples. ''' for i, f in enumerate(self._formulas): yield i, f - - def weight(self, idx, weight=None): + + cpdef weight(self, int idx, weight=None): ''' Returns or sets the weight of the formula with index `idx`. ''' @@ -287,9 +297,9 @@ def materialize(self, *dbs): ''' Materializes this MLN with respect to the databases given. This must be called before learning or inference can take place. - + Returns a new MLN instance containing expanded formula templates and - materialized weights. Normally, this method should not be called from the outside. + materialized weights. Normally, this method should not be called from the outside. Also takes into account whether or not particular domain values or predictaes are actually used in the data, i.e. if a predicate is not used in any of the databases, all formulas that make use of this predicate are ignored. @@ -344,9 +354,9 @@ def materialize(self, *dbs): def constant(self, domain, *values): ''' Adds to the MLN a constant domain value to the domain specified. - + If the domain doesn't exist, it is created. - + :param domain: (string) the name of the domain the given value shall be added to. :param values: (string) the values to be added. ''' @@ -359,7 +369,7 @@ def constant(self, domain, *values): def ground(self, db): ''' Creates and returns a ground Markov Random Field for the given database. - + :param db: database filename (string) or Database object :param cw: if the closed-world assumption shall be applied (to all predicates) :param cwpreds: a list of predicate names the closed-world assumption shall be applied. @@ -376,7 +386,7 @@ def ground(self, db): def update_domain(self, domain): ''' Combines the existing domain (if any) with the given one. - + :param domain: a dictionary with domain Name to list of string constants to add ''' for domname in domain: break @@ -387,11 +397,11 @@ def learn(self, databases, method=BPLL, **params): ''' Triggers the learning parameter learning process for a given set of databases. Returns a new MLN object with the learned parameters. - + :param databases: list of :class:`mln.database.Database` objects or filenames ''' verbose = params.get('verbose', False) - + # get a list of database objects if not databases: raise Exception('At least one database is needed for learning.') @@ -435,7 +445,7 @@ def learn(self, databases, method=BPLL, **params): fittingParams.update(params) print("fitting with params ", fittingParams) self._fitProbabilityConstraints(self.probreqs, **fittingParams) - + if params.get('ignore_zero_weight_formulas', False): formulas = list(newmln.formulas) weights = list(newmln.weights) @@ -450,29 +460,29 @@ def tofile(self, filename): Creates the file with the given filename and writes this MLN into it. ''' f = open(filename, 'w+') - self.write(f, color=False) + self.write(f, color=False) f.close() def write(self, stream=sys.stdout, color=None): ''' Writes the MLN to the given stream. - + The default stream is `sys.stdout`. In order to print the MLN to the console, a simple call of `mln.write()` is sufficient. If color is not specified (is None), then the - output to the console will be colored and uncolored for every other stream. - + output to the console will be colored and uncolored for every other stream. + :param stream: the stream to write the MLN to. :param color: whether or not output should be colorized. ''' if color is None: - if stream != sys.stdout: + if stream != sys.stdout: color = False else: color = True if 'learnwts_message' in dir(self): stream.write("/*\n%s*/\n\n" % self.learnwts_message) # domain declarations if self.domain_decls: stream.write(colorize("// domain declarations\n", comment_color, color)) - for d in self.domain_decls: + for d in self.domain_decls: stream.write("%s\n" % d) stream.write('\n') # variable definitions @@ -517,12 +527,12 @@ def iter_formulas_printable(self): yield "%-10.6f\t%s" % (f.weight, fstr(f)) else: yield "%s\t%s" % (str(f.weight), fstr(f)) - + @staticmethod def load(files, logic='FirstOrderLogic', grammar='PRACGrammar', mln=None): ''' Reads an MLN object from a file or a set of files. - + :param files: one or more :class:`pracmln.mlnpath` strings. If multiple file names are given, the contents of all files will be concatenated. :param logic: (string) the type of logic to be used. Either `FirstOrderLogic` or `FuzzyLogic`. @@ -537,7 +547,7 @@ def load(files, logic='FirstOrderLogic', grammar='PRACGrammar', mln=None): for f in files: if isinstance(f, str): p = mlnpath(f) - if p.project is not None: + if p.project is not None: projectpath = p.projectloc text += p.content elif isinstance(f, mlnpath): @@ -555,7 +565,7 @@ def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic' dirs = [os.path.abspath(os.path.expandvars(os.path.expanduser(p))) for p in searchpaths] formulatemplates = [] text = str(text) - if text == "": + if text == "": raise MLNParsingError("No MLN content to construct model from was given; must specify either file/list of files or content string!") # replace some meta-directives in comments text = re.compile(r'//\s*\s*$', re.MULTILINE).sub("#group", text) @@ -605,7 +615,7 @@ def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic' m = re.match(r'"(?P.+)"', filename) if m is not None: filename = m.group('filename') - # if the path is relative, look for the respective file + # if the path is relative, look for the respective file # relatively to all paths specified. Take the first file matching. if not mlnpath(filename).exists: includefilename = None @@ -629,7 +639,7 @@ def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic' includefilename = ':'.join([projectpath, filename]) logger.debug('Including file: "%s"' % includefilename) p = mlnpath(includefilename) - parse_mln(text=mlnpath(includefilename).content, searchpaths=[p.resolve_path()]+dirs, + parse_mln(text=mlnpath(includefilename).content, searchpaths=[p.resolve_path()]+dirs, projectpath=ifnone(p.project, projectpath, lambda x: '/'.join(p.path+[x])), logic=logic, grammar=grammar, mln=mln) continue @@ -660,7 +670,7 @@ def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic' domName, constants = parse domName = str(domName) constants = list(map(str, constants)) - if domName in mln.domains: + if domName in mln.domains: logger.debug("Domain redefinition: Domain '%s' is being updated with values %s." % (domName, str(constants))) if domName not in mln.domains: mln.domains[domName] = [] @@ -687,7 +697,7 @@ def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic' if m is None: raise MLNParsingError("Variable assigment malformed: %s" % line) mln.vars[m.group(1)] = "%s" % m.group(2).strip() - continue + continue # predicate decl or formula with weight else: isHard = False @@ -710,7 +720,7 @@ def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic' for i, dom in enumerate(argdoms): if dom[-1] in ('!', '?'): if mutex is not None: - raise Exception('More than one arguments are specified as (soft-)functional') + raise Exception('More than one arguments are specified as (soft-)functional') if fuzzy: raise Exception('(Soft-)functional predicates must not be fuzzy.') mutex = i if dom[-1] == '?': softmutex = True @@ -726,7 +736,7 @@ def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic' fuzzy = False else: pred = Predicate(predname, argdoms) - if pseudofuzzy: + if pseudofuzzy: mln.fuzzypreds.append(predname) pseudofuzzy = False mln.predicate(pred) @@ -769,16 +779,15 @@ def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic' raise MLNParsingError(err) # augment domains with constants appearing in formula templates + cdef dict c_constants = {} for _, f in mln.iterformulas(): - constants = {} - f.vardoms(None, constants) - for domain, constants in constants.items(): + c_constants = {} + f.vardoms(None, c_constants) + for domain, constants in c_constants.items(): for c in constants: mln.constant(domain, c) - + # save data on formula templates for materialization # mln.uniqueFormulaExpansions = uniqueFormulaExpansions mln.templateIdx2GroupIdx = templateIdx2GroupIdx # mln.fixedWeightTemplateIndices = fixedWeightTemplateIndices return mln - - diff --git a/python3/pracmln/mln/grounding/.gitignore b/python3/pracmln/mln/grounding/.gitignore new file mode 100644 index 00000000..c246e93e --- /dev/null +++ b/python3/pracmln/mln/grounding/.gitignore @@ -0,0 +1,5 @@ +*.pyo +*.c +*.html +build/* +pracmln/* diff --git a/python3/pracmln/mln/grounding/bpll.py b/python3/pracmln/mln/grounding/bpll.py index 6472d9cb..9416a93a 100644 --- a/python3/pracmln/mln/grounding/bpll.py +++ b/python3/pracmln/mln/grounding/bpll.py @@ -31,6 +31,8 @@ from ..errors import SatisfiabilityException from ...utils.undo import Ref, Number, List, ListDict, Boolean from ...logic.common import Logic +from ...logic.common import Equality as Logic_Equality +from ...logic.common import Formula as Logic_Formula from ...utils.multicore import with_tracing, checkmem import types @@ -105,7 +107,7 @@ def eqvardoms(self, v=None, c=None): return v for child in children: - if isinstance(child, Logic.Equality): + if isinstance(child, Logic_Equality): setattr(child, 'vardoms', types.MethodType(eqvardoms, child)) lits = sorted(children, key=self._conjsort) for gf in self._itergroundings_fast(formula, lits, 0, assignment={}, variables=[]): @@ -126,7 +128,7 @@ def _itergroundings_fast(self, formula, constituents, cidx, assignment, variable # check if it violates a hard constraint if formula.weight == HARD and gnd(self.mrf.evidence) < 1: raise SatisfiabilityException('MLN is unsatisfiable by evidence due to hard constraint violation {} (see above)'.format(global_bpll_grounding.mrf.formulas[formula.idx])) - if isinstance(gnd, Logic.Equality): + if isinstance(gnd, Logic_Equality): # if an equality grounding is false in a conjunction, we can # stop since the conjunction cannot be rendered true in any # grounding that follows @@ -375,7 +377,7 @@ def __init__(self, formula, mrf): """ self.mrf = mrf self.costs = .0 - if isinstance(formula, Logic.Formula): + if isinstance(formula, Logic_Formula): self.formula = formula self.root = FormulaGrounding(formula, mrf) elif isinstance(formula, FormulaGrounding): @@ -533,7 +535,7 @@ def gndAtom2Assignment(self, lit, atom): """ Returns None if the literal and the atom do not match. """ - if type(lit) is Logic.Equality or \ + if type(lit) is Logic_Equality or \ lit.predName != atom.predName: return None assignment = {} diff --git a/python3/pracmln/mln/grounding/default.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/grounding/default.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..8b96e349 --- /dev/null +++ b/python3/pracmln/mln/grounding/default.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/grounding/default.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/grounding/default.pxd b/python3/pracmln/mln/grounding/default.pxd new file mode 100644 index 00000000..ce0166a2 --- /dev/null +++ b/python3/pracmln/mln/grounding/default.pxd @@ -0,0 +1,12 @@ +from ..mrf cimport MRF + +cdef class DefaultGroundingFactory(): + cdef public MRF mrf + cdef public list _cache + cdef int _cachesize + cdef int total_gf + cdef bint __cacheinit + cdef bint __cachecomplete + cdef dict _params + cdef dict __dict__ + #cdef itergroundings(self) diff --git a/python3/pracmln/mln/grounding/default.py b/python3/pracmln/mln/grounding/default.pyx similarity index 92% rename from python3/pracmln/mln/grounding/default.py rename to python3/pracmln/mln/grounding/default.pyx index 748d363a..87462473 100644 --- a/python3/pracmln/mln/grounding/default.py +++ b/python3/pracmln/mln/grounding/default.pyx @@ -33,17 +33,17 @@ CACHE_SIZE = 100000 -class DefaultGroundingFactory: +cdef class DefaultGroundingFactory(): """ Implementation of the default grounding algorithm, which creates ALL ground atoms and ALL ground formulas. :param simplify: if `True`, the formula will be simplified according to the evidence given. - :param unsatfailure: raises a :class:`mln.errors.SatisfiabilityException` if a + :param unsatfailure: raises a :class:`mln.errors.SatisfiabilityException` if a hard logical constraint is violated by the evidence. """ - + def __init__(self, mrf, simplify=False, unsatfailure=False, formulas=None, cache=auto, **params): self.mrf = mrf self.formulas = ifnone(formulas, list(self.mrf.formulas)) @@ -51,44 +51,49 @@ def __init__(self, mrf, simplify=False, unsatfailure=False, formulas=None, cache for f in self.formulas: self.total_gf += f.countgroundings(self.mrf) self.grounder = None + #print('cache={}'.format(cache)) + if cache is None: + cache = -2 self._cachesize = CACHE_SIZE if cache is auto else cache - self._cache = None + #print('_cachesize={}'.format(self._cachesize)) + self._cache = []#None self.__cacheinit = False self.__cachecomplete = False self._params = params self.watch = StopWatch() self.simplify = simplify self.unsatfailure = unsatfailure - - + + @property def verbose(self): return self._params.get('verbose', False) - - + + @property def multicore(self): return self._params.get('multicore', False) - - + + @property def iscached(self): - return self._cache is not None and self.__cacheinit + return self._cache != [] and self.__cacheinit + - @property def usecache(self): - return self._cachesize is not None and self._cachesize > 0 - - + return self._cachesize is not None and self._cachesize > 0# cache is now never none (-2 instead) + + def _cacheinit(self): + # Q(gsoc): never executed? "if False: ..." if False:#self.total_gf > self._cachesize: logger.warning('Number of formula groundings (%d) exceeds cache size (%d). Caching is disabled.' % (self.total_gf, self._cachesize)) else: self._cache = [] self.__cacheinit = True - - + + def itergroundings(self): """ Iterates over all formula groundings. @@ -98,7 +103,7 @@ def itergroundings(self): self.grounder = iter(self._itergroundings(simplify=self.simplify, unsatfailure=self.unsatfailure)) if self.usecache and not self.iscached: self._cacheinit() - counter = -1 + cdef int counter = -1 while True: counter += 1 if self.iscached and len(self._cache) > counter: @@ -110,16 +115,15 @@ def itergroundings(self): self.__cachecomplete = True return else: - if self._cache is not None: - self._cache.append(gf) + self._cache.append(gf) yield gf else: return self.watch.finish('grounding') if self.verbose: print() - - + + def _itergroundings(self, simplify=False, unsatfailure=False): - if self.verbose: + if self.verbose: bar = ProgressBar(color='green') for i, formula in enumerate(self.formulas): if self.verbose: bar.update((i+1) / float(len(self.formulas))) @@ -129,19 +133,19 @@ def _itergroundings(self, simplify=False, unsatfailure=False): gndformula.print_structure(self.mrf.evidence) raise SatisfiabilityException('MLN is unsatisfiable due to hard constraint violation %s (see above)' % self.mrf.formulas[gndformula.idx]) yield gndformula - - + + class EqualityConstraintGrounder(object): """ Grounding factory for equality constraints only. """ - + def __init__(self, mrf, domains, mode, eq_constraints): """ Initialize the equality constraint grounder with the given MLN and formula. A formula is required that contains all variables in the equalities in order to infer the respective domain names. - + :param mode: either ``alltrue`` or ``allfalse`` """ self.constraints = eq_constraints @@ -149,8 +153,8 @@ def __init__(self, mrf, domains, mode, eq_constraints): self.truth = {'alltrue': 1, 'allfalse': 0}[mode] self.mode = mode eqvars = [c for eq in eq_constraints for c in eq.args if self.mrf.mln.logic.isvar(c)] - self.vardomains = dict([(v, d) for v, d in domains.items() if v in eqvars]) - + self.vardomains = dict([(v, d) for v, d in domains.items() if v in eqvars]) + def iter_valid_variable_assignments(self): """ Yields all variable assignments for which all equality constraints @@ -158,9 +162,9 @@ def iter_valid_variable_assignments(self): """ return self._iter_valid_variable_assignments(list(self.vardomains.keys()), {}, self.constraints) - + def _iter_valid_variable_assignments(self, variables, assignments, eq_groundings): - if not variables: + if not variables: yield assignments return eq_groundings = [eq for eq in eq_groundings if not all([not self.mrf.mln.logic.isvar(a) for a in eq.args])] @@ -178,7 +182,7 @@ def _iter_valid_variable_assignments(self, variables, assignments, eq_groundings if not goon: continue for assignment in self._iter_valid_variable_assignments(variables[1:], dict_union(assignments, {variable: value}), new_eq_groundings): yield assignment - + @staticmethod def vardoms_from_formula(mln, formula, *varnames): if isinstance(formula, str): @@ -190,21 +194,3 @@ def vardoms_from_formula(mln, formula, *varnames): raise Exception('Variable %s not bound to a domain by formula %s' % (var, fstr(formula))) vardomains[var] = f_vardomains[var] return vardomains - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/python3/pracmln/mln/grounding/fastconj.py b/python3/pracmln/mln/grounding/fastconj.py index fef0d929..49c01465 100644 --- a/python3/pracmln/mln/grounding/fastconj.py +++ b/python3/pracmln/mln/grounding/fastconj.py @@ -31,6 +31,12 @@ from ..errors import SatisfiabilityException from ..constants import HARD from ...logic.common import Logic +from ...logic.common import TrueFalse as Logic_TrueFalse +from ...logic.common import Lit as Logic_Lit +from ...logic.common import GroundLit as Logic_GroundLit +from ...logic.common import Equality as Logic_Equality +from ...logic.common import Conjunction as Logic_Conjunction +from ...logic.common import Disjunction as Logic_Disjunction from ...logic.fuzzy import FuzzyLogic from ...utils.multicore import with_tracing from collections import defaultdict @@ -70,21 +76,21 @@ def __init__(self, mrf, simplify=False, unsatfailure=False, formulas=None, def _conjsort(self, e): - if isinstance(e, Logic.Equality): + if isinstance(e, Logic_Equality): return 0.5 - elif isinstance(e, Logic.TrueFalse): + elif isinstance(e, Logic_TrueFalse): return 1 - elif isinstance(e, Logic.GroundLit): + elif isinstance(e, Logic_GroundLit): if self.mrf.evidence[e.gndatom.idx] is not None: return 2 elif type(self.mrf.mln.predicate(e.gndatom.predname)) in (FunctionalPredicate, SoftFunctionalPredicate): return 3 else: return 4 - elif isinstance(e, Logic.Lit) and type( + elif isinstance(e, Logic_Lit) and type( self.mrf.mln.predicate(e.predname)) in (FunctionalPredicate, SoftFunctionalPredicate, FuzzyPredicate): return 5 - elif isinstance(e, Logic.Lit): + elif isinstance(e, Logic_Lit): return 6 else: return 7 @@ -120,27 +126,27 @@ def eqvardoms(self, v=None, c=None): for child in children: - if isinstance(child, Logic.Equality): + if isinstance(child, Logic_Equality): # replace the vardoms method in this equality instance by # our customized one setattr(child, 'vardoms', types.MethodType(eqvardoms, child)) lits = sorted(children, key=self._conjsort) - truthpivot, pivotfct = (1, FuzzyLogic.min_undef) if isinstance(formula, Logic.Conjunction) else ((0, FuzzyLogic.max_undef) if isinstance(formula, Logic.Disjunction) else (None, None)) + truthpivot, pivotfct = (1, FuzzyLogic.min_undef) if isinstance(formula, Logic_Conjunction) else ((0, FuzzyLogic.max_undef) if isinstance(formula, Logic_Disjunction) else (None, None)) for gf in self._itergroundings_fast(formula, lits, 0, pivotfct, truthpivot, {}): yield gf def _itergroundings_fast(self, formula, constituents, cidx, pivotfct, truthpivot, assignment, level=0): - if truthpivot == 0 and (isinstance(formula, Logic.Conjunction) or self.mrf.mln.logic.islit(formula)): + if truthpivot == 0 and (isinstance(formula, Logic_Conjunction) or self.mrf.mln.logic.islit(formula)): if formula.weight == HARD: raise SatisfiabilityException('MLN is unsatisfiable given evidence due to hard constraint violation: {}'.format(str(formula))) return - if truthpivot == 1 and (isinstance(formula, Logic.Disjunction) or self.mrf.mln.logic.islit(formula)): + if truthpivot == 1 and (isinstance(formula, Logic_Disjunction) or self.mrf.mln.logic.islit(formula)): return if cidx == len(constituents): # we have reached the end of the formula constituents gf = formula.ground(self.mrf, assignment, simplify=True) - if isinstance(gf, Logic.TrueFalse): + if isinstance(gf, Logic_TrueFalse): return yield gf return diff --git a/python3/pracmln/mln/grounding/setup.py b/python3/pracmln/mln/grounding/setup.py new file mode 100644 index 00000000..68e8a682 --- /dev/null +++ b/python3/pracmln/mln/grounding/setup.py @@ -0,0 +1,6 @@ +from distutils.core import setup +from Cython.Build import cythonize + +setup( + ext_modules=cythonize("*.pyx")#, compiler_directives={'profile': True}) +) diff --git a/python3/pracmln/mln/inference/.gitignore b/python3/pracmln/mln/inference/.gitignore index 24fae75e..c246e93e 100644 --- a/python3/pracmln/mln/inference/.gitignore +++ b/python3/pracmln/mln/inference/.gitignore @@ -1 +1,5 @@ -*.pyo \ No newline at end of file +*.pyo +*.c +*.html +build/* +pracmln/* diff --git a/python3/pracmln/mln/inference/exact.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/inference/exact.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..b3aba482 --- /dev/null +++ b/python3/pracmln/mln/inference/exact.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/inference/exact.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/inference/exact.py b/python3/pracmln/mln/inference/exact.py deleted file mode 100644 index a660551c..00000000 --- a/python3/pracmln/mln/inference/exact.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Markov Logic Networks -# -# (C) 2012-2015 by Daniel Nyga -# 2006-2011 by Dominik Jain -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -from dnutils import logs, ProgressBar - -from .infer import Inference -from multiprocessing import Pool -from ..mrfvars import FuzzyVariable -from ..constants import auto, HARD -from ..errors import SatisfiabilityException -from ..grounding.fastconj import FastConjunctionGrounding -from ..util import Interval, colorize -from ...utils.multicore import with_tracing -from ...logic.fol import FirstOrderLogic -from ...logic.common import Logic -from numpy.ma.core import exp - - -logger = logs.getlogger(__name__) - -# this readonly global is for multiprocessing to exploit copy-on-write -# on linux systems -global_enumAsk = None - - -def eval_queries(world): - """ - Evaluates the queries given a possible world. - """ - numerators = [0] * len(global_enumAsk.queries) - denominator = 0 - expsum = 0 - for gf in global_enumAsk.grounder.itergroundings(): - if global_enumAsk.soft_evidence_formula(gf): - expsum += gf.noisyor(world) * gf.weight - else: - truth = gf(world) - if gf.weight == HARD: - if truth in Interval(']0,1['): - raise Exception('No real-valued degrees of truth are allowed in hard constraints.') - if truth == 1: - continue - else: - return numerators, 0 - expsum += gf(world) * gf.weight - expsum = exp(expsum) - # update numerators - for i, query in enumerate(global_enumAsk.queries): - if query(world): - numerators[i] += expsum - denominator += expsum - return numerators, denominator - - -class EnumerationAsk(Inference): - """ - Inference based on enumeration of (only) the worlds compatible with the - evidence; supports soft evidence (assuming independence) - """ - - def __init__(self, mrf, queries, **params): - Inference.__init__(self, mrf, queries, **params) - self.grounder = FastConjunctionGrounding(mrf, simplify=False, unsatfailure=False, formulas=mrf.formulas, cache=auto, verbose=False, multicore=False) - # self.grounder = DefaultGroundingFactory(mrf, simplify=False, - # unsatfailure=False, formulas=list(mrf.formulas), cache=auto, - # verbose=False) - # check consistency of fuzzy and functional variables - for variable in self.mrf.variables: - variable.consistent(self.mrf.evidence, strict=isinstance(variable, FuzzyVariable)) - - - def _run(self): - """ - verbose: whether to print results (or anything at all, in fact) - details: (given that verbose is true) whether to output additional - status information - debug: (given that verbose is true) if true, outputs debug - information, in particular the distribution over possible - worlds - debugLevel: level of detail for debug mode - """ - # check consistency with hard constraints: - self._watch.tag('check hard constraints', verbose=self.verbose) - hcgrounder = FastConjunctionGrounding(self.mrf, simplify=False, unsatfailure=True, - formulas=[f for f in self.mrf.formulas if f.weight == HARD], - **(self._params + {'multicore': False, 'verbose': False})) - for gf in hcgrounder.itergroundings(): - if isinstance(gf, Logic.TrueFalse) and gf.truth() == .0: - raise SatisfiabilityException('MLN is unsatisfiable due to hard constraint violation by evidence: {} ({})'.format(str(gf), str(self.mln.formula(gf.idx)))) - self._watch.finish('check hard constraints') - # compute number of possible worlds - worlds = 1 - for variable in self.mrf.variables: - values = variable.valuecount(self.mrf.evidence) - worlds *= values - numerators = [0.0 for i in range(len(self.queries))] - denominator = 0. - # start summing - logger.debug("Summing over %d possible worlds..." % worlds) - if worlds > 500000 and self.verbose: - print(colorize('!!! %d WORLDS WILL BE ENUMERATED !!!' % worlds, (None, 'red', True), True)) - k = 0 - self._watch.tag('enumerating worlds', verbose=self.verbose) - global global_enumAsk - global_enumAsk = self - bar = None - if self.verbose: - bar = ProgressBar(steps=worlds, color='green') - if self.multicore: - pool = Pool() - logger.debug('Using multiprocessing on {} core(s)...'.format(pool._processes)) - try: - for num, denum in pool.imap(with_tracing(eval_queries), self.mrf.worlds()): - denominator += denum - k += 1 - for i, v in enumerate(num): - numerators[i] += v - if self.verbose: bar.inc() - except Exception as e: - logger.error('Error in child process. Terminating pool...') - pool.close() - raise e - finally: - pool.terminate() - pool.join() - else: # do it single core - for world in self.mrf.worlds(): - # compute exp. sum of weights for this world - num, denom = eval_queries(world) - denominator += denom - for i, _ in enumerate(self.queries): - numerators[i] += num[i] - k += 1 - if self.verbose: - bar.update(float(k) / worlds) - logger.debug("%d worlds enumerated" % k) - self._watch.finish('enumerating worlds') - if 'grounding' in self.grounder.watch.tags: - self._watch.tags['grounding'] = self.grounder.watch['grounding'] - if denominator == 0: - raise SatisfiabilityException( - 'MLN is unsatisfiable. All probability masses returned 0.') - # normalize answers - dist = [float(x) / denominator for x in numerators] - result = {} - for q, p in zip(self.queries, dist): - result[str(q)] = p - return result - - def soft_evidence_formula(self, gf): - truths = [a.truth(self.mrf.evidence) for a in gf.gndatoms()] - if None in truths: - return False - return isinstance(self.mrf.mln.logic, FirstOrderLogic) and any([t in Interval('(0,1)') for t in truths]) diff --git a/python3/pracmln/mln/inference/cy_exact.pyx b/python3/pracmln/mln/inference/exact.pyx similarity index 75% rename from python3/pracmln/mln/inference/cy_exact.pyx rename to python3/pracmln/mln/inference/exact.pyx index 36b07de3..73b7b1d0 100644 --- a/python3/pracmln/mln/inference/cy_exact.pyx +++ b/python3/pracmln/mln/inference/exact.pyx @@ -25,7 +25,7 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from dnutils import logs, ProgressBar -from .infer import Inference +from .infer cimport Inference from multiprocessing import Pool from ..mrfvars import FuzzyVariable from ..constants import auto, HARD @@ -36,7 +36,10 @@ from ...utils.multicore import with_tracing from ...logic.fol import FirstOrderLogic from ...logic.common import Logic from numpy.ma.core import exp +#from numpy import zeros +from cpython cimport array +import array logger = logs.getlogger(__name__) @@ -45,28 +48,32 @@ logger = logs.getlogger(__name__) global_enumAsk = None -cdef eval_queries(float* world): +cpdef eval_queries(world): ''' Evaluates the queries given a possible world. ''' - numerators = [0] * len(global_enumAsk.queries) - denominator = 0 - expsum = 0 + numerators = array.array('d', [0] * len(global_enumAsk.queries)) # zeros(len(global_enumAsk.queries)) #numerators = [0] * len(global_enumAsk.queries) + cdef double denominator = 0 + cdef double expsum = 0 + cdef double truth for gf in global_enumAsk.grounder.itergroundings(): if global_enumAsk.soft_evidence_formula(gf): expsum += gf.noisyor(world) * gf.weight else: truth = gf(world) if gf.weight == HARD: - if truth in Interval(']0,1['): + #if truth in Interval(']0,1['): + #if truth in Interval('(0,1)'): + if truth > 0 and truth < 1: raise Exception('No real-valued degrees of truth are allowed in hard constraints.') if truth == 1: continue else: return numerators, 0 - expsum += gf(world) * gf.weight + expsum += truth * gf.weight expsum = exp(expsum) # update numerators + cdef int i for i, query in enumerate(global_enumAsk.queries): if query(world): numerators[i] += expsum @@ -74,7 +81,7 @@ cdef eval_queries(float* world): return numerators, denominator -class EnumerationAsk(Inference): +cdef class EnumerationAsk(Inference): """ Inference based on enumeration of (only) the worlds compatible with the evidence; supports soft evidence (assuming independence) @@ -91,7 +98,7 @@ class EnumerationAsk(Inference): variable.consistent(self.mrf.evidence, strict=isinstance(variable, FuzzyVariable)) - def _run(self): + cpdef _run(self): """ verbose: whether to print results (or anything at all, in fact) details: (given that verbose is true) whether to output additional @@ -103,41 +110,44 @@ class EnumerationAsk(Inference): """ # check consistency with hard constraints: self._watch.tag('check hard constraints', verbose=self.verbose) - hcgrounder = FastConjunctionGrounding(self.mrf, simplify=False, unsatfailure=True, - formulas=[f for f in self.mrf.formulas if f.weight == HARD], + hcgrounder = FastConjunctionGrounding(self.mrf, simplify=False, unsatfailure=True, + formulas=[f for f in self.mrf.formulas if f.weight == HARD], **(self._params + {'multicore': False, 'verbose': False})) for gf in hcgrounder.itergroundings(): if isinstance(gf, Logic.TrueFalse) and gf.truth() == .0: raise SatisfiabilityException('MLN is unsatisfiable due to hard constraint violation by evidence: {} ({})'.format(str(gf), str(self.mln.formula(gf.idx)))) self._watch.finish('check hard constraints') # compute number of possible worlds - worlds = 1 + cdef int worlds = 1 + cdef int values for variable in self.mrf.variables: values = variable.valuecount(self.mrf.evidence) worlds *= values - numerators = [0.0 for i in range(len(self.queries))] - denominator = 0. + numerators = array.array('d', [0] * len(self.queries))#zeros(len(self.queries))#numerators = [0.0 for i in range(len(self.queries))] + cdef double denominator = 0. # start summing logger.debug("Summing over %d possible worlds..." % worlds) if worlds > 500000 and self.verbose: print(colorize('!!! %d WORLDS WILL BE ENUMERATED !!!' % worlds, (None, 'red', True), True)) - k = 0 + cdef int k = 0 self._watch.tag('enumerating worlds', verbose=self.verbose) global global_enumAsk global_enumAsk = self - bar = None + bar = None # ??? if self.verbose: bar = ProgressBar(steps=worlds, color='green') if self.multicore: pool = Pool() logger.debug('Using multiprocessing on {} core(s)...'.format(pool._processes)) try: - for num, denum in pool.imap(with_tracing(eval_queries), self.mrf.worlds()): - denominator += denum + for num, denom in pool.imap(with_tracing(eval_queries), self.mrf.worlds()): + denominator += denom k += 1 + #numerators += num # assume length is the same - ie arrays have same shape/dimension? for i, v in enumerate(num): numerators[i] += v - if self.verbose: bar.inc() + if self.verbose: + bar.inc() except Exception as e: logger.error('Error in child process. Terminating pool...') pool.close() @@ -150,7 +160,9 @@ class EnumerationAsk(Inference): # compute exp. sum of weights for this world num, denom = eval_queries(world) denominator += denom - for i, _ in enumerate(self.queries): + #numerators += num + #for i, _ in enumerate(self.queries): + for i in range(len(self.queries)): numerators[i] += num[i] k += 1 if self.verbose: @@ -163,14 +175,25 @@ class EnumerationAsk(Inference): raise SatisfiabilityException( 'MLN is unsatisfiable. All probability masses returned 0.') # normalize answers - dist = [float(x) / denominator for x in numerators] + #dist = numerators / denominator + + cdef array.array dist = array.array('d', [float(x) / denominator for x in numerators]) result = {} for q, p in zip(self.queries, dist): result[str(q)] = p return result - def soft_evidence_formula(self, gf): - truths = [a.truth(self.mrf.evidence) for a in gf.gndatoms()] - if None in truths: - return False - return isinstance(self.mrf.mln.logic, FirstOrderLogic) and any([t in Interval('(0,1)') for t in truths]) + cpdef bint soft_evidence_formula(self, gf): + #print('result={}'.format(gf.gndatoms())) + # Q(gsoc): does truths ever have non None values? + cdef array.array truths = array.array('d', [-1] * len(gf.gndatoms())) + cdef int i = 0 + for i, a in enumerate(gf.gndatoms()): + result = a.truth(self.mrf.evidence) + if result is None: + return False + truths[i] = result + #truths = [a.truth(self.mrf.evidence) for a in gf.gndatoms()] + #if None in truths: + # return False + return isinstance(self.mrf.mln.logic, FirstOrderLogic) and any([ ( t>0 and t<1 ) for t in truths]) diff --git a/python3/pracmln/mln/inference/gibbs.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/inference/gibbs.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..feb12e75 --- /dev/null +++ b/python3/pracmln/mln/inference/gibbs.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/inference/gibbs.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/inference/gibbs.py b/python3/pracmln/mln/inference/gibbs.pyx similarity index 93% rename from python3/pracmln/mln/inference/gibbs.py rename to python3/pracmln/mln/inference/gibbs.pyx index 66bc1754..dbd203de 100644 --- a/python3/pracmln/mln/inference/gibbs.py +++ b/python3/pracmln/mln/inference/gibbs.pyx @@ -33,6 +33,8 @@ from ..constants import ALL from ..grounding.fastconj import FastConjunctionGrounding from ...logic.common import Logic +from ...logic.common import TrueFalse as Logic_TrueFalse +from numpy import zeros class GibbsSampler(MCMCInference): @@ -42,7 +44,8 @@ def __init__(self, mrf, queries=ALL, **params): self.var2gf = defaultdict(set) grounder = FastConjunctionGrounding(mrf, simplify=True, unsatfailure=True, cache=None) for gf in grounder.itergroundings(): - if isinstance(gf, Logic.TrueFalse): continue + if isinstance(gf, Logic_TrueFalse): + continue vars_ = set([self.mrf.variable(a).idx for a in gf.gndatoms()]) for v in vars_: self.var2gf[v].add(gf) @@ -62,7 +65,8 @@ def __init__(self, infer, queries): mrf = infer.mrf def _valueprobs(self, var, world): - sums = [0] * var.valuecount() + sums = [0] * var.valuecount() # Q(gsoc): TODO [can be turned into NumPy array, but seem to be breaking the logic right now] + cdef int i for gf in self.infer.var2gf[var.idx]: possible_values = [] for i, value in var.itervalues(self.infer.mrf.evidence): @@ -103,7 +107,7 @@ def step(self): # elif p < belief and expsums[0] > 0: # idx = 0 # sample value - if idx is None: + if idx is None: # Q(gsoc): redundant test, idx was just set =None on line 97... r = random.uniform(0, 1) idx = 0 s = probs[0] @@ -128,6 +132,7 @@ def _run(self, **params): # self.softEvidence = softEvidence # initialize chains chains = MCMCInference.ChainGroup(self) + cdef int i for i in range(self.chains): chain = GibbsSampler.Chain(self, self.queries) chains.chain(chain) @@ -135,8 +140,8 @@ def _run(self, **params): # chain.setSoftEvidence(self.softEvidence) # do Gibbs sampling # if verbose and details: print "sampling..." - converged = 0 - steps = 0 + cdef int converged = 0 + cdef int steps = 0 if self.verbose: bar = ProgressBar(color='green', steps=self.maxsteps) while converged != self.chains and steps < self.maxsteps: diff --git a/python3/pracmln/mln/inference/infer.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/inference/infer.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..3df5b7ea --- /dev/null +++ b/python3/pracmln/mln/inference/infer.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/inference/infer.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/inference/infer.pxd b/python3/pracmln/mln/inference/infer.pxd new file mode 100644 index 00000000..bc0e19f1 --- /dev/null +++ b/python3/pracmln/mln/inference/infer.pxd @@ -0,0 +1,7 @@ +from ..mrf cimport MRF +from ..base cimport MLN + +cdef class Inference: + cdef public MRF mrf #public MRF mrf + cdef MLN mln + cdef dict __dict__ diff --git a/python3/pracmln/mln/inference/infer.py b/python3/pracmln/mln/inference/infer.pyx similarity index 98% rename from python3/pracmln/mln/inference/infer.py rename to python3/pracmln/mln/inference/infer.pyx index 9e5ebf3e..26b2efa2 100644 --- a/python3/pracmln/mln/inference/infer.py +++ b/python3/pracmln/mln/inference/infer.pyx @@ -37,7 +37,7 @@ logger = logs.getlogger(__name__) -class Inference(object): +cdef class Inference(): """ Represents a super class for all inference methods. Also provides some convenience methods for collecting statistics @@ -57,6 +57,7 @@ class Inference(object): def __init__(self, mrf, queries=ALL, **params): self.mrf = mrf + #print(self.mrf) self.mln = mrf.mln self._params = edict(params) if not queries: @@ -161,7 +162,7 @@ def _expand_queries(self, queries): else: # just a predicate name if query not in self.mln.prednames: raise NoSuchPredicateError('Unsupported query: %s is not among the admissible predicates.' % (query)) - continue + #continue for gndatom in self.mln.predicate(query).groundatoms(self.mln, self.mrf.domains): equeries.append(self.mln.logic.gnd_lit(self.mrf.gndatom(gndatom), negated=False, mln=self.mln)) if len(equeries) - prevLen == 0: @@ -193,12 +194,13 @@ def run(self): def write(self, stream=sys.stdout, color=None, sort='prob', group=True, reverse=True): - barwidth = 30 + cdef int barwidth = 30 if tty(stream) and color is None: color = 'yellow' if sort not in ('alpha', 'prob'): raise Exception('Unknown sorting: %s' % sort) results = dict(self.results) + cdef bint wrote_results if group: wrote_results = False for var in sorted(self.mrf.variables, key=str): diff --git a/python3/pracmln/mln/inference/ipfpm.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/inference/ipfpm.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..bd427077 --- /dev/null +++ b/python3/pracmln/mln/inference/ipfpm.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/inference/ipfpm.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/inference/ipfpm.py b/python3/pracmln/mln/inference/ipfpm.pyx similarity index 100% rename from python3/pracmln/mln/inference/ipfpm.py rename to python3/pracmln/mln/inference/ipfpm.pyx diff --git a/python3/pracmln/mln/inference/maxwalk.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/inference/maxwalk.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..d0e1665c --- /dev/null +++ b/python3/pracmln/mln/inference/maxwalk.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/inference/maxwalk.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/inference/maxwalk.py b/python3/pracmln/mln/inference/maxwalk.pyx similarity index 90% rename from python3/pracmln/mln/inference/maxwalk.py rename to python3/pracmln/mln/inference/maxwalk.pyx index 713a3273..2490a71c 100644 --- a/python3/pracmln/mln/inference/maxwalk.py +++ b/python3/pracmln/mln/inference/maxwalk.pyx @@ -33,6 +33,7 @@ from ..constants import HARD, ALL from ..grounding.fastconj import FastConjunctionGrounding from ...logic.common import Logic +from ...logic.common import TrueFalse as Logic_TrueFalse class SAMaxWalkSAT(MCMCInference): @@ -58,7 +59,7 @@ def __init__(self, mrf, queries=ALL, state=None, **params): formulas.append(f_.nnf()) grounder = FastConjunctionGrounding(mrf, formulas=formulas, simplify=True, unsatfailure=True) for gf in grounder.itergroundings(): - if isinstance(gf, Logic.TrueFalse): continue + if isinstance(gf, Logic_TrueFalse): continue vars_ = set([self.mrf.variable(a).idx for a in gf.gndatoms()]) for v in vars_: self.var2gf[v].add(gf) self.sum += (self.hardw if gf.weight == HARD else gf.weight) * (1 - gf(self.state)) @@ -80,14 +81,20 @@ def maxsteps(self): def _run(self): - i = 0 - i_max = self.maxsteps + cdef int i = 0 + cdef int i_max = self.maxsteps thr = self.thr if self.verbose: bar = ProgressBar(steps=i_max, color='green') + cdef int sum_before = 0 + cdef int sum_after = 0 + cdef bint keep = False + cdef int improvement + cdef double prob + cdef int valuecount while i < i_max and self.sum > self.thr: # randomly choose a variable to modify - var = self.mrf.variables[random.randint(0, len(self.mrf.variables)-1)] + var = random.choice(self.mrf.variables)#[random.randint(0, len(self.mrf.variables)-1)] evdict = var.value2dict(var.evidence_value(self.mrf.evidence)) valuecount = var.valuecount(evdict) if valuecount == 1: # this is evidence @@ -98,7 +105,7 @@ def _run(self): sum_before += (self.hardw if gf.weight == HARD else gf.weight) * (1 - gf(self.state)) # modify the state validx = random.randint(0, valuecount - 1) - value = [v for _, v in var.itervalues(evdict)][validx] + value = [v for _, v in var.itervalues(evdict)][validx] # Q(gsoc): can replace the use of validx with random.choice oldstate = list(self.state) var.setval(value, self.state) # compute the sum after the modification diff --git a/python3/pracmln/mln/inference/mcmc.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/inference/mcmc.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..e5324f0a --- /dev/null +++ b/python3/pracmln/mln/inference/mcmc.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/inference/mcmc.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/inference/mcmc.py b/python3/pracmln/mln/inference/mcmc.pyx similarity index 89% rename from python3/pracmln/mln/inference/mcmc.py rename to python3/pracmln/mln/inference/mcmc.pyx index 078606fc..a808aec1 100644 --- a/python3/pracmln/mln/inference/mcmc.py +++ b/python3/pracmln/mln/inference/mcmc.pyx @@ -31,6 +31,7 @@ from .infer import Inference from ..util import fstr from ..constants import ALL +from numpy import zeros logger = logs.getlogger(__name__) @@ -49,6 +50,7 @@ def random_world(self, evidence=None): """ Get a random possible world, taking the evidence into account. """ + # "type" of evidence? if evidence is None: world = list(self.mrf.evidence) else: @@ -69,12 +71,16 @@ class Chain: Represents the state of a Markov Chain. """ + #cdef public int steps + #cdef public bint converged + #cdef public int lastresult + # Q(gsoc): other types? def __init__(self, infer, queries): self.queries = queries self.soft_evidence = None self.steps = 0 - self.truths = [0] * len(self.queries) + self.truths = zeros(len(self.queries)) self.converged = False self.lastresult = 10 self.infer = infer @@ -114,16 +120,13 @@ def set_soft_evidence(self, soft_evidence): def soft_evidence_frequency(self, formula): - if self.steps == 0: return 0 + if self.steps == 0: + return 0 return float(self.softev_counts[fstr(formula)]) / self.steps def results(self): - results = [] - for i in range(len(self.queries)): - results.append(float(self.truths[i]) / self.steps) - return results - + return self.truths/self.steps class ChainGroup: @@ -137,16 +140,17 @@ def chain(self, chain): def results(self): - chains = float(len(self.chains)) + cdef double chains = float(len(self.chains)) + cdef int i queries = self.chains[0].queries # compute average - results = [0.0] * len(queries) + results = [0.0] * len(queries) # Q(gsoc): zeros(len(queries)) for chain in self.chains: cr = chain.results() for i in range(len(queries)): results[i] += cr[i] / chains # compute variance - var = [0.0 for i in range(len(queries))] + var = [0.0] * len(queries)# Q(gsoc): zeros(len(queries))# for i in range(len(queries))] for chain in self.chains: cr = chain.results() for i in range(len(self.chains[0].queries)): @@ -156,7 +160,8 @@ def results(self): def avgtruth(self, formula): """ returns the fraction of chains in which the given formula is currently true """ - t = 0.0 + # Q(gsoc): t can byped as int? + t = 0.0 for c in self.chains: t += formula(c.state) return t / len(self.chains) diff --git a/python3/pracmln/mln/inference/mcsat.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/inference/mcsat.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..dd2f5746 --- /dev/null +++ b/python3/pracmln/mln/inference/mcsat.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/inference/mcsat.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/inference/mcsat.py b/python3/pracmln/mln/inference/mcsat.pyx similarity index 94% rename from python3/pracmln/mln/inference/mcsat.py rename to python3/pracmln/mln/inference/mcsat.pyx index 756614eb..0c47c87f 100644 --- a/python3/pracmln/mln/inference/mcsat.py +++ b/python3/pracmln/mln/inference/mcsat.pyx @@ -35,6 +35,9 @@ from ..grounding.fastconj import FastConjunctionGrounding from ..util import item from ...logic.common import Logic +from ...logic.common import TrueFalse as Logic_TrueFalse +from ...logic.common import GroundCountConstraint as Logic_GroundCountConstraint +from ...logic.common import Conjunction as Logic_Conjunction logger = logs.getlogger(__name__) @@ -73,7 +76,7 @@ def _initkb(self, verbose=False): grounder = FastConjunctionGrounding(self.mrf, formulas=self.formulas, simplify=True, verbose=self.verbose) self.gndformulas = [] for gf in grounder.itergroundings(): - if isinstance(gf, Logic.TrueFalse): continue + if isinstance(gf, Logic_TrueFalse): continue self.gndformulas.append(gf.cnf()) self._watch.tags.update(grounder.watch.tags) # self.gndformulas, self.formulas = Logic.cnf(grounder.itergroundings(), self.mln.formulas, self.mln.logic, allpos=True) @@ -82,15 +85,17 @@ def _initkb(self, verbose=False): self.gf2clauseidx = {} # ground formula index -> tuple (idxFirstClause, idxLastClause+1) for use with range self.clauses = [] # list of clauses, where each entry is a list of ground literals #self.GAoccurrences = {} # ground atom index -> list of clause indices (into self.clauses) - i_clause = 0 + cdef int i_clause = 0 + cdef int i_gf = 0 # process all ground formulas for i_gf, gf in enumerate(self.gndformulas): # get the list of clauses - if isinstance(gf, Logic.Conjunction): - clauses = [clause for clause in gf.children if not isinstance(clause, Logic.TrueFalse)] - elif not isinstance(gf, Logic.TrueFalse): + if isinstance(gf, Logic_Conjunction): + clauses = [clause for clause in gf.children if not isinstance(clause, Logic_TrueFalse)] + elif not isinstance(gf, Logic_TrueFalse): clauses = [gf] - else: continue + else: + continue self.gf2clauseidx[i_gf] = (i_clause, i_clause + len(clauses)) # process each clause for c in clauses: @@ -103,7 +108,9 @@ def _initkb(self, verbose=False): # next clause index i_clause += 1 # add clauses for soft evidence atoms + # Q(gsoc): the following code never executes? for se in []:#self.softEvidence: + print('Q(gsoc): ~un~reachable statement in line 110, mcsat.pyx') se["numTrue"] = 0.0 formula = self.mln.logic.parseFormula(se["expr"]) se["formula"] = formula.ground(self.mrf, {}) @@ -125,7 +132,7 @@ def _initkb(self, verbose=False): def _formula_clauses(self, f): # get the list of clauses - if isinstance(f, Logic.Conjunction): + if isinstance(f, Logic_Conjunction): lc = f.children else: lc = [f] @@ -205,6 +212,7 @@ def _run(self): # create chains chaingroup = MCMCInference.ChainGroup(self) self.chaingroup = chaingroup + cdef int i for i in range(self.chains): chain = MCMCInference.Chain(self, self.queries) chaingroup.chain(chain) @@ -264,6 +272,7 @@ def _satisfy_subset(self, chain): else: NLC.append(gf) # add soft evidence constraints + # Q(gsoc): the following code is actually unreachable ... if False:# self.softevidence: for se in self.softevidence: p = se["numTrue"] / self.step @@ -295,7 +304,10 @@ def _satisfy_subset(self, chain): def _prob_constraints_deviation(self): if len(self.softevidence) == 0: return {} - se_mean, se_max, se_max_item = 0.0, -1, None + cdef double se_mean = 0.0 + cdef double se_max = 0 + se_max_item = None + cdef double dev for se in self.softevidence: dev = abs((se["numTrue"] / self.step) - se["p"]) se_mean += dev @@ -360,7 +372,7 @@ def __init__(self, mrf, state, clause_indices, nlcs, infer, p=1): # stop('clause', 'v'.join(map(str, self.infer.clauses[cidx])), 'is', 'unsatisfied' if clause.unsatisfied else 'satisfied') # instantiate non-logical constraints for nlc in nlcs: - if isinstance(nlc, Logic.GroundCountConstraint): # count constraint + if isinstance(nlc, Logic_GroundCountConstraint): # count constraint SampleSAT._CountConstraint(self, nlc) else: raise Exception("SampleSAT cannot handle constraints of type '%s'" % str(type(nlc))) @@ -379,10 +391,12 @@ def run(self): if not clause.satisfied_in_world(world): skip = True break - if skip: continue + if skip: + continue worlds.append(world) state = worlds[random.randint(0, len(worlds)-1)] return state + # Q(gsoc): the following code is actually unreachable ... steps = 0 while self.unsatisfied: steps += 1 @@ -397,7 +411,7 @@ def run(self): def _walksat_move(self): """ Randomly pick one of the unsatisfied constraints and satisfy it - (or at least make one step towards satisfying it + (or at least make one step towards satisfying it) """ clauseidx = list(self.unsatisfied)[random.randint(0, len(self.unsatisfied) - 1)] # get the literal that makes the fewest other formulas false @@ -443,22 +457,25 @@ def _setvar(self, var, val): def _sa_move(self): # randomly pick a variable and flip its value variables = list(set(self.var2clauses)) - random.shuffle(variables) - var = variables[0] + var = random.choice(variables) ev = var.evidence_value() values = var.valuecount(self.mrf.evidence) - for _, v in var.itervalues(self.mrf.evidence): break + for _, v in var.itervalues(self.mrf.evidence): break # Q(gsoc): this statement is effectively like a no-op ... if values == 1: raise Exception('Only one remaining value for variable %s: %s. Please check your evidences.' % (var, v)) values = [v for _, v in var.itervalues(self.mrf.evidence) if v != ev] - val = values[random.randint(0, len(values)-1)] - unsat = 0 + val = random.choice(values)#values[random.randint(0, len(values)-1)] + cdef int unsat = 0 bottleneck_clauses = [c for c in self.var2clauses[var] if c.bottleneck is not None] for c in bottleneck_clauses: # count the constraints rendered unsatisfied for this value from the bottleneck clauses - uns = 1 if c.turns_false_with(var, val) else 0 + #cdef int uns + #uns = 1 if c.turns_false_with(var, val) else 0 # cur = 1 if c.unsatisfied else 0 - unsat += uns# - cur + #unsat += uns# - cur + if c.turns_false_with(var, val): + unsat += 1 + if unsat <= 0: # the flip causes an improvement. take it with p=1.0 p = 1. else: @@ -485,7 +502,7 @@ def __init__(self, lits, world, idx, mrf): self.truelits = set() self.atomidx2lits = defaultdict(set) for lit in lits: - if isinstance(lit, Logic.TrueFalse): continue + if isinstance(lit, Logic_TrueFalse): continue atomidx = lit.gndatom.idx self.atomidx2lits[atomidx].add(0 if lit.negated else 1) if lit(world) == 1: @@ -601,7 +618,7 @@ def _addBottlenecks(self): self.ss._addBottleneck(idxGA, self) def greedySatisfy(self): - c = len(self.trueOnes) + cdef int c = len(self.trueOnes) satisfied = self._isSatisfied() assert not satisfied if c < self.cc.count and not satisfied: @@ -614,7 +631,8 @@ def greedySatisfy(self): def flipSatisfies(self, idxGA): if self._isSatisfied(): return False - c = len(self.trueOnes) + cdef int c = len(self.trueOnes) + cdef int c2 if idxGA in self.trueOnes: c2 = c - 1 else: diff --git a/python3/pracmln/mln/inference/setup.py b/python3/pracmln/mln/inference/setup.py new file mode 100644 index 00000000..da01a8d0 --- /dev/null +++ b/python3/pracmln/mln/inference/setup.py @@ -0,0 +1,7 @@ +from distutils.core import setup +#from distutils.extension import Extension +from Cython.Build import cythonize + +setup( + ext_modules=cythonize("*.pyx")#, compiler_directives={'profile': True}) +) diff --git a/python3/pracmln/mln/inference/wcspinfer.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/inference/wcspinfer.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..53729190 --- /dev/null +++ b/python3/pracmln/mln/inference/wcspinfer.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/inference/wcspinfer.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/inference/wcspinfer.py b/python3/pracmln/mln/inference/wcspinfer.pyx similarity index 94% rename from python3/pracmln/mln/inference/wcspinfer.py rename to python3/pracmln/mln/inference/wcspinfer.pyx index 4f888bfd..0324812a 100644 --- a/python3/pracmln/mln/inference/wcspinfer.py +++ b/python3/pracmln/mln/inference/wcspinfer.pyx @@ -32,7 +32,8 @@ from ..util import (combinations, dict_union, Interval, temporary_evidence) from ...wcsp import Constraint, WCSP from ...logic.common import Logic - +from ...logic.common import TrueFalse as Logic_TrueFalse +from ...logic.common import GroundAtom as Logic_GroundAtom logger = logs.getlogger(__name__) @@ -96,7 +97,7 @@ def _createvars(self): self.domains = defaultdict(list) # maps a var index to a list of its MRF variable value tuples self.atom2var = {} # maps ground atom indices to their variable index self.val2idx = defaultdict(dict) - varidx = 0 + cdef int varidx = 0 for variable in self.mrf.variables: if isinstance(variable, FuzzyVariable): # fuzzy variables are not subject to reasoning continue @@ -134,7 +135,7 @@ def convert(self): # grounder = DefaultGroundingFactory(self.mrf, formulas=formulas, simplify=True, unsatfailure=True, multicore=self.multicore, verbose=self.verbose) grounder = FastConjunctionGrounding(self.mrf, simplify=True, unsatfailure=True, formulas=formulas, multicore=self.multicore, verbose=self.verbose, cache=0) for gf in grounder.itergroundings(): - if isinstance(gf, Logic.TrueFalse): + if isinstance(gf, Logic_TrueFalse): if gf.weight == HARD and gf.truth() == 0: raise SatisfiabilityException('MLN is unsatisfiable: hard constraint %s violated' % self.mrf.mln.formulas[gf.idx]) else:# formula is rendered true/false by the evidence -> equal in every possible world @@ -180,21 +181,23 @@ def _gather_constraint_tuples(self, varindices, formula): """ logic = self.mrf.mln.logic # we can treat conjunctions and disjunctions fairly efficiently - defaultProcedure = False + cdef bint defaultProcedure = False + cdef bint disj = False # Q(gsoc): guessing that this variable is boolean + cdef bint conj = False # Q(gsoc): guessing that this variable is boolean conj = logic.islitconj(formula) - disj = False if not conj: disj = logic.isclause(formula) if not varindices: return None if not conj and not disj: defaultProcedure = True + cdef long worlds = 1 # Q(gsoc): will long be enough to store all possible worlds? if not defaultProcedure: assignment = [None] * len(varindices) children = list(formula.literals()) for gndlit in children: # constants are handled in the maxtruth/mintruth calls below - if isinstance(gndlit, Logic.TrueFalse): continue + if isinstance(gndlit, Logic_TrueFalse): continue # get the value of the gndlit that renders the formula true (conj) or false (disj): # for a conjunction, the literal must be true, # for a disjunction, it must be false. @@ -242,7 +245,7 @@ def _gather_constraint_tuples(self, varindices, formula): defcost = formula.weight else: if formula.weight == HARD: - cost = self.wcsp.top + cost = self.wcsp.top # Q(gsoc): can cost be typed as an int? defcost = 0 else: defcost = 0 @@ -250,23 +253,23 @@ def _gather_constraint_tuples(self, varindices, formula): if len(assignment) != len(varindices): raise MRFValueException('Illegal variable assignments. Variables: %s, Assignment: %s' % (varindices, assignment)) return {cost: [tuple(assignment)], defcost: 'else'} - if defaultProcedure: + else: # fallback: go through all combinations of truth assignments domains = [self.domains[v] for v in varindices] cost2assignments = defaultdict(list) # compute number of worlds to be examined and print a warning - worlds = 1 + worlds = 1 # Q(gsoc): will long be enough to store all possible worlds? for d in domains: worlds *= len(d) if worlds > 1000000: logger.warning('!!! WARNING: %d POSSIBLE WORLDS ARE GOING TO BE EVALUATED. KEEP IN SIGHT YOUR MEMORY CONSUMPTION !!!' % worlds) for c in combinations(domains): - world = [0] * len(self.mrf.gndatoms) + world = [0] * len(self.mrf.gndatoms) assignment = [] for varidx, value in zip(varindices, c): world = self.variables[varidx].setval(value, world) assignment.append(self.val2idx[varidx][value]) # the MRF feature imposed by this formula - truth = formula(world) + truth = formula(world) # Q(gsoc): can world be converted to a numpy array? if truth is None: print('POSSIBLE WORLD:') print('===============') @@ -282,7 +285,7 @@ def _gather_constraint_tuples(self, varindices, formula): if truth == 1: cost = 0 else: - cost = self.wcsp.top + cost = self.wcsp.top # Q(gsoc): can cost be typed as an int? else: cost = ((1 - truth) * formula.weight) cost2assignments[cost].append(tuple(assignment)) @@ -316,7 +319,7 @@ def getPseudoDistributionForGndAtom(self, gndAtom): if isinstance(gndAtom, str): gndAtom = self.mrf.gndAtoms[gndAtom] - if not isinstance(gndAtom, Logic.GroundAtom): + if not isinstance(gndAtom, Logic_GroundAtom): raise Exception('Argument must be a ground atom') varIdx = self.gndAtom2VarIndex[gndAtom] @@ -362,4 +365,4 @@ def getPseudoDistributionForGndAtom(self, gndAtom): # print conv.varIdx2GndAtom[i][0], s, # for ga in conv.varIdx2GndAtom[i]: print ga, # print - \ No newline at end of file + diff --git a/python3/pracmln/mln/learning/cll.py b/python3/pracmln/mln/learning/cll.py index dc1cce5e..37fe4556 100644 --- a/python3/pracmln/mln/learning/cll.py +++ b/python3/pracmln/mln/learning/cll.py @@ -29,6 +29,8 @@ from numpy.ma.core import log, sqrt import numpy from ...logic.common import Logic +from ...logic.common import Equality as Logic_Equality +from ...logic.common import GroundAtom as Logic_GroundAtom from ..constants import HARD from ..errors import SatisfiabilityException @@ -121,7 +123,7 @@ def _compute_statistics(self): # equality constraints are evaluated first isconj = self.mrf.mln.logic.islitconj(formula) if isconj: - literals = sorted(literals, key=lambda l: -1 if isinstance(l, Logic.Equality) else 1) + literals = sorted(literals, key=lambda l: -1 if isinstance(l, Logic_Equality) else 1) self._compute_stat_rec(literals, [], {}, formula, isconj=isconj) @@ -137,7 +139,7 @@ def _compute_stat_rec(self, literals, gndliterals, var_assign, formula, f_gndlit part2gndlits = defaultdict(list) part_with_f_lit = None for gndlit in gndliterals: - if isinstance(gndlit, Logic.Equality) or hasattr(self, 'qpreds') and gndlit.gndatom.predname not in self.qpreds: continue + if isinstance(gndlit, Logic_Equality) or hasattr(self, 'qpreds') and gndlit.gndatom.predname not in self.qpreds: continue part = self.atomidx2partition[gndlit.gndatom.idx] part2gndlits[part].append(gndlit) if gndlit(self.mrf.evidence) == 0: @@ -187,7 +189,7 @@ def _compute_stat_rec(self, literals, gndliterals, var_assign, formula, f_gndlit lit = literals[0] # ground the literal with the existing assignments gndlit = lit.ground(self.mrf, var_assign, partial=True) - for assign in Logic.iter_eq_varassignments(gndlit, formula, self.mrf) if isinstance(gndlit, Logic.Equality) else gndlit.itervargroundings(self.mrf): + for assign in Logic.iter_eq_varassignments(gndlit, formula, self.mrf) if isinstance(gndlit, Logic_Equality) else gndlit.itervargroundings(self.mrf): # copy the arguments to avoid side effects # if f_gndlit_parts is None: f_gndlit_parts = set() # else: f_gndlit_parts = set(f_gndlit_parts) @@ -197,7 +199,7 @@ def _compute_stat_rec(self, literals, gndliterals, var_assign, formula, f_gndlit gndlit_ = gndlit.ground(self.mrf, assign) truth = gndlit_(self.mrf.evidence) # treatment of equality constraints - if isinstance(gndlit_, Logic.Equality): + if isinstance(gndlit_, Logic_Equality): if isconj: if truth == 1: self._compute_stat_rec(literals[1:], gndliterals, dict_union(var_assign, assign), formula, f_gndlit_parts, processed, isconj) @@ -309,7 +311,7 @@ def __contains__(self, atom): Returns True iff the given ground atom or ground atom index is part of this partition. """ - if isinstance(atom, Logic.GroundAtom): + if isinstance(atom, Logic_GroundAtom): return atom in self.gndatoms elif type(atom) is int: return self.mrf.gndatom(atom) in self diff --git a/python3/pracmln/mln/mlnpreds.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/mlnpreds.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..11522b52 --- /dev/null +++ b/python3/pracmln/mln/mlnpreds.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/mlnpreds.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/mlnpreds.pxd b/python3/pracmln/mln/mlnpreds.pxd new file mode 100644 index 00000000..be8f880a --- /dev/null +++ b/python3/pracmln/mln/mlnpreds.pxd @@ -0,0 +1,3 @@ +cdef class Predicate: + cdef public str name + cdef public list argdoms diff --git a/python3/pracmln/mln/mlnpreds.py b/python3/pracmln/mln/mlnpreds.pyx similarity index 91% rename from python3/pracmln/mln/mlnpreds.py rename to python3/pracmln/mln/mlnpreds.pyx index 975cbdda..b7e099ad 100644 --- a/python3/pracmln/mln/mlnpreds.py +++ b/python3/pracmln/mln/mlnpreds.pyx @@ -1,7 +1,7 @@ -# +# # # (C) 2011-2015 by Daniel Nyga (nyga@cs.uni-bremen.de) -# +# # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including @@ -22,61 +22,61 @@ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from dnutils import logs -from pracmln.mln.mrfvars import (BinaryVariable, FuzzyVariable, SoftMutexVariable, +from .mrfvars import (BinaryVariable, FuzzyVariable, SoftMutexVariable, MutexVariable) logger = logs.getlogger(__name__) -class Predicate(object): +cdef class Predicate(): """ Represents a logical predicate and its properties. - + :param predname: the name of the predicate. :param argdoms: the list of domains of the predicate's arguments. """ - - def __init__(self, name, argdoms): + + def __init__(self, str name, list argdoms): self.argdoms = argdoms self.name = name - - + + def varname(self, gndatom): """ Takes an instance of a ground atom and generates the name of the corresponding variable. """ return str(gndatom) - - + + def tovariable(self, mrf, gndatom): """ Creates a new instance of an atomic ground block instance depending on the type of the predicate """ return BinaryVariable(mrf, name=self.varname(gndatom), predicate=self) - - + + def groundatoms(self, mln, domains): """ Iterates over all ground atoms that can be generated by this predicate given the domains and the MLN. - + :param domains: dict mapping the domain names to their values. """ for gndatom in self._groundatoms(mln, domains, [], self.argdoms): yield gndatom - - + + def _groundatoms(self, mln, domains, values, argdoms): # if there are no more parameters to ground, we're done # and we cann add the ground atom to the MRF if not argdoms: yield mln.logic.gnd_atom(self.name, values, mln) return - # create ground atoms for each way of grounding the first of the + # create ground atoms for each way of grounding the first of the # remaining variables whose domains are given in domNames dom = domains.get(argdoms[0]) if dom is None or len(dom) == 0: @@ -85,24 +85,24 @@ def _groundatoms(self, mln, domains, values, argdoms): for value in dom: for gndatom in self._groundatoms(mln, domains, values + [value], argdoms[1:]): yield gndatom - - + + def __eq__(self, other): return type(other) == type(self) and other.name == self.name and other.argdoms == self.argdoms - - + + def __ne__(self, other): return not self == other - - + + def __str__(self): return '%s(%s)' % (self.name, self.argstr()) - - + + def __repr__(self): return '' % str(self) - - + + def argstr(self): return ','.join(map(str, self.argdoms)) @@ -111,30 +111,30 @@ class FuzzyPredicate(Predicate): """ Represents a predicate whose atom can take fuzzy degrees of truth in [0,1]. """ - + def __init__(self, name, argdoms): Predicate.__init__(self, name, argdoms) - - + + def __repr__(self): return '' % str(self) - - + + def tovariable(self, mrf, gndatom): return FuzzyVariable(mrf, name=self.varname(gndatom), predicate=self) - + class FunctionalPredicate(Predicate): """ Represents a predicate declaration for a functional constraint. - + :param mutex: (int) the index of the mutex argument - - .. seealso:: :class:`mln.base.Predicate` - + + .. seealso:: :class:`mln.base.Predicate` + """ - - + + def __init__(self, name, argdoms, mutex): Predicate.__init__(self, name, argdoms) self.mutex = mutex @@ -143,43 +143,43 @@ def __init__(self, name, argdoms, mutex): def varname(self, gndatom): nonfuncargs = [p if i != self.mutex else '_' for i, p in enumerate(gndatom.args)] return '%s(%s)' % (gndatom.predname, ','.join(nonfuncargs)) - + def tovariable(self, mrf, name): return MutexVariable(mrf, name, self) - - + + def __eq__(self, other): return Predicate.__eq__(self, other) and self.mutex == other.mutex - - + + def __str__(self): return '%s(%s)' % (self.name, self.argstr()) - - + + def __repr__(self): return '' % str(self) - + def argstr(self): return ','.join([arg if i != self.mutex else '%s!' % arg for i, arg in enumerate(self.argdoms)]) - + class SoftFunctionalPredicate(FunctionalPredicate): """ Represents a predicate declaration for soft function constraint. """ - + def tovariable(self, mrf, name): return SoftMutexVariable(mrf, name, self) def __str__(self): return '%s(%s)' % (self.name, self.argstr()) - + def argstr(self): return ','.join([arg if i != self.mutex else '%s?' % arg for i, arg in enumerate(self.argdoms)]) - - + + def __repr__(self): return '' % str(self) diff --git a/python3/pracmln/mln/mrf.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/mrf.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..7a997fcc --- /dev/null +++ b/python3/pracmln/mln/mrf.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/mrf.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/mrf.pxd b/python3/pracmln/mln/mrf.pxd new file mode 100644 index 00000000..3029e39f --- /dev/null +++ b/python3/pracmln/mln/mrf.pxd @@ -0,0 +1,11 @@ +from base cimport MLN +from cpython cimport array + + +cdef class MRF: + cdef public MLN mln + cdef list _evidence#cdef array.array _evidence + cdef dict _variables + cdef dict _gndatoms + cdef dict _gndatoms_indices + cdef dict __dict__ diff --git a/python3/pracmln/mln/mrf.py b/python3/pracmln/mln/mrf.pyx similarity index 94% rename from python3/pracmln/mln/mrf.py rename to python3/pracmln/mln/mrf.pyx index c611445e..367bbbeb 100644 --- a/python3/pracmln/mln/mrf.py +++ b/python3/pracmln/mln/mrf.pyx @@ -40,13 +40,19 @@ from .util import fstr, logx, mergedom, CallByRef, Interval from ..logic import FirstOrderLogic from ..logic.common import Logic +from ..logic.common import GroundAtom as Super_GroundAtom from ..logic.fuzzy import FuzzyLogic +from util cimport CallByRef +from mrfvars cimport MRFVariable +#from cpython cimport array +#import array + logger = logs.getlogger(__name__) -class MRF(object): +cdef class MRF(object): ''' Represents a ground Markov random field. @@ -54,7 +60,7 @@ class MRF(object): :member _gndatoms_indices: dict mapping ground atom index to Logic.GroundAtom object :member _evidence: vector of evidence truth values of all ground atoms :member _variables: dict mapping variable names to their :class:`mln.mrfvars.MRFVariable` instance. - + :param mln: the MLN tied to this MRF. :param db: the database that the MRF shall be grounded with. ''' @@ -64,23 +70,23 @@ def __init__(self, mln, db): self.mln = mln.materialize(db) else: self.mln = mln - self._evidence = [] + self._evidence = []#array.array('d', [])#[] # self.evidenceBackup = {} self._variables = {} self._variables_by_idx = {} # gnd atom idx -> variable self._variables_by_gndatomidx = {} # gnd atom idx self._gndatoms = {} - self._gndatoms_by_idx = {} + self._gndatoms_by_idx = {} # get combined domain self.domains = mergedom(self.mln.domains, db.domains) -# self.softEvidence = list(mln.posteriorProbReqs) # constraints on posterior - # probabilities are nothing but +# self.softEvidence = list(mln.posteriorProbReqs) # constraints on posterior + # probabilities are nothing but # soft evidence and can be handled in exactly the same way # ground members self.formulas = list(self.mln.formulas) if isinstance(db, str): db = Database.load(self.mln, dbfile=db) - elif isinstance(db, Database): + elif isinstance(db, Database): pass elif db is None: db = Database(self.mln) @@ -98,11 +104,11 @@ def probreqs(self): @property def variables(self): return sorted(list(self._variables.values()), key=lambda v: v.idx) - + @property def gndatoms(self): return list(self._gndatoms.values()) - + @property def evidence(self): return self._evidence @@ -111,11 +117,11 @@ def evidence(self): def evidence(self, evidence): self._evidence = evidence self.consistent() - + @property def predicates(self): return self.mln.predicates - + @property def hardformulas(self): ''' @@ -197,7 +203,7 @@ def __getitem__(self, key): return self.evidence[self.gndatom(key).idx] def __setitem__(self, key, value): - self.set_evidence({key: value}, erase=False) + self.set_evidence({key: value}, erase=False) def prior(self, f, p): self._probreqs.append(FirstOrderLogic.PriorConstraint(formula=f, p=p)) @@ -208,7 +214,7 @@ def posterior(self, f, p): def set_evidence(self, atomvalues, erase=False, cw=False): ''' Sets the evidence of variables in this MRF. - + If erase is `True`, for every ground atom appearing in atomvalues, the truth values of all ground ground atom in the respective MRF variable are erased before the evidences are set. All other ground atoms stay untouched. @@ -242,12 +248,12 @@ def set_evidence(self, atomvalues, erase=False, cw=False): # unset all atoms in this variable for atom in var.gndatoms: self._evidence[atom.idx] = None - + for key, value in atomvalues.items(): gndatom = self.gndatom(key) var = self.variable(gndatom) # create a template with admissible truth values for all - # ground atoms in this variable + # ground atoms in this variable values = [-1] * len(var.gndatoms) if isinstance(var, FuzzyVariable): self._evidence[gndatom.idx] = value @@ -267,19 +273,19 @@ def set_evidence(self, atomvalues, erase=False, cw=False): elif curval is None and val is not None: self._evidence[atom.idx] = val if cw: self.apply_cw() - + def erase(self): ''' Erases all evidence in the MRF. ''' self._evidence = [None] * len(self.gndatoms) - + def apply_cw(self, *prednames): ''' Applies the closed world assumption to this MRF. - + Sets all evidences to 0 if they don't have truth value yet. - + :param prednames: a list of predicate names the cw assumption shall be applied to. If empty, it is applied to all predicates. ''' @@ -287,11 +293,11 @@ def apply_cw(self, *prednames): if prednames and self.gndatom(i).predname not in prednames: continue if v is None: self._evidence[i] = 0 - + def consistent(self, strict=False): ''' Performs a consistency check on this MRF wrt. to the variable value assignments. - + Raises an MRFValueException if the MRF is inconsistent. ''' for variable in self.variables: @@ -301,11 +307,11 @@ def gndatom(self, identifier, *args): ''' Returns the the ground atom instance that is associated with the given identifier, or adds a new ground atom. - + :param identifier: Either the string representation of the ground atom or its index (int) :returns: the :class:`logic.common.Logic.GroundAtom` instance or None, if the ground atom doesn't exist. - + :Example: >>> mrf = MRF(mln) >>> mrf.gndatom('foo', 'x', 'y') # add the ground atom 'foo(x,y)' @@ -327,7 +333,7 @@ def gndatom(self, identifier, *args): return atom elif type(identifier) is int: return self._gndatoms_by_idx.get(identifier) - elif isinstance(identifier, Logic.GroundAtom): + elif isinstance(identifier, Super_GroundAtom): return self._gndatoms.get(str(identifier)) # else: # return self.new_gndatom(identifier.predname, *identifier.args) @@ -339,24 +345,24 @@ def variable(self, identifier): ''' Returns the :class:`mln.mrfvars.MRFVariable` instance of the variable with the name or index `var`, or None, if no such variable exists. - + :param identifier: (string/int/:class:`logic.common.Logic.GroundAtom`) the name or index of the variable, - or the instance of a ground atom that is part of the desired variable. + or the instance of a ground atom that is part of the desired variable. ''' if type(identifier) is int: return self._variables_by_idx.get(identifier) - elif isinstance(identifier, Logic.GroundAtom): + elif isinstance(identifier, Super_GroundAtom): return self._variables_by_gndatomidx[identifier.idx] elif isinstance(identifier, str): return self._variables.get(identifier) - + def new_gndatom(self, predname, *args): ''' - Adds a ground atom to the set (actually it's a dict) of ground atoms. - + Adds a ground atom to the set (actually it's a dict) of ground atoms. + If the ground atom is already in the MRF it does nothing but returning the existing ground atom instance. Also updates/adds the variables of the MRF. - + :param predname: the predicate name of the ground atom :param *args: the list of predicate arguments `logic.common.Logic.GroundAtom` object ''' @@ -375,16 +381,16 @@ def new_gndatom(self, predname, *args): variable = self.variable(varname) if variable is None: variable = predicate.tovariable(self, varname) - self._variables[variable.name] = variable - self._variables_by_idx[variable.idx] = variable - variable.gndatoms.append(gndatom) + self._variables[variable.name] = variable # name declared public just because of this line? + self._variables_by_idx[variable.idx] = variable # idx declared public just because of this line? + variable.gndatoms.append(gndatom) # declared public just because of this one usage? self._variables_by_gndatomidx[gndatom.idx] = variable return gndatom - + def print_variables(self): for var in self.variables: print(str(var)) - + def print_world_atoms(self, world, stream=sys.stdout): ''' Prints the given world `world` as a readable string of the plain gnd atoms to the given stream. @@ -393,7 +399,7 @@ def print_world_atoms(self, world, stream=sys.stdout): v = world[gndatom.idx] vstr = '%.3f' % v if v is not None else '? ' stream.write('%s %s\n' % (vstr, str(gndatom))) - + def print_world_vars(self, world, stream=sys.stdout, tb=2): ''' Prints the given world `world` as a readable string of the MRF variables to the given stream. @@ -403,12 +409,12 @@ def print_world_vars(self, world, stream=sys.stdout, tb=2): stream.write(repr(var) + '\n') for i, v in enumerate(var.evidence_value(world)): vstr = '%.3f' % v if v is not None else '? ' - stream.write(' %s %s\n' % (vstr, var.gndatoms[i])) + stream.write(' %s %s\n' % (vstr, var.gndatoms[i])) def print_domains(self): out('=== MRF DOMAINS ==', tb=2) for dom, values in self.domains.items(): - print(dom, '=', ','.join(values)) + print(dom, '=', ','.join(values)) def evidence_dicts(self): ''' @@ -431,10 +437,10 @@ def evidence_dicti(self): def countworlds(self, withevidence=False): ''' Computes the number of possible worlds this MRF can take. - + :param withevidence: (bool) if True, takes into account the evidence which is currently set in the MRF. if False, computes the total number of possible worlds. - + .. note:: this method does not enumerate the possible worlds. ''' worlds = 1 @@ -442,37 +448,47 @@ def countworlds(self, withevidence=False): for var in self.variables: worlds *= var.valuecount(ev) return worlds - + def iterworlds(self): ''' Iterates over the possible worlds of this MRF taking into account the evidence vector of truth values. - + :returns: a generator of (idx, possible world) tuples. ''' for res in self._iterworlds([v for v in self.variables if v.valuecount(self.evidence) > 1], list(self.evidence), CallByRef(0), self.evidence_dicti()): yield res - def _iterworlds(self, variables, world, worldidx, evidence): + def _iterworlds(self, list variables, list world, CallByRef worldidx, dict evidence): + #print('\n\nself is {} of type {}'.format(self, type(self))) + #print('variables is {} of type {}'.format(variables, type(variables))) + #for vari in variables: + # print('\tvari is {} of type {}'.format(vari, type(vari))) + #print('world is {} of type {}'.format(world, type(world))) + #print('worldidx is {} of type {}'.format(worldidx, type(worldidx))) + #print('worldidx.value of type {}'.format(type(worldidx.value))) + #print('evidence is {} of type {}'.format(evidence, type(evidence))) if not variables: yield worldidx.value, world worldidx.value += 1 return - variable = variables[0] + cdef MRFVariable variable = variables[0] + cdef list world_ + cdef tuple value if isinstance(variable, FuzzyVariable): world_ = list(world) value = variable.evidence_value(evidence) for res in self._iterworlds(variables[1:], variable.setval(value, world_), worldidx, evidence): - yield res + yield res else: for _, value in variable.itervalues(evidence): world_ = list(world) for res in self._iterworlds(variables[1:], variable.setval(value, world_), worldidx, evidence): - yield res + yield res def worlds(self): ''' Iterates over all possible worlds (taking evidence into account). - + :returns: a generator of possible worlds. ''' for _, world in self.iterworlds(): @@ -481,7 +497,7 @@ def worlds(self): def iterallworlds(self): ''' Iterates over all possible worlds (without) taking evidence into account). - + :returns: a generator of possible worlds. ''' world = [None] * len(self.evidence) @@ -491,9 +507,9 @@ def iterallworlds(self): def itergroundings(self, simplify=False, grounding_factory='DefaultGroundingFactory'): ''' Iterates over all groundings of all formulas of this MRF. - + :param simplify: if True, the ground formulas are simplified wrt to the evidence in the MRF. - :param grounding_factory: the grounding factory to be used. + :param grounding_factory: the grounding factory to be used. :returns: a generator yielding ground formulas ''' grounder = eval('%s(self, simplify=simplify)' % grounding_factory) @@ -510,7 +526,7 @@ def print_evidence_vars(self, stream=sys.stdout): ''' Prints the evidence truth values of the variables of this MRF to the given `stream`. ''' - self.print_world_vars(self.evidence, stream, tb=3) + self.print_world_vars(self.evidence, stream, tb=3) def getTruthDegreeGivenSoftEvidence(self, gf, world): cnf = gf.cnf() @@ -541,12 +557,12 @@ def print_gndatoms(self, stream=sys.stdout): for ga in sorted(l): stream.write(str(ga) + '\n') - def apply_prob_constraints(self, constraints, method=InferenceMethods.EnumerationAsk, - thr=1.0e-3, steps=20, fittingMCSATSteps=5000, - fittingParams=None, given=None, queries=None, + def apply_prob_constraints(self, constraints, method=InferenceMethods.EnumerationAsk, + thr=1.0e-3, steps=20, fittingMCSATSteps=5000, + fittingParams=None, given=None, queries=None, maxThreshold=None, greedy=False, probabilityFittingResultFileName=None, **args): ''' - Applies the given probability constraints (if any), dynamically + Applies the given probability constraints (if any), dynamically modifying weights of the underlying MLN by applying iterative proportional fitting :param constraints: list of constraints diff --git a/python3/pracmln/mln/mrfvars.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/mrfvars.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..5abd5681 --- /dev/null +++ b/python3/pracmln/mln/mrfvars.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/mrfvars.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/mrfvars.pxd b/python3/pracmln/mln/mrfvars.pxd new file mode 100644 index 00000000..8d6efd24 --- /dev/null +++ b/python3/pracmln/mln/mrfvars.pxd @@ -0,0 +1,9 @@ +from mrf cimport MRF +from mlnpreds cimport Predicate + +cdef class MRFVariable: + cdef MRF mrf + cdef public list gndatoms + cdef public int idx + cdef public str name + cdef public Predicate predicate # baffling multidb bpll learning error if not public... diff --git a/python3/pracmln/mln/mrfvars.py b/python3/pracmln/mln/mrfvars.pyx similarity index 95% rename from python3/pracmln/mln/mrfvars.py rename to python3/pracmln/mln/mrfvars.pyx index 70e37277..d52d1936 100644 --- a/python3/pracmln/mln/mrfvars.py +++ b/python3/pracmln/mln/mrfvars.pyx @@ -1,7 +1,7 @@ -# +# # # (C) 2011-2014 by Daniel Nyga (nyga@cs.uni-bremen.de) -# +# # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including @@ -25,20 +25,20 @@ from .errors import MRFValueException from .util import Interval -class MRFVariable(object): +cdef class MRFVariable(): """ Represents a (mutually exclusive) block of ground atoms. - + This is the base class for different types of variables an MRF may consist of, e.g. mutually exclusive ground atoms. The purpose - of these variables is to provide some convenience methods for + of these variables is to provide some convenience methods for easy iteration over their values ("possible worlds") and to ease introduction of new types of variables in an MRF. - + The values of a variable should have a fixed order, so every value must have a fixed index. """ - + def __init__(self, mrf, name, predicate, *gndatoms): """ :param mrf: the instance of the MRF that this variable is added to @@ -51,80 +51,86 @@ def __init__(self, mrf, name, predicate, *gndatoms): self.idx = len(mrf.variables) self.name = name self.predicate = predicate - - + #print('gndatoms is {} of type {}'.format(self.gndatoms, type(self.gndatoms))) + #for gi in self.gndatoms: + # print('\tgi is {} of type {}'.format(gi, type(gi))) + #print('idx is {} of type {}'.format(self.idx, type(self.idx))) + #print('name is {} of type {}'.format(self.name, type(self.name))) + #print('predicate is {} of type {}'.format(self.predicate, type(self.predicate))) + + def atomvalues(self, value): """ Returns a generator of (atom, value) pairs for the given variable value - + :param value: a tuple of truth values """ for atom, val in zip(self.gndatoms, value): yield atom, val - - + + def iteratoms(self): """ Yields all ground atoms in this variable, sorted by atom index ascending """ for atom in sorted(self.gndatoms, key=lambda a: a.idx): yield atom - - + + def strval(self, value): """ Returns a readable string representation for the value tuple given by `value`. """ return '<%s>' % ', '.join(['%s' % str(a_v[0]) if a_v[1] == 1 else ('!%s' % str(a_v[0]) if a_v[1] == 0 else '?%s?' % str(a_v[0])) for a_v in zip(self.gndatoms, value)]) - - + + def valuecount(self, evidence=None): """ Returns the number of values this variable can take. """ raise Exception('%s does not implement valuecount()' % self.__class__.__name__) - - + + def _itervalues(self, evidence=None): """ Generates all values of this variable as tuples of truth values. - + :param evidence: an optional dictionary mapping ground atoms to truth values. - + .. seealso:: values are given in the same format as in :method:`MRFVariable.itervalues()` """ raise Exception('%s does not implement _itervalues()' % self.__class__.__name__) - - + + def valueidx(self, value): """ Computes the index of the given value. - + .. seealso:: values are given in the same format as in :method:`MRFVariable.itervalues()` """ raise Exception('%s does not implement valueidx()' % self.__class__.__name__) - - + + def evidence_value_index(self, evidence=None): """ Returns the index of this atomic block value for the possible world given in `evidence`. - + .. seealso:: `MRFVariable.evidence_value()` """ value = self.evidence_value(evidence) if any([v is None for v in value]): return None return self.valueidx(tuple(value)) - - + + def evidence_value(self, evidence=None): """ Returns the value of this variable as a tuple of truth values in the possible world given by `evidence`. - + Exp: (0, 1, 0) for a mutex variable containing 3 gnd atoms - - :param evidence: the truth values wrt. the ground atom indices. Can be a + + :param evidence: the truth values wrt. the ground atom indices. Can be a complete assignment of truth values (i.e. a list) or a dict mapping ground atom indices to their truth values. If evidence is `None`, the evidence vector of the MRF is taken. @@ -138,78 +144,78 @@ def evidence_value(self, evidence=None): # if not all(map(lambda v: v is not None, value)) and not all(map(lambda v: v is None, value)): # raise Exception('Inconsistent truth assignment in evidence') return tuple(value) - - + + def value2dict(self, value): """ - Takes a tuple of truth values and transforms it into a dict + Takes a tuple of truth values and transforms it into a dict mapping the respective ground atom indices to their truth values. - + :param value: the value tuple to be converted. """ evidence = {} for atom, val in zip(self.gndatoms, value): evidence[atom.idx] = val return evidence - - + + def setval(self, value, world): """ Sets the value of this variable in the world `world` to the given value. - + :param value: tuple representing the value of the variable. :param world: vector representing the world to be modified: - :returns: the modified world. + :returns: the modified world. """ for i, v in self.value2dict(value).items(): world[i] = v return world - - + + def itervalues(self, evidence=None): """ Iterates over (idx, value) pairs for this variable. - - Values are given as tuples of truth values of the respective ground atoms. - For a binary variable (a 'normal' ground atom), for example, the two values - are represented by (0,) and (1,). If `evidence is` given, only values + + Values are given as tuples of truth values of the respective ground atoms. + For a binary variable (a 'normal' ground atom), for example, the two values + are represented by (0,) and (1,). If `evidence is` given, only values matching the evidence values are generated. - + :param evidence: an optional dictionary mapping ground atom indices to truth values. - + .. warning:: ground atom indices are with respect to the mrf instance, not to the index of the gnd atom in the variable - + .. warning:: The values are not necessarily orderd with respect to their actual index obtained by `MRFVariable.valueidx()`. - + """ if type(evidence) is list: evidence = dict([(i, v) for i, v in enumerate(evidence)]) for tup in self._itervalues(evidence): yield self.valueidx(tup), tup - - + + def values(self, evidence=None): """ Returns a generator of possible values of this variable under consideration of the evidence given, if any. - + Same as ``itervalues()`` but without value indices. """ for _, val in self.itervalues(evidence): yield val - - + + def iterworlds(self, evidence=None): """ Iterates over possible worlds of evidence which can be generated with this variable. - + This does not have side effects on the `evidence`. If no `evidence` is specified, the evidence vector of the MRF is taken. - + :param evidence: a possible world of truth values of all ground atoms in the MRF. - :returns: + :returns: """ if type(evidence) is not dict: raise Exception('evidence must be of type dict, is %s' % type(evidence)) @@ -220,12 +226,12 @@ def iterworlds(self, evidence=None): value = self.value2dict(val) world.update(value) yield i, world - - + + def consistent(self, world, strict=False): """ Checks for this variable if its assignment in the assignment `evidence` is consistent. - + :param evidence: the assignment to be checked. :param strict: if True, no unknown assignments are allowed, i.e. there must not be any ground atoms in the variable that do not have a truth value assigned. @@ -240,25 +246,25 @@ def consistent(self, world, strict=False): if not (total == 1 if strict else total in Interval('[0,1]')): raise MRFValueException('Invalid value of variable %s: %s' % (repr(self), evstr)) return True - - + + def __str__(self): return self.name - + def __repr__(self): return '<%s "%s": [%s]>' % (self.__class__.__name__, self.name, ','.join(map(str, self.gndatoms))) - + def __contains__(self, element): return element in self.gndatoms - + class FuzzyVariable(MRFVariable): """ Represents a fuzzy ground atom that can take values of truth in [0,1]. - + It does not support iteration over values or value indexing. """ - + def consistent(self, world, strict=False): value = self.evidence_value(world)[0] if value is not None: @@ -268,8 +274,8 @@ def consistent(self, world, strict=False): else: if strict: raise MRFValueException('Invalid value of variable %s: %s' % (repr(self), value)) else: return True - - + + def valuecount(self, evidence=None): if evidence is None or evidence[self.gndatoms[0].idx] is None: raise MRFValueException('Cannot count number of values of an unassigned FuzzyVariable: %s' % str(self)) @@ -282,14 +288,14 @@ def itervalues(self, evidence=None): raise MRFValueException('Cannot iterate over values of fuzzy variables: %s' % str(self)) else: yield None, (evidence[self.gndatoms[0].idx],) - + class BinaryVariable(MRFVariable): """ Represents a binary ("normal") ground atom with the two truth values 1 (true) and 0 (false). The first value is always the false one. """ - + def valuecount(self, evidence=None): if evidence is None: @@ -315,27 +321,27 @@ def valueidx(self, value): elif value == (1,): return 1 else: raise MRFValueException('Invalid world value for binary variable %s: %s' % (str(self), str(value))) - + def consistent(self, world, strict=False): val = world[self.gndatoms[0].idx] if strict and val is None: raise MRFValueException('Invalid value of variable %s: %s' % (repr(self), val)) - + class MutexVariable(MRFVariable): """ Represents a mutually exclusive block of ground atoms, i.e. a block in which exactly one ground atom must be true. """ - + def valuecount(self, evidence=None): if evidence is None: return len(self.gndatoms) else: return len(list(self.itervalues(evidence))) - - + + def _itervalues(self, evidence=None): if evidence is None: evidence = {} @@ -343,7 +349,7 @@ def _itervalues(self, evidence=None): valpattern = [] for mutexatom in atomindices: valpattern.append(evidence.get(mutexatom, None)) - # at this point, we have generated a value pattern with all values + # at this point, we have generated a value pattern with all values # that are fixed by the evidence argument and None for all others trues = sum([x for x in valpattern if x == 1]) if trues > 1: # sanity check @@ -359,21 +365,21 @@ def _itervalues(self, evidence=None): values = [0] * len(valpattern) values[i] = 1 yield tuple(values) - - + + def valueidx(self, value): if sum(value) != 1: raise MRFValueException('Invalid world value for mutex variable %s: %s' % (str(self), str(value))) else: return value.index(1) - + class SoftMutexVariable(MRFVariable): """ Represents a soft mutex block of ground atoms, i.e. a mutex block in which maximally one ground atom may be true. """ - + def valuecount(self, evidence=None): if evidence is None: return len(self.gndatoms) + 1 @@ -388,7 +394,7 @@ def _itervalues(self, evidence=None): valpattern = [] for mutexatom in atomindices: valpattern.append(evidence.get(mutexatom, None)) - # at this point, we have generated a value pattern with all values + # at this point, we have generated a value pattern with all values # that are fixed by the evidence argument and None for all others trues = sum([x for x in valpattern if x == 1]) if trues > 1: # sanity check @@ -403,8 +409,8 @@ def _itervalues(self, evidence=None): values[i] = 1 yield tuple(values) yield tuple([0] * len(atomindices)) - - + + def valueidx(self, value): if sum(value) > 1: raise Exception('Invalid world value for soft mutex block %s: %s' % (str(self), str(value))) diff --git a/python3/pracmln/mln/setup.py b/python3/pracmln/mln/setup.py new file mode 100644 index 00000000..da01a8d0 --- /dev/null +++ b/python3/pracmln/mln/setup.py @@ -0,0 +1,7 @@ +from distutils.core import setup +#from distutils.extension import Extension +from Cython.Build import cythonize + +setup( + ext_modules=cythonize("*.pyx")#, compiler_directives={'profile': True}) +) diff --git a/python3/pracmln/mln/util.cpython-35m-x86_64-linux-gnu.so b/python3/pracmln/mln/util.cpython-35m-x86_64-linux-gnu.so new file mode 120000 index 00000000..a3d3ace7 --- /dev/null +++ b/python3/pracmln/mln/util.cpython-35m-x86_64-linux-gnu.so @@ -0,0 +1 @@ +pracmln/mln/util.cpython-35m-x86_64-linux-gnu.so \ No newline at end of file diff --git a/python3/pracmln/mln/util.pxd b/python3/pracmln/mln/util.pxd new file mode 100644 index 00000000..2af88e14 --- /dev/null +++ b/python3/pracmln/mln/util.pxd @@ -0,0 +1,2 @@ +cdef class CallByRef: + cdef int value diff --git a/python3/pracmln/mln/util.py b/python3/pracmln/mln/util.pyx similarity index 95% rename from python3/pracmln/mln/util.py rename to python3/pracmln/mln/util.pyx index a5dde217..367e008e 100644 --- a/python3/pracmln/mln/util.py +++ b/python3/pracmln/mln/util.pyx @@ -67,8 +67,8 @@ def crash(*args, **kwargs): def flip(value): ''' Flips the given binary value to its complement. - - Works with ints and booleans. + + Works with ints and booleans. ''' if type(value) is bool: return True if value is False else False @@ -87,11 +87,11 @@ def batches(i, size): batch = [] for e in i: batch.append(e) - if len(batch) == size: + if len(batch) == size: yield batch batch = [] if batch: yield batch - + def rndbatches(i, size): i = list(i) @@ -120,7 +120,7 @@ def replacer(match): def parse_queries(mln, query_str): ''' Parses a list of comma-separated query strings. - + Admissible queries are all kinds of formulas or just predicate names. Returns a list of the queries. ''' @@ -139,7 +139,7 @@ def parse_queries(mln, query_str): prednames = [lit.predname for lit in literals] query_preds.update(prednames) except: - # not a formula, must be a pure predicate name + # not a formula, must be a pure predicate name query_preds.add(s) queries.append(q) q = '' @@ -156,8 +156,8 @@ def predicate_declaration_string(predName, domains, blocks): def getPredicateList(filename): - ''' - Gets the set of predicate names from an MLN file + ''' + Gets the set of predicate names from an MLN file ''' content = open(filename, "r").read() + "\n" content = stripComments(content) @@ -175,21 +175,21 @@ def avg(*a): return sum(map(float, a)) / len(a) -class CallByRef(object): +cdef class CallByRef(object): ''' Convenience class for treating any kind of variable as an object that can be manipulated in-place by a call-by-reference, in particular for primitive data types such as numbers. ''' - - def __init__(self, value): + + def __init__(self, int value): self.value = value - + INC = 1 EXC = 2 class Interval: - + def __init__(self, interval): tokens = re.findall(r'(\(|\[|\])([-+]?\d*\.\d+|\d+),([-+]?\d*\.\d+|\d+)(\)|\]|\[)', interval.strip())[0] if tokens[0] in ('(', ']'): @@ -198,26 +198,26 @@ def __init__(self, interval): self.left = INC else: raise Exception('Illegal interval: {}'.format(interval)) - if tokens[3] in (')', '['): + if tokens[3] in (')', '['): self.right = EXC elif tokens[3] == ']': self.right = INC else: raise Exception('Illegal interval: {}'.format(interval)) - self.start = float(tokens[1]) + self.start = float(tokens[1]) self.end = float(tokens[2]) - + def __contains__(self, x): return (self.start <= x if self.left == INC else self.start < x) and (self.end >= x if self.right == INC else self.end > x) - - + + def elapsedtime(start, end=None): ''' Compute the elapsed time of the interval `start` to `end`. - - Returns a pair (t,s) where t is the time in seconds elapsed thus + + Returns a pair (t,s) where t is the time in seconds elapsed thus far (since construction) and s is a readable string representation thereof. - + :param start: the starting point of the time interval. :param end: the end point of the time interval. If `None`, the current time is taken. ''' @@ -226,8 +226,8 @@ def elapsedtime(start, end=None): else: elapsed = time.time() - start return elapsed_time_str(elapsed) - - + + def elapsed_time_str(elapsed): hours = int(elapsed / 3600) elapsed -= hours * 3600 @@ -248,7 +248,7 @@ def balancedParentheses(s): return False cnt -= 1 return cnt == 0 - + def fstr(f): s = str(f) while s[0] == '(' and s[ -1] == ')': @@ -276,7 +276,7 @@ def tty(stream): return isatty and isatty() BOLD = (None, None, True) - + def headline(s): line = ''.ljust(len(s), '=') return '{}\n{}\n{}'.format(colorize(line, BOLD, True), colorize(s, BOLD, True), colorize(line, BOLD, True)) @@ -291,7 +291,7 @@ def gradGaussianZeroMean(x, sigma): def mergedom(*domains): - ''' + ''' Returning a new domains dictionary that contains the elements of all the given domains ''' fullDomain = {} @@ -332,31 +332,31 @@ def colorize(message, format, color=False): class StopWatchTag: - + def __init__(self, label, starttime, stoptime=None): self.label = label self.starttime = starttime self.stoptime = stoptime - + @property def elapsedtime(self): - return ifnone(self.stoptime, time.time()) - self.starttime - + return ifnone(self.stoptime, time.time()) - self.starttime + @property def finished(self): return self.stoptime is not None - + class StopWatch(object): ''' Simple tagging of time spans. ''' - - + + def __init__(self): self.tags = {} - - + + def tag(self, label, verbose=True): if verbose: print('{}...'.format(label)) @@ -367,8 +367,8 @@ def tag(self, label, verbose=True): else: tag.starttime = now self.tags[label] = tag - - + + def finish(self, label=None): now = time.time() if label is None: @@ -380,15 +380,15 @@ def finish(self, label=None): raise Exception('Unknown tag: {}'.format(label)) tag.stoptime = now - + def __getitem__(self, key): return self.tags.get(key) - + def reset(self): self.tags = {} - + def printSteps(self): for tag in sorted(list(self.tags.values()), key=lambda ta: ta.starttime): if tag.finished: @@ -409,7 +409,7 @@ def _combinations(domains, comb): for v in domains[0]: for ret in _combinations(domains[1:], comb + [v]): yield ret - + def deprecated(func): ''' This is a decorator which can be used to mark functions @@ -423,14 +423,14 @@ def newFunc(*args, **kwargs): newFunc.__doc__ = func.__doc__ newFunc.__dict__.update(func.__dict__) return newFunc - + def unifyDicts(d1, d2): ''' Adds all key-value pairs from d2 to d1. ''' for key in d2: d1[key] = d2[key] - + def dict_union(d1, d2): ''' Returns a new dict containing all items from d1 and d2. Entries in d1 are @@ -452,13 +452,13 @@ def dict_subset(subset, superset): class edict(dict): - + def __add__(self, d): return dict_union(self, d) - + def __radd__(self, d): return self + d - + def __sub__(self, d): if type(d) in (dict, defaultdict): ret = dict(self) @@ -468,13 +468,13 @@ def __sub__(self, d): ret = dict(self) del ret[d] return ret - - + + class eset(set): - + def __add__(self, s): return set(self).union(s) - + def item(s): ''' @@ -490,42 +490,42 @@ class temporary_evidence: Context guard class for enabling convenient handling of temporary evidence in MRFs using the python `with` statement. This guarantees that the evidence is set back to the original whatever happens in the `with` block. - + :Example: - + >> with temporary_evidence(mrf, [0, 0, 0, 1, 0, None, None]) as mrf_: ''' - - + + def __init__(self, mrf, evidence=None): self.mrf = mrf self.evidence_backup = list(mrf.evidence) if evidence is not None: - self.mrf.evidence = evidence - + self.mrf.evidence = evidence + def __enter__(self): return self.mrf - + def __exit__(self, exception_type, exception_value, tb): if exception_type is not None: traceback.print_exc() raise exception_type(exception_value) self.mrf.evidence = self.evidence_backup return True - - - - + + + + if __name__ == '__main__': - + l = [1,2,3] upto = 2 out(ifnone(upto, len(l))) out(l[:ifnone(upto, len(l))]) out(cumsum(l,1)) - + # d = edict({1:2,2:3,'hi':'world'}) # print d # print d + {'bla': 'blub'} @@ -533,4 +533,4 @@ def __exit__(self, exception_type, exception_value, tb): # print d - 1 # print d - {'hi': 'bla'} # print d -# +#