mace.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  1. # Natural Language Toolkit: Interface to the Mace4 Model Builder
  2. #
  3. # Author: Dan Garrette <dhgarrette@gmail.com>
  4. # Ewan Klein <ewan@inf.ed.ac.uk>
  5. # URL: <http://nltk.org/>
  6. # For license information, see LICENSE.TXT
  7. """
  8. A model builder that makes use of the external 'Mace4' package.
  9. """
  10. from __future__ import print_function
  11. import os
  12. import tempfile
  13. from nltk.sem.logic import is_indvar
  14. from nltk.sem import Valuation, Expression
  15. from nltk.inference.api import ModelBuilder, BaseModelBuilderCommand
  16. from nltk.inference.prover9 import Prover9CommandParent, Prover9Parent
  17. class MaceCommand(Prover9CommandParent, BaseModelBuilderCommand):
  18. """
  19. A ``MaceCommand`` specific to the ``Mace`` model builder. It contains
  20. a print_assumptions() method that is used to print the list
  21. of assumptions in multiple formats.
  22. """
  23. _interpformat_bin = None
  24. def __init__(self, goal=None, assumptions=None, max_models=500, model_builder=None):
  25. """
  26. :param goal: Input expression to prove
  27. :type goal: sem.Expression
  28. :param assumptions: Input expressions to use as assumptions in
  29. the proof.
  30. :type assumptions: list(sem.Expression)
  31. :param max_models: The maximum number of models that Mace will try before
  32. simply returning false. (Use 0 for no maximum.)
  33. :type max_models: int
  34. """
  35. if model_builder is not None:
  36. assert isinstance(model_builder, Mace)
  37. else:
  38. model_builder = Mace(max_models)
  39. BaseModelBuilderCommand.__init__(self, model_builder, goal, assumptions)
  40. @property
  41. def valuation(mbc):
  42. return mbc.model('valuation')
  43. def _convert2val(self, valuation_str):
  44. """
  45. Transform the output file into an NLTK-style Valuation.
  46. :return: A model if one is generated; None otherwise.
  47. :rtype: sem.Valuation
  48. """
  49. valuation_standard_format = self._transform_output(valuation_str, 'standard')
  50. val = []
  51. for line in valuation_standard_format.splitlines(False):
  52. l = line.strip()
  53. if l.startswith('interpretation'):
  54. # find the number of entities in the model
  55. num_entities = int(l[l.index('(') + 1 : l.index(',')].strip())
  56. elif l.startswith('function') and l.find('_') == -1:
  57. # replace the integer identifier with a corresponding alphabetic character
  58. name = l[l.index('(') + 1 : l.index(',')].strip()
  59. if is_indvar(name):
  60. name = name.upper()
  61. value = int(l[l.index('[') + 1 : l.index(']')].strip())
  62. val.append((name, MaceCommand._make_model_var(value)))
  63. elif l.startswith('relation'):
  64. l = l[l.index('(') + 1 :]
  65. if '(' in l:
  66. # relation is not nullary
  67. name = l[: l.index('(')].strip()
  68. values = [
  69. int(v.strip())
  70. for v in l[l.index('[') + 1 : l.index(']')].split(',')
  71. ]
  72. val.append(
  73. (name, MaceCommand._make_relation_set(num_entities, values))
  74. )
  75. else:
  76. # relation is nullary
  77. name = l[: l.index(',')].strip()
  78. value = int(l[l.index('[') + 1 : l.index(']')].strip())
  79. val.append((name, value == 1))
  80. return Valuation(val)
  81. @staticmethod
  82. def _make_relation_set(num_entities, values):
  83. """
  84. Convert a Mace4-style relation table into a dictionary.
  85. :param num_entities: the number of entities in the model; determines the row length in the table.
  86. :type num_entities: int
  87. :param values: a list of 1's and 0's that represent whether a relation holds in a Mace4 model.
  88. :type values: list of int
  89. """
  90. r = set()
  91. for position in [pos for (pos, v) in enumerate(values) if v == 1]:
  92. r.add(
  93. tuple(MaceCommand._make_relation_tuple(position, values, num_entities))
  94. )
  95. return r
  96. @staticmethod
  97. def _make_relation_tuple(position, values, num_entities):
  98. if len(values) == 1:
  99. return []
  100. else:
  101. sublist_size = len(values) // num_entities
  102. sublist_start = position // sublist_size
  103. sublist_position = int(position % sublist_size)
  104. sublist = values[
  105. sublist_start * sublist_size : (sublist_start + 1) * sublist_size
  106. ]
  107. return [
  108. MaceCommand._make_model_var(sublist_start)
  109. ] + MaceCommand._make_relation_tuple(
  110. sublist_position, sublist, num_entities
  111. )
  112. @staticmethod
  113. def _make_model_var(value):
  114. """
  115. Pick an alphabetic character as identifier for an entity in the model.
  116. :param value: where to index into the list of characters
  117. :type value: int
  118. """
  119. letter = [
  120. 'a',
  121. 'b',
  122. 'c',
  123. 'd',
  124. 'e',
  125. 'f',
  126. 'g',
  127. 'h',
  128. 'i',
  129. 'j',
  130. 'k',
  131. 'l',
  132. 'm',
  133. 'n',
  134. 'o',
  135. 'p',
  136. 'q',
  137. 'r',
  138. 's',
  139. 't',
  140. 'u',
  141. 'v',
  142. 'w',
  143. 'x',
  144. 'y',
  145. 'z',
  146. ][value]
  147. num = value // 26
  148. return letter + str(num) if num > 0 else letter
  149. def _decorate_model(self, valuation_str, format):
  150. """
  151. Print out a Mace4 model using any Mace4 ``interpformat`` format.
  152. See http://www.cs.unm.edu/~mccune/mace4/manual/ for details.
  153. :param valuation_str: str with the model builder's output
  154. :param format: str indicating the format for displaying
  155. models. Defaults to 'standard' format.
  156. :return: str
  157. """
  158. if not format:
  159. return valuation_str
  160. elif format == 'valuation':
  161. return self._convert2val(valuation_str)
  162. else:
  163. return self._transform_output(valuation_str, format)
  164. def _transform_output(self, valuation_str, format):
  165. """
  166. Transform the output file into any Mace4 ``interpformat`` format.
  167. :param format: Output format for displaying models.
  168. :type format: str
  169. """
  170. if format in [
  171. 'standard',
  172. 'standard2',
  173. 'portable',
  174. 'tabular',
  175. 'raw',
  176. 'cooked',
  177. 'xml',
  178. 'tex',
  179. ]:
  180. return self._call_interpformat(valuation_str, [format])[0]
  181. else:
  182. raise LookupError("The specified format does not exist")
  183. def _call_interpformat(self, input_str, args=[], verbose=False):
  184. """
  185. Call the ``interpformat`` binary with the given input.
  186. :param input_str: A string whose contents are used as stdin.
  187. :param args: A list of command-line arguments.
  188. :return: A tuple (stdout, returncode)
  189. :see: ``config_prover9``
  190. """
  191. if self._interpformat_bin is None:
  192. self._interpformat_bin = self._modelbuilder._find_binary(
  193. 'interpformat', verbose
  194. )
  195. return self._modelbuilder._call(
  196. input_str, self._interpformat_bin, args, verbose
  197. )
  198. class Mace(Prover9Parent, ModelBuilder):
  199. _mace4_bin = None
  200. def __init__(self, end_size=500):
  201. self._end_size = end_size
  202. """The maximum model size that Mace will try before
  203. simply returning false. (Use -1 for no maximum.)"""
  204. def _build_model(self, goal=None, assumptions=None, verbose=False):
  205. """
  206. Use Mace4 to build a first order model.
  207. :return: ``True`` if a model was found (i.e. Mace returns value of 0),
  208. else ``False``
  209. """
  210. if not assumptions:
  211. assumptions = []
  212. stdout, returncode = self._call_mace4(
  213. self.prover9_input(goal, assumptions), verbose=verbose
  214. )
  215. return (returncode == 0, stdout)
  216. def _call_mace4(self, input_str, args=[], verbose=False):
  217. """
  218. Call the ``mace4`` binary with the given input.
  219. :param input_str: A string whose contents are used as stdin.
  220. :param args: A list of command-line arguments.
  221. :return: A tuple (stdout, returncode)
  222. :see: ``config_prover9``
  223. """
  224. if self._mace4_bin is None:
  225. self._mace4_bin = self._find_binary('mace4', verbose)
  226. updated_input_str = ''
  227. if self._end_size > 0:
  228. updated_input_str += 'assign(end_size, %d).\n\n' % self._end_size
  229. updated_input_str += input_str
  230. return self._call(updated_input_str, self._mace4_bin, args, verbose)
  231. def spacer(num=30):
  232. print('-' * num)
  233. def decode_result(found):
  234. """
  235. Decode the result of model_found()
  236. :param found: The output of model_found()
  237. :type found: bool
  238. """
  239. return {True: 'Countermodel found', False: 'No countermodel found', None: 'None'}[
  240. found
  241. ]
  242. def test_model_found(arguments):
  243. """
  244. Try some proofs and exhibit the results.
  245. """
  246. for (goal, assumptions) in arguments:
  247. g = Expression.fromstring(goal)
  248. alist = [lp.parse(a) for a in assumptions]
  249. m = MaceCommand(g, assumptions=alist, max_models=50)
  250. found = m.build_model()
  251. for a in alist:
  252. print(' %s' % a)
  253. print('|- %s: %s\n' % (g, decode_result(found)))
  254. def test_build_model(arguments):
  255. """
  256. Try to build a ``nltk.sem.Valuation``.
  257. """
  258. g = Expression.fromstring('all x.man(x)')
  259. alist = [
  260. Expression.fromstring(a)
  261. for a in [
  262. 'man(John)',
  263. 'man(Socrates)',
  264. 'man(Bill)',
  265. 'some x.(-(x = John) & man(x) & sees(John,x))',
  266. 'some x.(-(x = Bill) & man(x))',
  267. 'all x.some y.(man(x) -> gives(Socrates,x,y))',
  268. ]
  269. ]
  270. m = MaceCommand(g, assumptions=alist)
  271. m.build_model()
  272. spacer()
  273. print("Assumptions and Goal")
  274. spacer()
  275. for a in alist:
  276. print(' %s' % a)
  277. print('|- %s: %s\n' % (g, decode_result(m.build_model())))
  278. spacer()
  279. # print m.model('standard')
  280. # print m.model('cooked')
  281. print("Valuation")
  282. spacer()
  283. print(m.valuation, '\n')
  284. def test_transform_output(argument_pair):
  285. """
  286. Transform the model into various Mace4 ``interpformat`` formats.
  287. """
  288. g = Expression.fromstring(argument_pair[0])
  289. alist = [lp.parse(a) for a in argument_pair[1]]
  290. m = MaceCommand(g, assumptions=alist)
  291. m.build_model()
  292. for a in alist:
  293. print(' %s' % a)
  294. print('|- %s: %s\n' % (g, m.build_model()))
  295. for format in ['standard', 'portable', 'xml', 'cooked']:
  296. spacer()
  297. print("Using '%s' format" % format)
  298. spacer()
  299. print(m.model(format=format))
  300. def test_make_relation_set():
  301. print(
  302. MaceCommand._make_relation_set(num_entities=3, values=[1, 0, 1])
  303. == set([('c',), ('a',)])
  304. )
  305. print(
  306. MaceCommand._make_relation_set(
  307. num_entities=3, values=[0, 0, 0, 0, 0, 0, 1, 0, 0]
  308. )
  309. == set([('c', 'a')])
  310. )
  311. print(
  312. MaceCommand._make_relation_set(num_entities=2, values=[0, 0, 1, 0, 0, 0, 1, 0])
  313. == set([('a', 'b', 'a'), ('b', 'b', 'a')])
  314. )
  315. arguments = [
  316. ('mortal(Socrates)', ['all x.(man(x) -> mortal(x))', 'man(Socrates)']),
  317. ('(not mortal(Socrates))', ['all x.(man(x) -> mortal(x))', 'man(Socrates)']),
  318. ]
  319. def demo():
  320. test_model_found(arguments)
  321. test_build_model(arguments)
  322. test_transform_output(arguments[1])
  323. if __name__ == '__main__':
  324. demo()