This commit is contained in:
Iliyan Angelov
2025-12-01 06:50:10 +02:00
parent 91f51bc6fe
commit 62c1fe5951
4682 changed files with 544807 additions and 31208 deletions

View File

@@ -0,0 +1,24 @@
# Natural Language Toolkit: Inference
#
# Copyright (C) 2001-2025 NLTK Project
# Author: Dan Garrette <dhgarrette@gmail.com>
# Ewan Klein <ewan@inf.ed.ac.uk>
#
# URL: <https://www.nltk.org/>
# For license information, see LICENSE.TXT
"""
Classes and interfaces for theorem proving and model building.
"""
from nltk.inference.api import ParallelProverBuilder, ParallelProverBuilderCommand
from nltk.inference.discourse import (
CfgReadingCommand,
DiscourseTester,
DrtGlueReadingCommand,
ReadingCommand,
)
from nltk.inference.mace import Mace, MaceCommand
from nltk.inference.prover9 import Prover9, Prover9Command
from nltk.inference.resolution import ResolutionProver, ResolutionProverCommand
from nltk.inference.tableau import TableauProver, TableauProverCommand

View File

@@ -0,0 +1,614 @@
# Natural Language Toolkit: Classifier Interface
#
# Author: Ewan Klein <ewan@inf.ed.ac.uk>
# Dan Garrette <dhgarrette@gmail.com>
#
# URL: <https://www.nltk.org/>
# For license information, see LICENSE.TXT
"""
Interfaces and base classes for theorem provers and model builders.
``Prover`` is a standard interface for a theorem prover which tries to prove a goal from a
list of assumptions.
``ModelBuilder`` is a standard interface for a model builder. Given just a set of assumptions.
the model builder tries to build a model for the assumptions. Given a set of assumptions and a
goal *G*, the model builder tries to find a counter-model, in the sense of a model that will satisfy
the assumptions plus the negation of *G*.
"""
import threading
import time
from abc import ABCMeta, abstractmethod
class Prover(metaclass=ABCMeta):
"""
Interface for trying to prove a goal from assumptions. Both the goal and
the assumptions are constrained to be formulas of ``logic.Expression``.
"""
def prove(self, goal=None, assumptions=None, verbose=False):
"""
:return: Whether the proof was successful or not.
:rtype: bool
"""
return self._prove(goal, assumptions, verbose)[0]
@abstractmethod
def _prove(self, goal=None, assumptions=None, verbose=False):
"""
:return: Whether the proof was successful or not, along with the proof
:rtype: tuple: (bool, str)
"""
class ModelBuilder(metaclass=ABCMeta):
"""
Interface for trying to build a model of set of formulas.
Open formulas are assumed to be universally quantified.
Both the goal and the assumptions are constrained to be formulas
of ``logic.Expression``.
"""
def build_model(self, goal=None, assumptions=None, verbose=False):
"""
Perform the actual model building.
:return: Whether a model was generated
:rtype: bool
"""
return self._build_model(goal, assumptions, verbose)[0]
@abstractmethod
def _build_model(self, goal=None, assumptions=None, verbose=False):
"""
Perform the actual model building.
:return: Whether a model was generated, and the model itself
:rtype: tuple(bool, sem.Valuation)
"""
class TheoremToolCommand(metaclass=ABCMeta):
"""
This class holds a goal and a list of assumptions to be used in proving
or model building.
"""
@abstractmethod
def add_assumptions(self, new_assumptions):
"""
Add new assumptions to the assumption list.
:param new_assumptions: new assumptions
:type new_assumptions: list(sem.Expression)
"""
@abstractmethod
def retract_assumptions(self, retracted, debug=False):
"""
Retract assumptions from the assumption list.
:param debug: If True, give warning when ``retracted`` is not present on
assumptions list.
:type debug: bool
:param retracted: assumptions to be retracted
:type retracted: list(sem.Expression)
"""
@abstractmethod
def assumptions(self):
"""
List the current assumptions.
:return: list of ``Expression``
"""
@abstractmethod
def goal(self):
"""
Return the goal
:return: ``Expression``
"""
@abstractmethod
def print_assumptions(self):
"""
Print the list of the current assumptions.
"""
class ProverCommand(TheoremToolCommand):
"""
This class holds a ``Prover``, a goal, and a list of assumptions. When
prove() is called, the ``Prover`` is executed with the goal and assumptions.
"""
@abstractmethod
def prove(self, verbose=False):
"""
Perform the actual proof.
"""
@abstractmethod
def proof(self, simplify=True):
"""
Return the proof string
:param simplify: bool simplify the proof?
:return: str
"""
@abstractmethod
def get_prover(self):
"""
Return the prover object
:return: ``Prover``
"""
class ModelBuilderCommand(TheoremToolCommand):
"""
This class holds a ``ModelBuilder``, a goal, and a list of assumptions.
When build_model() is called, the ``ModelBuilder`` is executed with the goal
and assumptions.
"""
@abstractmethod
def build_model(self, verbose=False):
"""
Perform the actual model building.
:return: A model if one is generated; None otherwise.
:rtype: sem.Valuation
"""
@abstractmethod
def model(self, format=None):
"""
Return a string representation of the model
:param simplify: bool simplify the proof?
:return: str
"""
@abstractmethod
def get_model_builder(self):
"""
Return the model builder object
:return: ``ModelBuilder``
"""
class BaseTheoremToolCommand(TheoremToolCommand):
"""
This class holds a goal and a list of assumptions to be used in proving
or model building.
"""
def __init__(self, goal=None, assumptions=None):
"""
:param goal: Input expression to prove
:type goal: sem.Expression
:param assumptions: Input expressions to use as assumptions in
the proof.
:type assumptions: list(sem.Expression)
"""
self._goal = goal
if not assumptions:
self._assumptions = []
else:
self._assumptions = list(assumptions)
self._result = None
"""A holder for the result, to prevent unnecessary re-proving"""
def add_assumptions(self, new_assumptions):
"""
Add new assumptions to the assumption list.
:param new_assumptions: new assumptions
:type new_assumptions: list(sem.Expression)
"""
self._assumptions.extend(new_assumptions)
self._result = None
def retract_assumptions(self, retracted, debug=False):
"""
Retract assumptions from the assumption list.
:param debug: If True, give warning when ``retracted`` is not present on
assumptions list.
:type debug: bool
:param retracted: assumptions to be retracted
:type retracted: list(sem.Expression)
"""
retracted = set(retracted)
result_list = list(filter(lambda a: a not in retracted, self._assumptions))
if debug and result_list == self._assumptions:
print(Warning("Assumptions list has not been changed:"))
self.print_assumptions()
self._assumptions = result_list
self._result = None
def assumptions(self):
"""
List the current assumptions.
:return: list of ``Expression``
"""
return self._assumptions
def goal(self):
"""
Return the goal
:return: ``Expression``
"""
return self._goal
def print_assumptions(self):
"""
Print the list of the current assumptions.
"""
for a in self.assumptions():
print(a)
class BaseProverCommand(BaseTheoremToolCommand, ProverCommand):
"""
This class holds a ``Prover``, a goal, and a list of assumptions. When
prove() is called, the ``Prover`` is executed with the goal and assumptions.
"""
def __init__(self, prover, goal=None, assumptions=None):
"""
:param prover: The theorem tool to execute with the assumptions
:type prover: Prover
:see: ``BaseTheoremToolCommand``
"""
self._prover = prover
"""The theorem tool to execute with the assumptions"""
BaseTheoremToolCommand.__init__(self, goal, assumptions)
self._proof = None
def prove(self, verbose=False):
"""
Perform the actual proof. Store the result to prevent unnecessary
re-proving.
"""
if self._result is None:
self._result, self._proof = self._prover._prove(
self.goal(), self.assumptions(), verbose
)
return self._result
def proof(self, simplify=True):
"""
Return the proof string
:param simplify: bool simplify the proof?
:return: str
"""
if self._result is None:
raise LookupError("You have to call prove() first to get a proof!")
else:
return self.decorate_proof(self._proof, simplify)
def decorate_proof(self, proof_string, simplify=True):
"""
Modify and return the proof string
:param proof_string: str the proof to decorate
:param simplify: bool simplify the proof?
:return: str
"""
return proof_string
def get_prover(self):
return self._prover
class BaseModelBuilderCommand(BaseTheoremToolCommand, ModelBuilderCommand):
"""
This class holds a ``ModelBuilder``, a goal, and a list of assumptions. When
build_model() is called, the ``ModelBuilder`` is executed with the goal and
assumptions.
"""
def __init__(self, modelbuilder, goal=None, assumptions=None):
"""
:param modelbuilder: The theorem tool to execute with the assumptions
:type modelbuilder: ModelBuilder
:see: ``BaseTheoremToolCommand``
"""
self._modelbuilder = modelbuilder
"""The theorem tool to execute with the assumptions"""
BaseTheoremToolCommand.__init__(self, goal, assumptions)
self._model = None
def build_model(self, verbose=False):
"""
Attempt to build a model. Store the result to prevent unnecessary
re-building.
"""
if self._result is None:
self._result, self._model = self._modelbuilder._build_model(
self.goal(), self.assumptions(), verbose
)
return self._result
def model(self, format=None):
"""
Return a string representation of the model
:param simplify: bool simplify the proof?
:return: str
"""
if self._result is None:
raise LookupError("You have to call build_model() first to " "get a model!")
else:
return self._decorate_model(self._model, format)
def _decorate_model(self, valuation_str, format=None):
"""
:param valuation_str: str with the model builder's output
:param format: str indicating the format for displaying
:return: str
"""
return valuation_str
def get_model_builder(self):
return self._modelbuilder
class TheoremToolCommandDecorator(TheoremToolCommand):
"""
A base decorator for the ``ProverCommandDecorator`` and
``ModelBuilderCommandDecorator`` classes from which decorators can extend.
"""
def __init__(self, command):
"""
:param command: ``TheoremToolCommand`` to decorate
"""
self._command = command
# The decorator has its own versions of 'result' different from the
# underlying command
self._result = None
def assumptions(self):
return self._command.assumptions()
def goal(self):
return self._command.goal()
def add_assumptions(self, new_assumptions):
self._command.add_assumptions(new_assumptions)
self._result = None
def retract_assumptions(self, retracted, debug=False):
self._command.retract_assumptions(retracted, debug)
self._result = None
def print_assumptions(self):
self._command.print_assumptions()
class ProverCommandDecorator(TheoremToolCommandDecorator, ProverCommand):
"""
A base decorator for the ``ProverCommand`` class from which other
prover command decorators can extend.
"""
def __init__(self, proverCommand):
"""
:param proverCommand: ``ProverCommand`` to decorate
"""
TheoremToolCommandDecorator.__init__(self, proverCommand)
# The decorator has its own versions of 'result' and 'proof'
# because they may be different from the underlying command
self._proof = None
def prove(self, verbose=False):
if self._result is None:
prover = self.get_prover()
self._result, self._proof = prover._prove(
self.goal(), self.assumptions(), verbose
)
return self._result
def proof(self, simplify=True):
"""
Return the proof string
:param simplify: bool simplify the proof?
:return: str
"""
if self._result is None:
raise LookupError("You have to call prove() first to get a proof!")
else:
return self.decorate_proof(self._proof, simplify)
def decorate_proof(self, proof_string, simplify=True):
"""
Modify and return the proof string
:param proof_string: str the proof to decorate
:param simplify: bool simplify the proof?
:return: str
"""
return self._command.decorate_proof(proof_string, simplify)
def get_prover(self):
return self._command.get_prover()
class ModelBuilderCommandDecorator(TheoremToolCommandDecorator, ModelBuilderCommand):
"""
A base decorator for the ``ModelBuilderCommand`` class from which other
prover command decorators can extend.
"""
def __init__(self, modelBuilderCommand):
"""
:param modelBuilderCommand: ``ModelBuilderCommand`` to decorate
"""
TheoremToolCommandDecorator.__init__(self, modelBuilderCommand)
# The decorator has its own versions of 'result' and 'valuation'
# because they may be different from the underlying command
self._model = None
def build_model(self, verbose=False):
"""
Attempt to build a model. Store the result to prevent unnecessary
re-building.
"""
if self._result is None:
modelbuilder = self.get_model_builder()
self._result, self._model = modelbuilder._build_model(
self.goal(), self.assumptions(), verbose
)
return self._result
def model(self, format=None):
"""
Return a string representation of the model
:param simplify: bool simplify the proof?
:return: str
"""
if self._result is None:
raise LookupError("You have to call build_model() first to " "get a model!")
else:
return self._decorate_model(self._model, format)
def _decorate_model(self, valuation_str, format=None):
"""
Modify and return the proof string
:param valuation_str: str with the model builder's output
:param format: str indicating the format for displaying
:return: str
"""
return self._command._decorate_model(valuation_str, format)
def get_model_builder(self):
return self._command.get_prover()
class ParallelProverBuilder(Prover, ModelBuilder):
"""
This class stores both a prover and a model builder and when either
prove() or build_model() is called, then both theorem tools are run in
parallel. Whichever finishes first, the prover or the model builder, is the
result that will be used.
"""
def __init__(self, prover, modelbuilder):
self._prover = prover
self._modelbuilder = modelbuilder
def _prove(self, goal=None, assumptions=None, verbose=False):
return self._run(goal, assumptions, verbose), ""
def _build_model(self, goal=None, assumptions=None, verbose=False):
return not self._run(goal, assumptions, verbose), ""
def _run(self, goal, assumptions, verbose):
# Set up two thread, Prover and ModelBuilder to run in parallel
tp_thread = TheoremToolThread(
lambda: self._prover.prove(goal, assumptions, verbose), verbose, "TP"
)
mb_thread = TheoremToolThread(
lambda: self._modelbuilder.build_model(goal, assumptions, verbose),
verbose,
"MB",
)
tp_thread.start()
mb_thread.start()
while tp_thread.is_alive() and mb_thread.is_alive():
# wait until either the prover or the model builder is done
pass
if tp_thread.result is not None:
return tp_thread.result
elif mb_thread.result is not None:
return not mb_thread.result
else:
return None
class ParallelProverBuilderCommand(BaseProverCommand, BaseModelBuilderCommand):
"""
This command stores both a prover and a model builder and when either
prove() or build_model() is called, then both theorem tools are run in
parallel. Whichever finishes first, the prover or the model builder, is the
result that will be used.
Because the theorem prover result is the opposite of the model builder
result, we will treat self._result as meaning "proof found/no model found".
"""
def __init__(self, prover, modelbuilder, goal=None, assumptions=None):
BaseProverCommand.__init__(self, prover, goal, assumptions)
BaseModelBuilderCommand.__init__(self, modelbuilder, goal, assumptions)
def prove(self, verbose=False):
return self._run(verbose)
def build_model(self, verbose=False):
return not self._run(verbose)
def _run(self, verbose):
# Set up two thread, Prover and ModelBuilder to run in parallel
tp_thread = TheoremToolThread(
lambda: BaseProverCommand.prove(self, verbose), verbose, "TP"
)
mb_thread = TheoremToolThread(
lambda: BaseModelBuilderCommand.build_model(self, verbose), verbose, "MB"
)
tp_thread.start()
mb_thread.start()
while tp_thread.is_alive() and mb_thread.is_alive():
# wait until either the prover or the model builder is done
pass
if tp_thread.result is not None:
self._result = tp_thread.result
elif mb_thread.result is not None:
self._result = not mb_thread.result
return self._result
class TheoremToolThread(threading.Thread):
def __init__(self, command, verbose, name=None):
threading.Thread.__init__(self)
self._command = command
self._result = None
self._verbose = verbose
self._name = name
def run(self):
try:
self._result = self._command()
if self._verbose:
print(
"Thread %s finished with result %s at %s"
% (self._name, self._result, time.localtime(time.time()))
)
except Exception as e:
print(e)
print("Thread %s completed abnormally" % (self._name))
@property
def result(self):
return self._result

