File Coverage

blib/lib/Attean/BindingEqualityTest.pm
Criterion Covered Total %
statement 138 184 75.0
branch 21 32 65.6
condition 2 2 100.0
subroutine 21 25 84.0
pod 4 4 100.0
total 186 247 75.3


line stmt bran cond sub pod time code
1 50     50   640 use v5.14;
  50         170  
2 50     50   277 use warnings;
  50         105  
  50         11362  
3              
4             =head1 NAME
5              
6             Attean::BindingEqualityTest - Test for equality of binding sets with bnode isomorphism
7              
8             =head1 VERSION
9              
10             This document describes Attean::BindingEqualityTest version 0.032
11              
12             =head1 SYNOPSIS
13              
14             use v5.14;
15             use Attean;
16             my $test = Attean::BindingEqualityTest->new();
17             if ($test->equals($iter_a, $iter_b)) {
18             say "Iterators contain equivalent bindings";
19             }
20              
21             =head1 DESCRIPTION
22              
23             ...
24              
25             =head1 METHODS
26              
27             =over 4
28              
29             =cut
30              
31             my $class = shift;
32             my @iters = @_;
33 0     0   0 my @values = $class->_materialize([], @iters);
34 0         0 return bless(\@values, $class);
35 0         0 }
36 0         0
37             my $class = shift;
38             my $v = shift;
39             my @iters = @_;
40 0     0   0 if (scalar(@iters)) {
41 0         0 my $i = shift(@iters);
42 0         0 my @values;
43 0 0       0 while (my $vv = $i->next) {
44 0         0 my $prefix = [@$v, @$vv];
45 0         0 push(@values, $class->_materialize($prefix, @iters));
46 0         0 }
47 0         0 return @values;
48 0         0 } else {
49             return $v;
50 0         0 }
51             }
52 0         0
53             my $self = shift;
54             return shift(@$self);
55             }
56             }
57 0     0   0  
58 0         0 use v5.14;
59             use warnings;
60             use Moo;
61             use Types::Standard qw(CodeRef ConsumerOf Str);
62             use Data::Dumper;
63 50     50   567 use Algorithm::Combinatorics qw(permutations);
  50         204  
64 50     50   323 use Scalar::Util qw(blessed);
  50         143  
  50         1594  
65 50     50   285 use List::Util qw(shuffle);
  50         117  
  50         322  
66 50     50   16493 use Attean::RDF;
  50         125  
  50         406  
67 50     50   32468 use Digest::MD5 qw(md5_hex);
  50         117  
  50         2657  
68 50     50   304 use namespace::clean;
  50         136  
  50         2403  
69 50     50   339
  50         109  
  50         2233  
70 50     50   307 with 'MooX::Log::Any';
  50         111  
  50         2606  
71 50     50   335
  50         111  
  50         383  
72 50     50   35801 has error => (is => 'rw', isa => Str, init_arg => undef);
  50         116  
  50         1997  
73 50     50   277
  50         113  
  50         339  
74             my $o = shift;
75             if ($o->does('Attean::API::Model')) {
76             return $o->get_quads;
77             } elsif ($o->does('Attean::API::Iterator')) {
78             return $o;
79             }
80 28     28   38 return;
81 28 50       65 }
    50          
