Initial commit
This commit is contained in:
24
backend/venv/Lib/site-packages/nltk/inference/__init__.py
Normal file
24
backend/venv/Lib/site-packages/nltk/inference/__init__.py
Normal 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
|
||||
614
backend/venv/Lib/site-packages/nltk/inference/api.py
Normal file
614
backend/venv/Lib/site-packages/nltk/inference/api.py
Normal 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
|
||||
651
backend/venv/Lib/site-packages/nltk/inference/discourse.py
Normal file
651
backend/venv/Lib/site-packages/nltk/inference/discourse.py
Normal 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()
|
||||
383
backend/venv/Lib/site-packages/nltk/inference/mace.py
Normal file
383
backend/venv/Lib/site-packages/nltk/inference/mace.py
Normal 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()
|
||||
561
backend/venv/Lib/site-packages/nltk/inference/nonmonotonic.py
Normal file
561
backend/venv/Lib/site-packages/nltk/inference/nonmonotonic.py
Normal 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()
|
||||
507
backend/venv/Lib/site-packages/nltk/inference/prover9.py
Normal file
507
backend/venv/Lib/site-packages/nltk/inference/prover9.py
Normal 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()
|
||||
759
backend/venv/Lib/site-packages/nltk/inference/resolution.py
Normal file
759
backend/venv/Lib/site-packages/nltk/inference/resolution.py
Normal 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()
|
||||
712
backend/venv/Lib/site-packages/nltk/inference/tableau.py
Normal file
712
backend/venv/Lib/site-packages/nltk/inference/tableau.py
Normal 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()
|
||||
Reference in New Issue
Block a user