From: Tara L Andrews Date: Wed, 20 Jun 2012 20:17:23 +0000 (+0200) Subject: leave meta readings out of equiv graph; test for warnings on relationshipstore.t X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?p=scpubgit%2Fstemmatology.git;a=commitdiff_plain;h=56772e8c48876c670c0cdb26451f4c59401795ef leave meta readings out of equiv graph; test for warnings on relationshipstore.t --- diff --git a/lib/Text/Tradition/Collation.pm b/lib/Text/Tradition/Collation.pm index ef0594d..d500390 100644 --- a/lib/Text/Tradition/Collation.pm +++ b/lib/Text/Tradition/Collation.pm @@ -299,7 +299,10 @@ sub add_reading { $self->_add_reading( $reading->id => $reading ); # Once the reading has been added, put it in both graphs. $self->sequence->add_vertex( $reading->id ); - $self->relations->add_reading( $reading->id ); + # All meta readings save 'start' and 'end' get disregarded for relationships. + unless( $reading->is_nonrel ) { + $self->relations->add_reading( $reading->id ); + } return $reading; }; @@ -308,17 +311,19 @@ around del_reading => sub { my $self = shift; my $arg = shift; - if( ref( $arg ) eq 'Text::Tradition::Collation::Reading' ) { - $arg = $arg->id; + unless( ref( $arg ) eq 'Text::Tradition::Collation::Reading' ) { + $arg = $self->reading( $arg ) } + my $argid = $arg->id; # Remove the reading from the graphs. $self->_graphcalc_done(0); $self->_clear_cache; # Explicitly clear caches to GC the reading - $self->sequence->delete_vertex( $arg ); - $self->relations->delete_reading( $arg ); + $self->sequence->delete_vertex( $argid ); + $self->relations->delete_reading( $argid ) + unless $arg->is_nonrel; # Carry on. - $self->$orig( $arg ); + $self->$orig( $argid ); }; =begin testing @@ -369,9 +374,21 @@ is( $c->reading('n21p0')->text, 'unto', "Reading n21p0 merged correctly" ); sub merge_readings { my $self = shift; + # Sanity check + my( $kept_obj, $del_obj, $combine, $combine_char ) = $self->_objectify_args( @_ ); + my $mergemeta = $kept_obj->is_meta; + throw( "Cannot merge meta and non-meta reading" ) + unless ( $mergemeta && $del_obj->is_meta ) + || ( !$mergemeta && !$del_obj->is_meta ); + if( $mergemeta ) { + throw( "Cannot merge with start or end node" ) + if( $kept_obj eq $self->start || $kept_obj eq $self->end + || $del_obj eq $self->start || $del_obj eq $self->end ); + } # We only need the IDs for adding paths to the graph, not the reading # objects themselves. - my( $kept, $deleted, $combine, $combine_char ) = $self->_stringify_args( @_ ); + my $kept = $kept_obj->id; + my $deleted = $del_obj->id; $self->_graphcalc_done(0); # The kept reading should inherit the paths and the relationships @@ -387,12 +404,11 @@ sub merge_readings { @wits{keys %$fwits} = values %$fwits; $self->sequence->set_edge_attributes( @vector, \%wits ); } - $self->relations->merge_readings( $kept, $deleted, $combine ); + $self->relations->merge_readings( $kept, $deleted, $combine ) + unless $mergemeta; # Do the deletion deed. if( $combine ) { - my $kept_obj = $self->reading( $kept ); - my $del_obj = $self->reading( $deleted ); my $joinstr = $combine_char; unless( defined $joinstr ) { $joinstr = '' if $kept_obj->join_next || $del_obj->join_prior; @@ -430,7 +446,7 @@ sub add_path { # We only need the IDs for adding paths to the graph, not the reading # objects themselves. - my( $source, $target, $wit ) = $self->_stringify_args( @_ ); + my( $source, $target, $wit ) = $self->_objectify_args( @_ ); $self->_graphcalc_done(0); # Connect the readings @@ -1522,17 +1538,21 @@ sub calculate_ranks { # Do the rankings based on the relationship equivalence graph, starting # with the start node. - my $topo_start = $self->equivalence( $self->start->id ); - my $node_ranks = { $topo_start => 0 }; - my @curr_origin = ( $topo_start ); - # A little iterative function. - while( @curr_origin ) { - @curr_origin = _assign_rank( $self->equivalence_graph, - $node_ranks, @curr_origin ); - } + my ( $node_ranks, $rank_nodes ) = $self->relations->equivalence_ranks(); + # Transfer our rankings from the topological graph to the real one. foreach my $r ( $self->readings ) { - if( defined $node_ranks->{$self->equivalence( $r->id )} ) { + if( $r->is_nonrel ) { + # These are not in the equivalence graph. Grab the rank of the highest + # predecessor + 1. + my @preds = $self->sequence->predecessors( $r ); + my $mrank = 0; + map { my $rk = $node_ranks->{$self->equivalence( $_ )} + 1; + $mrank = $rk > $mrank ? $rk : $mrank; } + $self->sequence->predecessors( $r ); + throw( "All predecessors of $r unranked!" ) unless $mrank; + $r->rank( $mrank ); + } elsif( defined $node_ranks->{$self->equivalence( $r->id )} ) { $r->rank( $node_ranks->{$self->equivalence( $r->id )} ); } else { # Die. Find the last rank we calculated. @@ -1559,41 +1579,6 @@ sub calculate_ranks { $self->_graphcalc_done(1); } -sub _assign_rank { - my( $graph, $node_ranks, @current_nodes ) = @_; - # Look at each of the children of @current_nodes. If all the child's - # parents have a rank, assign it the highest rank + 1 and add it to - # @next_nodes. Otherwise skip it; we will return when the highest-ranked - # parent gets a rank. - my @next_nodes; - foreach my $c ( @current_nodes ) { - warn "Current reading $c has no rank!" - unless exists $node_ranks->{$c}; - # print STDERR "Looking at child of node $c, rank " - # . $node_ranks->{$c} . "\n"; - foreach my $child ( $graph->successors( $c ) ) { - next if exists $node_ranks->{$child}; - my $highest_rank = -1; - my $skip = 0; - foreach my $parent ( $graph->predecessors( $child ) ) { - if( exists $node_ranks->{$parent} ) { - $highest_rank = $node_ranks->{$parent} - if $highest_rank <= $node_ranks->{$parent}; - } else { - $skip = 1; - last; - } - } - next if $skip; - my $c_rank = $highest_rank + 1; - # print STDERR "Assigning rank $c_rank to node $child \n"; - $node_ranks->{$child} = $c_rank; - push( @next_nodes, $child ); - } - } - return @next_nodes; -} - sub _clear_cache { my $self = shift; $self->wipe_svg if $self->has_cached_svg; diff --git a/lib/Text/Tradition/Collation/Reading.pm b/lib/Text/Tradition/Collation/Reading.pm index 5b11f85..108548b 100644 --- a/lib/Text/Tradition/Collation/Reading.pm +++ b/lib/Text/Tradition/Collation/Reading.pm @@ -249,6 +249,17 @@ sub is_meta { return $self->is_start || $self->is_end || $self->is_lacuna || $self->is_ph; } +=head2 is_nonrel + +Similar to is_meta, but returns false for the start and end readings. + +=cut + +sub is_nonrel { + my $self = shift; + return $self->is_lacuna || $self->is_ph; +} + =head1 Convenience methods =head2 related_readings diff --git a/lib/Text/Tradition/Collation/RelationshipStore.pm b/lib/Text/Tradition/Collation/RelationshipStore.pm index 2a0703a..253486d 100644 --- a/lib/Text/Tradition/Collation/RelationshipStore.pm +++ b/lib/Text/Tradition/Collation/RelationshipStore.pm @@ -31,10 +31,10 @@ use_ok( 'Text::Tradition::Collation::RelationshipStore' ); my $cxfile = 't/data/Collatex-16.xml'; my $t = Text::Tradition->new( - 'name' => 'inline', - 'input' => 'CollateX', - 'file' => $cxfile, - ); + 'name' => 'inline', + 'input' => 'CollateX', + 'file' => $cxfile, + ); my $c = $t->collation; my @v1 = $c->add_relationship( 'n21', 'n22', { 'type' => 'lexical' } ); @@ -260,10 +260,16 @@ add_relationship. =begin testing +use Test::Warn; use Text::Tradition; use TryCatch; -my $t1 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' ); +my $t1; +warning_is { + $t1 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' ); +} 'DROPPING r14.2 -> r8.1: Cannot set relationship on a meta reading', + "Got expected relationship drop warning on parse"; + # Test 1.1: try to equate nodes that are prevented with an intermediate collation ok( $t1, "Parsed test fragment file" ); my $c1 = $t1->collation; @@ -307,7 +313,11 @@ try { # Test 2.1: try to equate nodes that are prevented with a real intermediate # equivalence -my $t2 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' ); +my $t2; +warning_is { + $t2 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' ); +} 'DROPPING r14.2 -> r8.1: Cannot set relationship on a meta reading', + "Got expected relationship drop warning on parse"; my $c2 = $t2->collation; $c2->add_relationship( 'r9.2', 'r9.3', { 'type' => 'lexical' } ); my $trel2 = $c2->get_relationship( 'r9.2', 'r9.3' ); @@ -566,10 +576,10 @@ sub relationship_valid { my $c = $self->collation; ## Assume validity is okay if we are initializing from scratch. return ( 1, "initializing" ) unless $c->tradition->_initialized; - - if ( $rel eq 'transposition' || $rel eq 'repetition' ) { + if ( $rel eq 'transposition' || $rel eq 'repetition' ) { # Check that the two readings do (for a repetition) or do not (for # a transposition) appear in the same witness. + # TODO this might be called before witness paths are set... my %seen_wits; map { $seen_wits{$_} = 1 } $c->reading_witnesses( $source ); foreach my $w ( $c->reading_witnesses( $target ) ) { @@ -752,7 +762,7 @@ sub merge_readings { $rel = $self->get_relationship( @$edge ); $self->_set_relationship( $rel, @vector ); } - $self->_make_equivalence( $deleted, $kept, 1 ); + $self->_make_equivalence( $deleted, $kept ); } ### Equivalence logic @@ -786,6 +796,7 @@ sub add_equivalence_edge { my( $self, $source, $target ) = @_; my $seq = $self->equivalence( $source ); my $teq = $self->equivalence( $target ); + return unless $seq && $teq; $self->equivalence_graph->add_edge( $seq, $teq ); } @@ -811,28 +822,22 @@ sub _is_disconnected { # Equate two readings in the equivalence graph sub _make_equivalence { - my( $self, $source, $target, $removing ) = @_; + my( $self, $source, $target ) = @_; # Get the source equivalent readings my $seq = $self->equivalence( $source ); my $teq = $self->equivalence( $target ); # Nothing to do if they are already equivalent... return if $seq eq $teq; - # Get the readings equivalent to source - my @sourcepool = @{$self->eqreadings( $seq )}; - # If we are removing the source reading entirely, don't push - # it into the target pool. - @sourcepool = grep { $_ ne $seq } @sourcepool if $removing; + my $sourcepool = $self->eqreadings( $seq ); # and add them to the target readings. - push( @{$self->eqreadings( $teq )}, @sourcepool ); - map { $self->set_equivalence( $_, $teq ) } @sourcepool; + push( @{$self->eqreadings( $teq )}, @$sourcepool ); + map { $self->set_equivalence( $_, $teq ) } @$sourcepool; # Then merge the nodes in the equivalence graph. foreach my $pred ( $self->equivalence_graph->predecessors( $seq ) ) { - $self->equivalence_graph->add_edge( $pred, $teq ) - unless $teq eq $pred; + $self->equivalence_graph->add_edge( $pred, $teq ); } foreach my $succ ( $self->equivalence_graph->successors( $seq ) ) { - $self->equivalence_graph->add_edge( $teq, $succ ) - unless $teq eq $succ; + $self->equivalence_graph->add_edge( $teq, $succ ); } $self->equivalence_graph->delete_vertex( $seq ); # TODO enable this after collation parsing is done @@ -1037,6 +1042,61 @@ sub rebuild_equivalence { } } +=head2 equivalence_ranks + +Rank all vertices in the equivalence graph, and return a hash reference with +vertex => rank mapping. + +=cut + +sub equivalence_ranks { + my $self = shift; + my $eqstart = $self->equivalence( $self->collation->start ); + my $eqranks = { $eqstart => 0 }; + my $rankeqs = { 0 => [ $eqstart ] }; + my @curr_origin = ( $eqstart ); + # A little iterative function. + while( @curr_origin ) { + @curr_origin = $self->_assign_rank( $eqranks, $rankeqs, @curr_origin ); + } + return( $eqranks, $rankeqs ); +} + +sub _assign_rank { + my( $self, $node_ranks, $rank_nodes, @current_nodes ) = @_; + my $graph = $self->equivalence_graph; + # Look at each of the children of @current_nodes. If all the child's + # parents have a rank, assign it the highest rank + 1 and add it to + # @next_nodes. Otherwise skip it; we will return when the highest-ranked + # parent gets a rank. + my @next_nodes; + foreach my $c ( @current_nodes ) { + warn "Current reading $c has no rank!" + unless exists $node_ranks->{$c}; + foreach my $child ( $graph->successors( $c ) ) { + next if exists $node_ranks->{$child}; + my $highest_rank = -1; + my $skip = 0; + foreach my $parent ( $graph->predecessors( $child ) ) { + if( exists $node_ranks->{$parent} ) { + $highest_rank = $node_ranks->{$parent} + if $highest_rank <= $node_ranks->{$parent}; + } else { + $skip = 1; + last; + } + } + next if $skip; + my $c_rank = $highest_rank + 1; + # print STDERR "Assigning rank $c_rank to node $child \n"; + $node_ranks->{$child} = $c_rank if $node_ranks; + push( @{$rank_nodes->{$c_rank}}, $child ) if $rank_nodes; + push( @next_nodes, $child ); + } + } + return @next_nodes; +} + ### Output logic sub _as_graphml { diff --git a/t/text_tradition_collation_relationshipstore.t b/t/text_tradition_collation_relationshipstore.t index 16724a6..16ec737 100644 --- a/t/text_tradition_collation_relationshipstore.t +++ b/t/text_tradition_collation_relationshipstore.t @@ -17,10 +17,10 @@ use_ok( 'Text::Tradition::Collation::RelationshipStore' ); my $cxfile = 't/data/Collatex-16.xml'; my $t = Text::Tradition->new( - 'name' => 'inline', - 'input' => 'CollateX', - 'file' => $cxfile, - ); + 'name' => 'inline', + 'input' => 'CollateX', + 'file' => $cxfile, + ); my $c = $t->collation; my @v1 = $c->add_relationship( 'n21', 'n22', { 'type' => 'lexical' } ); @@ -42,10 +42,16 @@ is( scalar @v3, 0, "Nothing deleted on non-existent relationship" ); # =begin testing { +use Test::Warn; use Text::Tradition; use TryCatch; -my $t1 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' ); +my $t1; +warning_is { + $t1 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' ); +} 'DROPPING r14.2 -> r8.1: Cannot set relationship on a meta reading', + "Got expected relationship drop warning on parse"; + # Test 1.1: try to equate nodes that are prevented with an intermediate collation ok( $t1, "Parsed test fragment file" ); my $c1 = $t1->collation; @@ -89,7 +95,11 @@ try { # Test 2.1: try to equate nodes that are prevented with a real intermediate # equivalence -my $t2 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' ); +my $t2; +warning_is { + $t2 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' ); +} 'DROPPING r14.2 -> r8.1: Cannot set relationship on a meta reading', + "Got expected relationship drop warning on parse"; my $c2 = $t2->collation; $c2->add_relationship( 'r9.2', 'r9.3', { 'type' => 'lexical' } ); my $trel2 = $c2->get_relationship( 'r9.2', 'r9.3' );