View File

@@ -0,0 +1,651 @@
# Natural Language Toolkit: Discourse Processing
#
# Author: Ewan Klein <ewan@inf.ed.ac.uk>
# Dan Garrette <dhgarrette@gmail.com>
#
# URL: <https://www.nltk.org/>
# For license information, see LICENSE.TXT
r"""
Module for incrementally developing simple discourses, and checking for semantic ambiguity,
consistency and informativeness.
Many of the ideas are based on the CURT family of programs of Blackburn and Bos
(see http://homepages.inf.ed.ac.uk/jbos/comsem/book1.html).
Consistency checking is carried out by using the ``mace`` module to call the Mace4 model builder.
Informativeness checking is carried out with a call to ``Prover.prove()`` from
the ``inference`` module.
``DiscourseTester`` is a constructor for discourses.
The basic data structure is a list of sentences, stored as ``self._sentences``. Each sentence in the list
is assigned a "sentence ID" (``sid``) of the form ``s``\ *i*. For example::
s0: A boxer walks
s1: Every boxer chases a girl
Each sentence can be ambiguous between a number of readings, each of which receives a
"reading ID" (``rid``) of the form ``s``\ *i* -``r``\ *j*. For example::
s0 readings:
s0-r1: some x.(boxer(x) & walk(x))
s0-r0: some x.(boxerdog(x) & walk(x))
A "thread" is a list of readings, represented as a list of ``rid``\ s.
Each thread receives a "thread ID" (``tid``) of the form ``d``\ *i*.
For example::
d0: ['s0-r0', 's1-r0']
The set of all threads for a discourse is the Cartesian product of all the readings of the sequences of sentences.
(This is not intended to scale beyond very short discourses!) The method ``readings(filter=True)`` will only show
those threads which are consistent (taking into account any background assumptions).
"""
import os
from abc import ABCMeta, abstractmethod
from functools import reduce
from operator import add, and_
from nltk.data import show_cfg
from nltk.inference.mace import MaceCommand
from nltk.inference.prover9 import Prover9Command
from nltk.parse import load_parser
from nltk.parse.malt import MaltParser
from nltk.sem.drt import AnaphoraResolutionException, resolve_anaphora
from nltk.sem.glue import DrtGlue
from nltk.sem.logic import Expression
from nltk.tag import RegexpTagger
class ReadingCommand(metaclass=ABCMeta):
@abstractmethod
def parse_to_readings(self, sentence):
"""
:param sentence: the sentence to read
:type sentence: str
"""
def process_thread(self, sentence_readings):
"""
This method should be used to handle dependencies between readings such
as resolving anaphora.
:param sentence_readings: readings to process
:type sentence_readings: list(Expression)
:return: the list of readings after processing
:rtype: list(Expression)
"""
return sentence_readings
@abstractmethod
def combine_readings(self, readings):
"""
:param readings: readings to combine
:type readings: list(Expression)
:return: one combined reading
:rtype: Expression
"""
@abstractmethod
def to_fol(self, expression):
"""
Convert this expression into a First-Order Logic expression.
:param expression: an expression
:type expression: Expression
:return: a FOL version of the input expression
:rtype: Expression
"""
class CfgReadingCommand(ReadingCommand):
def __init__(self, gramfile=None):
"""
:param gramfile: name of file where grammar can be loaded
:type gramfile: str
"""
self._gramfile = (
gramfile if gramfile else "grammars/book_grammars/discourse.fcfg"
)
self._parser = load_parser(self._gramfile)
def parse_to_readings(self, sentence):
""":see: ReadingCommand.parse_to_readings()"""
from nltk.sem import root_semrep
tokens = sentence.split()
trees = self._parser.parse(tokens)
return [root_semrep(tree) for tree in trees]
def combine_readings(self, readings):
""":see: ReadingCommand.combine_readings()"""
return reduce(and_, readings)
def to_fol(self, expression):
""":see: ReadingCommand.to_fol()"""
return expression
class DrtGlueReadingCommand(ReadingCommand):
def __init__(self, semtype_file=None, remove_duplicates=False, depparser=None):
"""
:param semtype_file: name of file where grammar can be loaded
:param remove_duplicates: should duplicates be removed?
:param depparser: the dependency parser
"""
if semtype_file is None:
semtype_file = os.path.join(
"grammars", "sample_grammars", "drt_glue.semtype"
)
self._glue = DrtGlue(
semtype_file=semtype_file,
remove_duplicates=remove_duplicates,
depparser=depparser,
)
def parse_to_readings(self, sentence):
""":see: ReadingCommand.parse_to_readings()"""
return self._glue.parse_to_meaning(sentence)
def process_thread(self, sentence_readings):
""":see: ReadingCommand.process_thread()"""
try:
return [self.combine_readings(sentence_readings)]
except AnaphoraResolutionException:
return []
def combine_readings(self, readings):
""":see: ReadingCommand.combine_readings()"""
thread_reading = reduce(add, readings)
return resolve_anaphora(thread_reading.simplify())
def to_fol(self, expression):
""":see: ReadingCommand.to_fol()"""
return expression.fol()
class DiscourseTester:
"""
Check properties of an ongoing discourse.
"""
def __init__(self, input, reading_command=None, background=None):
"""
Initialize a ``DiscourseTester``.
:param input: the discourse sentences
:type input: list of str
:param background: Formulas which express background assumptions
:type background: list(Expression)
"""
self._input = input
self._sentences = {"s%s" % i: sent for i, sent in enumerate(input)}
self._models = None
self._readings = {}
self._reading_command = (
reading_command if reading_command else CfgReadingCommand()
)
self._threads = {}
self._filtered_threads = {}
if background is not None:
from nltk.sem.logic import Expression
for e in background:
assert isinstance(e, Expression)
self._background = background
else:
self._background = []
###############################
# Sentences
###############################
def sentences(self):
"""
Display the list of sentences in the current discourse.
"""
for id in sorted(self._sentences):
print(f"{id}: {self._sentences[id]}")
def add_sentence(self, sentence, informchk=False, consistchk=False):
"""
Add a sentence to the current discourse.
Updates ``self._input`` and ``self._sentences``.
:param sentence: An input sentence
:type sentence: str
:param informchk: if ``True``, check that the result of adding the sentence is thread-informative. Updates ``self._readings``.
:param consistchk: if ``True``, check that the result of adding the sentence is thread-consistent. Updates ``self._readings``.
"""
# check whether the new sentence is informative (i.e. not entailed by the previous discourse)
if informchk:
self.readings(verbose=False)
for tid in sorted(self._threads):
assumptions = [reading for (rid, reading) in self.expand_threads(tid)]
assumptions += self._background
for sent_reading in self._get_readings(sentence):
tp = Prover9Command(goal=sent_reading, assumptions=assumptions)
if tp.prove():
print(
"Sentence '%s' under reading '%s':"
% (sentence, str(sent_reading))
)
print("Not informative relative to thread '%s'" % tid)
self._input.append(sentence)
self._sentences = {"s%s" % i: sent for i, sent in enumerate(self._input)}
# check whether adding the new sentence to the discourse preserves consistency (i.e. a model can be found for the combined set of
# of assumptions
if consistchk:
self.readings(verbose=False)
self.models(show=False)
def retract_sentence(self, sentence, verbose=True):
"""
Remove a sentence from the current discourse.
Updates ``self._input``, ``self._sentences`` and ``self._readings``.
:param sentence: An input sentence
:type sentence: str
:param verbose: If ``True``, report on the updated list of sentences.
"""
try:
self._input.remove(sentence)
except ValueError:
print(
"Retraction failed. The sentence '%s' is not part of the current discourse:"
% sentence
)
self.sentences()
return None
self._sentences = {"s%s" % i: sent for i, sent in enumerate(self._input)}
self.readings(verbose=False)
if verbose:
print("Current sentences are ")
self.sentences()
def grammar(self):
"""
Print out the grammar in use for parsing input sentences
"""
show_cfg(self._reading_command._gramfile)
###############################
# Readings and Threads
###############################
def _get_readings(self, sentence):
"""
Build a list of semantic readings for a sentence.
:rtype: list(Expression)
"""
return self._reading_command.parse_to_readings(sentence)
def _construct_readings(self):
"""
Use ``self._sentences`` to construct a value for ``self._readings``.
"""
# re-initialize self._readings in case we have retracted a sentence
self._readings = {}
for sid in sorted(self._sentences):
sentence = self._sentences[sid]
readings = self._get_readings(sentence)
self._readings[sid] = {
f"{sid}-r{rid}": reading.simplify()
for rid, reading in enumerate(sorted(readings, key=str))
}
def _construct_threads(self):
"""
Use ``self._readings`` to construct a value for ``self._threads``
and use the model builder to construct a value for ``self._filtered_threads``
"""
thread_list = [[]]
for sid in sorted(self._readings):
thread_list = self.multiply(thread_list, sorted(self._readings[sid]))
self._threads = {"d%s" % tid: thread for tid, thread in enumerate(thread_list)}
# re-initialize the filtered threads
self._filtered_threads = {}
# keep the same ids, but only include threads which get models
consistency_checked = self._check_consistency(self._threads)
for tid, thread in self._threads.items():
if (tid, True) in consistency_checked:
self._filtered_threads[tid] = thread
def _show_readings(self, sentence=None):
"""
Print out the readings for the discourse (or a single sentence).
"""
if sentence is not None:
print("The sentence '%s' has these readings:" % sentence)
for r in [str(reading) for reading in (self._get_readings(sentence))]:
print(" %s" % r)
else:
for sid in sorted(self._readings):
print()
print("%s readings:" % sid)
print() #'-' * 30
for rid in sorted(self._readings[sid]):
lf = self._readings[sid][rid]
print(f"{rid}: {lf.normalize()}")
def _show_threads(self, filter=False, show_thread_readings=False):
"""
Print out the value of ``self._threads`` or ``self._filtered_hreads``
"""
threads = self._filtered_threads if filter else self._threads
for tid in sorted(threads):
if show_thread_readings:
readings = [
self._readings[rid.split("-")[0]][rid] for rid in self._threads[tid]
]
try:
thread_reading = (
": %s"
% self._reading_command.combine_readings(readings).normalize()
)
except Exception as e:
thread_reading = ": INVALID: %s" % e.__class__.__name__
else:
thread_reading = ""
print("%s:" % tid, self._threads[tid], thread_reading)
def readings(
self,
sentence=None,
threaded=False,
verbose=True,
filter=False,
show_thread_readings=False,
):
"""
Construct and show the readings of the discourse (or of a single sentence).
:param sentence: test just this sentence
:type sentence: str
:param threaded: if ``True``, print out each thread ID and the corresponding thread.
:param filter: if ``True``, only print out consistent thread IDs and threads.
"""
self._construct_readings()
self._construct_threads()
# if we are filtering or showing thread readings, show threads
if filter or show_thread_readings:
threaded = True
if verbose:
if not threaded:
self._show_readings(sentence=sentence)
else:
self._show_threads(
filter=filter, show_thread_readings=show_thread_readings
)
def expand_threads(self, thread_id, threads=None):
"""
Given a thread ID, find the list of ``logic.Expression`` objects corresponding to the reading IDs in that thread.
:param thread_id: thread ID
:type thread_id: str
:param threads: a mapping from thread IDs to lists of reading IDs
:type threads: dict
:return: A list of pairs ``(rid, reading)`` where reading is the ``logic.Expression`` associated with a reading ID
:rtype: list of tuple
"""
if threads is None:
threads = self._threads
return [
(rid, self._readings[sid][rid])
for rid in threads[thread_id]
for sid in rid.split("-")[:1]
]
###############################
# Models and Background
###############################
def _check_consistency(self, threads, show=False, verbose=False):
results = []
for tid in sorted(threads):
assumptions = [
reading for (rid, reading) in self.expand_threads(tid, threads=threads)
]
assumptions = list(
map(
self._reading_command.to_fol,
self._reading_command.process_thread(assumptions),
)
)
if assumptions:
assumptions += self._background
# if Mace4 finds a model, it always seems to find it quickly
mb = MaceCommand(None, assumptions, max_models=20)
modelfound = mb.build_model()
else:
modelfound = False
results.append((tid, modelfound))
if show:
spacer(80)
print("Model for Discourse Thread %s" % tid)
spacer(80)
if verbose:
for a in assumptions:
print(a)
spacer(80)
if modelfound:
print(mb.model(format="cooked"))
else:
print("No model found!\n")
return results
def models(self, thread_id=None, show=True, verbose=False):
"""
Call Mace4 to build a model for each current discourse thread.
:param thread_id: thread ID
:type thread_id: str
:param show: If ``True``, display the model that has been found.
"""
self._construct_readings()
self._construct_threads()
threads = {thread_id: self._threads[thread_id]} if thread_id else self._threads
for tid, modelfound in self._check_consistency(
threads, show=show, verbose=verbose
):
idlist = [rid for rid in threads[tid]]
if not modelfound:
print(f"Inconsistent discourse: {tid} {idlist}:")
for rid, reading in self.expand_threads(tid):
print(f" {rid}: {reading.normalize()}")
print()
else:
print(f"Consistent discourse: {tid} {idlist}:")
for rid, reading in self.expand_threads(tid):
print(f" {rid}: {reading.normalize()}")
print()
def add_background(self, background, verbose=False):
"""
Add a list of background assumptions for reasoning about the discourse.
When called, this method also updates the discourse model's set of readings and threads.
:param background: Formulas which contain background information
:type background: list(Expression)
"""
from nltk.sem.logic import Expression
for count, e in enumerate(background):
assert isinstance(e, Expression)
if verbose:
print("Adding assumption %s to background" % count)
self._background.append(e)
# update the state
self._construct_readings()
self._construct_threads()
def background(self):
"""
Show the current background assumptions.
"""
for e in self._background:
print(str(e))
###############################
# Misc
###############################
@staticmethod
def multiply(discourse, readings):
"""
Multiply every thread in ``discourse`` by every reading in ``readings``.
Given discourse = [['A'], ['B']], readings = ['a', 'b', 'c'] , returns
[['A', 'a'], ['A', 'b'], ['A', 'c'], ['B', 'a'], ['B', 'b'], ['B', 'c']]
:param discourse: the current list of readings
:type discourse: list of lists
:param readings: an additional list of readings
:type readings: list(Expression)
:rtype: A list of lists
"""
result = []
for sublist in discourse:
for r in readings:
new = []
new += sublist
new.append(r)
result.append(new)
return result
def load_fol(s):
"""
Temporarily duplicated from ``nltk.sem.util``.
Convert a file of first order formulas into a list of ``Expression`` objects.
:param s: the contents of the file
:type s: str
:return: a list of parsed formulas.
:rtype: list(Expression)
"""
statements = []
for linenum, line in enumerate(s.splitlines()):
line = line.strip()
if line.startswith("#") or line == "":
continue
try:
statements.append(Expression.fromstring(line))
except Exception as e:
raise ValueError(f"Unable to parse line {linenum}: {line}") from e
return statements
###############################
# Demo
###############################
def discourse_demo(reading_command=None):
"""
Illustrate the various methods of ``DiscourseTester``
"""
dt = DiscourseTester(
["A boxer walks", "Every boxer chases a girl"], reading_command
)
dt.models()
print()
# dt.grammar()
print()
dt.sentences()
print()
dt.readings()
print()
dt.readings(threaded=True)
print()
dt.models("d1")
dt.add_sentence("John is a boxer")
print()
dt.sentences()
print()
dt.readings(threaded=True)
print()
dt = DiscourseTester(
["A student dances", "Every student is a person"], reading_command
)
print()
dt.add_sentence("No person dances", consistchk=True)
print()
dt.readings()
print()
dt.retract_sentence("No person dances", verbose=True)
print()
dt.models()
print()
dt.readings("A person dances")
print()
dt.add_sentence("A person dances", informchk=True)
dt = DiscourseTester(
["Vincent is a boxer", "Fido is a boxer", "Vincent is married", "Fido barks"],
reading_command,
)
dt.readings(filter=True)
import nltk.data
background_file = os.path.join("grammars", "book_grammars", "background.fol")
background = nltk.data.load(background_file)
print()
dt.add_background(background, verbose=False)
dt.background()
print()
dt.readings(filter=True)
print()
dt.models()
def drt_discourse_demo(reading_command=None):
"""
Illustrate the various methods of ``DiscourseTester``
"""
dt = DiscourseTester(["every dog chases a boy", "he runs"], reading_command)
dt.models()
print()
dt.sentences()
print()
dt.readings()
print()
dt.readings(show_thread_readings=True)
print()
dt.readings(filter=True, show_thread_readings=True)
def spacer(num=30):
print("-" * num)
def demo():
discourse_demo()
tagger = RegexpTagger(
[
("^(chases|runs)$", "VB"),
("^(a)$", "ex_quant"),
("^(every)$", "univ_quant"),
("^(dog|boy)$", "NN"),
("^(he)$", "PRP"),
]
)
depparser = MaltParser(tagger=tagger)
drt_discourse_demo(
DrtGlueReadingCommand(remove_duplicates=False, depparser=depparser)
)
if __name__ == "__main__":
demo()

