def convert(line): s = OrderedDict([ ('==', 'Equal'), ('!=', 'NotEqual'), ('!~=', 'NotAlmostEqual'), ('~=', 'AlmostEqual'), (lambda line: bool(line.count('>') > 1), partial(_convert, '>', 'Greater')), (lambda line: bool(line.count('<') > 1), partial(_convert, '<', 'Less')), ('>=', 'GreaterEqual'), ('<=', 'LessEqual'), ('>', 'Greater'), ('<', 'Less'), ('raises', lambda line: ['with self.assertRaises(' + line.split('raises')[1].strip() + '):', ' ' + line.split('raises')[0].strip()]), (' is not instanceof ', 'NotIsInstance'), (' is instanceof ', 'IsInstance'), ('for ', lambda line: line), (' not in ', 'NotIn'), (' in ', 'In'), (' is not None', 'IsNotNone'), (' is None', 'IsNone'), (' is not ', 'IsNot'), (' is ', 'Is'), ]) def to_lambda(i): def to_code(k, v, line): l, op, r = line.rpartition(k) params = ', '.join(map(strip, (l, r))).rstrip().rstrip(',') return 'self.assert' + v + '(' + params + ')' k, v = i new_v = v if callable(v) else partial(to_code, k, v) del s[k] if callable(k): s[k] = new_v else: s[lambda line: k in line] = new_v map(to_lambda, s.items()) matches = filter(lambda k: k(line), s.keys()) return s[matches[0]](line) if len(matches) else line
class Engine(object): def __init__(self, ruleset_ids=None, var_list=None, test_ids=None): self.ruleset_ids = [] self.test_ids = [] self.fact_state = OrderedDict() self.rec_nodes = [] self.debug = False self.vars_tested = set() # restore state if ruleset_ids: for rsid in ruleset_ids: self.ruleset_ids.append(rsid) if var_list: self.add_vars(var_list, FACT_ASSERTED) if test_ids: for var_id in test_ids: self.test_ids.append(var_id) # list of ruleset to use def get_rulesets(self): return self.ruleset_ids # return list of variables which has been # tested/established/need to be tested def get_vars(self, need_state=False): var_list = [] for key, state in self.fact_state.items(): vid,value = self.decode_fact_key(key) if need_state: var_list.append((vid,value,state)) else: var_list.append((vid,value)) return var_list def get_tests(self): return self.test_ids def gen_fact_key(self, var_id, value): return '%d:%s' % (var_id, value) def decode_fact_key(self, key): idstr,value = key.split(':',1) var_id = int(idstr) return var_id,value def create_vnode(self, var_id, value): key = self.gen_fact_key(var_id, value) if key in self.fact_state: state = NODE_PASSED elif var_id in self.vars_tested: state = NODE_TESTED else: state = NODE_UNTESTED return FactNode(VAR_NODE, var_id, value, state) def get_unique_rnodes(self): # first get unique list of recommends nodes rdict = {} for rnode in self.rec_nodes: if rnode.node_id in rdict: # replace only if node has higher rank onode = rdict[rnode.node_id] if rnode.value > onode.value: rdict[rnode.node_id] = rnode else: rdict[rnode.node_id] = rnode return rdict.values() def get_questions(self): questions = [] for variable in Variable.objects.filter(id__in=self.test_ids): if variable.ask: questions.append(variable) return questions # return list of recommendation for rules that have fired # TODO fix views to use only get_reasons call def get_recommends(self): rnodes = self.get_unique_rnodes() recommends = [] for rnode in rnodes: recommend = Recommend.objects.get(pk=rnode.node_id) # FIXME - this is a hack recommend.rank = rnode.value recommends.append(recommend) return sorted(recommends, key=lambda recommend: recommend.rank, reverse=True) def get_answers(self): answers = [] for key, state in self.fact_state.items(): if state == FACT_ANSWERED: vid,value = self.decode_fact_key(key) # TODO should we include questions not answered if value: answers.append((vid,value)) return answers # reverse climb the tree to the top node (we use node_set to prevent loops) def next_premises(self, search_premises, qa_list, node_set): if len(search_premises) == 0: return tnode_list = [] for tnode in reversed(search_premises[0].get_nodes()): if tnode not in node_set: try: var = Variable.objects.get(pk=tnode.node_id) except Variable.DoesNotExist: continue # FIXME this really should be the fact name text = var.prompt if len(var.prompt) > 0 else var.name qa = (text, tnode.value) qa_list.append(qa) tnode_list.append(tnode) node_set.add(tnode) for tnode in tnode_list: self.next_premises(tnode.get_premises(), qa_list, node_set) def get_reasons(self): # first get unique list of recommends nodes rnode_list = self.get_unique_rnodes() # now build a list of the reasons that go with them reasons = [] for rnode in rnode_list: qa_list = [] node_set = set() self.next_premises(rnode.get_premises(), qa_list, node_set) recommend = Recommend.objects.get(pk=rnode.node_id) reasons.append(Reason( recommend.id, recommend.name, recommend.text, rnode.value, qa_list)) return sorted(reasons, key=lambda reason: reason.rank, reverse=True) # asserted fact = variable that has been assinged a value def add_var(self, var_id, value, state): # record that we've seen this variable self.vars_tested.add(var_id) # update fact state key = self.gen_fact_key(var_id, value) if key not in self.fact_state: self.fact_state[key] = state else: # ASSERTED < ANSWERED < INFERRED curr_state = self.fact_state[key] if state < curr_state: self.fact_state[key] = state def add_vars(self, facts, default_state): for item in facts: state = item[2] if len(item) > 2 else default_state self.add_var(item[0], item[1], state) def get_groups(self, tree, rule): group_list = [] parser = PremiseParser() # parse ast plist = [] for premise in rule.rulepremise_set.all(): plist.append(premise) try: root = parser.parse(plist) root = wff_dnf(root) except PremiseException as e: if self.debug: print e, plist return group_list # now get each or group or_list = [] grab_or_nodes(root, or_list) for or_group in or_list: premise_group = FactGroup() pnode_list = [] flatten_node(or_group, pnode_list) for pnode in pnode_list: if pnode.ptype != PTYPE_VAR: if self.debug: print '!!PremiseNode not a var', pnode continue variable_id = pnode.left value = pnode.right tnode = tree.get_fact(variable_id, value) if not tnode: tnode = self.create_vnode(variable_id, value) tree.add_fact(tnode) premise_group.add_node(tnode) group_list.append(premise_group) return group_list def build_tree(self, ruleset): tree = FactTree() for rule in ruleset.rule_set.all(): group_list = self.get_groups(tree, rule) """ premise_group = FactGroup() for premise in rule.rulepremise_set.all(): node = tree.get_fact(premise.variable_id, premise.value) if not node: node = self.create_vnode(premise.variable_id, premise.value) tree.add_fact(node) premise_group.add_node(node) """ for premise_group in group_list: conclusion_nodes = [] for conclusion in rule.ruleconclusion_set.all(): node = tree.get_fact(conclusion.variable_id, conclusion.value) if not node: node = self.create_vnode(conclusion.variable_id, conclusion.value) tree.add_fact(node) node.add_premise(premise_group) conclusion_nodes.append(node) recommend_nodes = [] for rrecommend in rule.rulerecommend_set.all(): node = tree.get_rec(rrecommend.recommend_id, rrecommend.rank) if not node: node = FactNode(REC_NODE, rrecommend.recommend_id, rrecommend.rank, NODE_UNTESTED) tree.add_rec(node) node.add_premise(premise_group) recommend_nodes.append(node) premise_group.add_children(conclusion_nodes) premise_group.add_children(recommend_nodes) tree.add_group(premise_group) return tree def forward_chain(self, tree): # now look for rules that have fired test_groups = tree.get_groups() new_facts = True while new_facts: new_facts = False next_test = [] for group in test_groups: if group.all_passed(): for child in group.get_children(): child.set_state(NODE_PASSED) if child.get_type() == VAR_NODE: self.add_var(child.node_id, child.value, FACT_INFERRED) new_facts = True else: next_test.append(group) test_groups = next_test return test_groups def get_first(self, premise_list, node_set): if self.debug: print 'get_first premise', [ str(x) for x in premise_list] for premise in premise_list: num_loop = 0 for node in premise.get_nodes(): if self.debug: print 'node', node if node in node_set: if self.debug: print '+++++++LOOP++++++++' num_loop += 1 continue node_set.add(node) if node.check_state(NODE_UNTESTED): leafp = self.get_first(node.get_premises(), node_set) if leafp: return leafp if num_loop == 0: return premise return None def find_backchains(self, node, node_set): if self.debug: print 'find_backchains', node backchains = [] if node in node_set: if self.debug: print '+++++++LOOP++++++++' #print 'Node', node, 'premises', [ str(x) for x in node_set ] return True node_set.add(node) #print 'Node', node, 'premises', [ str(x) for x in node.get_premises() ] for premise in node.get_premises(): if self.debug: print 'premise', premise untested = [] num_tested = 0 for pnode in premise.get_nodes(): if pnode.check_state(NODE_UNTESTED): untested.append(pnode) elif pnode.check_state(NODE_TESTED): num_tested = num_tested + 1 if num_tested == 0 and len(untested) > 0: num_backchain = 0 for pnode in untested: if self.find_backchains(pnode, node_set): num_backchain += 1 if self.debug: print 'num_backchain', num_backchain, len(untested) if num_backchain == len(untested): if self.debug: print 'backchain', premise backchains.append(premise) if self.debug: print 'Node', node, 'backchains', [ str(x) for x in backchains ] premsies = node.get_premises() node.set_premises(backchains) return len(premsies) == 0 or len(backchains) > 0 def find_goals(self, tree, unfired): test_premises = [] for rec_node in tree.get_goals(): if rec_node.check_state(NODE_PASSED): if self.debug: print 'Found goal!', rec_node self.rec_nodes.append(rec_node) else: if self.find_backchains(rec_node, set()): premises = rec_node.get_premises() test_premises.append(self.get_first(premises, set())) test_ids = [] for premise in test_premises: for node in premise.get_nodes(): if node.state == NODE_UNTESTED: try: variable = Variable.objects.get(pk=node.node_id) except Variable.DoesNotExist: continue if variable.ask: test_ids.append(node.node_id) if len(test_ids) > 0: self.test_ids.append(test_ids[0]) # given some answers update facts base and check if rules have fired # we use forward chaining here # answers = list of (var_id,value) tuples def next_state(self, answers=None): # first add asserted facts if answers: if self.debug: print 'Got answers', answers self.add_vars(answers, FACT_ANSWERED) # now add variables for which we did not get answers for var_id in self.test_ids: if var_id not in self.vars_tested: self.add_var(var_id, '', FACT_ANSWERED) # and reset testable node list self.test_ids = [] # reset rule list self.fire_ids = [] # this really needs to be fixed self.rec_nodes = [] for ruleset in RuleSet.objects.filter(id__in=self.ruleset_ids): tree = self.build_tree(ruleset) unfired = self.forward_chain(tree) self.find_goals(tree, unfired)