Message Boards Message Boards

[WSS19] Short Step-by-step Solution of Boolean Algebra

Posted 5 years ago

enter image description here

With the equational theorem proving framework in Wolfram Language, it is easy to check the correctness of a theorem with a machine-generated proof. However, such proofs almost always involve extremely lengthy and convoluted lemmas that are incomprehensible for human readers. In my project, I implemented a set of functions that can provide a step-by-step reduction of Boolean algebra expressions. The functions recursively apply heuristic rules to expressions until they reach normal form. Moreover, the functions can return the shortest reduction between any two equivalent Boolean algebra expressions by a dynamic version of Dijkstra's algorithm.

What can my functions do

Given an arbitrary Boolean expression, I can give a step-by-step reduction of it to normal form.

reduceList[DNF][and[or[a, b], b]] // generateProofNotebook

I can give a shortest step-by-step reduction.

reduceShortestList[DNF][and[or[a, b], b]] // generateProofNotebook

I can convert an expression in conjunctive normal form ("and" of "or") to disjunctive normal form ("or" of "and"), or the other way around:

generateProofNotebook /@ {reduceShortestList[CNF][or[and[a, not[b]], and[not[b], c]]],
reduceShortestList[DNF][and[not[b], or[a, c]]]}

Or a shortest path between any two equivalent Boolean expressions:

How do the functions do that

To provide a short step-by-step reduction of Boolean expressions, there are two major steps:

  • identify potential application of heuristic rules and apply them
  • find the shortest sequence of rule applications to reduce a Boolean expression to normal form

The identification functions use pattern matching, the applications functions use substitution frameworks, and the search for shortest path uses a dynamic version of Dijkstra's algorithm.

Setting up: symbols I use for Boolean algebra

Since Mathematica evaluates inputs upon entering them, we use the lower case version of Boolean literals and operators, namely "true", "false", "and", "or", and "not". We regard commutativity as a trivial property of "and" and "or" and set their attributes to be Orderless.

SetAttributes[and, Orderless];
SetAttributes[or, Orderless];

Identify Patterns and Apply Rules

List of heuristic rules I use

The set of heuristic logic equivalence rules I used are:

png

Identify one potential step of a given pattern and apply it

dentify all potential steps of a given pattern on the outermost level

Identify all potential steps of a given pattern on all levels

Reduce an expression to normal form

Find Shortest Reduction

Graph of Boolean Expressions

I can regard every Boolean expression as a vertex, and I claim that two Boolean expression vertices are adjacent if one can be transformed into another by one heuristic step. Then I have a graph of Boolean expressions, where each connected components is an equivalence class of Boolean expressions.

Each edge can be denoted in the form of {curr, prev, step}, where "curr" and "prev" are two Boolean expressions such that "curr" can be derived by applying step to "prev". Here is a graph of all first-order neighbors to the expression

Challenges to Dijkstra's Algorithm

Dijkstra's Algorithm

Dijkstra's Algorithm is a classic algorithm for finding the shortest path between any two vertices in a connected graph.

1. Potentially Infinite Size of Graph

The size of any connected component in a Boolean expression graph is infinite. For any expression e, you can easily find countably infinite equivalent expressions just be adding arbitrarily many double negations ahead of it. (You can easily find uncountably many by alternating between double negation and identity rules.) It is impossible to know how large the graph needs to be so that a path exists between two equivalent expressions. Since the framework for graphs in Wolfram Language can only handle static finite graphs, I cannot use the built-in "FindShortestPath" function.

2. Computationally Irreversible Equivalence Rules

Every step we take in reducing Boolean expressions is invertible, but only in theory. If we have the expression p[And][Not]p , we can easily reduce it to

Solutions to the Challenges

Examples

Generating Notebooks of Proofs

What I want my functions to do in the future

1. Potential improvements in the algorithm:
2. Explorations in multilevel logic reduction:

Appendix

