# RDF::Trine::Graph # ----------------------------------------------------------------------------- =head1 NAME RDF::Trine::Graph - Materialized RDF Graphs for testing isomorphism =head1 VERSION This document describes RDF::Trine::Graph version 1.001 =head1 SYNOPSIS use RDF::Trine::Graph; my $a = RDF::Trine::Graph->new( $model_a ); my $b = RDF::Trine::Graph->new( $model_b ); print "graphs are " . ($a->equals( $b ) ? "the same" : "different"); =head1 DESCRIPTION RDF::Trine::Graph provdes a mechanism for testing graph isomorphism based on graph triples from either a RDF::Trine::Model or a RDF::Trine::Iterator. Isomorphism testing requires materializing all of a graph's triples in memory, and so should be used carefully in situations with large graphs. =head1 METHODS =over 4 =cut package RDF::Trine::Graph; use strict; use warnings; no warnings 'redefine'; use Algorithm::Combinatorics qw(permutations); our ($VERSION, $debug, $AUTOLOAD); BEGIN { $debug = 0; $VERSION = '1.001'; } use overload '==' => \&RDF::Trine::Graph::_eq, 'eq' => \&RDF::Trine::Graph::_eq, 'le' => \&RDF::Trine::Graph::_le, 'ge' => \&RDF::Trine::Graph::_ge, 'lt' => \&RDF::Trine::Graph::_lt, 'gt' => \&RDF::Trine::Graph::_gt, ; sub _eq { my ($x, $y) = @_; return $x->equals($y); } sub _le { my ($x, $y) = @_; return $x->is_subgraph_of($y); } sub _ge { return _le(@_[1,0]); } sub _lt { my ($x, $y) = @_; # Test::More::diag(sprintf('%s // %s', ref($x), ref($y))); return ($x->size < $y->size) && ($x->is_subgraph_of($y)); } sub _gt { return _lt(@_[1,0]); } use Data::Dumper; use Log::Log4perl; use Scalar::Util qw(blessed); use RDF::Trine::Node; use RDF::Trine::Store; =item C<< new ( $model ) >> =item C<< new ( $iterator ) >> Returns a new graph from the given RDF::Trine::Model or RDF::Trine::Iterator::Graph object. =cut sub new { my $class = shift; unless (blessed($_[0])) { throw RDF::Trine::Error::MethodInvocationError -text => "RDF::Trine::Graph::new must be called with a Model or Iterator argument"; } my %data; if ($_[0]->isa('RDF::Trine::Iterator::Graph')) { my $iter = shift; my $model = RDF::Trine::Model->new( RDF::Trine::Store->temporary_store() ); while (my $st = $iter->next) { $model->add_statement( $st ); } $data{ model } = $model; } elsif ($_[0]->isa('RDF::Trine::Model')) { $data{ model } = shift; } else { throw RDF::Trine::Error::MethodInvocationError -text => "RDF::Trine::Graph::new must be called with a Model or Iterator argument"; } my $self = bless(\%data, $class); } =item C<< equals ( $graph ) >> Returns true if the invocant and $graph represent two equal RDF graphs (e.g. there exists a bijection between the RDF statements of the invocant and $graph). =cut sub equals { my $self = shift; my $graph = shift; undef($self->{error}); return $self->_check_equality($graph) ? 1 : 0; } sub _check_equality { my $self = shift; my $graph = shift; unless (blessed($graph) and $graph->isa('RDF::Trine::Graph')) { $self->{error} = "RDF::Trine::Graph::equals must be called with a Graph argument"; throw RDF::Trine::Error::MethodInvocationError -text => $self->{error}; } my @graphs = ($self, $graph); my ($ba, $nba) = $self->split_blank_statements; my ($bb, $nbb) = $graph->split_blank_statements; if (scalar(@$nba) != scalar(@$nbb)) { my $nbac = scalar(@$nba); my $nbbc = scalar(@$nbb); $self->{error} = "count of non-blank statements didn't match ($nbac != $nbbc)"; return 0; } my $bac = scalar(@$ba); my $bbc = scalar(@$bb); if ($bac != $bbc) { $self->{error} = "count of blank statements didn't match ($bac != $bbc)"; return 0; } for ($nba, $nbb) { @$_ = sort map { $_->as_string } @$_; } foreach my $i (0 .. $#{ $nba }) { unless ($nba->[$i] eq $nbb->[$i]) { $self->{error} = "non-blank triples don't match: " . Dumper($nba->[$i], $nbb->[$i]); return 0; } } return _find_mapping($self, $ba, $bb); } =item C<< is_subgraph_of ( $graph ) >> Returns true if the invocant is a subgraph of $graph. (i.e. there exists an injection of RDF statements from the invocant to $graph.) =cut sub is_subgraph_of { my $self = shift; my $graph = shift; undef($self->{error}); return $self->_check_subgraph($graph) ? 1 : 0; } =item C<< injection_map ( $graph ) >> If the invocant is a subgraph of $graph, returns a mapping of blank node identifiers from the invocant graph to $graph as a hashref. Otherwise returns false. The solution is not always unique; where there exist multiple solutions, the solution returned is arbitrary. =cut sub injection_map { my $self = shift; my $graph = shift; undef($self->{error}); my $map = $self->_check_subgraph($graph); return $map if $map; return; } sub _check_subgraph { my $self = shift; my $graph = shift; unless (blessed($graph) and $graph->isa('RDF::Trine::Graph')) { throw RDF::Trine::Error::MethodInvocationError -text => "RDF::Trine::Graph::equals must be called with a Graph argument"; } my @graphs = ($self, $graph); my ($ba, $nba) = $self->split_blank_statements; my ($bb, $nbb) = $graph->split_blank_statements; if (scalar(@$nba) > scalar(@$nbb)) { $self->{error} = "invocant had too many blank node statements to be a subgraph of argument"; return 0; } elsif (scalar(@$ba) > scalar(@$bb)) { $self->{error} = "invocant had too many non-blank node statements to be a subgraph of argument"; return 0; } my %NBB = map { $_->as_string => 1 } @$nbb; foreach my $st (@$nba) { unless ($NBB{ $st->as_string }) { return 0; } } return _find_mapping($self, $ba, $bb); } sub _find_mapping { my ($self, $ba, $bb) = @_; if (scalar(@$ba) == 0) { return {}; } my %blank_ids_a; foreach my $st (@$ba) { foreach my $n (grep { $_->isa('RDF::Trine::Node::Blank') } $st->nodes) { $blank_ids_a{ $n->blank_identifier }++; } } my %blank_ids_b; foreach my $st (@$bb) { foreach my $n (grep { $_->isa('RDF::Trine::Node::Blank') } $st->nodes) { $blank_ids_b{ $n->blank_identifier }++; } } my %bb_master = map { $_->as_string => 1 } @$bb; my @ka = keys %blank_ids_a; my @kb = keys %blank_ids_b; my $kbp = permutations( \@kb ); my $count = 0; MAPPING: while (my $mapping = $kbp->next) { my %mapping; @mapping{ @ka } = @$mapping; warn "trying mapping: " . Dumper(\%mapping) if ($debug); my %bb = %bb_master; foreach my $st (@$ba) { my @nodes; foreach my $method ($st->node_names) { my $n = $st->$method(); if ($n->isa('RDF::Trine::Node::Blank')) { my $id = $mapping{ $n->blank_identifier }; warn "mapping " . $n->blank_identifier . " to $id\n" if ($debug); push(@nodes, RDF::Trine::Node::Blank->new( $id )); } else { push(@nodes, $n); } } my $class = ref($st); my $mapped_st = $class->new( @nodes )->as_string; warn "checking for '$mapped_st' in " . Dumper(\%bb) if ($debug); if ($bb{ $mapped_st }) { delete $bb{ $mapped_st }; } else { next MAPPING; } } $self->{error} = "found mapping: " . Dumper(\%mapping) if ($debug); return \%mapping; } $self->{error} = "didn't find blank node mapping\n"; return 0; } =item C<< split_blank_statements >> Returns two array refs, containing triples with blank nodes and triples without any blank nodes, respectively. =cut sub split_blank_statements { my $self = shift; my $iter = $self->get_statements; my (@blanks, @nonblanks); while (my $st = $iter->next) { if ($st->has_blanks) { push(@blanks, $st); } else { push(@nonblanks, $st); } } return (\@blanks, \@nonblanks); } =item C<< get_statements >> Returns a RDF::Trine::Iterator::Graph object for the statements in this graph. =cut # The code below actually goes further now and makes RDF::Trine::Graph # into a subclass of RDF::Trine::Model via object delegation. This feature # is undocumented as it's not clear whether this is desirable or not. =begin private =item C<< isa >> =cut sub isa { my ($proto, $queried) = @_; $proto = ref($proto) if ref($proto); return UNIVERSAL::isa($proto, $queried) || RDF::Trine::Model->isa($queried); } =item C<< can >> =cut sub can { my ($proto, $queried) = @_; $proto = ref($proto) if ref($proto); return UNIVERSAL::can($proto, $queried) || RDF::Trine::Model->can($queried); } sub AUTOLOAD { my $self = shift; return if $AUTOLOAD =~ /::DESTROY$/; $AUTOLOAD =~ s/^(.+)::([^:]+)$/$2/; return $self->{model}->$AUTOLOAD(@_); } =end private =item C<< error >> Returns an error string explaining the last failed C<< equal >> call. =cut sub error { my $self = shift; return $self->{error}; } 1; __END__ =back =head1 BUGS Please report any bugs or feature requests to through the GitHub web interface at L. =head1 AUTHOR Gregory Todd Williams C<< >> =head1 COPYRIGHT Copyright (c) 2006-2012 Gregory Todd Williams. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. =cut