facts.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. r"""This is rule-based deduction system for SymPy
  2. The whole thing is split into two parts
  3. - rules compilation and preparation of tables
  4. - runtime inference
  5. For rule-based inference engines, the classical work is RETE algorithm [1],
  6. [2] Although we are not implementing it in full (or even significantly)
  7. it's still worth a read to understand the underlying ideas.
  8. In short, every rule in a system of rules is one of two forms:
  9. - atom -> ... (alpha rule)
  10. - And(atom1, atom2, ...) -> ... (beta rule)
  11. The major complexity is in efficient beta-rules processing and usually for an
  12. expert system a lot of effort goes into code that operates on beta-rules.
  13. Here we take minimalistic approach to get something usable first.
  14. - (preparation) of alpha- and beta- networks, everything except
  15. - (runtime) FactRules.deduce_all_facts
  16. _____________________________________
  17. ( Kirr: I've never thought that doing )
  18. ( logic stuff is that difficult... )
  19. -------------------------------------
  20. o ^__^
  21. o (oo)\_______
  22. (__)\ )\/\
  23. ||----w |
  24. || ||
  25. Some references on the topic
  26. ----------------------------
  27. [1] https://en.wikipedia.org/wiki/Rete_algorithm
  28. [2] http://reports-archive.adm.cs.cmu.edu/anon/1995/CMU-CS-95-113.pdf
  29. https://en.wikipedia.org/wiki/Propositional_formula
  30. https://en.wikipedia.org/wiki/Inference_rule
  31. https://en.wikipedia.org/wiki/List_of_rules_of_inference
  32. """
  33. from collections import defaultdict
  34. from typing import Iterator
  35. from .logic import Logic, And, Or, Not
  36. def _base_fact(atom):
  37. """Return the literal fact of an atom.
  38. Effectively, this merely strips the Not around a fact.
  39. """
  40. if isinstance(atom, Not):
  41. return atom.arg
  42. else:
  43. return atom
  44. def _as_pair(atom):
  45. if isinstance(atom, Not):
  46. return (atom.arg, False)
  47. else:
  48. return (atom, True)
  49. # XXX this prepares forward-chaining rules for alpha-network
  50. def transitive_closure(implications):
  51. """
  52. Computes the transitive closure of a list of implications
  53. Uses Warshall's algorithm, as described at
  54. http://www.cs.hope.edu/~cusack/Notes/Notes/DiscreteMath/Warshall.pdf.
  55. """
  56. full_implications = set(implications)
  57. literals = set().union(*map(set, full_implications))
  58. for k in literals:
  59. for i in literals:
  60. if (i, k) in full_implications:
  61. for j in literals:
  62. if (k, j) in full_implications:
  63. full_implications.add((i, j))
  64. return full_implications
  65. def deduce_alpha_implications(implications):
  66. """deduce all implications
  67. Description by example
  68. ----------------------
  69. given set of logic rules:
  70. a -> b
  71. b -> c
  72. we deduce all possible rules:
  73. a -> b, c
  74. b -> c
  75. implications: [] of (a,b)
  76. return: {} of a -> set([b, c, ...])
  77. """
  78. implications = implications + [(Not(j), Not(i)) for (i, j) in implications]
  79. res = defaultdict(set)
  80. full_implications = transitive_closure(implications)
  81. for a, b in full_implications:
  82. if a == b:
  83. continue # skip a->a cyclic input
  84. res[a].add(b)
  85. # Clean up tautologies and check consistency
  86. for a, impl in res.items():
  87. impl.discard(a)
  88. na = Not(a)
  89. if na in impl:
  90. raise ValueError(
  91. 'implications are inconsistent: %s -> %s %s' % (a, na, impl))
  92. return res
  93. def apply_beta_to_alpha_route(alpha_implications, beta_rules):
  94. """apply additional beta-rules (And conditions) to already-built
  95. alpha implication tables
  96. TODO: write about
  97. - static extension of alpha-chains
  98. - attaching refs to beta-nodes to alpha chains
  99. e.g.
  100. alpha_implications:
  101. a -> [b, !c, d]
  102. b -> [d]
  103. ...
  104. beta_rules:
  105. &(b,d) -> e
  106. then we'll extend a's rule to the following
  107. a -> [b, !c, d, e]
  108. """
  109. x_impl = {}
  110. for x in alpha_implications.keys():
  111. x_impl[x] = (set(alpha_implications[x]), [])
  112. for bcond, bimpl in beta_rules:
  113. for bk in bcond.args:
  114. if bk in x_impl:
  115. continue
  116. x_impl[bk] = (set(), [])
  117. # static extensions to alpha rules:
  118. # A: x -> a,b B: &(a,b) -> c ==> A: x -> a,b,c
  119. seen_static_extension = True
  120. while seen_static_extension:
  121. seen_static_extension = False
  122. for bcond, bimpl in beta_rules:
  123. if not isinstance(bcond, And):
  124. raise TypeError("Cond is not And")
  125. bargs = set(bcond.args)
  126. for x, (ximpls, bb) in x_impl.items():
  127. x_all = ximpls | {x}
  128. # A: ... -> a B: &(...) -> a is non-informative
  129. if bimpl not in x_all and bargs.issubset(x_all):
  130. ximpls.add(bimpl)
  131. # we introduced new implication - now we have to restore
  132. # completeness of the whole set.
  133. bimpl_impl = x_impl.get(bimpl)
  134. if bimpl_impl is not None:
  135. ximpls |= bimpl_impl[0]
  136. seen_static_extension = True
  137. # attach beta-nodes which can be possibly triggered by an alpha-chain
  138. for bidx, (bcond, bimpl) in enumerate(beta_rules):
  139. bargs = set(bcond.args)
  140. for x, (ximpls, bb) in x_impl.items():
  141. x_all = ximpls | {x}
  142. # A: ... -> a B: &(...) -> a (non-informative)
  143. if bimpl in x_all:
  144. continue
  145. # A: x -> a... B: &(!a,...) -> ... (will never trigger)
  146. # A: x -> a... B: &(...) -> !a (will never trigger)
  147. if any(Not(xi) in bargs or Not(xi) == bimpl for xi in x_all):
  148. continue
  149. if bargs & x_all:
  150. bb.append(bidx)
  151. return x_impl
  152. def rules_2prereq(rules):
  153. """build prerequisites table from rules
  154. Description by example
  155. ----------------------
  156. given set of logic rules:
  157. a -> b, c
  158. b -> c
  159. we build prerequisites (from what points something can be deduced):
  160. b <- a
  161. c <- a, b
  162. rules: {} of a -> [b, c, ...]
  163. return: {} of c <- [a, b, ...]
  164. Note however, that this prerequisites may be *not* enough to prove a
  165. fact. An example is 'a -> b' rule, where prereq(a) is b, and prereq(b)
  166. is a. That's because a=T -> b=T, and b=F -> a=F, but a=F -> b=?
  167. """
  168. prereq = defaultdict(set)
  169. for (a, _), impl in rules.items():
  170. if isinstance(a, Not):
  171. a = a.args[0]
  172. for (i, _) in impl:
  173. if isinstance(i, Not):
  174. i = i.args[0]
  175. prereq[i].add(a)
  176. return prereq
  177. ################
  178. # RULES PROVER #
  179. ################
  180. class TautologyDetected(Exception):
  181. """(internal) Prover uses it for reporting detected tautology"""
  182. pass
  183. class Prover:
  184. """ai - prover of logic rules
  185. given a set of initial rules, Prover tries to prove all possible rules
  186. which follow from given premises.
  187. As a result proved_rules are always either in one of two forms: alpha or
  188. beta:
  189. Alpha rules
  190. -----------
  191. This are rules of the form::
  192. a -> b & c & d & ...
  193. Beta rules
  194. ----------
  195. This are rules of the form::
  196. &(a,b,...) -> c & d & ...
  197. i.e. beta rules are join conditions that say that something follows when
  198. *several* facts are true at the same time.
  199. """
  200. def __init__(self):
  201. self.proved_rules = []
  202. self._rules_seen = set()
  203. def split_alpha_beta(self):
  204. """split proved rules into alpha and beta chains"""
  205. rules_alpha = [] # a -> b
  206. rules_beta = [] # &(...) -> b
  207. for a, b in self.proved_rules:
  208. if isinstance(a, And):
  209. rules_beta.append((a, b))
  210. else:
  211. rules_alpha.append((a, b))
  212. return rules_alpha, rules_beta
  213. @property
  214. def rules_alpha(self):
  215. return self.split_alpha_beta()[0]
  216. @property
  217. def rules_beta(self):
  218. return self.split_alpha_beta()[1]
  219. def process_rule(self, a, b):
  220. """process a -> b rule""" # TODO write more?
  221. if (not a) or isinstance(b, bool):
  222. return
  223. if isinstance(a, bool):
  224. return
  225. if (a, b) in self._rules_seen:
  226. return
  227. else:
  228. self._rules_seen.add((a, b))
  229. # this is the core of processing
  230. try:
  231. self._process_rule(a, b)
  232. except TautologyDetected:
  233. pass
  234. def _process_rule(self, a, b):
  235. # right part first
  236. # a -> b & c --> a -> b ; a -> c
  237. # (?) FIXME this is only correct when b & c != null !
  238. if isinstance(b, And):
  239. sorted_bargs = sorted(b.args, key=str)
  240. for barg in sorted_bargs:
  241. self.process_rule(a, barg)
  242. # a -> b | c --> !b & !c -> !a
  243. # --> a & !b -> c
  244. # --> a & !c -> b
  245. elif isinstance(b, Or):
  246. sorted_bargs = sorted(b.args, key=str)
  247. # detect tautology first
  248. if not isinstance(a, Logic): # Atom
  249. # tautology: a -> a|c|...
  250. if a in sorted_bargs:
  251. raise TautologyDetected(a, b, 'a -> a|c|...')
  252. self.process_rule(And(*[Not(barg) for barg in b.args]), Not(a))
  253. for bidx in range(len(sorted_bargs)):
  254. barg = sorted_bargs[bidx]
  255. brest = sorted_bargs[:bidx] + sorted_bargs[bidx + 1:]
  256. self.process_rule(And(a, Not(barg)), Or(*brest))
  257. # left part
  258. # a & b -> c --> IRREDUCIBLE CASE -- WE STORE IT AS IS
  259. # (this will be the basis of beta-network)
  260. elif isinstance(a, And):
  261. sorted_aargs = sorted(a.args, key=str)
  262. if b in sorted_aargs:
  263. raise TautologyDetected(a, b, 'a & b -> a')
  264. self.proved_rules.append((a, b))
  265. # XXX NOTE at present we ignore !c -> !a | !b
  266. elif isinstance(a, Or):
  267. sorted_aargs = sorted(a.args, key=str)
  268. if b in sorted_aargs:
  269. raise TautologyDetected(a, b, 'a | b -> a')
  270. for aarg in sorted_aargs:
  271. self.process_rule(aarg, b)
  272. else:
  273. # both `a` and `b` are atoms
  274. self.proved_rules.append((a, b)) # a -> b
  275. self.proved_rules.append((Not(b), Not(a))) # !b -> !a
  276. ########################################
  277. class FactRules:
  278. """Rules that describe how to deduce facts in logic space
  279. When defined, these rules allow implications to quickly be determined
  280. for a set of facts. For this precomputed deduction tables are used.
  281. see `deduce_all_facts` (forward-chaining)
  282. Also it is possible to gather prerequisites for a fact, which is tried
  283. to be proven. (backward-chaining)
  284. Definition Syntax
  285. -----------------
  286. a -> b -- a=T -> b=T (and automatically b=F -> a=F)
  287. a -> !b -- a=T -> b=F
  288. a == b -- a -> b & b -> a
  289. a -> b & c -- a=T -> b=T & c=T
  290. # TODO b | c
  291. Internals
  292. ---------
  293. .full_implications[k, v]: all the implications of fact k=v
  294. .beta_triggers[k, v]: beta rules that might be triggered when k=v
  295. .prereq -- {} k <- [] of k's prerequisites
  296. .defined_facts -- set of defined fact names
  297. """
  298. def __init__(self, rules):
  299. """Compile rules into internal lookup tables"""
  300. if isinstance(rules, str):
  301. rules = rules.splitlines()
  302. # --- parse and process rules ---
  303. P = Prover()
  304. for rule in rules:
  305. # XXX `a` is hardcoded to be always atom
  306. a, op, b = rule.split(None, 2)
  307. a = Logic.fromstring(a)
  308. b = Logic.fromstring(b)
  309. if op == '->':
  310. P.process_rule(a, b)
  311. elif op == '==':
  312. P.process_rule(a, b)
  313. P.process_rule(b, a)
  314. else:
  315. raise ValueError('unknown op %r' % op)
  316. # --- build deduction networks ---
  317. self.beta_rules = []
  318. for bcond, bimpl in P.rules_beta:
  319. self.beta_rules.append(
  320. ({_as_pair(a) for a in bcond.args}, _as_pair(bimpl)))
  321. # deduce alpha implications
  322. impl_a = deduce_alpha_implications(P.rules_alpha)
  323. # now:
  324. # - apply beta rules to alpha chains (static extension), and
  325. # - further associate beta rules to alpha chain (for inference
  326. # at runtime)
  327. impl_ab = apply_beta_to_alpha_route(impl_a, P.rules_beta)
  328. # extract defined fact names
  329. self.defined_facts = {_base_fact(k) for k in impl_ab.keys()}
  330. # build rels (forward chains)
  331. full_implications = defaultdict(set)
  332. beta_triggers = defaultdict(set)
  333. for k, (impl, betaidxs) in impl_ab.items():
  334. full_implications[_as_pair(k)] = {_as_pair(i) for i in impl}
  335. beta_triggers[_as_pair(k)] = betaidxs
  336. self.full_implications = full_implications
  337. self.beta_triggers = beta_triggers
  338. # build prereq (backward chains)
  339. prereq = defaultdict(set)
  340. rel_prereq = rules_2prereq(full_implications)
  341. for k, pitems in rel_prereq.items():
  342. prereq[k] |= pitems
  343. self.prereq = prereq
  344. def _to_python(self) -> str:
  345. """ Generate a string with plain python representation of the instance """
  346. return '\n'.join(self.print_rules())
  347. @classmethod
  348. def _from_python(cls, data : dict):
  349. """ Generate an instance from the plain python representation """
  350. self = cls('')
  351. for key in ['full_implications', 'beta_triggers', 'prereq']:
  352. d=defaultdict(set)
  353. d.update(data[key])
  354. setattr(self, key, d)
  355. self.beta_rules = data['beta_rules']
  356. self.defined_facts = set(data['defined_facts'])
  357. return self
  358. def _defined_facts_lines(self):
  359. yield 'defined_facts = ['
  360. for fact in sorted(self.defined_facts):
  361. yield f' {fact!r},'
  362. yield '] # defined_facts'
  363. def _full_implications_lines(self):
  364. yield 'full_implications = dict( ['
  365. for fact in sorted(self.defined_facts):
  366. for value in (True, False):
  367. yield f' # Implications of {fact} = {value}:'
  368. yield f' (({fact!r}, {value!r}), set( ('
  369. implications = self.full_implications[(fact, value)]
  370. for implied in sorted(implications):
  371. yield f' {implied!r},'
  372. yield ' ) ),'
  373. yield ' ),'
  374. yield ' ] ) # full_implications'
  375. def _prereq_lines(self):
  376. yield 'prereq = {'
  377. yield ''
  378. for fact in sorted(self.prereq):
  379. yield f' # facts that could determine the value of {fact}'
  380. yield f' {fact!r}: {{'
  381. for pfact in sorted(self.prereq[fact]):
  382. yield f' {pfact!r},'
  383. yield ' },'
  384. yield ''
  385. yield '} # prereq'
  386. def _beta_rules_lines(self):
  387. reverse_implications = defaultdict(list)
  388. for n, (pre, implied) in enumerate(self.beta_rules):
  389. reverse_implications[implied].append((pre, n))
  390. yield '# Note: the order of the beta rules is used in the beta_triggers'
  391. yield 'beta_rules = ['
  392. yield ''
  393. m = 0
  394. indices = {}
  395. for implied in sorted(reverse_implications):
  396. fact, value = implied
  397. yield f' # Rules implying {fact} = {value}'
  398. for pre, n in reverse_implications[implied]:
  399. indices[n] = m
  400. m += 1
  401. setstr = ", ".join(map(str, sorted(pre)))
  402. yield f' ({{{setstr}}},'
  403. yield f' {implied!r}),'
  404. yield ''
  405. yield '] # beta_rules'
  406. yield 'beta_triggers = {'
  407. for query in sorted(self.beta_triggers):
  408. fact, value = query
  409. triggers = [indices[n] for n in self.beta_triggers[query]]
  410. yield f' {query!r}: {triggers!r},'
  411. yield '} # beta_triggers'
  412. def print_rules(self) -> Iterator[str]:
  413. """ Returns a generator with lines to represent the facts and rules """
  414. yield from self._defined_facts_lines()
  415. yield ''
  416. yield ''
  417. yield from self._full_implications_lines()
  418. yield ''
  419. yield ''
  420. yield from self._prereq_lines()
  421. yield ''
  422. yield ''
  423. yield from self._beta_rules_lines()
  424. yield ''
  425. yield ''
  426. yield "generated_assumptions = {'defined_facts': defined_facts, 'full_implications': full_implications,"
  427. yield " 'prereq': prereq, 'beta_rules': beta_rules, 'beta_triggers': beta_triggers}"
  428. class InconsistentAssumptions(ValueError):
  429. def __str__(self):
  430. kb, fact, value = self.args
  431. return "%s, %s=%s" % (kb, fact, value)
  432. class FactKB(dict):
  433. """
  434. A simple propositional knowledge base relying on compiled inference rules.
  435. """
  436. def __str__(self):
  437. return '{\n%s}' % ',\n'.join(
  438. ["\t%s: %s" % i for i in sorted(self.items())])
  439. def __init__(self, rules):
  440. self.rules = rules
  441. def _tell(self, k, v):
  442. """Add fact k=v to the knowledge base.
  443. Returns True if the KB has actually been updated, False otherwise.
  444. """
  445. if k in self and self[k] is not None:
  446. if self[k] == v:
  447. return False
  448. else:
  449. raise InconsistentAssumptions(self, k, v)
  450. else:
  451. self[k] = v
  452. return True
  453. # *********************************************
  454. # * This is the workhorse, so keep it *fast*. *
  455. # *********************************************
  456. def deduce_all_facts(self, facts):
  457. """
  458. Update the KB with all the implications of a list of facts.
  459. Facts can be specified as a dictionary or as a list of (key, value)
  460. pairs.
  461. """
  462. # keep frequently used attributes locally, so we'll avoid extra
  463. # attribute access overhead
  464. full_implications = self.rules.full_implications
  465. beta_triggers = self.rules.beta_triggers
  466. beta_rules = self.rules.beta_rules
  467. if isinstance(facts, dict):
  468. facts = facts.items()
  469. while facts:
  470. beta_maytrigger = set()
  471. # --- alpha chains ---
  472. for k, v in facts:
  473. if not self._tell(k, v) or v is None:
  474. continue
  475. # lookup routing tables
  476. for key, value in full_implications[k, v]:
  477. self._tell(key, value)
  478. beta_maytrigger.update(beta_triggers[k, v])
  479. # --- beta chains ---
  480. facts = []
  481. for bidx in beta_maytrigger:
  482. bcond, bimpl = beta_rules[bidx]
  483. if all(self.get(k) is v for k, v in bcond):
  484. facts.append(bimpl)