Conversion between formats
SetAttributes[and, Orderless];
SetAttributes[or, Orderless];

<<Notation`
InfixNotation[ParsedBoxWrapper["\[And]"],and]
InfixNotation[ParsedBoxWrapper["\[Or]"],or]
Notation[ParsedBoxWrapper[RowBox[{"\[Not]", "x_", " "}]] \[DoubleLongLeftRightArrow] ParsedBoxWrapper[RowBox[{" ", RowBox[{"not", "[", "x_", "]"}]}]]]

and[or[not[and[not[a],b]],c],b]

PNG

Identify Steps

Helpers
indices[e_]:= Range[Length[e]]
pairs[set_]:= Subsets[set,{2}]
select[list_,test_,All]:= Select[list,test]
select[list_,test_,n_]:=S elect[list,test,n]
unequal[e_]:= e=!=#&
Recursive Handle
(*
recursion returns step structures of a given pattern from all levels of a given expression
input:
	opt_: All or 1;
	find_: a find function for a particular pattern, e.g. findDoubleNegation;
	e_: a boolean expression;
output:
 	if opt is 1, returns one potential step of the given pattern from some layer of the expression
	if opt is all, returns all potential step of the given patternfrom all layers of the expression
	returns {} if no potential steps of the given pattern exist in any layer of the expression
*)
recursion[opt_][find_][e_]:= Join[find[opt][e],Sequence @@Table[Append[Most[#],Prepend[Last[#],i]]&/@recursion[opt][find][e[[i]]],{i,1,Length[e],1}]]
Pattern Finding Functions
(*
find functions returns step structures of the given pattern from the outermost layer of a given expression
input:
	opt_: All or 1;
	e_: a boolean expression;
output:
 	if opt is 1, returns one potential step of the given pattern from the outermost layer of the expression
	if opt is all, returns all potential step of the given pattern from the outermost layer of the expression
	returns {} if no potential steps of the given pattern exist in the outermost layer of the expression
*)
identifyLiteral[and,true]:= "Identity";
identifyLiteral[and,false]:= "Domination";
identifyLiteral[or,true]:= "Domination";
identifyLiteral[or,false]:= "Identity";
identifyLiteral[not,true]:= "False";
identifyLiteral[not,false]:= "True";
identifyLiteral[_,_]:= {}
findLiteral[opt_][e_]:=select[indices[e],e[[#]]==true||e[[#]]==false&,opt]//Map[{identifyLiteral[e[[0]],e[[#]]],ToString[e[[0]]],{#}}&]

findIdempotent[opt_][e_]:=select[pairs[indices[e]],e[[#[[1]]]]===e[[#[[2]]]]&,opt]//Map[{"Idempotent",{#}}&]

findNegation[opt_][e_]:=select[pairs[indices[e]],(e[[#[[1]]]]==not[e[[#[[2]]]]]||not[e[[#[[1]]]]]==e[[#[[2]]]])&,opt]//Map[{"Negation",ToString[e[[0]]],{#}}&]

findDoubleNegation[opt_][not[not[_]]]:={{"Double Negation",{}}}
findDoubleNegation[_][_]:={}

findDeMorgan[_][not[and[__]]] := {{"De Morgan","and",{}}}
findDeMorgan[_][not[or[__]]] := {{"De Morgan","or",{}}}
findDeMorgan[_][_] := {}

findOrDistributive[opt_][or[terms__]]:=select[indices[{terms}],({terms}[[#]][[0]]===and)&,opt]//Map[Table[{"Distribution",or,i,#,{}},{i,select[indices[{terms}],unequal[#],opt]}]&]//Flatten[#,1]&
findAndDistributive[opt_][and[terms__]] :=select[indices[{terms}],({terms}[[#]][[0]]===or)&,opt]//Map[Table[{"Distribution",and,i,#,{}},{i,select[indices[{terms}],unequal[#],opt]}]&]//Flatten[#,1]&
findOrDistributive[_][_] := {}
findAndDistributive[_][_] := {}

findAssociative[opt_][or[terms__]]:=select[indices[{terms}],({terms}[[#]][[0]]===or)&,opt]//Map[{"Association",{#}}&]
findAssociative[opt_][and[terms__]]:=select[indices[{terms}],({terms}[[#]][[0]]===and)&,opt]//Map[{"Association",{#}}&]
findAssociative[_][_] :={}

matching[list1_,list2_]:=Flatten[Table[{#,i}&/@list1,{i,list2}],1]
findElementInTerm[terms_,ele_]:=SelectFirst[List@@terms,ele== #&,{}]
findAbsorption[opt_][and[terms__]]:=With[{orTerms=Select[indices[{terms}],{terms}[[#,0]]== or&]},
select[matching[orTerms,indices[{terms}]],findElementInTerm[{terms}[[#[[1]]]],{terms}[[#[[2]]]]]=!={}&,opt]]//Map[{"Absorption",Sequence@@#,{}}&]
findAbsorption[opt_][or[terms__]]:=With[{andTerms=Select[indices[{terms}],{terms}[[#,0]]== and&]},
select[matching[andTerms,indices[{terms}]],findElementInTerm[{terms}[[#[[1]]]],{terms}[[#[[2]]]]]=!={}&,opt]]//Map[{"Absorption",Sequence@@#,{}}&]
findAbsorption[_][_]:={}

Potential Step
(*
findPotentialStep returns potential steps of any pattern from any layer of a given expression
input:
	opt_: All or 1;
	seq_: CNF, DNF, or full;
	e_: a boolean expression;
output:
 	if opt is 1, returns one potential step of any pattern from some layer of the expression
	if opt is all, returns all potential step of all patterns from all layers of the expression
	returns {} if no potential steps of any given pattern exist in any layer of the expression, this happens when the expression is in normal form 
*)
CNF = {findDoubleNegation,findLiteral,findDeMorgan, findAssociative,findIdempotent, findOrDistributive,findNegation,findAbsorption};
DNF = {findDoubleNegation,findLiteral,findDeMorgan, findAssociative,findIdempotent, findAndDistributive,findNegation,findAbsorption};
full = Union[CNF,DNF];
findPotentialStep[seq_][1][e_] := Module[{result},SelectFirst[seq,(result = recursion[1][#][e])=!={}&,{}];first[result]]
findPotentialStep[seq_][All][e_] :=Join@@Table[recursion[All][find][e],{find,seq}]

Apply Steps

Helpers
(* these helper functions add options to replace or extract entire expressions *)
replacePart[e_,{}->new_]:= new
replacePart[e_,loc_->new_]:= ReplacePart[e,loc->new]
extract[e_,{}]:= e
extract[e_,loc_]:= Extract[e,loc]
reduceSingle[e_]:= If[Length[e]<=1,e[[1]],e]
Apply Step
(* applyStep takes a boolean expression and a step, and returns a boolean expression with the step applied*)
applyStep[e_,{}]:={}
applyStep[e_,{"Identity",_,loc_}]:=replacePart[e,Most[loc]->(Drop[extract[e,Most[loc]],{Last[loc]}] // reduceSingle)]
applyStep[e_,{"Domination","and",loc_}]:=replacePart[e,Most[loc]-> false]
applyStep[e_,{"Domination","or",loc_}]:=replacePart[e,Most[loc]-> true]
applyStep[e_,{"True",_,loc_}]:=replacePart[e,Most[loc]->true]
applyStep[e_,{"False",_,loc_}]:=replacePart[e,Most[loc]->false]
applyStep[e_,{"Idempotent",loc_}]:= replacePart[e,Most[loc]->(Drop[extract[e,Most[loc]],{Last[loc][[1]]}] // reduceSingle)]
applyStep[e_,{"Negation","and",loc_}]:= replacePart[e,Most[loc]-> false]
applyStep[e_,{"Negation","or",loc_}]:= replacePart[e,Most[loc]-> true]
applyStep[e_,{"Double Negation",loc_}] := replacePart[e,loc->extract[e,Join[loc,{1,1}]]]
applyStep[e_,{"De Morgan","and",loc_}]:=replacePart[e,loc->or @@not /@ extract[e,loc][[1]]]
applyStep[e_,{"De Morgan","or",loc_}]:=replacePart[e,loc->and @@not /@ extract[e,loc][[1]]]
applyStep[e_,{"Distribution",oper_,left_,right_,loc_}]:=Module[{smaller,bigger,orig,dropped,new},
{smaller,bigger}=Sort@{left,right};
orig=extract[e,loc];
dropped=Drop[#,{smaller}]&@Drop[#,{bigger}]&@ orig;
new= oper[orig[[left]],#]&/@orig[[right]];
replacePart[e,loc->(Append[dropped,new]//reduceSingle)]]
applyStep[e_,{"Association",loc_}]:=replacePart[e,Most[loc]->Join[Drop[extract[e,Most[loc]],{Last[loc]}],extract[e,loc]]]
applyStep[e_,{"Absorption",tIndex_,eIndex_,loc_}]:=replacePart[e,loc->(Drop[extract[e,loc],{tIndex}]// reduceSingle)]

Reduce Expressions

Helpers
first[{}] = {}; 
first[list_] := First[list]; 
last[{}] = {}; 
last[list_] := Last[list]; 
Reduce Expressions with a List of Steps
(*
reduceList returns a list of steps and expressions that reduces the given boolean to CNF or DNF;
input:
	seq_: CNF or DNF;
	e_: a boolean expression;
output:
 	a list of {expression, location, rule} 
*)
reduceList[seq_][e_]:=
NestWhileList[With[{trans=findPotentialStep[seq][1][#[[1]]]},{applyStep[#[[1]],trans],#[[1]],trans}]&,{e,{},{"Initial",{}}},#[[1]]=!={}&]
reduceListExpression[seq_][e_]:=NestWhileList[applyStep[#,findPotentialStep[seq][1][#]]&,e,#=!={}&]
reduceExpression[seq_][e_]:=reduceListExpression[seq][e][[-2]]
Find Shortest Path
Find Shortest Path
shortestRoute[e1_,e2_]:=Reverse[Switch[reduceExpression[CNF][e1]==reduceExpression[CNF][e2],True,findShortestRoute[e1,e2],False,{}]]

findShortestRoute[e1_,e2_]:=Module[{edges1={{e1,{},{"Initial",{}}}},edges2={{e2,{},{"End",{}}}},meet},
NestWhile[({edges1,edges2}={expandEdges[edges1],expandEdges[edges2]})&,{},(meet=findCommonEdge[edges1,edges2])==={}&];
Join[tracePath[edges1,meet[[1]],{meet[[1]]}],reverseTracePath[tracePath[edges2,meet[[2]],{meet[[2]]}]]]]//DeleteCases[#,x_List/;Length[x]==0]&

reduceShortestList[seq_][e_]:=findShortestRoute[e,reduceExpression[seq][e]]
Helper for Visualization
displayList[list_]:=list//Map[{first[last[#]],last[last[#]],first[#]}&]//TableForm
graph[edges_]:=Graph[#[[2]]->#[[1]]&/@DeleteCases[edges,a_List/;a[[3,1]]=="Initial"]]
Operations on Edges
Find Membership in Edges
(*
FindMemberInEdges checks if an expression is already in the list of edges. 
input:
	edges_: a list of edge structures;
	new_: an Boolean expression;
output:
 	{} if new is not already in edges;
 	the corresponding edge structure of new if new is already in edges.
*)
findMemberInEdges[edges_,new_]:=SelectFirst[edges,First[#]== new&,{}]
Find Intersection in Edges
(*
FindCommonInEdges checks if two list of edges has intersection. 
input:
	n1, n2: two lists of edge structure;
output:
	{} if intersection is empty;
	a edge structure in the intersection if intersection is not empty.
*)
findCommonEdge[n1_,n2_]:=Module[{m1,m2},m1=SelectFirst[n1,(m2=findMemberInEdges[n2,First[#]])=!={}&,{}];Switch[m1,{},{},_,{m1,m2}]]
Union Edges
(*
JoinEdges joins a edge structure into a list of edge structure if it is not already in. 
input:
	edges: a list of edge structures;
	new: a edge structure;
output:
	the original edges list if new is already in edges;
	the original edges list with new appended if new is not already in edges.
*)
joinEdges[edges_,new_]:=Switch[findMemberInEdges[edges,First[new]],{},Append[edges,new],_,edges]
(*
UnionEdges merge two list of edges and removes duplicates from the new list. 
input:
	old: a list of edge structures;
	new: a list of edge structures in which duplicates will be removed;
output:
	a merged list of edge structures
*)
unionEdges[old_,new_]:=Fold[joinEdges,old,new]
Expand Edges
findAllEdges[e_] := {applyStep[e,#],e,#}&/@findPotentialStep[full][All][e]
expandEdges[edges_]:=unionEdges[edges,Flatten[findAllEdges[#[[1]]]&/@edges,1]]
Trace Path
tracePath[_,{},trace_]:= trace
tracePath[edges_,end_,trace_]:=With[{lastEdge=findMemberInEdges[edges,end[[2]]]},tracePath[edges,lastEdge,Prepend[trace,lastEdge]]]
Reverse Path
reverseTracePath[edges_]:=Reverse[reverseEdge/@ edges]
reverseEdge[{curr_,prev_,{"Initial",{}}}]:={curr,prev,{"Initial",{}}}
reverseEdge[{curr_,prev_,{"End",{}}}]:={curr,prev,{"End",{}}}
reverseEdge[{curr_,prev_,{"Identity",oper_,loc_}}]:={prev,curr,{"Identity'",oper,Most[loc]}}
reverseEdge[{curr_,prev_,{"Domination",oper_,loc_}}]:={prev,curr,{"Domination'",oper,Most[loc]}}
reverseEdge[{curr_,prev_,{"False",oper_,loc_}}]:={prev,curr,{"False'",oper,Most[loc]}}
reverseEdge[{curr_,prev_,{"True",oper_,loc_}}]:={prev,curr,{"True'",oper,Most[loc]}}
reverseEdge[{curr_,prev_,{"Idempotent",loc_}}]:={prev,curr,{"Idempotent'",Most[loc]}}
reverseEdge[{curr_,prev_,{"Negation",oper_,loc_}}]:={prev,curr,{"Negation'",oper,Most[loc]}}
reverseEdge[{curr_,prev_,{"Double Negation",loc_}}]:={prev,curr,{"Double Negation'",loc}}
reverseEdge[{curr_,prev_,{"De Morgan",_,loc_}}]:={prev,curr,{"De Morgan'",loc}}(* change for better explanation *)
reverseEdge[{curr_,prev_,{"Distribution",oper_,tIndex_,eIndex_,loc_}}]:={prev,curr,{"Distribution'",loc}}(* change for better explanation *)
reverseEdge[{curr_,prev_,{"Association",loc_}}]:={prev,curr,{"Association'",Most[loc]}}
reverseEdge[{curr_,prev_,{"Absorption",_,_,loc_}}]:={prev,curr,{"Absorption'",loc}}
reverseEdge[_]:={}
POSTED BY: Adela Deng
Reply to this discussion
Community posts can be styled and formatted using the Markdown syntax.
Reply Preview
Attachments
Remove
or Discard

Group Abstract Group Abstract