mace.py 12 KB

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