def _process_rule(self, a, b): # right part first if isinstance(b, Logic): # a -> b & c --> a -> b ; a -> c # (?) FIXME this is only correct when b & c != null ! if b.op == '&': for barg in b.args: self.process_rule(a, barg) # a -> b | c --> !b & !c -> !a # --> a & !b -> c & !b # --> a & !c -> b & !c # # NB: the last two rewrites add 1 term, so the rule *grows* in size. # NB: without catching terminating conditions this could continue infinitely elif b.op == '|': # detect tautology first if not isinstance(a, Logic): # Atom # tautology: a -> a|c|... if a in b.args: raise TautologyDetected(a,b, 'a -> a|c|...') self.process_rule(And(*[Not(barg) for barg in b.args]), Not(a)) for bidx in range(len(b.args)): barg = b.args[bidx] brest= b.args[:bidx] + b.args[bidx+1:] self.process_rule(And(a, Not(barg)), And(b.__class__(*brest), Not(barg))) else: raise ValueError('unknown b.op %r' % b.op) # left part elif isinstance(a, Logic): # a & b -> c --> IRREDUCIBLE CASE -- WE STORE IT AS IS # (this will be the basis of beta-network) if a.op == '&': assert not isinstance(b, Logic) if b in a.args: raise TautologyDetected(a,b, 'a & b -> a') self.proved_rules.append((a,b)) # XXX NOTE at present we ignore !c -> !a | !b elif a.op == '|': if b in a.args: raise TautologyDetected(a,b, 'a | b -> a') for aarg in a.args: self.process_rule(aarg, b) else: raise ValueError('unknown a.op %r' % a.op) else: # both `a` and `b` are atoms na, nb = name_not(a), name_not(b) self.proved_rules.append((a,b)) # a -> b self.proved_rules.append((nb,na)) # !b -> !a
def _process_rule(self, a, b): # right part first if isinstance(b, Logic): # a -> b & c --> a -> b ; a -> c # (?) FIXME this is only correct when b & c != null ! if b.op == '&': for barg in b.args: self.process_rule(a, barg) # a -> b | c --> !b & !c -> !a # --> a & !b -> c & !b # --> a & !c -> b & !c # # NB: the last two rewrites add 1 term, so the rule *grows* in size. # NB: without catching terminating conditions this could continue infinitely elif b.op == '|': # detect tautology first if not isinstance(a, Logic): # Atom # tautology: a -> a|c|... if a in b.args: raise TautologyDetected(a, b, 'a -> a|c|...') self.process_rule(And(*[Not(barg) for barg in b.args]), Not(a)) for bidx in range(len(b.args)): barg = b.args[bidx] brest = b.args[:bidx] + b.args[bidx + 1:] self.process_rule(And(a, Not(barg)), And(b.__class__(*brest), Not(barg))) else: raise ValueError('unknown b.op %r' % b.op) # left part elif isinstance(a, Logic): # a & b -> c --> IRREDUCIBLE CASE -- WE STORE IT AS IS # (this will be the basis of beta-network) if a.op == '&': assert not isinstance(b, Logic) if b in a.args: raise TautologyDetected(a, b, 'a & b -> a') self.proved_rules.append((a, b)) # XXX NOTE at present we ignore !c -> !a | !b elif a.op == '|': if b in a.args: raise TautologyDetected(a, b, 'a | b -> a') for aarg in a.args: self.process_rule(aarg, b) else: raise ValueError('unknown a.op %r' % a.op) else: # both `a` and `b` are atoms na, nb = name_not(a), name_not(b) self.proved_rules.append((a, b)) # a -> b self.proved_rules.append((nb, na)) # !b -> !a
def deduce_alpha_implications(implications): """deduce all implications Description by example ---------------------- given set of logic rules: a -> b b -> c we deduce all possible rules: a -> b, c b -> c implications: [] of (a,b) return: {} of a -> [b, c, ...] """ res = {} for a, b in implications: if a == b: continue # skip a->a cyclic input I = res.setdefault(a, []) list_populate(I, b) # UC: ------------------------- # | | # v | # a='rat' -> b='real' ==> (a_='int') -> 'real' for a_ in res: ra_ = res[a_] if a in ra_: list_populate(ra_, b, skipif=a_) # UC: # a='pos' -> b='real' && (already have b='real' -> 'complex') # || # vv # a='pos' -> 'complex' if b in res: ra = res[a] for b_ in res[b]: list_populate(ra, b_, skipif=a) # let's see if the result is consistent for a, impl in res.iteritems(): na = name_not(a) if na in impl: raise ValueError('implications are inconsistent: %s -> %s %s' % (a, na, impl)) return res
def deduce_alpha_implications(implications): """deduce all implications Description by example ---------------------- given set of logic rules: a -> b b -> c we deduce all possible rules: a -> b, c b -> c implications: [] of (a,b) return: {} of a -> [b, c, ...] """ res = {} for a,b in implications: if a == b: continue # skip a->a cyclic input I = res.setdefault(a,[]) list_populate(I,b) # UC: ------------------------- # | | # v | # a='rat' -> b='real' ==> (a_='int') -> 'real' for a_ in res: ra_ = res[a_] if a in ra_: list_populate(ra_, b, skipif=a_) # UC: # a='pos' -> b='real' && (already have b='real' -> 'complex') # || # vv # a='pos' -> 'complex' if b in res: ra = res[a] for b_ in res[b]: list_populate(ra, b_, skipif=a) # let's see if the result is consistent for a, impl in res.iteritems(): na = name_not(a) if na in impl: raise ValueError('implications are inconsistent: %s -> %s %s' % (a, na, impl)) return res
def deduce_alpha_implications(implications): """deduce all implications Description by example ---------------------- given set of logic rules: a -> b b -> c we deduce all possible rules: a -> b, c b -> c implications: [] of (a,b) return: {} of a -> set([b, c, ...]) """ res = defaultdict(set) for a, b in implications: if a == b: continue # skip a->a cyclic input res[a].add(b) # (x >> a) & (a >> b) => x >> b for fact in res: implied = res[fact] if a in implied: implied.add(b) # (a >> b) & (b >> x) => a >> x if b in res: res[a] |= res[b] # Clean up tautologies and check consistency for a, impl in res.iteritems(): impl.discard(a) na = name_not(a) if na in impl: raise ValueError('implications are inconsistent: %s -> %s %s' % (a, na, impl)) return res
def deduce_all_facts(self, facts, base=None): """Deduce all facts from known facts ({} or [] of (k,v)) ********************************************* * This is the workhorse, so keep it *fast*. * ********************************************* base -- previously known facts (must be: fully deduced set) attention: base is modified *in place* /optional/ providing `base` could be needed for performance reasons -- we don't want to spend most of the time just re-deducing base from base (e.g. #base=50, #facts=2) """ # keep frequently used attributes locally, so we'll avoid extra # attribute access overhead rels = self.rels beta_rules = self.beta_rules if base is not None: new_facts = base else: new_facts = {} # XXX better name ? def x_new_facts(keys, v): for k in keys: if k in new_facts and new_facts[k] is not None: assert new_facts[k] == v, \ ('inconsistency between facts', new_facts, k, v) continue else: new_facts[k] = v if type(facts) is dict: fseq = facts.iteritems() else: fseq = facts while True: beta_maytrigger = set() # --- alpha chains --- #print '**' for k,v in fseq: #print '--' # first, convert name to be not a not-name if k[:1] == '!': k = name_not(k) v = fuzzy_not(v) #new_fact(k, v) if k in new_facts: assert new_facts[k] is None or new_facts[k] == v, \ ('inconsistency between facts', new_facts, k, v) # performance-wise it is important not to fire implied rules # for already-seen fact -- we already did them all. continue else: new_facts[k] = v # some known fact -- let's follow its implications if v is not None: # lookup routing tables try: tt, tf, tbeta, ft, ff, fbeta = rels[k] except KeyError: pass else: # Now we have routing tables with *all* the needed # implications for this k. This means we do not have to # process each implications recursively! # XXX this ^^^ is true only for alpha chains # k=T if v: x_new_facts(tt, True) # k -> i x_new_facts(tf, False) # k -> !i beta_maytrigger.update(tbeta) # k=F else: x_new_facts(ft, True) # !k -> i x_new_facts(ff, False) # !k -> !i beta_maytrigger.update(fbeta) # --- beta chains --- # if no beta-rules may trigger -- it's an end-of-story if not beta_maytrigger: break #print '(β) MayTrigger: %s' % beta_maytrigger fseq = [] # XXX this is dumb proof-of-concept trigger -- we'll need to optimize it # let's see which beta-rules to trigger for bidx in beta_maytrigger: bcond,bimpl = beta_rules[bidx] # let's see whether bcond is satisfied for bk in bcond.args: try: if bk[:1] == '!': bv = fuzzy_not(new_facts[bk[1:]]) else: bv = new_facts[bk] except KeyError: break # fact not found -- bcond not satisfied # one of bcond's condition does not hold if not bv: break else: # all of bcond's condition hold -- let's fire this beta rule #print '(β) Trigger #%i (%s)' % (bidx, bimpl) if bimpl[:1] == '!': bimpl = bimpl[1:] v = False else: v = True fseq.append( (bimpl,v) ) return new_facts
def __init__(self, rules): """Compile rules into internal lookup tables""" if isinstance(rules, basestring): rules = rules.splitlines() # --- parse and process rules --- P = Prover() for rule in rules: # XXX `a` is hardcoded to be always atom a, op, b = rule.split(None, 2) a = Logic.fromstring(a) b = Logic.fromstring(b) if op == '->': P.process_rule(a, b) elif op == '==': P.process_rule(a, b) P.process_rule(b, a) else: raise ValueError('unknown op %r' % op) #P.print_proved('RULES') #P.print_beta('BETA-RULES') # --- build deduction networks --- # deduce alpha implications impl_a = deduce_alpha_implications(P.rules_alpha) # now: # - apply beta rules to alpha chains (static extension), and # - further associate beta rules to alpha chain (for inference at runtime) impl_ab = apply_beta_to_alpha_route(impl_a, P.rules_beta) if 0: print '\n --- ALPHA-CHAINS (I) ---' for a,b in impl_a.iteritems(): print '%s\t-> α(%2i):%s' % (a,len(b),b) print ' - - - - - ' print if 0: print '\n --- ALPHA-CHAINS (II) ---' for a,(b,bb) in impl_ab.iteritems(): print '%s\t-> α(%2i):%s β(%s)' % (a,len(b),b, ' '.join(str(x) for x in bb)) print ' - - - - - ' print # extract defined fact names self.defined_facts = set() for k in impl_ab.keys(): if k[:1] == '!': k = k[1:] self.defined_facts.add(k) #print 'defined facts: (%2i) %s' % (len(self.defined_facts), self.defined_facts) # now split each rule into four logic chains # (removing betaidxs from impl_ab view) (XXX is this needed?) impl_ab_ = dict( (k,impl) for k, (impl,betaidxs) in impl_ab.iteritems()) rel_tt, rel_tf, rel_ft, rel_ff = split_rules_tt_tf_ft_ff(impl_ab_) # XXX merge me with split_rules_tt_tf_ft_ff ? rel_tbeta = {} rel_fbeta = {} for k, (impl,betaidxs) in impl_ab.iteritems(): if k[:1] == '!': rel_xbeta = rel_fbeta k = name_not(k) else: rel_xbeta = rel_tbeta rel_xbeta[k] = betaidxs self.rel_tt = rel_tt self.rel_tf = rel_tf self.rel_tbeta = rel_tbeta self.rel_ff = rel_ff self.rel_ft = rel_ft self.rel_fbeta = rel_fbeta self.beta_rules = P.rules_beta # build rels (forward chains) K = set (rel_tt.keys()) K.update(rel_tf.keys()) K.update(rel_ff.keys()) K.update(rel_ft.keys()) rels = {} empty= () for k in K: tt = rel_tt.get(k,empty) tf = rel_tf.get(k,empty) ft = rel_ft.get(k,empty) ff = rel_ff.get(k,empty) tbeta = rel_tbeta.get(k,empty) fbeta = rel_fbeta.get(k,empty) rels[k] = tt, tf, tbeta, ft, ff, fbeta self.rels = rels # build prereq (backward chains) prereq = {} for rel in [rel_tt, rel_tf, rel_ff, rel_ft]: rel_prereq = rules_2prereq(rel) for k,pitems in rel_prereq.iteritems(): kp = prereq.setdefault(k,[]) for p in pitems: list_populate(kp, p) self.prereq = prereq
def deduce_alpha_implications(implications): """deduce all implications Description by example ---------------------- given set of logic rules: a -> b b -> c we deduce all possible rules: a -> b, c b -> c implications: [] of (a,b) return: {} of a -> [b, c, ...] """ res = {} # a -> [] of implications(a) # NOTE: this could be optimized with e.g. toposort (?), but we need this # only at FactRules creation time (i.e. only once), so there is no demand # in further optimising this. for a,b in implications: if a==b: continue # skip a->a cyclic input I = res.setdefault(a,[]) list_populate(I,b) # UC: ------------------------- # | | # v | # a='rat' -> b='real' ==> (a_='int') -> 'real' for a_ in res: ra_ = res[a_] if a in ra_: list_populate(ra_, b, skipif=a_) # UC: # a='pos' -> b='real' && (already have b='real' -> 'complex') # || # vv # a='pos' -> 'complex' if b in res: ra = res[a] for b_ in res[b]: list_populate(ra, b_, skipif=a) #print 'D:', res # let's see if the result is consistent for a, impl in res.iteritems(): na = name_not(a) if na in impl: raise ValueError('implications are inconsistent: %s -> %s %s' % (a, na, impl)) return res
def deduce_all_facts(self, facts, base=None): """Deduce all facts from known facts ({} or [] of (k,v)) ********************************************* * This is the workhorse, so keep it *fast*. * ********************************************* base -- previously known facts (must be: fully deduced set) attention: base is modified *in place* /optional/ providing `base` could be needed for performance reasons -- we don't want to spend most of the time just re-deducing base from base (e.g. #base=50, #facts=2) """ # keep frequently used attributes locally, so we'll avoid extra # attribute access overhead rels = self.rels beta_rules = self.beta_rules if base is None: new_facts = {} else: new_facts = base def x_new_facts(keys, v): for k in keys: if k in new_facts and new_facts[k] is not None: assert new_facts[k] == v, \ ('inconsistency between facts', new_facts, k, v) continue else: new_facts[k] = v if type(facts) is dict: fseq = facts.iteritems() else: fseq = facts while True: beta_maytrigger = set() # --- alpha chains --- for k, v in fseq: # first, convert name to be not a not-name if k[:1] == '!': k = name_not(k) v = fuzzy_not(v) #new_fact(k, v) if k in new_facts: assert new_facts[k] is None or new_facts[k] == v, \ ('inconsistency between facts', new_facts, k, v) # performance-wise it is important not to fire implied rules # for already-seen fact -- we already did them all. continue else: new_facts[k] = v # some known fact -- let's follow its implications if v is not None: # lookup routing tables try: tt, tf, tbeta, ft, ff, fbeta = rels[k] except KeyError: pass else: # Now we have routing tables with *all* the needed # implications for this k. This means we do not have to # process each implications recursively! # XXX this ^^^ is true only for alpha chains # k=T if v: x_new_facts(tt, True) # k -> i x_new_facts(tf, False) # k -> !i beta_maytrigger.update(tbeta) # k=F else: x_new_facts(ft, True) # !k -> i x_new_facts(ff, False) # !k -> !i beta_maytrigger.update(fbeta) # --- beta chains --- # if no beta-rules may trigger -- it's an end-of-story if not beta_maytrigger: break fseq = [] # let's see which beta-rules to trigger for bidx in beta_maytrigger: bcond, bimpl = beta_rules[bidx] # let's see whether bcond is satisfied for bk in bcond.args: try: if bk[:1] == '!': bv = fuzzy_not(new_facts[bk[1:]]) else: bv = new_facts[bk] except KeyError: break # fact not found -- bcond not satisfied # one of bcond's condition does not hold if not bv: break else: # all of bcond's condition hold -- let's fire this beta rule if bimpl[:1] == '!': bimpl = bimpl[1:] v = False else: v = True fseq.append((bimpl, v)) return new_facts
def __init__(self, rules): """Compile rules into internal lookup tables""" if isinstance(rules, basestring): rules = rules.splitlines() # --- parse and process rules --- P = Prover() for rule in rules: # XXX `a` is hardcoded to be always atom a, op, b = rule.split(None, 2) a = Logic.fromstring(a) b = Logic.fromstring(b) if op == '->': P.process_rule(a, b) elif op == '==': P.process_rule(a, b) P.process_rule(b, a) else: raise ValueError('unknown op %r' % op) # --- build deduction networks --- # deduce alpha implications impl_a = deduce_alpha_implications(P.rules_alpha) # now: # - apply beta rules to alpha chains (static extension), and # - further associate beta rules to alpha chain (for inference at runtime) impl_ab = apply_beta_to_alpha_route(impl_a, P.rules_beta) # extract defined fact names self.defined_facts = set() for k in impl_ab.keys(): if k[:1] == '!': k = k[1:] self.defined_facts.add(k) # now split each rule into four logic chains # (removing betaidxs from impl_ab view) (XXX is this needed?) impl_ab_ = dict( (k, impl) for k, (impl, betaidxs) in impl_ab.iteritems()) rel_tt, rel_tf, rel_ft, rel_ff = split_rules_tt_tf_ft_ff(impl_ab_) rel_tbeta = {} rel_fbeta = {} for k, (impl, betaidxs) in impl_ab.iteritems(): if k[:1] == '!': rel_xbeta = rel_fbeta k = name_not(k) else: rel_xbeta = rel_tbeta rel_xbeta[k] = betaidxs self.rel_tt = rel_tt self.rel_tf = rel_tf self.rel_tbeta = rel_tbeta self.rel_ff = rel_ff self.rel_ft = rel_ft self.rel_fbeta = rel_fbeta self.beta_rules = P.rules_beta # build rels (forward chains) K = set(rel_tt.keys()) K.update(rel_tf.keys()) K.update(rel_ff.keys()) K.update(rel_ft.keys()) rels = {} empty = () for k in K: tt = rel_tt.get(k, empty) tf = rel_tf.get(k, empty) ft = rel_ft.get(k, empty) ff = rel_ff.get(k, empty) tbeta = rel_tbeta.get(k, empty) fbeta = rel_fbeta.get(k, empty) rels[k] = tt, tf, tbeta, ft, ff, fbeta self.rels = rels # build prereq (backward chains) prereq = {} for rel in [rel_tt, rel_tf, rel_ff, rel_ft]: rel_prereq = rules_2prereq(rel) for k, pitems in rel_prereq.iteritems(): kp = prereq.setdefault(k, []) for p in pitems: list_populate(kp, p) self.prereq = prereq
def __init__(self, rules): """Compile rules into internal lookup tables""" if isinstance(rules, basestring): rules = rules.splitlines() # --- parse and process rules --- P = Prover() for rule in rules: # XXX `a` is hardcoded to be always atom a, op, b = rule.split(None, 2) a = Logic.fromstring(a) b = Logic.fromstring(b) if op == '->': P.process_rule(a, b) elif op == '==': P.process_rule(a, b) P.process_rule(b, a) else: raise ValueError('unknown op %r' % op) # --- build deduction networks --- # deduce alpha implications impl_a = deduce_alpha_implications(P.rules_alpha) # now: # - apply beta rules to alpha chains (static extension), and # - further associate beta rules to alpha chain (for inference at runtime) impl_ab = apply_beta_to_alpha_route(impl_a, P.rules_beta) # extract defined fact names self.defined_facts = set() for k in impl_ab.keys(): if k[:1] == '!': k = k[1:] self.defined_facts.add(k) # now split each rule into four logic chains # (removing betaidxs from impl_ab view) (XXX is this needed?) impl_ab_ = dict( (k,impl) for k, (impl,betaidxs) in impl_ab.iteritems()) rel_tt, rel_tf, rel_ft, rel_ff = split_rules_tt_tf_ft_ff(impl_ab_) rel_tbeta = {} rel_fbeta = {} for k, (impl,betaidxs) in impl_ab.iteritems(): if k[:1] == '!': rel_xbeta = rel_fbeta k = name_not(k) else: rel_xbeta = rel_tbeta rel_xbeta[k] = betaidxs self.rel_tt = rel_tt self.rel_tf = rel_tf self.rel_tbeta = rel_tbeta self.rel_ff = rel_ff self.rel_ft = rel_ft self.rel_fbeta = rel_fbeta self.beta_rules = P.rules_beta # build rels (forward chains) K = set (rel_tt.keys()) K.update(rel_tf.keys()) K.update(rel_ff.keys()) K.update(rel_ft.keys()) rels = {} empty= () for k in K: tt = rel_tt[k] tf = rel_tf[k] ft = rel_ft[k] ff = rel_ff[k] tbeta = rel_tbeta.get(k,empty) fbeta = rel_fbeta.get(k,empty) rels[k] = tt, tf, tbeta, ft, ff, fbeta self.rels = rels # build prereq (backward chains) prereq = defaultdict(set) for rel in [rel_tt, rel_tf, rel_ff, rel_ft]: rel_prereq = rules_2prereq(rel) for k, pitems in rel_prereq.iteritems(): prereq[k] |= pitems self.prereq = prereq