new library working with old graph functionality tests
Tara L Andrews [Mon, 18 Apr 2011 20:41:45 +0000 (22:41 +0200)]
lib/Traditions/Graph.pm
lib/Traditions/Graph/Position.pm [new file with mode: 0644]
lib/Traditions/Parser/GraphML.pm
t/graph.t

index 64042c9..6f0e0ba 100644 (file)
@@ -5,6 +5,65 @@ use warnings;
 use Graph::Easy;
 use IPC::Run qw( run binary );
 use Module::Load;
+use Traditions::Graph::Position;
+
+=head1 NAME
+
+(Text?)::Traditions::Graph
+
+=head1 SYNOPSIS
+
+use Traditions::Graph;
+
+my $text = Traditions::Graph->new( 'GraphML' => '/my/graphml/file.xml' );
+my $text = Traditions::Graph->new( 'TEI' => '/my/tei/file.xml' );
+my $text = Traditions::Graph->new( 'CSV' => '/my/csv/file.csv',
+                                   'base' => '/my/basefile.txt' );
+my $text = Traditions::Graph->new( 'CTE' => '/my/cte/file.txt',
+                                   'base' => '/my/basefile.txt' );
+
+my $svg_string = $text->as_svg();
+
+my $lemma_nodes = $text->active_nodes();
+$text->toggle_node( 'some_word' );
+
+=head1 DESCRIPTION
+
+A text tradition is the representation of our knowledge of a text that
+has been passed down via manuscript copies from a time before printing
+presses.  Each text has a number of witnesses, that is, manuscripts
+that bear a version of the text.  The tradition is the aggregation of
+these witnesses, which is to say, the collation of the text.
+
+This module takes a text collation and represents it as a horizontal
+directed graph, suitable for SVG rendering and for analysis of various
+forms.  Since this module was written by a medievalist, it also
+provides a facility for making a critical text reconstruction by
+choosing certain variants to be 'lemma' text - that is, text which
+should be considered the 'standard' reading.
+
+Although the graph is a very good way to render text collation, and is
+visually very easy for a human to interpret, it doesn't have any
+inherent information about which nodes 'go together' - that is, which
+text readings appear in the same place as other readings.  This module
+therefore calculates 'positions' on the graph, thus holding some
+information about which readings can and can't be substituted for
+others.
+
+=head1 METHODS
+
+=over 4
+
+=item B<new>
+
+Constructor.  Takes a source collation file from which to construct
+the initial graph.  This file can be TEI (parallel segmentation) XML,
+CSV in a format yet to be documented, GraphML as documented (someday)
+by CollateX, or a Classical Text Editor apparatus.  For CSV and
+Classical Text Editor files, the user must also supply a base text to
+which the line numbering in the collation file refers.
+
+=cut
 
 sub new {
     my $proto = shift;
@@ -107,9 +166,9 @@ sub start {
     return $self->{'graph'}->node('#START#');
 }
 
-sub save_positions {
-    my( $self, $positions ) = @_;
-    $self->{'positions'} = $positions;
+sub set_identical_nodes {
+    my( $self, $node_hash ) = @_;
+    $self->{'identical_nodes'} = $node_hash;
 }
 
 sub next_word {
@@ -208,43 +267,70 @@ sub as_svg {
     return $svg;
 }
 
-1;
-__END__
-#### EXAMINE BELOW ####
+## Methods for lemmatizing a text.
 
-# Returns a list of the nodes that are currently on and the nodes for
-# which an ellipsis needs to stand in.  Optionally takes a list of
-# nodes that have just been turned off, to include in the list.
+sub init_lemmatizer {
+    my $self = shift;
+    # Initialize the 'lemma' hash, going through all the nodes and seeing
+    # which ones are common nodes.  This should only be called once.
+
+    return if exists $self->{'lemma'};
+
+    my $lemma = {};
+    foreach my $node ( $self->nodes() ) {
+       my $state = $node->get_attribute('class') eq 'common' ? 1 : 0;
+       $lemma->{ $node->name() } = $state;
+    }
+
+    $self->{'lemma'} = $lemma;
+}
+
+sub make_positions {
+    my( $self, $common_nodes, $paths ) = @_;
+    my $positions = Traditions::Graph::Position->new( $common_nodes, $paths );
+    $self->{'positions'} = $positions;
+}
+
+# Takes a list of nodes that have just been turned off, and returns a
+# set of tuples of the form ['node', 'state'] that indicates what
+# changes need to be made to the graph.
+# A state of 1 means 'turn on this node'
+# A state of 0 means 'turn off this node'
+# A state of undef means 'an ellipsis belongs in the text here because
+#   no decision has been made'
 sub active_nodes {
     my( $self, @toggled_off_nodes ) = @_;
-    
-    my $all_nodes = {};
-    map { $all_nodes->{ $_ } = $self->_find_position( $_ ) } keys %{$self->{node_state}};
-    my $positions = _invert_hash( $all_nodes );
+
+    # In case this is the first run
+    $self->init_lemmatizer();
+    # First get the positions of those nodes which have been
+    # toggled off.
     my $positions_off = {};
-    map { $positions_off->{ $all_nodes->{$_} } = $_ } @toggled_off_nodes;
+    map { $positions_off->{ $self->{'positions'}->node_position( $_ ) } = $_ }
+             @toggled_off_nodes;
     
     # Now for each position, we have to see if a node is on, and we
     # have to see if a node has been turned off.
     my @answer;
-    foreach my $pos ( @{$self->{_all_positions}} ) {
-       my $nodes = $positions->{$pos};
-
+    foreach my $pos ( $self->{'positions'}->all() ) {
+       my @nodes = $self->{'positions'}->nodes_at_position( $pos );
+       
        # See if there is an active node for this position.
-       my @active_nodes = grep { $self->{node_state}->{$_} == 1 } @$nodes;
+       my @active_nodes = grep { $self->{'lemma'}->{$_} == 1 } @nodes;
        warn "More than one active node at position $pos!"
            unless scalar( @active_nodes ) < 2;
        my $active;
        if( scalar( @active_nodes ) ) {
-           $active = $self->node_to_svg( $active_nodes[0]  );
+           $active = $active_nodes[0] ;
        }
 
        # Is there a formerly active node that was toggled off?
        if( exists( $positions_off->{$pos} ) ) {
-           my $off_node = $self->node_to_svg( $positions_off->{$pos} );
+           my $off_node = $positions_off->{$pos};
            if( $active ) {
                push( @answer, [ $off_node, 0 ], [ $active, 1 ] );
-           } elsif ( scalar @$nodes == 1 ) {
+           } elsif ( scalar @nodes == 1 ) {
                # This was the only node at its position. No ellipsis.
                push( @answer, [ $off_node, 0 ] );
            } else {
@@ -260,173 +346,96 @@ sub active_nodes {
        } else {
            # There is no change here; we need an ellipsis. Use
            # the first node in the list, arbitrarily.
-           push( @answer, [ $self->node_to_svg( $nodes->[0] ), undef ] );
+           push( @answer, [ $nodes[0] , undef ] );
        }
     }
 
     return @answer;
 }
 
-# Compares two nodes according to their positions in the witness 
-# index hash.
-sub _by_position {
-    my $self = shift;
-    return _cmp_position( $self->_find_position( $a ), 
-                        $self->_find_position( $b ) );
-}
-
-# Takes two position strings (X,Y) and sorts them.
-sub _cmp_position {
-    my @pos_a = split(/,/, $a );
-    my @pos_b = split(/,/, $b );
-
-    my $big_cmp = $pos_a[0] <=> $pos_b[0];
-    return $big_cmp if $big_cmp;
-    # else 
-    return $pos_a[1] <=> $pos_b[1];
-}
-# Finds the position of a node in the witness index hash.  Warns if
-# the same node has non-identical positions across witnesses.  Quite
-# possibly should not warn.
-sub _find_position {
-    my $self = shift;
-    my $node = shift;
-
-    my $position;
-    foreach my $wit ( keys %{$self->{indices}} ) {
-       if( exists $self->{indices}->{$wit}->{$node} ) {
-           if( $position && $self->{indices}->{$wit}->{$node} ne $position ) {
-               warn "Position for node $node varies between witnesses";
-           }
-           $position = $self->{indices}->{$wit}->{$node};
-       }
-    }
+# A couple of helpers. TODO These should be gathered in the same place
+# eventually
 
-    warn "No position found for node $node" unless $position;
-    return $position;
+sub is_common {
+    my( $self, $node ) = @_;
+    $node = $self->_nodeobj( $node );
+    return $node->get_attribute('class') eq 'common';
 }
 
-sub _invert_hash {
-    my ( $hash, $plaintext_keys ) = @_;
-    my %new_hash;
-    foreach my $key ( keys %$hash ) {
-        my $val = $hash->{$key};
-        my $valkey = $val;
-        if( $plaintext_keys 
-            && ref( $val ) ) {
-            $valkey = $plaintext_keys->{ scalar( $val ) };
-            warn( "No plaintext value given for $val" ) unless $valkey;
-        }
-        if( exists ( $new_hash{$valkey} ) ) {
-            push( @{$new_hash{$valkey}}, $key );
-        } else {
-            $new_hash{$valkey} = [ $key ];
-        }
+sub _nodeobj {
+    my( $self, $node ) = @_;
+    unless( ref $node eq 'Graph::Easy::Node' ) {
+       $node = $self->node( $node );
     }
-    return \%new_hash;
+    return $node;
 }
 
+# toggle_node takes a node name, and either lemmatizes or de-lemmatizes it.
+# Returns a list of nodes that are de-lemmatized as a result of the toggle.
 
-# Takes a node ID to toggle; returns a list of nodes that are
-# turned OFF as a result.
 sub toggle_node {
-    my( $self, $node_id ) = @_;
-    $node_id = $self->node_from_svg( $node_id );
+    my( $self, $node ) = @_;
+    
+    # In case this is being called for the first time.
+    $self->init_lemmatizer();
 
-    # Is it a common node? If so, we don't want to turn it off.
-    # Later we might want to allow it off, but give a warning.
-    if( grep { $_ =~ /^$node_id$/ } @{$self->{common_nodes}} ) {
-       return ();
-    }
+    if( $self->is_common( $node ) ) {
+       # Do nothing, it's a common node.
+       return;
+    } 
 
     my @nodes_off;
     # If we are about to turn on a node...
-    if( !$self->{node_state}->{$node_id} ) {
+    if( !$self->{'lemma'}->{ $node } ) {
        # Turn on the node.
-       $self->{node_state}->{$node_id} = 1;
+       $self->{'lemma'}->{ $node } = 1;
        # Turn off any other 'on' nodes in the same position.
-       push( @nodes_off, $self->colocated_nodes( $node_id ) );
+       push( @nodes_off, $self->colocated_nodes( $node ) );
        # Turn off any node that is an identical transposed one.
-       push( @nodes_off, $self->identical_nodes( $node_id ) )
-           if $self->identical_nodes( $node_id );
+       push( @nodes_off, $self->identical_nodes( $node ) )
+           if $self->identical_nodes( $node );
     } else {
-       push( @nodes_off, $node_id );
+       push( @nodes_off, $node );
     }
+    @nodes_off = unique_list( @nodes_off );
 
     # Turn off the nodes that need to be turned off.
-    map { $self->{node_state}->{$_} = 0 } @nodes_off;
+    map { $self->{'lemma'}->{$_} = 0 } @nodes_off;
     return @nodes_off;
 }
 
-sub node_from_svg {
-    my( $self, $node_id ) = @_;
-    # TODO: implement this for real.  Need a mapping between SVG titles
-    # and GraphML IDs, as created in make_graphviz.
-    $node_id =~ s/^node_//;
-    return $node_id;
-}
-
-sub node_to_svg {
-    my( $self, $node_id ) = @_;
-    # TODO: implement this for real.  Need a mapping between SVG titles
-    # and GraphML IDs, as created in make_graphviz.
-    $node_id = "node_$node_id";
-    return $node_id;
-}
-
 sub colocated_nodes {
-    my( $self, $node ) = @_;
-    my @cl;
-
-    # Get the position of the stated node.
-    my $position;
-    foreach my $index ( values %{$self->{indices}} ) {
-       if( exists( $index->{$node} ) ) {
-           if( $position && $position ne $index->{$node} ) {
-               warn "Two ms positions for the same node!";
-           }
-           $position = $index->{$node};
-       }
-    }
-       
-    # Now find the other nodes in that position, if any.
-    foreach my $index ( values %{$self->{indices}} ) {
-       my %location = reverse( %$index );
-       push( @cl, $location{$position} )
-           if( exists $location{$position} 
-               && $location{$position} ne $node );
-    }
-    return @cl;
+    my $self = shift;
+    return $self->{'positions'}->colocated_nodes( @_ );
 }
 
 sub identical_nodes {
     my( $self, $node ) = @_;
-    return undef unless exists $self->{transpositions} &&
-       exists $self->{transpositions}->{$node};
-    return $self->{transpositions}->{$node};
+    return undef unless exists $self->{'identical_nodes'} &&
+       exists $self->{'identical_nodes'}->{$node};
+    return $self->{'identical_nodes'}->{$node};
+}
+
+sub text_of_node {
+    my( $self, $node_id ) = @_;
+    # This is the label of the given node.
+    return $self->node( $node_id )->label();
 }
 
 sub text_for_witness {
     my( $self, $wit ) = @_;
-    # Get the witness name
-    my %wit_id_for = reverse %{$self->{witnesses}};
-    my $wit_id = $wit_id_for{$wit};
-    unless( $wit_id ) {
-        warn "Could not find an ID for witness $wit";
-        return;
-    }
     
-    my $path = $self->{indices}->{$wit_id};
-    my @nodes = sort { $self->_cmp_position( $path->{$a}, $path->{$b} ) } keys( %$path );
-    my @words = map { $self->text_of_node( $_ ) } @nodes;
+    my @nodes = $self->{'positions'}->witness_path( $wit );
+    my @words = map { $self->node( $_ )->label() } @nodes;
     return join( ' ', @words );
 }
 
-sub text_of_node {
-    my( $self, $node_id ) = @_;
-    my $xpath = '//g:node[@id="' . $self->node_from_svg( $node_id) .
-        '"]/g:data[@key="' . $self->{nodedata}->{token} . '"]/child::text()';
-    return $self->{xpc}->findvalue( $xpath );
+sub unique_list {
+    my( @list ) = @_;
+    my %h;
+    map { $h{$_} = 1 } @list;
+    return keys( %h );
 }
+
 1;
+
diff --git a/lib/Traditions/Graph/Position.pm b/lib/Traditions/Graph/Position.pm
new file mode 100644 (file)
index 0000000..8011e39
--- /dev/null
@@ -0,0 +1,195 @@
+package Traditions::Graph::Position;
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+Traditions::Graph::Position
+
+=head1 SUMMARY
+
+An object to go with a text graph that keeps track of relative
+positions of the nodes.
+
+=head1 METHODS
+
+=over 4
+
+=item B<new>
+
+Takes two arguments: a list of names of common nodes in the graph, and
+a list of witness paths.  Calculates position identifiers for each
+node based on this.
+
+=cut
+
+sub new {
+    my $proto = shift;
+    my( $common_nodes, $witness_paths ) = @_;
+
+    my $self = {};
+
+    # We have to calculate the position identifiers for each word,
+    # keyed on the common nodes.  This will be 'fun'.  The end result
+    # is a hash per witness, whose key is the word node and whose
+    # value is its position in the text.  Common nodes are always N,1
+    # so have identical positions in each text.
+
+    my $node_pos = {};
+    foreach my $wit ( keys %$witness_paths ) {
+       # First we walk each path, making a matrix for each witness that
+       # corresponds to its eventual position identifier.  Common nodes
+       # always start a new row, and are thus always in the first column.
+
+       my $wit_matrix = [];
+       my $cn = 0;  # We should hit the common nodes in order.
+       my $row = [];
+       foreach my $wn ( @{$witness_paths->{$wit}} ) { # $wn is a node name
+           if( $wn eq $common_nodes->[$cn] ) {
+               # Set up to look for the next common node, and
+               # start a new row of words.
+               $cn++;
+               push( @$wit_matrix, $row ) if scalar( @$row );
+               $row = [];
+           }
+           push( @$row, $wn );
+       }
+       push( @$wit_matrix, $row );  # Push the last row onto the matrix
+
+       # Now we have a matrix per witness, so that each row in the
+       # matrix begins with a common node, and continues with all the
+       # variant words that appear in the witness.  We turn this into
+       # real positions in row,cell format.  But we need some
+       # trickery in order to make sure that each node gets assigned
+       # to only one position.
+
+       foreach my $li ( 1..scalar(@$wit_matrix) ) {
+           foreach my $di ( 1..scalar(@{$wit_matrix->[$li-1]}) ) {
+               my $node = $wit_matrix->[$li-1]->[$di-1];
+               my $position = "$li,$di";
+               # If we have seen this node before, we need to compare
+               # its position with what went before.
+               unless( exists $node_pos->{ $node } && 
+                       _cmp_position( $position, $node_pos->{ $node }) < 1 ) {
+                   # The new position ID replaces the old one.
+                   $node_pos->{$node} = $position;
+               } # otherwise, the old position needs to stay.
+           }
+       }
+    }
+    # Now we have a hash of node positions keyed on node.
+    $self->{'node_positions'} = $node_pos;
+    $self->{'witness_paths'} = $witness_paths;
+
+    bless( $self, $proto );
+    return $self;
+}
+
+sub node_position {
+    my( $self, $node ) = @_;
+    $node = _name( $node );
+
+    unless( exists( $self->{'node_positions'}->{ $node } ) ) {
+       warn "No node with name $node known to the graph";
+       return;
+    }
+
+    return $self->{'node_positions'}->{ $node };
+}
+
+sub nodes_at_position {
+    my( $self, $pos ) = @_;
+
+    my $positions = $self->calc_positions();
+    unless( exists $positions->{ $pos } ) {
+       warn "No position $pos in the graph";
+       return;
+    }
+    return @{ $positions->{ $pos }};
+}
+
+sub colocated_nodes {
+    my( $self, $node ) = @_;
+    $node = _name( $node );
+    my $pos = $self->node_position( $node );
+    my @loc_nodes = $self->nodes_at_position( $pos );
+
+    my @cn = grep { $_ !~ /^$node$/ } @loc_nodes;
+    return @cn;
+}
+
+sub all {
+    my( $self ) = @_;
+    my $pos = $self->calc_positions;
+    return sort by_position keys( %$pos );
+}
+
+sub witness_path {
+    my( $self, $wit ) = @_;
+    return @{$self->{'witness_paths'}->{ $wit }};
+}
+
+# At some point I may find myself using scalar references for the node
+# positions, in order to keep them easily in sync.  Just in case, I will
+# calculate this every time I need it.
+sub calc_positions {
+    my $self = shift;
+    return _invert_hash( $self->{'node_positions'} )
+}
+
+# Helper for dealing with node refs
+sub _name {
+    my( $node ) = @_;
+    # We work with node names in this library
+    if( ref( $node ) && ref( $node ) eq 'Graph::Easy::Node' ) {
+       $node = $node->name();
+    }
+    return $node;
+}
+
+### Comparison functions
+
+# Compares two nodes according to their positions in the witness 
+# index hash.
+sub by_position {
+    my $self = shift;
+    return _cmp_position( $a, $b );
+}
+
+# Takes two position strings (X,Y) and sorts them.
+sub _cmp_position {
+    my( $a, $b ) = @_;
+    my @pos_a = split(/,/, $a );
+    my @pos_b = split(/,/, $b );
+
+    my $big_cmp = $pos_a[0] <=> $pos_b[0];
+    return $big_cmp if $big_cmp;
+    # else 
+    return $pos_a[1] <=> $pos_b[1];
+}
+
+# Useful helper.  Will be especially useful if I find myself using
+# scalar references for the positions after all - it can dereference
+# them here.
+sub _invert_hash {
+    my ( $hash, $plaintext_keys ) = @_;
+    my %new_hash;
+    foreach my $key ( keys %$hash ) {
+        my $val = $hash->{$key};
+        my $valkey = $val;
+        if( $plaintext_keys 
+            && ref( $val ) ) {
+            $valkey = $plaintext_keys->{ scalar( $val ) };
+            warn( "No plaintext value given for $val" ) unless $valkey;
+        }
+        if( exists ( $new_hash{$valkey} ) ) {
+            push( @{$new_hash{$valkey}}, $key );
+        } else {
+            $new_hash{$valkey} = [ $key ];
+        }
+    }
+    return \%new_hash;
+}
+
+1;
index 5915505..3656eb9 100644 (file)
@@ -75,7 +75,19 @@ sub parse {
 
     ## Reverse the node_name hash so that we have two-way lookup.
     my %node_id = reverse %node_name;
-    ## TODO mark transpositions somehow.
+
+    ## Record the nodes that are marked as transposed.
+    my $id_xpath = '//g:node[g:data[@key="' . $nodedata{'identity'} . '"]]';
+    my $transposed_nodes = $xpc->find( $id_xpath );
+    my $identical_nodes;
+    foreach my $tn ( @$transposed_nodes ) {
+        $identical_nodes->{ $node_name{ $tn->getAttribute('id') }} = 
+            $node_name{ $xpc->findvalue( './g:data[@key="' 
+                                        . $nodedata{'identity'} 
+                                        . '"]/text()', $tn ) };
+    }
+    $graph->set_identical_nodes( $identical_nodes );
+
 
     # Find the beginning and end nodes of the graph.  The beginning node
     # has no incoming edges; the end node has no outgoing edges.
@@ -151,38 +163,9 @@ sub parse {
            unless $n->get_attribute( 'class' ) eq 'common';
     }
 
-    # And then we have to calculate the position identifiers for
-    # each word, keyed on the common nodes.  This will be 'fun'.
-    # The end result is a hash per witness, whose key is the word
-    # node and whose value is its position in the text.  Common 
-    # nodes are always N,1 so have identical positions in each text.
-    my $wit_indices = {};
-    my $positions = {};
-    foreach my $wit ( values %witnesses ) {
-       my $wit_matrix = [];
-       my $cn = 0;
-       my $row = [];
-       foreach my $wn ( @{$paths->{$wit}} ) {
-           if( $wn eq $common_nodes[$cn] ) {
-               $cn++;
-               push( @$wit_matrix, $row ) if scalar( @$row );
-               $row = [];
-           }
-           push( @$row, $wn );
-       }
-       push( @$wit_matrix, $row );
-       # Now we have a matrix; we really want to invert this.
-       my $wit_index;
-       foreach my $li ( 1..scalar(@$wit_matrix) ) {
-           foreach my $di ( 1..scalar(@{$wit_matrix->[$li-1]}) ) {
-               $wit_index->{ $wit_matrix->[$li-1]->[$di-1] } = "$li,$di";
-               $positions->{ "$li,$di" } = 1;
-           }
-       }
-       
-       $wit_indices->{$wit} = $wit_index;
-    }
-    $graph->save_positions( $positions, $wit_indices );
+    # Now calculate graph positions.
+    $graph->make_positions( \@common_nodes, $paths );
+
 }
     
 1;
index 76e9871..21418c4 100644 (file)
--- a/t/graph.t
+++ b/t/graph.t
@@ -3,7 +3,7 @@
 use strict; use warnings;
 use Test::More;
 use lib 'lib';
-use lemmatizer::Model::Graph;
+use Traditions::Graph;
 use XML::LibXML;
 use XML::LibXML::XPathContext;
 
@@ -12,7 +12,7 @@ my $datafile = 't/data/Collatex-16.xml';
 open( GRAPHFILE, $datafile ) or die "Could not open $datafile";
 my @lines = <GRAPHFILE>;
 close GRAPHFILE;
-my $graph = lemmatizer::Model::Graph->new( 'xml' => join( '', @lines ) );
+my $graph = Traditions::Graph->new( 'GraphML' => join( '', @lines ) );
 
 # Test the svg creation
 my $parser = XML::LibXML->new();
@@ -31,7 +31,7 @@ my @svg_edges = $svg_xpc->findnodes( '//svg:g[@class="edge"]' );
 is( scalar @svg_edges, 27, "Correct number of edges in the graph" );
 
 # Test for the correct common nodes
-my @expected_nodes = map { [ "node_$_", 1 ] } qw/0 1 8 12 13 16 19 20 23 27/;
+my @expected_nodes = map { [ $_, 1 ] } qw/#START# 1 8 12 13 16 19 20 23 27/;
 foreach my $idx ( qw/2 3 5 8 10 13 15/ ) {
     splice( @expected_nodes, $idx, 0, [ "node_null", undef ] );
 }
@@ -83,20 +83,20 @@ my $transposed_nodes = { 2 => 9,
                         17 => 15,
                         18 => 14
 };
-is_deeply( $graph->{transpositions}, $transposed_nodes, "Found the right transpositions" );
+is_deeply( $graph->{'identical_nodes'}, $transposed_nodes, "Found the right transpositions" );
 
 # Test turning on a node
-my @off = $graph->toggle_node( 'node_24' );
-$expected_nodes[ 15 ] = [ "node_24", 1 ];
-splice( @expected_nodes, 15, 1, ( [ "node_26", 0 ], [ "node_24", 1 ] ) );
+my @off = $graph->toggle_node( '24' );
+$expected_nodes[ 15 ] = [ "24", 1 ];
+splice( @expected_nodes, 15, 1, ( [ "26", 0 ], [ "24", 1 ] ) );
 @active_nodes = $graph->active_nodes( @off );
 subtest 'Turned on node for new location' => \&compare_active;
 $string = '# when ... ... showers sweet with ... fruit the ... of ... has pierced ... the root #';
 is( make_text( @active_nodes ), $string, "Got the right text" );
  
 # Test the toggling effects of same-column
-@off = $graph->toggle_node( 'node_26' );
-splice( @expected_nodes, 15, 2, ( [ "node_24", 0 ], [ "node_26", 1 ] ) );
+@off = $graph->toggle_node( '26' );
+splice( @expected_nodes, 15, 2, ( [ "24", 0 ], [ "26", 1 ] ) );
 @active_nodes = $graph->active_nodes( @off );
 subtest 'Turned on other node in that location' => \&compare_active;
 $string = '# when ... ... showers sweet with ... fruit the ... of ... has pierced ... the rood #';
@@ -104,11 +104,11 @@ is( make_text( @active_nodes ), $string, "Got the right text" );
 
 # Test the toggling effects of transposition
 
-@off = $graph->toggle_node( 'node_14' );
+@off = $graph->toggle_node( '14' );
 # Add the turned on node
-splice( @expected_nodes, 8, 1, ( [ "node_15", 0 ], [ "node_14", 1 ] ) );
+splice( @expected_nodes, 8, 1, ( [ "15", 0 ], [ "14", 1 ] ) );
 # Add the off transposition node
-splice( @expected_nodes, 11, 1, [ "node_18", undef ] );
+splice( @expected_nodes, 11, 1, [ "18", undef ] );
 # Remove the explicit turning off of the earlier node
 splice( @expected_nodes, 16, 1 );
 @active_nodes = $graph->active_nodes( @off );
@@ -116,32 +116,32 @@ subtest 'Turned on transposition node' => \&compare_active;
 $string = '# when ... ... showers sweet with ... fruit the drought of ... has pierced ... the rood #';
 is( make_text( @active_nodes ), $string, "Got the right text" );
 
-@off = $graph->toggle_node( 'node_18' );
-splice( @expected_nodes, 8, 2, [ "node_14", undef ] );
-splice( @expected_nodes, 10, 1, ( [ "node_17", 0 ], [ "node_18", 1 ] ) );
+@off = $graph->toggle_node( '18' );
+splice( @expected_nodes, 8, 2, [ "14", undef ] );
+splice( @expected_nodes, 10, 1, ( [ "17", 0 ], [ "18", 1 ] ) );
 @active_nodes = $graph->active_nodes( @off );
 subtest 'Turned on that node\'s partner' => \&compare_active;
 $string = '# when ... ... showers sweet with ... fruit the ... of drought has pierced ... the rood #';
 is( make_text( @active_nodes ), $string, "Got the right text" );
 
-@off = $graph->toggle_node( 'node_14' );
-splice( @expected_nodes, 8, 1, [ "node_15", 0 ], [ "node_14", 1 ] );
-splice( @expected_nodes, 11, 2, ( [ "node_18", undef ] ) );
+@off = $graph->toggle_node( '14' );
+splice( @expected_nodes, 8, 1, [ "15", 0 ], [ "14", 1 ] );
+splice( @expected_nodes, 11, 2, ( [ "18", undef ] ) );
 @active_nodes = $graph->active_nodes( @off );
 subtest 'Turned on the original node' => \&compare_active;
 $string = '# when ... ... showers sweet with ... fruit the drought of ... has pierced ... the rood #';
 is( make_text( @active_nodes ), $string, "Got the right text" );
 
-@off = $graph->toggle_node( 'node_3' );
-splice( @expected_nodes, 3, 1, [ "node_3", 1 ] );
+@off = $graph->toggle_node( '3' );
+splice( @expected_nodes, 3, 1, [ "3", 1 ] );
 splice( @expected_nodes, 8, 1 );
 @active_nodes = $graph->active_nodes( @off );
 subtest 'Turned on a singleton node' => \&compare_active;
 $string = '# when ... with his showers sweet with ... fruit the drought of ... has pierced ... the rood #';
 is( make_text( @active_nodes ), $string, "Got the right text" );
 
-@off = $graph->toggle_node( 'node_3' );
-splice( @expected_nodes, 3, 1, [ "node_3", 0 ] );
+@off = $graph->toggle_node( '3' );
+splice( @expected_nodes, 3, 1, [ "3", 0 ] );
 @active_nodes = $graph->active_nodes( @off );
 subtest 'Turned off a singleton node' => \&compare_active;
 $string = '# when ... showers sweet with ... fruit the drought of ... has pierced ... the rood #';