9

I know only one prover that translates the algorithm that Quine gave for classical propositional logic in his book Methods of Logic (Harvard University Press, 1982, ch. 1 sec. 5, pp. 33-40), this prover is in Haskell and it is here: Quine's algorithm in Haskell

I tried to translate Quine's algorithm in Prolog, but until now I have not succeeded. It is a pity because it is an efficient algorithm and a Prolog translation would be interesting. I am going to describe this algorithm. The only Prolog code that I give at the start is the list of operators that would be useful to test the prover:

% operator definitions (TPTP syntax)

:- op( 500, fy, ~).      % negation
:- op(1000, xfy, &).     % conjunction
:- op(1100, xfy, '|').   % disjunction
:- op(1110, xfy, =>).    % conditional
:- op(1120, xfy, <=>).   % biconditional

Truth constants are top and bot for, respectively, true and false. The algorithm starts as follows: For any propositional formula F, make two copies of it and replace the atom which has the highest occurrence in F by top in the first copy, and by bot in the second copy, and then apply the following ten reduction rules one rule at a time for as many times as possible, for each of the copies:

 1.  p & bot  --> bot
 2.  p & top  --> p
 3.  p | bot  --> p
 4.  p | top  --> top
 5.  p => bot --> ~p 
 6.  p => top --> top
 7.  bot => p --> top
 8.  top => p -->  p
 9.  p <=> bot --> ~p
 10. p <=> top --> p

Of course, we have also the following rules for negation and double negation:

 1. ~bot --> top
 2. ~top --> bot
 3. ~~p  --> p 

When there is neither top nor bot in the formula so none of the rules apply, split it again and pick one atom to replace it by top and by bot in yet another two sided table. The formula F is proved if and only if the algorithm ends with top in all copies, and fails to be proved, otherwise.

Example:

                         (p => q) <=> (~q => ~p)

 (p => top) <=> (bot => ~p)                 (p => bot) <=> (top => ~p)

       top  <=>  top                              ~p   <=>  ~p  

            top                       top <=> top                bot <=> bot

                                          top                        top

It is clear that Quine's algorithm is an optimization of the truth tables method, but starting from codes of program of truth tables generator, I did not succeed to get it in Prolog code.

A help at least to start would be welcome. In advance, many thanks.


EDIT by Guy Coder