View File

@@ -0,0 +1,383 @@
# Natural Language Toolkit: Interface to the Mace4 Model Builder
#
# Author: Dan Garrette <dhgarrette@gmail.com>
# Ewan Klein <ewan@inf.ed.ac.uk>
# URL: <https://www.nltk.org/>
# For license information, see LICENSE.TXT
"""
A model builder that makes use of the external 'Mace4' package.
"""
import os
import tempfile
from nltk.inference.api import BaseModelBuilderCommand, ModelBuilder
from nltk.inference.prover9 import Prover9CommandParent, Prover9Parent
from nltk.sem import Expression, Valuation
from nltk.sem.logic import is_indvar
class MaceCommand(Prover9CommandParent, BaseModelBuilderCommand):
"""
A ``MaceCommand`` specific to the ``Mace`` model builder. It contains
a print_assumptions() method that is used to print the list
of assumptions in multiple formats.
"""
_interpformat_bin = None
def __init__(self, goal=None, assumptions=None, max_models=500, model_builder=None):
"""
:param goal: Input expression to prove
:type goal: sem.Expression
:param assumptions: Input expressions to use as assumptions in
the proof.
:type assumptions: list(sem.Expression)
:param max_models: The maximum number of models that Mace will try before
simply returning false. (Use 0 for no maximum.)
:type max_models: int
"""
if model_builder is not None:
assert isinstance(model_builder, Mace)
else:
model_builder = Mace(max_models)
BaseModelBuilderCommand.__init__(self, model_builder, goal, assumptions)
@property
def valuation(mbc):
return mbc.model("valuation")
def _convert2val(self, valuation_str):
"""
Transform the output file into an NLTK-style Valuation.
:return: A model if one is generated; None otherwise.
:rtype: sem.Valuation
"""
valuation_standard_format = self._transform_output(valuation_str, "standard")
val = []
for line in valuation_standard_format.splitlines(False):
l = line.strip()
if l.startswith("interpretation"):
# find the number of entities in the model
num_entities = int(l[l.index("(") + 1 : l.index(",")].strip())
elif l.startswith("function") and l.find("_") == -1:
# replace the integer identifier with a corresponding alphabetic character
name = l[l.index("(") + 1 : l.index(",")].strip()
if is_indvar(name):
name = name.upper()
value = int(l[l.index("[") + 1 : l.index("]")].strip())
val.append((name, MaceCommand._make_model_var(value)))
elif l.startswith("relation"):
l = l[l.index("(") + 1 :]
if "(" in l:
# relation is not nullary
name = l[: l.index("(")].strip()
values = [
int(v.strip())
for v in l[l.index("[") + 1 : l.index("]")].split(",")
]
val.append(
(name, MaceCommand._make_relation_set(num_entities, values))
)
else:
# relation is nullary
name = l[: l.index(",")].strip()
value = int(l[l.index("[") + 1 : l.index("]")].strip())
val.append((name, value == 1))
return Valuation(val)
@staticmethod
def _make_relation_set(num_entities, values):
"""
Convert a Mace4-style relation table into a dictionary.
:param num_entities: the number of entities in the model; determines the row length in the table.
:type num_entities: int
:param values: a list of 1's and 0's that represent whether a relation holds in a Mace4 model.
:type values: list of int
"""
r = set()
for position in [pos for (pos, v) in enumerate(values) if v == 1]:
r.add(
tuple(MaceCommand._make_relation_tuple(position, values, num_entities))
)
return r
@staticmethod
def _make_relation_tuple(position, values, num_entities):
if len(values) == 1:
return []
else:
sublist_size = len(values) // num_entities
sublist_start = position // sublist_size
sublist_position = int(position % sublist_size)
sublist = values[
sublist_start * sublist_size : (sublist_start + 1) * sublist_size
]
return [
MaceCommand._make_model_var(sublist_start)
] + MaceCommand._make_relation_tuple(
sublist_position, sublist, num_entities
)
@staticmethod
def _make_model_var(value):
"""
Pick an alphabetic character as identifier for an entity in the model.
:param value: where to index into the list of characters
:type value: int
"""
letter = [
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
][value]
num = value // 26
return letter + str(num) if num > 0 else letter
def _decorate_model(self, valuation_str, format):
"""
Print out a Mace4 model using any Mace4 ``interpformat`` format.
See https://www.cs.unm.edu/~mccune/mace4/manual/ for details.
:param valuation_str: str with the model builder's output
:param format: str indicating the format for displaying
models. Defaults to 'standard' format.
:return: str
"""
if not format:
return valuation_str
elif format == "valuation":
return self._convert2val(valuation_str)
else:
return self._transform_output(valuation_str, format)
def _transform_output(self, valuation_str, format):
"""
Transform the output file into any Mace4 ``interpformat`` format.
:param format: Output format for displaying models.
:type format: str
"""
if format in [
"standard",
"standard2",
"portable",
"tabular",
"raw",
"cooked",
"xml",
"tex",
]:
return self._call_interpformat(valuation_str, [format])[0]
else:
raise LookupError("The specified format does not exist")
def _call_interpformat(self, input_str, args=[], verbose=False):
"""
Call the ``interpformat`` binary with the given input.
:param input_str: A string whose contents are used as stdin.
:param args: A list of command-line arguments.
:return: A tuple (stdout, returncode)
:see: ``config_prover9``
"""
if self._interpformat_bin is None:
self._interpformat_bin = self._modelbuilder._find_binary(
"interpformat", verbose
)
return self._modelbuilder._call(
input_str, self._interpformat_bin, args, verbose
)
class Mace(Prover9Parent, ModelBuilder):
_mace4_bin = None
def __init__(self, end_size=500):
self._end_size = end_size
"""The maximum model size that Mace will try before
simply returning false. (Use -1 for no maximum.)"""
def _build_model(self, goal=None, assumptions=None, verbose=False):
"""
Use Mace4 to build a first order model.
:return: ``True`` if a model was found (i.e. Mace returns value of 0),
else ``False``
"""
if not assumptions:
assumptions = []
stdout, returncode = self._call_mace4(
self.prover9_input(goal, assumptions), verbose=verbose
)
return (returncode == 0, stdout)
def _call_mace4(self, input_str, args=[], verbose=False):
"""
Call the ``mace4`` binary with the given input.
:param input_str: A string whose contents are used as stdin.
:param args: A list of command-line arguments.
:return: A tuple (stdout, returncode)
:see: ``config_prover9``
"""
if self._mace4_bin is None:
self._mace4_bin = self._find_binary("mace4", verbose)
updated_input_str = ""
if self._end_size > 0:
updated_input_str += "assign(end_size, %d).\n\n" % self._end_size
updated_input_str += input_str
return self._call(updated_input_str, self._mace4_bin, args, verbose)
def spacer(num=30):
print("-" * num)
def decode_result(found):
"""
Decode the result of model_found()
:param found: The output of model_found()
:type found: bool
"""
return {True: "Countermodel found", False: "No countermodel found", None: "None"}[
found
]
def test_model_found(arguments):
"""
Try some proofs and exhibit the results.
"""
for goal, assumptions in arguments:
g = Expression.fromstring(goal)
alist = [lp.parse(a) for a in assumptions]
m = MaceCommand(g, assumptions=alist, max_models=50)
found = m.build_model()
for a in alist:
print(" %s" % a)
print(f"|- {g}: {decode_result(found)}\n")
def test_build_model(arguments):
"""
Try to build a ``nltk.sem.Valuation``.
"""
g = Expression.fromstring("all x.man(x)")
alist = [
Expression.fromstring(a)
for a in [
"man(John)",
"man(Socrates)",
"man(Bill)",
"some x.(-(x = John) & man(x) & sees(John,x))",
"some x.(-(x = Bill) & man(x))",
"all x.some y.(man(x) -> gives(Socrates,x,y))",
]
]
m = MaceCommand(g, assumptions=alist)
m.build_model()
spacer()
print("Assumptions and Goal")
spacer()
for a in alist:
print(" %s" % a)
print(f"|- {g}: {decode_result(m.build_model())}\n")
spacer()
# print(m.model('standard'))
# print(m.model('cooked'))
print("Valuation")
spacer()
print(m.valuation, "\n")
def test_transform_output(argument_pair):
"""
Transform the model into various Mace4 ``interpformat`` formats.
"""
g = Expression.fromstring(argument_pair[0])
alist = [lp.parse(a) for a in argument_pair[1]]
m = MaceCommand(g, assumptions=alist)
m.build_model()
for a in alist:
print(" %s" % a)
print(f"|- {g}: {m.build_model()}\n")
for format in ["standard", "portable", "xml", "cooked"]:
spacer()
print("Using '%s' format" % format)
spacer()
print(m.model(format=format))
def test_make_relation_set():
print(
MaceCommand._make_relation_set(num_entities=3, values=[1, 0, 1])
== {("c",), ("a",)}
)
print(
MaceCommand._make_relation_set(
num_entities=3, values=[0, 0, 0, 0, 0, 0, 1, 0, 0]
)
== {("c", "a")}
)
print(
MaceCommand._make_relation_set(num_entities=2, values=[0, 0, 1, 0, 0, 0, 1, 0])
== {("a", "b", "a"), ("b", "b", "a")}
)
arguments = [
("mortal(Socrates)", ["all x.(man(x) -> mortal(x))", "man(Socrates)"]),
("(not mortal(Socrates))", ["all x.(man(x) -> mortal(x))", "man(Socrates)"]),
]
def demo():
test_model_found(arguments)
test_build_model(arguments)
test_transform_output(arguments[1])
if __name__ == "__main__":
demo()

View File