82 0         0  
83             =item C<< equals ( $graph1, $graph2 ) >>
84 28         1046  
85             Returns true if the invocant and $graph represent two equal RDF graphs (e.g.
86 0         0 there exists a bijection between the RDF statements of the invocant and $graph).
87              
88             =cut
89              
90             my $self = shift;
91             $self->error('');
92             return $self->_check_equality(@_) ? 1 : 0;
93             }
94              
95             my $self = shift;
96             my ($a, $b) = map { _coerce($_) } @_;
97 10     10 1 1981
98 10         151 my @graphs = ($a, $b);
99 10 100       304 my ($ba, $nba) = $self->split_blank_statements($a);
100             my ($bb, $nbb) = $self->split_blank_statements($b);
101             if (scalar(@$nba) != scalar(@$nbb)) {
102             my $nbac = scalar(@$nba);
103 10     10   14 my $nbbc = scalar(@$nbb);
104 10         23
  20         33  
105             # warn "====================================================\n";
106 10         32 # warn "BindingEqualityTest count of non-blank statements didn't match:\n";
107 10         27 # warn "-------- a\n";
108 10         31 # foreach my $t (@$nba) {
109 10 100       36 # warn $t->as_string . "\n";
110 1         4 # }
111 1         2 # warn "-------- b\n";
112             # foreach my $t (@$nbb) {
113             # warn $t->as_string . "\n";
114             # }
115             $self->error("count of non-blank statements didn't match ($nbac != $nbbc)");
116             return 0;
117             }
118             my $bac = scalar(@$ba);
119             my $bbc = scalar(@$bb);
120             if ($bac != $bbc) {
121             $self->error("count of blank statements didn't match ($bac != $bbc)");
122             return 0;
123 1         22 }
124 1         45
125             for ($nba, $nbb) {
126 9         13 @$_ = sort map { $_->as_string } @$_;
127 9         18 }
128 9 100       22
129 1         20 foreach my $i (0 .. $#{ $nba }) {
130 1         29 unless ($nba->[$i] eq $nbb->[$i]) {
131             # warn "====================================================\n";
132             # warn "BindingEqualityTest non-blank statements didn't match:\n";
133 8         16 # warn "-------- a\n";
134 16         31 # foreach my $t (@$nba) {
  10         27  
135             # warn $t . "\n";
136             # }
137 8         17 # warn "-------- b\n";
  8         27  
138 5 100       21 # foreach my $t (@$nbb) {
139             # warn $t . "\n";
140             # }
141             $self->error("non-blank triples don't match:\n" . Dumper($nba->[$i], $nbb->[$i]));
142             return 0;
143             }
144             }
145            
146             return _find_mapping($self, $ba, $bb, 1);
147             }
148              
149 2         12 =item C<< is_subgraph_of ( $graph1, $graph2 ) >>
150 2         230  
151             Returns true if the invocant is a subgraph of $graph. (i.e. there exists an
152             injection of RDF statements from the invocant to $graph.)
153              
154 6         22 =cut
155              
156             my $self = shift;
157             $self->error('');
158             return $self->_check_subgraph(@_) ? 1 : 0;
159             }
160              
161             =item C<< injection_map ( $graph1, $graph2 ) >>
162              
163             If the invocant is a subgraph of $graph, returns a mapping of blank node
164             identifiers from the invocant graph to $graph as a hashref. Otherwise
165 1     1 1 9 returns false. The solution is not always unique; where there exist multiple
166 1         21 solutions, the solution returned is arbitrary.
167 1 50       35  
168             =cut
169              
170             my $self = shift;
171             $self->error('');
172             my $map = $self->_check_subgraph(@_);
173             return $map if $map;
174             return;
175             }
176              
177             my $self = shift;
178             my ($a, $b) = map { _coerce($_) } @_;
179            
180 3     3 1 110 my @graphs = ($a, $b);
181 3         61 my ($ba, $nba) = $self->split_blank_statements($a);
182 3         97 my ($bb, $nbb) = $self->split_blank_statements($b);
183 3 50       19
184 0         0 if (scalar(@$nba) > scalar(@$nbb)) {
185             $self->error("invocant had too many blank node statements to be a subgraph of argument");
186             return 0;
187             } elsif (scalar(@$ba) > scalar(@$bb)) {
188 4     4   11 $self->error("invocant had too many non-blank node statements to be a subgraph of argument");
189 4         10 return 0;
  8         17  
190             }
191 4         13  
192 4         16 my %NBB = map { $_->as_string => 1 } @$nbb;
193 4         16
194             foreach my $st (@$nba) {
195 4 50       22 unless ($NBB{ $st->as_string }) {
    50          
196 0         0 return 0;
197 0         0 }
198             }
199 0         0
200 0         0 return _find_mapping($self, $ba, $bb);
201             }
202              
203 4         12 my $self = shift;
  1         6  