This is double posted at SWI-Prolog forum which has a lively discussion and where provers Prolog are published but not reproduced in this page.

  • What have you tried so far and where did you encounter difficulty? For example, have you tried writing out all of the rules as Prolog clauses, like `mostCommonVar(Formula, NumberOfOccurrences, PropositionalVar)`, or `reduce(P & bot, Reduced) :- Reduced = reduce(P). reduce(P & top, Reduced) :- …`? – Jon Purdy Aug 20 '20 at 17:32
  • Thank you Jon! I did not know `mostCommonVar/3` I will try it. I have tried reduce, but I missed a good starting point. I confess that I was lost. – Joseph Vidal-Rosset Aug 20 '20 at 18:23
  • Convince me why Quine's algorithm is important and I will toss in another 200 points for a bounty to help. What makes Quine's algorithm different from others and where can it be used effectively. – Guy Coder Aug 26 '20 at 11:08
  • When learning digital logic design in EE [Karnaugh mapping](https://en.wikipedia.org/wiki/Karnaugh_map) was awesome. – Guy Coder Aug 26 '20 at 11:11
  • Found [In Pursuit of an Efficient SAT Encoding for the Hamiltonian Cycle Problem](https://modref.github.io/papers/ModRef2019_In%20Pursuit%20of%20an%20Efficient%20SAT%20Encoding%20for%20the%20Hamiltonian%20Cycle%20Problem.pdf) – Guy Coder Aug 26 '20 at 11:13
  • Of interest: [Logic optimization](https://en.wikipedia.org/wiki/Logic_optimization) – Guy Coder Aug 26 '20 at 11:34
  • Of interest: `the Quine–McCluskey method is practical only for functions with a limited number of input variables and output functions.` [ref](https://en.wikipedia.org/wiki/Espresso_heuristic_logic_minimizer#Classical_minimization_methods) – Guy Coder Aug 26 '20 at 12:58
  • 1
    @user:1243762 (Guy Coder) who wrote "Convince me why Quine's algorithm is important" Quine's algorithm is much more simple to understand than Quine-McCluskey method and it is an efficient semantic proof search that is an optimization of truth-tables. Take for example the following equivalence: ((a | b) => c) <=> ((a => c) & (b => c)). Replacing c by T, the reduction is straightforward: T <=> (T & T). Replacing c by F you get and instance of De Morgan Law: ~(a | b) <=> (~ a & ~ b). Compare with the truth-table method... Best wishes. Jo. – Joseph Vidal-Rosset Aug 26 '20 at 16:19
  • Don't you want to map `p | top` to `top`? – dfeuer Sep 01 '20 at 16:40
  • @user:1477667 (dfeuer) I do not understand your question. Can you explain what you mean exactly? – Joseph Vidal-Rosset Sep 02 '20 at 07:11
  • 1
    `p | top` should be `top`, just like `p & bot` is `bot`. You got that rule wrong. Why do you give user numbers instead of user names? – dfeuer Feb 14 '21 at 01:00

3 Answers3

5

The Haskell code seemed complicated to me. Here's an implementation based on the description of the algorithm given in the question. (Using maplist and dif from the SWI-Prolog library, but easy to make self-contained.)

First, single simplification steps:

formula_simpler(_P & bot,   bot).
formula_simpler(P & top,    P).
formula_simpler(P '|' bot,  P).
formula_simpler(_P '|' top, top).  % not P as in the question
formula_simpler(P => bot,   ~P).
formula_simpler(_P => top,  top).
formula_simpler(bot => _P,  top).
formula_simpler(top => P,   P).
formula_simpler(P <=> bot,  ~P).
formula_simpler(P <=> top,  P).
formula_simpler(~bot,       top).
formula_simpler(~top,       bot).
formula_simpler(~(~P),      P).

Then, iterated application of these steps to subterms and iteration at the root until nothing changes anymore:

formula_simple(Formula, Simple) :-
    Formula =.. [Operator | Args],
    maplist(formula_simple, Args, SimpleArgs),
    SimplerFormula =.. [Operator | SimpleArgs],
    (   formula_simpler(SimplerFormula, EvenSimplerFormula)
    ->  formula_simple(EvenSimplerFormula, Simple)
    ;   Simple = SimplerFormula ).

For example:

?- formula_simple(~ ~ ~ ~ ~ a, Simple).
Simple = ~a.

For the replacement of variables by other values, first a predicate for finding variables in formulas:

formula_variable(Variable, Variable) :-
    atom(Variable),
    dif(Variable, top),
    dif(Variable, bot).
formula_variable(Formula, Variable) :-
    Formula =.. [_Operator | Args],
    member(Arg, Args),
    formula_variable(Arg, Variable).

On backtracking this will enumerate all occurrences of variables in a formula, for example:

?- formula_variable((p => q) <=> (~q => ~p), Var).
Var = p ;
Var = q ;
Var = q ;
Var = p ;
false.

This is the only source of nondeterminism in the proof procedure below, and you can insert a cut after the formula_variable call to commit to a single choice.

Now the actual replacement of a Variable in a Formula by Replacement:

variable_replacement_formula_replaced(Variable, Replacement, Variable, Replacement).
variable_replacement_formula_replaced(Variable, _Replacement, Formula, Formula) :-
    atom(Formula),
    dif(Formula, Variable).
variable_replacement_formula_replaced(Variable, Replacement, Formula, Replaced) :-
    Formula =.. [Operator | Args],
    Args = [_ | _],
    maplist(variable_replacement_formula_replaced(Variable, Replacement), Args, ReplacedArgs),
    Replaced =.. [Operator | ReplacedArgs].

And finally the prover, constructing a proof term like the Haskell version:

formula_proof(Formula, trivial(Formula)) :-
    formula_simple(Formula, top).
formula_proof(Formula, split(Formula, Variable, TopProof, BotProof)) :-
    formula_simple(Formula, SimpleFormula),
    formula_variable(SimpleFormula, Variable),
    variable_replacement_formula_replaced(Variable, top, Formula, TopFormula),
    variable_replacement_formula_replaced(Variable, bot, Formula, BotFormula),
    formula_proof(TopFormula, TopProof),
    formula_proof(BotFormula, BotProof).

A proof of the example from the question:

?- formula_proof((p => q) <=> (~q => ~p), Proof).
Proof = split((p=>q<=> ~q=> ~p),
              p,
              split((top=>q<=> ~q=> ~top),
                    q,
                    trivial((top=>top<=> ~top=> ~top)),
                    trivial((top=>bot<=> ~bot=> ~top))),
              trivial((bot=>q<=> ~q=> ~bot))) .

All of its proofs:

?- formula_proof((p => q) <=> (~q => ~p), Proof).
Proof = split((p=>q<=> ~q=> ~p), p, split((top=>q<=> ~q=> ~top), q, trivial((top=>top<=> ~top=> ~top)), trivial((top=>bot<=> ~bot=> ~top))), trivial((bot=>q<=> ~q=> ~bot))) ;
Proof = split((p=>q<=> ~q=> ~p), p, split((top=>q<=> ~q=> ~top), q, trivial((top=>top<=> ~top=> ~top)), trivial((top=>bot<=> ~bot=> ~top))), trivial((bot=>q<=> ~q=> ~bot))) ;
Proof = split((p=>q<=> ~q=> ~p), q, trivial((p=>top<=> ~top=> ~p)), split((p=>bot<=> ~bot=> ~p), p, trivial((top=>bot<=> ~bot=> ~top)), trivial((bot=>bot<=> ~bot=> ~bot)))) ;
Proof = split((p=>q<=> ~q=> ~p), q, trivial((p=>top<=> ~top=> ~p)), split((p=>bot<=> ~bot=> ~p), p, trivial((top=>bot<=> ~bot=> ~top)), trivial((bot=>bot<=> ~bot=> ~bot)))) ;
Proof = split((p=>q<=> ~q=> ~p), q, trivial((p=>top<=> ~top=> ~p)), split((p=>bot<=> ~bot=> ~p), p, trivial((top=>bot<=> ~bot=> ~top)), trivial((bot=>bot<=> ~bot=> ~bot)))) ;
Proof = split((p=>q<=> ~q=> ~p), q, trivial((p=>top<=> ~top=> ~p)), split((p=>bot<=> ~bot=> ~p), p, trivial((top=>bot<=> ~bot=> ~top)), trivial((bot=>bot<=> ~bot=> ~bot)))) ;
Proof = split((p=>q<=> ~q=> ~p), p, split((top=>q<=> ~q=> ~top), q, trivial((top=>top<=> ~top=> ~top)), trivial((top=>bot<=> ~bot=> ~top))), trivial((bot=>q<=> ~q=> ~bot))) ;
Proof = split((p=>q<=> ~q=> ~p), p, split((top=>q<=> ~q=> ~top), q, trivial((top=>top<=> ~top=> ~top)), trivial((top=>bot<=> ~bot=> ~top))), trivial((bot=>q<=> ~q=> ~bot))) ;
false.

This contains lots of redundancy. Again, this is because formula_variable enumerates occurrences of variables. It can be made more deterministic in various ways depending on one's requirements.

EDIT: The above implementation of formula_simple is naive and inefficient: Every time it makes a successful simplification at the root of the formula, it revisits all of the subformulas as well. But on this problem, no new simplifications of the subformulas will become possible when the root is simplified. Here is a new version that is more careful to first fully rewrite the subformulas, and then only iterate rewrites at the root:

formula_simple2(Formula, Simple) :-
    Formula =.. [Operator | Args],
    maplist(formula_simple2, Args, SimpleArgs),
    SimplerFormula =.. [Operator | SimpleArgs],
    formula_rootsimple(SimplerFormula, Simple).

formula_rootsimple(Formula, Simple) :-
    (   formula_simpler(Formula, Simpler)
    ->  formula_rootsimple(Simpler, Simple)
    ;   Simple = Formula ).

This is considerably faster:

?- time(formula_simple(~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~(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), Simple)).
% 11,388 inferences, 0.004 CPU in 0.004 seconds (100% CPU, 2676814 Lips)
Simple = ~ (a&b&c&d&e&f&g&h& ... & ...).

?- time(formula_simple2(~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~(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), Simple)).
% 988 inferences, 0.000 CPU in 0.000 seconds (100% CPU, 2274642 Lips)
Simple = ~ (a&b&c&d&e&f&g&h& ... & ...).

Edit: As pointed out in the comments, the prover as written above can be veeery slow on slightly bigger formulas. The problem is that I forgot that some operators are commutative! Thanks jnmonette for pointing this out. The rewrite rules must be expanded a bit:

formula_simpler(_P & bot,   bot).
formula_simpler(bot & _P,   bot).
formula_simpler(P & top,    P).
formula_simpler(top & P,    P).
formula_simpler(P '|' bot,  P).
formula_simpler(bot '|' P,  P).
...

And with this the prover behaves nicely.

Isabelle Newbie
  • 9,258
  • 1
  • 20
  • 32
  • @user:4391743 (Isabelle Newbie); thanks for your helpful reply and for this clear program that is helpful for me. I just tested it and it seems to me correct. It works well for well-known tautologies, if I am not mistaken. However, the program fails to solve problems that are big formulas (valid or invalid), maybe because of redundancies? – Joseph Vidal-Rosset Aug 27 '20 at 07:40
  • 1
    Possible. It should definitely go faster if you put a cut after the `formula_variable` call in `formula_proof`. I haven't benchmarked it. Also, I should say that all the univ (`=..`) calls are not cheap, they create large amounts of intermediate data structures. Since we know that formulas can only have a handful of fixed forms, everything could be written out more explicitly. For example, `formula_variable(A & _B, V) :- formula_variable(A, V). formula_variable(_A & B, V) :- formula_variable(B, V).` etc. – Isabelle Newbie Aug 27 '20 at 07:59
  • I've edited my answer with a faster implementation of `formula_simple`, see if calling the new version speeds up your tests. – Isabelle Newbie Aug 27 '20 at 08:29
  • Yes, I just saw your edit and I just worked on your program. I understand that I have to replace all occurrences of ` formula_simple ` by ` formula_simple2 ` . That is the only change that I have to do, right? – Joseph Vidal-Rosset Aug 27 '20 at 08:41
  • Yes, that should be it. – Isabelle Newbie Aug 27 '20 at 08:54
  • I put a cut after the ``` formula_variable ``` i.e. ``` , ! , and I used the ``` formula_simple2 ``` instead of ``` formula_simple ``` . But nevertheless, your program fails to provide an output for the same big (valid) formula : ``` | | | | | | | | | | | | | | | | ERROR: Out of global-stack. ERROR: No room for exception term. Aborting. % Execution Aborted ``` – Joseph Vidal-Rosset Aug 27 '20 at 08:56
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/220545/discussion-between-user1121356-and-isabelle-newbie). – Joseph Vidal-Rosset Aug 27 '20 at 09:06
  • 3
    I think you need to add some clauses to your formula_simpler/2 predicate: In addition to ```formula_simpler(_P & bot, bot).```, you should also have ```formula_simpler(bot & _P, bot).``` and the same for all clauses that use a commutative operator... The inefficiency on the large formula might come from there... – jnmonette Aug 27 '20 at 18:14
  • 1
    I like that your predicate returns a complete proof, by the way! – jnmonette Aug 27 '20 at 18:19
  • 1
    *facepalm* Thanks @jnmonette, adding the commutative rewrite rules makes my original prover terminate in 0.2 seconds on the big formula from the chat, without any optimizations or cuts. I feel very silly now :-) I'll edit my answer. – Isabelle Newbie Aug 27 '20 at 20:22
  • `iteration at the root until nothing changes anymore` Is that not a fixed point? – Guy Coder Aug 28 '20 at 11:48
  • If you want to use math terminology that not everybody might be familiar with: Yes, it is a fixed point. – Isabelle Newbie Aug 28 '20 at 12:35
  • @user:4391743 (Isabelle Newbie) , jnmonette , Guy Coder and Mostowski Collapse, many thanks for your replies and these programs that reply to my question. It is a very precious help and I am very thankful. (I know that comment should not be thanks, but where can I say it?). – Joseph Vidal-Rosset Aug 28 '20 at 13:24
  • @user1121356 I just rejected your proposed edit of my answer, not because of the content but because it would not be clear to readers that it came from you (or anyone else but me). Please post it as a separate answer! – Isabelle Newbie Aug 31 '20 at 19:19
  • @user:4391743 (Isabelle Newbie) really no problem ! Sorry for this confusion. It was not my intention to create it. – Joseph Vidal-Rosset Sep 01 '20 at 08:35
3

Here is a skeleton of solution. I hope it can help you fill the holes.

is_valid(Formula) :-
    \+ derive(Formula,bot).

is_satisfiable(Formula) :-
    derive(Formula, top).

derive(bot,D):-
    !,
    D=bot.
derive(top,D):-
    !,
    D=top.
derive(Formula,D):-
    reduce(Formula, Formula1),
    (
      Formula=Formula1
    ->
      branch(Formula1,D)
    ;
      derive(Formula1,D)
    ).

Now you need to implement reduce/2 that applies the reduction rules (recursively in the sub-formulas), and branch/2 that replaces non-deterministically an atom of the formula with either top or bot, then calls recursively derive/2. Something like:

branch(Formula, D):-
    pickAtom(Formula, Atom),
    (
      Rep=top
    ; 
      Rep=bot
    ),
    replace(Formula, Atom, Rep, Formula1),
    derive(Formula1,D).
jnmonette
  • 1,794
  • 4
  • 7
  • Many thanks for this skeleton of solution, jnmoette !. I will try to go ahead with this help, and I hope to be able soon to publish the result in this page. That is very kind of you to have taken time to help me. – Joseph Vidal-Rosset Aug 20 '20 at 18:27
  • I am afraid to fail to publish this prover in Prolog. My main difficulty is that I am unable to define recursively in the subformulas the reduction rules for propositional formulas. I am not competent enough to succeed, not because Quine algorithm is difficult to implement in Prolog, but because I am not competent enough in Prolog. It is very time consuming for me and all my tries are unsuccessful. :( – Joseph Vidal-Rosset Aug 25 '20 at 14:50
  • @user:1243762 Thanks. I did not know the existence of this Prolog forum. I posted my question one minute ago, thanks to you : https://swi-prolog.discourse.group/t/https-stackoverflow-com-questions-63505466-prolog-implementation-of-quines-algorithm-for-classical-propositional-logic-in/2830 – Joseph Vidal-Rosset Aug 26 '20 at 15:58
3

Warning! We dont use TPTP syntax, but syntax from SWI-Prolog CLP(B). Seems that this brute force method is older (*), and as Prolog code its so small, it even fits into the pocket of your trousers:

enter image description here

Here is a full implementation. The cut is only used to priorize the rewriting and corresponds pretty much Haskell rules. Except that Haskell might not have a datatype logical variable like Prolog:

:- op(300, fy, ~).

eval(A, A) :- var(A), !.
eval(A+B, R) :- !, eval(A, X), eval(B, Y), simp(X+Y, R).
eval(A*B, R) :- !, eval(A, X), eval(B, Y), simp(X*Y, R).
eval(~A, R) :- !, eval(A, X), simp(~X, R).
eval(A, A).

simp(A, A) :- var(A), !.
simp(A+B, B) :- A == 0, !.
simp(A+B, A) :- B == 0, !.
simp(A+_, 1) :- A == 1, !.
simp(_+B, 1) :- B == 1, !.
simp(A*_, 0) :- A == 0, !.
simp(_*B, 0) :- B == 0, !.
simp(A*B, B) :- A == 1, !.
simp(A*B, A) :- B == 1, !.
simp(~A, 1) :- A == 0, !.
simp(~A, 0) :- A == 1, !.
simp(A, A).

The code is not pure Prolog and uses non-logical var/1, (==)/2, etc.. meta programming. Like Boole we linearly reduce and perform a conjunction of the two substitutions, so we do the Quine split without some branching and via a single front:

judge(A, [B|R]) :- eval(A, B), 
                   term_variables(B, L), judge(B, L, R).

judge(_, [], R) :- !, R = [].
judge(A, [B|L], R) :-
  copy_term(A-[B|L], C-[0|L]),
  copy_term(A-[B|L], D-[1|L]), judge(C*D, R).

In the above we use copy_term/2 to do substitution. The idea is borrowed from Ulrich Neumerkels lambda library. We need to also make available =< and =:= in eval/2 and simp/2. For full source code see here. Here are example runs in any of your favorite ISO Prolog:

?- judge(A+ ~A, L).
L = [A+ ~A, 1] /* Ends in 1, Tautology */

?- judge(A+ ~B, L).
L = [A+ ~B, ~B, 0] /* Ends in 0, Falsifiable */

?- judge(((P+Q)=<R)=:=((P=<R)*(Q=<R)), L).
L = [(P+Q =< R) =:= (P =< R)*(Q =< R),
  ((Q =< R) =:= (Q =< R))*(R =:= R*(Q =< R)),
  (R =:= R)*((R =:= R)*(R =:= R*R)), 1].  

(*) From:
U. Martin and T. Nipkow. Boolean unification—the story so far.
In Unification, pages 437–455. Academic Press, London, 1990.

  • 1
    @user:502187 (Mostowski Collapse), many thanks for your very instructive reply and your concise code. It is indeed helpful. Could you explain your choice of using ! that is also the cut symbol to translate the alternation connective ? Is it not confusing for Prolog? Again, many thanks. – Joseph Vidal-Rosset Aug 27 '20 at 07:32
  • I just tested your prover with a big disjunctive formula that is not valid and that, as input given to your prover, should received as reply the values that falsify the formula. But the output is false, as for a tautology, if I have understood your prover. I cannot paste this in a comment and I hesitate to give it in your code. Please, tell me how to show you this test. – Joseph Vidal-Rosset Aug 27 '20 at 08:31
  • Thanks. I just tested it with Prolog variables and the program loops indefinitely. If it would start with assuming that all variables are false, the output (that is the falsification) of the formula would be straightforward. Is it possible to improve this point? – Joseph Vidal-Rosset Aug 27 '20 at 09:23
  • I changed your ! for disjuction with code ' | ' and I got the reply: A11 = A21, A21 = A31, A31 = A41, A41 = A51, A51 = A12, A12 = A22, A22 = A32, A32 = A42, A42 = A52, A52 = A13, A13 = A23, A23 = A33, A33 = A43, A43 = A53, A53 = A14, A14 = A24, A24 = A34, A34 = A44, A44 = A54, A54 = A15, A15 = A25, A25 = A35, A35 = A45, A45 = A55, A55 = 0. It works ! – Joseph Vidal-Rosset Aug 27 '20 at 09:54