@@ -0,0 +1,561 @@
# Natural Language Toolkit: Nonmonotonic Reasoning
#
# Author: Daniel H. Garrette <dhgarrette@gmail.com>
#
# Copyright (C) 2001-2025 NLTK Project
# URL: <https://www.nltk.org/>
# For license information, see LICENSE.TXT
"""
A module to perform nonmonotonic reasoning. The ideas and demonstrations in
this module are based on "Logical Foundations of Artificial Intelligence" by
Michael R. Genesereth and Nils J. Nilsson.
"""
from collections import defaultdict
from functools import reduce
from nltk.inference.api import Prover, ProverCommandDecorator
from nltk.inference.prover9 import Prover9, Prover9Command
from nltk.sem.logic import (
AbstractVariableExpression,
AllExpression,
AndExpression,
ApplicationExpression,
BooleanExpression,
EqualityExpression,
ExistsExpression,
Expression,
ImpExpression,
NegatedExpression,
Variable,
VariableExpression,
operator,
unique_variable,
)
class ProverParseError(Exception):
pass
def get_domain(goal, assumptions):
if goal is None:
all_expressions = assumptions
else:
all_expressions = assumptions + [-goal]
return reduce(operator.or_, (a.constants() for a in all_expressions), set())
class ClosedDomainProver(ProverCommandDecorator):
"""
This is a prover decorator that adds domain closure assumptions before
proving.
"""
def assumptions(self):
assumptions = [a for a in self._command.assumptions()]
goal = self._command.goal()
domain = get_domain(goal, assumptions)
return [self.replace_quants(ex, domain) for ex in assumptions]
def goal(self):
goal = self._command.goal()
domain = get_domain(goal, self._command.assumptions())
return self.replace_quants(goal, domain)
def replace_quants(self, ex, domain):
"""
Apply the closed domain assumption to the expression
- Domain = union([e.free()|e.constants() for e in all_expressions])
- translate "exists x.P" to "(z=d1 | z=d2 | ... ) & P.replace(x,z)" OR
"P.replace(x, d1) | P.replace(x, d2) | ..."
- translate "all x.P" to "P.replace(x, d1) & P.replace(x, d2) & ..."
:param ex: ``Expression``
:param domain: set of {Variable}s
:return: ``Expression``
"""
if isinstance(ex, AllExpression):
conjuncts = [
ex.term.replace(ex.variable, VariableExpression(d)) for d in domain
]
conjuncts = [self.replace_quants(c, domain) for c in conjuncts]
return reduce(lambda x, y: x & y, conjuncts)
elif isinstance(ex, BooleanExpression):
return ex.__class__(
self.replace_quants(ex.first, domain),
self.replace_quants(ex.second, domain),
)
elif isinstance(ex, NegatedExpression):
return -self.replace_quants(ex.term, domain)
elif isinstance(ex, ExistsExpression):
disjuncts = [
ex.term.replace(ex.variable, VariableExpression(d)) for d in domain
]
disjuncts = [self.replace_quants(d, domain) for d in disjuncts]
return reduce(lambda x, y: x | y, disjuncts)
else:
return ex
class UniqueNamesProver(ProverCommandDecorator):
"""
This is a prover decorator that adds unique names assumptions before
proving.
"""
def assumptions(self):
"""
- Domain = union([e.free()|e.constants() for e in all_expressions])
- if "d1 = d2" cannot be proven from the premises, then add "d1 != d2"
"""
assumptions = self._command.assumptions()
domain = list(get_domain(self._command.goal(), assumptions))
# build a dictionary of obvious equalities
eq_sets = SetHolder()
for a in assumptions:
if isinstance(a, EqualityExpression):
av = a.first.variable
bv = a.second.variable
# put 'a' and 'b' in the same set
eq_sets[av].add(bv)
new_assumptions = []
for i, a in enumerate(domain):
for b in domain[i + 1 :]:
# if a and b are not already in the same equality set
if b not in eq_sets[a]:
newEqEx = EqualityExpression(
VariableExpression(a), VariableExpression(b)
)
if Prover9().prove(newEqEx, assumptions):
# we can prove that the names are the same entity.
# remember that they are equal so we don't re-check.
eq_sets[a].add(b)
else:
# we can't prove it, so assume unique names
new_assumptions.append(-newEqEx)
return assumptions + new_assumptions
class SetHolder(list):
"""
A list of sets of Variables.
"""
def __getitem__(self, item):
"""
:param item: ``Variable``
:return: the set containing 'item'
"""
assert isinstance(item, Variable)
for s in self:
if item in s:
return s
# item is not found in any existing set. so create a new set
new = {item}
self.append(new)
return new
class ClosedWorldProver(ProverCommandDecorator):
"""
This is a prover decorator that completes predicates before proving.
If the assumptions contain "P(A)", then "all x.(P(x) -> (x=A))" is the completion of "P".
If the assumptions contain "all x.(ostrich(x) -> bird(x))", then "all x.(bird(x) -> ostrich(x))" is the completion of "bird".
If the assumptions don't contain anything that are "P", then "all x.-P(x)" is the completion of "P".
walk(Socrates)
Socrates != Bill
+ all x.(walk(x) -> (x=Socrates))
----------------
-walk(Bill)
see(Socrates, John)
see(John, Mary)
Socrates != John
John != Mary
+ all x.all y.(see(x,y) -> ((x=Socrates & y=John) | (x=John & y=Mary)))
----------------
-see(Socrates, Mary)
all x.(ostrich(x) -> bird(x))
bird(Tweety)
-ostrich(Sam)
Sam != Tweety
+ all x.(bird(x) -> (ostrich(x) | x=Tweety))
+ all x.-ostrich(x)
-------------------
-bird(Sam)
"""
def assumptions(self):
assumptions = self._command.assumptions()
predicates = self._make_predicate_dict(assumptions)
new_assumptions = []
for p in predicates:
predHolder = predicates[p]
new_sig = self._make_unique_signature(predHolder)
new_sig_exs = [VariableExpression(v) for v in new_sig]
disjuncts = []
# Turn the signatures into disjuncts
for sig in predHolder.signatures:
equality_exs = []
for v1, v2 in zip(new_sig_exs, sig):
equality_exs.append(EqualityExpression(v1, v2))
disjuncts.append(reduce(lambda x, y: x & y, equality_exs))
# Turn the properties into disjuncts
for prop in predHolder.properties:
# replace variables from the signature with new sig variables
bindings = {}
for v1, v2 in zip(new_sig_exs, prop[0]):
bindings[v2] = v1
disjuncts.append(prop[1].substitute_bindings(bindings))
# make the assumption
if disjuncts:
# disjuncts exist, so make an implication
antecedent = self._make_antecedent(p, new_sig)
consequent = reduce(lambda x, y: x | y, disjuncts)
accum = ImpExpression(antecedent, consequent)
else:
# nothing has property 'p'
accum = NegatedExpression(self._make_antecedent(p, new_sig))
# quantify the implication
for new_sig_var in new_sig[::-1]:
accum = AllExpression(new_sig_var, accum)
new_assumptions.append(accum)
return assumptions + new_assumptions
def _make_unique_signature(self, predHolder):
"""
This method figures out how many arguments the predicate takes and
returns a tuple containing that number of unique variables.
"""
return tuple(unique_variable() for i in range(predHolder.signature_len))
def _make_antecedent(self, predicate, signature):
"""
Return an application expression with 'predicate' as the predicate
and 'signature' as the list of arguments.
"""
antecedent = predicate
for v in signature:
antecedent = antecedent(VariableExpression(v))
return antecedent
def _make_predicate_dict(self, assumptions):
"""
Create a dictionary of predicates from the assumptions.
:param assumptions: a list of ``Expression``s
:return: dict mapping ``AbstractVariableExpression`` to ``PredHolder``
"""
predicates = defaultdict(PredHolder)
for a in assumptions:
self._map_predicates(a, predicates)
return predicates
def _map_predicates(self, expression, predDict):
if isinstance(expression, ApplicationExpression):
func, args = expression.uncurry()
if isinstance(func, AbstractVariableExpression):
predDict[func].append_sig(tuple(args))
elif isinstance(expression, AndExpression):
self._map_predicates(expression.first, predDict)
self._map_predicates(expression.second, predDict)
elif isinstance(expression, AllExpression):
# collect all the universally quantified variables
sig = [expression.variable]
term = expression.term
while isinstance(term, AllExpression):
sig.append(term.variable)
term = term.term
if isinstance(term, ImpExpression):
if isinstance(term.first, ApplicationExpression) and isinstance(
term.second, ApplicationExpression
):
func1, args1 = term.first.uncurry()
func2, args2 = term.second.uncurry()
if (
isinstance(func1, AbstractVariableExpression)
and isinstance(func2, AbstractVariableExpression)
and sig == [v.variable for v in args1]
and sig == [v.variable for v in args2]
):
predDict[func2].append_prop((tuple(sig), term.first))
predDict[func1].validate_sig_len(sig)
class PredHolder:
"""
This class will be used by a dictionary that will store information
about predicates to be used by the ``ClosedWorldProver``.
The 'signatures' property is a list of tuples defining signatures for
which the predicate is true. For instance, 'see(john, mary)' would be
result in the signature '(john,mary)' for 'see'.
The second element of the pair is a list of pairs such that the first
element of the pair is a tuple of variables and the second element is an
expression of those variables that makes the predicate true. For instance,
'all x.all y.(see(x,y) -> know(x,y))' would result in "((x,y),('see(x,y)'))"
for 'know'.
"""
def __init__(self):
self.signatures = []
self.properties = []
self.signature_len = None
def append_sig(self, new_sig):
self.validate_sig_len(new_sig)
self.signatures.append(new_sig)
def append_prop(self, new_prop):
self.validate_sig_len(new_prop[0])
self.properties.append(new_prop)
def validate_sig_len(self, new_sig):
if self.signature_len is None:
self.signature_len = len(new_sig)
elif self.signature_len != len(new_sig):
raise Exception("Signature lengths do not match")
def __str__(self):
return f"({self.signatures},{self.properties},{self.signature_len})"
def __repr__(self):
return "%s" % self
def closed_domain_demo():
lexpr = Expression.fromstring
p1 = lexpr(r"exists x.walk(x)")
p2 = lexpr(r"man(Socrates)")
c = lexpr(r"walk(Socrates)")
prover = Prover9Command(c, [p1, p2])
print(prover.prove())
cdp = ClosedDomainProver(prover)
print("assumptions:")
for a in cdp.assumptions():
print(" ", a)
print("goal:", cdp.goal())
print(cdp.prove())
p1 = lexpr(r"exists x.walk(x)")
p2 = lexpr(r"man(Socrates)")
p3 = lexpr(r"-walk(Bill)")
c = lexpr(r"walk(Socrates)")
prover = Prover9Command(c, [p1, p2, p3])
print(prover.prove())
cdp = ClosedDomainProver(prover)
print("assumptions:")
for a in cdp.assumptions():
print(" ", a)
print("goal:", cdp.goal())
print(cdp.prove())
p1 = lexpr(r"exists x.walk(x)")
p2 = lexpr(r"man(Socrates)")
p3 = lexpr(r"-walk(Bill)")
c = lexpr(r"walk(Socrates)")
prover = Prover9Command(c, [p1, p2, p3])
print(prover.prove())
cdp = ClosedDomainProver(prover)
print("assumptions:")
for a in cdp.assumptions():
print(" ", a)
print("goal:", cdp.goal())
print(cdp.prove())
p1 = lexpr(r"walk(Socrates)")
p2 = lexpr(r"walk(Bill)")
c = lexpr(r"all x.walk(x)")
prover = Prover9Command(c, [p1, p2])
print(prover.prove())
cdp = ClosedDomainProver(prover)
print("assumptions:")
for a in cdp.assumptions():
print(" ", a)
print("goal:", cdp.goal())
print(cdp.prove())
p1 = lexpr(r"girl(mary)")
p2 = lexpr(r"dog(rover)")
p3 = lexpr(r"all x.(girl(x) -> -dog(x))")
p4 = lexpr(r"all x.(dog(x) -> -girl(x))")
p5 = lexpr(r"chase(mary, rover)")
c = lexpr(r"exists y.(dog(y) & all x.(girl(x) -> chase(x,y)))")
prover = Prover9Command(c, [p1, p2, p3, p4, p5])
print(prover.prove())
cdp = ClosedDomainProver(prover)
print("assumptions:")
for a in cdp.assumptions():
print(" ", a)
print("goal:", cdp.goal())
print(cdp.prove())
def unique_names_demo():
lexpr = Expression.fromstring
p1 = lexpr(r"man(Socrates)")
p2 = lexpr(r"man(Bill)")
c = lexpr(r"exists x.exists y.(x != y)")
prover = Prover9Command(c, [p1, p2])
print(prover.prove())
unp = UniqueNamesProver(prover)
print("assumptions:")
for a in unp.assumptions():
print(" ", a)
print("goal:", unp.goal())
print(unp.prove())
p1 = lexpr(r"all x.(walk(x) -> (x = Socrates))")
p2 = lexpr(r"Bill = William")
p3 = lexpr(r"Bill = Billy")
c = lexpr(r"-walk(William)")
prover = Prover9Command(c, [p1, p2, p3])
print(prover.prove())
unp = UniqueNamesProver(prover)
print("assumptions:")
for a in unp.assumptions():
print(" ", a)
print("goal:", unp.goal())
print(unp.prove())
def closed_world_demo():
lexpr = Expression.fromstring
p1 = lexpr(r"walk(Socrates)")
p2 = lexpr(r"(Socrates != Bill)")
c = lexpr(r"-walk(Bill)")
prover = Prover9Command(c, [p1, p2])
print(prover.prove())
cwp = ClosedWorldProver(prover)
print("assumptions:")
for a in cwp.assumptions():
print(" ", a)
print("goal:", cwp.goal())
print(cwp.prove())
p1 = lexpr(r"see(Socrates, John)")
p2 = lexpr(r"see(John, Mary)")
p3 = lexpr(r"(Socrates != John)")
p4 = lexpr(r"(John != Mary)")
c = lexpr(r"-see(Socrates, Mary)")
prover = Prover9Command(c, [p1, p2, p3, p4])
print(prover.prove())
cwp = ClosedWorldProver(prover)
print("assumptions:")
for a in cwp.assumptions():
print(" ", a)
print("goal:", cwp.goal())
print(cwp.prove())
p1 = lexpr(r"all x.(ostrich(x) -> bird(x))")
p2 = lexpr(r"bird(Tweety)")
p3 = lexpr(r"-ostrich(Sam)")
p4 = lexpr(r"Sam != Tweety")
c = lexpr(r"-bird(Sam)")
prover = Prover9Command(c, [p1, p2, p3, p4])
print(prover.prove())
cwp = ClosedWorldProver(prover)
print("assumptions:")
for a in cwp.assumptions():
print(" ", a)
print("goal:", cwp.goal())
print(cwp.prove())
def combination_prover_demo():
lexpr = Expression.fromstring
p1 = lexpr(r"see(Socrates, John)")
p2 = lexpr(r"see(John, Mary)")
c = lexpr(r"-see(Socrates, Mary)")
prover = Prover9Command(c, [p1, p2])
print(prover.prove())
command = ClosedDomainProver(UniqueNamesProver(ClosedWorldProver(prover)))
for a in command.assumptions():
print(a)
print(command.prove())
def default_reasoning_demo():
lexpr = Expression.fromstring
premises = []
# define taxonomy
premises.append(lexpr(r"all x.(elephant(x) -> animal(x))"))
premises.append(lexpr(r"all x.(bird(x) -> animal(x))"))
premises.append(lexpr(r"all x.(dove(x) -> bird(x))"))
premises.append(lexpr(r"all x.(ostrich(x) -> bird(x))"))
premises.append(lexpr(r"all x.(flying_ostrich(x) -> ostrich(x))"))
# default properties
premises.append(
lexpr(r"all x.((animal(x) & -Ab1(x)) -> -fly(x))")
) # normal animals don't fly
premises.append(
lexpr(r"all x.((bird(x) & -Ab2(x)) -> fly(x))")
) # normal birds fly
premises.append(
lexpr(r"all x.((ostrich(x) & -Ab3(x)) -> -fly(x))")
) # normal ostriches don't fly
# specify abnormal entities
premises.append(lexpr(r"all x.(bird(x) -> Ab1(x))")) # flight
premises.append(lexpr(r"all x.(ostrich(x) -> Ab2(x))")) # non-flying bird
premises.append(lexpr(r"all x.(flying_ostrich(x) -> Ab3(x))")) # flying ostrich
# define entities
premises.append(lexpr(r"elephant(E)"))
premises.append(lexpr(r"dove(D)"))
premises.append(lexpr(r"ostrich(O)"))
# print the assumptions
prover = Prover9Command(None, premises)
command = UniqueNamesProver(ClosedWorldProver(prover))
for a in command.assumptions():
print(a)
print_proof("-fly(E)", premises)
print_proof("fly(D)", premises)
print_proof("-fly(O)", premises)
def print_proof(goal, premises):
lexpr = Expression.fromstring
prover = Prover9Command(lexpr(goal), premises)
command = UniqueNamesProver(ClosedWorldProver(prover))
print(goal, prover.prove(), command.prove())
def demo():
closed_domain_demo()
unique_names_demo()
closed_world_demo()
combination_prover_demo()
default_reasoning_demo()
if __name__ == "__main__":
demo()