204             my @st = @_;
205 4         12 my %blank_ids_b_iris;
206 1 50       4 foreach my $st (@st) {
207 0         0 my @iris = map { $_->value } grep { $_->does('Attean::API::IRI') } $st->values;
208             unless (scalar(@iris)) {
209             push(@iris, '_');
210             }
211 4         18 foreach my $n (grep { $_->does('Attean::API::Blank') } $st->values) {
212             foreach my $i (@iris) {
213             $blank_ids_b_iris{$n->value}{$i}++;
214             }
215 0     0   0 }
216 0         0 }
217 0         0
218 0         0 my %iri_blanks;
219 0         0 foreach my $bid (sort keys %blank_ids_b_iris) {
  0         0  
  0         0  
220 0 0       0 my $d = Digest::MD5->new();
221 0         0 foreach my $iri (sort keys %{ $blank_ids_b_iris{$bid} }) {
222             $d->add($iri);
223 0         0 }
  0         0  
224 0         0 $iri_blanks{$d->hexdigest}{$bid}++;
225 0         0 }
226             return \%iri_blanks;
227             }
228            
229             my $self = shift;
230 0         0 my $ba = shift;
231 0         0 my $bb = shift;
232 0         0 my $equal = shift || 0;
233 0         0  
  0         0  
