From 797706ab9f44102b57ddd411e1151d0a196e8e92 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Tue, 22 May 2018 01:56:38 +0530 Subject: [PATCH 01/39] python3 exact.py -> exact.pyx --- python3/pracmln/mln/inference/.gitignore | 7 +- python3/pracmln/mln/inference/cy_exact.pyx | 176 ------------------ .../mln/inference/{exact.py => exact.pyx} | 37 ++-- 3 files changed, 27 insertions(+), 193 deletions(-) delete mode 100644 python3/pracmln/mln/inference/cy_exact.pyx rename python3/pracmln/mln/inference/{exact.py => exact.pyx} (87%) diff --git a/python3/pracmln/mln/inference/.gitignore b/python3/pracmln/mln/inference/.gitignore index 24fae75e..e22a709f 100644 --- a/python3/pracmln/mln/inference/.gitignore +++ b/python3/pracmln/mln/inference/.gitignore @@ -1 +1,6 @@ -*.pyo \ No newline at end of file +*.pyo +*.c +*.html +setup.py +build/* +pracmln/* diff --git a/python3/pracmln/mln/inference/cy_exact.pyx b/python3/pracmln/mln/inference/cy_exact.pyx deleted file mode 100644 index 36b07de3..00000000 --- a/python3/pracmln/mln/inference/cy_exact.pyx +++ /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 - - -cdef eval_queries(float* 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/exact.py b/python3/pracmln/mln/inference/exact.pyx similarity index 87% rename from python3/pracmln/mln/inference/exact.py rename to python3/pracmln/mln/inference/exact.pyx index a660551c..37d425d3 100644 --- a/python3/pracmln/mln/inference/exact.py +++ b/python3/pracmln/mln/inference/exact.pyx @@ -36,7 +36,7 @@ from ...logic.fol import FirstOrderLogic from ...logic.common import Logic from numpy.ma.core import exp - +from numpy import zeros logger = logs.getlogger(__name__) @@ -46,12 +46,12 @@ def eval_queries(world): - """ + ''' Evaluates the queries given a possible world. - """ - numerators = [0] * len(global_enumAsk.queries) + ''' + numerators = zeros(len(global_enumAsk.queries)) #numerators = [0] * len(global_enumAsk.queries) denominator = 0 - expsum = 0 + cdef double expsum = 0 for gf in global_enumAsk.grounder.itergroundings(): if global_enumAsk.soft_evidence_formula(gf): expsum += gf.noisyor(world) * gf.weight @@ -67,6 +67,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 +116,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))#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 # redundant variable ? self._watch.tag('enumerating worlds', verbose=self.verbose) global global_enumAsk global_enumAsk = self @@ -132,12 +133,14 @@ def _run(self): 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 - for i, v in enumerate(num): - numerators[i] += v - if self.verbose: bar.inc() + 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() except Exception as e: logger.error('Error in child process. Terminating pool...') pool.close() @@ -150,8 +153,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 +167,8 @@ def _run(self): raise SatisfiabilityException( 'MLN is unsatisfiable. All probability masses returned 0.') # normalize answers - dist = [float(x) / denominator for x in numerators] + dist = numerators / denominator + #dist = [float(x) / denominator for x in numerators] result = {} for q, p in zip(self.queries, dist): result[str(q)] = p From efef8eda57360445d782d3a6a618b3ec14476c51 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Tue, 22 May 2018 02:20:53 +0530 Subject: [PATCH 02/39] python2 exact.py -> exact.pyx --- python2/pracmln/mln/inference/.gitignore | 7 +++- .../mln/inference/{exact.py => exact.pyx} | 32 +++++++++++-------- 2 files changed, 25 insertions(+), 14 deletions(-) rename python2/pracmln/mln/inference/{exact.py => exact.pyx} (90%) 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 From bff2eac08175b600d033f091283083a4ce98c3ff Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sat, 2 Jun 2018 03:11:05 +0530 Subject: [PATCH 03/39] setup dev environment --- python3/pracmln/__init__.py | 4 +++- python3/pracmln/_version | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) create mode 120000 python3/pracmln/_version diff --git a/python3/pracmln/__init__.py b/python3/pracmln/__init__.py index 508f8057..29c5b06d 100644 --- a/python3/pracmln/__init__.py +++ b/python3/pracmln/__init__.py @@ -29,4 +29,6 @@ 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 + +print("cython-gsoc import successful") diff --git a/python3/pracmln/_version b/python3/pracmln/_version new file mode 120000 index 00000000..b14486bb --- /dev/null +++ b/python3/pracmln/_version @@ -0,0 +1 @@ +/home/kaivalya/Documents/WORK/GSoC/kPracMLN/_version \ No newline at end of file From 78014a5532fdadaf8a0900d18bde0c115e0c96f4 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sat, 2 Jun 2018 04:45:11 +0530 Subject: [PATCH 04/39] remove print on import --- python3/pracmln/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/python3/pracmln/__init__.py b/python3/pracmln/__init__.py index 29c5b06d..f1a7fc56 100644 --- a/python3/pracmln/__init__.py +++ b/python3/pracmln/__init__.py @@ -30,5 +30,3 @@ from .mlnlearn import EVIDENCE_PREDS from .utils.project import mlnpath from .utils.project import PRACMLNConfig - -print("cython-gsoc import successful") From c34e02e1e6b47fbb04e9cb1ef4a62703d8c1d380 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Tue, 5 Jun 2018 04:46:10 +0530 Subject: [PATCH 05/39] use numpy zeros in mcmc --- python3/pracmln/mln/inference/mcmc.pyx | 170 +++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 python3/pracmln/mln/inference/mcmc.pyx diff --git a/python3/pracmln/mln/inference/mcmc.pyx b/python3/pracmln/mln/inference/mcmc.pyx new file mode 100644 index 00000000..d02b973b --- /dev/null +++ b/python3/pracmln/mln/inference/mcmc.pyx @@ -0,0 +1,170 @@ +# -*- 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. +import random + + +from dnutils import logs + +from .infer import Inference +from ..util import fstr +from ..constants import ALL +from numpy import zeros + + +logger = logs.getlogger(__name__) + + +class MCMCInference(Inference): + """ + Abstract super class for Markov chain Monte Carlo-based inference. + """ + + def __init__(self, mrf, queries=ALL, **params): + Inference.__init__(self, mrf, queries, **params) + + + def random_world(self, evidence=None): + """ + Get a random possible world, taking the evidence into account. + """ + if evidence is None: + world = list(self.mrf.evidence) + else: + world = list(evidence) + for var in self.mrf.variables: + evdict = var.value2dict(var.evidence_value(world)) + valuecount = var.valuecount(evdict) + if valuecount > 1: + # get a random value of the variable + validx = random.randint(0, valuecount - 1) + value = [v for _, v in var.itervalues(evdict)][validx] + var.setval(value, world) + return world + + + class Chain: + """ + Represents the state of a Markov Chain. + """ + + + def __init__(self, infer, queries): + self.queries = queries + self.soft_evidence = None + self.steps = 0 + self.truths = zeros(len(self.queries)) + self.converged = False + self.lastresult = 10 + self.infer = infer + # copy the current evidence as this chain's state + # initialize remaining variables randomly (but consistently with the evidence) + self.state = infer.random_world() + + + def update(self, state): + self.steps += 1 + self.state = state + # keep track of counts for queries + for i, q in enumerate(self.queries): + self.truths[i] += q(self.state) + # check if converged !!! TODO check for all queries + if self.steps % 50 == 0: + result = self.results()[0] + diff = abs(result - self.lastresult) + if diff < 0.001: + self.converged = True + self.lastresult = result + # keep track of counts for soft evidence + if self.soft_evidence is not None: + for se in self.soft_evidence: + self.softev_counts[se["expr"]] += se["formula"](self.state) + + + def set_soft_evidence(self, soft_evidence): + self.soft_evidence = soft_evidence + self.softev_counts = {} + for se in soft_evidence: + if 'formula' not in se: + formula = self.infer.mrf.mln.logic.parse_formula(se['expr']) + se['formula'] = formula.ground(self.infer.mrf, {}) + se['expr'] = fstr(se['formula']) + self.softev_counts[se["expr"]] = se["formula"](self.state) + + + def soft_evidence_frequency(self, formula): + 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 + + + class ChainGroup: + + def __init__(self, infer): + self.chains = [] + self.infer = infer + + + def chain(self, chain): + self.chains.append(chain) + + + def results(self): + chains = float(len(self.chains)) + queries = self.chains[0].queries + # compute average + results = [0.0] * 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))] + for chain in self.chains: + cr = chain.results() + for i in range(len(self.chains[0].queries)): + var[i] += (cr[i] - results[i]) ** 2 / chains + return dict([(str(q), p) for q, p in zip(queries, results)]), var + + + def avgtruth(self, formula): + """ returns the fraction of chains in which the given formula is currently true """ + t = 0.0 + for c in self.chains: + t += formula(c.state) + return t / len(self.chains) + + +# def write(self, short=False): +# if len(self.chains) > 1: +# for i in range(len(self.infer.queries)): +# self.infer.additionalQueryInfo[i] = "[%d x %d steps, sd=%.3f]" % (len(self.chains), self.chains[0].steps, sqrt(self.var[i])) +# self.inferObject._writeResults(sys.stdout, self.results, shortOutput) From 590b3637b56a47d82c093d281b4768b4cdb7ec56 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Tue, 5 Jun 2018 04:46:55 +0530 Subject: [PATCH 06/39] delete mcmc.py --- python3/pracmln/mln/inference/mcmc.py | 169 -------------------------- 1 file changed, 169 deletions(-) delete mode 100644 python3/pracmln/mln/inference/mcmc.py diff --git a/python3/pracmln/mln/inference/mcmc.py b/python3/pracmln/mln/inference/mcmc.py deleted file mode 100644 index 078606fc..00000000 --- a/python3/pracmln/mln/inference/mcmc.py +++ /dev/null @@ -1,169 +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. -import random - - -from dnutils import logs - -from .infer import Inference -from ..util import fstr -from ..constants import ALL - - -logger = logs.getlogger(__name__) - - -class MCMCInference(Inference): - """ - Abstract super class for Markov chain Monte Carlo-based inference. - """ - - def __init__(self, mrf, queries=ALL, **params): - Inference.__init__(self, mrf, queries, **params) - - - def random_world(self, evidence=None): - """ - Get a random possible world, taking the evidence into account. - """ - if evidence is None: - world = list(self.mrf.evidence) - else: - world = list(evidence) - for var in self.mrf.variables: - evdict = var.value2dict(var.evidence_value(world)) - valuecount = var.valuecount(evdict) - if valuecount > 1: - # get a random value of the variable - validx = random.randint(0, valuecount - 1) - value = [v for _, v in var.itervalues(evdict)][validx] - var.setval(value, world) - return world - - - class Chain: - """ - Represents the state of a Markov Chain. - """ - - - def __init__(self, infer, queries): - self.queries = queries - self.soft_evidence = None - self.steps = 0 - self.truths = [0] * len(self.queries) - self.converged = False - self.lastresult = 10 - self.infer = infer - # copy the current evidence as this chain's state - # initialize remaining variables randomly (but consistently with the evidence) - self.state = infer.random_world() - - - def update(self, state): - self.steps += 1 - self.state = state - # keep track of counts for queries - for i, q in enumerate(self.queries): - self.truths[i] += q(self.state) - # check if converged !!! TODO check for all queries - if self.steps % 50 == 0: - result = self.results()[0] - diff = abs(result - self.lastresult) - if diff < 0.001: - self.converged = True - self.lastresult = result - # keep track of counts for soft evidence - if self.soft_evidence is not None: - for se in self.soft_evidence: - self.softev_counts[se["expr"]] += se["formula"](self.state) - - - def set_soft_evidence(self, soft_evidence): - self.soft_evidence = soft_evidence - self.softev_counts = {} - for se in soft_evidence: - if 'formula' not in se: - formula = self.infer.mrf.mln.logic.parse_formula(se['expr']) - se['formula'] = formula.ground(self.infer.mrf, {}) - se['expr'] = fstr(se['formula']) - self.softev_counts[se["expr"]] = se["formula"](self.state) - - - def soft_evidence_frequency(self, formula): - 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 - - - class ChainGroup: - - def __init__(self, infer): - self.chains = [] - self.infer = infer - - - def chain(self, chain): - self.chains.append(chain) - - - def results(self): - chains = float(len(self.chains)) - queries = self.chains[0].queries - # compute average - results = [0.0] * 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))] - for chain in self.chains: - cr = chain.results() - for i in range(len(self.chains[0].queries)): - var[i] += (cr[i] - results[i]) ** 2 / chains - return dict([(str(q), p) for q, p in zip(queries, results)]), var - - - def avgtruth(self, formula): - """ returns the fraction of chains in which the given formula is currently true """ - t = 0.0 - for c in self.chains: - t += formula(c.state) - return t / len(self.chains) - - -# def write(self, short=False): -# if len(self.chains) > 1: -# for i in range(len(self.infer.queries)): -# self.infer.additionalQueryInfo[i] = "[%d x %d steps, sd=%.3f]" % (len(self.chains), self.chains[0].steps, sqrt(self.var[i])) -# self.inferObject._writeResults(sys.stdout, self.results, shortOutput) From cdb4b70555f8ab6631d7bac65275ecd62d6f9a77 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Tue, 5 Jun 2018 05:42:43 +0530 Subject: [PATCH 07/39] setup all .pyx --- python3/pracmln/mln/inference/.gitignore | 1 - .../mln/inference/{gibbs.py => gibbs.pyx} | 0 .../mln/inference/{infer.py => infer.pyx} | 0 python3/pracmln/mln/inference/ipfpm.pyx | 64 ++ python3/pracmln/mln/inference/maxwalk.pyx | 129 ++++ python3/pracmln/mln/inference/mcsat.pyx | 705 ++++++++++++++++++ python3/pracmln/mln/inference/setup.py | 14 + python3/pracmln/mln/inference/wcspinfer.pyx | 365 +++++++++ 8 files changed, 1277 insertions(+), 1 deletion(-) rename python3/pracmln/mln/inference/{gibbs.py => gibbs.pyx} (100%) rename python3/pracmln/mln/inference/{infer.py => infer.pyx} (100%) create mode 100644 python3/pracmln/mln/inference/ipfpm.pyx create mode 100644 python3/pracmln/mln/inference/maxwalk.pyx create mode 100644 python3/pracmln/mln/inference/mcsat.pyx create mode 100644 python3/pracmln/mln/inference/setup.py create mode 100644 python3/pracmln/mln/inference/wcspinfer.pyx diff --git a/python3/pracmln/mln/inference/.gitignore b/python3/pracmln/mln/inference/.gitignore index e22a709f..c246e93e 100644 --- a/python3/pracmln/mln/inference/.gitignore +++ b/python3/pracmln/mln/inference/.gitignore @@ -1,6 +1,5 @@ *.pyo *.c *.html -setup.py build/* pracmln/* diff --git a/python3/pracmln/mln/inference/gibbs.py b/python3/pracmln/mln/inference/gibbs.pyx similarity index 100% rename from python3/pracmln/mln/inference/gibbs.py rename to python3/pracmln/mln/inference/gibbs.pyx diff --git a/python3/pracmln/mln/inference/infer.py b/python3/pracmln/mln/inference/infer.pyx similarity index 100% rename from python3/pracmln/mln/inference/infer.py rename to python3/pracmln/mln/inference/infer.pyx diff --git a/python3/pracmln/mln/inference/ipfpm.pyx b/python3/pracmln/mln/inference/ipfpm.pyx new file mode 100644 index 00000000..74278cf5 --- /dev/null +++ b/python3/pracmln/mln/inference/ipfpm.pyx @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +# +# Markov Logic Networks +# +# (C) 2006-2010 by Dominik Jain (jain@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 dnutils import logs + +from .infer import Inference +from .exact import EnumerationAsk + + +logger = logs.getlogger(__name__) + + +class IPFPM(Inference): + """ the iterative proportional fitting procedure applied at the model level (IPFP-M) """ + + def __init__(self, mrf): + # check if there's any soft evidence to actually work on + if len(mrf.getSoftEvidence()) == 0: + raise Exception("Application of IPFP-M inappropriate! IPFP-M is a wrapper method for other inference algorithms that allows to fit probability constraints. An application is not sensical if the model contains no such constraints.") + Inference.__init__(self, mrf) + + def _infer(self, verbose=True, details=False, fittingMethod=EnumerationAsk, fittingThreshold=1e-3, + fittingSteps=100, fittingParams=None, maxThreshold=None, greedy=False, **args): + # add formulas to the model whose weights we can then fit + if verbose: logger.info("extending model with %d formulas whose weights will be fit..." % len(self.mrf.getSoftEvidence())) + for req in self.mrf.getSoftEvidence(): + formula = self.mln.logic.parseFormula(req["expr"]) + idxFormula = self.mrf._addFormula(formula, 0.0) + gndFormula = formula.ground(self.mrf, {}) + self.mrf._addGroundFormula(gndFormula, idxFormula) + req["gndExpr"] = req["expr"] + req["gndFormula"] = gndFormula + req["idxFormula"] = idxFormula + + # do fitting + if fittingParams is None: fittingParams = {} + fittingParams.update(args) + results, self.data = self.mrf._fitProbabilityConstraints(self.mrf.getSoftEvidence(), fittingMethod=fittingMethod, + fittingThreshold=fittingThreshold, fittingSteps=fittingSteps, + given=self.given, queries=self.queries, verbose=details, + fittingParams=fittingParams, maxThreshold=maxThreshold, greedy=greedy) + + return results diff --git a/python3/pracmln/mln/inference/maxwalk.pyx b/python3/pracmln/mln/inference/maxwalk.pyx new file mode 100644 index 00000000..713a3273 --- /dev/null +++ b/python3/pracmln/mln/inference/maxwalk.pyx @@ -0,0 +1,129 @@ +# -*- 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. + +import random +from collections import defaultdict + +from dnutils import ProgressBar + +from .mcmc import MCMCInference +from ..constants import HARD, ALL +from ..grounding.fastconj import FastConjunctionGrounding +from ...logic.common import Logic + + +class SAMaxWalkSAT(MCMCInference): + """ + A MaxWalkSAT MPE solver using simulated annealing. + """ + + + def __init__(self, mrf, queries=ALL, state=None, **params): + MCMCInference.__init__(self, mrf, queries, **params) + if state is None: + self.state = self.random_world(self.mrf.evidence) + else: + self.state = state + self.sum = 0 + self.var2gf = defaultdict(set) + self.weights = list(self.mrf.mln.weights) + formulas = [] + for f in self.mrf.formulas: + if f.weight < 0: + f_ = self.mrf.mln.logic.negate(f) + f_.weight = - f.weight + formulas.append(f_.nnf()) + grounder = FastConjunctionGrounding(mrf, formulas=formulas, simplify=True, unsatfailure=True) + for gf in grounder.itergroundings(): + 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)) + + + @property + def thr(self): + return self._params.get('thr', 0) + + + @property + def hardw(self): + return self._params.get('hardw', 10) + + + @property + def maxsteps(self): + return self._params.get('maxsteps', 500) + + + def _run(self): + i = 0 + i_max = self.maxsteps + thr = self.thr + if self.verbose: + bar = ProgressBar(steps=i_max, color='green') + 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)] + evdict = var.value2dict(var.evidence_value(self.mrf.evidence)) + valuecount = var.valuecount(evdict) + if valuecount == 1: # this is evidence + continue + # compute the sum of relevant gf weights before the modification + sum_before = 0 + for gf in self.var2gf[var.idx]: + 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] + oldstate = list(self.state) + var.setval(value, self.state) + # compute the sum after the modification + sum_after = 0 + for gf in self.var2gf[var.idx]: + sum_after += (self.hardw if gf.weight == HARD else gf.weight) * (1 - gf(self.state)) + # determine whether to keep the new state + keep = False + improvement = sum_after - sum_before + if improvement < 0 or sum_after <= thr: + prob = 1.0 + keep = True + else: + prob = (1.0 - min(1.0, abs(improvement / self.sum))) * (1 - (float(i) / i_max)) + keep = random.uniform(0.0, 1.0) <= prob +# keep = False # !!! no annealing + # apply new objective value + if keep: self.sum += improvement + else: self.state = oldstate + # next iteration + i += 1 + if self.verbose: + bar.label('sum = %f' % self.sum) + bar.inc() + if self.verbose: + print("SAMaxWalkSAT: %d iterations, sum=%f, threshold=%f" % (i, self.sum, self.thr)) + self.mrf.mln.weights = self.weights + return dict([(str(q), self.state[q.gndatom.idx]) for q in self.queries]) diff --git a/python3/pracmln/mln/inference/mcsat.pyx b/python3/pracmln/mln/inference/mcsat.pyx new file mode 100644 index 00000000..756614eb --- /dev/null +++ b/python3/pracmln/mln/inference/mcsat.pyx @@ -0,0 +1,705 @@ +# -*- 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. + +import math +import random +from collections import defaultdict + +from dnutils import logs, ProgressBar, out + +from .mcmc import MCMCInference +from ..constants import ALL, HARD +from ..grounding.fastconj import FastConjunctionGrounding +from ..util import item +from ...logic.common import Logic + + +logger = logs.getlogger(__name__) + + +class MCSAT(MCMCInference): + """ + MC-SAT/MC-SAT-PC + """ + + def __init__(self, mrf, queries=ALL, **params): + MCMCInference.__init__(self, mrf, queries, **params) + self._weight_backup = list(self.mrf.mln.weights) + + + def _initkb(self, verbose=False): + """ + Initialize the knowledge base to the required format and collect structural information for optimization purposes + """ + # convert the MLN ground formulas to CNF + logger.debug("converting formulas to cnf...") + #self.mln._toCNF(allPositive=True) + self.formulas = [] + for f in self.mrf.formulas: + if f.weight < 0: + f.weight = -f.weight + f = self.mln.logic.negate(f) + self.formulas.append(f) +# softweights = [1 - 1 / numpy.exp(w) for w in self.mln.weights if w != HARD] +# stop(sorted(softweights, reverse=True)) +# w_mean = numpy.mean(softweights) +# w_stdev = numpy.std(softweights) +# for f in self.mrf.formulas: +# if f.ishard: continue +# f.weight = min(w_stdev, f.weight) + 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 + 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) + # get clause data + logger.debug("gathering clause data...") + 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 + # 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): + clauses = [gf] + else: continue + self.gf2clauseidx[i_gf] = (i_clause, i_clause + len(clauses)) + # process each clause + for c in clauses: + if hasattr(c, "children"): + lits = c.children + else: # unit clause + lits = [c] + # add clause to list + self.clauses.append(lits) + # next clause index + i_clause += 1 + # add clauses for soft evidence atoms + for se in []:#self.softEvidence: + se["numTrue"] = 0.0 + formula = self.mln.logic.parseFormula(se["expr"]) + se["formula"] = formula.ground(self.mrf, {}) + cnf = formula.toCNF().ground(self.mrf, {}) + idxFirst = i_clause + for clause in self._formulaClauses(cnf): + self.clauses.append(clause) + #print clause + i_clause += 1 + se["idxClausePositive"] = (idxFirst, i_clause) + cnf = self.mln.logic.negation([formula]).toCNF().ground(self.mrf, {}) + idxFirst = i_clause + for clause in self._formulaClauses(cnf): + self.clauses.append(clause) + #print clause + i_clause += 1 + se["idxClauseNegative"] = (idxFirst, i_clause) + + + def _formula_clauses(self, f): + # get the list of clauses + if isinstance(f, Logic.Conjunction): + lc = f.children + else: + lc = [f] + # process each clause + for c in lc: + if hasattr(c, "children"): + yield c.children + else: # unit clause + yield [c] + + + @property + def chains(self): + return self._params.get('chains', 1) + + @property + def maxsteps(self): + return self._params.get('maxsteps', 500) + + @property + def softevidence(self): + return self._params.get('softevidence', False) + + @property + def use_se(self): + return self._params.get('use_se') + + @property + def p(self): + return self._params.get('p', .5) + + @property + def resulthistory(self): + return self._params.get('resulthistory', False) + + @property + def historyfile(self): + return self._params.get('historyfile', None) + + @property + def rndseed(self): + return self._params.get('rndseed', None) + + @property + def initalgo(self): + return self._params.get('initalgo', 'SampleSAT') + + + def _run(self): + """ + p: probability of a greedy (WalkSAT) move + initAlgo: algorithm to use in order to find an initial state that satisfies all hard constraints ("SampleSAT" or "SAMaxWalkSat") + verbose: whether to display results upon completion + details: whether to display information while the algorithm is running + infoInterval: [if details==True] interval (no. of steps) in which to display the current step number and some additional info + resultsInterval: [if details==True] interval (no. of steps) in which to display intermediate results; [if keepResultsHistory==True] interval in which to store intermediate results in the history + debug: whether to display debug information (e.g. internal data structures) while the algorithm is running + debugLevel: controls degree to which debug information is presented + keepResultsHistory: whether to store the history of results (at each resultsInterval) + referenceResults: reference results to compare obtained results to + saveHistoryFile: if not None, save history to given filename + sampleCallback: function that is called for every sample with the sample and step number as parameters + softEvidence: if None, use soft evidence from MLN, otherwise use given dictionary of soft evidence + handleSoftEvidence: if False, ignore all soft evidence in the MCMC sampling (but still compute softe evidence statistics if soft evidence is there) + """ + logger.debug("starting MC-SAT with maxsteps=%d, softevidence=%s" % (self.maxsteps, self.softevidence)) + # initialize the KB and gather required info + self._initkb() + # print CNF KB + logger.debug("CNF KB:") + for gf in self.gndformulas: + logger.debug("%7.3f %s" % (gf.weight, str(gf))) + print() + # set the random seed if it was given + if self.rndseed is not None: + random.seed(self.rndseed) + # create chains + chaingroup = MCMCInference.ChainGroup(self) + self.chaingroup = chaingroup + for i in range(self.chains): + chain = MCMCInference.Chain(self, self.queries) + chaingroup.chain(chain) + # satisfy hard constraints using initialization algorithm + M = [] + NLC = [] + for i, gf in enumerate(self.gndformulas): + if gf.weight == HARD: + if gf.islogical(): + clause_range = self.gf2clauseidx[i] + M.extend(list(range(*clause_range))) + else: + NLC.append(gf) + if M or NLC: + logger.debug('Running SampleSAT') + chain.state = SampleSAT(self.mrf, chain.state, M, NLC, self, p=self.p).run() # Note: can't use p=1.0 because there is a chance of getting into an oscillating state + if logger.level == logs.DEBUG: + self.mrf.print_world_vars(chain.state) + self.step = 1 + logger.debug('running MC-SAT with %d chains' % len(chaingroup.chains)) + self._watch.tag('running MC-SAT', self.verbose) + if self.verbose: + bar = ProgressBar(steps=self.maxsteps, color='green') + while self.step <= self.maxsteps: + # take one step in each chain + for chain in chaingroup.chains: + # choose a subset of the satisfied formulas and sample a state that satisfies them + state = self._satisfy_subset(chain) + # update chain counts + chain.update(state) + if self.verbose: + bar.inc() + bar.label('%d / %d' % (self.step, self.maxsteps)) + # intermediate results + self.step += 1 + # get results + self.step -= 1 + results = chaingroup.results() + return results[0] + + + def _satisfy_subset(self, chain): + """ + Choose a set of logical formulas M to be satisfied (more specifically, M is a set of clause indices) + and also choose a set of non-logical constraints NLC to satisfy + """ + M = [] + NLC = [] + for gfidx, gf in enumerate(self.gndformulas): + if gf(chain.state) == 1 or gf.ishard: + expweight = math.exp(gf.weight) + u = random.uniform(0, expweight) + if u > 1: + if gf.islogical(): + clause_range = self.gf2clauseidx[gfidx] + M.extend(list(range(*clause_range))) + else: + NLC.append(gf) + # add soft evidence constraints + if False:# self.softevidence: + for se in self.softevidence: + p = se["numTrue"] / self.step + + #l = self.phistory.get(strFormula(se["formula"]), []) + #l.append(p) + #self.phistory[strFormula(se["formula"])] = l + + if se["formula"](chain.state): + #print "true case" + add = False + if p < se['p']: + add = True + if add: + M.extend(list(range(*se["idxClausePositive"]))) + #print "positive case: add=%s, %s, %f should become %f" % (add, map(str, [map(str, self.clauses[i]) for i in range(*se["idxClausePositive"])]), p, se["p"]) + else: + #print "false case" + add = False + if p > se["p"]: + add = True + if add: + M.extend(list(range(*se["idxClauseNegative"]))) + #print "negative case: add=%s, %s, %f should become %f" % (add, map(str, [map(str, self.clauses[i]) for i in range(*se["idxClauseNegative"])]), p, se["p"]) + # (uniformly) sample a state that satisfies them + return SampleSAT(self.mrf, chain.state, M, NLC, self, p=self.p).run() + + + def _prob_constraints_deviation(self): + if len(self.softevidence) == 0: + return {} + se_mean, se_max, se_max_item = 0.0, -1, None + for se in self.softevidence: + dev = abs((se["numTrue"] / self.step) - se["p"]) + se_mean += dev + if dev > se_max: + se_max = max(se_max, dev) + se_max_item = se + se_mean /= len(self.softevidence) + return {"pc_dev_mean": se_mean, "pc_dev_max": se_max, "pc_dev_max_item": se_max_item["expr"]} + + + def _extend_results_history(self, results): + cur_results = {"step": self.step, "results": list(results), "time": self._getElapsedTime()[0]} + cur_results.update(self._getProbConstraintsDeviation()) + if self.referenceResults is not None: + cur_results.update(self._compareResults(results, self.referenceResults)) + self.history.append(cur_results) + + + def getResultsHistory(self): + return self.resultsHistory + + +class SampleSAT: + """ + Sample-SAT algorithm. + """ + + def __init__(self, mrf, state, clause_indices, nlcs, infer, p=1): + """ + clause_indices: list of indices of clauses to satisfy + p: probability of performing a greedy WalkSAT move + state: the state (array of booleans) to work with (is reinitialized randomly by this constructor) + NLConstraints: list of grounded non-logical constraints + """ + self.debug = logger.level == logs.DEBUG + self.infer = infer + self.mrf = mrf + self.mln = mrf.mln + self.p = p + # initialize the state randomly (considering the evidence) and obtain block info + self.blockInfo = {} + self.state = self.infer.random_world() +# out(self.state, '(initial state)') + self.init = list(state) + # these are the variables we need to consider for SampleSAT +# self.variables = [v for v in self.mrf.variables if v.valuecount(self.mrf.evidence) > 1] + # list of unsatisfied constraints + self.unsatisfied = set() + # keep a map of bottlenecks: index of the ground atom -> list of constraints where the corresponding lit is a bottleneck + self.bottlenecks = defaultdict(list) # bottlenecks are clauses with exactly one true literal + # ground atom occurrences in constraints: ground atom index -> list of constraints + self.var2clauses = defaultdict(set) + self.clauses = {} + # instantiate clauses + for cidx in clause_indices: + clause = SampleSAT._Clause(self.infer.clauses[cidx], self.state, cidx, self.mrf) + self.clauses[cidx] = clause + if clause.unsatisfied: + self.unsatisfied.add(cidx) + for v in clause.variables(): + self.var2clauses[v].add(clause) +# 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 + SampleSAT._CountConstraint(self, nlc) + else: + raise Exception("SampleSAT cannot handle constraints of type '%s'" % str(type(nlc))) + + + def _print_unsatisfied_constraints(self): + out(" %d unsatisfied: %s" % (len(self.unsatisfied), list(map(str, [self.clauses[i] for i in self.unsatisfied]))), tb=2) + + + def run(self): + # sampling by enumerating all worlds + worlds = [] + for world in self.mrf.worlds(): + skip = False + for clause in list(self.clauses.values()): + if not clause.satisfied_in_world(world): + skip = True + break + if skip: continue + worlds.append(world) + state = worlds[random.randint(0, len(worlds)-1)] + return state + steps = 0 + while self.unsatisfied: + steps += 1 + # make a WalkSat move or a simulated annealing move + if random.uniform(0, 1) <= self.p: + self._walksat_move() + else: + self._sa_move() + return self.state + + + def _walksat_move(self): + """ + Randomly pick one of the unsatisfied constraints and satisfy 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 + clause = self.clauses[clauseidx] + varval_opt = [] + opt = None + for var in clause.variables(): + bottleneck_clauses = [cl for cl in self.var2clauses[var] if cl.bottleneck is not None] + for _, value in var.itervalues(self.mrf.evidence_dicti()): + if not clause.turns_true_with(var, value): continue + unsat = 0 + for c in bottleneck_clauses: + # count the constraints rendered unsatisfied for this value from the bottleneck atoms + turnsfalse = 1 if c.turns_false_with(var, value) else 0 + unsat += turnsfalse + append = False + if opt is None or unsat < opt: + opt = unsat + varval_opt = [] + append = True + elif opt == unsat: + append = True + if append: + varval_opt.append((var, value)) + if varval_opt: + varval = varval_opt[random.randint(0, len(varval_opt) - 1)] + self._setvar(*varval) + + + def _setvar(self, var, val): + """ + Set the truth value of a variable and update the information in the constraints. + """ + var.setval(val, self.state) + for c in self.var2clauses[var]: + satisfied, _ = c.update(var, val) + if satisfied: + if c.cidx in self.unsatisfied: self.unsatisfied.remove(c.cidx) + else: + self.unsatisfied.add(c.cidx) + + + def _sa_move(self): + # randomly pick a variable and flip its value + variables = list(set(self.var2clauses)) + random.shuffle(variables) + var = variables[0] + ev = var.evidence_value() + values = var.valuecount(self.mrf.evidence) + for _, v in var.itervalues(self.mrf.evidence): break + 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 + 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 +# cur = 1 if c.unsatisfied else 0 + unsat += uns# - cur + if unsat <= 0: # the flip causes an improvement. take it with p=1.0 + p = 1. + else: + # !!! the temperature has a great effect on the uniformity of the sampled states! it's a "magic" number + # that needs to be chosen with care. if it's too low, then probabilities will be way off; if it's too high, it will take longer to find solutions + # temp = 14.0 # the higher the temperature, the greater the probability of deciding for a flip + # we take as a heuristic the normalized difference between bottleneck clauses and clauses that actually + # will turn false, such that if no clause will turn false, p is 1 + p = math.exp(- 1 + float(len(bottleneck_clauses) - unsat) / len(bottleneck_clauses)) + # decide and set + if random.uniform(0, 1) <= p: + self._setvar(var, val) + + + class _Clause(object): + + def __init__(self, lits, world, idx, mrf): + self.cidx = idx + self.world = world + self.bottleneck = None + self.mrf = mrf + # check all the literals + self.lits = lits + self.truelits = set() + self.atomidx2lits = defaultdict(set) + for lit in lits: + if isinstance(lit, Logic.TrueFalse): continue + atomidx = lit.gndatom.idx + self.atomidx2lits[atomidx].add(0 if lit.negated else 1) + if lit(world) == 1: + self.truelits.add(atomidx) + if len(self.truelits) == 1 and self._isbottleneck(item(self.truelits)): + self.bottleneck = item(self.truelits) + + + def _isbottleneck(self, atomidx): + atomidx2lits = self.atomidx2lits + if len(self.truelits) != 1 or atomidx not in self.truelits: return False + if len(atomidx2lits[atomidx]) == 1: return True + fst = item(atomidx2lits[atomidx]) + if all([x == fst for x in atomidx2lits[atomidx]]): return False # the atom appears with different polarity in the clause, this is not a bottleneck + return True + + + def turns_false_with(self, var, val): + """ + Returns whether or not this clause would become false if the given variable would take + the given value. Returns False if the clause is already False. + """ + for a, v in var.atomvalues(val): + if a.idx == self.bottleneck and v not in self.atomidx2lits[a.idx]: return True + return False + + + def turns_true_with(self, var, val): + """ + Returns true if this clause will be rendered true by the given variable taking + its given value. + """ + for a, v in var.atomvalues(val): + if self.unsatisfied and v in self.atomidx2lits[a.idx]: return True + return False + + + def update(self, var, val): + """ + Updates the clause information with the given variable and value set in a SampleSAT state. + """ + for a, v in var.atomvalues(val): + if v not in self.atomidx2lits[a.idx]: + if a.idx in self.truelits: self.truelits.remove(a.idx) + else: self.truelits.add(a.idx) + if len(self.truelits) == 1 and self._isbottleneck(item(self.truelits)): + self.bottleneck = item(self.truelits) + else: + self.bottleneck = None + return self.satisfied, self.bottleneck + + + def satisfied_in_world(self, world): + return self.mrf.mln.logic.disjugate(self.lits)(world) == 1 + + @property + def unsatisfied(self): + return not self.truelits + + @property + def satisfied(self): + return not self.unsatisfied + + + def variables(self): + return [self.mrf.variable(self.mrf.gndatom(a)) for a in self.atomidx2lits] + + def greedySatisfy(self): + self.ss._pickAndFlipLiteral([x.gndAtom.idx for x in self.lits], self) + + def __str__(self): + return ' v '.join(map(str, self.lits)) + + + class _CountConstraint: + def __init__(self, sampleSAT, groundCountConstraint): + self.ss = sampleSAT + self.cc = groundCountConstraint + self.trueOnes = [] + self.falseOnes = [] + # determine true and false ones + for ga in groundCountConstraint.gndAtoms: + idxGA = ga.idx + if self.ss.state[idxGA]: + self.trueOnes.append(idxGA) + else: + self.falseOnes.append(idxGA) + self.ss._addGAOccurrence(idxGA, self) + # determine bottlenecks + self._addBottlenecks() + # if the formula is unsatisfied, add it to the list + if not self._isSatisfied(): + self.ss.unsatisfiedConstraints.append(self) + + def _isSatisfied(self): + return eval("len(self.trueOnes) %s self.cc.count" % self.cc.op) + + def _addBottlenecks(self): + # there are only bottlenecks if we are at the border of the interval + numTrue = len(self.trueOnes) + if self.cc.op == "!=": + trueNecks = numTrue == self.cc.count + 1 + falseNecks = numTrue == self.cc.count - 1 + else: + border = numTrue == self.cc.count + trueNecks = border and self.cc.op in ["==", ">="] + falseNecks = border and self.cc.op in ["==", "<="] + if trueNecks: + for idxGA in self.trueOnes: + self.ss._addBottleneck(idxGA, self) + if falseNecks: + for idxGA in self.falseOnes: + self.ss._addBottleneck(idxGA, self) + + def greedySatisfy(self): + c = len(self.trueOnes) + satisfied = self._isSatisfied() + assert not satisfied + if c < self.cc.count and not satisfied: + self.ss._pickAndFlipLiteral(self.falseOnes, self) + elif c > self.cc.count and not satisfied: + self.ss._pickAndFlipLiteral(self.trueOnes, self) + else: # count must be equal and op must be != + self.ss._pickAndFlipLiteral(self.trueOnes + self.falseOnes, self) + + def flipSatisfies(self, idxGA): + if self._isSatisfied(): + return False + c = len(self.trueOnes) + if idxGA in self.trueOnes: + c2 = c - 1 + else: + assert idxGA in self.falseOnes + c2 = c + 1 + return eval("c2 %s self.cc.count" % self.cc.op) + + def handleFlip(self, idxGA): + """ + Handle all effects of the flip except bottlenecks of the flipped + gnd atom and clauses that became unsatisfied as a result of a bottleneck flip + """ + wasSatisfied = self._isSatisfied() + # update true and false ones + if idxGA in self.trueOnes: + self.trueOnes.remove(idxGA) + self.falseOnes.append(idxGA) + else: + self.trueOnes.append(idxGA) + self.falseOnes.remove(idxGA) + isSatisfied = self._isSatisfied() + # if the constraint was previously satisfied and is now unsatisfied or + # if the constraint was previously satisfied and is still satisfied (i.e. we are pushed further into the satisfying interval, away from the border), + # remove all the bottlenecks (if any) + if wasSatisfied: + for idxGndAtom in self.trueOnes + self.falseOnes: + if idxGndAtom in self.ss.bottlenecks and self in self.ss.bottlenecks[idxGndAtom]: # TODO perhaps have a smarter method to know which ones actually were bottlenecks (or even info about whether we had bottlenecks) + if idxGA != idxGndAtom: + self.ss.bottlenecks[idxGndAtom].remove(self) + # the constraint was added to the list of unsatisfied ones in SampleSAT._flipGndAtom (bottleneck flip) + # if the constraint is newly satisfied, remove it from the list of unsatisfied ones + elif not wasSatisfied and isSatisfied: + self.ss.unsatisfiedConstraints.remove(self) + # bottlenecks must be added if, because of the flip, we are now at the border of the satisfying interval + self._addBottlenecks() + + def __str__(self): + return str(self.cc) + + def getFormula(self): + return self.cc + + +# class FuzzyMCSAT(Inference): +# """ +# MC-SAT version supporting fuzzy evidence atoms. +# """ +# +# def __init__(self, mrf, queries=ALL, **params): +# Inference.__init__(self, mrf, queries, **params) +# +# +# def _run(self): +# # create the transformed MLN without fuzzy evidence +# mln_ = self.mrf.mln.copy() +# # mln.domains = {} +# mln_._rmformulas() +# # remove fuzzy preds +# grounder = FastConjunctionGrounding(self.mrf, self.mrf.formulas, unsatfailure=True, multicore=self.multicore) +# for gf in grounder.itergroundings(): +# if gf.ishard: mln_.formula(gf, weight=HARD) +# cnf = gf.cnf() +# # out(cnf.weight, cnf) +# if isinstance(cnf, Logic.TrueFalse) or not hasattr(cnf, 'children'): continue +# mintruth = cnf.mintruth(self.mrf.evidence) +# maxtruth = cnf.maxtruth(self.mrf.evidence) +# # out(mintruth, maxtruth) +# # the result of following list comprehension is a list of clauses of literals of the +# # CNF, where all truth constants have been removed. If there are conjuncts consisting +# # of single literals (e.g. such as A in A ^ (B v C)), those will be wrapped in a list +# # with only one element +# clauses = [([mln_.logic.lit(l.negated, l.predname, l.args, mln_) for l in d.children if not isinstance(l, Logic.TrueFalse)] if hasattr(d, 'children') else [mln_.logic.lit(d.negated, d.predname, d.args, mln_)]) for d in cnf.children if not isinstance(d, Logic.TrueFalse)] +# # out(clauses) +# weight = gf.weight * maxtruth +# formula = mln_.logic.conjugate([mln_.logic.disjugate(clause) for clause in clauses]) +# mln_.formula(formula, weight) +# # stop('=>', formula, weight) +# # formula.print_structure() +# # stop() +# # mln_.write() +# # stop() +# mln_.tofile('fuzzyconv.mln') +# mln__ = mln_.materialize(self.mrf.db) +# mrf = mln__.ground(self.mrf.db) +# self.mrf = mrf +# mcsat = MCSAT(mrf, queries=self.queries, **self._params) +# return mcsat._run() + diff --git a/python3/pracmln/mln/inference/setup.py b/python3/pracmln/mln/inference/setup.py new file mode 100644 index 00000000..126b3607 --- /dev/null +++ b/python3/pracmln/mln/inference/setup.py @@ -0,0 +1,14 @@ +from distutils.core import setup +#from distutils.extension import Extension +from Cython.Build import cythonize + +#''' +#ext_modules=[ +# Extension("exact", ["exact.pyx"]), +# Extension("mcmc", ["mcmc.pyx"]), +#] +#''' + +setup( + ext_modules=cythonize( ['*.pyx'] ) +) diff --git a/python3/pracmln/mln/inference/wcspinfer.pyx b/python3/pracmln/mln/inference/wcspinfer.pyx new file mode 100644 index 00000000..1f81e98f --- /dev/null +++ b/python3/pracmln/mln/inference/wcspinfer.pyx @@ -0,0 +1,365 @@ +# Weighted Constraint Satisfaction Problems -- MPE inference on MLNs +# +# (C) 2012 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 collections import defaultdict + +from dnutils import logs + +from .infer import Inference +from ..constants import infty, HARD +from ..errors import SatisfiabilityException, MRFValueException +from ..grounding.fastconj import FastConjunctionGrounding +from ..mrfvars import FuzzyVariable +from ..util import (combinations, dict_union, Interval, temporary_evidence) +from ...wcsp import Constraint, WCSP +from ...logic.common import Logic + + +logger = logs.getlogger(__name__) + + +class WCSPInference(Inference): + + def __init__(self, mrf, queries, **params): + Inference.__init__(self, mrf, queries, **params) + + + def _run(self): + result_ = {} + with temporary_evidence(self.mrf): + self.converter = WCSPConverter(self.mrf, multicore=self.multicore, verbose=self.verbose) + result = self.result_dict(verbose=self.verbose) + for query in self.queries: + query = str(query) + result_[query] = result[query] if query in result else self.mrf[query] + return result_ + + + def result_dict(self, verbose=False): + """ + Returns a Database object with the most probable truth assignment. + """ + wcsp = self.converter.convert() + solution, _ = wcsp.solve() + if solution is None: + raise Exception('MLN is unsatisfiable.') + result = {} + for varidx, validx in enumerate(solution): + value = self.converter.domains[varidx][validx] + result.update(self.converter.variables[varidx].value2dict(value)) + return dict([(str(self.mrf.gndatom(idx)), val) for idx, val in result.items()]) + + + +class WCSPConverter(object): + """ + Class for converting an MLN into a WCSP problem for efficient + MPE inference. + """ + + + def __init__(self, mrf, verbose=False, multicore=False): + self.mrf = mrf + self.constraints = {} # mapping the signature of a constaint to its constraint object + self.verbose = verbose + self._createvars() + self.wcsp = WCSP() + self.wcsp.domsizes = [len(self.domains[i]) for i in self.variables] + self.multicore = multicore + + + def _createvars(self): + """ + Create the variables, one binary for each ground atom. + Considers also mutually exclusive blocks of ground atoms. + """ + self.variables = {} # maps an index to its MRF variable + 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 + for variable in self.mrf.variables: + if isinstance(variable, FuzzyVariable): # fuzzy variables are not subject to reasoning + continue + if variable.valuecount(self.mrf.evidence_dicti()) == 1: # the var is fully determined by the evidence + for _, value in variable.itervalues(self.mrf.evidence_dicti()): + break + self.mrf.set_evidence(variable.value2dict(value), erase=False) + continue + self.variables[varidx] = variable + for gndatom in variable.gndatoms: + self.atom2var[gndatom.idx] = varidx + for validx, (_, value) in enumerate(variable.itervalues(self.mrf.evidence_dicti())): + self.domains[varidx].append(value) + self.val2idx[varidx][value] = validx + varidx += 1 + + def convert(self): + """ + Performs a conversion from an MLN into a WCSP. + """ + # mln to be restored after inference + self._weights = list(self.mrf.mln.weights) + mln = self.mrf.mln + logic = mln.logic + # preprocess the formulas + formulas = [] + for f in self.mrf.formulas: + if f.weight == 0: + continue + if f.weight < 0: + f = logic.negate(f) + f.weight = -f.weight + formulas.append(f.nnf()) + # preprocess the ground formulas +# 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 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 + continue + self.generate_constraint(gf) + self.mrf.mln.weights = self._weights + return self.wcsp + + + def generate_constraint(self, wf): + """ + Generates and adds a constraint from a given weighted formula. + """ + varindices = tuple([self.atom2var[x] for x in wf.gndatom_indices()]) + seen = set() + varindices_ = [] + for v in varindices: + if v in seen: continue + varindices_.append(v) + seen.add(v) + varindices = tuple(varindices_) + # collect the constraint tuples + cost2assignments = self._gather_constraint_tuples(varindices, wf) + if cost2assignments is None: + return + defcost = max(cost2assignments, key=lambda x: infty if cost2assignments[x] == 'else' else len(cost2assignments[x])) + del cost2assignments[defcost] # remove the default cost values + + constraint = Constraint(varindices, defcost=defcost) + for cost, tuples in cost2assignments.items(): + for t in tuples: + constraint.tuple(t, cost) + self.wcsp.constraint(constraint) + + + def _gather_constraint_tuples(self, varindices, formula): + """ + Collects and evaluates all tuples that belong to the constraint + given by a formula. In case of disjunctions and conjunctions, + this is fairly efficient since not all combinations + need to be evaluated. Returns a dictionary mapping the constraint + costs to the list of respective variable assignments. + """ + logic = self.mrf.mln.logic + # we can treat conjunctions and disjunctions fairly efficiently + defaultProcedure = False + 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 + 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 + # 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. + (gndatom, val) = (gndlit.gndatom, not gndlit.negated) + if disj: val = not val + val = 1 if val else 0 + variable = self.variables[self.atom2var[gndatom.idx]] + # in case there are multiple values of a variable that may render the formula true + # we cannot apply this efficient implementation and have to fall back to the naive impl. + tmp_evidence = variable.value2dict(variable.evidence_value()) + evval = tmp_evidence.get(gndatom.idx) + if evval is not None and evval != val: + # the supposed value of the variable and the evidence value mismatch, + # so the conjunction (disjunction) can never be rendered true (false) + return + tmp_evidence = dict_union(tmp_evidence, {gndatom.idx: val}) + if variable.valuecount(tmp_evidence) > 1: + defaultProcedure = True + break + # there is only one value remaining + for _, value in variable.itervalues(tmp_evidence): + varidx = self.atom2var[gndatom.idx] + validx = self.val2idx[varidx][value] + # if there are two different values needed to render the formula true... + if assignment[varindices.index(varidx)] is not None and assignment[varindices.index(varidx)] != value: + if formula.weight == HARD: + if conj: # ...if it's a hard conjunction, the MLN is unsatisfiable -- e.g. foo(x) ^ !foo(x) + raise SatisfiabilityException('Knowledge base is unsatisfiable due to hard constraint violation: %s' % formula) + elif disj: # ...if it's a hard disjunction, it's a tautology -- e.g. foo(x) v !foo(x) + continue + else: # for soft constraints, unsatisfiable formulas and tautologies can be ignored + return None + assignment[varindices.index(varidx)] = validx + if not defaultProcedure: + maxtruth = formula.maxtruth(self.mrf.evidence) + mintruth = formula.mintruth(self.mrf.evidence) + if formula.weight == HARD and (maxtruth in Interval(']0,1[') or mintruth in Interval(']0,1[')): + raise MRFValueException('No fuzzy truth values are allowed in hard constraints.') + if conj: + if formula.weight == HARD: + cost = 0 + defcost = self.wcsp.top + else: + cost = formula.weight * (1 - maxtruth) + defcost = formula.weight + else: + if formula.weight == HARD: + cost = self.wcsp.top + defcost = 0 + else: + defcost = 0 + cost = formula.weight * (1 - mintruth) + if len(assignment) != len(varindices): + raise MRFValueException('Illegal variable assignments. Variables: %s, Assignment: %s' % (varindices, assignment)) + return {cost: [tuple(assignment)], defcost: 'else'} + if defaultProcedure: + # 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 + 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) + 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) + if truth is None: + print('POSSIBLE WORLD:') + print('===============') + self.mrf.print_world_vars(world) + print('GROUND FORMULA:') + print('===============') + formula.print_structure(world) + raise Exception('Something went wrong: Truth of ground formula cannot be evaluated (see above)') + + if truth in Interval(']0,1[') and formula.weight == HARD: + raise MRFValueException('No fuzzy truth values are allowed in hard constraints.') + if formula.weight == HARD: + if truth == 1: + cost = 0 + else: + cost = self.wcsp.top + else: + cost = ((1 - truth) * formula.weight) + cost2assignments[cost].append(tuple(assignment)) + return cost2assignments + assert False # unreachable + + + def forbid_gndatom(self, atom, truth=True): + """ + Adds a unary constraint that prohibits the given ground atom + being true. + """ + atomidx = atom if type(atom) is int else (self.mrf.gndatom(atom).idx if type(atom) is str else atom.idx) + varidx = self.atom2var[atomidx] + variable = self.variables[varidx] + evidence = list(self.mrf.evidence) + evidence[atomidx] = {True: 1, False: 0}[truth] + c = Constraint((varidx,)) + for _, value in variable.itervalues(evidence): + validx = self.val2idx[varidx][value] + c.tuple((validx,), self.wcsp.top) + self.wcsp.constraint(c) + + + def getPseudoDistributionForGndAtom(self, gndAtom): + """ + Computes a relative "distribution" for all possible variable assignments of + a mutex constraint. This can be used to determine the confidence in particular + most probable world by comparing the score with the second-most probable one. + """ + if isinstance(gndAtom, str): + gndAtom = self.mrf.gndAtoms[gndAtom] + + if not isinstance(gndAtom, Logic.GroundAtom): + raise Exception('Argument must be a ground atom') + + varIdx = self.gndAtom2VarIndex[gndAtom] + valIndices = list(range(len(self.varIdx2GndAtom[varIdx]))) + mutex = len(self.varIdx2GndAtom[varIdx]) > 1 + if not mutex: + raise Exception("Pseudo distribution is provided for mutex constraints only.") + wcsp = self.convert() + atoms = [] + cost = [] + try: + while len(valIndices) > 0: + s, c = wcsp.solve() + if s is None: raise + val = s[varIdx] + atom = self.varIdx2GndAtom[varIdx][val] + self.forbidGndAtom(atom, wcsp) + valIndices.remove(val) + cost.append(c) + atoms.append(atom) + except: pass + c_max = max(cost) + for i, c in enumerate(cost): + cost[i] = c_max - c + c_sum = sum(cost) + for i, c in enumerate(cost): + cost[i] = float(c) / c_sum + return dict([(a,c) for a, c in zip(atoms, cost)]) + + +# for debugging only +if __name__ == '__main__': + pass +# mln = MLN('/home/nyga/code/prac/models/experimental/deep_sense/priors.mln') +# db = Database(mln, '/home/nyga/code/prac/models/experimental/deep_sense/db/1.db') +# mrf = mln.groundMRF(db) +# +# conv = WCSPConverter(mrf) +# wcsp = conv.convert() +# wcsp.write(sys.stdout) +# solution = wcsp.solve() +# for i, s in enumerate(solution): +# print conv.varIdx2GndAtom[i][0], s, +# for ga in conv.varIdx2GndAtom[i]: print ga, +# print + From c5eebf7e87ecbd4c48584bb75a799bf75b4440d6 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Tue, 5 Jun 2018 05:43:46 +0530 Subject: [PATCH 08/39] remove unneccessary old .py files --- python3/pracmln/mln/inference/ipfpm.py | 64 -- python3/pracmln/mln/inference/maxwalk.py | 129 ---- python3/pracmln/mln/inference/mcsat.py | 705 --------------------- python3/pracmln/mln/inference/wcspinfer.py | 365 ----------- 4 files changed, 1263 deletions(-) delete mode 100644 python3/pracmln/mln/inference/ipfpm.py delete mode 100644 python3/pracmln/mln/inference/maxwalk.py delete mode 100644 python3/pracmln/mln/inference/mcsat.py delete mode 100644 python3/pracmln/mln/inference/wcspinfer.py diff --git a/python3/pracmln/mln/inference/ipfpm.py b/python3/pracmln/mln/inference/ipfpm.py deleted file mode 100644 index 74278cf5..00000000 --- a/python3/pracmln/mln/inference/ipfpm.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Markov Logic Networks -# -# (C) 2006-2010 by Dominik Jain (jain@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 dnutils import logs - -from .infer import Inference -from .exact import EnumerationAsk - - -logger = logs.getlogger(__name__) - - -class IPFPM(Inference): - """ the iterative proportional fitting procedure applied at the model level (IPFP-M) """ - - def __init__(self, mrf): - # check if there's any soft evidence to actually work on - if len(mrf.getSoftEvidence()) == 0: - raise Exception("Application of IPFP-M inappropriate! IPFP-M is a wrapper method for other inference algorithms that allows to fit probability constraints. An application is not sensical if the model contains no such constraints.") - Inference.__init__(self, mrf) - - def _infer(self, verbose=True, details=False, fittingMethod=EnumerationAsk, fittingThreshold=1e-3, - fittingSteps=100, fittingParams=None, maxThreshold=None, greedy=False, **args): - # add formulas to the model whose weights we can then fit - if verbose: logger.info("extending model with %d formulas whose weights will be fit..." % len(self.mrf.getSoftEvidence())) - for req in self.mrf.getSoftEvidence(): - formula = self.mln.logic.parseFormula(req["expr"]) - idxFormula = self.mrf._addFormula(formula, 0.0) - gndFormula = formula.ground(self.mrf, {}) - self.mrf._addGroundFormula(gndFormula, idxFormula) - req["gndExpr"] = req["expr"] - req["gndFormula"] = gndFormula - req["idxFormula"] = idxFormula - - # do fitting - if fittingParams is None: fittingParams = {} - fittingParams.update(args) - results, self.data = self.mrf._fitProbabilityConstraints(self.mrf.getSoftEvidence(), fittingMethod=fittingMethod, - fittingThreshold=fittingThreshold, fittingSteps=fittingSteps, - given=self.given, queries=self.queries, verbose=details, - fittingParams=fittingParams, maxThreshold=maxThreshold, greedy=greedy) - - return results diff --git a/python3/pracmln/mln/inference/maxwalk.py b/python3/pracmln/mln/inference/maxwalk.py deleted file mode 100644 index 713a3273..00000000 --- a/python3/pracmln/mln/inference/maxwalk.py +++ /dev/null @@ -1,129 +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. - -import random -from collections import defaultdict - -from dnutils import ProgressBar - -from .mcmc import MCMCInference -from ..constants import HARD, ALL -from ..grounding.fastconj import FastConjunctionGrounding -from ...logic.common import Logic - - -class SAMaxWalkSAT(MCMCInference): - """ - A MaxWalkSAT MPE solver using simulated annealing. - """ - - - def __init__(self, mrf, queries=ALL, state=None, **params): - MCMCInference.__init__(self, mrf, queries, **params) - if state is None: - self.state = self.random_world(self.mrf.evidence) - else: - self.state = state - self.sum = 0 - self.var2gf = defaultdict(set) - self.weights = list(self.mrf.mln.weights) - formulas = [] - for f in self.mrf.formulas: - if f.weight < 0: - f_ = self.mrf.mln.logic.negate(f) - f_.weight = - f.weight - formulas.append(f_.nnf()) - grounder = FastConjunctionGrounding(mrf, formulas=formulas, simplify=True, unsatfailure=True) - for gf in grounder.itergroundings(): - 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)) - - - @property - def thr(self): - return self._params.get('thr', 0) - - - @property - def hardw(self): - return self._params.get('hardw', 10) - - - @property - def maxsteps(self): - return self._params.get('maxsteps', 500) - - - def _run(self): - i = 0 - i_max = self.maxsteps - thr = self.thr - if self.verbose: - bar = ProgressBar(steps=i_max, color='green') - 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)] - evdict = var.value2dict(var.evidence_value(self.mrf.evidence)) - valuecount = var.valuecount(evdict) - if valuecount == 1: # this is evidence - continue - # compute the sum of relevant gf weights before the modification - sum_before = 0 - for gf in self.var2gf[var.idx]: - 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] - oldstate = list(self.state) - var.setval(value, self.state) - # compute the sum after the modification - sum_after = 0 - for gf in self.var2gf[var.idx]: - sum_after += (self.hardw if gf.weight == HARD else gf.weight) * (1 - gf(self.state)) - # determine whether to keep the new state - keep = False - improvement = sum_after - sum_before - if improvement < 0 or sum_after <= thr: - prob = 1.0 - keep = True - else: - prob = (1.0 - min(1.0, abs(improvement / self.sum))) * (1 - (float(i) / i_max)) - keep = random.uniform(0.0, 1.0) <= prob -# keep = False # !!! no annealing - # apply new objective value - if keep: self.sum += improvement - else: self.state = oldstate - # next iteration - i += 1 - if self.verbose: - bar.label('sum = %f' % self.sum) - bar.inc() - if self.verbose: - print("SAMaxWalkSAT: %d iterations, sum=%f, threshold=%f" % (i, self.sum, self.thr)) - self.mrf.mln.weights = self.weights - return dict([(str(q), self.state[q.gndatom.idx]) for q in self.queries]) diff --git a/python3/pracmln/mln/inference/mcsat.py b/python3/pracmln/mln/inference/mcsat.py deleted file mode 100644 index 756614eb..00000000 --- a/python3/pracmln/mln/inference/mcsat.py +++ /dev/null @@ -1,705 +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. - -import math -import random -from collections import defaultdict - -from dnutils import logs, ProgressBar, out - -from .mcmc import MCMCInference -from ..constants import ALL, HARD -from ..grounding.fastconj import FastConjunctionGrounding -from ..util import item -from ...logic.common import Logic - - -logger = logs.getlogger(__name__) - - -class MCSAT(MCMCInference): - """ - MC-SAT/MC-SAT-PC - """ - - def __init__(self, mrf, queries=ALL, **params): - MCMCInference.__init__(self, mrf, queries, **params) - self._weight_backup = list(self.mrf.mln.weights) - - - def _initkb(self, verbose=False): - """ - Initialize the knowledge base to the required format and collect structural information for optimization purposes - """ - # convert the MLN ground formulas to CNF - logger.debug("converting formulas to cnf...") - #self.mln._toCNF(allPositive=True) - self.formulas = [] - for f in self.mrf.formulas: - if f.weight < 0: - f.weight = -f.weight - f = self.mln.logic.negate(f) - self.formulas.append(f) -# softweights = [1 - 1 / numpy.exp(w) for w in self.mln.weights if w != HARD] -# stop(sorted(softweights, reverse=True)) -# w_mean = numpy.mean(softweights) -# w_stdev = numpy.std(softweights) -# for f in self.mrf.formulas: -# if f.ishard: continue -# f.weight = min(w_stdev, f.weight) - 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 - 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) - # get clause data - logger.debug("gathering clause data...") - 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 - # 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): - clauses = [gf] - else: continue - self.gf2clauseidx[i_gf] = (i_clause, i_clause + len(clauses)) - # process each clause - for c in clauses: - if hasattr(c, "children"): - lits = c.children - else: # unit clause - lits = [c] - # add clause to list - self.clauses.append(lits) - # next clause index - i_clause += 1 - # add clauses for soft evidence atoms - for se in []:#self.softEvidence: - se["numTrue"] = 0.0 - formula = self.mln.logic.parseFormula(se["expr"]) - se["formula"] = formula.ground(self.mrf, {}) - cnf = formula.toCNF().ground(self.mrf, {}) - idxFirst = i_clause - for clause in self._formulaClauses(cnf): - self.clauses.append(clause) - #print clause - i_clause += 1 - se["idxClausePositive"] = (idxFirst, i_clause) - cnf = self.mln.logic.negation([formula]).toCNF().ground(self.mrf, {}) - idxFirst = i_clause - for clause in self._formulaClauses(cnf): - self.clauses.append(clause) - #print clause - i_clause += 1 - se["idxClauseNegative"] = (idxFirst, i_clause) - - - def _formula_clauses(self, f): - # get the list of clauses - if isinstance(f, Logic.Conjunction): - lc = f.children - else: - lc = [f] - # process each clause - for c in lc: - if hasattr(c, "children"): - yield c.children - else: # unit clause - yield [c] - - - @property - def chains(self): - return self._params.get('chains', 1) - - @property - def maxsteps(self): - return self._params.get('maxsteps', 500) - - @property - def softevidence(self): - return self._params.get('softevidence', False) - - @property - def use_se(self): - return self._params.get('use_se') - - @property - def p(self): - return self._params.get('p', .5) - - @property - def resulthistory(self): - return self._params.get('resulthistory', False) - - @property - def historyfile(self): - return self._params.get('historyfile', None) - - @property - def rndseed(self): - return self._params.get('rndseed', None) - - @property - def initalgo(self): - return self._params.get('initalgo', 'SampleSAT') - - - def _run(self): - """ - p: probability of a greedy (WalkSAT) move - initAlgo: algorithm to use in order to find an initial state that satisfies all hard constraints ("SampleSAT" or "SAMaxWalkSat") - verbose: whether to display results upon completion - details: whether to display information while the algorithm is running - infoInterval: [if details==True] interval (no. of steps) in which to display the current step number and some additional info - resultsInterval: [if details==True] interval (no. of steps) in which to display intermediate results; [if keepResultsHistory==True] interval in which to store intermediate results in the history - debug: whether to display debug information (e.g. internal data structures) while the algorithm is running - debugLevel: controls degree to which debug information is presented - keepResultsHistory: whether to store the history of results (at each resultsInterval) - referenceResults: reference results to compare obtained results to - saveHistoryFile: if not None, save history to given filename - sampleCallback: function that is called for every sample with the sample and step number as parameters - softEvidence: if None, use soft evidence from MLN, otherwise use given dictionary of soft evidence - handleSoftEvidence: if False, ignore all soft evidence in the MCMC sampling (but still compute softe evidence statistics if soft evidence is there) - """ - logger.debug("starting MC-SAT with maxsteps=%d, softevidence=%s" % (self.maxsteps, self.softevidence)) - # initialize the KB and gather required info - self._initkb() - # print CNF KB - logger.debug("CNF KB:") - for gf in self.gndformulas: - logger.debug("%7.3f %s" % (gf.weight, str(gf))) - print() - # set the random seed if it was given - if self.rndseed is not None: - random.seed(self.rndseed) - # create chains - chaingroup = MCMCInference.ChainGroup(self) - self.chaingroup = chaingroup - for i in range(self.chains): - chain = MCMCInference.Chain(self, self.queries) - chaingroup.chain(chain) - # satisfy hard constraints using initialization algorithm - M = [] - NLC = [] - for i, gf in enumerate(self.gndformulas): - if gf.weight == HARD: - if gf.islogical(): - clause_range = self.gf2clauseidx[i] - M.extend(list(range(*clause_range))) - else: - NLC.append(gf) - if M or NLC: - logger.debug('Running SampleSAT') - chain.state = SampleSAT(self.mrf, chain.state, M, NLC, self, p=self.p).run() # Note: can't use p=1.0 because there is a chance of getting into an oscillating state - if logger.level == logs.DEBUG: - self.mrf.print_world_vars(chain.state) - self.step = 1 - logger.debug('running MC-SAT with %d chains' % len(chaingroup.chains)) - self._watch.tag('running MC-SAT', self.verbose) - if self.verbose: - bar = ProgressBar(steps=self.maxsteps, color='green') - while self.step <= self.maxsteps: - # take one step in each chain - for chain in chaingroup.chains: - # choose a subset of the satisfied formulas and sample a state that satisfies them - state = self._satisfy_subset(chain) - # update chain counts - chain.update(state) - if self.verbose: - bar.inc() - bar.label('%d / %d' % (self.step, self.maxsteps)) - # intermediate results - self.step += 1 - # get results - self.step -= 1 - results = chaingroup.results() - return results[0] - - - def _satisfy_subset(self, chain): - """ - Choose a set of logical formulas M to be satisfied (more specifically, M is a set of clause indices) - and also choose a set of non-logical constraints NLC to satisfy - """ - M = [] - NLC = [] - for gfidx, gf in enumerate(self.gndformulas): - if gf(chain.state) == 1 or gf.ishard: - expweight = math.exp(gf.weight) - u = random.uniform(0, expweight) - if u > 1: - if gf.islogical(): - clause_range = self.gf2clauseidx[gfidx] - M.extend(list(range(*clause_range))) - else: - NLC.append(gf) - # add soft evidence constraints - if False:# self.softevidence: - for se in self.softevidence: - p = se["numTrue"] / self.step - - #l = self.phistory.get(strFormula(se["formula"]), []) - #l.append(p) - #self.phistory[strFormula(se["formula"])] = l - - if se["formula"](chain.state): - #print "true case" - add = False - if p < se['p']: - add = True - if add: - M.extend(list(range(*se["idxClausePositive"]))) - #print "positive case: add=%s, %s, %f should become %f" % (add, map(str, [map(str, self.clauses[i]) for i in range(*se["idxClausePositive"])]), p, se["p"]) - else: - #print "false case" - add = False - if p > se["p"]: - add = True - if add: - M.extend(list(range(*se["idxClauseNegative"]))) - #print "negative case: add=%s, %s, %f should become %f" % (add, map(str, [map(str, self.clauses[i]) for i in range(*se["idxClauseNegative"])]), p, se["p"]) - # (uniformly) sample a state that satisfies them - return SampleSAT(self.mrf, chain.state, M, NLC, self, p=self.p).run() - - - def _prob_constraints_deviation(self): - if len(self.softevidence) == 0: - return {} - se_mean, se_max, se_max_item = 0.0, -1, None - for se in self.softevidence: - dev = abs((se["numTrue"] / self.step) - se["p"]) - se_mean += dev - if dev > se_max: - se_max = max(se_max, dev) - se_max_item = se - se_mean /= len(self.softevidence) - return {"pc_dev_mean": se_mean, "pc_dev_max": se_max, "pc_dev_max_item": se_max_item["expr"]} - - - def _extend_results_history(self, results): - cur_results = {"step": self.step, "results": list(results), "time": self._getElapsedTime()[0]} - cur_results.update(self._getProbConstraintsDeviation()) - if self.referenceResults is not None: - cur_results.update(self._compareResults(results, self.referenceResults)) - self.history.append(cur_results) - - - def getResultsHistory(self): - return self.resultsHistory - - -class SampleSAT: - """ - Sample-SAT algorithm. - """ - - def __init__(self, mrf, state, clause_indices, nlcs, infer, p=1): - """ - clause_indices: list of indices of clauses to satisfy - p: probability of performing a greedy WalkSAT move - state: the state (array of booleans) to work with (is reinitialized randomly by this constructor) - NLConstraints: list of grounded non-logical constraints - """ - self.debug = logger.level == logs.DEBUG - self.infer = infer - self.mrf = mrf - self.mln = mrf.mln - self.p = p - # initialize the state randomly (considering the evidence) and obtain block info - self.blockInfo = {} - self.state = self.infer.random_world() -# out(self.state, '(initial state)') - self.init = list(state) - # these are the variables we need to consider for SampleSAT -# self.variables = [v for v in self.mrf.variables if v.valuecount(self.mrf.evidence) > 1] - # list of unsatisfied constraints - self.unsatisfied = set() - # keep a map of bottlenecks: index of the ground atom -> list of constraints where the corresponding lit is a bottleneck - self.bottlenecks = defaultdict(list) # bottlenecks are clauses with exactly one true literal - # ground atom occurrences in constraints: ground atom index -> list of constraints - self.var2clauses = defaultdict(set) - self.clauses = {} - # instantiate clauses - for cidx in clause_indices: - clause = SampleSAT._Clause(self.infer.clauses[cidx], self.state, cidx, self.mrf) - self.clauses[cidx] = clause - if clause.unsatisfied: - self.unsatisfied.add(cidx) - for v in clause.variables(): - self.var2clauses[v].add(clause) -# 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 - SampleSAT._CountConstraint(self, nlc) - else: - raise Exception("SampleSAT cannot handle constraints of type '%s'" % str(type(nlc))) - - - def _print_unsatisfied_constraints(self): - out(" %d unsatisfied: %s" % (len(self.unsatisfied), list(map(str, [self.clauses[i] for i in self.unsatisfied]))), tb=2) - - - def run(self): - # sampling by enumerating all worlds - worlds = [] - for world in self.mrf.worlds(): - skip = False - for clause in list(self.clauses.values()): - if not clause.satisfied_in_world(world): - skip = True - break - if skip: continue - worlds.append(world) - state = worlds[random.randint(0, len(worlds)-1)] - return state - steps = 0 - while self.unsatisfied: - steps += 1 - # make a WalkSat move or a simulated annealing move - if random.uniform(0, 1) <= self.p: - self._walksat_move() - else: - self._sa_move() - return self.state - - - def _walksat_move(self): - """ - Randomly pick one of the unsatisfied constraints and satisfy 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 - clause = self.clauses[clauseidx] - varval_opt = [] - opt = None - for var in clause.variables(): - bottleneck_clauses = [cl for cl in self.var2clauses[var] if cl.bottleneck is not None] - for _, value in var.itervalues(self.mrf.evidence_dicti()): - if not clause.turns_true_with(var, value): continue - unsat = 0 - for c in bottleneck_clauses: - # count the constraints rendered unsatisfied for this value from the bottleneck atoms - turnsfalse = 1 if c.turns_false_with(var, value) else 0 - unsat += turnsfalse - append = False - if opt is None or unsat < opt: - opt = unsat - varval_opt = [] - append = True - elif opt == unsat: - append = True - if append: - varval_opt.append((var, value)) - if varval_opt: - varval = varval_opt[random.randint(0, len(varval_opt) - 1)] - self._setvar(*varval) - - - def _setvar(self, var, val): - """ - Set the truth value of a variable and update the information in the constraints. - """ - var.setval(val, self.state) - for c in self.var2clauses[var]: - satisfied, _ = c.update(var, val) - if satisfied: - if c.cidx in self.unsatisfied: self.unsatisfied.remove(c.cidx) - else: - self.unsatisfied.add(c.cidx) - - - def _sa_move(self): - # randomly pick a variable and flip its value - variables = list(set(self.var2clauses)) - random.shuffle(variables) - var = variables[0] - ev = var.evidence_value() - values = var.valuecount(self.mrf.evidence) - for _, v in var.itervalues(self.mrf.evidence): break - 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 - 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 -# cur = 1 if c.unsatisfied else 0 - unsat += uns# - cur - if unsat <= 0: # the flip causes an improvement. take it with p=1.0 - p = 1. - else: - # !!! the temperature has a great effect on the uniformity of the sampled states! it's a "magic" number - # that needs to be chosen with care. if it's too low, then probabilities will be way off; if it's too high, it will take longer to find solutions - # temp = 14.0 # the higher the temperature, the greater the probability of deciding for a flip - # we take as a heuristic the normalized difference between bottleneck clauses and clauses that actually - # will turn false, such that if no clause will turn false, p is 1 - p = math.exp(- 1 + float(len(bottleneck_clauses) - unsat) / len(bottleneck_clauses)) - # decide and set - if random.uniform(0, 1) <= p: - self._setvar(var, val) - - - class _Clause(object): - - def __init__(self, lits, world, idx, mrf): - self.cidx = idx - self.world = world - self.bottleneck = None - self.mrf = mrf - # check all the literals - self.lits = lits - self.truelits = set() - self.atomidx2lits = defaultdict(set) - for lit in lits: - if isinstance(lit, Logic.TrueFalse): continue - atomidx = lit.gndatom.idx - self.atomidx2lits[atomidx].add(0 if lit.negated else 1) - if lit(world) == 1: - self.truelits.add(atomidx) - if len(self.truelits) == 1 and self._isbottleneck(item(self.truelits)): - self.bottleneck = item(self.truelits) - - - def _isbottleneck(self, atomidx): - atomidx2lits = self.atomidx2lits - if len(self.truelits) != 1 or atomidx not in self.truelits: return False - if len(atomidx2lits[atomidx]) == 1: return True - fst = item(atomidx2lits[atomidx]) - if all([x == fst for x in atomidx2lits[atomidx]]): return False # the atom appears with different polarity in the clause, this is not a bottleneck - return True - - - def turns_false_with(self, var, val): - """ - Returns whether or not this clause would become false if the given variable would take - the given value. Returns False if the clause is already False. - """ - for a, v in var.atomvalues(val): - if a.idx == self.bottleneck and v not in self.atomidx2lits[a.idx]: return True - return False - - - def turns_true_with(self, var, val): - """ - Returns true if this clause will be rendered true by the given variable taking - its given value. - """ - for a, v in var.atomvalues(val): - if self.unsatisfied and v in self.atomidx2lits[a.idx]: return True - return False - - - def update(self, var, val): - """ - Updates the clause information with the given variable and value set in a SampleSAT state. - """ - for a, v in var.atomvalues(val): - if v not in self.atomidx2lits[a.idx]: - if a.idx in self.truelits: self.truelits.remove(a.idx) - else: self.truelits.add(a.idx) - if len(self.truelits) == 1 and self._isbottleneck(item(self.truelits)): - self.bottleneck = item(self.truelits) - else: - self.bottleneck = None - return self.satisfied, self.bottleneck - - - def satisfied_in_world(self, world): - return self.mrf.mln.logic.disjugate(self.lits)(world) == 1 - - @property - def unsatisfied(self): - return not self.truelits - - @property - def satisfied(self): - return not self.unsatisfied - - - def variables(self): - return [self.mrf.variable(self.mrf.gndatom(a)) for a in self.atomidx2lits] - - def greedySatisfy(self): - self.ss._pickAndFlipLiteral([x.gndAtom.idx for x in self.lits], self) - - def __str__(self): - return ' v '.join(map(str, self.lits)) - - - class _CountConstraint: - def __init__(self, sampleSAT, groundCountConstraint): - self.ss = sampleSAT - self.cc = groundCountConstraint - self.trueOnes = [] - self.falseOnes = [] - # determine true and false ones - for ga in groundCountConstraint.gndAtoms: - idxGA = ga.idx - if self.ss.state[idxGA]: - self.trueOnes.append(idxGA) - else: - self.falseOnes.append(idxGA) - self.ss._addGAOccurrence(idxGA, self) - # determine bottlenecks - self._addBottlenecks() - # if the formula is unsatisfied, add it to the list - if not self._isSatisfied(): - self.ss.unsatisfiedConstraints.append(self) - - def _isSatisfied(self): - return eval("len(self.trueOnes) %s self.cc.count" % self.cc.op) - - def _addBottlenecks(self): - # there are only bottlenecks if we are at the border of the interval - numTrue = len(self.trueOnes) - if self.cc.op == "!=": - trueNecks = numTrue == self.cc.count + 1 - falseNecks = numTrue == self.cc.count - 1 - else: - border = numTrue == self.cc.count - trueNecks = border and self.cc.op in ["==", ">="] - falseNecks = border and self.cc.op in ["==", "<="] - if trueNecks: - for idxGA in self.trueOnes: - self.ss._addBottleneck(idxGA, self) - if falseNecks: - for idxGA in self.falseOnes: - self.ss._addBottleneck(idxGA, self) - - def greedySatisfy(self): - c = len(self.trueOnes) - satisfied = self._isSatisfied() - assert not satisfied - if c < self.cc.count and not satisfied: - self.ss._pickAndFlipLiteral(self.falseOnes, self) - elif c > self.cc.count and not satisfied: - self.ss._pickAndFlipLiteral(self.trueOnes, self) - else: # count must be equal and op must be != - self.ss._pickAndFlipLiteral(self.trueOnes + self.falseOnes, self) - - def flipSatisfies(self, idxGA): - if self._isSatisfied(): - return False - c = len(self.trueOnes) - if idxGA in self.trueOnes: - c2 = c - 1 - else: - assert idxGA in self.falseOnes - c2 = c + 1 - return eval("c2 %s self.cc.count" % self.cc.op) - - def handleFlip(self, idxGA): - """ - Handle all effects of the flip except bottlenecks of the flipped - gnd atom and clauses that became unsatisfied as a result of a bottleneck flip - """ - wasSatisfied = self._isSatisfied() - # update true and false ones - if idxGA in self.trueOnes: - self.trueOnes.remove(idxGA) - self.falseOnes.append(idxGA) - else: - self.trueOnes.append(idxGA) - self.falseOnes.remove(idxGA) - isSatisfied = self._isSatisfied() - # if the constraint was previously satisfied and is now unsatisfied or - # if the constraint was previously satisfied and is still satisfied (i.e. we are pushed further into the satisfying interval, away from the border), - # remove all the bottlenecks (if any) - if wasSatisfied: - for idxGndAtom in self.trueOnes + self.falseOnes: - if idxGndAtom in self.ss.bottlenecks and self in self.ss.bottlenecks[idxGndAtom]: # TODO perhaps have a smarter method to know which ones actually were bottlenecks (or even info about whether we had bottlenecks) - if idxGA != idxGndAtom: - self.ss.bottlenecks[idxGndAtom].remove(self) - # the constraint was added to the list of unsatisfied ones in SampleSAT._flipGndAtom (bottleneck flip) - # if the constraint is newly satisfied, remove it from the list of unsatisfied ones - elif not wasSatisfied and isSatisfied: - self.ss.unsatisfiedConstraints.remove(self) - # bottlenecks must be added if, because of the flip, we are now at the border of the satisfying interval - self._addBottlenecks() - - def __str__(self): - return str(self.cc) - - def getFormula(self): - return self.cc - - -# class FuzzyMCSAT(Inference): -# """ -# MC-SAT version supporting fuzzy evidence atoms. -# """ -# -# def __init__(self, mrf, queries=ALL, **params): -# Inference.__init__(self, mrf, queries, **params) -# -# -# def _run(self): -# # create the transformed MLN without fuzzy evidence -# mln_ = self.mrf.mln.copy() -# # mln.domains = {} -# mln_._rmformulas() -# # remove fuzzy preds -# grounder = FastConjunctionGrounding(self.mrf, self.mrf.formulas, unsatfailure=True, multicore=self.multicore) -# for gf in grounder.itergroundings(): -# if gf.ishard: mln_.formula(gf, weight=HARD) -# cnf = gf.cnf() -# # out(cnf.weight, cnf) -# if isinstance(cnf, Logic.TrueFalse) or not hasattr(cnf, 'children'): continue -# mintruth = cnf.mintruth(self.mrf.evidence) -# maxtruth = cnf.maxtruth(self.mrf.evidence) -# # out(mintruth, maxtruth) -# # the result of following list comprehension is a list of clauses of literals of the -# # CNF, where all truth constants have been removed. If there are conjuncts consisting -# # of single literals (e.g. such as A in A ^ (B v C)), those will be wrapped in a list -# # with only one element -# clauses = [([mln_.logic.lit(l.negated, l.predname, l.args, mln_) for l in d.children if not isinstance(l, Logic.TrueFalse)] if hasattr(d, 'children') else [mln_.logic.lit(d.negated, d.predname, d.args, mln_)]) for d in cnf.children if not isinstance(d, Logic.TrueFalse)] -# # out(clauses) -# weight = gf.weight * maxtruth -# formula = mln_.logic.conjugate([mln_.logic.disjugate(clause) for clause in clauses]) -# mln_.formula(formula, weight) -# # stop('=>', formula, weight) -# # formula.print_structure() -# # stop() -# # mln_.write() -# # stop() -# mln_.tofile('fuzzyconv.mln') -# mln__ = mln_.materialize(self.mrf.db) -# mrf = mln__.ground(self.mrf.db) -# self.mrf = mrf -# mcsat = MCSAT(mrf, queries=self.queries, **self._params) -# return mcsat._run() - diff --git a/python3/pracmln/mln/inference/wcspinfer.py b/python3/pracmln/mln/inference/wcspinfer.py deleted file mode 100644 index 4f888bfd..00000000 --- a/python3/pracmln/mln/inference/wcspinfer.py +++ /dev/null @@ -1,365 +0,0 @@ -# Weighted Constraint Satisfaction Problems -- MPE inference on MLNs -# -# (C) 2012 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 collections import defaultdict - -from dnutils import logs - -from .infer import Inference -from ..constants import infty, HARD -from ..errors import SatisfiabilityException, MRFValueException -from ..grounding.fastconj import FastConjunctionGrounding -from ..mrfvars import FuzzyVariable -from ..util import (combinations, dict_union, Interval, temporary_evidence) -from ...wcsp import Constraint, WCSP -from ...logic.common import Logic - - -logger = logs.getlogger(__name__) - - -class WCSPInference(Inference): - - def __init__(self, mrf, queries, **params): - Inference.__init__(self, mrf, queries, **params) - - - def _run(self): - result_ = {} - with temporary_evidence(self.mrf): - self.converter = WCSPConverter(self.mrf, multicore=self.multicore, verbose=self.verbose) - result = self.result_dict(verbose=self.verbose) - for query in self.queries: - query = str(query) - result_[query] = result[query] if query in result else self.mrf[query] - return result_ - - - def result_dict(self, verbose=False): - """ - Returns a Database object with the most probable truth assignment. - """ - wcsp = self.converter.convert() - solution, _ = wcsp.solve() - if solution is None: - raise Exception('MLN is unsatisfiable.') - result = {} - for varidx, validx in enumerate(solution): - value = self.converter.domains[varidx][validx] - result.update(self.converter.variables[varidx].value2dict(value)) - return dict([(str(self.mrf.gndatom(idx)), val) for idx, val in result.items()]) - - - -class WCSPConverter(object): - """ - Class for converting an MLN into a WCSP problem for efficient - MPE inference. - """ - - - def __init__(self, mrf, verbose=False, multicore=False): - self.mrf = mrf - self.constraints = {} # mapping the signature of a constaint to its constraint object - self.verbose = verbose - self._createvars() - self.wcsp = WCSP() - self.wcsp.domsizes = [len(self.domains[i]) for i in self.variables] - self.multicore = multicore - - - def _createvars(self): - """ - Create the variables, one binary for each ground atom. - Considers also mutually exclusive blocks of ground atoms. - """ - self.variables = {} # maps an index to its MRF variable - 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 - for variable in self.mrf.variables: - if isinstance(variable, FuzzyVariable): # fuzzy variables are not subject to reasoning - continue - if variable.valuecount(self.mrf.evidence_dicti()) == 1: # the var is fully determined by the evidence - for _, value in variable.itervalues(self.mrf.evidence_dicti()): - break - self.mrf.set_evidence(variable.value2dict(value), erase=False) - continue - self.variables[varidx] = variable - for gndatom in variable.gndatoms: - self.atom2var[gndatom.idx] = varidx - for validx, (_, value) in enumerate(variable.itervalues(self.mrf.evidence_dicti())): - self.domains[varidx].append(value) - self.val2idx[varidx][value] = validx - varidx += 1 - - def convert(self): - """ - Performs a conversion from an MLN into a WCSP. - """ - # mln to be restored after inference - self._weights = list(self.mrf.mln.weights) - mln = self.mrf.mln - logic = mln.logic - # preprocess the formulas - formulas = [] - for f in self.mrf.formulas: - if f.weight == 0: - continue - if f.weight < 0: - f = logic.negate(f) - f.weight = -f.weight - formulas.append(f.nnf()) - # preprocess the ground formulas -# 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 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 - continue - self.generate_constraint(gf) - self.mrf.mln.weights = self._weights - return self.wcsp - - - def generate_constraint(self, wf): - """ - Generates and adds a constraint from a given weighted formula. - """ - varindices = tuple([self.atom2var[x] for x in wf.gndatom_indices()]) - seen = set() - varindices_ = [] - for v in varindices: - if v in seen: continue - varindices_.append(v) - seen.add(v) - varindices = tuple(varindices_) - # collect the constraint tuples - cost2assignments = self._gather_constraint_tuples(varindices, wf) - if cost2assignments is None: - return - defcost = max(cost2assignments, key=lambda x: infty if cost2assignments[x] == 'else' else len(cost2assignments[x])) - del cost2assignments[defcost] # remove the default cost values - - constraint = Constraint(varindices, defcost=defcost) - for cost, tuples in cost2assignments.items(): - for t in tuples: - constraint.tuple(t, cost) - self.wcsp.constraint(constraint) - - - def _gather_constraint_tuples(self, varindices, formula): - """ - Collects and evaluates all tuples that belong to the constraint - given by a formula. In case of disjunctions and conjunctions, - this is fairly efficient since not all combinations - need to be evaluated. Returns a dictionary mapping the constraint - costs to the list of respective variable assignments. - """ - logic = self.mrf.mln.logic - # we can treat conjunctions and disjunctions fairly efficiently - defaultProcedure = False - 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 - 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 - # 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. - (gndatom, val) = (gndlit.gndatom, not gndlit.negated) - if disj: val = not val - val = 1 if val else 0 - variable = self.variables[self.atom2var[gndatom.idx]] - # in case there are multiple values of a variable that may render the formula true - # we cannot apply this efficient implementation and have to fall back to the naive impl. - tmp_evidence = variable.value2dict(variable.evidence_value()) - evval = tmp_evidence.get(gndatom.idx) - if evval is not None and evval != val: - # the supposed value of the variable and the evidence value mismatch, - # so the conjunction (disjunction) can never be rendered true (false) - return - tmp_evidence = dict_union(tmp_evidence, {gndatom.idx: val}) - if variable.valuecount(tmp_evidence) > 1: - defaultProcedure = True - break - # there is only one value remaining - for _, value in variable.itervalues(tmp_evidence): - varidx = self.atom2var[gndatom.idx] - validx = self.val2idx[varidx][value] - # if there are two different values needed to render the formula true... - if assignment[varindices.index(varidx)] is not None and assignment[varindices.index(varidx)] != value: - if formula.weight == HARD: - if conj: # ...if it's a hard conjunction, the MLN is unsatisfiable -- e.g. foo(x) ^ !foo(x) - raise SatisfiabilityException('Knowledge base is unsatisfiable due to hard constraint violation: %s' % formula) - elif disj: # ...if it's a hard disjunction, it's a tautology -- e.g. foo(x) v !foo(x) - continue - else: # for soft constraints, unsatisfiable formulas and tautologies can be ignored - return None - assignment[varindices.index(varidx)] = validx - if not defaultProcedure: - maxtruth = formula.maxtruth(self.mrf.evidence) - mintruth = formula.mintruth(self.mrf.evidence) - if formula.weight == HARD and (maxtruth in Interval(']0,1[') or mintruth in Interval(']0,1[')): - raise MRFValueException('No fuzzy truth values are allowed in hard constraints.') - if conj: - if formula.weight == HARD: - cost = 0 - defcost = self.wcsp.top - else: - cost = formula.weight * (1 - maxtruth) - defcost = formula.weight - else: - if formula.weight == HARD: - cost = self.wcsp.top - defcost = 0 - else: - defcost = 0 - cost = formula.weight * (1 - mintruth) - if len(assignment) != len(varindices): - raise MRFValueException('Illegal variable assignments. Variables: %s, Assignment: %s' % (varindices, assignment)) - return {cost: [tuple(assignment)], defcost: 'else'} - if defaultProcedure: - # 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 - 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) - 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) - if truth is None: - print('POSSIBLE WORLD:') - print('===============') - self.mrf.print_world_vars(world) - print('GROUND FORMULA:') - print('===============') - formula.print_structure(world) - raise Exception('Something went wrong: Truth of ground formula cannot be evaluated (see above)') - - if truth in Interval(']0,1[') and formula.weight == HARD: - raise MRFValueException('No fuzzy truth values are allowed in hard constraints.') - if formula.weight == HARD: - if truth == 1: - cost = 0 - else: - cost = self.wcsp.top - else: - cost = ((1 - truth) * formula.weight) - cost2assignments[cost].append(tuple(assignment)) - return cost2assignments - assert False # unreachable - - - def forbid_gndatom(self, atom, truth=True): - """ - Adds a unary constraint that prohibits the given ground atom - being true. - """ - atomidx = atom if type(atom) is int else (self.mrf.gndatom(atom).idx if type(atom) is str else atom.idx) - varidx = self.atom2var[atomidx] - variable = self.variables[varidx] - evidence = list(self.mrf.evidence) - evidence[atomidx] = {True: 1, False: 0}[truth] - c = Constraint((varidx,)) - for _, value in variable.itervalues(evidence): - validx = self.val2idx[varidx][value] - c.tuple((validx,), self.wcsp.top) - self.wcsp.constraint(c) - - - def getPseudoDistributionForGndAtom(self, gndAtom): - """ - Computes a relative "distribution" for all possible variable assignments of - a mutex constraint. This can be used to determine the confidence in particular - most probable world by comparing the score with the second-most probable one. - """ - if isinstance(gndAtom, str): - gndAtom = self.mrf.gndAtoms[gndAtom] - - if not isinstance(gndAtom, Logic.GroundAtom): - raise Exception('Argument must be a ground atom') - - varIdx = self.gndAtom2VarIndex[gndAtom] - valIndices = list(range(len(self.varIdx2GndAtom[varIdx]))) - mutex = len(self.varIdx2GndAtom[varIdx]) > 1 - if not mutex: - raise Exception("Pseudo distribution is provided for mutex constraints only.") - wcsp = self.convert() - atoms = [] - cost = [] - try: - while len(valIndices) > 0: - s, c = wcsp.solve() - if s is None: raise - val = s[varIdx] - atom = self.varIdx2GndAtom[varIdx][val] - self.forbidGndAtom(atom, wcsp) - valIndices.remove(val) - cost.append(c) - atoms.append(atom) - except: pass - c_max = max(cost) - for i, c in enumerate(cost): - cost[i] = c_max - c - c_sum = sum(cost) - for i, c in enumerate(cost): - cost[i] = float(c) / c_sum - return dict([(a,c) for a, c in zip(atoms, cost)]) - - -# for debugging only -if __name__ == '__main__': - pass -# mln = MLN('/home/nyga/code/prac/models/experimental/deep_sense/priors.mln') -# db = Database(mln, '/home/nyga/code/prac/models/experimental/deep_sense/db/1.db') -# mrf = mln.groundMRF(db) -# -# conv = WCSPConverter(mrf) -# wcsp = conv.convert() -# wcsp.write(sys.stdout) -# solution = wcsp.solve() -# for i, s in enumerate(solution): -# print conv.varIdx2GndAtom[i][0], s, -# for ga in conv.varIdx2GndAtom[i]: print ga, -# print - \ No newline at end of file From 3434dd2ff713a04f374105ba115ed5769c8e07b2 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Wed, 6 Jun 2018 08:36:13 +0530 Subject: [PATCH 09/39] add unreviewed pending changes --- python3/pracmln/mln/inference/gibbs.pyx | 11 ++++-- python3/pracmln/mln/inference/infer.pyx | 3 +- python3/pracmln/mln/inference/maxwalk.pyx | 14 +++++-- python3/pracmln/mln/inference/mcmc.pyx | 24 +++++++----- python3/pracmln/mln/inference/mcsat.pyx | 43 ++++++++++++++------- python3/pracmln/mln/inference/wcspinfer.pyx | 20 +++++----- 6 files changed, 73 insertions(+), 42 deletions(-) diff --git a/python3/pracmln/mln/inference/gibbs.pyx b/python3/pracmln/mln/inference/gibbs.pyx index 66bc1754..4cbd12b3 100644 --- a/python3/pracmln/mln/inference/gibbs.pyx +++ b/python3/pracmln/mln/inference/gibbs.pyx @@ -33,6 +33,7 @@ from .mcmc import MCMCInference from ..constants import ALL from ..grounding.fastconj import FastConjunctionGrounding from ...logic.common import Logic +from numpy import zeros class GibbsSampler(MCMCInference): @@ -62,7 +63,8 @@ class GibbsSampler(MCMCInference): 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 +105,7 @@ class GibbsSampler(MCMCInference): # 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 +130,7 @@ class GibbsSampler(MCMCInference): # 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 +138,8 @@ class GibbsSampler(MCMCInference): # 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.pyx b/python3/pracmln/mln/inference/infer.pyx index 9e5ebf3e..dfb6894b 100644 --- a/python3/pracmln/mln/inference/infer.pyx +++ b/python3/pracmln/mln/inference/infer.pyx @@ -193,12 +193,13 @@ class Inference(object): 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/maxwalk.pyx b/python3/pracmln/mln/inference/maxwalk.pyx index 713a3273..5681147e 100644 --- a/python3/pracmln/mln/inference/maxwalk.pyx +++ b/python3/pracmln/mln/inference/maxwalk.pyx @@ -80,14 +80,20 @@ class SAMaxWalkSAT(MCMCInference): 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 +104,7 @@ class SAMaxWalkSAT(MCMCInference): 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.pyx b/python3/pracmln/mln/inference/mcmc.pyx index d02b973b..a808aec1 100644 --- a/python3/pracmln/mln/inference/mcmc.pyx +++ b/python3/pracmln/mln/inference/mcmc.pyx @@ -50,6 +50,7 @@ class MCMCInference(Inference): """ Get a random possible world, taking the evidence into account. """ + # "type" of evidence? if evidence is None: world = list(self.mrf.evidence) else: @@ -70,6 +71,10 @@ class MCMCInference(Inference): 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 @@ -115,16 +120,13 @@ class MCMCInference(Inference): 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: @@ -138,16 +140,17 @@ class MCMCInference(Inference): 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)): @@ -157,7 +160,8 @@ class MCMCInference(Inference): 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.pyx b/python3/pracmln/mln/inference/mcsat.pyx index 756614eb..dafe4303 100644 --- a/python3/pracmln/mln/inference/mcsat.pyx +++ b/python3/pracmln/mln/inference/mcsat.pyx @@ -82,7 +82,8 @@ class MCSAT(MCMCInference): 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 @@ -90,7 +91,8 @@ class MCSAT(MCMCInference): 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 +105,9 @@ class MCSAT(MCMCInference): # 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, {}) @@ -205,6 +209,7 @@ class MCSAT(MCMCInference): # 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 +269,7 @@ class MCSAT(MCMCInference): 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 +301,10 @@ class MCSAT(MCMCInference): 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 @@ -379,10 +388,12 @@ class SampleSAT: 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 +408,7 @@ class SampleSAT: 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 +454,25 @@ class SampleSAT: 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: @@ -601,7 +615,7 @@ class SampleSAT: 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 +628,8 @@ class SampleSAT: 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/wcspinfer.pyx b/python3/pracmln/mln/inference/wcspinfer.pyx index 1f81e98f..8e655bfa 100644 --- a/python3/pracmln/mln/inference/wcspinfer.pyx +++ b/python3/pracmln/mln/inference/wcspinfer.pyx @@ -96,7 +96,7 @@ class WCSPConverter(object): 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 @@ -180,15 +180,17 @@ class WCSPConverter(object): """ 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()) @@ -242,7 +244,7 @@ class WCSPConverter(object): 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 +252,23 @@ class WCSPConverter(object): 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 +284,7 @@ class WCSPConverter(object): 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)) From 4b62dbccdcf8270cd5214b673405c663d08b750f Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 11 Jun 2018 10:10:31 +0530 Subject: [PATCH 10/39] conversion to extension type ... exact -> infer -> mrf(?) -> ... --- python3/pracmln/mln/.gitignore | 6 +++++- python3/pracmln/mln/inference/exact.pyx | 4 ++-- python3/pracmln/mln/inference/infer.pxd | 4 ++++ python3/pracmln/mln/inference/infer.pyx | 4 ++-- python3/pracmln/mln/mrf.pxd | 2 ++ python3/pracmln/mln/{mrf.py => mrf.pyx} | 2 +- python3/pracmln/mln/setup.py | 14 ++++++++++++++ 7 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 python3/pracmln/mln/inference/infer.pxd create mode 100644 python3/pracmln/mln/mrf.pxd rename python3/pracmln/mln/{mrf.py => mrf.pyx} (99%) create mode 100644 python3/pracmln/mln/setup.py diff --git a/python3/pracmln/mln/.gitignore b/python3/pracmln/mln/.gitignore index 24fae75e..c246e93e 100644 --- a/python3/pracmln/mln/.gitignore +++ b/python3/pracmln/mln/.gitignore @@ -1 +1,5 @@ -*.pyo \ No newline at end of file +*.pyo +*.c +*.html +build/* +pracmln/* diff --git a/python3/pracmln/mln/inference/exact.pyx b/python3/pracmln/mln/inference/exact.pyx index 37d425d3..96a266e1 100644 --- a/python3/pracmln/mln/inference/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 @@ -75,7 +75,7 @@ def eval_queries(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) diff --git a/python3/pracmln/mln/inference/infer.pxd b/python3/pracmln/mln/inference/infer.pxd new file mode 100644 index 00000000..3c6a86a4 --- /dev/null +++ b/python3/pracmln/mln/inference/infer.pxd @@ -0,0 +1,4 @@ +from ..mrf cimport MRF + +cdef class Inference: + cdef MRF mrf diff --git a/python3/pracmln/mln/inference/infer.pyx b/python3/pracmln/mln/inference/infer.pyx index dfb6894b..eac0c60a 100644 --- a/python3/pracmln/mln/inference/infer.pyx +++ b/python3/pracmln/mln/inference/infer.pyx @@ -37,7 +37,7 @@ from functools import reduce 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 @@ -161,7 +161,7 @@ class Inference(object): 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: diff --git a/python3/pracmln/mln/mrf.pxd b/python3/pracmln/mln/mrf.pxd new file mode 100644 index 00000000..b89b9d08 --- /dev/null +++ b/python3/pracmln/mln/mrf.pxd @@ -0,0 +1,2 @@ +cdef class MRF: + pass diff --git a/python3/pracmln/mln/mrf.py b/python3/pracmln/mln/mrf.pyx similarity index 99% rename from python3/pracmln/mln/mrf.py rename to python3/pracmln/mln/mrf.pyx index c611445e..d3865c66 100644 --- a/python3/pracmln/mln/mrf.py +++ b/python3/pracmln/mln/mrf.pyx @@ -46,7 +46,7 @@ logger = logs.getlogger(__name__) -class MRF(object): +cdef class MRF(object): ''' Represents a ground Markov random field. diff --git a/python3/pracmln/mln/setup.py b/python3/pracmln/mln/setup.py new file mode 100644 index 00000000..126b3607 --- /dev/null +++ b/python3/pracmln/mln/setup.py @@ -0,0 +1,14 @@ +from distutils.core import setup +#from distutils.extension import Extension +from Cython.Build import cythonize + +#''' +#ext_modules=[ +# Extension("exact", ["exact.pyx"]), +# Extension("mcmc", ["mcmc.pyx"]), +#] +#''' + +setup( + ext_modules=cythonize( ['*.pyx'] ) +) From f1734b4fda776ca40c8f354ed133db558018e239 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sat, 16 Jun 2018 12:06:28 +0530 Subject: [PATCH 11/39] convert mln to extension type --- python3/pracmln/mln/base.pxd | 4 + python3/pracmln/mln/base.pyx | 785 +++++++++++++++++++++++++++++++++++ python3/pracmln/mln/mrf.pxd | 4 +- 3 files changed, 792 insertions(+), 1 deletion(-) create mode 100644 python3/pracmln/mln/base.pxd create mode 100644 python3/pracmln/mln/base.pyx diff --git a/python3/pracmln/mln/base.pxd b/python3/pracmln/mln/base.pxd new file mode 100644 index 00000000..d207d64b --- /dev/null +++ b/python3/pracmln/mln/base.pxd @@ -0,0 +1,4 @@ +from ..logic.common cimport Logic + +cdef class MLN: + cdef Logic logic diff --git a/python3/pracmln/mln/base.pyx b/python3/pracmln/mln/base.pyx new file mode 100644 index 00000000..b07188e8 --- /dev/null +++ b/python3/pracmln/mln/base.pyx @@ -0,0 +1,785 @@ +# -*- coding: utf-8 -*- +# +# Markov Logic Networks +# +# (C) 2012-2013 by Daniel Nyga (nyga@cs.uni-bremen.de) +# (C) 2006-2011 by Dominik Jain (jain@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. +import pyparsing +from dnutils import logs, ifnone, out + +from ..logic import FirstOrderLogic, FuzzyLogic + +import platform +from .mrf import MRF +from .errors import MLNParsingError +from pyparsing import ParseException +from .constants import HARD, comment_color, predicate_color, weight_color +import copy +import os +from .util import StopWatch, mergedom, fstr, colorize, stripComments +from .mlnpreds import (Predicate, FuzzyPredicate, SoftFunctionalPredicate, + FunctionalPredicate) +from .database import Database +from .learning.multidb import MultipleDatabaseLearner +import sys +import re +import traceback +from .learning.bpll import BPLL +from ..utils.project import mlnpath +from importlib import util as imputil + +logger = logs.getlogger(__name__) + + +if platform.architecture()[0] == '32bit': + if imputil.find_spec('psyco') is not None: + import psyco # Don't use Psyco when debugging! @UnresolvedImport + psyco.full() + else: + logger.warning("Note: Psyco (http://psyco.sourceforge.net) was not loaded. On 32bit systems, it is recommended to install it for improved performance.") + + +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 + `PRACGrammar` and `StandardGrammar`. + :param mlnfile: can be a path to an MLN file or a file object. + ''' + + + def __init__(self, logic='FirstOrderLogic', grammar='PRACGrammar', mlnfile=None): + # instantiate the logic and grammar + logic_str = '%s("%s", self)' % (logic, grammar) + self.logic = eval(logic_str) + logger.debug('Creating MLN with %s syntax and %s semantics' % (grammar, logic)) + self._predicates = {} # maps from predicate name to the predicate instance + self.domains = {} # maps from domain names to list of values + self._formulas = [] # list of MLNFormula instances + self.domain_decls = [] + self.weights = [] + self.fixweights = [] + self.vars = {} + self._unique_templvars = [] + self._probreqs = [] + self._materialized = False + self.fuzzypreds = [] # for saving fuzzy predicates that have been converted to binary preds + if mlnfile is not None: + MLN.load(mlnfile, logic=logic, grammar=grammar, mln=self) + return + self.closedWorldPreds = [] + self.formulaGroups = [] + self.templateIdx2GroupIdx = {} + self.posteriorProbReqs = [] + self.watch = StopWatch() + + @property + def predicates(self): + return list(self.iterpreds()) + + @property + def formulas(self): + return list(self._formulas) + + @property + def weights(self): + return self._weights + + @weights.setter + def weights(self, wts): + if len(wts) != len(self._formulas): + raise Exception('Weight vector must have the same length as formula vector.') + wts = map(lambda w: float('%-10.6f' % float(eval(str(w)))) if type(w) in (float, int) and w not in ( + HARD, float('inf'), -float('inf')) else w, wts) + self._weights = wts + + @property + def fixweights(self): + return self._fixweights + + @fixweights.setter + def fixweights(self, fw): + self._fixweights = 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] + + @property + def prednames(self): + return [p.name for p in self.predicates] + + def prior(self, f, p): + self._probreqs.append(FirstOrderLogic.PriorConstraint(formula=f, p=p)) + + def posterior(self, f, p): + self._probreqs.append(FirstOrderLogic.PosteriorConstraint(formula=f, p=p)) + + def copy(self): + ''' + Returns a deep copy of this MLN, which is not yet materialized. + ''' + mln_ = MLN(logic=self.logic.__class__.__name__, grammar=self.logic.grammar.__class__.__name__) + for pred in self.iterpreds(): + mln_.predicate(copy.copy(pred)) + mln_.domain_decls = list(self.domain_decls) + for i, f in self.iterformulas(): + mln_.formula(f.copy(mln=mln_), weight=self.weight(i), fixweight=self.fixweights[i], unique_templvars=self._unique_templvars[i]) + mln_.domains = dict(self.domains) + mln_.vars = dict(self.vars) + mln_._probreqs = list(self.probreqs) + mln_.fuzzypreds = list(self.fuzzypreds) + return mln_ + + 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 + 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 + 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) + elif isinstance(predicate, str): + return self._predicates.get(predicate, None) + elif isinstance(predicate, pyparsing.ParseResults): + 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 + specifying a predicate declaration. + ''' + pred = self._predicates.get(predicate.name) + if pred is not None and pred != predicate: + raise Exception('Contradictory predicate definitions: %s <--> %s' % (pred, predicate)) + else: + self._predicates[predicate.name] = predicate + for dom in predicate.argdoms: + if dom not in self.domains: + self.domains[dom] = [] + return self + + def formula(self, formula, weight=0., fixweight=False, unique_templvars=None): + ''' + Adds a formula to this MLN. The respective domains of constants + 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 + or `mln.infty` to indicate a hard formula. + :param fixweight: indicates whether or not the weight of this + formula should be fixed during learning. + :param unique_templvars: specifies a list of template variables that will create + only unique combinations of expanded formulas + ''' + if type(formula) is str: + formula = self.logic.parse_formula(formula) + elif type(formula) is int: + return self._formulas[formula] + cdef dict constants = {} + formula.vardoms(None, constants) + for domain, constants in constants.items(): + for c in constants: self.constant(domain, c) + formula.mln = self + formula.idx = len(self._formulas) + self._formulas.append(formula) + self.weights.append(weight) + self.fixweights.append(fixweight) + self._unique_templvars.append(list(unique_templvars) if unique_templvars is not None else []) + return self._formulas[-1] + + def _rmformulas(self): + self._formulas = [] + 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): + ''' + Returns or sets the weight of the formula with index `idx`. + ''' + if weight is not None: + self.weights[idx] = weight + else: + return self.weights[idx] + + def __lshift__(self, _input): + parse_mln(_input, '.', logic=None, grammar=None, mln=self) + + 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. + 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. + + :param dbs: list of :class:`database.Database` objects for materialization. + ''' + logger.debug("materializing formula templates...") + + # obtain full domain with all objects + fulldomain = mergedom(self.domains, *[db.domains for db in dbs]) + logger.debug('full domains: %s' % fulldomain) + mln_ = self.copy() + # collect the admissible formula templates. templates might be not + # admissible since the domain of a template variable might be empty. + for ft in list(mln_.formulas): + domnames = list(ft.vardoms().values()) + if any([domname not in fulldomain for domname in domnames]): + logger.debug('Discarding formula template %s, since it cannot be grounded (domain(s) %s empty).' % \ + (fstr(ft), ','.join([d for d in domnames if d not in fulldomain]))) + mln_.rmf(ft) + # collect the admissible predicates. a predicate may become inadmissible + # if either the domain of one of its arguments is empty or there is + # no formula containing the respective predicate. + predicates_used = set() + for _, f in mln_.iterformulas(): + predicates_used.update(f.prednames()) + for predicate in self.iterpreds(): + remove = False + if any([not dom in fulldomain for dom in predicate.argdoms]): + logger.debug('Discarding predicate %s, since it cannot be grounded.' % (predicate.name)) + remove = True + if predicate.name not in predicates_used: + logger.debug('Discarding predicate %s, since it is unused.' % predicate.name) + remove = True + if remove: del mln_._predicates[predicate.name] + # permanently transfer domains of variables that were expanded from templates + for _, ft in mln_.iterformulas(): + domnames = list(ft.template_variables().values()) + for domname in domnames: + mln_.domains[domname] = fulldomain[domname] + # materialize the formula templates + mln__ = mln_.copy() + mln__ ._rmformulas() + for i, template in mln_.iterformulas(): + for variant in template.template_variants(): + idx = len(mln__._formulas) + f = mln__.formula(variant, weight=template.weight, fixweight=mln_.fixweights[i]) + f.idx = idx + mln__._materialized = True + return mln__ + + 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. + ''' + if domain not in self.domains: self.domains[domain] = [] + dom = self.domains[domain] + for value in values: + if value not in dom: dom.append(value) + return self + + 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. + ''' + logger.debug('creating ground MRF...') + mrf = MRF(self, db) + for pred in self.predicates: + for gndatom in pred.groundatoms(self, mrf.domains): + mrf.gndatom(gndatom.predname, *gndatom.args) + evidence = dict([(atom, value) for atom, value in db.evidence.items() if mrf.gndatom(atom) is not None]) + mrf.set_evidence(evidence, erase=False) + return mrf + + 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 + for value in domain[domname]: + self.constant(domname, value) + + 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.') + dbs = [] + for db in databases: + if isinstance(db, str): + db = Database.load(self, db) + if type(db) is list: dbs.extend(db) + else: dbs.append(db) + elif type(db) is list: dbs.extend(db) + else: dbs.append(db) + logger.debug('loaded %s evidence databases for learning' % len(dbs)) + newmln = self.materialize(*dbs) + + logger.debug('MLN predicates:') + for p in newmln.predicates: logger.debug(p) + logger.debug('MLN domains:') + for d in newmln.domains.items(): logger.debug(d) + if not newmln.formulas: + raise Exception('No formulas in the materialized MLN.') + logger.debug('MLN formulas:') + for f in newmln.formulas: logger.debug('%s %s' % (str(f.weight).ljust(10, ' '), f)) + # run learner + if len(dbs) == 1: + mrf = newmln.ground(dbs[0]) + logger.debug('Loading %s-Learner' % method.__name__) + learner = method(mrf, **params) + else: + learner = MultipleDatabaseLearner(newmln, dbs, method, **params) + if verbose: + "learner: %s" % learner.name + wt = learner.run(**params) + newmln.weights = wt + # fit prior prob. constraints if any available + if len(self.probreqs) > 0: + fittingParams = { + "fittingMethod": self.probabilityFittingInferenceMethod, + "fittingSteps": self.probabilityFittingMaxSteps, + "fittingThreshold": self.probabilityFittingThreshold + } + 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) + fix = list(newmln.fixweights) + newmln._rmformulas() + for f, w,fi in zip(formulas, weights, fix): + if w != 0: newmln.formula(f, w, fi) + return newmln + + 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) + 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. + + :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: + 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: + stream.write("%s\n" % d) + stream.write('\n') + # variable definitions + if self.vars: stream.write(colorize('// variable definitions\n', comment_color, color)) + for var, val in self.vars.items(): + stream.write('%s = %s' % (var, val)) + stream.write('\n') + stream.write(colorize("\n// predicate declarations\n", comment_color, color)) + for predicate in self.iterpreds(): + if isinstance(predicate, FuzzyPredicate) or predicate.name in self.fuzzypreds: + stream.write('#fuzzy\n') + stream.write("%s(%s)\n" % (colorize(predicate.name, predicate_color, color), predicate.argstr())) + stream.write(colorize("\n// formulas\n", comment_color, color)) + for idx, formula in self.iterformulas(): + if self._unique_templvars[idx]: + stream.write('#unique{%s}\n' % ','.join(self._unique_templvars[idx])) + if formula.weight == HARD: + stream.write("%s.\n" % fstr(formula.cstr(color))) + else: + try: + w = colorize("%-10.6f", weight_color, color) % float(eval(str(formula.weight))) + except: + w = colorize(str(formula.weight), weight_color, color) + stream.write("%s %s\n" % (w, fstr(formula.cstr(color)))) + + def print_formulas(self): + ''' + Nicely prints the formulas and their weights. + ''' + for f in self.iterFormulasPrintable(): + print(f) + + def iter_formulas_printable(self): + ''' + Iterate over all formulas, yield nicely formatted strings. + ''' + formulas = sorted(self.formulas) + for f in formulas: + if f.weight == HARD: + yield '%s.' % fstr(f) + elif type(f.weight) is float: + 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`. + :param grammar: (string) the syntax to be used for parsing the MLN file. Either `PRACGrammar` or `StandardGrammar`. + ''' + # read MLN file + text = '' + if files is not None: + if not type(files) is list: + files = [files] + projectpath = None + for f in files: + if isinstance(f, str): + p = mlnpath(f) + if p.project is not None: + projectpath = p.projectloc + text += p.content + elif isinstance(f, mlnpath): + text += f.content + else: raise Exception('Unexpected file specification: %s' % str(f)) + dirs = [os.path.dirname(fn) for fn in files] + return parse_mln(text, searchpaths=dirs, projectpath=projectpath, logic=logic, grammar=grammar, mln=mln) + raise Exception('No mln files given.') + + +def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic', grammar='PRACGrammar', mln=None): + ''' + Reads an MLN from a stream providing a 'read' method. + ''' + dirs = [os.path.abspath(os.path.expandvars(os.path.expanduser(p))) for p in searchpaths] + formulatemplates = [] + text = str(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) + text = re.compile(r'//\s*\s*$', re.MULTILINE).sub("#group.", text) + # remove comments + text = stripComments(text) + if mln is None: + mln = MLN(logic, grammar) + # read lines + mln.hard_formulas = [] + templateIdx2GroupIdx = {} + inGroup = False + idxGroup = -1 + fixWeightOfNextFormula = False + fuzzy = False + pseudofuzzy = False + uniquevars = None + fixedWeightTemplateIndices = [] + lines = text.split("\n") + iLine = 0 + while iLine < len(lines): + line = lines[iLine] + iLine += 1 + line = line.strip() + try: + if len(line) == 0: continue + # meta directives + if line == "#group": + idxGroup += 1 + inGroup = True + continue + elif line == "#group.": + inGroup = False + continue + elif line.startswith("#fixweight"): + fixWeightOfNextFormula = True + continue + elif line.startswith('#fuzzy'): + if not isinstance(mln.logic, FuzzyLogic): + logger.warning('Fuzzy declarations are not supported in %s. Treated as a binary predicate.' % mln.logic.__class__.__name__) + pseudofuzzy = True + else: + fuzzy = True + continue + elif line.startswith("#include"): + filename = line[len("#include "):].strip() + 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 + # relatively to all paths specified. Take the first file matching. + if not mlnpath(filename).exists: + includefilename = None + for d in dirs: + mlnp = '/'.join([d, filename]) + if mlnpath(mlnp).exists: + includefilename = mlnp + break + if includefilename is None: + raise Exception('File not found: %s' % filename) + else: + includefilename = filename + else: + m = re.match(r'<(?P.+)>', filename) + if m is not None: + filename = m.group('filename') + else: + raise MLNParsingError('Malformed #include statement: %s' % line) + if projectpath is None: + raise MLNParsingError('No project specified: Cannot locate import from project: %s' % filename) + includefilename = ':'.join([projectpath, filename]) + logger.debug('Including file: "%s"' % includefilename) + p = mlnpath(includefilename) + 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 + elif line.startswith('#unique'): + try: + uniVars = re.search('#unique{(.+)}', line) + uniVars = uniVars.groups()[0] + uniVars = list(map(str.strip, uniVars.split(','))) + uniquevars = uniVars + except: + raise MLNParsingError('Malformed #unique expression: "%s"' % line) + continue + elif line.startswith("#AdaptiveMLNDependency"): # declared as "#AdaptiveMLNDependency:pred:domain"; seems to be deprecated + depPredicate, domain = line.split(":")[1:3] + if hasattr(mln, 'AdaptiveDependencyMap'): + if depPredicate in mln.AdaptiveDependencyMap: + mln.AdaptiveDependencyMap[depPredicate].add(domain) + else: + mln.AdaptiveDependencyMap[depPredicate] = set([domain]) + else: + mln.AdaptiveDependencyMap = {depPredicate:set([domain])} + continue + # domain decl + if '=' in line: + # try normal domain definition + parse = mln.logic.parse_domain(line) + if parse is not None: + domName, constants = parse + domName = str(domName) + constants = list(map(str, constants)) + 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] = [] + mln.constant(domName, *constants) + mln.domain_decls.append(line) + continue + # prior probability requirement + if line.startswith("P("): + m = re.match(r"P\((.*?)\)\s*=\s*([\.\de]+)", line) + if m is None: + raise MLNParsingError("Prior probability constraint formatted incorrectly: %s" % line) + mln.prior(f=mln.logic.parse_formula(m.group(1)), p=float(m.group(2))) + continue + # posterior probability requirement/soft evidence + if line.startswith("R(") or line.startswith("SE("): + m = re.match(r"(?:R|SE)\((.*?)\)\s*=\s*([\.\de]+)", line) + if m is None: + raise MLNParsingError("Posterior probability constraint formatted incorrectly: %s" % line) + mln.posterior(f=mln.logic.parse_formula(m.group(1)), p=float(m.group(2))) + continue + # variable definition + if re.match(r'(\$\w+)\s*=(.+)', line): + m = re.match(r'(\$\w+)\s*=(.+)', line) + if m is None: + raise MLNParsingError("Variable assigment malformed: %s" % line) + mln.vars[m.group(1)] = "%s" % m.group(2).strip() + continue + # predicate decl or formula with weight + else: + isHard = False + isPredDecl = False + if line[ -1] == '.': # hard (without explicit weight -> determine later) + isHard = True + formula = line[:-1] + else: # with weight + # try predicate declaration + isPredDecl = True + try: + pred = mln.logic.parse_predicate(line) + except Exception as e: + isPredDecl = False + if isPredDecl: + predname = str(pred[0]) + argdoms = list(map(str, pred[1])) + softmutex = False + mutex = None + 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') + if fuzzy: raise Exception('(Soft-)functional predicates must not be fuzzy.') + mutex = i + if dom[-1] == '?': softmutex = True + argdoms = [x.strip('!?') for x in argdoms] + pred = None + if mutex is not None: + if softmutex: + pred = SoftFunctionalPredicate(predname, argdoms, mutex) + else: + pred = FunctionalPredicate(predname, argdoms, mutex) + elif fuzzy: + pred = FuzzyPredicate(predname, argdoms) + fuzzy = False + else: + pred = Predicate(predname, argdoms) + if pseudofuzzy: + mln.fuzzypreds.append(predname) + pseudofuzzy = False + mln.predicate(pred) + continue + else: + # formula (template) with weight or terminated by '.' + if not isHard: + spacepos = line.find(' ') + weight = line[:spacepos] + formula = line[spacepos:].strip() + try: + formula = mln.logic.parse_formula(formula) + if isHard: + weight = HARD # not set until instantiation when other weights are known + idxTemplate = len(formulatemplates) + formulatemplates.append(formula) + fixweight = False + if inGroup: + templateIdx2GroupIdx[idxTemplate] = idxGroup + if fixWeightOfNextFormula == True: + fixWeightOfNextFormula = False + fixweight = True + fixedWeightTemplateIndices.append(idxTemplate) + + # expand predicate groups + for variant in formula.expandgrouplits(): + mln.formula(variant, weight=weight, + fixweight=fixweight, + unique_templvars=uniquevars) + + if uniquevars: + uniquevars = None + except ParseException as e: + raise MLNParsingError("Error parsing formula '%s'\n" % formula) + if fuzzy and not isPredDecl: raise Exception('"#fuzzy" decorator not allowed at this place: %s' % line) + except MLNParsingError as err: + sys.stderr.write("Error processing line '%s'\n" % line) + cls, e, tb = sys.exc_info() + traceback.print_tb(tb) + raise MLNParsingError(err) + + # augment domains with constants appearing in formula templates + cdef dict c_constants = {} + for _, f in mln.iterformulas(): + 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/mrf.pxd b/python3/pracmln/mln/mrf.pxd index b89b9d08..9915e666 100644 --- a/python3/pracmln/mln/mrf.pxd +++ b/python3/pracmln/mln/mrf.pxd @@ -1,2 +1,4 @@ +from base cimport MLN + cdef class MRF: - pass + cdef MLN mln From e88927eb6aadc6b12d17ffd98a298b2fd95f90b2 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sat, 16 Jun 2018 12:07:01 +0530 Subject: [PATCH 12/39] convert logic to extension type --- python3/build-cython.sh | 16 + python3/pracmln/logic/.gitignore | 5 + python3/pracmln/logic/common.pxd | 4 + .../pracmln/logic/{common.py => common.pyx} | 4 +- python3/pracmln/logic/setup.py | 6 + python3/pracmln/mln/base.py | 784 ------------------ 6 files changed, 33 insertions(+), 786 deletions(-) create mode 100644 python3/build-cython.sh create mode 100644 python3/pracmln/logic/.gitignore create mode 100644 python3/pracmln/logic/common.pxd rename python3/pracmln/logic/{common.py => common.pyx} (99%) create mode 100644 python3/pracmln/logic/setup.py delete mode 100644 python3/pracmln/mln/base.py diff --git a/python3/build-cython.sh b/python3/build-cython.sh new file mode 100644 index 00000000..2585c5cf --- /dev/null +++ b/python3/build-cython.sh @@ -0,0 +1,16 @@ + +cd pracmln/logic/ +echo "======================LOGIC======================" +python3 setup.py build_ext --inplace +echo "=================================================" + +echo "=======================MLN=======================" +cd ../mln/ +python3 setup.py build_ext --inplace +echo "=================================================" + +echo "====================INFERENCE====================" +cd inference/ +python3 setup.py build_ext --inplace +echo "=================================================" + diff --git a/python3/pracmln/logic/.gitignore b/python3/pracmln/logic/.gitignore new file mode 100644 index 00000000..c246e93e --- /dev/null +++ b/python3/pracmln/logic/.gitignore @@ -0,0 +1,5 @@ +*.pyo +*.c +*.html +build/* +pracmln/* diff --git a/python3/pracmln/logic/common.pxd b/python3/pracmln/logic/common.pxd new file mode 100644 index 00000000..922dfa20 --- /dev/null +++ b/python3/pracmln/logic/common.pxd @@ -0,0 +1,4 @@ +cdef class Logic: + pass + #cdef class Constraint(): + # pass diff --git a/python3/pracmln/logic/common.py b/python3/pracmln/logic/common.pyx similarity index 99% rename from python3/pracmln/logic/common.py rename to python3/pracmln/logic/common.pyx index 1b5ff7af..68f53ec0 100644 --- a/python3/pracmln/logic/common.py +++ b/python3/pracmln/logic/common.pyx @@ -46,7 +46,7 @@ def latexsym(sym): # return ' ' return r'\textit{%s}' % str(sym) -class Logic(object): +class Logic(): """ Abstract factory class for instantiating logical constructs like conjunctions, disjunctions etc. Every specifc logic should implement the methods and return @@ -78,7 +78,7 @@ def __setstate__(self, d): # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Constraint(object): + class Constraint(): """ Super class of every constraint. """ diff --git a/python3/pracmln/logic/setup.py b/python3/pracmln/logic/setup.py new file mode 100644 index 00000000..8055b07f --- /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'] ) +) diff --git a/python3/pracmln/mln/base.py b/python3/pracmln/mln/base.py deleted file mode 100644 index 9da4b024..00000000 --- a/python3/pracmln/mln/base.py +++ /dev/null @@ -1,784 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Markov Logic Networks -# -# (C) 2012-2013 by Daniel Nyga (nyga@cs.uni-bremen.de) -# (C) 2006-2011 by Dominik Jain (jain@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. -import pyparsing -from dnutils import logs, ifnone, out - -from ..logic import FirstOrderLogic, FuzzyLogic - -import platform -from .mrf import MRF -from .errors import MLNParsingError -from pyparsing import ParseException -from .constants import HARD, comment_color, predicate_color, weight_color -import copy -import os -from .util import StopWatch, mergedom, fstr, colorize, stripComments -from .mlnpreds import (Predicate, FuzzyPredicate, SoftFunctionalPredicate, - FunctionalPredicate) -from .database import Database -from .learning.multidb import MultipleDatabaseLearner -import sys -import re -import traceback -from .learning.bpll import BPLL -from ..utils.project import mlnpath -from importlib import util as imputil - -logger = logs.getlogger(__name__) - - -if platform.architecture()[0] == '32bit': - if imputil.find_spec('psyco') is not None: - import psyco # Don't use Psyco when debugging! @UnresolvedImport - psyco.full() - else: - 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): - ''' - 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 - `PRACGrammar` and `StandardGrammar`. - :param mlnfile: can be a path to an MLN file or a file object. - ''' - - - def __init__(self, logic='FirstOrderLogic', grammar='PRACGrammar', mlnfile=None): - # instantiate the logic and grammar - logic_str = '%s("%s", self)' % (logic, grammar) - self.logic = eval(logic_str) - logger.debug('Creating MLN with %s syntax and %s semantics' % (grammar, logic)) - self._predicates = {} # maps from predicate name to the predicate instance - self.domains = {} # maps from domain names to list of values - self._formulas = [] # list of MLNFormula instances - self.domain_decls = [] - self.weights = [] - self.fixweights = [] - self.vars = {} - self._unique_templvars = [] - self._probreqs = [] - self._materialized = False - self.fuzzypreds = [] # for saving fuzzy predicates that have been converted to binary preds - if mlnfile is not None: - MLN.load(mlnfile, logic=logic, grammar=grammar, mln=self) - return - self.closedWorldPreds = [] - self.formulaGroups = [] - self.templateIdx2GroupIdx = {} - self.posteriorProbReqs = [] - self.watch = StopWatch() - - @property - def predicates(self): - return list(self.iterpreds()) - - @property - def formulas(self): - return list(self._formulas) - - @property - def weights(self): - return self._weights - - @weights.setter - def weights(self, wts): - if len(wts) != len(self._formulas): - raise Exception('Weight vector must have the same length as formula vector.') - wts = map(lambda w: float('%-10.6f' % float(eval(str(w)))) if type(w) in (float, int) and w not in ( - HARD, float('inf'), -float('inf')) else w, wts) - self._weights = wts - - @property - def fixweights(self): - return self._fixweights - - @fixweights.setter - def fixweights(self, fw): - self._fixweights = 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] - - @property - def prednames(self): - return [p.name for p in self.predicates] - - def prior(self, f, p): - self._probreqs.append(FirstOrderLogic.PriorConstraint(formula=f, p=p)) - - def posterior(self, f, p): - self._probreqs.append(FirstOrderLogic.PosteriorConstraint(formula=f, p=p)) - - def copy(self): - ''' - Returns a deep copy of this MLN, which is not yet materialized. - ''' - mln_ = MLN(logic=self.logic.__class__.__name__, grammar=self.logic.grammar.__class__.__name__) - for pred in self.iterpreds(): - mln_.predicate(copy.copy(pred)) - mln_.domain_decls = list(self.domain_decls) - for i, f in self.iterformulas(): - mln_.formula(f.copy(mln=mln_), weight=self.weight(i), fixweight=self.fixweights[i], unique_templvars=self._unique_templvars[i]) - mln_.domains = dict(self.domains) - mln_.vars = dict(self.vars) - mln_._probreqs = list(self.probreqs) - mln_.fuzzypreds = list(self.fuzzypreds) - return mln_ - - 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 - 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 - 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) - elif isinstance(predicate, str): - return self._predicates.get(predicate, None) - elif isinstance(predicate, pyparsing.ParseResults): - 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 - specifying a predicate declaration. - ''' - pred = self._predicates.get(predicate.name) - if pred is not None and pred != predicate: - raise Exception('Contradictory predicate definitions: %s <--> %s' % (pred, predicate)) - else: - self._predicates[predicate.name] = predicate - for dom in predicate.argdoms: - if dom not in self.domains: - self.domains[dom] = [] - return self - - def formula(self, formula, weight=0., fixweight=False, unique_templvars=None): - ''' - Adds a formula to this MLN. The respective domains of constants - 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 - or `mln.infty` to indicate a hard formula. - :param fixweight: indicates whether or not the weight of this - formula should be fixed during learning. - :param unique_templvars: specifies a list of template variables that will create - only unique combinations of expanded formulas - ''' - if type(formula) is str: - formula = self.logic.parse_formula(formula) - elif type(formula) is int: - return self._formulas[formula] - constants = {} - formula.vardoms(None, constants) - for domain, constants in constants.items(): - for c in constants: self.constant(domain, c) - formula.mln = self - formula.idx = len(self._formulas) - self._formulas.append(formula) - self.weights.append(weight) - self.fixweights.append(fixweight) - self._unique_templvars.append(list(unique_templvars) if unique_templvars is not None else []) - return self._formulas[-1] - - def _rmformulas(self): - self._formulas = [] - 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): - ''' - Returns or sets the weight of the formula with index `idx`. - ''' - if weight is not None: - self.weights[idx] = weight - else: - return self.weights[idx] - - def __lshift__(self, _input): - parse_mln(_input, '.', logic=None, grammar=None, mln=self) - - 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. - 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. - - :param dbs: list of :class:`database.Database` objects for materialization. - ''' - logger.debug("materializing formula templates...") - - # obtain full domain with all objects - fulldomain = mergedom(self.domains, *[db.domains for db in dbs]) - logger.debug('full domains: %s' % fulldomain) - mln_ = self.copy() - # collect the admissible formula templates. templates might be not - # admissible since the domain of a template variable might be empty. - for ft in list(mln_.formulas): - domnames = list(ft.vardoms().values()) - if any([domname not in fulldomain for domname in domnames]): - logger.debug('Discarding formula template %s, since it cannot be grounded (domain(s) %s empty).' % \ - (fstr(ft), ','.join([d for d in domnames if d not in fulldomain]))) - mln_.rmf(ft) - # collect the admissible predicates. a predicate may become inadmissible - # if either the domain of one of its arguments is empty or there is - # no formula containing the respective predicate. - predicates_used = set() - for _, f in mln_.iterformulas(): - predicates_used.update(f.prednames()) - for predicate in self.iterpreds(): - remove = False - if any([not dom in fulldomain for dom in predicate.argdoms]): - logger.debug('Discarding predicate %s, since it cannot be grounded.' % (predicate.name)) - remove = True - if predicate.name not in predicates_used: - logger.debug('Discarding predicate %s, since it is unused.' % predicate.name) - remove = True - if remove: del mln_._predicates[predicate.name] - # permanently transfer domains of variables that were expanded from templates - for _, ft in mln_.iterformulas(): - domnames = list(ft.template_variables().values()) - for domname in domnames: - mln_.domains[domname] = fulldomain[domname] - # materialize the formula templates - mln__ = mln_.copy() - mln__ ._rmformulas() - for i, template in mln_.iterformulas(): - for variant in template.template_variants(): - idx = len(mln__._formulas) - f = mln__.formula(variant, weight=template.weight, fixweight=mln_.fixweights[i]) - f.idx = idx - mln__._materialized = True - return mln__ - - 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. - ''' - if domain not in self.domains: self.domains[domain] = [] - dom = self.domains[domain] - for value in values: - if value not in dom: dom.append(value) - return self - - 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. - ''' - logger.debug('creating ground MRF...') - mrf = MRF(self, db) - for pred in self.predicates: - for gndatom in pred.groundatoms(self, mrf.domains): - mrf.gndatom(gndatom.predname, *gndatom.args) - evidence = dict([(atom, value) for atom, value in db.evidence.items() if mrf.gndatom(atom) is not None]) - mrf.set_evidence(evidence, erase=False) - return mrf - - 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 - for value in domain[domname]: - self.constant(domname, value) - - 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.') - dbs = [] - for db in databases: - if isinstance(db, str): - db = Database.load(self, db) - if type(db) is list: dbs.extend(db) - else: dbs.append(db) - elif type(db) is list: dbs.extend(db) - else: dbs.append(db) - logger.debug('loaded %s evidence databases for learning' % len(dbs)) - newmln = self.materialize(*dbs) - - logger.debug('MLN predicates:') - for p in newmln.predicates: logger.debug(p) - logger.debug('MLN domains:') - for d in newmln.domains.items(): logger.debug(d) - if not newmln.formulas: - raise Exception('No formulas in the materialized MLN.') - logger.debug('MLN formulas:') - for f in newmln.formulas: logger.debug('%s %s' % (str(f.weight).ljust(10, ' '), f)) - # run learner - if len(dbs) == 1: - mrf = newmln.ground(dbs[0]) - logger.debug('Loading %s-Learner' % method.__name__) - learner = method(mrf, **params) - else: - learner = MultipleDatabaseLearner(newmln, dbs, method, **params) - if verbose: - "learner: %s" % learner.name - wt = learner.run(**params) - newmln.weights = wt - # fit prior prob. constraints if any available - if len(self.probreqs) > 0: - fittingParams = { - "fittingMethod": self.probabilityFittingInferenceMethod, - "fittingSteps": self.probabilityFittingMaxSteps, - "fittingThreshold": self.probabilityFittingThreshold - } - 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) - fix = list(newmln.fixweights) - newmln._rmformulas() - for f, w,fi in zip(formulas, weights, fix): - if w != 0: newmln.formula(f, w, fi) - return newmln - - 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) - 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. - - :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: - 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: - stream.write("%s\n" % d) - stream.write('\n') - # variable definitions - if self.vars: stream.write(colorize('// variable definitions\n', comment_color, color)) - for var, val in self.vars.items(): - stream.write('%s = %s' % (var, val)) - stream.write('\n') - stream.write(colorize("\n// predicate declarations\n", comment_color, color)) - for predicate in self.iterpreds(): - if isinstance(predicate, FuzzyPredicate) or predicate.name in self.fuzzypreds: - stream.write('#fuzzy\n') - stream.write("%s(%s)\n" % (colorize(predicate.name, predicate_color, color), predicate.argstr())) - stream.write(colorize("\n// formulas\n", comment_color, color)) - for idx, formula in self.iterformulas(): - if self._unique_templvars[idx]: - stream.write('#unique{%s}\n' % ','.join(self._unique_templvars[idx])) - if formula.weight == HARD: - stream.write("%s.\n" % fstr(formula.cstr(color))) - else: - try: - w = colorize("%-10.6f", weight_color, color) % float(eval(str(formula.weight))) - except: - w = colorize(str(formula.weight), weight_color, color) - stream.write("%s %s\n" % (w, fstr(formula.cstr(color)))) - - def print_formulas(self): - ''' - Nicely prints the formulas and their weights. - ''' - for f in self.iterFormulasPrintable(): - print(f) - - def iter_formulas_printable(self): - ''' - Iterate over all formulas, yield nicely formatted strings. - ''' - formulas = sorted(self.formulas) - for f in formulas: - if f.weight == HARD: - yield '%s.' % fstr(f) - elif type(f.weight) is float: - 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`. - :param grammar: (string) the syntax to be used for parsing the MLN file. Either `PRACGrammar` or `StandardGrammar`. - ''' - # read MLN file - text = '' - if files is not None: - if not type(files) is list: - files = [files] - projectpath = None - for f in files: - if isinstance(f, str): - p = mlnpath(f) - if p.project is not None: - projectpath = p.projectloc - text += p.content - elif isinstance(f, mlnpath): - text += f.content - else: raise Exception('Unexpected file specification: %s' % str(f)) - dirs = [os.path.dirname(fn) for fn in files] - return parse_mln(text, searchpaths=dirs, projectpath=projectpath, logic=logic, grammar=grammar, mln=mln) - raise Exception('No mln files given.') - - -def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic', grammar='PRACGrammar', mln=None): - ''' - Reads an MLN from a stream providing a 'read' method. - ''' - dirs = [os.path.abspath(os.path.expandvars(os.path.expanduser(p))) for p in searchpaths] - formulatemplates = [] - text = str(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) - text = re.compile(r'//\s*\s*$', re.MULTILINE).sub("#group.", text) - # remove comments - text = stripComments(text) - if mln is None: - mln = MLN(logic, grammar) - # read lines - mln.hard_formulas = [] - templateIdx2GroupIdx = {} - inGroup = False - idxGroup = -1 - fixWeightOfNextFormula = False - fuzzy = False - pseudofuzzy = False - uniquevars = None - fixedWeightTemplateIndices = [] - lines = text.split("\n") - iLine = 0 - while iLine < len(lines): - line = lines[iLine] - iLine += 1 - line = line.strip() - try: - if len(line) == 0: continue - # meta directives - if line == "#group": - idxGroup += 1 - inGroup = True - continue - elif line == "#group.": - inGroup = False - continue - elif line.startswith("#fixweight"): - fixWeightOfNextFormula = True - continue - elif line.startswith('#fuzzy'): - if not isinstance(mln.logic, FuzzyLogic): - logger.warning('Fuzzy declarations are not supported in %s. Treated as a binary predicate.' % mln.logic.__class__.__name__) - pseudofuzzy = True - else: - fuzzy = True - continue - elif line.startswith("#include"): - filename = line[len("#include "):].strip() - 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 - # relatively to all paths specified. Take the first file matching. - if not mlnpath(filename).exists: - includefilename = None - for d in dirs: - mlnp = '/'.join([d, filename]) - if mlnpath(mlnp).exists: - includefilename = mlnp - break - if includefilename is None: - raise Exception('File not found: %s' % filename) - else: - includefilename = filename - else: - m = re.match(r'<(?P.+)>', filename) - if m is not None: - filename = m.group('filename') - else: - raise MLNParsingError('Malformed #include statement: %s' % line) - if projectpath is None: - raise MLNParsingError('No project specified: Cannot locate import from project: %s' % filename) - includefilename = ':'.join([projectpath, filename]) - logger.debug('Including file: "%s"' % includefilename) - p = mlnpath(includefilename) - 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 - elif line.startswith('#unique'): - try: - uniVars = re.search('#unique{(.+)}', line) - uniVars = uniVars.groups()[0] - uniVars = list(map(str.strip, uniVars.split(','))) - uniquevars = uniVars - except: - raise MLNParsingError('Malformed #unique expression: "%s"' % line) - continue - elif line.startswith("#AdaptiveMLNDependency"): # declared as "#AdaptiveMLNDependency:pred:domain"; seems to be deprecated - depPredicate, domain = line.split(":")[1:3] - if hasattr(mln, 'AdaptiveDependencyMap'): - if depPredicate in mln.AdaptiveDependencyMap: - mln.AdaptiveDependencyMap[depPredicate].add(domain) - else: - mln.AdaptiveDependencyMap[depPredicate] = set([domain]) - else: - mln.AdaptiveDependencyMap = {depPredicate:set([domain])} - continue - # domain decl - if '=' in line: - # try normal domain definition - parse = mln.logic.parse_domain(line) - if parse is not None: - domName, constants = parse - domName = str(domName) - constants = list(map(str, constants)) - 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] = [] - mln.constant(domName, *constants) - mln.domain_decls.append(line) - continue - # prior probability requirement - if line.startswith("P("): - m = re.match(r"P\((.*?)\)\s*=\s*([\.\de]+)", line) - if m is None: - raise MLNParsingError("Prior probability constraint formatted incorrectly: %s" % line) - mln.prior(f=mln.logic.parse_formula(m.group(1)), p=float(m.group(2))) - continue - # posterior probability requirement/soft evidence - if line.startswith("R(") or line.startswith("SE("): - m = re.match(r"(?:R|SE)\((.*?)\)\s*=\s*([\.\de]+)", line) - if m is None: - raise MLNParsingError("Posterior probability constraint formatted incorrectly: %s" % line) - mln.posterior(f=mln.logic.parse_formula(m.group(1)), p=float(m.group(2))) - continue - # variable definition - if re.match(r'(\$\w+)\s*=(.+)', line): - m = re.match(r'(\$\w+)\s*=(.+)', line) - if m is None: - raise MLNParsingError("Variable assigment malformed: %s" % line) - mln.vars[m.group(1)] = "%s" % m.group(2).strip() - continue - # predicate decl or formula with weight - else: - isHard = False - isPredDecl = False - if line[ -1] == '.': # hard (without explicit weight -> determine later) - isHard = True - formula = line[:-1] - else: # with weight - # try predicate declaration - isPredDecl = True - try: - pred = mln.logic.parse_predicate(line) - except Exception as e: - isPredDecl = False - if isPredDecl: - predname = str(pred[0]) - argdoms = list(map(str, pred[1])) - softmutex = False - mutex = None - 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') - if fuzzy: raise Exception('(Soft-)functional predicates must not be fuzzy.') - mutex = i - if dom[-1] == '?': softmutex = True - argdoms = [x.strip('!?') for x in argdoms] - pred = None - if mutex is not None: - if softmutex: - pred = SoftFunctionalPredicate(predname, argdoms, mutex) - else: - pred = FunctionalPredicate(predname, argdoms, mutex) - elif fuzzy: - pred = FuzzyPredicate(predname, argdoms) - fuzzy = False - else: - pred = Predicate(predname, argdoms) - if pseudofuzzy: - mln.fuzzypreds.append(predname) - pseudofuzzy = False - mln.predicate(pred) - continue - else: - # formula (template) with weight or terminated by '.' - if not isHard: - spacepos = line.find(' ') - weight = line[:spacepos] - formula = line[spacepos:].strip() - try: - formula = mln.logic.parse_formula(formula) - if isHard: - weight = HARD # not set until instantiation when other weights are known - idxTemplate = len(formulatemplates) - formulatemplates.append(formula) - fixweight = False - if inGroup: - templateIdx2GroupIdx[idxTemplate] = idxGroup - if fixWeightOfNextFormula == True: - fixWeightOfNextFormula = False - fixweight = True - fixedWeightTemplateIndices.append(idxTemplate) - - # expand predicate groups - for variant in formula.expandgrouplits(): - mln.formula(variant, weight=weight, - fixweight=fixweight, - unique_templvars=uniquevars) - - if uniquevars: - uniquevars = None - except ParseException as e: - raise MLNParsingError("Error parsing formula '%s'\n" % formula) - if fuzzy and not isPredDecl: raise Exception('"#fuzzy" decorator not allowed at this place: %s' % line) - except MLNParsingError as err: - sys.stderr.write("Error processing line '%s'\n" % line) - cls, e, tb = sys.exc_info() - traceback.print_tb(tb) - raise MLNParsingError(err) - - # augment domains with constants appearing in formula templates - for _, f in mln.iterformulas(): - constants = {} - f.vardoms(None, constants) - for domain, constants in 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 - - From 1762f0244620ae3cbf89749e9304928db6bd46a7 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sun, 17 Jun 2018 22:15:55 +0530 Subject: [PATCH 13/39] move inner classes --- python3/pracmln/logic/.gitignore | 1 + python3/pracmln/logic/common.pyx | 2241 +----------------------------- python3/pracmln/logic/misc.pxd | 0 python3/pracmln/logic/misc.pyx | 2055 +++++++++++++++++++++++++++ 4 files changed, 2110 insertions(+), 2187 deletions(-) create mode 100644 python3/pracmln/logic/misc.pxd create mode 100644 python3/pracmln/logic/misc.pyx diff --git a/python3/pracmln/logic/.gitignore b/python3/pracmln/logic/.gitignore index c246e93e..1ddd5cc6 100644 --- a/python3/pracmln/logic/.gitignore +++ b/python3/pracmln/logic/.gitignore @@ -1,3 +1,4 @@ +common.py *.pyo *.c *.html diff --git a/python3/pracmln/logic/common.pyx b/python3/pracmln/logic/common.pyx index 68f53ec0..c9282ee4 100644 --- a/python3/pracmln/logic/common.pyx +++ b/python3/pracmln/logic/common.pyx @@ -32,2165 +32,38 @@ from ..mln.constants import HARD, predicate_color, inherit, auto from collections import defaultdict import itertools from functools import reduce +from misc import * 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(): - """ - 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(): - """ - 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) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - +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. + """ - class NonLogicalConstraint(Constraint): - """ - A constraint that is not somehow made up of logical connectives and (ground) atoms. + def __init__(self, grammar, mln): """ + Creates a new instance of a Logic factory class. - 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. + :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 __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 __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): """ @@ -2230,21 +103,21 @@ class Logic(): 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, Logic.GroundLit) or isinstance(f, Logic.Lit) or isinstance(f, Logic.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, Logic.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. @@ -2265,7 +138,7 @@ class Logic(): 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) @@ -2286,7 +159,7 @@ class Logic(): return False return True - + # Q(gsoc): possible modifications required upon removing inner classes ... def negate(self, formula): """ Returns a negation of the given formula. @@ -2306,7 +179,6 @@ class Logic(): 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. @@ -2321,7 +193,6 @@ class Logic(): 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. @@ -2335,13 +206,12 @@ class Logic(): 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() @@ -2353,7 +223,7 @@ class Logic(): 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: @@ -2366,7 +236,6 @@ class Logic(): for assignment in Logic._iter_eq_varassignments(mrf, variables, dict_union(assignment, {variable: value})): yield assignment - @staticmethod def clauseset(cnf): """ @@ -2388,28 +257,27 @@ class Logic(): 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_ = [] + formulas_ = [] negated = [] if allpos: for f in formulas: if f.weight < 0: negated.append(f.idx) - f = logic.negate(f) + f = logic.negate(f) f.weight = -f.weight formulas_.append(f) # get CNF version of each ground formula @@ -2432,62 +300,61 @@ class Logic(): 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. @@ -2499,29 +366,29 @@ class Logic(): 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 diff --git a/python3/pracmln/logic/misc.pxd b/python3/pracmln/logic/misc.pxd new file mode 100644 index 00000000..e69de29b diff --git a/python3/pracmln/logic/misc.pyx b/python3/pracmln/logic/misc.pyx new file mode 100644 index 00000000..3ac9546b --- /dev/null +++ b/python3/pracmln/logic/misc.pyx @@ -0,0 +1,2055 @@ +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): + return r'\textit{%s}' % str(sym) + +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))) + + 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=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, 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 + +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]) + + + 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, 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) + +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]) + + 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, 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) + +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, 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) + +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) + +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() + + + 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, 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) + +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 From 5f59e2043f1cc141a2c15f8a522a5718a1d3df83 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 18 Jun 2018 08:08:10 +0530 Subject: [PATCH 14/39] Try moving inner classes to misc --- python3/pracmln/logic/common.pyx | 61 ++++++++++++++++++++++---------- python3/pracmln/logic/fol.py | 46 +++++++++++++++++------- python3/pracmln/logic/fuzzy.py | 1 + python3/pracmln/logic/misc.pxd | 36 +++++++++++++++++++ python3/pracmln/logic/misc.pyx | 36 +++++++++---------- 5 files changed, 130 insertions(+), 50 deletions(-) diff --git a/python3/pracmln/logic/common.pyx b/python3/pracmln/logic/common.pyx index c9282ee4..32fbda76 100644 --- a/python3/pracmln/logic/common.pyx +++ b/python3/pracmln/logic/common.pyx @@ -32,7 +32,26 @@ from ..mln.constants import HARD, predicate_color, inherit, auto from collections import defaultdict import itertools from functools import reduce -from misc import * + +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__) @@ -43,6 +62,10 @@ cdef class Logic(): an instance of the respective element. They also might override the respective implementations and behavior of the logic. """ + + Constraint = misc_Constraint + NonLogicalConstraint = misc_NonLogicalConstraint + Formula = misc_Formula def __init__(self, grammar, mln): """ @@ -390,21 +413,21 @@ cdef class Logic(): # 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 +# 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.py b/python3/pracmln/logic/fol.py index 6f73791b..124a5f50 100644 --- a/python3/pracmln/logic/fol.py +++ b/python3/pracmln/logic/fol.py @@ -26,6 +26,26 @@ from .common import Logic from ..mln.util import fstr +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 + + class FirstOrderLogic(Logic): """ @@ -83,31 +103,31 @@ def _noisyOr(self, mln, worldValues, disj): # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class ComplexFormula(Logic.ComplexFormula, Formula): pass + class ComplexFormula(misc_ComplexFormula, Formula): pass # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Lit(Logic.Lit, Formula): pass + class Lit(misc_Lit, Formula): pass # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Litgroup(Logic.LitGroup, Formula): pass + class Litgroup(misc_LitGroup, Formula): pass # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class GroundAtom(Logic.GroundAtom): pass + class GroundAtom(misc_GroundAtom): pass # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class GroundLit(Logic.GroundLit, Formula): + class GroundLit(misc_GroundLit, Formula): def noisyor(self, world): truth = self(world) @@ -117,7 +137,7 @@ def noisyor(self, world): # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Disjunction(Logic.Disjunction, ComplexFormula): + class Disjunction(misc_Disjunction, ComplexFormula): def truth(self, world): dontKnow = False @@ -164,7 +184,7 @@ def noisyor(self, world): # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Conjunction(Logic.Conjunction, ComplexFormula): + class Conjunction(misc_Conjunction, ComplexFormula): def truth(self, world): dontKnow = False @@ -210,7 +230,7 @@ def noisyor(self, world): # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Implication(Logic.Implication, ComplexFormula): + class Implication(misc_Implication, ComplexFormula): def truth(self, world): ant = self.children[0].truth(world) @@ -225,7 +245,7 @@ def truth(self, world): # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Biimplication(Logic.Biimplication, ComplexFormula): + class Biimplication(misc_Biimplication, ComplexFormula): def truth(self, world): c1 = self.children[0].truth(world) @@ -238,25 +258,25 @@ def truth(self, world): # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Negation(Logic.Negation, ComplexFormula): pass + class Negation(misc_Negation, ComplexFormula): pass # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Exist(Logic.Exist, ComplexFormula): pass + class Exist(misc_Exist, ComplexFormula): pass # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Equality(Logic.Equality, ComplexFormula): pass + class Equality(misc_Equality, ComplexFormula): pass # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class TrueFalse(Logic.TrueFalse, Formula): + class TrueFalse(misc_TrueFalse, Formula): @property def value(self): diff --git a/python3/pracmln/logic/fuzzy.py b/python3/pracmln/logic/fuzzy.py index f1508aeb..6e99fdc2 100644 --- a/python3/pracmln/logic/fuzzy.py +++ b/python3/pracmln/logic/fuzzy.py @@ -24,6 +24,7 @@ from .common import Logic from functools import reduce +import misc class FuzzyLogic(Logic): """ diff --git a/python3/pracmln/logic/misc.pxd b/python3/pracmln/logic/misc.pxd index e69de29b..fb6e165d 100644 --- a/python3/pracmln/logic/misc.pxd +++ b/python3/pracmln/logic/misc.pxd @@ -0,0 +1,36 @@ +cdef class Constraint(): + pass +cdef class Formula(Constraint): + pass +cdef class ComplexFormula(Formula): + pass +cdef class Conjunction(ComplexFormula): + pass +cdef class Disjunction(ComplexFormula): + pass +cdef class Lit(Formula): + pass +cdef class LitGroup(Formula): + pass +cdef class GroundLit(Formula): + pass +cdef class GroundAtom: + pass +cdef class Equality(ComplexFormula): + pass +cdef class Implication(ComplexFormula): + pass +cdef class Biimplication(ComplexFormula): + pass +cdef class Negation(ComplexFormula): + pass +cdef class Exist(ComplexFormula): + pass +cdef class TrueFalse(Formula): + pass +cdef class NonLogicalConstraint(Constraint): + pass +cdef class CountConstraint(NonLogicalConstraint): + pass +cdef class GroundCountConstraint(NonLogicalConstraint): + pass diff --git a/python3/pracmln/logic/misc.pyx b/python3/pracmln/logic/misc.pyx index 3ac9546b..b099b259 100644 --- a/python3/pracmln/logic/misc.pyx +++ b/python3/pracmln/logic/misc.pyx @@ -16,7 +16,7 @@ logger = logs.getlogger(__name__) def latexsym(sym): return r'\textit{%s}' % str(sym) -class Constraint(): +cdef class Constraint(): """ Super class of every constraint. """ @@ -58,7 +58,7 @@ class Constraint(): def gndatoms(self, l=None): raise Exception("%s does not implement gndatoms" % str(type(self))) -class Formula(Constraint): +cdef class Formula(Constraint): """ The base class for all logical formulas. """ @@ -533,7 +533,7 @@ class Formula(Constraint): def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, str(self)) -class ComplexFormula(Formula): +cdef class ComplexFormula(Formula): """ A formula that has other formulas as subelements (children) """ @@ -627,7 +627,7 @@ class ComplexFormula(Formula): prednames = child.prednames(prednames) return prednames -class Conjunction(ComplexFormula): +cdef class Conjunction(ComplexFormula): """ Represents a logical conjunction. """ @@ -734,7 +734,7 @@ class Conjunction(ComplexFormula): conjuncts.append(c) return self.mln.logic.conjunction(conjuncts, mln=self.mln, idx=self.idx) -class Disjunction(ComplexFormula): +cdef class Disjunction(ComplexFormula): """ Represents a disjunction of formulas. """ @@ -847,7 +847,7 @@ class Disjunction(ComplexFormula): disjuncts.append(c) return self.mln.logic.disjunction(disjuncts, mln=self.mln, idx=self.idx) -class Lit(Formula): +cdef class Lit(Formula): """ Represents a literal. """ @@ -1022,7 +1022,7 @@ class Lit(Formula): def __ne__(self, other): return not self == other -class LitGroup(Formula): +cdef class LitGroup(Formula): """ Represents a group of literals with identical arguments. """ @@ -1182,7 +1182,7 @@ class LitGroup(Formula): def __ne__(self, other): return not self == other -class GroundLit(Formula): +cdef class GroundLit(Formula): """ Represents a ground literal. """ @@ -1325,7 +1325,7 @@ class GroundLit(Formula): def __ne__(self, other): return not self == other -class GroundAtom: +cdef class GroundAtom: """ Represents a ground atom. """ @@ -1422,7 +1422,7 @@ class GroundAtom: def __ne__(self, other): return not self == other -class Equality(ComplexFormula): +cdef class Equality(ComplexFormula): """ Represents (in)equality constraints between two symbols. """ @@ -1546,7 +1546,7 @@ class Equality(ComplexFormula): 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): +cdef class Implication(ComplexFormula): """ Represents an implication """ @@ -1597,7 +1597,7 @@ class Implication(ComplexFormula): 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): +cdef class Biimplication(ComplexFormula): """ Represents a bi-implication. """ @@ -1655,7 +1655,7 @@ class Biimplication(ComplexFormula): 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): +cdef class Negation(ComplexFormula): """ Represents a negation of a complex formula. """ @@ -1777,7 +1777,7 @@ class Negation(ComplexFormula): else: return self.mln.logic.negation([f], mln=self.mln, idx=self.idx) -class Exist(ComplexFormula): +cdef class Exist(ComplexFormula): """ Existential quantifier. """ @@ -1889,7 +1889,7 @@ class Exist(ComplexFormula): def truth(self, w): raise Exception("'%s' does not implement truth()" % self.__class__.__name__) -class TrueFalse(Formula): +cdef class TrueFalse(Formula): """ Represents constant truth values. """ @@ -1931,7 +1931,7 @@ class TrueFalse(Formula): 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): +cdef class NonLogicalConstraint(Constraint): """ A constraint that is not somehow made up of logical connectives and (ground) atoms. """ @@ -1946,7 +1946,7 @@ class NonLogicalConstraint(Constraint): def negate(self): raise Exception("%s does not implement negate()" % str(type(self))) -class CountConstraint(NonLogicalConstraint): +cdef class CountConstraint(NonLogicalConstraint): """ A constraint that tests the number of relation instances against an integer. """ @@ -2007,7 +2007,7 @@ class CountConstraint(NonLogicalConstraint): self.literal.getVariables(mln, variables, constants) return variables -class GroundCountConstraint(NonLogicalConstraint): +cdef class GroundCountConstraint(NonLogicalConstraint): def __init__(self, gndAtoms, op, count): self.gndAtoms = gndAtoms self.count = count From 9d7951372a292ecbe1d54cf4cb96f4ba4f5c8b2e Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 2 Jul 2018 12:58:50 +0530 Subject: [PATCH 15/39] Cythonise logic superclass --- python3/pracmln/logic/common.pxd | 39 + python3/pracmln/logic/common.pyx | 2165 +++++++++++++++++++++++++++++- python3/pracmln/logic/misc.pxd | 36 - python3/pracmln/logic/misc.pyx | 2055 ---------------------------- 4 files changed, 2171 insertions(+), 2124 deletions(-) delete mode 100644 python3/pracmln/logic/misc.pxd delete mode 100644 python3/pracmln/logic/misc.pyx diff --git a/python3/pracmln/logic/common.pxd b/python3/pracmln/logic/common.pxd index 922dfa20..325ae696 100644 --- a/python3/pracmln/logic/common.pxd +++ b/python3/pracmln/logic/common.pxd @@ -1,4 +1,43 @@ + +cdef class Constraint(): + pass +cdef class Formula(Constraint): + pass +cdef class ComplexFormula(Formula): + pass +cdef class Conjunction(ComplexFormula): + pass +cdef class Disjunction(ComplexFormula): + pass +cdef class Lit(Formula): + pass +cdef class LitGroup(Formula): + pass +cdef class GroundLit(Formula): + pass +cdef class GroundAtom: + pass +cdef class Equality(ComplexFormula): + pass +cdef class Implication(ComplexFormula): + pass +cdef class Biimplication(ComplexFormula): + pass +cdef class Negation(ComplexFormula): + cdef dict __dict__ +cdef class Exist(ComplexFormula): + pass +cdef class TrueFalse(Formula): + pass +cdef class NonLogicalConstraint(Constraint): + pass +cdef class CountConstraint(NonLogicalConstraint): + pass +cdef class GroundCountConstraint(NonLogicalConstraint): + pass + cdef class Logic: pass #cdef class Constraint(): # pass + diff --git a/python3/pracmln/logic/common.pyx b/python3/pracmln/logic/common.pyx index 32fbda76..70c45b1d 100644 --- a/python3/pracmln/logic/common.pyx +++ b/python3/pracmln/logic/common.pyx @@ -33,7 +33,7 @@ from collections import defaultdict import itertools from functools import reduce -from .misc import Constraint as misc_Constraint +'''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 @@ -50,10 +50,2106 @@ 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 +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))) + + def 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'): + 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=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]) + + + 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, 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]) + + 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, 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 + 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 + +cdef 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 + +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._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) + +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() + + + 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, 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 + + @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) + +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(): """ @@ -61,11 +2157,7 @@ cdef class Logic(): 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. - """ - - Constraint = misc_Constraint - NonLogicalConstraint = misc_NonLogicalConstraint - Formula = misc_Formula + """ def __init__(self, grammar, mln): """ @@ -78,6 +2170,13 @@ cdef class Logic(): 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() @@ -131,14 +2230,14 @@ cdef class Logic(): """ Determines whether or not a formula is a literal. """ - return isinstance(f, Logic.GroundLit) or isinstance(f, Logic.Lit) or isinstance(f, Logic.GroundAtom) + 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, Logic.Equality) + return isinstance(f, Equality) # Q(gsoc): possible modifications required upon removing inner classes ... def islitconj(self, f): @@ -146,18 +2245,18 @@ cdef class Logic(): 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): + 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, Logic.Lit) and \ - not isinstance(child, Logic.GroundLit) and \ - not isinstance(child, Logic.Equality) and \ - not isinstance(child, Logic.TrueFalse): + if not isinstance(child, Lit) and \ + not isinstance(child, GroundLit) and \ + not isinstance(child, Equality) and \ + not isinstance(child, TrueFalse): return False return True @@ -167,18 +2266,18 @@ cdef class Logic(): 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): + 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, Logic.Lit) and \ - not isinstance(child, Logic.GroundLit) and \ - not isinstance(child, Logic.Equality) and \ - not isinstance(child, Logic.TrueFalse): + if not isinstance(child, Lit) and \ + not isinstance(child, GroundLit) and \ + not isinstance(child, Equality) and \ + not isinstance(child, TrueFalse): return False return True @@ -192,10 +2291,10 @@ cdef class Logic(): a rudimentary simplification in case of `formula` is a (ground) literal or equality. """ - if isinstance(formula, Logic.Lit) or isinstance(formula, Logic.GroundLit): + if isinstance(formula, Lit) or isinstance(formula, GroundLit): ret = formula.copy() ret.negated = not ret.negated - elif isinstance(formula, Logic.Equality): + elif isinstance(formula, Equality): ret = formula.copy() ret.negated = not ret.negated else: @@ -266,13 +2365,13 @@ cdef class Logic(): containing literals. All literals are converted into strings. """ clauses = [] - if isinstance(cnf, Logic.Disjunction): + if isinstance(cnf, Disjunction): clauses.append(set(map(str, cnf.children))) - elif isinstance(cnf, Logic.Conjunction): + elif isinstance(cnf, Conjunction): for disj in cnf.children: clause = set() clauses.append(clause) - if isinstance(disj, Logic.Disjunction): + if isinstance(disj, Disjunction): for c in disj.children: clause.add(str(c)) else: @@ -317,7 +2416,7 @@ cdef class Logic(): cnf = logic.negate(gf).cnf() else: cnf = gf.cnf() - if isinstance(cnf, Logic.TrueFalse): # formulas that are always true or false can be ignored + if isinstance(cnf, TrueFalse): # formulas that are always true or false can be ignored continue cnf.idx = gf.idx gfs_.append(cnf) diff --git a/python3/pracmln/logic/misc.pxd b/python3/pracmln/logic/misc.pxd deleted file mode 100644 index fb6e165d..00000000 --- a/python3/pracmln/logic/misc.pxd +++ /dev/null @@ -1,36 +0,0 @@ -cdef class Constraint(): - pass -cdef class Formula(Constraint): - pass -cdef class ComplexFormula(Formula): - pass -cdef class Conjunction(ComplexFormula): - pass -cdef class Disjunction(ComplexFormula): - pass -cdef class Lit(Formula): - pass -cdef class LitGroup(Formula): - pass -cdef class GroundLit(Formula): - pass -cdef class GroundAtom: - pass -cdef class Equality(ComplexFormula): - pass -cdef class Implication(ComplexFormula): - pass -cdef class Biimplication(ComplexFormula): - pass -cdef class Negation(ComplexFormula): - pass -cdef class Exist(ComplexFormula): - pass -cdef class TrueFalse(Formula): - pass -cdef class NonLogicalConstraint(Constraint): - pass -cdef class CountConstraint(NonLogicalConstraint): - pass -cdef class GroundCountConstraint(NonLogicalConstraint): - pass diff --git a/python3/pracmln/logic/misc.pyx b/python3/pracmln/logic/misc.pyx deleted file mode 100644 index b099b259..00000000 --- a/python3/pracmln/logic/misc.pyx +++ /dev/null @@ -1,2055 +0,0 @@ -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): - 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))) - - def 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'): - 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=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]) - - - 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, 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]) - - 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, 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 - 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 - -cdef 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 - -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._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) - -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() - - - 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, 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 - - @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) - -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 From 2111172833fee750431a1bbe3df23b59e4c81270 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 2 Jul 2018 12:59:05 +0530 Subject: [PATCH 16/39] Cythonise logic subclasses --- python3/pracmln/logic/fol.py | 261 ++++++++++++++++----------------- python3/pracmln/logic/fuzzy.py | 95 +++++++----- 2 files changed, 187 insertions(+), 169 deletions(-) diff --git a/python3/pracmln/logic/fol.py b/python3/pracmln/logic/fol.py index 124a5f50..b60c782b 100644 --- a/python3/pracmln/logic/fol.py +++ b/python3/pracmln/logic/fol.py @@ -1,5 +1,5 @@ # FIRST-ORDER LOGIC -- PROCESSING -# +# # (C) 2013 by Daniel Nyga (nyga@cs.uni-bremen.de) # (C) 2007-2012 by Dominik Jain # @@ -24,52 +24,50 @@ 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 -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 - - class FirstOrderLogic(Logic): """ Factory class for first-order logic. """ -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + class Constraint(Super_Constraint): pass - class Constraint(Logic.Constraint): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + class Formula(Super_Formula, Constraint): - - 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, @@ -79,7 +77,7 @@ def _getEvidenceTruthDegreeCW(self, gndAtom, worldValues): 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): @@ -92,53 +90,53 @@ def _noisyOr(self, mln, worldValues, disj): for lit in lits: p = mln._getEvidenceTruthDegreeCW(lit.gndAtom, worldValues) if not lit.negated: - factor = p + factor = p else: factor = 1.0 - p prod *= 1.0 - factor return 1.0 - prod - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class ComplexFormula(misc_ComplexFormula, Formula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + class ComplexFormula(Super_ComplexFormula, Formula): pass + + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + class Lit(Super_Lit, Formula): pass + + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class Lit(misc_Lit, Formula): pass + class Litgroup(Super_LitGroup, Formula): pass -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Litgroup(misc_LitGroup, Formula): pass + class GroundAtom(Super_GroundAtom): pass -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundAtom(misc_GroundAtom): pass - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class GroundLit(misc_GroundLit, Formula): + + class GroundLit(Super_GroundLit, Formula): def noisyor(self, world): truth = self(world) if self.negated: truth = 1. - truth return truth -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + class Disjunction(Super_Disjunction, ComplexFormula): - - class Disjunction(misc_Disjunction, ComplexFormula): - def truth(self, world): dontKnow = False for child in self.children: @@ -152,7 +150,7 @@ def truth(self, world): else: return 0 - + def simplify(self, world): sf_children = [] for child in self.children: @@ -168,24 +166,24 @@ def simplify(self, world): 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): + + def noisyor(self, world): prod = 1.0 for lit in self.children: p = ifnone(lit(world), 1) if not lit.negated: - factor = p + factor = p else: factor = 1.0 - p prod *= 1.0 - factor return 1.0 - prod - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class Conjunction(misc_Conjunction, ComplexFormula): - + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + class Conjunction(Super_Conjunction, ComplexFormula): + def truth(self, world): dontKnow = False for child in self.children: @@ -198,7 +196,7 @@ def truth(self, world): return None else: return 1. - + def simplify(self, world): sf_children = [] @@ -215,8 +213,8 @@ def simplify(self, world): 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 @@ -227,10 +225,10 @@ def noisyor(self, world): prod *= cnf.noisyor(world) return prod -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Implication(misc_Implication, ComplexFormula): + class Implication(Super_Implication, ComplexFormula): def truth(self, world): ant = self.children[0].truth(world) @@ -242,10 +240,10 @@ def truth(self, world): return 0 -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class Biimplication(misc_Biimplication, ComplexFormula): + + class Biimplication(Super_Biimplication, ComplexFormula): def truth(self, world): c1 = self.children[0].truth(world) @@ -253,133 +251,133 @@ def truth(self, world): if c1 is None or c2 is None: return None return 1 if (c1 == c2) else 0 - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class Negation(misc_Negation, ComplexFormula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + class Negation(Super_Negation, ComplexFormula): pass + + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + - - class Exist(misc_Exist, ComplexFormula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + class Exist(Super_Exist, ComplexFormula): pass - - class Equality(misc_Equality, ComplexFormula): pass - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + class Equality(Super_Equality, ComplexFormula): pass + + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + class TrueFalse(Super_TrueFalse, Formula): - class TrueFalse(misc_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 PriorConstraint(ProbabilityConstraint): """ Class representing a prior probability. """ def __str__(self): return 'P(%s) = %.2f' % (fstr(self.formula), self.p) - - -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - class PosteriorConstraint(ProbabilityConstraint): + + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + + + 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 @@ -396,6 +394,7 @@ def true_false(self, *args, **kwargs): Negation = FirstOrderLogic.Negation Exist = FirstOrderLogic.Exist TrueFalse = FirstOrderLogic.TrueFalse -NonLogicalConstraint = FirstOrderLogic.NonLogicalConstraint -CountConstraint = FirstOrderLogic.CountConstraint -GroundCountConstraint = FirstOrderLogic.GroundCountConstraint +# the following attributes no longer exist (!): +# NonLogicalConstraint = FirstOrderLogic.NonLogicalConstraint +# CountConstraint = FirstOrderLogic.CountConstraint +# GroundCountConstraint = FirstOrderLogic.GroundCountConstraint diff --git a/python3/pracmln/logic/fuzzy.py b/python3/pracmln/logic/fuzzy.py index 6e99fdc2..391a3ff7 100644 --- a/python3/pracmln/logic/fuzzy.py +++ b/python3/pracmln/logic/fuzzy.py @@ -21,10 +21,28 @@ # 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 -import misc class FuzzyLogic(Logic): """ @@ -54,55 +72,55 @@ def max_undef(*args): 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 Constraint(Super_Constraint): pass -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Formula(Logic.Formula): pass + class Formula(Super_Formula): pass -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class ComplexFormula(Logic.Formula): pass + class ComplexFormula(Super_Formula): pass -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Lit(Logic.Lit): pass + class Lit(Super_Lit): pass -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class LitGroup(Logic.LitGroup): pass + class LitGroup(Super_LitGroup): pass -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class GroundLit(Logic.GroundLit): pass + class GroundLit(Super_GroundLit): pass -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class GroundAtom(Logic.GroundAtom): + class GroundAtom(Super_GroundAtom): def truth(self, world): return world[self.idx] -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Negation(Logic.Negation, ComplexFormula): + class Negation(Super_Negation, ComplexFormula): def truth(self, world): val = self.children[0].truth(world) @@ -111,16 +129,16 @@ def truth(self, world): def simplify(self, world): f = self.children[0].simplify(world) - if isinstance(f, Logic.TrueFalse): + if isinstance(f, Super_TrueFalse): return f.invert() else: return self.mln.logic.negation([f], mln=self.mln, idx=self.idx) -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Conjunction(Logic.Conjunction, ComplexFormula): + class Conjunction(Super_Conjunction, ComplexFormula): def truth(self, world): @@ -133,7 +151,7 @@ def simplify(self, world): minTruth = None for child_ in self.children: child = child_.simplify(world) - if isinstance(child, Logic.TrueFalse): + if isinstance(child, Super_TrueFalse): truth = child.truth() if truth == 0: return self.mln.logic.true_false(0., mln=self.mln, idx=self.idx) @@ -151,10 +169,10 @@ def simplify(self, world): assert False # should be unreachable -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Disjunction(Logic.Disjunction, ComplexFormula): + class Disjunction(Super_Disjunction, ComplexFormula): def truth(self, world): @@ -166,7 +184,7 @@ def simplify(self, world): maxTruth = None for child in self.children: child = child.simplify(world) - if isinstance(child, Logic.TrueFalse): + if isinstance(child, Super_TrueFalse): truth = child.truth() if truth == 1: return self.mln.logic.true_false(1., mln=self.mln, idx=self.idx) @@ -184,10 +202,10 @@ def simplify(self, world): assert False -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Implication(Logic.Implication, ComplexFormula): + class Implication(Super_Implication, ComplexFormula): def truth(self, world): ant = self.children[0].truth(world) @@ -198,10 +216,10 @@ def simplify(self, world): self.children[1]], mln=self.mln, idx=self.idx).simplify(world) -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Biimplication(Logic.Biimplication, ComplexFormula): + class Biimplication(Super_Biimplication, ComplexFormula): def truth(self, world): return FuzzyLogic.min_undef(self.children[0].truth(world), self.children[1].truth(world)) @@ -212,10 +230,10 @@ def simplify(self, world): return self.mln.logic.conjunction([c1,c2], mln=self.mln, idx=self.idx).simplify(world) -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Equality(Logic.Equality): + class Equality(Super_Equality): def truth(self, world=None): if any(map(self.mln.logic.isvar, self.args)): @@ -229,10 +247,10 @@ def simplify(self, world): return self.mln.logic.equality(list(self.args), negated=self.negated, mln=self.mln, idx=self.idx) -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class TrueFalse(Formula, Logic.TrueFalse): + class TrueFalse(Formula, Super_TrueFalse): # def __init__(self, truth, mln, idx=None): # if not (truth >= 0. and truth <= 1.): @@ -259,14 +277,14 @@ def invert(self): return self.mln.logic.true_false(1. - self.value, idx=self.idx, mln=self.mln) -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - class Exist(Logic.Exist, Logic.ComplexFormula): + class Exist(Super_Exist, Super_ComplexFormula): pass -# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # + # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # def conjunction(self, *args, **kwargs): @@ -336,7 +354,8 @@ def true_false(self, *args, **kwargs): Negation = FuzzyLogic.Negation Exist = FuzzyLogic.Exist TrueFalse = FuzzyLogic.TrueFalse -NonLogicalConstraint = FuzzyLogic.NonLogicalConstraint -CountConstraint = FuzzyLogic.CountConstraint -GroundCountConstraint = FuzzyLogic.GroundCountConstraint +# the following attributes no longer exist (!): +# NonLogicalConstraint = FuzzyLogic.NonLogicalConstraint +# CountConstraint = FuzzyLogic.CountConstraint +# GroundCountConstraint = FuzzyLogic.GroundCountConstraint From 6db7590dbe3266ce4b621612b437c170f124fef7 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 2 Jul 2018 13:01:15 +0530 Subject: [PATCH 17/39] Cythonise miscellaneous logic module usage --- python3/pracmln/mln/mrf.pyx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/python3/pracmln/mln/mrf.pyx b/python3/pracmln/mln/mrf.pyx index d3865c66..8d8d4d71 100644 --- a/python3/pracmln/mln/mrf.pyx +++ b/python3/pracmln/mln/mrf.pyx @@ -40,8 +40,12 @@ from .mrfvars import (MutexVariable, SoftMutexVariable, FuzzyVariable, 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 cpython cimport array +#import array + logger = logs.getlogger(__name__) @@ -64,7 +68,7 @@ cdef class MRF(object): 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 @@ -327,7 +331,7 @@ cdef class MRF(object): 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) @@ -345,7 +349,7 @@ cdef class MRF(object): ''' 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) From 0bdf67f14263b210fd6b2038c704e1873a90d2e9 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 2 Jul 2018 13:01:55 +0530 Subject: [PATCH 18/39] [TEMPORARY] attribute cythonisation --- python3/pracmln/mln/base.pxd | 3 ++- python3/pracmln/mln/inference/infer.pxd | 3 +++ python3/pracmln/mln/mrf.pxd | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/python3/pracmln/mln/base.pxd b/python3/pracmln/mln/base.pxd index d207d64b..465f4dce 100644 --- a/python3/pracmln/mln/base.pxd +++ b/python3/pracmln/mln/base.pxd @@ -1,4 +1,5 @@ from ..logic.common cimport Logic cdef class MLN: - cdef Logic logic + cdef public Logic logic + cdef dict __dict__ diff --git a/python3/pracmln/mln/inference/infer.pxd b/python3/pracmln/mln/inference/infer.pxd index 3c6a86a4..dc954bce 100644 --- a/python3/pracmln/mln/inference/infer.pxd +++ b/python3/pracmln/mln/inference/infer.pxd @@ -1,4 +1,7 @@ from ..mrf cimport MRF +from ..base cimport MLN cdef class Inference: cdef MRF mrf + cdef MLN mln + cdef dict __dict__ diff --git a/python3/pracmln/mln/mrf.pxd b/python3/pracmln/mln/mrf.pxd index 9915e666..66df7a46 100644 --- a/python3/pracmln/mln/mrf.pxd +++ b/python3/pracmln/mln/mrf.pxd @@ -1,4 +1,8 @@ from base cimport MLN +from cpython cimport array + cdef class MRF: - cdef MLN mln + cdef public MLN mln + #cdef array.array _evidence + cdef dict __dict__ From 032634a08bd7086e1dbb0a31d6cfd5cf975248e8 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Wed, 4 Jul 2018 03:49:51 +0530 Subject: [PATCH 19/39] [TEMPORARY] add cython print on import --- python3/pracmln/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/python3/pracmln/__init__.py b/python3/pracmln/__init__.py index f1a7fc56..e33e232e 100644 --- a/python3/pracmln/__init__.py +++ b/python3/pracmln/__init__.py @@ -30,3 +30,5 @@ from .mlnlearn import EVIDENCE_PREDS from .utils.project import mlnpath from .utils.project import PRACMLNConfig + +print('[Cython]') From 12904354133ff70283b6e1c21eb3962b710e23ea Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Tue, 10 Jul 2018 12:06:43 +0530 Subject: [PATCH 20/39] Restore functionality --- python3/pracmln/mln/base.pyx | 7 ++++-- python3/pracmln/mln/grounding/bpll.py | 10 ++++---- python3/pracmln/mln/grounding/fastconj.py | 26 +++++++++++++-------- python3/pracmln/mln/inference/gibbs.pyx | 4 +++- python3/pracmln/mln/inference/infer.pxd | 2 +- python3/pracmln/mln/inference/infer.pyx | 1 + python3/pracmln/mln/inference/maxwalk.pyx | 3 ++- python3/pracmln/mln/inference/mcsat.pyx | 17 ++++++++------ python3/pracmln/mln/inference/wcspinfer.pyx | 9 +++---- python3/pracmln/mln/learning/cll.py | 12 ++++++---- 10 files changed, 56 insertions(+), 35 deletions(-) diff --git a/python3/pracmln/mln/base.pyx b/python3/pracmln/mln/base.pyx index b07188e8..9f14d296 100644 --- a/python3/pracmln/mln/base.pyx +++ b/python3/pracmln/mln/base.pyx @@ -247,9 +247,12 @@ cdef class MLN(object): elif type(formula) is int: return self._formulas[formula] 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) 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/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/inference/gibbs.pyx b/python3/pracmln/mln/inference/gibbs.pyx index 4cbd12b3..dbd203de 100644 --- a/python3/pracmln/mln/inference/gibbs.pyx +++ b/python3/pracmln/mln/inference/gibbs.pyx @@ -33,6 +33,7 @@ from .mcmc import MCMCInference 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 @@ -43,7 +44,8 @@ class GibbsSampler(MCMCInference): 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) diff --git a/python3/pracmln/mln/inference/infer.pxd b/python3/pracmln/mln/inference/infer.pxd index dc954bce..bc0e19f1 100644 --- a/python3/pracmln/mln/inference/infer.pxd +++ b/python3/pracmln/mln/inference/infer.pxd @@ -2,6 +2,6 @@ from ..mrf cimport MRF from ..base cimport MLN cdef class Inference: - cdef MRF mrf + cdef public MRF mrf #public MRF mrf cdef MLN mln cdef dict __dict__ diff --git a/python3/pracmln/mln/inference/infer.pyx b/python3/pracmln/mln/inference/infer.pyx index eac0c60a..26b2efa2 100644 --- a/python3/pracmln/mln/inference/infer.pyx +++ b/python3/pracmln/mln/inference/infer.pyx @@ -57,6 +57,7 @@ cdef class Inference(): def __init__(self, mrf, queries=ALL, **params): self.mrf = mrf + #print(self.mrf) self.mln = mrf.mln self._params = edict(params) if not queries: diff --git a/python3/pracmln/mln/inference/maxwalk.pyx b/python3/pracmln/mln/inference/maxwalk.pyx index 5681147e..2490a71c 100644 --- a/python3/pracmln/mln/inference/maxwalk.pyx +++ b/python3/pracmln/mln/inference/maxwalk.pyx @@ -33,6 +33,7 @@ from .mcmc import MCMCInference 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 @@ class SAMaxWalkSAT(MCMCInference): 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)) diff --git a/python3/pracmln/mln/inference/mcsat.pyx b/python3/pracmln/mln/inference/mcsat.pyx index dafe4303..0c47c87f 100644 --- a/python3/pracmln/mln/inference/mcsat.pyx +++ b/python3/pracmln/mln/inference/mcsat.pyx @@ -35,6 +35,9 @@ from ..constants import ALL, HARD 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 @@ class MCSAT(MCMCInference): 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) @@ -87,9 +90,9 @@ class MCSAT(MCMCInference): # 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 @@ -129,7 +132,7 @@ class MCSAT(MCMCInference): 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] @@ -369,7 +372,7 @@ class SampleSAT: # 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))) @@ -499,7 +502,7 @@ class SampleSAT: 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: diff --git a/python3/pracmln/mln/inference/wcspinfer.pyx b/python3/pracmln/mln/inference/wcspinfer.pyx index 8e655bfa..0324812a 100644 --- a/python3/pracmln/mln/inference/wcspinfer.pyx +++ b/python3/pracmln/mln/inference/wcspinfer.pyx @@ -32,7 +32,8 @@ from ..mrfvars import FuzzyVariable 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__) @@ -134,7 +135,7 @@ class WCSPConverter(object): # 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 @@ -196,7 +197,7 @@ class WCSPConverter(object): 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. @@ -318,7 +319,7 @@ class WCSPConverter(object): 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] 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 From e81cd19d309e1f2b138ff5ddcc33bdda237885b7 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 16 Jul 2018 23:48:38 +0530 Subject: [PATCH 21/39] profiling --- python3/pracmln/logic/common.pyx | 24 +++++++++++++++++++++--- python3/pracmln/logic/setup.py | 2 +- python3/pracmln/mln/base.pyx | 9 ++++++++- python3/pracmln/mln/inference/exact.pyx | 5 +++-- python3/pracmln/mln/inference/setup.py | 9 +-------- python3/pracmln/mln/setup.py | 9 +-------- 6 files changed, 35 insertions(+), 23 deletions(-) diff --git a/python3/pracmln/logic/common.pyx b/python3/pracmln/logic/common.pyx index 70c45b1d..1f047875 100644 --- a/python3/pracmln/logic/common.pyx +++ b/python3/pracmln/logic/common.pyx @@ -169,6 +169,7 @@ cdef class Formula(Constraint): 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. @@ -193,17 +194,20 @@ cdef class Formula(Constraint): child.gndatom_indices(l) return l - + # Q(gsoc): needs speedup ... def 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 @@ -1244,28 +1248,34 @@ cdef class GroundLit(Formula): @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 @@ -1327,6 +1337,7 @@ cdef class GroundLit(Formula): def 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 @@ -1380,9 +1391,13 @@ cdef class GroundAtom: 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 @@ -1435,6 +1450,7 @@ cdef class GroundAtom: return '' % str(self) + # Q(gsoc): needs speedup ... def __str__(self): return "%s(%s)" % (self.predname, ",".join(self.args)) @@ -1946,8 +1962,10 @@ cdef class TrueFalse(Formula): Formula.__init__(self, mln, idx) self.value = truth + # Q(gsoc): where is this property used? @property def value(self): + #print('getting value {} of type {}'.format(self._value, type(self._value))) return self._value def cstr(self, color=False): @@ -2157,7 +2175,7 @@ cdef class Logic(): 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): """ @@ -2176,7 +2194,7 @@ cdef class Logic(): #self.Conjunction = Conjunction(mln) # children = ? #self.Disjunction = Disjunction(mln) # children = ? #self.Lit = Lit() - + def __getstate__(self): d = self.__dict__.copy() diff --git a/python3/pracmln/logic/setup.py b/python3/pracmln/logic/setup.py index 8055b07f..6e967ce4 100644 --- a/python3/pracmln/logic/setup.py +++ b/python3/pracmln/logic/setup.py @@ -2,5 +2,5 @@ from Cython.Build import cythonize setup( - ext_modules=cythonize( ['*.pyx'] ) + ext_modules=cythonize("*.pyx", compiler_directives={'profile': True}) ) diff --git a/python3/pracmln/mln/base.pyx b/python3/pracmln/mln/base.pyx index 9f14d296..015dbed1 100644 --- a/python3/pracmln/mln/base.pyx +++ b/python3/pracmln/mln/base.pyx @@ -47,6 +47,10 @@ from .learning.bpll import BPLL from ..utils.project import mlnpath from importlib import util as imputil +#from cpython cimport array +#import array + + logger = logs.getlogger(__name__) @@ -82,7 +86,7 @@ cdef class MLN(object): 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 = [] @@ -108,6 +112,7 @@ cdef class MLN(object): @property def weights(self): + #print('_weight is {} of type {}'.format(self._weights, type(self._weights))) return self._weights @weights.setter @@ -256,6 +261,8 @@ cdef class MLN(object): 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 []) diff --git a/python3/pracmln/mln/inference/exact.pyx b/python3/pracmln/mln/inference/exact.pyx index 96a266e1..dfba85d0 100644 --- a/python3/pracmln/mln/inference/exact.pyx +++ b/python3/pracmln/mln/inference/exact.pyx @@ -104,8 +104,8 @@ cdef 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: @@ -175,6 +175,7 @@ cdef class EnumerationAsk(Inference): return result def soft_evidence_formula(self, gf): + #print('result={}'.format(gf.gndatoms())) truths = [a.truth(self.mrf.evidence) for a in gf.gndatoms()] if None in truths: return False diff --git a/python3/pracmln/mln/inference/setup.py b/python3/pracmln/mln/inference/setup.py index 126b3607..36c9a65e 100644 --- a/python3/pracmln/mln/inference/setup.py +++ b/python3/pracmln/mln/inference/setup.py @@ -2,13 +2,6 @@ #from distutils.extension import Extension from Cython.Build import cythonize -#''' -#ext_modules=[ -# Extension("exact", ["exact.pyx"]), -# Extension("mcmc", ["mcmc.pyx"]), -#] -#''' - setup( - ext_modules=cythonize( ['*.pyx'] ) + ext_modules=cythonize("*.pyx", compiler_directives={'profile': True}) ) diff --git a/python3/pracmln/mln/setup.py b/python3/pracmln/mln/setup.py index 126b3607..36c9a65e 100644 --- a/python3/pracmln/mln/setup.py +++ b/python3/pracmln/mln/setup.py @@ -2,13 +2,6 @@ #from distutils.extension import Extension from Cython.Build import cythonize -#''' -#ext_modules=[ -# Extension("exact", ["exact.pyx"]), -# Extension("mcmc", ["mcmc.pyx"]), -#] -#''' - setup( - ext_modules=cythonize( ['*.pyx'] ) + ext_modules=cythonize("*.pyx", compiler_directives={'profile': True}) ) From 644f60846a38e6109ebac65ab728c8cbc2e1f354 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Tue, 17 Jul 2018 01:41:27 +0530 Subject: [PATCH 22/39] speedup common-logic(1) --- python3/pracmln/logic/common.pxd | 40 ++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/python3/pracmln/logic/common.pxd b/python3/pracmln/logic/common.pxd index 325ae696..5f448365 100644 --- a/python3/pracmln/logic/common.pxd +++ b/python3/pracmln/logic/common.pxd @@ -1,38 +1,64 @@ +from ..mln.base cimport MLN + + cdef class Constraint(): - pass + cdef dict __dict__ + cdef class Formula(Constraint): + cdef MLN _mln pass + cdef class ComplexFormula(Formula): pass + cdef class Conjunction(ComplexFormula): pass + cdef class Disjunction(ComplexFormula): pass + cdef class Lit(Formula): - pass + cdef int _negated + cdef str _predname + cdef class LitGroup(Formula): - pass + cdef int _negated + cdef str _predname + cdef class GroundLit(Formula): - pass + cdef GroundAtom _gndatom + cdef int _negated + cdef class GroundAtom: - pass + cdef str _predname + cdef MLN mln + cdef dict __dict__ + cdef class Equality(ComplexFormula): - pass + cdef int _negated + cdef class Implication(ComplexFormula): pass + cdef class Biimplication(ComplexFormula): pass + cdef class Negation(ComplexFormula): - cdef dict __dict__ + pass + cdef class Exist(ComplexFormula): pass + cdef class TrueFalse(Formula): pass + cdef class NonLogicalConstraint(Constraint): pass + cdef class CountConstraint(NonLogicalConstraint): pass + cdef class GroundCountConstraint(NonLogicalConstraint): pass From 6c157dd6d3062e760e2800e6f884fbca6792ab09 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Tue, 17 Jul 2018 01:48:40 +0530 Subject: [PATCH 23/39] fix speedup error --- python3/pracmln/mln/mrfvars.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/python3/pracmln/mln/mrfvars.py b/python3/pracmln/mln/mrfvars.py index 70e37277..f13551c2 100644 --- a/python3/pracmln/mln/mrfvars.py +++ b/python3/pracmln/mln/mrfvars.py @@ -25,6 +25,9 @@ from .errors import MRFValueException from .util import Interval +import array +#import inspect + class MRFVariable(object): """ Represents a (mutually exclusive) block of ground atoms. @@ -186,6 +189,8 @@ def itervalues(self, evidence=None): """ if type(evidence) is list: evidence = dict([(i, v) for i, v in enumerate(evidence)]) + if type(evidence) is array.array: + evidence = dict([(i, v) for i, v in enumerate(evidence)]) for tup in self._itervalues(evidence): yield self.valueidx(tup), tup @@ -303,6 +308,9 @@ def _itervalues(self, evidence=None): evidence = {} if len(self.gndatoms) != 1: raise Exception('Illegal number of ground atoms in the variable %s' % repr(self)) gndatom = self.gndatoms[0] + #curframe = inspect.currentframe() + #calframe = inspect.getouterframes(curframe, 2) + #print('evidence is {} of type {} - called by {}'.format(evidence, type(evidence), calframe[1][3])) if evidence.get(gndatom.idx) is not None and evidence.get(gndatom.idx) in (0,1): yield (evidence[gndatom.idx],) return From 9a594a4d6943e9e1abae0ca266fd0dbab0ed54b2 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sun, 22 Jul 2018 03:49:24 +0530 Subject: [PATCH 24/39] revert --- python3/pracmln/mln/mrfvars.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/python3/pracmln/mln/mrfvars.py b/python3/pracmln/mln/mrfvars.py index f13551c2..70e37277 100644 --- a/python3/pracmln/mln/mrfvars.py +++ b/python3/pracmln/mln/mrfvars.py @@ -25,9 +25,6 @@ from .errors import MRFValueException from .util import Interval -import array -#import inspect - class MRFVariable(object): """ Represents a (mutually exclusive) block of ground atoms. @@ -189,8 +186,6 @@ def itervalues(self, evidence=None): """ if type(evidence) is list: evidence = dict([(i, v) for i, v in enumerate(evidence)]) - if type(evidence) is array.array: - evidence = dict([(i, v) for i, v in enumerate(evidence)]) for tup in self._itervalues(evidence): yield self.valueidx(tup), tup @@ -308,9 +303,6 @@ def _itervalues(self, evidence=None): evidence = {} if len(self.gndatoms) != 1: raise Exception('Illegal number of ground atoms in the variable %s' % repr(self)) gndatom = self.gndatoms[0] - #curframe = inspect.currentframe() - #calframe = inspect.getouterframes(curframe, 2) - #print('evidence is {} of type {} - called by {}'.format(evidence, type(evidence), calframe[1][3])) if evidence.get(gndatom.idx) is not None and evidence.get(gndatom.idx) in (0,1): yield (evidence[gndatom.idx],) return From c7fd93c7e1cd95b43022cba8418b42353c05c58b Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sun, 22 Jul 2018 06:19:51 +0530 Subject: [PATCH 25/39] minor optimisations --- python3/pracmln/mln/inference/exact.pyx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/python3/pracmln/mln/inference/exact.pyx b/python3/pracmln/mln/inference/exact.pyx index dfba85d0..5d4c1f22 100644 --- a/python3/pracmln/mln/inference/exact.pyx +++ b/python3/pracmln/mln/inference/exact.pyx @@ -38,6 +38,9 @@ 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__) # this readonly global is for multiprocessing to exploit copy-on-write @@ -49,9 +52,10 @@ def eval_queries(world): ''' Evaluates the queries given a possible world. ''' - numerators = zeros(len(global_enumAsk.queries)) #numerators = [0] * len(global_enumAsk.queries) - denominator = 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 @@ -64,7 +68,7 @@ def eval_queries(world): continue else: return numerators, 0 - expsum += gf(world) * gf.weight + expsum += truth * gf.weight expsum = exp(expsum) # update numerators cdef int i From c064f697da280f9c89f668d3876ec0929c401ce3 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sun, 22 Jul 2018 05:55:30 +0530 Subject: [PATCH 26/39] cythonise callbyref containers --- python3/pracmln/mln/util.pxd | 2 ++ python3/pracmln/mln/{util.py => util.pyx} | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 python3/pracmln/mln/util.pxd rename python3/pracmln/mln/{util.py => util.pyx} (99%) 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 99% rename from python3/pracmln/mln/util.py rename to python3/pracmln/mln/util.pyx index a5dde217..966ef58d 100644 --- a/python3/pracmln/mln/util.py +++ b/python3/pracmln/mln/util.pyx @@ -175,13 +175,13 @@ 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 From 7ee1b0eabfd01e40c36d2c8027e6aabb934b26fc Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sun, 22 Jul 2018 05:57:34 +0530 Subject: [PATCH 27/39] cythonise superclass MRFVariable --- python3/pracmln/mln/mrfvars.pxd | 9 + .../pracmln/mln/{mrfvars.py => mrfvars.pyx} | 174 +++++++++--------- 2 files changed, 99 insertions(+), 84 deletions(-) create mode 100644 python3/pracmln/mln/mrfvars.pxd rename python3/pracmln/mln/{mrfvars.py => mrfvars.pyx} (95%) 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))) From 1f1dbe759125576704f23f07ec3e4d30e25eb703 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sun, 22 Jul 2018 05:58:13 +0530 Subject: [PATCH 28/39] complete iterworlds, restore functionality --- python3/pracmln/mln/mlnpreds.pxd | 3 + .../pracmln/mln/{mlnpreds.py => mlnpreds.pyx} | 104 ++++++++-------- python3/pracmln/mln/mrf.pyx | 116 ++++++++++-------- 3 files changed, 119 insertions(+), 104 deletions(-) create mode 100644 python3/pracmln/mln/mlnpreds.pxd rename python3/pracmln/mln/{mlnpreds.py => mlnpreds.pyx} (91%) 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.pyx b/python3/pracmln/mln/mrf.pyx index 8d8d4d71..367bbbeb 100644 --- a/python3/pracmln/mln/mrf.pyx +++ b/python3/pracmln/mln/mrf.pyx @@ -43,6 +43,8 @@ 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 @@ -58,7 +60,7 @@ cdef 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. ''' @@ -74,17 +76,17 @@ cdef class MRF(object): 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) @@ -102,11 +104,11 @@ cdef class MRF(object): @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 @@ -115,11 +117,11 @@ cdef class MRF(object): def evidence(self, evidence): self._evidence = evidence self.consistent() - + @property def predicates(self): return self.mln.predicates - + @property def hardformulas(self): ''' @@ -201,7 +203,7 @@ cdef class MRF(object): 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)) @@ -212,7 +214,7 @@ cdef class MRF(object): 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. @@ -246,12 +248,12 @@ cdef class MRF(object): # 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 @@ -271,19 +273,19 @@ cdef class MRF(object): 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. ''' @@ -291,11 +293,11 @@ cdef class MRF(object): 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: @@ -305,11 +307,11 @@ cdef class MRF(object): ''' 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)' @@ -343,9 +345,9 @@ cdef class MRF(object): ''' 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) @@ -353,14 +355,14 @@ cdef class MRF(object): 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 ''' @@ -379,16 +381,16 @@ cdef class MRF(object): 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. @@ -397,7 +399,7 @@ cdef class MRF(object): 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. @@ -407,12 +409,12 @@ cdef class MRF(object): 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): ''' @@ -435,10 +437,10 @@ cdef class MRF(object): 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 @@ -446,37 +448,47 @@ cdef class MRF(object): 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(): @@ -485,7 +497,7 @@ cdef class MRF(object): def iterallworlds(self): ''' Iterates over all possible worlds (without) taking evidence into account). - + :returns: a generator of possible worlds. ''' world = [None] * len(self.evidence) @@ -495,9 +507,9 @@ cdef class MRF(object): 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) @@ -514,7 +526,7 @@ cdef class MRF(object): ''' 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() @@ -545,12 +557,12 @@ cdef class MRF(object): 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 From 666937657fee2567aa04d7db2586fa78d4d6dba0 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 23 Jul 2018 03:27:01 +0530 Subject: [PATCH 29/39] truth --- python3/pracmln/logic/common.pxd | 27 +++++-- python3/pracmln/logic/common.pyx | 123 ++++++++++++++++++++----------- 2 files changed, 100 insertions(+), 50 deletions(-) diff --git a/python3/pracmln/logic/common.pxd b/python3/pracmln/logic/common.pxd index 5f448365..bdf0e262 100644 --- a/python3/pracmln/logic/common.pxd +++ b/python3/pracmln/logic/common.pxd @@ -13,10 +13,12 @@ cdef class ComplexFormula(Formula): pass cdef class Conjunction(ComplexFormula): - pass + cpdef int maxtruth(self, world) + cpdef int mintruth(self, world) cdef class Disjunction(ComplexFormula): - pass + cpdef int maxtruth(self, world) + cpdef int mintruth(self, world) cdef class Lit(Formula): cdef int _negated @@ -29,14 +31,26 @@ cdef class LitGroup(Formula): cdef class GroundLit(Formula): cdef GroundAtom _gndatom cdef int _negated - -cdef class GroundAtom: + 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 @@ -51,7 +65,10 @@ cdef class Exist(ComplexFormula): pass cdef class TrueFalse(Formula): - pass + cdef float _value + cpdef float truth(self, world=*) + cpdef mintruth(self, world=*) + cpdef maxtruth(self, world=*) cdef class NonLogicalConstraint(Constraint): pass diff --git a/python3/pracmln/logic/common.pyx b/python3/pracmln/logic/common.pyx index 1f047875..215e15d4 100644 --- a/python3/pracmln/logic/common.pyx +++ b/python3/pracmln/logic/common.pyx @@ -712,8 +712,8 @@ cdef class Conjunction(ComplexFormula): return ' \land '.join([('(%s)' % c.latex()) if isinstance(c, ComplexFormula) else c.latex() for c in self.children]) - def maxtruth(self, world): - mintruth = 1 + cpdef int maxtruth(self, world): + cdef int mintruth = 1 for c in self.children: truth = c.truth(world) if truth is None: continue @@ -721,8 +721,8 @@ cdef class Conjunction(ComplexFormula): return mintruth - def mintruth(self, world): - mintruth = 1 + cpdef int mintruth(self, world): + cdef int mintruth = 1 for c in self.children: truth = c.truth(world) if truth is None: return 0 @@ -823,8 +823,8 @@ cdef class Disjunction(ComplexFormula): def latex(self): return ' \lor '.join([('(%s)' % c.latex()) if isinstance(c, ComplexFormula) else c.latex() for c in self.children]) - def maxtruth(self, world): - maxtruth = 0 + cpdef int maxtruth(self, world): + cdef maxtruth = 0 for c in self.children: truth = c.truth(world) if truth is None: return 1 @@ -832,8 +832,8 @@ cdef class Disjunction(ComplexFormula): return maxtruth - def mintruth(self, world): - maxtruth = 0 + cpdef int mintruth(self, world): + cdef maxtruth = 0 for c in self.children: truth = c.truth(world) if truth is None: continue @@ -1281,23 +1281,35 @@ cdef class GroundLit(Formula): return self.gndatom.args - def truth(self, world): + #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) - if tv is None: return None - if self.negated: return (1. - tv) + #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 - def mintruth(self, world): + cpdef mintruth(self, list world): truth = self.truth(world) - if truth is None: return 0 - else: return truth + if truth is None: + return 0 + else: + return truth - def maxtruth(self, world): + cpdef maxtruth(self, list world): truth = self.truth(world) - if truth is None: return 1 - else: return truth + if truth is None: + return 1 + else: + return truth def __str__(self): @@ -1384,7 +1396,7 @@ cdef class GroundLit(Formula): def __ne__(self, other): return not self == other -cdef class GroundAtom: +cdef class GroundAtom(): """ Represents a ground atom. """ @@ -1427,23 +1439,28 @@ cdef class GroundAtom: @idx.setter def idx(self, idx): - self._idx = idx + #print('idx is of type {} and has value {}'.format(type(idx), idx)) + self._idx = idx - def truth(self, world): + cpdef truth(self, list world): return world[self.idx] - def mintruth(self, world): + cpdef mintruth(self, list world): truth = self.truth(world) - if truth is None: return 0 - else: return truth + if truth is None: + return 0 + else: + return truth - def maxtruth(self, world): + cpdef maxtruth(self, list world): truth = self.truth(world) - if truth is None: return 1 - else: return truth + if truth is None: + return 1 + else: + return truth def __repr__(self): @@ -1500,14 +1517,17 @@ cdef class Equality(ComplexFormula): @property def args(self): - return self._args + return [self._argsA, self._argsB] + #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 + raise Exception('Illegal number of arguments of equality: %d' % len(args)) + #self._args = args + self._argsA = args[0] + self._argsB = args[1] @property @@ -1586,23 +1606,29 @@ cdef class Equality(ComplexFormula): return prednames - def truth(self, world=None): + 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 - equals = 1 if (self.args[0] == self.args[1]) else 0 + 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 - 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 + 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): @@ -1962,22 +1988,29 @@ cdef class TrueFalse(Formula): Formula.__init__(self, mln, idx) self.value = truth - # Q(gsoc): where is this property used? + # 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) - def truth(self, world=None): + cpdef float truth(self, world=None): + #print('getting value type {} = {}'.format(type(self.value), self.value)) return self.value - def mintruth(self, world=None): + cpdef mintruth(self, world=None): return self.truth - def maxtruth(self, world=None): + cpdef maxtruth(self, world=None): return self.truth def invert(self): From 15a0091f572eac6b8f40aa473b6759e734ee27ec Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 23 Jul 2018 06:04:54 +0530 Subject: [PATCH 30/39] Cythonise FoL --- python3/pracmln/logic/fol.pxd | 76 +++++++ python3/pracmln/logic/fol.py | 400 ---------------------------------- python3/pracmln/logic/fol.pyx | 359 ++++++++++++++++++++++++++++++ 3 files changed, 435 insertions(+), 400 deletions(-) create mode 100644 python3/pracmln/logic/fol.pxd delete mode 100644 python3/pracmln/logic/fol.py create mode 100644 python3/pracmln/logic/fol.pyx diff --git a/python3/pracmln/logic/fol.pxd b/python3/pracmln/logic/fol.pxd new file mode 100644 index 00000000..c646865a --- /dev/null +++ b/python3/pracmln/logic/fol.pxd @@ -0,0 +1,76 @@ +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 + diff --git a/python3/pracmln/logic/fol.py b/python3/pracmln/logic/fol.py deleted file mode 100644 index b60c782b..00000000 --- a/python3/pracmln/logic/fol.py +++ /dev/null @@ -1,400 +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 .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 - - -class FirstOrderLogic(Logic): - """ - Factory class for first-order logic. - """ - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Constraint(Super_Constraint): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Formula(Super_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(Super_ComplexFormula, Formula): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Lit(Super_Lit, Formula): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Litgroup(Super_LitGroup, Formula): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundAtom(Super_GroundAtom): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundLit(Super_GroundLit, Formula): - - def noisyor(self, world): - truth = self(world) - if self.negated: truth = 1. - truth - return truth - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Disjunction(Super_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(Super_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(Super_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(Super_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(Super_Negation, ComplexFormula): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Exist(Super_Exist, ComplexFormula): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Equality(Super_Equality, ComplexFormula): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class TrueFalse(Super_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 -# the following attributes no longer exist (!): -# 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..17ebb036 --- /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) + + + + + + + + + + + + + + + + +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 From 5656082d3c99d1a55b97f7d3c4c35178ff9a651d Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 23 Jul 2018 06:05:15 +0530 Subject: [PATCH 31/39] Cythonise Fuzzy --- python3/pracmln/logic/fuzzy.pxd | 67 ++++++ python3/pracmln/logic/fuzzy.py | 361 -------------------------------- python3/pracmln/logic/fuzzy.pyx | 298 ++++++++++++++++++++++++++ 3 files changed, 365 insertions(+), 361 deletions(-) create mode 100644 python3/pracmln/logic/fuzzy.pxd delete mode 100644 python3/pracmln/logic/fuzzy.py create mode 100644 python3/pracmln/logic/fuzzy.pyx diff --git a/python3/pracmln/logic/fuzzy.pxd b/python3/pracmln/logic/fuzzy.pxd new file mode 100644 index 00000000..2c305f36 --- /dev/null +++ b/python3/pracmln/logic/fuzzy.pxd @@ -0,0 +1,67 @@ +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 + diff --git a/python3/pracmln/logic/fuzzy.py b/python3/pracmln/logic/fuzzy.py deleted file mode 100644 index 391a3ff7..00000000 --- a/python3/pracmln/logic/fuzzy.py +++ /dev/null @@ -1,361 +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 .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 - - -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(Super_Constraint): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Formula(Super_Formula): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class ComplexFormula(Super_Formula): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Lit(Super_Lit): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class LitGroup(Super_LitGroup): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundLit(Super_GroundLit): pass - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class GroundAtom(Super_GroundAtom): - - def truth(self, world): - return world[self.idx] - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Negation(Super_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, Super_TrueFalse): - return f.invert() - else: - return self.mln.logic.negation([f], mln=self.mln, idx=self.idx) - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Conjunction(Super_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, 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 - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Disjunction(Super_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, 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 - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class Implication(Super_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(Super_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(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) - - - # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # - - - class TrueFalse(Formula, Super_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(Super_Exist, Super_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 -# the following attributes no longer exist (!): -# 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..a8514b93 --- /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 + + + + + + + + + + + + + + + + +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 From d76b6bf6c89be306d221e58c4655efe518e5ba28 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 23 Jul 2018 06:07:37 +0530 Subject: [PATCH 32/39] Convert Fuzzy and FoL to extension type --- python3/pracmln/logic/common.pxd | 2 +- python3/pracmln/logic/fol.pxd | 3 +++ python3/pracmln/logic/fol.pyx | 2 +- python3/pracmln/logic/fuzzy.pxd | 3 +++ python3/pracmln/logic/fuzzy.pyx | 2 +- 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/python3/pracmln/logic/common.pxd b/python3/pracmln/logic/common.pxd index bdf0e262..50ed324a 100644 --- a/python3/pracmln/logic/common.pxd +++ b/python3/pracmln/logic/common.pxd @@ -80,7 +80,7 @@ cdef class GroundCountConstraint(NonLogicalConstraint): pass cdef class Logic: - pass + cdef dict __dict__ #cdef class Constraint(): # pass diff --git a/python3/pracmln/logic/fol.pxd b/python3/pracmln/logic/fol.pxd index c646865a..b11a5532 100644 --- a/python3/pracmln/logic/fol.pxd +++ b/python3/pracmln/logic/fol.pxd @@ -74,3 +74,6 @@ cdef class PriorConstraint(ProbabilityConstraint): cdef class PosteriorConstraint(ProbabilityConstraint): pass + +cdef class FirstOrderLogic(Logic): + pass diff --git a/python3/pracmln/logic/fol.pyx b/python3/pracmln/logic/fol.pyx index 17ebb036..d8634e2c 100644 --- a/python3/pracmln/logic/fol.pyx +++ b/python3/pracmln/logic/fol.pyx @@ -292,7 +292,7 @@ cdef class PosteriorConstraint(ProbabilityConstraint): -class FirstOrderLogic(Logic): +cdef class FirstOrderLogic(Logic): """ Factory class for first-order logic. """ diff --git a/python3/pracmln/logic/fuzzy.pxd b/python3/pracmln/logic/fuzzy.pxd index 2c305f36..d0650dda 100644 --- a/python3/pracmln/logic/fuzzy.pxd +++ b/python3/pracmln/logic/fuzzy.pxd @@ -65,3 +65,6 @@ cdef class TrueFalse(Super_TrueFalse): cdef class Exist(Super_Exist): pass + +cdef class FuzzyLogic(Logic): + pass diff --git a/python3/pracmln/logic/fuzzy.pyx b/python3/pracmln/logic/fuzzy.pyx index a8514b93..b3fe8dc6 100644 --- a/python3/pracmln/logic/fuzzy.pyx +++ b/python3/pracmln/logic/fuzzy.pyx @@ -199,7 +199,7 @@ cdef class Exist(Super_Exist): -class FuzzyLogic(Logic): +cdef class FuzzyLogic(Logic): """ Implementation of fuzzy logic for MLNs. """ From 7c9405ffa6744a0b870a2a6b2adbedd0eae278d3 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Mon, 30 Jul 2018 01:41:53 +0530 Subject: [PATCH 33/39] temporary: make evidence a list --- python3/pracmln/mln/mrf.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python3/pracmln/mln/mrf.pxd b/python3/pracmln/mln/mrf.pxd index 66df7a46..b2956797 100644 --- a/python3/pracmln/mln/mrf.pxd +++ b/python3/pracmln/mln/mrf.pxd @@ -4,5 +4,5 @@ from cpython cimport array cdef class MRF: cdef public MLN mln - #cdef array.array _evidence + cdef list _evidence#cdef array.array _evidence cdef dict __dict__ From d3629717e06e70d28760144b88199bb4851efc0a Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Wed, 8 Aug 2018 22:28:46 +0800 Subject: [PATCH 34/39] cythonize exact - soft_evidence_formula --- python3/pracmln/mln/.gitignore | 1 + python3/pracmln/mln/inference/exact.pyx | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/python3/pracmln/mln/.gitignore b/python3/pracmln/mln/.gitignore index c246e93e..2db71cff 100644 --- a/python3/pracmln/mln/.gitignore +++ b/python3/pracmln/mln/.gitignore @@ -1,3 +1,4 @@ +mini-test.py *.pyo *.c *.html diff --git a/python3/pracmln/mln/inference/exact.pyx b/python3/pracmln/mln/inference/exact.pyx index 5d4c1f22..a3dc391b 100644 --- a/python3/pracmln/mln/inference/exact.pyx +++ b/python3/pracmln/mln/inference/exact.pyx @@ -178,9 +178,17 @@ cdef class EnumerationAsk(Inference): result[str(q)] = p return result - def soft_evidence_formula(self, gf): + cpdef bint soft_evidence_formula(self, gf): #print('result={}'.format(gf.gndatoms())) - 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]) + # 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]) From c652f6eb0a2a3b6642e1d1d1f79bb4aa4ddd7df2 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Thu, 9 Aug 2018 04:17:44 +0800 Subject: [PATCH 35/39] intervals, arrays, etc --- python3/pracmln/logic/common.pxd | 5 +- python3/pracmln/logic/common.pyx | 7 +- python3/pracmln/mln/base.pxd | 1 + python3/pracmln/mln/base.pyx | 100 ++++++++++--------- python3/pracmln/mln/inference/exact.pyx | 35 ++++--- python3/pracmln/mln/mrf.pxd | 3 + python3/pracmln/mln/util.pyx | 124 ++++++++++++------------ 7 files changed, 142 insertions(+), 133 deletions(-) diff --git a/python3/pracmln/logic/common.pxd b/python3/pracmln/logic/common.pxd index 50ed324a..18dcb947 100644 --- a/python3/pracmln/logic/common.pxd +++ b/python3/pracmln/logic/common.pxd @@ -7,6 +7,7 @@ cdef class Constraint(): cdef class Formula(Constraint): cdef MLN _mln + #cdef public list children pass cdef class ComplexFormula(Formula): @@ -34,7 +35,7 @@ cdef class GroundLit(Formula): cpdef truth(self, list world) cpdef mintruth(self, list world) cpdef maxtruth(self, list world) - + cdef class GroundAtom(): cdef str _predname cdef MLN mln @@ -59,6 +60,7 @@ cdef class Biimplication(ComplexFormula): pass cdef class Negation(ComplexFormula): + cpdef truth(self, list world) pass cdef class Exist(ComplexFormula): @@ -83,4 +85,3 @@ cdef class Logic: cdef dict __dict__ #cdef class Constraint(): # pass - diff --git a/python3/pracmln/logic/common.pyx b/python3/pracmln/logic/common.pyx index 215e15d4..b939686b 100644 --- a/python3/pracmln/logic/common.pyx +++ b/python3/pracmln/logic/common.pyx @@ -146,7 +146,7 @@ cdef class Formula(Constraint): @mln.setter def mln(self, mln): - if hasattr(self, 'children'): + if hasattr(self, 'children'):# and self.children is not None: for child in self.children: child.mln = mln self._mln = mln @@ -158,7 +158,7 @@ cdef class Formula(Constraint): @weight.setter - def weight(self, w): + 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) @@ -1243,6 +1243,7 @@ cdef class GroundLit(Formula): 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 @@ -1785,7 +1786,7 @@ cdef class Negation(ComplexFormula): return r'\lnot (%s)' % self.children[0].latex() - def truth(self, world): + cpdef truth(self, list world): childValue = self.children[0].truth(world) if childValue is None: return None diff --git a/python3/pracmln/mln/base.pxd b/python3/pracmln/mln/base.pxd index 465f4dce..074a2fee 100644 --- a/python3/pracmln/mln/base.pxd +++ b/python3/pracmln/mln/base.pxd @@ -3,3 +3,4 @@ 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.pyx b/python3/pracmln/mln/base.pyx index 015dbed1..95e2c1c7 100644 --- a/python3/pracmln/mln/base.pyx +++ b/python3/pracmln/mln/base.pyx @@ -65,10 +65,10 @@ if platform.architecture()[0] == '32bit': 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 @@ -105,16 +105,16 @@ cdef class MLN(object): @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): @@ -134,7 +134,7 @@ cdef class MLN(object): @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] @@ -168,25 +168,25 @@ cdef class MLN(object): 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) @@ -196,29 +196,29 @@ cdef class MLN(object): 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) @@ -237,7 +237,7 @@ cdef class MLN(object): 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 @@ -273,15 +273,15 @@ cdef class MLN(object): 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`. ''' @@ -297,9 +297,9 @@ cdef class MLN(object): ''' 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. @@ -354,9 +354,9 @@ cdef class MLN(object): 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. ''' @@ -369,7 +369,7 @@ cdef class MLN(object): 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. @@ -386,7 +386,7 @@ cdef class MLN(object): 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 @@ -397,11 +397,11 @@ cdef class MLN(object): ''' 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.') @@ -445,7 +445,7 @@ cdef class MLN(object): 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) @@ -460,29 +460,29 @@ cdef class MLN(object): 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 @@ -527,12 +527,12 @@ cdef class MLN(object): 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`. @@ -547,7 +547,7 @@ cdef class MLN(object): 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): @@ -565,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) @@ -615,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 @@ -639,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 @@ -670,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] = [] @@ -697,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 @@ -720,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 @@ -736,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) @@ -785,11 +785,9 @@ def parse_mln(text, searchpaths=['.'], projectpath=None, logic='FirstOrderLogic' 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/inference/exact.pyx b/python3/pracmln/mln/inference/exact.pyx index a3dc391b..73b7b1d0 100644 --- a/python3/pracmln/mln/inference/exact.pyx +++ b/python3/pracmln/mln/inference/exact.pyx @@ -36,7 +36,7 @@ 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 numpy import zeros from cpython cimport array import array @@ -48,7 +48,7 @@ logger = logs.getlogger(__name__) global_enumAsk = None -def eval_queries(world): +cpdef eval_queries(world): ''' Evaluates the queries given a possible world. ''' @@ -62,7 +62,9 @@ def eval_queries(world): 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 @@ -96,7 +98,7 @@ cdef 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 @@ -116,21 +118,22 @@ cdef class EnumerationAsk(Inference): 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 = zeros(len(self.queries))#numerators = [0.0 for i in range(len(self.queries))] + 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)) - cdef int k = 0 # redundant variable ? + 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: @@ -140,9 +143,9 @@ cdef class EnumerationAsk(Inference): 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 + #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() except Exception as e: @@ -157,9 +160,10 @@ cdef class EnumerationAsk(Inference): # compute exp. sum of weights for this world num, denom = eval_queries(world) denominator += denom - numerators += num + #numerators += num #for i, _ in enumerate(self.queries): - # numerators[i] += num[i] + for i in range(len(self.queries)): + numerators[i] += num[i] k += 1 if self.verbose: bar.update(float(k) / worlds) @@ -171,8 +175,9 @@ cdef class EnumerationAsk(Inference): raise SatisfiabilityException( 'MLN is unsatisfiable. All probability masses returned 0.') # normalize answers - dist = numerators / denominator - #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 diff --git a/python3/pracmln/mln/mrf.pxd b/python3/pracmln/mln/mrf.pxd index b2956797..3029e39f 100644 --- a/python3/pracmln/mln/mrf.pxd +++ b/python3/pracmln/mln/mrf.pxd @@ -5,4 +5,7 @@ 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/util.pyx b/python3/pracmln/mln/util.pyx index 966ef58d..367e008e 100644 --- a/python3/pracmln/mln/util.pyx +++ 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 stripComments(text): 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) @@ -180,16 +180,16 @@ 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, 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 @@ class 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 @@ class StopWatch(object): else: tag.starttime = now self.tags[label] = tag - - + + def finish(self, label=None): now = time.time() if label is None: @@ -380,15 +380,15 @@ class StopWatch(object): 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 deprecated(func): 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 @@ class edict(dict): 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 @@ if __name__ == '__main__': # print d - 1 # print d - {'hi': 'bla'} # print d -# +# From e35ef87b109ad0efa6cb3bde1ae5a507cf6f9366 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Thu, 9 Aug 2018 04:54:19 +0800 Subject: [PATCH 36/39] gndatoms, __str__ --- python3/pracmln/logic/common.pxd | 1 + python3/pracmln/logic/common.pyx | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/python3/pracmln/logic/common.pxd b/python3/pracmln/logic/common.pxd index 18dcb947..db15b464 100644 --- a/python3/pracmln/logic/common.pxd +++ b/python3/pracmln/logic/common.pxd @@ -4,6 +4,7 @@ from ..mln.base cimport MLN cdef class Constraint(): cdef dict __dict__ + cpdef gndatoms(self, l=*) cdef class Formula(Constraint): cdef MLN _mln diff --git a/python3/pracmln/logic/common.pyx b/python3/pracmln/logic/common.pyx index b939686b..af8d2e50 100644 --- a/python3/pracmln/logic/common.pyx +++ b/python3/pracmln/logic/common.pyx @@ -103,7 +103,7 @@ cdef class Constraint(): def idx_gndatoms(self, l=None): raise Exception("%s does not implement idxgndatoms" % str(type(self))) - def gndatoms(self, l=None): + cpdef gndatoms(self, l=None): raise Exception("%s does not implement gndatoms" % str(type(self))) cdef class Formula(Constraint): @@ -195,7 +195,7 @@ cdef class Formula(Constraint): return l # Q(gsoc): needs speedup ... - def gndatoms(self, l=None): + cpdef gndatoms(self, l=None): """ Returns a list of all ground atoms that are contained in this formula. @@ -1347,7 +1347,7 @@ cdef class GroundLit(Formula): return l - def gndatoms(self, l=None): + 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))) @@ -1499,7 +1499,8 @@ cdef class GroundAtom(): def __eq__(self, other): - return str(self) == str(other) + return (self.predname == other.predname) and (self.args == other.args) + #return str(self) == str(other) def __ne__(self, other): return not self == other From 95c6e1aead5426078843cc255faac76252905008 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Thu, 9 Aug 2018 16:13:38 +0800 Subject: [PATCH 37/39] cythonise default grounding --- python3/build-cython.sh | 7 +- python3/pracmln/mln/grounding/default.pxd | 12 +++ .../mln/grounding/{default.py => default.pyx} | 86 ++++++++----------- python3/pracmln/mln/grounding/setup.py | 6 ++ 4 files changed, 60 insertions(+), 51 deletions(-) create mode 100644 python3/pracmln/mln/grounding/default.pxd rename python3/pracmln/mln/grounding/{default.py => default.pyx} (92%) create mode 100644 python3/pracmln/mln/grounding/setup.py diff --git a/python3/build-cython.sh b/python3/build-cython.sh index 2585c5cf..ad3ea36b 100644 --- a/python3/build-cython.sh +++ b/python3/build-cython.sh @@ -9,8 +9,13 @@ cd ../mln/ python3 setup.py build_ext --inplace echo "=================================================" +echo "====================GROUNDING====================" +cd grounding/ +python3 setup.py build_ext --inplace +echo "=================================================" + echo "====================INFERENCE====================" -cd inference/ +cd ../inference/ python3 setup.py build_ext --inplace echo "=================================================" 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/setup.py b/python3/pracmln/mln/grounding/setup.py new file mode 100644 index 00000000..6e967ce4 --- /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}) +) From a4f156a4cd8c0321b6a4b4bd3ad692f10ad17fbb Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sat, 11 Aug 2018 18:03:44 +0800 Subject: [PATCH 38/39] revert temporary edits --- python3/pracmln/__init__.py | 2 -- python3/pracmln/logic/setup.py | 2 +- python3/pracmln/mln/grounding/setup.py | 2 +- python3/pracmln/mln/inference/setup.py | 2 +- python3/pracmln/mln/setup.py | 2 +- 5 files changed, 4 insertions(+), 6 deletions(-) diff --git a/python3/pracmln/__init__.py b/python3/pracmln/__init__.py index e33e232e..f1a7fc56 100644 --- a/python3/pracmln/__init__.py +++ b/python3/pracmln/__init__.py @@ -30,5 +30,3 @@ from .mlnlearn import EVIDENCE_PREDS from .utils.project import mlnpath from .utils.project import PRACMLNConfig - -print('[Cython]') diff --git a/python3/pracmln/logic/setup.py b/python3/pracmln/logic/setup.py index 6e967ce4..68e8a682 100644 --- a/python3/pracmln/logic/setup.py +++ b/python3/pracmln/logic/setup.py @@ -2,5 +2,5 @@ from Cython.Build import cythonize setup( - ext_modules=cythonize("*.pyx", compiler_directives={'profile': True}) + ext_modules=cythonize("*.pyx")#, compiler_directives={'profile': True}) ) diff --git a/python3/pracmln/mln/grounding/setup.py b/python3/pracmln/mln/grounding/setup.py index 6e967ce4..68e8a682 100644 --- a/python3/pracmln/mln/grounding/setup.py +++ b/python3/pracmln/mln/grounding/setup.py @@ -2,5 +2,5 @@ from Cython.Build import cythonize setup( - ext_modules=cythonize("*.pyx", compiler_directives={'profile': True}) + ext_modules=cythonize("*.pyx")#, compiler_directives={'profile': True}) ) diff --git a/python3/pracmln/mln/inference/setup.py b/python3/pracmln/mln/inference/setup.py index 36c9a65e..da01a8d0 100644 --- a/python3/pracmln/mln/inference/setup.py +++ b/python3/pracmln/mln/inference/setup.py @@ -3,5 +3,5 @@ from Cython.Build import cythonize setup( - ext_modules=cythonize("*.pyx", compiler_directives={'profile': True}) + ext_modules=cythonize("*.pyx")#, compiler_directives={'profile': True}) ) diff --git a/python3/pracmln/mln/setup.py b/python3/pracmln/mln/setup.py index 36c9a65e..da01a8d0 100644 --- a/python3/pracmln/mln/setup.py +++ b/python3/pracmln/mln/setup.py @@ -3,5 +3,5 @@ from Cython.Build import cythonize setup( - ext_modules=cythonize("*.pyx", compiler_directives={'profile': True}) + ext_modules=cythonize("*.pyx")#, compiler_directives={'profile': True}) ) From 8e92f8667072bd109cd4ef86f5624cf1d27cd416 Mon Sep 17 00:00:00 2001 From: Kaivalya Rawal Date: Sat, 11 Aug 2018 20:57:59 +0800 Subject: [PATCH 39/39] add links --- .gitignore | 2 -- python3/pracmln/logic/common.cpython-35m-x86_64-linux-gnu.so | 1 + python3/pracmln/logic/fol.cpython-35m-x86_64-linux-gnu.so | 1 + python3/pracmln/logic/fuzzy.cpython-35m-x86_64-linux-gnu.so | 1 + python3/pracmln/mln/base.cpython-35m-x86_64-linux-gnu.so | 1 + python3/pracmln/mln/grounding/.gitignore | 5 +++++ .../mln/grounding/default.cpython-35m-x86_64-linux-gnu.so | 1 + .../mln/inference/exact.cpython-35m-x86_64-linux-gnu.so | 1 + .../mln/inference/gibbs.cpython-35m-x86_64-linux-gnu.so | 1 + .../mln/inference/infer.cpython-35m-x86_64-linux-gnu.so | 1 + .../mln/inference/ipfpm.cpython-35m-x86_64-linux-gnu.so | 1 + .../mln/inference/maxwalk.cpython-35m-x86_64-linux-gnu.so | 1 + .../mln/inference/mcmc.cpython-35m-x86_64-linux-gnu.so | 1 + .../mln/inference/mcsat.cpython-35m-x86_64-linux-gnu.so | 1 + .../mln/inference/wcspinfer.cpython-35m-x86_64-linux-gnu.so | 1 + python3/pracmln/mln/mlnpreds.cpython-35m-x86_64-linux-gnu.so | 1 + python3/pracmln/mln/mrf.cpython-35m-x86_64-linux-gnu.so | 1 + python3/pracmln/mln/mrfvars.cpython-35m-x86_64-linux-gnu.so | 1 + python3/pracmln/mln/util.cpython-35m-x86_64-linux-gnu.so | 1 + 19 files changed, 22 insertions(+), 2 deletions(-) create mode 120000 python3/pracmln/logic/common.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/logic/fol.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/logic/fuzzy.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/base.cpython-35m-x86_64-linux-gnu.so create mode 100644 python3/pracmln/mln/grounding/.gitignore create mode 120000 python3/pracmln/mln/grounding/default.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/inference/exact.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/inference/gibbs.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/inference/infer.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/inference/ipfpm.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/inference/maxwalk.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/inference/mcmc.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/inference/mcsat.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/inference/wcspinfer.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/mlnpreds.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/mrf.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/mrfvars.cpython-35m-x86_64-linux-gnu.so create mode 120000 python3/pracmln/mln/util.cpython-35m-x86_64-linux-gnu.so 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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