View File

@@ -0,0 +1,507 @@
# Natural Language Toolkit: Interface to the Prover9 Theorem Prover
#
# Copyright (C) 2001-2025 NLTK Project
# Author: Dan Garrette <dhgarrette@gmail.com>
# Ewan Klein <ewan@inf.ed.ac.uk>
#
# URL: <https://www.nltk.org/>
# For license information, see LICENSE.TXT
"""
A theorem prover that makes use of the external 'Prover9' package.
"""
import os
import subprocess
import nltk
from nltk.inference.api import BaseProverCommand, Prover
from nltk.sem.logic import (
AllExpression,
AndExpression,
EqualityExpression,
ExistsExpression,
Expression,
IffExpression,
ImpExpression,
NegatedExpression,
OrExpression,
)
#
# Following is not yet used. Return code for 2 actually realized as 512.
#
p9_return_codes = {
0: True,
1: "(FATAL)", # A fatal error occurred (user's syntax error).
2: False, # (SOS_EMPTY) Prover9 ran out of things to do
# (sos list exhausted).
3: "(MAX_MEGS)", # The max_megs (memory limit) parameter was exceeded.
4: "(MAX_SECONDS)", # The max_seconds parameter was exceeded.
5: "(MAX_GIVEN)", # The max_given parameter was exceeded.
6: "(MAX_KEPT)", # The max_kept parameter was exceeded.
7: "(ACTION)", # A Prover9 action terminated the search.
101: "(SIGSEGV)", # Prover9 crashed, most probably due to a bug.
}
class Prover9CommandParent:
"""
A common base class used by both ``Prover9Command`` and ``MaceCommand``,
which is responsible for maintaining a goal and a set of assumptions,
and generating prover9-style input files from them.
"""
def print_assumptions(self, output_format="nltk"):
"""
Print the list of the current assumptions.
"""
if output_format.lower() == "nltk":
for a in self.assumptions():
print(a)
elif output_format.lower() == "prover9":
for a in convert_to_prover9(self.assumptions()):
print(a)
else:
raise NameError(
"Unrecognized value for 'output_format': %s" % output_format
)
class Prover9Command(Prover9CommandParent, BaseProverCommand):
"""
A ``ProverCommand`` specific to the ``Prover9`` prover. It contains
the a print_assumptions() method that is used to print the list
of assumptions in multiple formats.
"""
def __init__(self, goal=None, assumptions=None, timeout=60, prover=None):
"""
:param goal: Input expression to prove
:type goal: sem.Expression
:param assumptions: Input expressions to use as assumptions in
the proof.
:type assumptions: list(sem.Expression)
:param timeout: number of seconds before timeout; set to 0 for
no timeout.
:type timeout: int
:param prover: a prover. If not set, one will be created.
:type prover: Prover9
"""
if not assumptions:
assumptions = []
if prover is not None:
assert isinstance(prover, Prover9)
else:
prover = Prover9(timeout)
BaseProverCommand.__init__(self, prover, goal, assumptions)
def decorate_proof(self, proof_string, simplify=True):
"""
:see BaseProverCommand.decorate_proof()
"""
if simplify:
return self._prover._call_prooftrans(proof_string, ["striplabels"])[
0
].rstrip()
else:
return proof_string.rstrip()
class Prover9Parent:
"""
A common class extended by both ``Prover9`` and ``Mace <mace.Mace>``.
It contains the functionality required to convert NLTK-style
expressions into Prover9-style expressions.
"""
_binary_location = None
def config_prover9(self, binary_location, verbose=False):
if binary_location is None:
self._binary_location = None
self._prover9_bin = None
else:
name = "prover9"
self._prover9_bin = nltk.internals.find_binary(
name,
path_to_bin=binary_location,
env_vars=["PROVER9"],
url="https://www.cs.unm.edu/~mccune/prover9/",
binary_names=[name, name + ".exe"],
verbose=verbose,
)
self._binary_location = self._prover9_bin.rsplit(os.path.sep, 1)
def prover9_input(self, goal, assumptions):
"""
:return: The input string that should be provided to the
prover9 binary. This string is formed based on the goal,
assumptions, and timeout value of this object.
"""
s = ""
if assumptions:
s += "formulas(assumptions).\n"
for p9_assumption in convert_to_prover9(assumptions):
s += " %s.\n" % p9_assumption
s += "end_of_list.\n\n"
if goal:
s += "formulas(goals).\n"
s += " %s.\n" % convert_to_prover9(goal)
s += "end_of_list.\n\n"
return s
def binary_locations(self):
"""
A list of directories that should be searched for the prover9
executables. This list is used by ``config_prover9`` when searching
for the prover9 executables.
"""
return [
"/usr/local/bin/prover9",
"/usr/local/bin/prover9/bin",
"/usr/local/bin",
"/usr/bin",
"/usr/local/prover9",
"/usr/local/share/prover9",
]
def _find_binary(self, name, verbose=False):
binary_locations = self.binary_locations()
if self._binary_location is not None:
binary_locations += [self._binary_location]
return nltk.internals.find_binary(
name,
searchpath=binary_locations,
env_vars=["PROVER9"],
url="https://www.cs.unm.edu/~mccune/prover9/",
binary_names=[name, name + ".exe"],
verbose=verbose,
)
def _call(self, input_str, binary, args=[], verbose=False):
"""
Call the binary with the given input.
:param input_str: A string whose contents are used as stdin.
:param binary: The location of the binary to call
:param args: A list of command-line arguments.
:return: A tuple (stdout, returncode)
:see: ``config_prover9``
"""
if verbose:
print("Calling:", binary)
print("Args:", args)
print("Input:\n", input_str, "\n")
# Call prover9 via a subprocess
cmd = [binary] + args
try:
input_str = input_str.encode("utf8")
except AttributeError:
pass
p = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.PIPE
)
(stdout, stderr) = p.communicate(input=input_str)
if verbose:
print("Return code:", p.returncode)
if stdout:
print("stdout:\n", stdout, "\n")
if stderr:
print("stderr:\n", stderr, "\n")
return (stdout.decode("utf-8"), p.returncode)
def convert_to_prover9(input):
"""
Convert a ``logic.Expression`` to Prover9 format.
"""
if isinstance(input, list):
result = []
for s in input:
try:
result.append(_convert_to_prover9(s.simplify()))
except:
print("input %s cannot be converted to Prover9 input syntax" % input)
raise
return result
else:
try:
return _convert_to_prover9(input.simplify())
except:
print("input %s cannot be converted to Prover9 input syntax" % input)
raise
def _convert_to_prover9(expression):
"""
Convert ``logic.Expression`` to Prover9 formatted string.
"""
if isinstance(expression, ExistsExpression):
return (
"exists "
+ str(expression.variable)
+ " "
+ _convert_to_prover9(expression.term)
)
elif isinstance(expression, AllExpression):
return (
"all "
+ str(expression.variable)
+ " "
+ _convert_to_prover9(expression.term)
)
elif isinstance(expression, NegatedExpression):
return "-(" + _convert_to_prover9(expression.term) + ")"
elif isinstance(expression, AndExpression):
return (
"("
+ _convert_to_prover9(expression.first)
+ " & "
+ _convert_to_prover9(expression.second)
+ ")"
)
elif isinstance(expression, OrExpression):
return (
"("
+ _convert_to_prover9(expression.first)
+ " | "
+ _convert_to_prover9(expression.second)
+ ")"
)
elif isinstance(expression, ImpExpression):
return (
"("
+ _convert_to_prover9(expression.first)
+ " -> "
+ _convert_to_prover9(expression.second)
+ ")"
)
elif isinstance(expression, IffExpression):
return (
"("
+ _convert_to_prover9(expression.first)
+ " <-> "
+ _convert_to_prover9(expression.second)
+ ")"
)
elif isinstance(expression, EqualityExpression):
return (
"("
+ _convert_to_prover9(expression.first)
+ " = "
+ _convert_to_prover9(expression.second)
+ ")"
)
else:
return str(expression)
class Prover9(Prover9Parent, Prover):
_prover9_bin = None
_prooftrans_bin = None
def __init__(self, timeout=60):
self._timeout = timeout
"""The timeout value for prover9. If a proof can not be found
in this amount of time, then prover9 will return false.
(Use 0 for no timeout.)"""
def _prove(self, goal=None, assumptions=None, verbose=False):
"""
Use Prover9 to prove a theorem.
:return: A pair whose first element is a boolean indicating if the
proof was successful (i.e. returns value of 0) and whose second element
is the output of the prover.
"""
if not assumptions:
assumptions = []
stdout, returncode = self._call_prover9(
self.prover9_input(goal, assumptions), verbose=verbose
)
return (returncode == 0, stdout)
def prover9_input(self, goal, assumptions):
"""
:see: Prover9Parent.prover9_input
"""
s = "clear(auto_denials).\n" # only one proof required
return s + Prover9Parent.prover9_input(self, goal, assumptions)
def _call_prover9(self, input_str, args=[], verbose=False):
"""
Call the ``prover9`` binary with the given input.
:param input_str: A string whose contents are used as stdin.
:param args: A list of command-line arguments.
:return: A tuple (stdout, returncode)
:see: ``config_prover9``
"""
if self._prover9_bin is None:
self._prover9_bin = self._find_binary("prover9", verbose)
updated_input_str = ""
if self._timeout > 0:
updated_input_str += "assign(max_seconds, %d).\n\n" % self._timeout
updated_input_str += input_str
stdout, returncode = self._call(
updated_input_str, self._prover9_bin, args, verbose
)
if returncode not in [0, 2]:
errormsgprefix = "%%ERROR:"
if errormsgprefix in stdout:
msgstart = stdout.index(errormsgprefix)
errormsg = stdout[msgstart:].strip()
else:
errormsg = None
if returncode in [3, 4, 5, 6]:
raise Prover9LimitExceededException(returncode, errormsg)
else:
raise Prover9FatalException(returncode, errormsg)
return stdout, returncode
def _call_prooftrans(self, input_str, args=[], verbose=False):
"""
Call the ``prooftrans`` binary with the given input.
:param input_str: A string whose contents are used as stdin.
:param args: A list of command-line arguments.
:return: A tuple (stdout, returncode)
:see: ``config_prover9``
"""
if self._prooftrans_bin is None:
self._prooftrans_bin = self._find_binary("prooftrans", verbose)
return self._call(input_str, self._prooftrans_bin, args, verbose)
class Prover9Exception(Exception):
def __init__(self, returncode, message):
msg = p9_return_codes[returncode]
if message:
msg += "\n%s" % message
Exception.__init__(self, msg)
class Prover9FatalException(Prover9Exception):
pass
class Prover9LimitExceededException(Prover9Exception):
pass
######################################################################
# { Tests and Demos
######################################################################
def test_config():
a = Expression.fromstring("(walk(j) & sing(j))")
g = Expression.fromstring("walk(j)")
p = Prover9Command(g, assumptions=[a])
p._executable_path = None
p.prover9_search = []
p.prove()
# config_prover9('/usr/local/bin')
print(p.prove())
print(p.proof())
def test_convert_to_prover9(expr):
"""
Test that parsing works OK.
"""
for t in expr:
e = Expression.fromstring(t)
print(convert_to_prover9(e))
def test_prove(arguments):
"""
Try some proofs and exhibit the results.
"""
for goal, assumptions in arguments:
g = Expression.fromstring(goal)
alist = [Expression.fromstring(a) for a in assumptions]
p = Prover9Command(g, assumptions=alist).prove()
for a in alist:
print(" %s" % a)
print(f"|- {g}: {p}\n")
arguments = [
("(man(x) <-> (not (not man(x))))", []),
("(not (man(x) & (not man(x))))", []),
("(man(x) | (not man(x)))", []),
("(man(x) & (not man(x)))", []),
("(man(x) -> man(x))", []),
("(not (man(x) & (not man(x))))", []),
("(man(x) | (not man(x)))", []),
("(man(x) -> man(x))", []),
("(man(x) <-> man(x))", []),
("(not (man(x) <-> (not man(x))))", []),
("mortal(Socrates)", ["all x.(man(x) -> mortal(x))", "man(Socrates)"]),
("((all x.(man(x) -> walks(x)) & man(Socrates)) -> some y.walks(y))", []),
("(all x.man(x) -> all x.man(x))", []),
("some x.all y.sees(x,y)", []),
(
"some e3.(walk(e3) & subj(e3, mary))",
[
"some e1.(see(e1) & subj(e1, john) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))"
],
),
(
"some x e1.(see(e1) & subj(e1, x) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))",
[
"some e1.(see(e1) & subj(e1, john) & some e2.(pred(e1, e2) & walk(e2) & subj(e2, mary)))"
],
),
]
expressions = [
r"some x y.sees(x,y)",
r"some x.(man(x) & walks(x))",
r"\x.(man(x) & walks(x))",
r"\x y.sees(x,y)",
r"walks(john)",
r"\x.big(x, \y.mouse(y))",
r"(walks(x) & (runs(x) & (threes(x) & fours(x))))",
r"(walks(x) -> runs(x))",
r"some x.(PRO(x) & sees(John, x))",
r"some x.(man(x) & (not walks(x)))",
r"all x.(man(x) -> walks(x))",
]
def spacer(num=45):
print("-" * num)
def demo():
print("Testing configuration")
spacer()
test_config()
print()
print("Testing conversion to Prover9 format")
spacer()
test_convert_to_prover9(expressions)
print()
print("Testing proofs")
spacer()
test_prove(arguments)
if __name__ == "__main__":
demo()