234 0         0 # warn "########### _find_mapping:\n";
235             # warn "============ A\n";
236 0         0 # foreach my $t (@$ba) {
237             # warn $t->as_string . "\n";
238 0         0 # }
239             # warn "============ B\n";
240             # foreach my $t (@$bb) {
241             # warn $t->as_string . "\n";
242 10     10   16 # }
243 10         18  
244 10         14 if (scalar(@$ba) == 0) {
245 10   100     115 return {};
246             }
247            
248             my %blank_ids_a;
249             foreach my $st (@$ba) {
250             foreach my $n ($st->blanks) {
251             $blank_ids_a{ $n->value }++;
252             }
253             }
254              
255             my %blank_ids_b;
256             foreach my $st (@$bb) {
257 10 100       29 foreach my $n ($st->blanks) {
258 2         18 $blank_ids_b{ $n->value }++;
259             }
260             }
261 8         17
262 8         19
263 14         42 my (@ka, @kb);
264 18         46 my $kbp;
265             # if ($equal) {
266             # # if we're testing for equality, and not just finding an injection mapping,
267             # # we can avoid unnecessary work by restricting mappings to those where each
268 8         19 # # permutation only maps blank nodes to other blank nodes that appear in
269 8         17 # # similar bindings (in this case they appear with all the same IRIs)
270 16         37 # my $ba_iri_blanks = $self->_statement_blank_irisets(@$ba);
271 21         49 #
272             # my $bb_iri_blanks = $self->_statement_blank_irisets(@$bb);
273             #
274             # my $ba_keys = join('|', sort keys %$ba_iri_blanks);
275             # my $bb_keys = join('|', sort keys %$bb_iri_blanks);
276 8         54 # unless ($ba_keys eq $bb_keys) {
277 8         0 # $self->error("didn't find blank node mapping\n");
278             # return 0;
279             # }
280             #
281             # my @iters;
282             # foreach my $k (sort keys %$ba_iri_blanks) {
283             # unless (scalar(@{[keys %{ $ba_iri_blanks->{$k} }]}) == scalar(@{[keys %{ $bb_iri_blanks->{$k} }]})) {
284             # $self->error("didn't find blank node mapping\n");
285             # return 0;
286             # }
287             # push(@ka, keys %{ $ba_iri_blanks->{$k} });
288             # push(@kb, keys %{ $bb_iri_blanks->{$k} });
289             # my $i = permutations([keys %{ $bb_iri_blanks->{$k} }]);
290             # push(@iters, $i);
291             # }
292             #
293             # if (scalar(@iters) == 1) {
294             # $kbp = shift(@iters);
295             # } else {
296             # $kbp = Attean::BindingEqualityTest::_Iter->new(@iters);
297             # }
298             # } else {
299             @ka = keys %blank_ids_a;
300             @kb = keys %blank_ids_b;
301             $kbp = permutations( [shuffle @kb] );
302             # }
303            
304             my $canon_map = Attean::TermMap->canonicalization_map;
305             my %bb_master = map { $_->apply_map($canon_map)->as_string => 1 } @$bb;
306            
307             my $count = 0;
308             MAPPING: while (my $mapping = $kbp->next) {
309             my %mapping_str;
310             @mapping_str{ @ka } = @$mapping;
311             my %mapping = map {
312 8         22 Attean::Blank->new($_)->as_string => Attean::Blank->new($mapping_str{$_})
313 8         15 } (keys %mapping_str);
314 8         100 my $mapper = Attean::TermMap->rewrite_map(\%mapping);
315             $self->log->trace("trying mapping: " . Dumper($mapping));
316            
317 8         485 my %bb = %bb_master;
318 8         365 foreach my $st (@$ba) {
  16         50  
319             my $mapped_st = $st->apply_map($mapper)->as_string;
320 8         18 # warn ">>>>>>>\n";
321 8         39 # warn "-> " . $st->as_string . "\n";
322 11         146 # warn "-> " . $mapped_st . "\n";
323 11         44 $self->log->trace("checking for '$mapped_st' in " . Dumper(\%bb));
324             if ($bb{ $mapped_st }) {
325 11         30 $self->log->trace("Found mapping for binding: " . Dumper($mapped_st));
  19         605  
326             delete $bb{ $mapped_st };
327 11         484 } else {
328 11         563 $self->log->trace("No mapping found for binding: " . Dumper($mapped_st));
329             # warn "No mapping found for binding: " . Dumper($mapped_st);
330 11         3936 # warn Dumper(\%bb);
331 11         31 next MAPPING;
332 18         68 }
333             }
334             $self->error("found mapping: " . Dumper(\%mapping_str));
335             return \%mapping_str;
336 18         267 }
337 18 100       1122
338 14         223 # warn "didn't find blank node mapping:\n";
339 14         605 # warn "============ A\n";
340             # foreach my $t (@$ba) {
341 4         67 # warn $t->as_string . "\n";
342             # }
343             # warn "============ B\n";
344 4         208 # foreach my $t (@$bb) {
345             # warn $t->as_string . "\n";
346             # }
347 7         31 $self->error("didn't find blank node mapping\n");
348 7         771 return 0;
349             }
350              
351             =item C<< split_blank_statements( $iter ) >>
352              
353             Returns two array refs containing bindings from C<< $iter >>, with bindings
354             containing blank nodes and bindings without any blank nodes, respectively.
355              
356             =cut
357              
358             my $self = shift;
359             my $iter = shift;
360 1         27 my (@blanks, @nonblanks);
361 1         35 while (my $st = $iter->next) {
362             if ($st->has_blanks) {
363             push(@blanks, $st);
364             } else {
365             push(@nonblanks, $st);
366             }
367             }
368             return (\@blanks, \@nonblanks);
369             }
370              
371             }
372 28     28 1 44  
373 28         31 1;
374 28         50  
375 28         92  
376 55 100       134 =back
377 39         592  
378             =head1 BUGS
379 16         50  
380             Please report any bugs or feature requests to through the GitHub web interface
381             at L<https://github.com/kasei/attean/issues>.
382 28         87  
383             =head1 SEE ALSO
384              
385              
386              
387             =head1 AUTHOR
388              
389             Gregory Todd Williams C<< <gwilliams@cpan.org> >>
390              
391             =head1 COPYRIGHT
392              
393             Copyright (c) 2014--2022 Gregory Todd Williams.
394             This program is free software; you can redistribute it and/or modify it under
395             the same terms as Perl itself.
396              
397             =cut