Arithmetik im Automatischen Theorembeweisen Diplomarbeit von: Holger Trölenberg Universität Potsdam Institut für Informatik Aufgabenstellung und Betreuung: Prof. Dr. Christoph Kreitz Inhaltsverzeichnis 1 Einleitung......................................................................................................................1 2 Stand der Technik.........................................................................................................2 2.1 Bedeutung der Arithmetik im Automatischen Beweisen.............................................2 2.2 Übersicht über Arithmetische Theorien.......................................................................2 2.2.1 Peano-Arithmetik......................................................................................................2 2.2.2 Presburger Arithmetik...............................................................................................2 2.2.3 Induktionsfreie Arithmetik........................................................................................3 2.3 Arithmetische Entscheidungsprozeduren....................................................................5 2.3.1 Elimination of Quantifiers........................................................................................5 2.3.2 Fourier-Motzkin Variable Elimination.....................................................................5 2.3.3 Omega-Test...............................................................................................................6 2.3.4 Cooper......................................................................................................................6 2.3.5 Hodes........................................................................................................................6 2.3.6 Loop Residue............................................................................................................6 2.3.7 Automatenbasierte Entscheidungsprozeduren..........................................................6 2.3.8 Sup-Inf......................................................................................................................6 2.3.9 Lineare Optimierung.................................................................................................7 2.3.10 Arith........................................................................................................................7 3 Implementierung..........................................................................................................8 3.1 Erster Schritt: Umformen in polynomielle Form......................................................10 3.2 Zweiter Schritt: Umformen in kanonische polynomielle Form.................................11 3.3 Dritter Schritt: Ersetzen der Konstanten-freien Teile durch Variablen......................13 3.4 Vierter Schritt: Konvertieren aller Relationen in Größergleich-Relationen..............15 3.5 Fünfter Schritt: Konstruieren des Graphen für den Max-Path-Weights-Algorithmus................................................................................16 3.6 Sechster Schritt: Suchen nach einem positiven Zyklus.............................................17 4 Test der Implementierung..........................................................................................18 4.1 Test von polyRelations...........................................................................................18 4.2 Test von canRelations.............................................................................................19 4.3 Test von varRelations.............................................................................................20 4.4 Test von geRelations...............................................................................................20 4.5 Test von mpwGraph.....................................................................................................21 4.6 Test von maxPathWeights.........................................................................................21 4.7 Test von arith...........................................................................................................22 4.8 Test der Laufzeit von Arith......................................................................................23 5 Ausblick.......................................................................................................................24 Quellen............................................................................................................................25 Anhang: Quelltext der Implementierung....................................................................26 ii Kapitel 1: Einleitung Diese Arbeit beschäftigt sich mit dem Versuch, dem automatischen Beweiser LeanCop Unterstützung für Arithmetik hinzuzufügen. Dazu wurde eine Entscheidungsprozedur für eine eingeschränkte Arithmetik implementiert, um sie in LeanCop zu integrieren. LeanCop ist ein automatischer Theorembeweiser für klassische Logik 1. Stufe, der auf dem Konnektionskalkül von W. Bibel basiert. Er ist von Jens Otten in Prolog implementiert, sehr kompakt und effizient. In der aktuellen Version LeanCop 2.1 unterstützt er SWIProlog, ECLiPSe Prolog und SICStus Prolog, Eingabe von Formeln im TPTP-Format neben dem eigenen LeanCop-Format und die Generierung von lesbaren Beweisen in verschiedenen Detailgraden. Bei der CASC-22 belegte er den 5. Platz von insgesamt 11 Theorembeweisern nach Anzahl gelöster Probleme. Es gibt bereits viele interaktive Beweiser, die Arithmetik beherrschen, z.B. Coq, MetaPRL und NuPRL. Seit kurzem beginnen auch automatische Beweiser, Arithmetik zu unterstützen. Als ersten Schritt, Arithmetik in LeanCop zu integrieren, wurde die Entscheidungsprozedur Arith implementiert, da sie eine Arithmetik verwendet, die mächtiger ist als die Presburger Arithmetik, welche die meisten anderen Entscheidungsprozeduren verwenden: die Induktionsfreie Arithmetik. Nach Arith sollen weitere Entscheidungsprozeduren für andere Arten von Arithmetik integriert werden, damit LeanCop für eine logische Formel, die Arithmetik enthält, die passendste Prozedur auswählen und aufrufen kann. Außerdem soll die Behandlung von Arithmetik so verbessert werden, dass auch dabei ein Beweis generiert wird. Kapitel 2 versucht einen Überblick über arithmetische Entscheidungsprozeduren und deren zu Grunde liegenden arithmetischen Theorien zu geben. Kapitel 3 beleuchtet die Implementierung der Entscheidungsprozedur Arith und Kapitel 4 deren Test. Kapitel 5 zeigt mögliche Anknüpfungspunkte für weiterführende Arbeiten auf. [1, 2] 1 Kapitel 2: Stand der Technik 2.1 Bedeutung der Arithmetik im Automatischen Beweisen Arithmetik hat im automatischen Theorembeweisen viele Anwendungen: z.B. in Programmsynthese und -verifikation und Mathematik. In der Programmverifikation, beispielsweise, lässt sich die Gültigkeit von Arrayindizes und For-Schleifenvariablen mit Hilfe von Ungleichungssystemen prüfen. 2.2 Übersicht über Arithmetische Theorien Gödel's Unvollständigkeitssätzen zu Folge ist die Arithmetik der natürlichen Zahlen weder entscheidbar noch vollständig axiomatisierbar. Daher muss man sich im Automatischen Beweisen auf entscheidbare Fragmente der Arithmetik beschränken. Im Folgenden werden die wichtigsten Arithmetischen Theorien jeweils kurz genauer beleuchtet. 2.2.1 Peano-Arithmetik Die Peano-Axiome zur Definition der Menge der natürlichen Zahlen wurden 1889 von Giuseppe Peano formal formuliert. Sie lauten (informal): 1. 0 ist eine Zahl. 2. Wenn n eine Zahl ist, so gibt es genau einen Nachfolger von n, welcher ebenso eine Zahl ist. 3. 0 ist nicht Nachfolger einer Zahl. 4. Zwei Zahlen mit gleichem Nachfolger sind selbst gleich. 5. Die kleinste Menge, die 0 und jeden Nachfolger eines ihrer Elemente enthält, ist die Menge der natürlichen Zahlen. Diese Axiome beschreiben die natürlichen Zahlen, die Nachfolgeroperation, Gleichheit und Induktion. Darauf aufbauend können Addition, Multiplikation und die KleinergleichRelation definiert werden, sodass (ℕ, +, 0, ⋅, 1, ≤) einen geordneten Halbring bildet. Die Peano-Arithmetik enthält alle Peano-Axiome, nur das Induktionsaxiom 5 ist in Prädikatenlogik zweiter Stufe definiert und wird durch abzählbar unendlich viele Induktionsaxiome der Prädikatenlogik erster Stufe ersetzt, eines für jede mögliche logische Formel: 5. Für jede logische Formel 1. Stufe P(x): P(0) ∧ ∀(x.P(x) ⇒ P(x')) ⇒ ∀x.P(x) Peano-Arithmetik ist unentscheidbar und unvollständig und ihre Konsistenz ist nicht nachweisbar. [3] 2.2.2 Presburger Arithmetik 1929 erfand Mojżesz Presburger eine Arithmetik, die nach ihm benannt wurde. Sie basiert auf der Peano-Arithmetik, beinhaltet aber nicht die Multiplikation sondern nur die Addition. Die Axiome lauten: 1. 0 ≠ x' 2 2. x' = y' ⇒ x = y 3. x + 0 = x 4. x + y' = (x + y)' 5. Für jede logische Formel 1. Stufe P(x): P(0) ∧ ∀(x.P(x) ⇒ P(x')) ⇒ ∀x.P(x) Wieder lassen sich die Operationen Addition, Multiplikation mit Konstanten und die Relationen definieren. Die Presburger Arithmetik ist schwächer als die Peano-Arithmetik, da Konzepte wie Multiplikation, Teilbarkeit und Primzahlen mit ihr nicht formalisiert werden können. Das macht sie entscheidbar, vollständig und konsistent. [4] 2.2.3 Induktionsfreie Arithmetik Die Induktionsfreie Arithmetik wurde 1978 von Constable und O'Donnell formuliert. Sie kennt Addition, Subtraktion, Multiplikation auf ganzen Zahlen und die Vergleichsrelationen, aber keine Induktion. Außerdem ist die Substitution auf ganze Vergleichsoperanden einer Relation beschränkt. Das macht Induktionsfreie Arithmetik entscheidbar. Axiome der Induktionsfreien Arithmetik: (gelten für alle ganzen Zahlen w, x, y, z) Gleichheitsaxiome: Reflexivität: x = x Symmetrie: x = y ⇒ y = x Transitivität: x = y ∧ y = z ⇒ x = z eingeschränkte Substitutivität: x = y ∧ x ρ z ⇒ y ρ z für ρ ∈ {=, ≠, <, >, ≤, ≥} x=y∧zρx⇒zρy Axiome der Konstantenarithmetik: 1 + 1 = 2, 2 + 1 = 3, 3 + 1 = 4, … Ringaxiome der ganzen Zahlen: Kommutativgesetze: x + y = y + x, x ∗ y = y ∗ x Assoziativgesetze: (x + y) + z = x + (y + z), (x ∗ y) ∗ z = x ∗ (y ∗ z) Distributivgesetz: x ∗ (y + z) = (x ∗ y) + (x ∗ z) Neutrale Elemente: x + 0 = x, x ∗ 1 = x Entgegengesetztes: x + (−x) = 0 Definition der Subtraktion: x − y = x + (−y) Axiome der diskreten linearen Ordnung: Irreflexivität: ¬(x < x) Trichotomie: x < y ∨ x = y ∨ y < x Transitivität: x < y ∧ y < z ⇒ x < z Diskretheit: ¬(x < y ∧ y < x + 1) Definition von Ordnungsrelationen und Ungleichheiten: 3 x ≠ y ⇔ ¬(x = y) x>y⇔y<x x≤y⇔x<y∨x=y x≥y⇔y<x∨x=y Monotonieaxiome: Addition: x ≥ y ∧ z ≥ w ⇒ x + z ≥ y + w Subtraktion: x ≥ y ∧ z ≤ w ⇒ x − z ≥ y − w Multiplikation: x ≥ 0 ∧ y ≥ z ⇒ x ∗ y ≥ x ∗ z Faktorisierung: x > 0 ∧ x ∗ y ≥ x ∗ z ⇒ y ≥ z Die Monotonoieaxiome der Addition und Subtraktion werden auch als triviale Monotonien bezeichnet, wenn z und w Konstanten sind. [5, 6] 4 2.3 Arithmetische Entscheidungsprozeduren Im formalen Schließen gibt es oft Teilprobleme, die zwar einfach zu beweisen sind, für die sich ein formaler Beweis jedoch nicht lohnt und lediglich die Aussage, ob die aufgestellte Behauptung wahr ist oder nicht, von Interesse ist. Hier lohnt sich der Einsatz von Entscheidungsprozeduren, die das Problem oft auf ein anderes reduzieren, um so eine schnelle Lösung zu finden, die einem normalen Benutzer nicht unbedingt verständlich ist. Eine Arithmetische Entscheidungsprozedur prüft die Allgemeingültigkeit einer Formel gemäß einer bestimmten Arithmetischen Theorie. Dabei ist eine Formel eine logische Verknüpfung von Gleichungen und Ungleichungen der Arithmetischen Theorie. Die Prozedur muss korrekt und vollständig sein und für jede syntaktisch gültige Formel terminieren. Die Korrektheit der Entscheidungsprozedur muss sich in der jeweiligen Arithmetischen Theorie beweisen lassen. Da Arithmetik im Allgemeinen unentscheidbar ist, muss sich eine Entscheidungsprozedur auf eine eingeschränkte, entscheidbare arithmetische Theorie beschränken. Im Folgenden werden die wichtigsten Arithmetischen Entscheidungsprozeduren vorgestellt. Alle außer Arith (Kapitel 2.3.10) sind für die Presburger Arithmetik geeignet, Arith für die Induktionsfreie Arithmetik. [7] 2.3.1 Elimination of Quantifiers Bei diesem Verfahren werden nacheinander alle Quantoren und ihre quantifizierten Variablen entfernt, indem jeweils die logische Formel durch eine äquivalente Formel ohne den Quantor ersetzt wird. Das ursprünglich von G. Kreisel und J. L. Krevine entwickelte Verfahren ist sehr ineffizient, da sich die Formel dabei stark vergrößert. Diese Idee wird u.a. verwendet in den Entscheidungsprozeduren Fourier-Motzkin Variable Elimination, Omega-Test, Cooper und Hodes. [8, 9, 10] 2.3.2 Fourier-Motzkin Variable Elimination Dieses Verfahren zum Lösen von Ungleichungssystemen mit rationalen oder reellen Variablen wurde 1826 von Joseph Fourier entwickelt und 1936 von Theodore Motzkin wieder entdeckt. Eine Variable nach der anderen kann eliminiert werden, bis das Ungleichungssystem nur noch Konstanten enthält, deren Vergleich trivial ist. Um eine Variable x zu eliminieren, werden die Ungleichungen so umgeformt, dass die Variable mit einem positiven Koeffizienten allein auf einer Seite steht, und dann in drei Gruppen eingeteilt: m Ungleichungen der Form aix ≥ Ai, n der Form bix ≤ Bi (ai, bi > 0) und k Ungleichungen, die x nicht enthalten. Es gilt max { ∣1≤i≤m }≤x≤min { ∣1≤i ≤n }. Ai ai Bi bi Das Ungleichungssystem ist äquivalent zu dem mit den m⋅n Ungleichungen bj⋅Ai ≤ ai⋅Bj für alle 1 ≤ i ≤ m, 1 ≤ j ≤ n und den k Ungleichungen ohne x. In diesem Ungleichungssystem kommt x nicht mehr vor. Mit Hilfe obiger Formel kann eine erfüllende Variablenbelegung rekonstruiert werden. [11] 5 2.3.3 Omega-Test Der Omega-Test in eine Erweiterung der Fourier-Motzkin Variable Elimination auf die Arithmetik der ganzen Zahlen. Er wurde 1991 von William Pugh entwickelt. [12] 2.3.4 Cooper Diese Entscheidungsprozedur basiert auf Elimination of Quantifiers, ist aber effizienter, da die Formel nicht in disjunktive Normalform transformiert werden muss und im weiteren Verfahren deutlich weniger wächst. Sie wurde 1972 von D. C. Cooper entwickelt. Laut D. 2 Oppen (1975) hat der Algorithmus im Worst-Case mit O 22 die bestmögliche, n erreichbare Zeitkomplexität. [8, 10] 2.3.5 Hodes Auch diese Entscheidungsprozedur basiert auf Elimination of Quantifiers, verwendet jedoch Presburger Arithmetik mit reellen Zahlen. Sie wurde 1971 von Louis Hodes entwickelt. [9] 2.3.6 Loop Residue Dieses Verfahren wurde 1981 von Robert Shostak entwickelt und entscheidet Presburger Arithmetik mit reellen Variablen. Es ist eine Erweiterung eines Verfahrens von V. R. Pratt, 1971, das ein System von Ungleichungen mit jeweils höchstens zwei Variablen in einen Graphen umformt und bestimmte Kreise (Loops) sucht, die das Ungleichungssystem widerlegen. Das Loop Residue-Verfahren kann darüber hinaus Ungleichungen mit beliebig vielen Variablen und beliebigen Koeffizienten verarbeiten. [13] 2.3.7 Automatenbasierte Entscheidungsprozeduren Diese Klasse von Entscheidungsprozeduren basiert auf endlichen Automaten. Für Arithmetik mit reellen Variablen werden Büchli-Automaten benötigt, die ω-Sprachen, also Sprachen mit unendlichen Wörtern, akzeptieren. [14] 2.3.8 Sup-Inf Diese Entscheidungsprozedur für existenzquantorenfreie Presburger Arithmetik wurde 1975 von W. W. Bledsoe entwickelt und 1977 von Robert E. Shostak verbessert. Sie verwendet zwei Algorithmen SUP und INF, die das Supremum (kleinste untere Schranke) und das Infimum (größte obere Schranke) der möglichen Belegungen der Variablen in der negierten Formel ermitteln. Die Formel ist gültig genau dann, wenn für eine der Variablen das Supremum kleiner als das Infimum ist, genau dann, wenn es keine erfüllende Belegung für die negierte Formel gibt. [10] 6 2.3.9 Lineare Optimierung Ungleichungssysteme in Presburger Arithmetik lassen sich auch als Probleme der linearen Optimierung darstellen. Dadurch lassen sich Lösungsverfahren für lineare Optimierung auch als Entscheidungsprozeduren verwenden. 2.3.10 Arith Diese Entscheidungsprozedur für Induktionsfreie Arithmetik wurde 1982 von T. Chan entwickelt, um die Korrektheit eines logischen Schlusses über Arithmetik zu prüfen. Arith prüft, ob eine gegebene Konklusion, die eine Disjunktion arithmetischer Relationen ist, aus einer gegebenen Hypothese folgt, die eine Konjunktion arithmetischer Relationen ist. Alle sechs Vergleichsrelationen (=, ≠, <, >, ≤, ≥) werden akzeptiert; alle Variablen sind implizit allquantifiziert. Arith führt im Groben die folgenden Schritte durch, um zu prüfen ob Hypothese ⇒ Konklusion gültig ist: Zuerst werden alle Relationen der Konklusion negiert und der Hypothese hinzugefügt, so dass nun die Widersprüchlichkeit der Hypothese zu zeigen ist. Dann werden alle arithmetischen Terme in eine polynomielle Normalform umgeformt. Jeder Term hat dann die Form c oder c + T oder T, wobei c eine Konstante ist und T ein beliebiges Polynom (in mehreren Variablen) ohne ein konstantes Glied. Dann werden alle konstantenfreien Terme T durch neu eingeführte Variablen ersetzt, gleiche Terme durch gleiche Variablen. Nun werden alle Gleichungen und Ungleichungen zuerst in Größergleich-Ungleichungen und dann in gewichtete Kanten eines gerichteten Graphen umgeformt. Die Konjunktion der Größergleich-Ungleichungen ist genau dann widersprüchlich, wenn der Graph einen Kreis besitzt, dessen Kantengewichte eine positive Summe ergeben. Der Schwachpunkt der Prozedur ist, dass für viele Probleme die nichttrivialen Monotonieaxiome benötigt werden (siehe 2.2.3), welche von dem geschilderten Verfahren nicht berücksichtigt werden. Beispielsweise lässt sich der Schluss x = 3 ∧ y = 2 ⇒ x + y = 5 nicht prüfen, da der Term x + y durch eine neue Variable ersetzt wird und eine Anwendung des Monotonieaxioms für Addition nötig wäre. Aus diesem Grund wird Arith in erster Linie in interaktiven Beweisentwicklungssystemen eingesetzt, beispielsweise in MetaPRL und NuPRL. [6, 15] 7 Kapitel 3: Implementierung Dieses Kapitel beschäftigt sich mit der Implementierung der Entscheidungsprozedur Arith für Induktionsfreie Arithmetik entsprechend [6]. Auf Optimierungen, wie z.B. die dort beschriebene Prozedur nodesat, wurde dabei zugunsten der leichten Verständlichkeit verzichtet. Als Implementierungssprache wurde Prolog gewählt, da sich die Prozedur so unkompliziert in LeanCop, welches hauptsächlich in Prolog geschrieben wurde, integrieren lässt. Die Hauptprozedur arith nimmt als Eingabe eine Hypothese und eine Konklusion, die jeweils Listen von Relationen sind. Die Hypothese repräsentiert die Konjunktion ihrer Relationen und die Konklusion die Disjunktion der ihren. Die möglichen Relationsoperatoren sind die Prolog-Operatoren =, \=, <, >, =< und >=. Deren Operanden werden als Komparanden bezeichnet und dürfen so aufgebaut sein, wie in Kapitel 3.1 beschrieben. liefert true, falls die Disjunktion der eingegebenen Konklusionsrelationen in der Induktionsfreien Arithmetik aus der Konjunktion der Hypothesenrelationen folgt. Sämtliche Variablen werden dabei als implizit all-quantifiziert behandelt. arith % arith(+Hypothesis, +Conclusion) arith(Hypothesis, [Pos|Conclusion]) :!, negRelation(Pos, Neg), arith([Neg|Hypothesis], Conclusion). arith(Relations, []) :\+(( polyRelations(Relations, PolyRelations), canRelations(PolyRelations, CanRelations), varRelations(CanRelations, VarRelations, Vars, Const), geRelations(VarRelations, GERelations), N is Vars+Const, mpwGraph(GERelations, N, Graph), maxPathWeights(Graph, N) )). % negRelation(+Positive, -Negative) negRelation(A=B, A\=B). negRelation(A\=B, A=B). negRelation(A<B, A>=B). negRelation(A>B, A=<B). negRelation(A=<B, A>B). negRelation(A>=B, A<B). Zuerst werden nacheinander alle Relationen der Konklusion entfernt, mit Hilfe der Prozedur negRelation negiert und der Hypothese hinzugefügt, wodurch die repräsentierte Aussage logisch äquivalent bleibt. Dann wird geprüft, ob die entstehende Konklusion widersprüchlich, also nicht gültig ist. Die Prüfung auf Gültigkeit erfolgt in sechs Schritten, die durch separate Prozeduren behandelt und von der Hauptprozedur nacheinander aufgerufen werden. Die ersten drei Schritte dienen dazu, die Relationen Schritt für Schritt umzuformen und sind nach demselben Grundschema konzipiert: Ein- und Ausgabe sind jeweils eine Liste von Relationen. Rekursiv wird eine Relation nach der anderen weitestgehend unabhängig von den anderen Relationen umgeformt. Dazu wird zuerst die Relation in linken Komparand, Operator und rechten Komparand 8 zerlegt. Dann werden beide Komparanden nacheinander umgeformt und dann wieder mit demselben Operator zur Ergebnis-Relation zusammengesetzt. Dieses Vorgehen lässt sich im Quellcode des ersten Schritts gut erkennen: % polyRelations(+Relations, -PolynomialRelations) polyRelations([], []). polyRelations([Rel|Relations], [Poly|PolyRelations]) :Rel=..[Op, RelA, RelB], polyComparand(RelA, PolyA), polyComparand(RelB, PolyB), Poly=..[Op, PolyA, PolyB], polyRelations(Relations, PolyRelations). 9 3.1 Erster Schritt: Umformen in polynomielle Form Im ersten Schritt werden alle Komparanden durch Anwendung des Distributivgesetzes von Addition und Multiplikation in eine polynomielle Form umgeformt. Die Prozedur polyRelations (Quelltext siehe oben) implementiert diesen Schritt. Der Eingabekomparand darf sich auf beliebige Weise aus Variablen, ganzen Zahlen, Atomen und den Operationen Addition, Subtraktion, Multiplikation und den beiden einstelligen Vorzeichenoperationen zusammensetzen. Dabei werden die Prolog-Operatoren +, - und * verwendet. Andere Rechenoperationen werden von dieser Implementierung nicht unterstützt. Potenzen mit natürlichen Exponenten sollten daher als Produkt dargestellt werden. Beispiel: X3 sollte als X*X*X dargestellt werden. Atome werden wie Variablen behandelt. Der Ausgabekomparand ist eine Summe von Produkten. Die Produkte werden erst im nächsten Schritt vereinfacht und bleiben zunächst so, wie sie durchs „Ausmultiplizieren“ entstehen. Beispiel: 3*X*Y+3*(-1*1)*Y+4*Y+Y*2 Die Prozedur polyComparand leistet die eigentliche Umformung eines Komparanden: Bei Multiplikation und Addition werden zunächst die beiden Operanden rekursiv umgeformt, bei der Multiplikation werden dann die entstehenden Polynome miteinander multipliziert. Dazu multipliziert die Prozedur polyLeftMult zuerst das rechte Polynom mit den Gliedern des linken und danach die Prozedur polyRightMult jeweils ein linkes Glied mit den rechten. Die Subtraktion wird auf Addition des Entgegengesetzten abgebildet, das negative Vorzeichen auf Multiplikation mit −1 und das positive Vorzeichen wird ignoriert. Ein Term, der nur aus einer Variable besteht, muss in der ersten Klausel behandelt werden, weil er sonst mit allem unifizieren würde, und bleibt unverändert. Alle anderen Terme, also Zahlen und Atome, – aber auch alle unerwarteten Terme – werden in der letzten Klausel behandelt. Damit nicht jeder Term nach Backtracking auch mit der letzten Klausel unifizieren kann, haben alle vorigen Klauseln Cuts. % polyComparand(+Term, -Polynomial) polyComparand(Var, Var) :- var(Var), !. polyComparand(A*B, P) :- !, polyComparand(A, P1), polyComparand(B, P2), polyLeftMult(P1, P2, P). polyComparand(A+B, P1+P2) :- !, polyComparand(A, P1), polyComparand(B, P2). polyComparand(A-B, P) :- !, polyComparand(A+(-B), P). polyComparand(-A, P) :- !, polyComparand((-1)*A, P). polyComparand(+A, P) :- !, polyComparand(A, P). polyComparand(Atom, Atom). % polyLeftMult(+P1, +P2, -P) polyLeftMult(Var, R, P) :- var(Var), !, polyRightMult(Var, R, P). polyLeftMult(A+B, R, P1+P2) :- !, polyLeftMult(A, R, P1), polyLeftMult(B, R, P2). polyLeftMult(Monomial, R, P) :- polyRightMult(Monomial, R, P). % polyRightMult(+Factor, +P2, -P) polyRightMult(L, Var, L*Var) :- var(Var), !. polyRightMult(L, A+B, P1+P2) :- !, polyRightMult(L, A, P1), polyRightMult(L, B, P2). polyRightMult(L, Monomial, L*Monomial). 10 Der im Quelltext verwendete Begriff „Monomial“, zu deutsch Monom, bezeichnet ein Glied eines Polynoms, also ein Produkt aus Potenzen von Variablen und einem optionalen Koeffizienten. Beispiel: 3ab2. In dieser Implementierung kommen jedoch ggf. mehrere Zahlen aber keine Potenzen in den „Monomials“ vor, wie bereits in einem Beispiel demonstriert. 3.2 Zweiter Schritt: Umformen in kanonische polynomielle Form Im zweiten Schritt werden die polynomiellen Komparanden zu einer kanonischen Form vereinfacht, indem die in den Gliedern enthaltenen Zahlen zu Koeffizienten und Glieder mit gleichen Variablen zusammengefasst werden und alles sortiert wird. Der Quelltext der Prozedur canRelations folgt wieder dem Grundschema und ist im Anhang zu finden. Der Eingabekomparand muss eine Summe von Produkten sein, wie im ersten Schritt beschrieben. Der Ausgabekomparand ist ebenfalls eine Summe von Gliedern, die Produkte sind, wobei jedoch kein Glied 0 ist, höchstens der erste Faktor eines Glieds eine Zahl sein kann – als Koeffizient – und keine zwei Glieder abgesehen vom Koeffizienten gleich sind. Außerdem sind in jedem Glied alle Faktoren (hinter dem Koeffizienten) sortiert, so dass gleiche Faktoren aufeinander folgen, und die Glieder sind ebenfalls sortiert, so dass ein Glied, das nur aus einer Zahl besteht, ganz links steht. Letzteres ist für den dritten Schritt von Bedeutung. Beispiel: 2+3*X*Y+(-3)*Y+6*Y*Y Die Prozedur canComparand formt einen Komparanden um: % canComparand(+Polynomial, -CanonicalComparand) canComparand(Poly, Can) :canPoly2List(Poly, PL), msort(PL, SPL), canList2Can(SPL, Can2), (Can2==[] -> Can=0 ; Can=Can2). Dazu wird der Komparand zunächst in eine Liste von Monomen umgewandelt, diese dann sortiert und wieder zurück in ein kanonisches Polynom umgewandelt. Die letzte Zeile behandelt den Spezialfall, dass sich der gesamte Komparand zu 0 vereinfacht. Die Umwandlung von Polynom in Liste von Monomen übernehmen die Prozeduren canPoly2List und canMonEnc: % canPoly2List(+Polynomial, -Monomials) canPoly2List(Var, [monomial([Var], 1)]) :- var(Var), !. canPoly2List(A+B, P) :!, canPoly2List(A, PA), canPoly2List(B, PB), append(PA, PB, P). canPoly2List(Monomial, [monomial(CM, C)]) :canMonEnc(Monomial, M, C), msort(M, CM). % canMonEnc(+Monomial, -Factors, -Coefficient) canMonEnc(Var, [Var], 1) :- var(Var), !. canMonEnc(A*B, M, C) :!, canMonEnc(A, MA, CA), canMonEnc(B, MB, CB), append(MA, MB, M), C is CA*CB. canMonEnc(N, [], N) :- number(N), !. canMonEnc(Factor, [Factor], 1). canPoly2List durchläuft das Polynom rekursiv und kodiert jedes Glied mit Hilfe von canMonEnc zu einem Monom in der Form monomial(Factors, Coefficient), wobei Coefficient der Koeffizient und Factors eine sortierte Liste aller anderen Faktoren ist. Für die Addition werden zunächst beide Operanden rekursiv in Monomlisten umgewandelt 11 und diese dann konkateniert. Wie schon bei polyComparand behandelt die erste Klausel Variablen und die letzte Glieder und alles unerwartete; wieder werden Cuts verwendet. Die letzte Klausel kodiert ein Glied mit Hilfe von canMonEnc und sortiert seine Faktoren. durchläuft ein Glied rekursiv analog zu canPoly2List und unterscheidet zwischen Zahlen und anderen Faktoren. Die Zahlen werden von den anderen Faktoren getrennt, zu einem Koeffizienten multipliziert und rekursiv zurück gereicht. Die anderen Faktoren werden in Listen konkateniert. canMonEnc Zum Sortieren der Faktoren innerhalb eines Glieds und der kodierten Monome wird eine vordefinierte Prolog-Prozedur verwendet. Damit die Monome nicht nach Koeffizienten sondern nach den anderen Faktoren sortiert werden, steht in der Monomkodierung die Liste der Faktoren vor dem Koeffizienten. Die Rückumwandlung von der Liste von Monomen in ein kanonisches Polynom wird von den Prozeduren canList2Can und canMonDec geleistet: % canList2Can(+Monomials, -CanonicalTerm) canList2Can([], []). canList2Can([monomial(M1, C1), monomial(M2, C2)|SPL], Can) :M1==M2, !, C is C1+C2, canList2Can([monomial(M1, C)|SPL], Can). canList2Can([monomial(_, 0)|SPL], Can) :- !, canList2Can(SPL, Can). canList2Can([monomial(M, C)|SPL], Can) :- canMonDec(M, C, CM), canList2Can(SPL, Can2), (Can2==[] -> Can=CM ; Can=CM+Can2). % canMonDec(+Factors, +Coefficient, -Monomial) canMonDec(_, 0, 0) :- !. canMonDec([], C, C). canMonDec([F, F2|M], 1, F*CM) :- !, canMonDec([F2|M], 1, CM). canMonDec([F], 1, F) :- !. canMonDec([F|M], C, C*CM) :- canMonDec([F|M], 1, CM). durchläuft die Liste der Monome von links nach rechts und wandelt sie in eine rechtsassoziative Summe um. Ein konstantes Monom, das also außer einem Koeffizienten keine anderen Faktoren hat, wurde zuvor beim Sortieren links eingeordnet und landet so in der kanonischen Form links außen, was für den dritten Schritt von Bedeutung ist. canList2Can Monome, die abgesehen von ihren Koeffizienten gleich sind, folgen auf Grund der Sortierung aufeinander und werden entsprechend dem Distributivgesetz zusammengefasst. Ein Monom mit Koeffizient 0 wird ignoriert und eines mit einem anderen Koeffizienten wird dekodiert und rekursiv mit allen folgenden in eine Summe umgewandelt. Das Dekodieren der Monome leistet canMonDec, indem die Liste der Faktoren und der Koeffizient rekursiv in ein Produkt umgewandelt werden. Zuerst wird in der letzten Klausel der Koeffizient verwendet (sofern er nicht 1 ist), dann werden die anderen Faktoren der Reihe nach rekursiv als rechtsassoziatives Produkt nachgestellt, wofür der Koeffizient nun auf 1 gesetzt ist und ignoriert wird. Die erste Klausel für Monome mit Koeffizient 0 wird nicht verwendet und ist nur der Vollständigkeit halber implementiert. 12 3.3 Dritter Schritt: Ersetzen der Konstanten-freien Teile durch Variablen Nach dem zweiten Schritt haben die Komparanden die Form c+T, c oder T, wobei c eine Konstante – also eine ganze Zahl – ist und T ein beliebiges Polynom, das keine Konstanten enthält. T ist ein Konstanten-freier Teil. Im dritten Schritt wird jeder Konstanten-freie Teil aller Komparanden durch neue Variablen ersetzt, wobei gleiche Teile auch durch gleiche Variablen ersetzt werden. Der Eingabekomparand muss in kanonischer Form sein, wie im zweiten Schritt beschrieben. Der Ausgabekomparand hat die Form N+v(V), N oder v(V), wobei N eine ganze Zahl und V eine positive ganze Zahl ist. Auch die Prozedur varRelations folgt dem Grundschema, sie liefert jedoch zusätzlich noch die Anzahl der eingeführten Variablen zurück und ob es Komparanden der Form c bzw. N, also ohne variablen Teil, gibt: % varRelations(+CanonicalRelations, -VarReplacedRelations, % -NVariables, -Constants) varRelations(CanRelations, VarRelations, Vars, Const) :varRelations(CanRelations, VarRelations, [], Vars, Const). varRelations([], [], [], 0, 0) :- !. varRelations([], [], [subst(_, Vars)|_], Vars, 0). varRelations([Can|CanRelations], [Var|VarRelations], Substitutions, Vars, Const) :Can=..[Op, CanA, CanB], varSubst(CanA, VarA, Substitutions, SubsA, CA), varSubst(CanB, VarB, SubsA, SubsB, CB), Var=..[Op, VarA, VarB], varRelations(CanRelations, VarRelations, SubsB, Vars, C), Const is C\/CA\/CB. In einem zusätzlichen Parameter wird die anfangs leere Liste der Variablensubstitutionen von der Behandlung des linken Operanden zu der des rechten und rekursiv weitergereicht und dabei nach Bedarf erweitert. Ein konstanten-freier Term wird durch einen Term der Form v(N) ersetzt, wobei N eine natürliche Zahl ist. Die neuen Variablen werden in der Reihenfolge ihrer Einführung bei 1 beginnend durchnummeriert. Für jeden neuen ersetzten Term wird eine entsprechende Variablensubstitution der Form subst(Term, N) der Liste vorangestellt. Die Anzahl der Variablen wird am Ende der Rekursion anhand der Nummer der ersten Substitution in der Liste ermittelt, die auch der Anzahl der Substitutionen entspricht, und rekursiv bis zum ursprünglichen Aufruf zurück gereicht. Ob konstante Komparanden vorkommen, wird bei der Behandlung der einzelnen Komparanden durch die Prozedur varSubst ermittelt und mit Hilfe bitweiser Disjunktion rekursiv zurück gereicht. Dabei steht 1 für wahr und 0 für falsch. % varSubst(+CanonicalComparand, -VarReplacedTerm, +SubstitutionsIn, % -SubstitutionsOut, -Constant) varSubst(N, N, S, S, 1) :- number(N), !. varSubst(N+C, N+V, S, S2, 0) :- number(N), !, varSubst(C, V, S, S2, _). varSubst(C, v(V), S, S, 0) :- member(subst(D, V), S), C==D, !. varSubst(C, v(V), S, [subst(C, V)|S], 0) :- length([_|S], V). varSubst belässt konstante Komparanden unverändert und liefert dabei 1 in Constant 13 zurück. Bei Komparanden mit Konstante und variablem Teil, bleibt die Konstante unverändert und der Variable Teil wird genau so behandelt, wie ohne Konstante; in Constant wird 0 zurückgeliefert. Bei einem variablen Teil wird zunächst die Liste der Substitutionen nach diesem Term durchsucht und bei Erfolg die entsprechende Variablennummer verwendet. Dabei muss die nicht unifizierende Gleichheit verwendet werden, da z.B. Terme, die nur aus einer PrologVariable bestehen, mit allen Termen unifizieren können. Existiert der Term noch nicht, so wird er hinzugefügt, wobei seine Nummer um eins höher ist als die bisherige Anzahl der Substitutionen, die der bisherigen höchsten Nummer entspricht. 14 3.4 Vierter Schritt: Konvertieren aller Relationen in GrößergleichRelationen Im vierten Schritt wird jede Relation durch ein oder mehrere äquivalente GrößergleichRelationen ersetzt. Die Komparanden der Relationen in der Eingabeliste müssen in der Form N+v(V), N oder v(V) vorliegen, wie im dritten Schritt beschrieben. Zusätzlich wird auch die Form v(V)+N unterstützt. Die Ausgabe ist eine Liste von Größergleich-Relationen, die äquivalent zur eingegebenen Liste von Relationen ist. Falls Ungleich-Relationen in der Eingabe vorkommen, werden mehrere alternative Listen durch Backtracking zurückgeliefert, deren Disjunktion logisch äquivalent zur Eingabeliste ist. Bei n Ungleich-Relationen entstehen 2n Alternativen. Die eingegebene Relationenliste ist in der Induktionsfreien Arithmetik genau dann gültig, wenn die Disjunktion aller Alternativen gültig ist, also wenn wenigstens eine Alternative gültig ist. Die Prozedur geRelations wandelt rekursiv jede Relation mit Hilfe der Prozedur geRel in eine Liste äquivalenter Größergleich-Relationen um und konkateniert diese Listen rekursiv. % geRelations(+VarReplacedRelations, -GreaterEqualRelations) geRelations([], []). geRelations([Var|VarRelations], GERelations) :geRel(Var, GERel), geRelations(VarRelations, GERels), append(GERel, GERels, GERelations). % geRel(+Relation, -GreaterEqualRelations) geRel(X\=Y, R) :- geRel(X>Y, R). geRel(X\=Y, R) :- geRel(Y>X, R). geRel(X=Y, [X>=Y, Y>=X]). geRel(X>=Y, [X>=Y]). geRel(X=<Y, [Y>=X]). geRel(X<Y, R) :- geRel(Y>X, R). geRel(X>Y, [X>=Z]) :(number(Y) -> Z is Y+1; Y=v(_) -> Z=Y+1; Y=v(V)+A -> B is A+1, Z=v(V)+B; Y=A+v(V), B is A+1, Z=B+v(V)). wandelt eine Relation in eine Liste äquivalenter Größergleich-Relationen um. Die meisten Fälle sind selbsterklärend. Bei der Größer-Relation muss rechts 1 addiert werden, was bei jeder möglichen Form des rechten Komparanden anders geschieht. Für UngleichRelationen gibt es zwei verschiedene Varianten, die mittels Backtracking durchprobiert werden können. geRel 15 3.5 Fünfter Schritt: Konstruieren des Graphen für den Max-PathWeights-Algorithmus Im fünften Schritt wird eine Liste von Größergleich-Relationen in einen Graph umgeformt, der genau dann einen positiven Zyklus enthält, wenn die Konjunktion der Relationen widersprüchlich ist. Die Eingabe sind eine Liste von Größergleich-Relationen, in deren Komparanden die Konstanten-freien Teile durch Variablen ersetzt sind, und die Anzahl der Knoten des Graphen. Diese ergibt sich aus der Anzahl der eingeführten Variablen, erhöht um 1, falls es konstante Komparanden gibt, was in der Hauptprozedur implementiert ist. Die Ausgabe ist ein gerichteter Graph mit Kantengewichten, dargestellt als eine Liste von gewichteten Kanten in der Form mpw(Origin, Target, Weight). Origin ist der Ausgangsknoten der Kante, Target der Endknoten und Weight das Kantengewicht. Es besteht folgender Zusammenhang zur Eingabe: Es sei N die Anzahl der Knoten. Ein Knoten Node korrespondiert mit jedem Komparanden, der die eingeführte Variable v(Node) enthält; falls ein konstanter Komparand vorhanden ist, korrespondiert der letzte Knoten N mit jedem konstanten Komparanden. Eine Kante mpw(Origin, Target, Weight) korrespondiert mit jeder Größergleich-Relation, deren linker Komparand mit Origin und rechter Komparand mit Target korrespondiert. Seien LWeight und RWeight die dabei jeweils in dem linken und rechten Komparanden enthaltenen Konstanten (bzw. 0, falls nicht vorhanden), dann ist Weight das Maximum aller RWeight − LWeight. Der Grund dafür ist, dass für die Suche nach einem positiven Zyklus nur der jeweils größte Weight-Wert zwischen zwei bestimmten Knoten relevant ist. Die Prozedur mpwGraph betrachtet rekursiv eine Größergleich-Relation nach der anderen. Zunächst werden die Komparanden untersucht, Origin, Target, LWeight und RWeight ermittelt und Weight errechnet. Nach dem rekursiven Aufruf wird die neue Kante in den zurück gereichten Graphen eingefügt, falls sie noch nicht existiert. Falls diese Kante bereits mit niedrigerem Weight-Wert existiert, wird sie ersetzt. % mpwGraph(+GreaterEqualRelations, +NNodes, -Graph) mpwGraph([], _, []). mpwGraph([X>=Y|GERelations], N, Graph) :(number(X) -> Origin=N, LWeight=X ; (X=v(Origin) -> LWeight=0 ; (X=v(Origin)+LWeight ; X=LWeight+v(Origin)), !)), (number(Y) -> Target=N, RWeight=Y ; (Y=v(Target) -> RWeight=0 ; (Y=v(Target)+RWeight ; Y=RWeight+v(Target)), !)), Weight is RWeight-LWeight, mpwGraph(GERelations, N, G), (append(Head, [mpw(Origin, Target, W)|Tail], G) -> (W<Weight -> append(Head, [mpw(Origin, Target, Weight)|Tail], Graph) ; Graph=G) ; Graph=[mpw(Origin, Target, Weight)|G]). 16 3.6 Sechster Schritt: Suchen nach einem positiven Zyklus Im sechsten Schritt wird der im fünften Schritt konstruierte Graph nach einem positiven Zyklus durchsucht. Ein positiver Zyklus ist ein Kreis im Graphen, bei dem die Summe der Kantengewichte positiv ist. Die Prozedur maxPathWeights implementiert den Algorithmus aus [6], wobei die dortigen For-Schleifen in eine Rekursion mit Hilfe der Parameter K, I und J , die den For-SchleifenVariablen entsprechen, umgewandelt werden. Zur Eingabe gehört neben der Kantenliste aus dem fünften Schritt wieder die Anzahl der Knoten des Graphen. Die Prozedur liefert genau dann true, wenn kein positiver Zyklus gefunden wird und ansonsten false. Der Algorithmus folgt dem Bottom-Up-Prinzip: Für von 1 bis N steigende K wird jeweils zwischen allen möglichen Knoten I und J der Pfad mit dem jeweils höchsten Gewicht ermittelt, der nur über Knoten mit Nummern nicht größer als K führt. Das Gewicht eines Pfads ist dabei die Summe seiner Kantengewichte und N die Anzahl der Knoten. Sobald ein Pfad von I nach I mit positivem Gewicht entsteht, also ein positiver Zyklus, wird der Algorithmus abgebrochen und die Prozedur liefert false. % maxPathWeights(+Graph, +NNodes) maxPathWeights(Graph, N) :- maxPathWeights(Graph, N, 1, 1, 1). maxPathWeights(Graph, N, K, I, J) :(K>N, ! ; (I>N -> K1 is K+1, maxPathWeights(Graph, N, K1, 1, 1) ; (J>N -> I1 is I+1, maxPathWeights(Graph, N, K, I1, 1) ; NegInf = [], % negative infinity (append(Head, [mpw(I,J,Aij)|Tail], Graph), ! ; Aij=NegInf, Head=[], Tail=Graph), (append(_, [mpw(I,K,Aik)|_], Graph), ! ; Aik=NegInf), (append(_, [mpw(K,J,Akj)|_], Graph), ! ; Akj=NegInf), ((Aik=NegInf;Akj=NegInf) -> NAij=Aij ; (Aij=NegInf -> NAij is Aik+Akj ; NAij is max(Aij, Aik+Akj))), (NAij=NegInf -> NGraph=Graph ; append(Head, [mpw(I,J,NAij)|Tail], NGraph)), (J\=N, ! ; (append(_, [mpw(I,I,Aii)|_], NGraph) -> Aii=<0 ; true)), J1 is J+1, maxPathWeights(NGraph, N, K, I, J1) ) ) ). 17 Kapitel 4: Test der Implementierung Zum Testen der Implementierung wurde SWI-Prolog 5.6.64 unter Windows XP verwendet. Bei der Wahl der Testfälle wurde versucht, sowohl viele verschiedene Fälle als auch einige interessante Fälle abzudecken. In der folgenden Dokumentation der Testfälle folgt hinter ?- stets die gestellte Anfrage und in den darauf folgenden Zeilen die Antwort von Prolog. 4.1 Test von polyRelations leere Liste: ?- polyRelations([], P). P = []. einfachste Komparanden: ?- polyRelations([0\=1, 5=X, D>=Y], P). P = [0=1, 5=X, D=Y]. Summen: ?- polyRelations([X+Y+Z=A+B, 3+5>7+A], P). P = [X+Y+Z=A+B, 3+5>7+A]. Vorzeichen: ?- polyRelations([-(-X)=< -0, -(+3*4)< +X+(-4)], P). P = [-1* (-1*X)=<0, -1* (3*4)<X+ -4]. Differenzen: ?- polyRelations([0-5=3-X, A-(-B)\=X-4], P). P = [0+ -1*5=3+ -1*X, A+ -1* (-1*B)\=X+ -1*4]. Produkte: ?- polyRelations([3*0*5<X*X*X, Y*4>X*(-X)], P). P = [3*0*5<X*X*X, Y*4>X* (-1*X)]. verschachtelte Operationen: ?- polyRelations([(X+1)*(X-1)\=(4+(N-M)*3)*(X+2*Y)], P). P = [X*X+X* (-1*1)+ (1*X+1* (-1*1))\=4*X+4* (2*Y)+ (N*3*X+N*3* (2*Y)+ (1*M*3*X+ -1*M*3* (2*Y)))]. nicht unterstützte Terme: ?- polyRelations(['Hallo Welt'], P). false. ?- polyRelations([(1+X//Y)*5\=Y^'irgendwas'*X, c=5], P). P = [1*5+X//Y*5\=Y^irgendwas*X, c=5]. Ergebnis: Jede Antwort entspricht der Erwartung. 18 4.2 Test von canRelations leere Liste: ?- polyRelations([], P), canRelations(P, C). P = [], C = []. einfachste Komparanden: ?- polyRelations([0\=1, 5=X, D>=Y], P), canRelations(P, C). P = [0\=1, 5=X, D>=Y], C = [0\=1, 5=X, D>=Y]. Summen: ?- polyRelations([5+0-5=3-X, A-(-B)\=X-4], P), canRelations(P, C). P = [5+0+ -1*5=3+ -1*X, A+ -1* (-1*B)\=X+ -1*4], C = [0=3+ -1*X, B+A\= -4+X]. Vorzeichen: ?- polyRelations([-(-X)=< -0, -(+3*4)< +X+(-4)], P), canRelations(P, C). P = [-1* (-1*X)=<0, -1* (3*4)<X+ -4], C = [X=<0, -12< -4+X]. Differenzen: ?- polyRelations([0-5=3-X, A-(-B)\=X-4], P), canRelations(P, C). P = [0+ -1*5=3+ -1*X, A+ -1* (-1*B)\=X+ -1*4], C = [-5=3+ -1*X, B+A\= -4+X]. Produkte: ?- polyRelations([3*0*5<X*X*X, Y*4>X*(-X)], P), canRelations(P, C). P = [3*0*5<X*X*X, Y*4>X* (-1*X)], C = [0<X* (X*X), 4*Y> -1* (X*X)]. verschachtelte Operationen: ?- polyRelations([(X+1)*(X-1)\=(4+(N-M)*3)*(X+2*Y)], P), canRelations(P, C). P = [X*X+X* (-1*1)+ (1*X+1* (-1*1))\=4*X+4* (2*Y)+ (N*3*X+N*3* (2*Y)+ (1*M*3*X+ -1*M*3* (2*Y)))], C = [-1+X*X\=4*X+ (3* (X*N)+ (-3* (X*M)+ (6* (N*Y)+ (-6* (M*Y)+8*Y))))]. nicht unterstützte Terme: ?- canRelations(['Hallo Welt'], C). false. ?- polyRelations([(1+X//Y)*5\=Y^'irgendwas'*3*X, c*D+D=5], P), canRelations(P, C). P = [1*5+X//Y*5\=Y^irgendwas*3*X, c*D+D=5], C = [5+5* (X//Y)\=3* (X*Y^irgendwas), D+D*c=5]. Ergebnis: Jede Antwort entspricht der Erwartung. 19 4.3 Test von varRelations leere Liste: ?- varRelations([], V, Vars, Const). V = [], Vars = 0, Const = 0. Konstanten: ?- varRelations([3>0, -5=<5], V, Vars, Const). V = [3>0, -5=<5], Vars = 0, Const = 1. mit Konstanten-freien Teilen: ?- varRelations([X=A+B, -5+(A+B)>3+(A+(B+X)), -5*A+3*B\=2+X], V, Va, Co). V = [v(1)=v(2), -5+v(2)>3+v(3), v(4)\=2+v(1)], Va = 4, Co = 0. nicht unterstützte Terme: ?- varRelations([3+X*'Heinz'>=5+X//Y, X*'Heinz'=4], V, Vars, Const). V = [3+v(1)>=5+v(2), v(1)=4], Vars = 2, Const = 1. Ergebnis: Jede Antwort entspricht der Erwartung. 4.4 Test von geRelations Gleichheitsrelationen: ?- geRelations([v(1)=3, 5+v(2)=v(1)], G). G = [v(1)>=3, 3>=v(1), 5+v(2)>=v(1), v(1)>=5+v(2)]. Größergleich- und Kleinergleich-Relationen: ?- geRelations([v(1)>=3, 5+v(2)=<v(1), 5=<3], G). G = [v(1)>=3, v(1)>=5+v(2), 3>=5]. Größer- und Kleiner-Relationen: ?- geRelations([v(1)>3, 5+v(2)<v(1), -1+v(2)<3], G). G = [v(1)>=4, v(1)>=6+v(2), 3>=0+v(2)]. Ungleichheitsrelationen: ?- geRelations([v(1)\=3, 5+v(2)=<v(1), -1\=3], G). G = [v(1)>=4, v(1)>=5+v(2), -1>=4] ; G = [v(1)>=4, v(1)>=5+v(2), 3>=0] ; G = [3>=v(1)+1, v(1)>=5+v(2), -1>=4] ; G = [3>=v(1)+1, v(1)>=5+v(2), 3>=0]. Ergebnis: Jede Antwort entspricht der Erwartung. Bei den Ungleichheitsrelationen werden die zusätzlichen 3 Antworten nach Backtracking ausgegeben. 20 4.5 Test von mpwGraph ohne konstante Komparanden: ?- mpwGraph([v(1)>=v(2), 2+v(2)>=5+v(3), v(3)>=1+v(2)], 3, G). G = [mpw(1, 2, 0), mpw(2, 3, 3), mpw(3, 2, 1)]. mit konstanten Komparanden: ?- mpwGraph([10>=v(1), 2+v(2)>=5, v(3)>=1], 4, G). G = [mpw(4, 1, -10), mpw(2, 4, 3), mpw(3, 4, 1)]. mehrere Ungleichungen zwischen denselben Variablen: ?- mpwGraph([10>=v(1), 2>=5+v(1), v(2)>=v(1), 4+v(2)>=2+v(1)], 3, G). G = [mpw(3, 1, 3), mpw(2, 1, 0)]. Ergebnis: Jede Antwort entspricht der Erwartung. Im letzten Test werden nur die Kanten mit dem höheren Weight-Wert verwendet. 4.6 Test von maxPathWeights alle Kantengewichte 0; kein positiver Zyklus: ?- maxPathWeights([mpw(1,1,0), mpw(1,2,0), mpw(2,2,0), mpw(2,1,0)], 2). true. kein positiver Zyklus: ?- maxPathWeights([mpw(1,1,-10), mpw(1,2,2), mpw(2,3,5), mpw(3,1,-8)],3). true. positiver Zyklus (1 - 1): ?- maxPathWeights([mpw(1,1,10), mpw(1,2,2), mpw(2,3,5), mpw(3,1,-8)], 3). false. positiver Zyklus (1 - 2 - 3 - 1): ?- maxPathWeights([mpw(1,2,2), mpw(2,1,-2), mpw(2,3,5), mpw(3,3,-2), mpw(3,1,-6)], 3). false. Ergebnis: Jede Antwort entspricht der Erwartung. 21 4.7 Test von arith leere Listen (entspricht wahr ⇒ falsch): ?- arith([], []). false. leere Konklusion (true, wenn Hypothese ⇒ falsch, bzw. Hypothese ist widersprüchlich): ?- arith([3=3], []). false. ?- arith([3\=3], []). true. ?- arith([X>2, Y=X, 3>Y], []). true. leere Hypothese (true, wenn wahr ⇒ Konklusion, bzw. Konklusion ?- arith([], [1<5]). true. ?- arith([], [1>=5]). false. mit Hypothese und Konklusion, true: ?- arith([X=3], [X>0]). true. ?- arith([X\=3], [X>3, 3>X]). true. ?- arith([1<A, A<B+C, B+C<D, D<6, D\=5], [3=B+C]). true. ?- arith([1<A, A<B+C, B+C<D, D<4], [-1>1]). true. ?- arith([(N+1)*(N-1)=X, Y=(N+1)*(1+N)-N*2], [X<Y]). true. mit Hypothese und Konklusion, false: ?- arith([X>0], [X=3]). false. ?- arith([(N+1)*(N-1)=X, Y+2=(N+1)*(1+N)-N*2], [X<Y]). false. false, obwohl herleitbar in Standardarithmetik: ?- arith([X=2, Y=3], [X+Y=5]). false. ?- arith([X=2, Y=3], [X=5-Y]). false. ist gültig): Ergebnis: Jede Antwort entspricht der Erwartung. Die letzten beiden Testfälle demonstrieren die Schwäche der Induktionsfreien Arithmetik: Nur ein vollständiger konstantenfreier Teil eines Komparanden kann substituiert werden. Beim letzten Testfall werden Y und -Y bzw. (-1)*Y durch unterschiedliche Variablen substituiert. 22 4.8 Test der Laufzeit von Arith Die Antwort für jeden der obigen Testfälle kam sofort, was bei der geringen Komplexität der Testfälle nicht verwunderlich ist. Um eine merkliche Verzögerung zu erzielen, wird folgende Anfrage verwendet, die die Auffächerung des Suchraums bei vielen Ungleichheitsrelationen ausnutzt: ?- arith([X\=1, X\=2, X\=3, X\=4, X\=5, X\=6, X\=7, X\=8, X\=9, X\=10, X\=11, X\=12, X\=13, X= -1], [X<0]). true. Bei dieser Anfrage gibt es auf dem verwendeten Testsystem (3 GHz Doppelkernprozessor) bis zur Antwort eine ungefähre Verzögerung von 0,5 Sekunden. Untersucht man die Antwortzeit bei verlängerten Versionen dieser Anfrage, so stellt man eine exponentielle Abhängigkeit der Laufzeit von der Anzahl der Ungleichheitsrelationen fest. ca. 1 Sekunde ca. 2 Sekunden ca. 4 Sekunden ca. 8 Sekunden ca. 16 Sekunden Diese exponentielle Abhängigkeit zur Basis 2 ist aufgrund der Implementierung offensichtlich, da im schlimmsten Fall die Kombination von je 2 Alternativen für jede Ungleichheitsrelation untersucht wird. arith([X\=1, arith([X\=1, arith([X\=1, arith([X\=1, arith([X\=1, … … … … … , , , , , X\=14, X\=15, X\=16, X\=17, X\=18, X= X= X= X= X= -1], -1], -1], -1], -1], [X<0]).: [X<0]).: [X<0]).: [X<0]).: [X<0]).: 23 Kapitel 5: Ausblick Es ist viel Potenzial zum Optimieren von Arith vorhanden. Die Schritte 1 bis 3 verfolgen alle dasselbe Prinzip und nehmen jede Relation auseinander, nur um sie am Ende wieder zusammenzusetzen. Sie können so zusammengezogen werden, so dass die Relationen nur einmal zerteilt werden und zum Schluss gleich zu den Größergleich-Relationen von Schritt 4 zusammengefügt werden. Schritt 6 könnte kantenbasiert vorgehen und nur die Kanten betrachten, die auch tatsächlich existieren. Nach der Implementierung von Arith ist der nächste logische Schritt die Integration in LeanCop. Dazu muss das Zusammenspiel von Arithmetik mit dem Konnektionskalkül untersucht werden, um Situationen zu finden, in denen Arith hilfreich ist, ebenso wie die entsprechenden Punkte in der Implementierung, an denen Arith aufgerufen werden kann. Zum Einen können arithmetische Terme in Argumenten von Prädikaten vorkommen, die unifiziert werden müssen (Beispiel: P(3) ∧ (∀X(P(2X + 1) ⇒ Q(3X + 3))) ⇒ Q(6)), zum Anderen können die arithmetischen Relationen selbst als Prädikate vorkommen (Beispiel: ∀X∀Y(X = 2 ∧ Y = 3 ⇒ X + Y = 5). Um Arithmetik in TPTP-Formeln zu unterstützen, muss die Übersetzung in das eigene LeanCop-Format über die Prädikatenebene hinaus fortgeführt werden, da arithmetische Terme in Argumenten von Prädikaten oder, im Falle der Relationen, als Prädikate vorkommen können. In letzterem Fall besteht ein Konflikt zur in leanCop bereits implementierten Behandlung von Gleichheit, die Ungleichheiten A!=B im TPTP-Format nicht zu A\=B übersetzt, sondern zu ~(A=B). Dieser Konflikt könnte aufgelöst werden, indem die Übersetzung berücksichtigt, ob A und B arithmetische Terme sind. Arith muss vervollständigt werden, indem eine Taktik für die Anwendung der nichttrivialen Monotonieaxiome vor dem Aufruf der implementierten Entscheidungsprozedur Arith entwickelt wird, damit auch Formeln wie die letzten Testfälle von arith korrekt geprüft werden können. Weitere Axiome können zusätzliche Arten von arithmetischen Termen umformen, wie Division und Potenzen mit Konstanten. Weitere Entscheidungsprozeduren und andere Verfahren zur Behandlung anderer Teilmengen der Arithmetik können implementiert werden und dabei miteinander kooperieren. Die Verfahren können gegebenenfalls modifiziert werden, um einen Beweis zu generieren. 24 Quellen: 1. www.leancop.de, 10.12.2009 2. Jens Otten: leanCoP 2.0 and ileanCoP 1.2: High Performance Lean Theorem Proving in Classical and Intuitionistic Logic. Potsdam, 2008 3. http://planetmath.org/encyclopedia/PeanoArithmetic.html, 10.12.2009 4. http://planetmath.org/encyclopedia/PressburgerArithmetic.html, 10.12.2009 5. R. L. Constable, M. J. O'Donnell: A Programming Logic. Winthrop, Cambridge, Massachusetts, 1978 6. T. Chan: An algorithm for checking PL/CV arithmetic inferences. Technical Report TR77-326, Cornell University, Ithaca, NY, USA 1982. 7. C. Kreitz: Skriptum zur Vorlesung Automatisierte Logik und Programmierung, Kapitel 5. http://www.cs.uni-potsdam.de/ti/lehre/03-ALuP-I/skript.htm, 10.12.2009 8. D. C. Cooper: Theorem proving in arithmetic without multiplication. Machine Intelligence 7, pp. 91-99, 1972 9. Louis Hodes: Solving problems by formula manipulation in logic and linear inequalities. Proceedings of the 2nd international joint conference on Artificial intelligence, pp. 553– 559, London, England, 1971 10. Robert E. Shostak: On the SUP-INF Method for Proving Presburger Formulas. Journal of the ACM (JACM), Volume 24, Issue 4, pp. 529–543, ACM, New York, NY, USA, 1977 11. Alexander Schrijver: Theory of linear and integer programming. John Wiley & Sons, Inc., New York, NY, USA, 1986 12. William Pugh: The Omega test: a fast and practical integer programming algorithm for dependence analysis. Proceedings of the 1991 ACM/IEEE conference on Supercomputing, pp. 4–13, ACM, New York, NY, USA, 1991 13. Robert Shostak: Deciding Linear Inequalities by Computing Loop Residues. Journal of the ACM (JACM), Volume 28, Issue 4, ACM, New York, NY, USA, 1981 14. Bernard Boigelot, Sébastien Jodogne, Pierre Wolper: An effective decision procedure for linear arithmetic over the integers and reals. ACM Transactions on Computational Logic (TOCL), Volume 6, Issue 3, pp. 614–633, ACM, New York, NY, USA, 2005 15. Yegor Bryukhov, Alexei Kopylov, Vladimir Krupski, Aleksey Nogin: Implementing and Automating Basic Number Theory In MetaPRL Proof Assistant. 2003 25 Anhang: Quelltext der Implementierung %% %% %% %% %% %% %% %% %% %% %% %% %% %% %% %% %% %% File: arith.pl - Date: 06 December 2009 Purpose: Check whether a conclusion can be deduced from a hypothesis in induction-free arithmetic The hypothesis is a conjunction and the conclusion a disjunction of arithmetic relations. Author: Holger Trölenberg Usage: arith(+H, +C). % % % % where H and C are lists of relations (=, \=, <, >, =<, >=) whose comparands can contain integers, variables, and the operations +, -, *, unary +, and unary - Example: arith([(N+1)*(N-1)=X], [X<N*N]). true. arith([A=2, B=3], [A+B=5]). false. arith(Hypothesis, [Pos|Conclusion]) :!, negRelation(Pos, Neg), arith([Neg|Hypothesis], Conclusion). arith(Relations, []) :\+(( polyRelations(Relations, PolyRelations), canRelations(PolyRelations, CanRelations), varRelations(CanRelations, VarRelations, Vars, Const), geRelations(VarRelations, GERelations), N is Vars+Const, mpwGraph(GERelations, N, Graph), maxPathWeights(Graph, N) )). % negRelation(+Positive, -Negative) % negates a relation by substituting the operand negRelation(A=B, A\=B). negRelation(A\=B, A=B). negRelation(A<B, A>=B). negRelation(A>B, A=<B). negRelation(A=<B, A>B). negRelation(A>=B, A<B). % % % % ------------------------------------------------------------------polyRelations(+Relations, -PolynomialRelations) converts both comparands of each relation in the list into polynomials polyRelations([], []). polyRelations([Rel|Relations], [Poly|PolyRelations]) :Rel=..[Op, RelA, RelB], polyComparand(RelA, PolyA), polyComparand(RelB, PolyB), Poly=..[Op, PolyA, PolyB], polyRelations(Relations, PolyRelations). % polyComparand(+Term, -Polynomial) % converts one comparand into a polynomial polyComparand(Var, Var) :- var(Var), !. polyComparand(A*B, P) :- !, polyComparand(A, P1), 26 polyComparand(B, P2), polyLeftMult(P1, P2, P). polyComparand(A+B, P1+P2) :- !, polyComparand(A, P1), polyComparand(B, P2). polyComparand(A-B, P) :- !, polyComparand(A+(-B), P). polyComparand(-A, P) :- !, polyComparand((-1)*A, P). polyComparand(+A, P) :- !, polyComparand(A, P). polyComparand(Atom, Atom). % polyLeftMult(+P1, +P2, -P) % multiplies 2 polynomials polyLeftMult(Var, R, P) :- var(Var), !, polyRightMult(Var, R, P). polyLeftMult(A+B, R, P1+P2) :- !, polyLeftMult(A, R, P1), polyLeftMult(B, R, P2). polyLeftMult(Monomial, R, P) :- polyRightMult(Monomial, R, P). % polyRightMult(+Factor, +P2, -P) % multiplies a polynomial with a factor polyRightMult(L, Var, L*Var) :- var(Var), !. polyRightMult(L, A+B, P1+P2) :- !, polyRightMult(L, A, P1), polyRightMult(L, B, P2). polyRightMult(L, Monomial, L*Monomial). % % % % ------------------------------------------------------------------canRelations(+PolynomialRelations, -CanonicalRelations) converts both comparands of each relation in the list from polynomial into canonical form canRelations([], []). canRelations([Poly|PolyRelations], [Can|CanRelations]) :Poly=..[Op, PolyA, PolyB], canComparand(PolyA, CanA), canComparand(PolyB, CanB), Can=..[Op, CanA, CanB], canRelations(PolyRelations, CanRelations). % canComparand(+Polynomial, -CanonicalComparand) % converts one polynomial comparand into canonical form canComparand(Poly, Can) :canPoly2List(Poly, PL), msort(PL, SPL), canList2Can(SPL, Can2), (Can2==[] -> Can=0 ; Can=Can2). % canPoly2List(+Polynomial, -Monomials) % converts a polynomial term into a list of encoded monomials canPoly2List(Var, [monomial([Var], 1)]) :- var(Var), !. canPoly2List(A+B, P) :!, canPoly2List(A, PA), canPoly2List(B, PB), append(PA, PB, P). canPoly2List(Monomial, [monomial(CM, C)]) :canMonEnc(Monomial, M, C), msort(M, CM). % canMonEnc(+Monomial, -Factors, -Coefficient) % splits a monomial term into the coefficient and the other factors canMonEnc(Var, [Var], 1) :- var(Var), !. canMonEnc(A*B, M, C) :!, canMonEnc(A, MA, CA), canMonEnc(B, MB, CB), append(MA, MB, M), C is CA*CB. canMonEnc(N, [], N) :- number(N), !. canMonEnc(Factor, [Factor], 1). 27 % canList2Can(+Monomials, -CanonicalTerm) % converts a sorted list of encoded monomials into a canonical term canList2Can([], []). canList2Can([monomial(M1, C1), monomial(M2, C2)|SPL], Can) :M1==M2, !, C is C1+C2, canList2Can([monomial(M1, C)|SPL], Can). canList2Can([monomial(_, 0)|SPL], Can) :- !, canList2Can(SPL, Can). canList2Can([monomial(M, C)|SPL], Can) :- canMonDec(M, C, CM), canList2Can(SPL, Can2), (Can2==[] -> Can=CM ; Can=CM+Can2). % canMonDec(+Factors, +Coefficient, -Monomial) % combines a coefficient and a sorted list of factors into a % canonical Monomial canMonDec(_, 0, 0) :- !. canMonDec([], C, C). canMonDec([F, F2|M], 1, F*CM) :- !, canMonDec([F2|M], 1, CM). canMonDec([F], 1, F) :- !. canMonDec([F|M], C, C*CM) :- canMonDec([F|M], 1, CM). % % % % % % % ------------------------------------------------------------------varRelations(+CanonicalRelations, -VarReplacedRelations, -NVariables, -Constants) replaces the constant-free parts of the canonical comparands of the relations in the list with variables, returns the number of introduced variables, and sets Constants to 1, if there are any constant comparands, and to 0 otherwise varRelations(CanRelations, VarRelations, Vars, Const) :varRelations(CanRelations, VarRelations, [], Vars, Const). varRelations([], [], [], 0, 0) :- !. varRelations([], [], [subst(_, Vars)|_], Vars, 0). varRelations([Can|CanRelations], [Var|VarRelations], Substitutions, Vars, Const) :Can=..[Op, CanA, CanB], varSubst(CanA, VarA, Substitutions, SubsA, CA), varSubst(CanB, VarB, SubsA, SubsB, CB), Var=..[Op, VarA, VarB], varRelations(CanRelations, VarRelations, SubsB, Vars, C), Const is C\/CA\/CB. % % % % % % % varSubst(+CanonicalComparand, -VarReplacedTerm, +SubstitutionsIn, -SubstitutionsOut, -Constant) replaces the constant-free part of one comparand with a new or previously used variable, depending on the supplied list of substitutions, updates the substitution list accordingly, and sets Constant to 1, if the comparand is only a constant, and to 0 otherwise varSubst(N, N, S, S, 1) :- number(N), !. varSubst(N+C, N+V, S, S2, 0) :- number(N), !, varSubst(C, V, S, S2, _). varSubst(C, v(V), S, S, 0) :- member(subst(D, V), S), C==D, !. varSubst(C, v(V), S, [subst(C, V)|S], 0) :- length([_|S], V). % % % % % ------------------------------------------------------------------geRelations(+VarReplacedRelations, -GreaterEqualRelations) converts each relation with replaced constant-free parts in the list into one or two equivalent greater-than-or-equal-relations; each inequality (\=) is first converted like the greater-than- 28 % relation (>) and then, after backtracking, like the lower-than% relation (<) geRelations([], []). geRelations([Var|VarRelations], GERelations) :geRel(Var, GERel), geRelations(VarRelations, GERels), append(GERel, GERels, GERelations). % geRel(+Relation, -GreaterEqualRelations) % converts one relation into a list of equivalent relations geRel(X\=Y, R) :- geRel(X>Y, R). geRel(X\=Y, R) :- geRel(Y>X, R). geRel(X=Y, [X>=Y, Y>=X]). geRel(X>=Y, [X>=Y]). geRel(X=<Y, [Y>=X]). geRel(X<Y, R) :- geRel(Y>X, R). geRel(X>Y, [X>=Z]) :(number(Y) -> Z is Y+1; Y=v(_) -> Z=Y+1; Y=v(V)+A -> B is A+1, Z=v(V)+B; Y=A+v(V), B is A+1, Z=B+v(V)). % % % % geRelations(+Relations, -GreaterEqualRelations, -Inequalities) a different version that separates the inequalities from the other relations and leaves them unconverted this predicate is unused but intended for future use geRelations([],[],[]). geRelations([X\=Y|VarRelations], GERelations, [X\=Y|INRelations]) :!, geRelations(VarRelations, GERelations, INRelations). geRelations([Var|VarRelations], GERelations, INRelations) :geRel(Var, GERel), geRelations(VarRelations, GERels, INRelations), append(GERel, GERels, GERelations). % % % % ------------------------------------------------------------------mpwGraph(+GreaterEqualRelations, +NNodes, -Graph) constructs a graph with NNodes nodes from the list of greater-orequal-relations for the MaxPathWeights algorithm mpwGraph([], _, []). mpwGraph([X>=Y|GERelations], N, Graph) :(number(X) -> Origin=N, LWeight=X ; (X=v(Origin) -> LWeight=0 ; (X=v(Origin)+LWeight ; X=LWeight+v(Origin)), !)), (number(Y) -> Target=N, RWeight=Y ; (Y=v(Target) -> RWeight=0 ; (Y=v(Target)+RWeight ; Y=RWeight+v(Target)), !)), Weight is RWeight-LWeight, mpwGraph(GERelations, N, G), (append(Head, [mpw(Origin, Target, W)|Tail], G) -> (W<Weight -> append(Head, [mpw(Origin, Target, Weight)|Tail], Graph) ; Graph=G) ; Graph=[mpw(Origin, Target, Weight)|G]). % ------------------------------------------------------------------% maxPathWeights(+Graph, +NNodes) % searches for a positive cycle in the graph with NNodes nodes and 29 % succeeds, iff there is no positive cycle % I, J, and K are variables used for the for-loop maxPathWeights(Graph, N) :- maxPathWeights(Graph, N, 1, 1, 1). maxPathWeights(Graph, N, K, I, J) :(K>N, ! ; (I>N -> K1 is K+1, maxPathWeights(Graph, N, K1, 1, 1) ; (J>N -> I1 is I+1, maxPathWeights(Graph, N, K, I1, 1) ; NegInf = [], % negative infinity (append(Head, [mpw(I,J,Aij)|Tail], Graph), ! ; Aij=NegInf, Head=[], Tail=Graph), (append(_, [mpw(I,K,Aik)|_], Graph), ! ; Aik=NegInf), (append(_, [mpw(K,J,Akj)|_], Graph), ! ; Akj=NegInf), ((Aik=NegInf;Akj=NegInf) -> NAij=Aij ; (Aij=NegInf -> NAij is Aik+Akj ; NAij is max(Aij, Aik+Akj))), (NAij=NegInf -> NGraph=Graph ; append(Head, [mpw(I,J,NAij)|Tail], NGraph)), (J\=N, ! ; (append(_, [mpw(I,I,Aii)|_], NGraph) -> Aii=<0 ; true)), J1 is J+1, maxPathWeights(NGraph, N, K, I, J1) ) ) ). 30