View File

@@ -0,0 +1,759 @@
# Natural Language Toolkit: First-order Resolution-based Theorem Prover
#
# Author: Dan Garrette <dhgarrette@gmail.com>
#
# Copyright (C) 2001-2025 NLTK Project
# URL: <https://www.nltk.org/>
# For license information, see LICENSE.TXT
"""
Module for a resolution-based First Order theorem prover.
"""
import operator
from collections import defaultdict
from functools import reduce
from nltk.inference.api import BaseProverCommand, Prover
from nltk.sem import skolemize
from nltk.sem.logic import (
AndExpression,
ApplicationExpression,
EqualityExpression,
Expression,
IndividualVariableExpression,
NegatedExpression,
OrExpression,
Variable,
VariableExpression,
is_indvar,
unique_variable,
)
class ProverParseError(Exception):
pass
class ResolutionProver(Prover):
ANSWER_KEY = "ANSWER"
_assume_false = True
def _prove(self, goal=None, assumptions=None, verbose=False):
"""
:param goal: Input expression to prove
:type goal: sem.Expression
:param assumptions: Input expressions to use as assumptions in the proof
:type assumptions: list(sem.Expression)
"""
if not assumptions:
assumptions = []
result = None
try:
clauses = []
if goal:
clauses.extend(clausify(-goal))
for a in assumptions:
clauses.extend(clausify(a))
result, clauses = self._attempt_proof(clauses)
if verbose:
print(ResolutionProverCommand._decorate_clauses(clauses))
except RuntimeError as e:
if self._assume_false and str(e).startswith(
"maximum recursion depth exceeded"
):
result = False
clauses = []
else:
if verbose:
print(e)
else:
raise e
return (result, clauses)
def _attempt_proof(self, clauses):
# map indices to lists of indices, to store attempted unifications
tried = defaultdict(list)
i = 0
while i < len(clauses):
if not clauses[i].is_tautology():
# since we try clauses in order, we should start after the last
# index tried
if tried[i]:
j = tried[i][-1] + 1
else:
j = i + 1 # nothing tried yet for 'i', so start with the next
while j < len(clauses):
# don't: 1) unify a clause with itself,
# 2) use tautologies
if i != j and j and not clauses[j].is_tautology():
tried[i].append(j)
newclauses = clauses[i].unify(clauses[j])
if newclauses:
for newclause in newclauses:
newclause._parents = (i + 1, j + 1)
clauses.append(newclause)
if not len(newclause): # if there's an empty clause
return (True, clauses)
i = -1 # since we added a new clause, restart from the top
break
j += 1
i += 1
return (False, clauses)
class ResolutionProverCommand(BaseProverCommand):
def __init__(self, goal=None, assumptions=None, prover=None):
"""
:param goal: Input expression to prove
:type goal: sem.Expression
:param assumptions: Input expressions to use as assumptions in
the proof.
:type assumptions: list(sem.Expression)
"""
if prover is not None:
assert isinstance(prover, ResolutionProver)
else:
prover = ResolutionProver()
BaseProverCommand.__init__(self, prover, goal, assumptions)
self._clauses = None
def prove(self, verbose=False):
"""
Perform the actual proof. Store the result to prevent unnecessary
re-proving.
"""
if self._result is None:
self._result, clauses = self._prover._prove(
self.goal(), self.assumptions(), verbose
)
self._clauses = clauses
self._proof = ResolutionProverCommand._decorate_clauses(clauses)
return self._result
def find_answers(self, verbose=False):
self.prove(verbose)
answers = set()
answer_ex = VariableExpression(Variable(ResolutionProver.ANSWER_KEY))
for clause in self._clauses:
if (
len(clause) == 1
and isinstance(clause[0], ApplicationExpression)
and clause[0].function == answer_ex
and not isinstance(clause[0].argument, IndividualVariableExpression)
):
answers.add(clause[0].argument)
return answers
@staticmethod
def _decorate_clauses(clauses):
"""
Decorate the proof output.
"""
out = ""
max_clause_len = max(len(str(clause)) for clause in clauses)
max_seq_len = len(str(len(clauses)))
for i in range(len(clauses)):
parents = "A"
taut = ""
if clauses[i].is_tautology():
taut = "Tautology"
if clauses[i]._parents:
parents = str(clauses[i]._parents)
parents = " " * (max_clause_len - len(str(clauses[i])) + 1) + parents
seq = " " * (max_seq_len - len(str(i + 1))) + str(i + 1)
out += f"[{seq}] {clauses[i]} {parents} {taut}\n"
return out
class Clause(list):
def __init__(self, data):
list.__init__(self, data)
self._is_tautology = None
self._parents = None
def unify(self, other, bindings=None, used=None, skipped=None, debug=False):
"""
Attempt to unify this Clause with the other, returning a list of
resulting, unified, Clauses.
:param other: ``Clause`` with which to unify
:param bindings: ``BindingDict`` containing bindings that should be used
during the unification
:param used: tuple of two lists of atoms. The first lists the
atoms from 'self' that were successfully unified with atoms from
'other'. The second lists the atoms from 'other' that were successfully
unified with atoms from 'self'.
:param skipped: tuple of two ``Clause`` objects. The first is a list of all
the atoms from the 'self' Clause that have not been unified with
anything on the path. The second is same thing for the 'other' Clause.
:param debug: bool indicating whether debug statements should print
:return: list containing all the resulting ``Clause`` objects that could be
obtained by unification
"""
if bindings is None:
bindings = BindingDict()
if used is None:
used = ([], [])
if skipped is None:
skipped = ([], [])
if isinstance(debug, bool):
debug = DebugObject(debug)
newclauses = _iterate_first(
self, other, bindings, used, skipped, _complete_unify_path, debug
)
# remove subsumed clauses. make a list of all indices of subsumed
# clauses, and then remove them from the list
subsumed = []
for i, c1 in enumerate(newclauses):
if i not in subsumed:
for j, c2 in enumerate(newclauses):
if i != j and j not in subsumed and c1.subsumes(c2):
subsumed.append(j)
result = []
for i in range(len(newclauses)):
if i not in subsumed:
result.append(newclauses[i])
return result
def isSubsetOf(self, other):
"""
Return True iff every term in 'self' is a term in 'other'.
:param other: ``Clause``
:return: bool
"""
for a in self:
if a not in other:
return False
return True
def subsumes(self, other):
"""
Return True iff 'self' subsumes 'other', this is, if there is a
substitution such that every term in 'self' can be unified with a term
in 'other'.
:param other: ``Clause``
:return: bool
"""
negatedother = []
for atom in other:
if isinstance(atom, NegatedExpression):
negatedother.append(atom.term)
else:
negatedother.append(-atom)
negatedotherClause = Clause(negatedother)
bindings = BindingDict()
used = ([], [])
skipped = ([], [])
debug = DebugObject(False)
return (
len(
_iterate_first(
self,
negatedotherClause,
bindings,
used,
skipped,
_subsumes_finalize,
debug,
)
)
> 0
)
def __getslice__(self, start, end):
return Clause(list.__getslice__(self, start, end))
def __sub__(self, other):
return Clause([a for a in self if a not in other])
def __add__(self, other):
return Clause(list.__add__(self, other))
def is_tautology(self):
"""
Self is a tautology if it contains ground terms P and -P. The ground
term, P, must be an exact match, ie, not using unification.
"""
if self._is_tautology is not None:
return self._is_tautology
for i, a in enumerate(self):
if not isinstance(a, EqualityExpression):
j = len(self) - 1
while j > i:
b = self[j]
if isinstance(a, NegatedExpression):
if a.term == b:
self._is_tautology = True
return True
elif isinstance(b, NegatedExpression):
if a == b.term:
self._is_tautology = True
return True
j -= 1
self._is_tautology = False
return False
def free(self):
return reduce(operator.or_, ((atom.free() | atom.constants()) for atom in self))
def replace(self, variable, expression):
"""
Replace every instance of variable with expression across every atom
in the clause
:param variable: ``Variable``
:param expression: ``Expression``
"""
return Clause([atom.replace(variable, expression) for atom in self])
def substitute_bindings(self, bindings):
"""
Replace every binding
:param bindings: A list of tuples mapping Variable Expressions to the
Expressions to which they are bound.
:return: ``Clause``
"""
return Clause([atom.substitute_bindings(bindings) for atom in self])
def __str__(self):
return "{" + ", ".join("%s" % item for item in self) + "}"
def __repr__(self):
return "%s" % self
def _iterate_first(first, second, bindings, used, skipped, finalize_method, debug):
"""
This method facilitates movement through the terms of 'self'
"""
debug.line(f"unify({first},{second}) {bindings}")
if not len(first) or not len(second): # if no more recursions can be performed
return finalize_method(first, second, bindings, used, skipped, debug)
else:
# explore this 'self' atom
result = _iterate_second(
first, second, bindings, used, skipped, finalize_method, debug + 1
)
# skip this possible 'self' atom
newskipped = (skipped[0] + [first[0]], skipped[1])
result += _iterate_first(
first[1:], second, bindings, used, newskipped, finalize_method, debug + 1
)
try:
newbindings, newused, unused = _unify_terms(
first[0], second[0], bindings, used
)
# Unification found, so progress with this line of unification
# put skipped and unused terms back into play for later unification.
newfirst = first[1:] + skipped[0] + unused[0]
newsecond = second[1:] + skipped[1] + unused[1]
result += _iterate_first(
newfirst,
newsecond,
newbindings,
newused,
([], []),
finalize_method,
debug + 1,
)
except BindingException:
# the atoms could not be unified,
pass
return result
def _iterate_second(first, second, bindings, used, skipped, finalize_method, debug):
"""
This method facilitates movement through the terms of 'other'
"""
debug.line(f"unify({first},{second}) {bindings}")
if not len(first) or not len(second): # if no more recursions can be performed
return finalize_method(first, second, bindings, used, skipped, debug)
else:
# skip this possible pairing and move to the next
newskipped = (skipped[0], skipped[1] + [second[0]])
result = _iterate_second(
first, second[1:], bindings, used, newskipped, finalize_method, debug + 1
)
try:
newbindings, newused, unused = _unify_terms(
first[0], second[0], bindings, used
)
# Unification found, so progress with this line of unification
# put skipped and unused terms back into play for later unification.
newfirst = first[1:] + skipped[0] + unused[0]
newsecond = second[1:] + skipped[1] + unused[1]
result += _iterate_second(
newfirst,
newsecond,
newbindings,
newused,
([], []),
finalize_method,
debug + 1,
)
except BindingException:
# the atoms could not be unified,
pass
return result
def _unify_terms(a, b, bindings=None, used=None):
"""
This method attempts to unify two terms. Two expressions are unifiable
if there exists a substitution function S such that S(a) == S(-b).
:param a: ``Expression``
:param b: ``Expression``
:param bindings: ``BindingDict`` a starting set of bindings with which
the unification must be consistent
:return: ``BindingDict`` A dictionary of the bindings required to unify
:raise ``BindingException``: If the terms cannot be unified
"""
assert isinstance(a, Expression)
assert isinstance(b, Expression)
if bindings is None:
bindings = BindingDict()
if used is None:
used = ([], [])
# Use resolution
if isinstance(a, NegatedExpression) and isinstance(b, ApplicationExpression):
newbindings = most_general_unification(a.term, b, bindings)
newused = (used[0] + [a], used[1] + [b])
unused = ([], [])
elif isinstance(a, ApplicationExpression) and isinstance(b, NegatedExpression):
newbindings = most_general_unification(a, b.term, bindings)
newused = (used[0] + [a], used[1] + [b])
unused = ([], [])
# Use demodulation
elif isinstance(a, EqualityExpression):
newbindings = BindingDict([(a.first.variable, a.second)])
newused = (used[0] + [a], used[1])
unused = ([], [b])
elif isinstance(b, EqualityExpression):
newbindings = BindingDict([(b.first.variable, b.second)])
newused = (used[0], used[1] + [b])
unused = ([a], [])
else:
raise BindingException((a, b))
return newbindings, newused, unused
def _complete_unify_path(first, second, bindings, used, skipped, debug):
if used[0] or used[1]: # if bindings were made along the path
newclause = Clause(skipped[0] + skipped[1] + first + second)
debug.line(" -> New Clause: %s" % newclause)
return [newclause.substitute_bindings(bindings)]
else: # no bindings made means no unification occurred. so no result
debug.line(" -> End")
return []
def _subsumes_finalize(first, second, bindings, used, skipped, debug):
if not len(skipped[0]) and not len(first):
# If there are no skipped terms and no terms left in 'first', then
# all of the terms in the original 'self' were unified with terms
# in 'other'. Therefore, there exists a binding (this one) such that
# every term in self can be unified with a term in other, which
# is the definition of subsumption.
return [True]
else:
return []
def clausify(expression):
"""
Skolemize, clausify, and standardize the variables apart.
"""
clause_list = []
for clause in _clausify(skolemize(expression)):
for free in clause.free():
if is_indvar(free.name):
newvar = VariableExpression(unique_variable())
clause = clause.replace(free, newvar)
clause_list.append(clause)
return clause_list
def _clausify(expression):
"""
:param expression: a skolemized expression in CNF
"""
if isinstance(expression, AndExpression):
return _clausify(expression.first) + _clausify(expression.second)
elif isinstance(expression, OrExpression):
first = _clausify(expression.first)
second = _clausify(expression.second)
assert len(first) == 1
assert len(second) == 1
return [first[0] + second[0]]
elif isinstance(expression, EqualityExpression):
return [Clause([expression])]
elif isinstance(expression, ApplicationExpression):
return [Clause([expression])]
elif isinstance(expression, NegatedExpression):
if isinstance(expression.term, ApplicationExpression):
return [Clause([expression])]
elif isinstance(expression.term, EqualityExpression):
return [Clause([expression])]
raise ProverParseError()
class BindingDict:
def __init__(self, binding_list=None):
"""
:param binding_list: list of (``AbstractVariableExpression``, ``AtomicExpression``) to initialize the dictionary
"""
self.d = {}
if binding_list:
for v, b in binding_list:
self[v] = b
def __setitem__(self, variable, binding):
"""
A binding is consistent with the dict if its variable is not already bound, OR if its
variable is already bound to its argument.
:param variable: ``Variable`` The variable to bind
:param binding: ``Expression`` The atomic to which 'variable' should be bound
:raise BindingException: If the variable cannot be bound in this dictionary
"""
assert isinstance(variable, Variable)
assert isinstance(binding, Expression)
try:
existing = self[variable]
except KeyError:
existing = None
if not existing or binding == existing:
self.d[variable] = binding
elif isinstance(binding, IndividualVariableExpression):
# Since variable is already bound, try to bind binding to variable
try:
existing = self[binding.variable]
except KeyError:
existing = None
binding2 = VariableExpression(variable)
if not existing or binding2 == existing:
self.d[binding.variable] = binding2
else:
raise BindingException(
"Variable %s already bound to another " "value" % (variable)
)
else:
raise BindingException(
"Variable %s already bound to another " "value" % (variable)
)
def __getitem__(self, variable):
"""
Return the expression to which 'variable' is bound
"""
assert isinstance(variable, Variable)
intermediate = self.d[variable]
while intermediate:
try:
intermediate = self.d[intermediate]
except KeyError:
return intermediate
def __contains__(self, item):
return item in self.d
def __add__(self, other):
"""
:param other: ``BindingDict`` The dict with which to combine self
:return: ``BindingDict`` A new dict containing all the elements of both parameters
:raise BindingException: If the parameter dictionaries are not consistent with each other
"""
try:
combined = BindingDict()
for v in self.d:
combined[v] = self.d[v]
for v in other.d:
combined[v] = other.d[v]
return combined
except BindingException as e:
raise BindingException(
"Attempting to add two contradicting "
"BindingDicts: '%s' and '%s'" % (self, other)
) from e
def __len__(self):
return len(self.d)
def __str__(self):
data_str = ", ".join(f"{v}: {self.d[v]}" for v in sorted(self.d.keys()))
return "{" + data_str + "}"
def __repr__(self):
return "%s" % self
def most_general_unification(a, b, bindings=None):
"""
Find the most general unification of the two given expressions
:param a: ``Expression``
:param b: ``Expression``
:param bindings: ``BindingDict`` a starting set of bindings with which the
unification must be consistent
:return: a list of bindings
:raise BindingException: if the Expressions cannot be unified
"""
if bindings is None:
bindings = BindingDict()
if a == b:
return bindings
elif isinstance(a, IndividualVariableExpression):
return _mgu_var(a, b, bindings)
elif isinstance(b, IndividualVariableExpression):
return _mgu_var(b, a, bindings)
elif isinstance(a, ApplicationExpression) and isinstance(b, ApplicationExpression):
return most_general_unification(
a.function, b.function, bindings
) + most_general_unification(a.argument, b.argument, bindings)
raise BindingException((a, b))
def _mgu_var(var, expression, bindings):
if var.variable in expression.free() | expression.constants():
raise BindingException((var, expression))
else:
return BindingDict([(var.variable, expression)]) + bindings
class BindingException(Exception):
def __init__(self, arg):
if isinstance(arg, tuple):
Exception.__init__(self, "'%s' cannot be bound to '%s'" % arg)
else:
Exception.__init__(self, arg)
class UnificationException(Exception):
def __init__(self, a, b):
Exception.__init__(self, f"'{a}' cannot unify with '{b}'")
class DebugObject:
def __init__(self, enabled=True, indent=0):
self.enabled = enabled
self.indent = indent
def __add__(self, i):
return DebugObject(self.enabled, self.indent + i)
def line(self, line):
if self.enabled:
print(" " * self.indent + line)
def testResolutionProver():
resolution_test(r"man(x)")
resolution_test(r"(man(x) -> man(x))")
resolution_test(r"(man(x) -> --man(x))")
resolution_test(r"-(man(x) and -man(x))")
resolution_test(r"(man(x) or -man(x))")
resolution_test(r"(man(x) -> man(x))")
resolution_test(r"-(man(x) and -man(x))")
resolution_test(r"(man(x) or -man(x))")
resolution_test(r"(man(x) -> man(x))")
resolution_test(r"(man(x) iff man(x))")
resolution_test(r"-(man(x) iff -man(x))")
resolution_test("all x.man(x)")
resolution_test("-all x.some y.F(x,y) & some x.all y.(-F(x,y))")
resolution_test("some x.all y.sees(x,y)")
p1 = Expression.fromstring(r"all x.(man(x) -> mortal(x))")
p2 = Expression.fromstring(r"man(Socrates)")
c = Expression.fromstring(r"mortal(Socrates)")
print(f"{p1}, {p2} |- {c}: {ResolutionProver().prove(c, [p1, p2])}")
p1 = Expression.fromstring(r"all x.(man(x) -> walks(x))")
p2 = Expression.fromstring(r"man(John)")
c = Expression.fromstring(r"some y.walks(y)")
print(f"{p1}, {p2} |- {c}: {ResolutionProver().prove(c, [p1, p2])}")
p = Expression.fromstring(r"some e1.some e2.(believe(e1,john,e2) & walk(e2,mary))")
c = Expression.fromstring(r"some e0.walk(e0,mary)")
print(f"{p} |- {c}: {ResolutionProver().prove(c, [p])}")
def resolution_test(e):
f = Expression.fromstring(e)
t = ResolutionProver().prove(f)
print(f"|- {f}: {t}")
def test_clausify():
lexpr = Expression.fromstring
print(clausify(lexpr("P(x) | Q(x)")))
print(clausify(lexpr("(P(x) & Q(x)) | R(x)")))
print(clausify(lexpr("P(x) | (Q(x) & R(x))")))
print(clausify(lexpr("(P(x) & Q(x)) | (R(x) & S(x))")))
print(clausify(lexpr("P(x) | Q(x) | R(x)")))
print(clausify(lexpr("P(x) | (Q(x) & R(x)) | S(x)")))
print(clausify(lexpr("exists x.P(x) | Q(x)")))
print(clausify(lexpr("-(-P(x) & Q(x))")))
print(clausify(lexpr("P(x) <-> Q(x)")))
print(clausify(lexpr("-(P(x) <-> Q(x))")))
print(clausify(lexpr("-(all x.P(x))")))
print(clausify(lexpr("-(some x.P(x))")))
print(clausify(lexpr("some x.P(x)")))
print(clausify(lexpr("some x.all y.P(x,y)")))
print(clausify(lexpr("all y.some x.P(x,y)")))
print(clausify(lexpr("all z.all y.some x.P(x,y,z)")))
print(clausify(lexpr("all x.(all y.P(x,y) -> -all y.(Q(x,y) -> R(x,y)))")))
def demo():
test_clausify()
print()
testResolutionProver()
print()
p = Expression.fromstring("man(x)")
print(ResolutionProverCommand(p, [p]).prove())
if __name__ == "__main__":
demo()

