File Coverage

blib/lib/Algorithm/SAT/Backtracking/DPLL.pm
Criterion Covered Total %
statement 85 91 93.4
branch 31 42 73.8
condition 19 35 54.2
subroutine 16 17 94.1
pod 1 1 100.0
total 152 186 81.7


line stmt bran cond sub pod time code
1             package Algorithm::SAT::Backtracking::DPLL;
2 9     9   913 use Storable qw(dclone);
  9         11  
  9         492  
3 7     7   3692 use Data::Dumper;
  7         24316  
  7         387  
4 4     4   30 use strict;
  4         4  
  4         122  
5 4     4   14 use warnings;
  4         5  
  4         3553  
6             our $VERSION = "0.13";
7              
8             # this allow to switch the parent implementation (needed for the Ordered alternative)
9             sub import {
10 22     22   107 my ( $class, $flag ) = @_;
11 22 100       55 if ($flag) {
12 2     2   8 eval "use base '$flag'";
  2         2  
  2         4628  
  2         124  
13             }
14             else {
15 4     4   24 eval "use base 'Algorithm::SAT::Backtracking'";
  4     4   9  
  4     4   5904  
  4         20  
  4         5  
  4         150  
  4         19  
  4         6  
  4         127  
  20         1360  
16             }
17             }
18              
19             sub solve {
20              
21             # ### solve
22             #
23             # * `variables` is the list of all variables
24             # * `clauses` is an array of clauses.
25             # * `model` is a set of variable assignments.
26 60     60 1 128 my ( $self, $variables, $clauses, $model ) = @_;
27 60 100       132 $model = {} if !defined $model;
28 60         1554 my $impurity = dclone($clauses);
29              
30 60 100       191 if ( !exists $self->{_impurity} ) {
31 12         17 $self->{_impurity}->{$_}++ for ( map { @{$_} } @{$impurity} );
  45         38  
  45         262  
  12         31  
32             }
33              
34             # If every clause is satisfiable, return the model which worked.
35 226 100 66     761 return $model
36             if (
37             ( grep {
38 60         97 ( defined $self->satisfiable( $_, $model )
39             and $self->satisfiable( $_, $model ) == 1 )
40             ? 0
41             : 1
42 60 100       73 } @{$clauses}
43             ) == 0
44             );
45              
46             # If any clause is **exactly** false, return `false`; this model will not
47             # work.
48 48 50       143 return 0 if !$self->_consistency_check( $clauses, $model );
49              
50 48         124 $model = $self->_up( $variables, $clauses, $model )
51             ; # find unit clauses and sets them
52              
53 48 50       78 return 0 if !$self->_consistency_check( $clauses, $model );
54              
55             # TODO: pure unit optimization
56             # XXX: not working
57              
58             # $self->_pure($_)
59             # ? ( $model->{$_} = 1 and $self->_remove_clause_if_contains( $_, $clauses ) )
60             # : $self->_pure( "-" . $_ )
61             # ? ( $model->{$_} = 0 and $self->_remove_clause_if_contains( $_, $clauses ) )
62             # : ()
63             # for @{$variables};
64             # return $model if ( @{$clauses} == 0 ); #we were lucky
65              
66             # XXX: end
67              
68             # Choose a new value to test by simply looping over the possible variables
69             # and checking to see if the variable has been given a value yet.
70              
71 48         150 my $choice = $self->_choice( $variables, $model );
72              
73             # If there are no more variables to try, return false.
74              
75 48 50       88 return 0 if ( !$choice );
76              
77             # Recurse into two cases. The variable we chose will need to be either
78             # true or false for the expression to be satisfied.
79 48   33     136 return $self->solve( $variables, $clauses,
80             $self->update( $model, $choice, 1 ) ) #true
81             || $self->solve( $variables, $clauses,
82             $self->update( $model, $choice, 0 ) ); #false
83             }
84              
85             sub _consistency_check {
86 96     96   99 my ( $self, $clauses, $model ) = @_;
87 362 50 66     749 return 0
88             if (
89             ( grep {
90 96         110 ( defined $self->satisfiable( $_, $model )
91             and $self->satisfiable( $_, $model ) == 0 )
92             ? 1
93             : 0
94 96 50       88 } @{$clauses}
95             ) > 0
96             );
97 96         253 return 1;
98              
99             }
100              
101             sub _pure {
102 24     24   179 my ( $self, $literal ) = @_;
103              
104             # Pure literal rule
105              
106             # if a variable only occurs positively in a formula, set it to true
107             # if a variable only occurs negated in a formula, set it to false
108              
109 24 50       98 my $opposite
110             = substr( $literal, 0, 1 ) eq "-"
111             ? substr( $literal, 1 )
112             : "-" . $literal;
113 24 100 66     277 return 1
      66        
      66        
114             if (
115             ( exists $self->{_impurity}->{$literal}
116             and $self->{_impurity}->{$literal} != 0
117             )
118             and (
119             !exists $self->{_impurity}->{$opposite}
120             or ( exists $self->{_impurity}->{$opposite}
121             and $self->{_impurity}->{$opposite} == 0 )
122             )
123             );
124              
125             # print STDERR "$literal is IMpure\n" and
126 18         88 return 0;
127             }
128              
129             sub _up {
130 36     36   73 my ( $self, $variables, $clauses, $model ) = @_;
131 36 100       63 $model = {} if !defined $model;
132              
133             #Finding single clauses that must be true, and updating the model
134 36         67 ( @{$_} != 1 )
  136         296  
135             ? ()
136             : ( substr( $_->[0], 0, 1 ) eq "-" ) ? (
137             $self->_remove_literal( substr( $_->[0], 1 ), $clauses, $model
138             ) #remove the positive clause form OR's and add it to the model with a false value
139             )
140             : ( $self->_add_literal( "-" . $_->[0], $clauses )
141             and $model->{ $_->[0] }
142             = 1
143             ) # if the literal is present, remove it from SINGLE ARRAYS in $clauses and add it to the model with a true value
144 36 50 0     30 for ( @{$clauses} );
    100          
145 36         62 return $model;
146             }
147              
148             sub _remove_literal {
149 20     20   92 my ( $self, $literal, $clauses, $model ) = @_;
150              
151             return
152 20 100 100     147 if $model
      66        
153             and exists $model->{$literal}
154             and $model->{$literal} == 0; #avoid cycle if already set
155             #remove the literal from the model (set to false)
156 9         23 $model->{$literal} = 0;
157              
158             #remove the literal from the model (set to false) and delete it from index
159              
160 9         28 $self->_delete_from_index( $literal, $clauses );
161              
162 9         26 return 1;
163             }
164              
165             sub _add_literal {
166 0     0   0 my ( $self, $literal, $clauses, $model ) = @_;
167              
168 0 0       0 $literal
169             = ( substr( $literal, 0, 1 ) eq "-" )
170             ? $literal
171             : substr( $literal, 1 );
172             return
173 0 0 0     0 if $model
      0        
174             and exists $model->{$literal}
175             and $model->{$literal} == 1; #avoid cycle if already set
176             #remove the literal from the model (set to false) and delete it from index
177 0         0 $model->{$literal} = 1;
178 0         0 $self->_delete_from_index( $literal, $clauses );
179 0         0 return 1;
180             }
181              
182             sub _delete_from_index {
183 16     16   28 my ( $self, $string, $list ) = @_;
184              
185 16         24 foreach my $c ( @{$list} ) {
  16         41  
186 64 100       80 next if @{$c} <= 1;
  64         169  
187 48         41 for ( my $index = scalar( @{$c} ); $index >= 0; --$index ) {
  48         105  
188 230 100 100     982 do {
189 12         15 splice( @{$c}, $index, 1 );
  12         22  
190 12         40 $self->{_impurity}->{$string}--;
191             }
192             if $c->[$index]
193             and $c->[$index] eq $string; # remove certain elements
194             }
195             }
196             }
197              
198             sub _remove_clause_if_contains {
199 12     12   10139 my ( $self, $literal, $list ) = @_;
200              
201 12         20 my $index = 0;
202 12         47 while ( $index < scalar @{$list} ) {
  48         128  
203 15         28 splice( @{$list}, $index, 1 )
  102         197  
204 36 100       39 if grep { $_ eq $literal } @{ $list->[$index] };
  36         62  
205 36         47 $index++;
206             }
207              
208             }
209              
210             1;
211              
212             =encoding utf-8
213              
214             =head1 NAME
215              
216             Algorithm::SAT::Backtracking::DPLL - A DPLL Backtracking SAT solver written in pure Perl
217              
218             =head1 SYNOPSIS
219              
220              
221             # You can use it with Algorithm::SAT::Expression
222             use Algorithm::SAT::Expression;
223              
224             my $expr = Algorithm::SAT::Expression->new->with("Algorithm::SAT::Backtracking::DPLL");
225             $expr->or( '-foo@2.1', 'bar@2.2' );
226             $expr->or( '-foo@2.3', 'bar@2.2' );
227             $expr->or( '-baz@2.3', 'bar@2.3' );
228             $expr->or( '-baz@1.2', 'bar@2.2' );
229             my $model = $exp->solve();
230              
231             # Or you can use it directly:
232             use Algorithm::SAT::Backtracking;
233             my $solver = Algorithm::SAT::Backtracking->new;
234             my $variables = [ 'blue', 'green', 'yellow', 'pink', 'purple' ];
235             my $clauses = [
236             [ 'blue', 'green', '-yellow' ],
237             [ '-blue', '-green', 'yellow' ],
238             [ 'pink', 'purple', 'green', 'blue', '-yellow' ]
239             ];
240              
241             my $model = $solver->solve( $variables, $clauses );
242              
243             =head1 DESCRIPTION
244              
245             Algorithm::SAT::Backtracking::DPLL is a pure Perl implementation of a SAT Backtracking solver.
246              
247             Look at L for a theory description.
248              
249             The DPLL variant applies the "unit propagation" and the "pure literal" technique to be faster.
250              
251             Look also at the tests file for an example of usage.
252              
253             L use this module to solve Boolean expressions.
254              
255             =head1 METHODS
256              
257             Inherits all the methods from L and implements new private methods to use the unit propagation and pure literal rule techniques.
258              
259             =head1 LICENSE
260              
261             Copyright (C) mudler.
262              
263             This library is free software; you can redistribute it and/or modify
264             it under the same terms as Perl itself.
265              
266             =head1 AUTHOR
267              
268             mudler Emudler@dark-lab.netE
269              
270             =head1 SEE ALSO
271              
272             L, L, L, L
273              
274             =cut
275