View File

@@ -0,0 +1,712 @@
# Natural Language Toolkit: First-Order Tableau Theorem Prover
#
# Copyright (C) 2001-2025 NLTK Project
# Author: Dan Garrette <dhgarrette@gmail.com>
#
# URL: <https://www.nltk.org/>
# For license information, see LICENSE.TXT
"""
Module for a tableau-based First Order theorem prover.
"""
from nltk.inference.api import BaseProverCommand, Prover
from nltk.internals import Counter
from nltk.sem.logic import (
AbstractVariableExpression,
AllExpression,
AndExpression,
ApplicationExpression,
EqualityExpression,
ExistsExpression,
Expression,
FunctionVariableExpression,
IffExpression,
ImpExpression,
LambdaExpression,
NegatedExpression,
OrExpression,
Variable,
VariableExpression,
unique_variable,
)
_counter = Counter()
class ProverParseError(Exception):
pass
class TableauProver(Prover):
_assume_false = False
def _prove(self, goal=None, assumptions=None, verbose=False):
if not assumptions:
assumptions = []
result = None
try:
agenda = Agenda()
if goal:
agenda.put(-goal)
agenda.put_all(assumptions)
debugger = Debug(verbose)
result = self._attempt_proof(agenda, set(), set(), debugger)
except RuntimeError as e:
if self._assume_false and str(e).startswith(
"maximum recursion depth exceeded"
):
result = False
else:
if verbose:
print(e)
else:
raise e
return (result, "\n".join(debugger.lines))
def _attempt_proof(self, agenda, accessible_vars, atoms, debug):
(current, context), category = agenda.pop_first()
# if there's nothing left in the agenda, and we haven't closed the path
if not current:
debug.line("AGENDA EMPTY")
return False
proof_method = {
Categories.ATOM: self._attempt_proof_atom,
Categories.PROP: self._attempt_proof_prop,
Categories.N_ATOM: self._attempt_proof_n_atom,
Categories.N_PROP: self._attempt_proof_n_prop,
Categories.APP: self._attempt_proof_app,
Categories.N_APP: self._attempt_proof_n_app,
Categories.N_EQ: self._attempt_proof_n_eq,
Categories.D_NEG: self._attempt_proof_d_neg,
Categories.N_ALL: self._attempt_proof_n_all,
Categories.N_EXISTS: self._attempt_proof_n_some,
Categories.AND: self._attempt_proof_and,
Categories.N_OR: self._attempt_proof_n_or,
Categories.N_IMP: self._attempt_proof_n_imp,
Categories.OR: self._attempt_proof_or,
Categories.IMP: self._attempt_proof_imp,
Categories.N_AND: self._attempt_proof_n_and,
Categories.IFF: self._attempt_proof_iff,
Categories.N_IFF: self._attempt_proof_n_iff,
Categories.EQ: self._attempt_proof_eq,
Categories.EXISTS: self._attempt_proof_some,
Categories.ALL: self._attempt_proof_all,
}[category]
debug.line((current, context))
return proof_method(current, context, agenda, accessible_vars, atoms, debug)
def _attempt_proof_atom(
self, current, context, agenda, accessible_vars, atoms, debug
):
# Check if the branch is closed. Return 'True' if it is
if (current, True) in atoms:
debug.line("CLOSED", 1)
return True
if context:
if isinstance(context.term, NegatedExpression):
current = current.negate()
agenda.put(context(current).simplify())
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
else:
# mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars
agenda.mark_alls_fresh()
return self._attempt_proof(
agenda,
accessible_vars | set(current.args),
atoms | {(current, False)},
debug + 1,
)
def _attempt_proof_n_atom(
self, current, context, agenda, accessible_vars, atoms, debug
):
# Check if the branch is closed. Return 'True' if it is
if (current.term, False) in atoms:
debug.line("CLOSED", 1)
return True
if context:
if isinstance(context.term, NegatedExpression):
current = current.negate()
agenda.put(context(current).simplify())
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
else:
# mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars
agenda.mark_alls_fresh()
return self._attempt_proof(
agenda,
accessible_vars | set(current.term.args),
atoms | {(current.term, True)},
debug + 1,
)
def _attempt_proof_prop(
self, current, context, agenda, accessible_vars, atoms, debug
):
# Check if the branch is closed. Return 'True' if it is
if (current, True) in atoms:
debug.line("CLOSED", 1)
return True
# mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars
agenda.mark_alls_fresh()
return self._attempt_proof(
agenda, accessible_vars, atoms | {(current, False)}, debug + 1
)
def _attempt_proof_n_prop(
self, current, context, agenda, accessible_vars, atoms, debug
):
# Check if the branch is closed. Return 'True' if it is
if (current.term, False) in atoms:
debug.line("CLOSED", 1)
return True
# mark all AllExpressions as 'not exhausted' into the agenda since we are (potentially) adding new accessible vars
agenda.mark_alls_fresh()
return self._attempt_proof(
agenda, accessible_vars, atoms | {(current.term, True)}, debug + 1
)
def _attempt_proof_app(
self, current, context, agenda, accessible_vars, atoms, debug
):
f, args = current.uncurry()
for i, arg in enumerate(args):
if not TableauProver.is_atom(arg):
ctx = f
nv = Variable("X%s" % _counter.get())
for j, a in enumerate(args):
ctx = ctx(VariableExpression(nv)) if i == j else ctx(a)
if context:
ctx = context(ctx).simplify()
ctx = LambdaExpression(nv, ctx)
agenda.put(arg, ctx)
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
raise Exception("If this method is called, there must be a non-atomic argument")
def _attempt_proof_n_app(
self, current, context, agenda, accessible_vars, atoms, debug
):
f, args = current.term.uncurry()
for i, arg in enumerate(args):
if not TableauProver.is_atom(arg):
ctx = f
nv = Variable("X%s" % _counter.get())
for j, a in enumerate(args):
ctx = ctx(VariableExpression(nv)) if i == j else ctx(a)
if context:
# combine new context with existing
ctx = context(ctx).simplify()
ctx = LambdaExpression(nv, -ctx)
agenda.put(-arg, ctx)
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
raise Exception("If this method is called, there must be a non-atomic argument")
def _attempt_proof_n_eq(
self, current, context, agenda, accessible_vars, atoms, debug
):
###########################################################################
# Since 'current' is of type '~(a=b)', the path is closed if 'a' == 'b'
###########################################################################
if current.term.first == current.term.second:
debug.line("CLOSED", 1)
return True
agenda[Categories.N_EQ].add((current, context))
current._exhausted = True
return self._attempt_proof(
agenda,
accessible_vars | {current.term.first, current.term.second},
atoms,
debug + 1,
)
def _attempt_proof_d_neg(
self, current, context, agenda, accessible_vars, atoms, debug
):
agenda.put(current.term.term, context)
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_n_all(
self, current, context, agenda, accessible_vars, atoms, debug
):
agenda[Categories.EXISTS].add(
(ExistsExpression(current.term.variable, -current.term.term), context)
)
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_n_some(
self, current, context, agenda, accessible_vars, atoms, debug
):
agenda[Categories.ALL].add(
(AllExpression(current.term.variable, -current.term.term), context)
)
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_and(
self, current, context, agenda, accessible_vars, atoms, debug
):
agenda.put(current.first, context)
agenda.put(current.second, context)
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_n_or(
self, current, context, agenda, accessible_vars, atoms, debug
):
agenda.put(-current.term.first, context)
agenda.put(-current.term.second, context)
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_n_imp(
self, current, context, agenda, accessible_vars, atoms, debug
):
agenda.put(current.term.first, context)
agenda.put(-current.term.second, context)
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_or(
self, current, context, agenda, accessible_vars, atoms, debug
):
new_agenda = agenda.clone()
agenda.put(current.first, context)
new_agenda.put(current.second, context)
return self._attempt_proof(
agenda, accessible_vars, atoms, debug + 1
) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_imp(
self, current, context, agenda, accessible_vars, atoms, debug
):
new_agenda = agenda.clone()
agenda.put(-current.first, context)
new_agenda.put(current.second, context)
return self._attempt_proof(
agenda, accessible_vars, atoms, debug + 1
) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_n_and(
self, current, context, agenda, accessible_vars, atoms, debug
):
new_agenda = agenda.clone()
agenda.put(-current.term.first, context)
new_agenda.put(-current.term.second, context)
return self._attempt_proof(
agenda, accessible_vars, atoms, debug + 1
) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_iff(
self, current, context, agenda, accessible_vars, atoms, debug
):
new_agenda = agenda.clone()
agenda.put(current.first, context)
agenda.put(current.second, context)
new_agenda.put(-current.first, context)
new_agenda.put(-current.second, context)
return self._attempt_proof(
agenda, accessible_vars, atoms, debug + 1
) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_n_iff(
self, current, context, agenda, accessible_vars, atoms, debug
):
new_agenda = agenda.clone()
agenda.put(current.term.first, context)
agenda.put(-current.term.second, context)
new_agenda.put(-current.term.first, context)
new_agenda.put(current.term.second, context)
return self._attempt_proof(
agenda, accessible_vars, atoms, debug + 1
) and self._attempt_proof(new_agenda, accessible_vars, atoms, debug + 1)
def _attempt_proof_eq(
self, current, context, agenda, accessible_vars, atoms, debug
):
#########################################################################
# Since 'current' is of the form '(a = b)', replace ALL free instances
# of 'a' with 'b'
#########################################################################
agenda.put_atoms(atoms)
agenda.replace_all(current.first, current.second)
accessible_vars.discard(current.first)
agenda.mark_neqs_fresh()
return self._attempt_proof(agenda, accessible_vars, set(), debug + 1)
def _attempt_proof_some(
self, current, context, agenda, accessible_vars, atoms, debug
):
new_unique_variable = VariableExpression(unique_variable())
agenda.put(current.term.replace(current.variable, new_unique_variable), context)
agenda.mark_alls_fresh()
return self._attempt_proof(
agenda, accessible_vars | {new_unique_variable}, atoms, debug + 1
)
def _attempt_proof_all(
self, current, context, agenda, accessible_vars, atoms, debug
):
try:
current._used_vars
except AttributeError:
current._used_vars = set()
# if there are accessible_vars on the path
if accessible_vars:
# get the set of bound variables that have not be used by this AllExpression
bv_available = accessible_vars - current._used_vars
if bv_available:
variable_to_use = list(bv_available)[0]
debug.line("--> Using '%s'" % variable_to_use, 2)
current._used_vars |= {variable_to_use}
agenda.put(
current.term.replace(current.variable, variable_to_use), context
)
agenda[Categories.ALL].add((current, context))
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
else:
# no more available variables to substitute
debug.line("--> Variables Exhausted", 2)
current._exhausted = True
agenda[Categories.ALL].add((current, context))
return self._attempt_proof(agenda, accessible_vars, atoms, debug + 1)
else:
new_unique_variable = VariableExpression(unique_variable())
debug.line("--> Using '%s'" % new_unique_variable, 2)
current._used_vars |= {new_unique_variable}
agenda.put(
current.term.replace(current.variable, new_unique_variable), context
)
agenda[Categories.ALL].add((current, context))
agenda.mark_alls_fresh()
return self._attempt_proof(
agenda, accessible_vars | {new_unique_variable}, atoms, debug + 1
)
@staticmethod
def is_atom(e):
if isinstance(e, NegatedExpression):
e = e.term
if isinstance(e, ApplicationExpression):
for arg in e.args:
if not TableauProver.is_atom(arg):
return False
return True
elif isinstance(e, AbstractVariableExpression) or isinstance(
e, LambdaExpression
):
return True
else:
return False
class TableauProverCommand(BaseProverCommand):
def __init__(self, goal=None, assumptions=None, prover=None):
"""
:param goal: Input expression to prove
:type goal: sem.Expression
:param assumptions: Input expressions to use as assumptions in
the proof.
:type assumptions: list(sem.Expression)
"""
if prover is not None:
assert isinstance(prover, TableauProver)
else:
prover = TableauProver()
BaseProverCommand.__init__(self, prover, goal, assumptions)
class Agenda:
def __init__(self):
self.sets = tuple(set() for i in range(21))
def clone(self):
new_agenda = Agenda()
set_list = [s.copy() for s in self.sets]
new_allExs = set()
for allEx, _ in set_list[Categories.ALL]:
new_allEx = AllExpression(allEx.variable, allEx.term)
try:
new_allEx._used_vars = {used for used in allEx._used_vars}
except AttributeError:
new_allEx._used_vars = set()
new_allExs.add((new_allEx, None))
set_list[Categories.ALL] = new_allExs
set_list[Categories.N_EQ] = {
(NegatedExpression(n_eq.term), ctx)
for (n_eq, ctx) in set_list[Categories.N_EQ]
}
new_agenda.sets = tuple(set_list)
return new_agenda
def __getitem__(self, index):
return self.sets[index]
def put(self, expression, context=None):
if isinstance(expression, AllExpression):
ex_to_add = AllExpression(expression.variable, expression.term)
try:
ex_to_add._used_vars = {used for used in expression._used_vars}
except AttributeError:
ex_to_add._used_vars = set()
else:
ex_to_add = expression
self.sets[self._categorize_expression(ex_to_add)].add((ex_to_add, context))
def put_all(self, expressions):
for expression in expressions:
self.put(expression)
def put_atoms(self, atoms):
for atom, neg in atoms:
if neg:
self[Categories.N_ATOM].add((-atom, None))
else:
self[Categories.ATOM].add((atom, None))
def pop_first(self):
"""Pop the first expression that appears in the agenda"""
for i, s in enumerate(self.sets):
if s:
if i in [Categories.N_EQ, Categories.ALL]:
for ex in s:
try:
if not ex[0]._exhausted:
s.remove(ex)
return (ex, i)
except AttributeError:
s.remove(ex)
return (ex, i)
else:
return (s.pop(), i)
return ((None, None), None)
def replace_all(self, old, new):
for s in self.sets:
for ex, ctx in s:
ex.replace(old.variable, new)
if ctx is not None:
ctx.replace(old.variable, new)
def mark_alls_fresh(self):
for u, _ in self.sets[Categories.ALL]:
u._exhausted = False
def mark_neqs_fresh(self):
for neq, _ in self.sets[Categories.N_EQ]:
neq._exhausted = False
def _categorize_expression(self, current):
if isinstance(current, NegatedExpression):
return self._categorize_NegatedExpression(current)
elif isinstance(current, FunctionVariableExpression):
return Categories.PROP
elif TableauProver.is_atom(current):
return Categories.ATOM
elif isinstance(current, AllExpression):
return Categories.ALL
elif isinstance(current, AndExpression):
return Categories.AND
elif isinstance(current, OrExpression):
return Categories.OR
elif isinstance(current, ImpExpression):
return Categories.IMP
elif isinstance(current, IffExpression):
return Categories.IFF
elif isinstance(current, EqualityExpression):
return Categories.EQ
elif isinstance(current, ExistsExpression):
return Categories.EXISTS
elif isinstance(current, ApplicationExpression):
return Categories.APP
else:
raise ProverParseError("cannot categorize %s" % current.__class__.__name__)
def _categorize_NegatedExpression(self, current):
negated = current.term
if isinstance(negated, NegatedExpression):
return Categories.D_NEG
elif isinstance(negated, FunctionVariableExpression):
return Categories.N_PROP
elif TableauProver.is_atom(negated):
return Categories.N_ATOM
elif isinstance(negated, AllExpression):
return Categories.N_ALL
elif isinstance(negated, AndExpression):
return Categories.N_AND
elif isinstance(negated, OrExpression):
return Categories.N_OR
elif isinstance(negated, ImpExpression):
return Categories.N_IMP
elif isinstance(negated, IffExpression):
return Categories.N_IFF
elif isinstance(negated, EqualityExpression):
return Categories.N_EQ
elif isinstance(negated, ExistsExpression):
return Categories.N_EXISTS
elif isinstance(negated, ApplicationExpression):
return Categories.N_APP
else:
raise ProverParseError("cannot categorize %s" % negated.__class__.__name__)
class Debug:
def __init__(self, verbose, indent=0, lines=None):
self.verbose = verbose
self.indent = indent
if not lines:
lines = []
self.lines = lines
def __add__(self, increment):
return Debug(self.verbose, self.indent + 1, self.lines)
def line(self, data, indent=0):
if isinstance(data, tuple):
ex, ctx = data
if ctx:
data = f"{ex}, {ctx}"
else:
data = "%s" % ex
if isinstance(ex, AllExpression):
try:
used_vars = "[%s]" % (
",".join("%s" % ve.variable.name for ve in ex._used_vars)
)
data += ": %s" % used_vars
except AttributeError:
data += ": []"
newline = "{}{}".format(" " * (self.indent + indent), data)
self.lines.append(newline)
if self.verbose:
print(newline)
class Categories:
ATOM = 0
PROP = 1
N_ATOM = 2
N_PROP = 3
APP = 4
N_APP = 5
N_EQ = 6
D_NEG = 7
N_ALL = 8
N_EXISTS = 9
AND = 10
N_OR = 11
N_IMP = 12
OR = 13
IMP = 14
N_AND = 15
IFF = 16
N_IFF = 17
EQ = 18
EXISTS = 19
ALL = 20
def testTableauProver():
tableau_test("P | -P")
tableau_test("P & -P")
tableau_test("Q", ["P", "(P -> Q)"])
tableau_test("man(x)")
tableau_test("(man(x) -> man(x))")
tableau_test("(man(x) -> --man(x))")
tableau_test("-(man(x) and -man(x))")
tableau_test("(man(x) or -man(x))")
tableau_test("(man(x) -> man(x))")
tableau_test("-(man(x) and -man(x))")
tableau_test("(man(x) or -man(x))")
tableau_test("(man(x) -> man(x))")
tableau_test("(man(x) iff man(x))")
tableau_test("-(man(x) iff -man(x))")
tableau_test("all x.man(x)")
tableau_test("all x.all y.((x = y) -> (y = x))")
tableau_test("all x.all y.all z.(((x = y) & (y = z)) -> (x = z))")
# tableau_test('-all x.some y.F(x,y) & some x.all y.(-F(x,y))')
# tableau_test('some x.all y.sees(x,y)')
p1 = "all x.(man(x) -> mortal(x))"
p2 = "man(Socrates)"
c = "mortal(Socrates)"
tableau_test(c, [p1, p2])
p1 = "all x.(man(x) -> walks(x))"
p2 = "man(John)"
c = "some y.walks(y)"
tableau_test(c, [p1, p2])
p = "((x = y) & walks(y))"
c = "walks(x)"
tableau_test(c, [p])
p = "((x = y) & ((y = z) & (z = w)))"
c = "(x = w)"
tableau_test(c, [p])
p = "some e1.some e2.(believe(e1,john,e2) & walk(e2,mary))"
c = "some e0.walk(e0,mary)"
tableau_test(c, [p])
c = "(exists x.exists z3.((x = Mary) & ((z3 = John) & sees(z3,x))) <-> exists x.exists z4.((x = John) & ((z4 = Mary) & sees(x,z4))))"
tableau_test(c)
# p = 'some e1.some e2.((believe e1 john e2) and (walk e2 mary))'
# c = 'some x.some e3.some e4.((believe e3 x e4) and (walk e4 mary))'
# tableau_test(c, [p])
def testHigherOrderTableauProver():
tableau_test("believe(j, -lie(b))", ["believe(j, -lie(b) & -cheat(b))"])
tableau_test("believe(j, lie(b) & cheat(b))", ["believe(j, lie(b))"])
tableau_test(
"believe(j, lie(b))", ["lie(b)"]
) # how do we capture that John believes all things that are true
tableau_test(
"believe(j, know(b, cheat(b)))",
["believe(j, know(b, lie(b)) & know(b, steals(b) & cheat(b)))"],
)
tableau_test("P(Q(y), R(y) & R(z))", ["P(Q(x) & Q(y), R(y) & R(z))"])
tableau_test("believe(j, cheat(b) & lie(b))", ["believe(j, lie(b) & cheat(b))"])
tableau_test("believe(j, -cheat(b) & -lie(b))", ["believe(j, -lie(b) & -cheat(b))"])
def tableau_test(c, ps=None, verbose=False):
pc = Expression.fromstring(c)
pps = [Expression.fromstring(p) for p in ps] if ps else []
if not ps:
ps = []
print(
"%s |- %s: %s"
% (", ".join(ps), pc, TableauProver().prove(pc, pps, verbose=verbose))
)
def demo():
testTableauProver()
testHigherOrderTableauProver()
if __name__ == "__main__":
demo()