tested on all existing traditions, fixed bugs that arose
[scpubgit/stemmatology.git] / base / lib / Text / Tradition / Collation / RelationshipStore.pm
CommitLineData
22222af9 1package Text::Tradition::Collation::RelationshipStore;
2
3use strict;
4use warnings;
24efa55d 5use Safe::Isa;
63778331 6use Text::Tradition::Error;
22222af9 7use Text::Tradition::Collation::Relationship;
24efa55d 8use Text::Tradition::Collation::RelationshipType;
a1615ee4 9use TryCatch;
22222af9 10
11use Moose;
12
13=head1 NAME
14
2626f709 15Text::Tradition::Collation::RelationshipStore - Keeps track of the relationships
16between readings in a given collation
22222af9 17
18=head1 DESCRIPTION
19
20Text::Tradition is a library for representation and analysis of collated
21texts, particularly medieval ones. The RelationshipStore is an internal object
22of the collation, to keep track of the defined relationships (both specific and
23general) between readings.
24
3ae5e2ad 25=begin testing
26
27use Text::Tradition;
ee801e17 28use TryCatch;
3ae5e2ad 29
30use_ok( 'Text::Tradition::Collation::RelationshipStore' );
31
ee801e17 32# Add some relationships, and delete them
33
34my $cxfile = 't/data/Collatex-16.xml';
35my $t = Text::Tradition->new(
56772e8c 36 'name' => 'inline',
37 'input' => 'CollateX',
38 'file' => $cxfile,
39 );
ee801e17 40my $c = $t->collation;
41
f8331a4d 42my @v1 = $c->add_relationship( 'n21', 'n22', { 'type' => 'lexical' } );
ee801e17 43is( scalar @v1, 1, "Added a single relationship" );
44is( $v1[0]->[0], 'n21', "Got correct node 1" );
45is( $v1[0]->[1], 'n22', "Got correct node 2" );
679f17e1 46my @v2 = $c->add_relationship( 'n24', 'n23',
ee801e17 47 { 'type' => 'spelling', 'scope' => 'global' } );
48is( scalar @v2, 2, "Added a global relationship with two instances" );
49@v1 = $c->del_relationship( 'n22', 'n21' );
50is( scalar @v1, 1, "Deleted first relationship" );
679f17e1 51@v2 = $c->del_relationship( 'n12', 'n13' );
ee801e17 52is( scalar @v2, 2, "Deleted second global relationship" );
681893aa 53my @v3 = $c->del_relationship( 'n1', 'n2' );
54is( scalar @v3, 0, "Nothing deleted on non-existent relationship" );
ee801e17 55
3ae5e2ad 56=end testing
57
22222af9 58=head1 METHODS
59
60=head2 new( collation => $collation );
61
62Creates a new relationship store for the given collation.
63
64=cut
65
66has 'collation' => (
67 is => 'ro',
68 isa => 'Text::Tradition::Collation',
69 required => 1,
70 weak_ref => 1,
71 );
24efa55d 72
73=head2 types
74
75Registry of possible relationship types. See RelationshipType for more info.
76
77=cut
78
79has 'relationship_types' => (
80 is => 'ro',
81 traits => ['Hash'],
82 handles => {
83 has_type => 'exists',
84 add_type => 'set',
85 type => 'get',
86 del_type => 'delete'
87 },
88 );
22222af9 89
90has 'scopedrels' => (
91 is => 'ro',
92 isa => 'HashRef[HashRef[Text::Tradition::Collation::Relationship]]',
93 default => sub { {} },
94 );
95
96has 'graph' => (
97 is => 'ro',
98 isa => 'Graph',
99 default => sub { Graph->new( undirected => 1 ) },
100 handles => {
101 relationships => 'edges',
102 add_reading => 'add_vertex',
103 delete_reading => 'delete_vertex',
24efa55d 104 },
22222af9 105 );
106
359944f7 107=head2 equivalence_graph()
108
109Returns an equivalence graph of the collation, in which all readings
110related via a 'colocated' relationship are transformed into a single
111vertex. Can be used to determine the validity of a new relationship.
112
113=cut
114
115has 'equivalence_graph' => (
116 is => 'ro',
117 isa => 'Graph',
118 default => sub { Graph->new() },
e1083e99 119 writer => '_reset_equivalence',
359944f7 120 );
121
122has '_node_equivalences' => (
123 is => 'ro',
124 traits => ['Hash'],
125 handles => {
126 equivalence => 'get',
127 set_equivalence => 'set',
128 remove_equivalence => 'delete',
04482188 129 _clear_equivalence => 'clear',
24efa55d 130 },
359944f7 131 );
132
133has '_equivalence_readings' => (
134 is => 'ro',
135 traits => ['Hash'],
136 handles => {
137 eqreadings => 'get',
138 set_eqreadings => 'set',
139 remove_eqreadings => 'delete',
04482188 140 _clear_eqreadings => 'clear',
24efa55d 141 },
359944f7 142 );
143
24efa55d 144## Build function - here we have our default set of relationship types.
145
146sub BUILD {
147 my $self = shift;
148
24efa55d 149 my @DEFAULT_TYPES = (
150 { name => 'collated', bindlevel => 50, is_weak => 1, is_transitive => 0, is_generalizable => 0 },
c7bd2768 151 { name => 'orthographic', bindlevel => 0, use_regular => 0 },
152 { name => 'spelling', bindlevel => 1 },
153 { name => 'punctuation', bindlevel => 2 },
154 { name => 'grammatical', bindlevel => 2 },
155 { name => 'lexical', bindlevel => 2 },
156 { name => 'uncertain', bindlevel => 50, is_transitive => 0, is_generalizable => 0 },
157 { name => 'other', bindlevel => 50, is_transitive => 0, is_generalizable => 0 },
9e9b7540 158 { name => 'transposition', bindlevel => 50, is_colocation => 0 },
24efa55d 159 { name => 'repetition', bindlevel => 50, is_colocation => 0, is_transitive => 0 }
160 );
161
162 foreach my $type ( @DEFAULT_TYPES ) {
163 $self->add_type( $type );
164 }
165}
166
24efa55d 167around add_type => sub {
168 my $orig = shift;
169 my $self = shift;
170 my $new_type;
171 if( @_ == 1 && $_[0]->$_isa( 'Text::Tradition::Collation::RelationshipType' ) ) {
172 $new_type = shift;
173 } else {
174 my %args = @_ == 1 ? %{$_[0]} : @_;
175 $new_type = Text::Tradition::Collation::RelationshipType->new( %args );
176 }
177 $self->$orig( $new_type->name => $new_type );
178 return $new_type;
179};
180
359944f7 181around add_reading => sub {
182 my $orig = shift;
183 my $self = shift;
184
185 $self->equivalence_graph->add_vertex( @_ );
186 $self->set_equivalence( $_[0], $_[0] );
187 $self->set_eqreadings( $_[0], [ $_[0] ] );
188 $self->$orig( @_ );
189};
190
191around delete_reading => sub {
192 my $orig = shift;
193 my $self = shift;
194
359944f7 195 $self->_remove_equivalence_node( @_ );
196 $self->$orig( @_ );
197};
198
3ae5e2ad 199=head2 get_relationship
200
201Return the relationship object, if any, that exists between two readings.
202
203=cut
204
205sub get_relationship {
4633f9e4 206 my $self = shift;
207 my @vector;
208 if( @_ == 1 && ref( $_[0] ) eq 'ARRAY' ) {
209 # Dereference the edge arrayref that was passed.
210 my $edge = shift;
211 @vector = @$edge;
212 } else {
213 @vector = @_;
214 }
3ae5e2ad 215 my $relationship;
216 if( $self->graph->has_edge_attribute( @vector, 'object' ) ) {
217 $relationship = $self->graph->get_edge_attribute( @vector, 'object' );
ca6e6095 218 }
3ae5e2ad 219 return $relationship;
220}
221
222sub _set_relationship {
223 my( $self, $relationship, @vector ) = @_;
224 $self->graph->add_edge( @vector );
225 $self->graph->set_edge_attribute( @vector, 'object', $relationship );
176badfe 226 $self->_make_equivalence( @vector ) if $relationship->colocated;
3ae5e2ad 227}
a1615ee4 228
22222af9 229=head2 create
230
231Create a new relationship with the given options and return it.
232Warn and return undef if the relationship cannot be created.
233
234=cut
235
236sub create {
237 my( $self, $options ) = @_;
238 # Check to see if a relationship exists between the two given readings
239 my $source = delete $options->{'orig_a'};
240 my $target = delete $options->{'orig_b'};
3ae5e2ad 241 my $rel = $self->get_relationship( $source, $target );
242 if( $rel ) {
24efa55d 243 if( $self->type( $rel->type )->is_weak ) {
244 # Always replace a weak relationship with a more descriptive
3d14b48e 245 # one, if asked.
246 $self->del_relationship( $source, $target );
247 } elsif( $rel->type ne $options->{'type'} ) {
63778331 248 throw( "Another relationship of type " . $rel->type
249 . " already exists between $source and $target" );
22222af9 250 } else {
251 return $rel;
252 }
253 }
254
99ab9535 255 $rel = Text::Tradition::Collation::Relationship->new( $options );
24efa55d 256 my $reltype = $self->type( $rel->type );
c7bd2768 257 throw( "Unrecognized relationship type " . $rel->type ) unless $reltype;
24efa55d 258 # Validate the options given against the relationship type wanted
259 throw( "Cannot set nonlocal scope on relationship of type " . $reltype->name )
260 if $rel->nonlocal && !$reltype->is_generalizable;
261
99ab9535 262 $self->add_scoped_relationship( $rel ) if $rel->nonlocal;
263 return $rel;
22222af9 264}
265
266=head2 add_scoped_relationship( $rel )
267
268Keep track of relationships defined between specific readings that are scoped
269non-locally. Key on whichever reading occurs first alphabetically.
270
271=cut
272
273sub add_scoped_relationship {
274 my( $self, $rel ) = @_;
24efa55d 275 my $rdga = $rel->reading_a;
276 my $rdgb = $rel->reading_b;
f222800e 277 my $r = $self->scoped_relationship( $rdga, $rdgb );
22222af9 278 if( $r ) {
279 warn sprintf( "Scoped relationship of type %s already exists between %s and %s",
f222800e 280 $r->type, $rdga, $rdgb );
22222af9 281 return;
282 }
f222800e 283 my( $first, $second ) = sort ( $rdga, $rdgb );
284 $self->scopedrels->{$first}->{$second} = $rel;
22222af9 285}
286
287=head2 scoped_relationship( $reading_a, $reading_b )
288
289Returns the general (document-level or global) relationship that has been defined
290between the two reading strings. Returns undef if there is no general relationship.
291
292=cut
293
294sub scoped_relationship {
295 my( $self, $rdga, $rdgb ) = @_;
296 my( $first, $second ) = sort( $rdga, $rdgb );
297 if( exists $self->scopedrels->{$first}->{$second} ) {
298 return $self->scopedrels->{$first}->{$second};
24efa55d 299 }
300 return undef;
22222af9 301}
302
303=head2 add_relationship( $self, $source, $sourcetext, $target, $targettext, $opts )
304
305Adds the relationship specified in $opts (see Text::Tradition::Collation::Relationship
306for the possible options) between the readings given in $source and $target. Sets
307up a scoped relationship between $sourcetext and $targettext if the relationship is
308scoped non-locally.
309
310Returns a status boolean and a list of all reading pairs connected by the call to
311add_relationship.
312
6d381462 313=begin testing
314
56772e8c 315use Test::Warn;
6d381462 316use Text::Tradition;
317use TryCatch;
318
56772e8c 319my $t1;
e92d4229 320warnings_exist {
56772e8c 321 $t1 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' );
e92d4229 322} [qr/Cannot set relationship on a meta reading/],
56772e8c 323 "Got expected relationship drop warning on parse";
324
176badfe 325# Test 1.1: try to equate nodes that are prevented with an intermediate collation
6d381462 326ok( $t1, "Parsed test fragment file" );
327my $c1 = $t1->collation;
10e4b1ac 328my $trel = $c1->get_relationship( 'r9.2', 'r9.3' );
6d381462 329is( ref( $trel ), 'Text::Tradition::Collation::Relationship',
330 "Troublesome relationship exists" );
331is( $trel->type, 'collated', "Troublesome relationship is a collation" );
332
333# Try to make the link we want
334try {
10e4b1ac 335 $c1->add_relationship( 'r8.6', 'r10.3', { 'type' => 'orthographic' } );
6d381462 336 ok( 1, "Added cross-collation relationship as expected" );
176badfe 337} catch( Text::Tradition::Error $e ) {
338 ok( 0, "Existing collation blocked equivalence relationship: " . $e->message );
6d381462 339}
340
341try {
342 $c1->calculate_ranks();
343 ok( 1, "Successfully calculated ranks" );
176badfe 344} catch ( Text::Tradition::Error $e ) {
345 ok( 0, "Collation now has a cycle: " . $e->message );
6d381462 346}
347
176badfe 348# Test 1.2: attempt merge of an identical reading
359944f7 349try {
10e4b1ac 350 $c1->merge_readings( 'r9.3', 'r11.5' );
359944f7 351 ok( 1, "Successfully merged reading 'pontifex'" );
352} catch ( Text::Tradition::Error $e ) {
353 ok( 0, "Merge of mergeable readings failed: $e->message" );
354
355}
356
176badfe 357# Test 1.3: attempt relationship with a meta reading (should fail)
358try {
10e4b1ac 359 $c1->add_relationship( 'r8.1', 'r9.2', { 'type' => 'collated' } );
176badfe 360 ok( 0, "Allowed a meta-reading to be used in a relationship" );
361} catch ( Text::Tradition::Error $e ) {
362 is( $e->message, 'Cannot set relationship on a meta reading',
363 "Relationship link prevented for a meta reading" );
364}
365
beb47b16 366# Test 1.4: try to break a relationship near a meta reading
367$c1->add_relationship( 'r7.6', 'r7.3', { type => 'orthographic' } );
368try {
369 $c1->del_relationship( 'r7.6', 'r7.7' );
370 $c1->del_relationship( 'r7.6', 'r7.3' );
371 ok( 1, "Relationship broken with a meta reading as neighbor" );
372} catch {
373 ok( 0, "Relationship deletion failed with a meta reading as neighbor" );
374}
375
176badfe 376# Test 2.1: try to equate nodes that are prevented with a real intermediate
6d381462 377# equivalence
56772e8c 378my $t2;
e92d4229 379warnings_exist {
56772e8c 380 $t2 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/legendfrag.xml' );
e92d4229 381} [qr/Cannot set relationship on a meta reading/],
56772e8c 382 "Got expected relationship drop warning on parse";
6d381462 383my $c2 = $t2->collation;
10e4b1ac 384$c2->add_relationship( 'r9.2', 'r9.3', { 'type' => 'lexical' } );
385my $trel2 = $c2->get_relationship( 'r9.2', 'r9.3' );
6d381462 386is( ref( $trel2 ), 'Text::Tradition::Collation::Relationship',
387 "Created blocking relationship" );
388is( $trel2->type, 'lexical', "Blocking relationship is not a collation" );
389# This time the link ought to fail
390try {
10e4b1ac 391 $c2->add_relationship( 'r8.6', 'r10.3', { 'type' => 'orthographic' } );
414cc046 392 ok( 0, "Added cross-equivalent bad relationship" );
176badfe 393} catch ( Text::Tradition::Error $e ) {
394 like( $e->message, qr/witness loop/,
395 "Existing equivalence blocked crossing relationship" );
6d381462 396}
397
398try {
399 $c2->calculate_ranks();
400 ok( 1, "Successfully calculated ranks" );
176badfe 401} catch ( Text::Tradition::Error $e ) {
402 ok( 0, "Collation now has a cycle: " . $e->message );
6d381462 403}
404
176badfe 405# Test 3.1: make a straightforward pair of transpositions.
cc31ebaa 406my $t3 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/lf2.xml' );
407# Test 1: try to equate nodes that are prevented with an intermediate collation
408my $c3 = $t3->collation;
409try {
10e4b1ac 410 $c3->add_relationship( 'r36.4', 'r38.3', { 'type' => 'transposition' } );
cc31ebaa 411 ok( 1, "Added straightforward transposition" );
176badfe 412} catch ( Text::Tradition::Error $e ) {
413 ok( 0, "Failed to add normal transposition: " . $e->message );
cc31ebaa 414}
415try {
10e4b1ac 416 $c3->add_relationship( 'r36.3', 'r38.2', { 'type' => 'transposition' } );
cc31ebaa 417 ok( 1, "Added straightforward transposition complement" );
176badfe 418} catch ( Text::Tradition::Error $e ) {
419 ok( 0, "Failed to add normal transposition complement: " . $e->message );
cc31ebaa 420}
421
176badfe 422# Test 3.2: try to make a transposition that could be a parallel.
cc31ebaa 423try {
10e4b1ac 424 $c3->add_relationship( 'r28.2', 'r29.2', { 'type' => 'transposition' } );
cc31ebaa 425 ok( 0, "Added bad colocated transposition" );
176badfe 426} catch ( Text::Tradition::Error $e ) {
427 like( $e->message, qr/Readings appear to be colocated/,
428 "Prevented bad colocated transposition" );
cc31ebaa 429}
430
176badfe 431# Test 3.3: make the parallel, and then make the transposition again.
cc31ebaa 432try {
10e4b1ac 433 $c3->add_relationship( 'r28.3', 'r29.3', { 'type' => 'orthographic' } );
cc31ebaa 434 ok( 1, "Equated identical readings for transposition" );
176badfe 435} catch ( Text::Tradition::Error $e ) {
436 ok( 0, "Failed to equate identical readings: " . $e->message );
cc31ebaa 437}
438try {
10e4b1ac 439 $c3->add_relationship( 'r28.2', 'r29.2', { 'type' => 'transposition' } );
cc31ebaa 440 ok( 1, "Added straightforward transposition complement" );
176badfe 441} catch ( Text::Tradition::Error $e ) {
442 ok( 0, "Failed to add normal transposition complement: " . $e->message );
cc31ebaa 443}
444
9e9b7540 445# Test 4: make a global relationship that involves re-ranking a node first, when
c7bd2768 446# the prior rank has a potential match too
447my $t4 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/globalrel_test.xml' );
98a66507 448my $c4 = $t4->collation;
449# Can we even add the relationship?
450try {
451 $c4->add_relationship( 'r463.2', 'r463.4',
452 { type => 'orthographic', scope => 'global' } );
453 ok( 1, "Added global relationship without error" );
454} catch ( Text::Tradition::Error $e ) {
455 ok( 0, "Failed to add global relationship when same-rank alternative exists: "
456 . $e->message );
457}
458$c4->calculate_ranks();
459# Do our readings now share a rank?
460is( $c4->reading('r463.2')->rank, $c4->reading('r463.4')->rank,
461 "Expected readings now at same rank" );
9e9b7540 462
463# Test group 5: relationship transitivity.
464my $t5 = Text::Tradition->new( 'input' => 'Self', 'file' => 't/data/john.xml' );
465my $c5 = $t5->collation;
466
467# Test 5.1: make a grammatical link to an orthographically-linked reading
468$c5->add_relationship( 'r13.5', 'r13.2', { type => 'orthographic' } );
469$c5->add_relationship( 'r13.1', 'r13.2', { type => 'grammatical', propagate => 1 } );
470my $impliedrel = $c5->get_relationship( 'r13.1', 'r13.5' );
471ok( $impliedrel, 'Relationship was made between indirectly linked readings' );
472if( $impliedrel ) {
473 is( $impliedrel->type, 'grammatical', 'Implicit inbound relationship has the correct type' );
474}
475
476# Test 5.2: make another orthographic link, see if the grammatical one propagates
477$c5->add_relationship( 'r13.3', 'r13.5', { type => 'orthographic', propagate => 1 } );
478foreach my $rdg ( qw/ r13.3 r13.5 / ) {
479 my $newgram = $c5->get_relationship( 'r13.1', $rdg );
480 ok( $newgram, 'Relationship was propagaged up between indirectly linked readings' );
481 if( $newgram ) {
482 is( $newgram->type, 'grammatical', 'Implicit outbound relationship has the correct type' );
483 }
484}
485my $neworth = $c5->get_relationship( 'r13.2', 'r13.3' );
486ok( $neworth, 'Relationship was made between indirectly linked siblings' );
487if( $neworth ) {
488 is( $neworth->type, 'orthographic', 'Implicit direct relationship has the correct type' );
489}
490
491# Test 5.3: make an intermediate (spelling) link to the remaining node
492$c5->add_relationship( 'r13.4', 'r13.2', { type => 'spelling', propagate => 1 } );
493# Should be linked grammatically to 12.1, spelling-wise to the rest
494my $newgram = $c5->get_relationship( 'r13.4', 'r13.1' );
495ok( $newgram, 'Relationship was made between indirectly linked readings' );
496if( $newgram ) {
497 is( $newgram->type, 'grammatical', 'Implicit intermediate-out relationship has the correct type' );
498}
499foreach my $rdg ( qw/ r13.3 r13.5 / ) {
500 my $newspel = $c5->get_relationship( 'r13.4', $rdg );
501 ok( $newspel, 'Relationship was made between indirectly linked readings' );
502 if( $newspel ) {
503 is( $newspel->type, 'spelling', 'Implicit intermediate-in relationship has the correct type' );
504 }
505}
506
52179f61 507# Test 5.4: delete a spelling relationship, add it again, make sure it doesn't
508# throw and make sure all the relationships are the same
509my $numrel = scalar $c5->relationships;
510$c5->del_relationship( 'r13.4', 'r13.2' );
511try {
512 $c5->add_relationship( 'r13.4', 'r13.2', { type => 'spelling', propagate => 1 } );
513 ok( 1, "Managed not to throw an exception re-adding the relationship" );
514} catch( Text::Tradition::Error $e ) {
515 ok( 0, "Threw an exception trying to re-add our intermediate relationship: " . $e->message );
516}
517is( $numrel, scalar $c5->relationships, "Number of relationships did not change" );
518foreach my $rdg ( qw/ r13.2 r13.3 r13.5 / ) {
519 my $newspel = $c5->get_relationship( 'r13.4', $rdg );
520 ok( $newspel, 'Relationship was made between indirectly linked readings' );
521 if( $newspel ) {
522 is( $newspel->type, 'spelling', 'Reinstated intermediate-in relationship has the correct type' );
523 }
524}
525my $stillgram = $c5->get_relationship( 'r13.4', 'r13.1' );
526ok( $stillgram, 'Relationship was made between indirectly linked readings' );
527if( $stillgram ) {
528 is( $stillgram->type, 'grammatical', 'Reinstated intermediate-out relationship has the correct type' );
529}
530
531# Test 5.5: add a parallel but not sibling relationship
9e9b7540 532$c5->add_relationship( 'r13.6', 'r13.2', { type => 'lexical', propagate => 1 } );
533ok( !$c5->get_relationship( 'r13.6', 'r13.1' ),
534 "Lexical relationship did not affect grammatical" );
535foreach my $rdg ( qw/ r13.3 r13.4 r13.5 / ) {
536 my $newlex = $c5->get_relationship( 'r13.6', $rdg );
537 ok( $newlex, 'Parallel was made between indirectly linked readings' );
538 if( $newlex ) {
539 is( $newlex->type, 'lexical', 'Implicit parallel-down relationship has the correct type' );
540 }
541}
542
52179f61 543# Test 5.6: try it with non-colocated relationships
544$numrel = scalar $c5->relationships;
9e9b7540 545$c5->add_relationship( 'r62.1', 'r64.1', { type => 'transposition', propagate => 1 } );
546is( scalar $c5->relationships, $numrel+1,
547 "Adding non-colo relationship did not propagate" );
548# Add a pivot point
549$c5->add_relationship( 'r61.1', 'r61.5', { type => 'orthographic' } );
550# Add a third transposed node
551$c5->add_relationship( 'r62.1', 'r60.3', { type => 'transposition', propagate => 1 } );
552my $newtrans = $c5->get_relationship( 'r64.1', 'r60.3' );
553ok( $newtrans, 'Non-colo relationship was made between indirectly linked readings' );
554if( $newtrans ) {
555 is( $newtrans->type, 'transposition', 'Implicit non-colo relationship has the correct type' );
556}
557is( scalar $c5->relationships, $numrel+4,
558 "Adding non-colo relationship only propagated on non-colos" );
559
52179f61 560# TODO test that attempts to cross boundaries on bindlevel-equal relationships fail.
c7bd2768 561
6d381462 562=end testing
563
22222af9 564=cut
565
566sub add_relationship {
414cc046 567 my( $self, $source, $target, $options ) = @_;
568 my $c = $self->collation;
176badfe 569 my $sourceobj = $c->reading( $source );
570 my $targetobj = $c->reading( $target );
359944f7 571 throw( "Adding self relationship at $source" ) if $source eq $target;
176badfe 572 throw( "Cannot set relationship on a meta reading" )
573 if( $sourceobj->is_meta || $targetobj->is_meta );
ca6e6095 574 my $relationship;
24efa55d 575 my $reltype;
c7bd2768 576 my $thispaironly = delete $options->{thispaironly};
9e9b7540 577 my $propagate = delete $options->{propagate};
414cc046 578 my $droppedcolls = [];
ca6e6095 579 if( ref( $options ) eq 'Text::Tradition::Collation::Relationship' ) {
580 $relationship = $options;
24efa55d 581 $reltype = $self->type( $relationship->type );
ca6e6095 582 $thispaironly = 1; # If existing rel, set only where asked.
24efa55d 583 # Test the validity
414cc046 584 my( $is_valid, $reason ) = $self->relationship_valid( $source, $target,
24efa55d 585 $relationship->type, $droppedcolls );
ca6e6095 586 unless( $is_valid ) {
587 throw( "Invalid relationship: $reason" );
588 }
24efa55d 589 } else {
590 $reltype = $self->type( $options->{type} );
ca6e6095 591
592 # Try to create the relationship object.
c7bd2768 593 my $rdga = $reltype->regularize( $sourceobj );
594 my $rdgb = $reltype->regularize( $targetobj );
24efa55d 595 $options->{'orig_a'} = $sourceobj;
596 $options->{'orig_b'} = $targetobj;
597 $options->{'reading_a'} = $rdga;
598 $options->{'reading_b'} = $rdgb;
599 if( exists $options->{'scope'} && $options->{'scope'} ne 'local' ) {
0ac5e750 600 # Is there a relationship with this a & b already?
24efa55d 601 if( $rdga eq $rdgb ) {
602 # If we have canonified to the same thing for the relationship
603 # type we want, something is wrong.
604 # NOTE we want to allow this at the local level, as a cheap means
605 # of merging readings in the UI, until we get a better means.
606 throw( "Canonifier returns identical form $rdga for this relationship type" );
607 }
608
f222800e 609 my $otherrel = $self->scoped_relationship( $rdga, $rdgb );
0ac5e750 610 if( $otherrel && $otherrel->type eq $options->{type}
611 && $otherrel->scope eq $options->{scope} ) {
24efa55d 612 # warn "Applying existing scoped relationship for $rdga / $rdgb";
0ac5e750 613 $relationship = $otherrel;
99ab9535 614 } elsif( $otherrel ) {
24efa55d 615 throw( 'Conflicting scoped relationship '
616 . join( '/', $otherrel->type, $otherrel->scope ) . ' vs. '
617 . join( '/', $options->{type}, $options->{scope} )
618 . " for $rdga / $rdgb at $source / $target" );
0ac5e750 619 }
620 }
24efa55d 621 $relationship = $self->create( $options ) unless $relationship;
622 # ... Will throw on error
623
624 # See if the relationship is actually valid here
625 my( $is_valid, $reason ) = $self->relationship_valid( $source, $target,
626 $options->{'type'}, $droppedcolls );
627 unless( $is_valid ) {
628 throw( "Invalid relationship: $reason" );
629 }
22222af9 630 }
ca6e6095 631
22222af9 632
22222af9 633 # Now set the relationship(s).
634 my @pairs_set;
414cc046 635 my $rel = $self->get_relationship( $source, $target );
cc31ebaa 636 my $skip;
414cc046 637 if( $rel && $rel ne $relationship ) {
638 if( $rel->nonlocal ) {
639 throw( "Found conflicting relationship at $source - $target" );
24efa55d 640 } elsif( !$reltype->is_weak ) {
641 # Replace a weak relationship; leave any other sort in place.
414cc046 642 my $r1ann = $rel->has_annotation ? $rel->annotation : '';
643 my $r2ann = $relationship->has_annotation ? $relationship->annotation : '';
644 unless( $rel->type eq $relationship->type && $r1ann eq $r2ann ) {
645 warn sprintf( "Not overriding local relationship %s with global %s "
646 . "set at %s -> %s (%s -> %s)", $rel->type, $relationship->type,
647 $source, $target, $rel->reading_a, $rel->reading_b );
414cc046 648 }
24efa55d 649 $skip = 1;
414cc046 650 }
651 }
cc31ebaa 652 $self->_set_relationship( $relationship, $source, $target ) unless $skip;
9e9b7540 653 push( @pairs_set, [ $source, $target, $relationship->type ] );
414cc046 654
428bcf0b 655 # Find all the pairs for which we need to set the relationship.
656 if( $relationship->colocated && $relationship->nonlocal && !$thispaironly ) {
9e9b7540 657 my @global_set = $self->add_global_relationship( $relationship );
658 map { push( @$_, $relationship->type ) } @global_set;
659 push( @pairs_set, @global_set );
660 }
661 if( $propagate ) {
662 my @prop;
663 foreach my $ps ( @pairs_set ) {
664 my @extra = $self->propagate_relationship( $ps->[0], $ps->[1] );
665 push( @prop, @extra );
666 }
667 push( @pairs_set, @prop ) if @prop;
22222af9 668 }
9e9b7540 669
414cc046 670 # Finally, restore whatever collations we can, and return.
24efa55d 671 $self->_restore_weak( @$droppedcolls );
63778331 672 return @pairs_set;
22222af9 673}
674
428bcf0b 675=head2 add_global_relationship( $options, $skipvector )
676
677Adds the relationship specified wherever the relevant readings appear together
678in the graph. Options as in add_relationship above.
679
680=cut
681
682sub add_global_relationship {
24efa55d 683 my( $self, $relationship ) = @_;
428bcf0b 684 # Sanity checking
24efa55d 685 my $reltype = $self->type( $relationship->type );
428bcf0b 686 throw( "Relationship passed to add_global is not global" )
687 unless $relationship->nonlocal;
688 throw( "Relationship passed to add_global is not a valid global type" )
24efa55d 689 unless $reltype->is_generalizable;
428bcf0b 690
691 # Apply the relationship wherever it is valid
692 my @pairs_set;
693 foreach my $v ( $self->_find_applicable( $relationship ) ) {
694 my $exists = $self->get_relationship( @$v );
24efa55d 695 my $etype = $exists ? $self->type( $exists->type ) : '';
696 if( $exists && !$etype->is_weak ) {
697 unless( $exists->is_equivalent( $relationship ) ) {
698 throw( "Found conflicting relationship at @$v" );
699 }
428bcf0b 700 } else {
24efa55d 701 my @added;
702 try {
703 @added = $self->add_relationship( @$v, $relationship );
704 } catch {
705 my $reldesc = sprintf( "%s %s -> %s", $relationship->type,
706 $relationship->reading_a, $relationship->reading_b );
98a66507 707 # print STDERR "Global relationship $reldesc not applicable at @$v\n";
24efa55d 708 }
709 push( @pairs_set, @added ) if @added;
428bcf0b 710 }
711 }
712 return @pairs_set;
713}
714
715
9d829138 716=head2 del_scoped_relationship( $reading_a, $reading_b )
717
718Returns the general (document-level or global) relationship that has been defined
719between the two reading strings. Returns undef if there is no general relationship.
720
721=cut
722
723sub del_scoped_relationship {
724 my( $self, $rdga, $rdgb ) = @_;
725 my( $first, $second ) = sort( $rdga, $rdgb );
726 return delete $self->scopedrels->{$first}->{$second};
727}
728
bf6e338d 729sub _find_applicable {
730 my( $self, $rel ) = @_;
731 my $c = $self->collation;
24efa55d 732 my $reltype = $self->type( $rel->type );
bf6e338d 733 my @vectors;
734 my @identical_readings;
c7bd2768 735 @identical_readings = grep { $reltype->regularize( $_ ) eq $rel->reading_a }
24efa55d 736 $c->readings;
bf6e338d 737 foreach my $ir ( @identical_readings ) {
738 my @itarget;
c7bd2768 739 @itarget = grep { $reltype->regularize( $_ ) eq $rel->reading_b }
24efa55d 740 $c->readings_at_rank( $ir->rank );
bf6e338d 741 if( @itarget ) {
24efa55d 742 # Warn if there is more than one hit with no closer link between them.
bf6e338d 743 my $itmain = shift @itarget;
744 if( @itarget ) {
745 my %all_targets;
24efa55d 746 my $bindlevel = $reltype->bindlevel;
bf6e338d 747 map { $all_targets{$_} = 1 } @itarget;
748 map { delete $all_targets{$_} }
24efa55d 749 $self->related_readings( $itmain, sub {
750 $self->type( $_[0]->type )->bindlevel < $bindlevel } );
bf6e338d 751 warn "More than one unrelated reading with text " . $itmain->text
752 . " at rank " . $ir->rank . "!" if keys %all_targets;
753 }
754 push( @vectors, [ $ir->id, $itmain->id ] );
755 }
756 }
757 return @vectors;
758}
759
ee801e17 760=head2 del_relationship( $source, $target )
761
762Removes the relationship between the given readings. If the relationship is
763non-local, removes the relationship everywhere in the graph.
764
765=cut
766
767sub del_relationship {
768 my( $self, $source, $target ) = @_;
769 my $rel = $self->get_relationship( $source, $target );
681893aa 770 return () unless $rel; # Nothing to delete; return an empty set.
24efa55d 771 my $reltype = $self->type( $rel->type );
359944f7 772 my $colo = $rel->colocated;
ee801e17 773 my @vectors = ( [ $source, $target ] );
359944f7 774 $self->_remove_relationship( $colo, $source, $target );
ee801e17 775 if( $rel->nonlocal ) {
776 # Remove the relationship wherever it occurs.
24efa55d 777 my @rel_edges = grep { $self->get_relationship( @$_ ) eq $rel }
ee801e17 778 $self->relationships;
779 foreach my $re ( @rel_edges ) {
359944f7 780 $self->_remove_relationship( $colo, @$re );
ee801e17 781 push( @vectors, $re );
782 }
9d829138 783 $self->del_scoped_relationship( $rel->reading_a, $rel->reading_b );
ee801e17 784 }
785 return @vectors;
786}
787
ca6e6095 788sub _remove_relationship {
359944f7 789 my( $self, $equiv, @vector ) = @_;
ca6e6095 790 $self->graph->delete_edge( @vector );
176badfe 791 $self->_break_equivalence( @vector ) if $equiv;
ca6e6095 792}
793
22222af9 794=head2 relationship_valid( $source, $target, $type )
795
796Checks whether a relationship of type $type may exist between the readings given
797in $source and $target. Returns a tuple of ( status, message ) where status is
798a yes/no boolean and, if the answer is no, message gives the reason why.
799
800=cut
801
802sub relationship_valid {
414cc046 803 my( $self, $source, $target, $rel, $mustdrop ) = @_;
804 $mustdrop = [] unless $mustdrop; # in case we were passed nothing
22222af9 805 my $c = $self->collation;
24efa55d 806 my $reltype = $self->type( $rel );
10943ab0 807 ## Assume validity is okay if we are initializing from scratch.
3579c22b 808 return ( 1, "initializing" ) unless $c->tradition->_initialized;
c7bd2768 809 ## TODO Move this block to relationship type definition when we can save
810 ## coderefs
24efa55d 811 if ( $rel eq 'transposition' || $rel eq 'repetition' ) {
22222af9 812 # Check that the two readings do (for a repetition) or do not (for
813 # a transposition) appear in the same witness.
56772e8c 814 # TODO this might be called before witness paths are set...
22222af9 815 my %seen_wits;
816 map { $seen_wits{$_} = 1 } $c->reading_witnesses( $source );
817 foreach my $w ( $c->reading_witnesses( $target ) ) {
818 if( $seen_wits{$w} ) {
819 return ( 0, "Readings both occur in witness $w" )
820 if $rel eq 'transposition';
821 return ( 1, "ok" ) if $rel eq 'repetition';
d6936dea 822 }
22222af9 823 }
abadc997 824 return ( 0, "Readings occur only in distinct witnesses" )
825 if $rel eq 'repetition';
826 }
24efa55d 827 if ( $reltype->is_colocation ) {
22222af9 828 # Check that linking the source and target in a relationship won't lead
414cc046 829 # to a path loop for any witness.
830 # First, drop/stash any collations that might interfere
831 my $sourceobj = $c->reading( $source );
832 my $targetobj = $c->reading( $target );
833 my $sourcerank = $sourceobj->has_rank ? $sourceobj->rank : -1;
834 my $targetrank = $targetobj->has_rank ? $targetobj->rank : -1;
835 unless( $rel eq 'collated' || $sourcerank == $targetrank ) {
24efa55d 836 push( @$mustdrop, $self->_drop_weak( $source ) );
837 push( @$mustdrop, $self->_drop_weak( $target ) );
359944f7 838 if( $c->end->has_rank ) {
176badfe 839 foreach my $rk ( $sourcerank .. $targetrank ) {
24efa55d 840 map { push( @$mustdrop, $self->_drop_weak( $_->id ) ) }
414cc046 841 $c->readings_at_rank( $rk );
842 }
843 }
a1615ee4 844 }
359944f7 845 unless( $self->test_equivalence( $source, $target ) ) {
24efa55d 846 $self->_restore_weak( @$mustdrop );
414cc046 847 return( 0, "Relationship would create witness loop" );
a1615ee4 848 }
22222af9 849 return ( 1, "ok" );
24efa55d 850 } else {
851 # We also need to check that the readings are not in the same place.
852 # That is, proposing to equate them should cause a witness loop.
853 if( $self->test_equivalence( $source, $target ) ) {
854 return ( 0, "Readings appear to be colocated" );
855 } else {
856 return ( 1, "ok" );
857 }
22222af9 858 }
859}
860
24efa55d 861sub _drop_weak {
778251a6 862 my( $self, $reading ) = @_;
414cc046 863 my @dropped;
778251a6 864 foreach my $n ( $self->graph->neighbors( $reading ) ) {
24efa55d 865 my $nrel = $self->get_relationship( $reading, $n );
866 if( $self->type( $nrel->type )->is_weak ) {
867 push( @dropped, [ $reading, $n, $nrel->type ] );
778251a6 868 $self->del_relationship( $reading, $n );
24efa55d 869 #print STDERR "Dropped weak relationship $reading -> $n\n";
778251a6 870 }
871 }
414cc046 872 return @dropped;
873}
874
24efa55d 875sub _restore_weak {
414cc046 876 my( $self, @vectors ) = @_;
877 foreach my $v ( @vectors ) {
24efa55d 878 my $type = pop @$v;
879 eval {
880 $self->add_relationship( @$v, { 'type' => $type } );
881 #print STDERR "Restored weak relationship @$v\n";
882 }; # if it fails we don't care
414cc046 883 }
778251a6 884}
885
7f52eac8 886=head2 related_readings( $reading, $filter )
22222af9 887
9e9b7540 888Returns a list of readings that are connected via direct relationship links
889to $reading. If $filter is set to a subroutine ref, returns only those
890related readings where $filter( $relationship ) returns a true value.
22222af9 891
892=cut
893
894sub related_readings {
7f52eac8 895 my( $self, $reading, $filter ) = @_;
22222af9 896 my $return_object;
897 if( ref( $reading ) eq 'Text::Tradition::Collation::Reading' ) {
898 $reading = $reading->id;
899 $return_object = 1;
900 }
c84275ff 901 my @answer;
7f52eac8 902 if( $filter ) {
903 # Backwards compat
904 if( $filter eq 'colocated' ) {
905 $filter = sub { $_[0]->colocated };
d002ccb7 906 } elsif( !ref( $filter ) ) {
907 my $type = $filter;
908 $filter = sub { $_[0]->type eq $type };
7f52eac8 909 }
9e9b7540 910 @answer = grep { &$filter( $self->get_relationship( $reading, $_ ) ) }
911 $self->graph->neighbors( $reading );
c84275ff 912 } else {
9e9b7540 913 @answer = $self->graph->neighbors( $reading );
22222af9 914 }
915 if( $return_object ) {
916 my $c = $self->collation;
c84275ff 917 return map { $c->reading( $_ ) } @answer;
22222af9 918 } else {
c84275ff 919 return @answer;
22222af9 920 }
921}
922
9e9b7540 923=head2 propagate_relationship( $rel )
924
925Apply the transitivity and binding level rules to propagate the consequences of
926the specified relationship link, ensuring all consequent relationships exist.
927For now, we only propagate colocation links if we are passed a colocation, and
928we only propagate displacement links if we are given a displacement.
929
930Returns an array of tuples ( rdg1, rdg2, type ) for each new reading set.
931
932=cut
933
934sub propagate_relationship {
935 my( $self, @rel ) = @_;
936 ## Check that the vector is an arrayref
937 my $rel = @rel > 1 ? \@rel : $rel[0];
938 ## Get the relationship info
939 my $relobj = $self->get_relationship( $rel );
940 my $reltype = $self->type( $relobj->type );
941 return () unless $reltype->is_transitive;
942 my @newly_set;
943
944 my $colo = $reltype->is_colocation;
945 my $bindlevel = $reltype->bindlevel;
946
947 ## Find all readings that are linked via this relationship type
948 my %thislevel = ( $rel->[0] => 1, $rel->[1] => 1 );
949 my $check = $rel;
950 my $iter = 0;
951 while( @$check ) {
952 my $more = [];
953 foreach my $r ( @$check ) {
954 push( @$more, grep { !exists $thislevel{$_}
955 && $self->get_relationship( $r, $_ )
956 && $self->get_relationship( $r, $_ )->type eq $relobj->type }
957 $self->graph->neighbors( $r ) );
958 }
959 map { $thislevel{$_} = 1 } @$more;
960 $check = $more;
961 }
962
963 ## Make sure every reading of our relationship type is linked to every other
964 my @samelevel = keys %thislevel;
965 while( @samelevel ) {
966 my $r = shift @samelevel;
967 foreach my $nr ( @samelevel ) {
968 my $existing = $self->get_relationship( $r, $nr );
52179f61 969 my $skip;
9e9b7540 970 if( $existing ) {
52179f61 971 my $extype = $self->type( $existing->type );
972 unless( $extype->is_weak ) {
973 # Check that it's a matching type, or a type subsumed by our
974 # bindlevel
975 throw( "Conflicting existing relationship of type "
976 . $existing->type . " at $r, $nr trying to propagate "
977 . $relobj->type . " relationship at @$rel" )
978 unless $existing->type eq $relobj->type
979 || $extype->bindlevel <= $reltype->bindlevel;
980 $skip = 1;
981 }
982 }
983 unless( $skip ) {
9e9b7540 984 # Try to add a new relationship here
985 try {
986 my @new = $self->add_relationship( $r, $nr, { type => $relobj->type,
987 annotation => "Propagated from relationship at @$rel" } );
988 push( @newly_set, @new );
989 } catch ( Text::Tradition::Error $e ) {
990 throw( "Could not propagate " . $relobj->type .
991 " relationship (original @$rel) at $r -- $nr: " .
992 $e->message );
993 }
994 }
995 }
996
997 ## Now for each sibling our set, look for its direct connections to
998 ## transitive readings of a different bindlevel, and make sure that
999 ## all siblings are related to those readings.
1000 my @other;
1001 foreach my $n ( $self->graph->neighbors( $r ) ) {
1002 my $crel = $self->get_relationship( $r, $n );
1003 next unless $crel;
1004 my $crt = $self->type( $crel->type );
1005 if( $crt->is_transitive && $crt->is_colocation == $colo ) {
1006 next if $crt->bindlevel == $reltype->bindlevel;
1007 my $nrel = $crt->bindlevel < $reltype->bindlevel
1008 ? $reltype->name : $crt->name;
1009 push( @other, [ $n, $nrel ] );
1010 }
1011 }
1012 # The @other array now contains tuples of ( reading, type ) where the
1013 # reading is the non-sibling and the type is the type of relationship
1014 # that the siblings should have to the non-sibling.
1015 foreach ( @other ) {
1016 my( $nr, $nrtype ) = @$_;
1017 foreach my $sib ( keys %thislevel ) {
1018 next if $sib eq $r;
52179f61 1019 next if $sib eq $nr; # can happen if linked to $r by tightrel
1020 # but linked to a sib of $r by thisrel
1021 # e.g. when a rel has been part propagated
9e9b7540 1022 my $existing = $self->get_relationship( $sib, $nr );
52179f61 1023 my $skip;
9e9b7540 1024 if( $existing ) {
1025 # Check that it's compatible. The existing relationship type
52179f61 1026 # should match or be subsumed by the looser of the two
1027 # relationships in play, whether the original relationship
1028 # being worked on or the relationship between $r and $or.
1029 my $extype = $self->type( $existing->type );
1030 unless( $extype->is_weak ) {
1031 if( $nrtype ne $extype->name
1032 && $self->type( $nrtype )->bindlevel <= $extype->bindlevel ) {
1033 throw( "Conflicting existing relationship at $nr ( -> "
1034 . $self->get_relationship( $nr, $r )->type . " to $r) "
1035 . " -- $sib trying to propagate " . $relobj->type
1036 . " relationship at @$rel" );
1037 }
1038 $skip = 1;
9e9b7540 1039 }
52179f61 1040 }
1041 unless( $skip ) {
9e9b7540 1042 # Try to add a new relationship here
1043 try {
1044 my @new = $self->add_relationship( $sib, $nr, { type => $nrtype,
1045 annotation => "Propagated from relationship at @$rel" } );
1046 push( @newly_set, @new );
1047 } catch ( Text::Tradition::Error $e ) {
1048 throw( "Could not propagate $nrtype relationship (original " .
1049 $relobj->type . " at @$rel) at $sib -- $nr: " .
1050 $e->message );
1051 }
1052 }
1053 }
1054 }
1055 }
1056
1057 return @newly_set;
1058}
1059
52179f61 1060=head2 propagate_all_relationships
1061
1062Apply propagation logic retroactively to all relationships in the tradition.
1063
1064=cut
1065
1066sub propagate_all_relationships {
1067 my $self = shift;
1068 my @allrels = sort { $self->_propagate_rel_order( $a, $b ) } $self->relationships;
1069 foreach my $rel ( @allrels ) {
1070 my $relobj = $self->get_relationship( $rel );
1071 if( $self->type( $relobj->type )->is_transitive ) {
1072 my @added = $self->propagate_relationship( $rel );
1073 }
1074 }
1075}
1076
1077# Helper sorting function for retroactive propagation order.
1078sub _propagate_rel_order {
1079 my( $self, $a, $b ) = @_;
1080 my $aobj = $self->get_relationship( $a );
1081 my $bobj = $self->get_relationship( $b );
1082 my $at = $self->type( $aobj->type ); my $bt = $self->type( $bobj->type );
1083 # Apply strong relationships before weak
1084 return -1 if $bt->is_weak && !$at->is_weak;
1085 return 1 if $at->is_weak && !$bt->is_weak;
1086 # Apply more tightly bound relationships first
1087 return $at->bindlevel <=> $bt->bindlevel;
1088}
1089
1090
22222af9 1091=head2 merge_readings( $kept, $deleted );
1092
1093Makes a best-effort merge of the relationship links between the given readings, and
1094stops tracking the to-be-deleted reading.
1095
1096=cut
1097
1098sub merge_readings {
1099 my( $self, $kept, $deleted, $combined ) = @_;
1100 foreach my $edge ( $self->graph->edges_at( $deleted ) ) {
1101 # Get the pair of kept / rel
1102 my @vector = ( $kept );
1103 push( @vector, $edge->[0] eq $deleted ? $edge->[1] : $edge->[0] );
1104 next if $vector[0] eq $vector[1]; # Don't add a self loop
1105
1106 # If kept changes its text, drop the relationship.
1107 next if $combined;
1108
f222800e 1109 # If kept / rel already has a relationship, just keep the old
3ae5e2ad 1110 my $rel = $self->get_relationship( @vector );
f222800e 1111 next if $rel;
22222af9 1112
1113 # Otherwise, adopt the relationship that would be deleted.
3ae5e2ad 1114 $rel = $self->get_relationship( @$edge );
1115 $self->_set_relationship( $rel, @vector );
22222af9 1116 }
56772e8c 1117 $self->_make_equivalence( $deleted, $kept );
22222af9 1118}
1119
359944f7 1120### Equivalence logic
1121
1122sub _remove_equivalence_node {
1123 my( $self, $node ) = @_;
1124 my $group = $self->equivalence( $node );
1125 my $nodelist = $self->eqreadings( $group );
1126 if( @$nodelist == 1 && $nodelist->[0] eq $node ) {
3579c22b 1127 $self->equivalence_graph->delete_vertex( $group );
359944f7 1128 $self->remove_eqreadings( $group );
3579c22b 1129 $self->remove_equivalence( $group );
359944f7 1130 } elsif( @$nodelist == 1 ) {
3579c22b 1131 throw( "DATA INCONSISTENCY in equivalence graph: " . $nodelist->[0] .
1132 " in group that should have only $node" );
359944f7 1133 } else {
10e4b1ac 1134 my @newlist = grep { $_ ne $node } @$nodelist;
359944f7 1135 $self->set_eqreadings( $group, \@newlist );
1136 $self->remove_equivalence( $node );
1137 }
1138}
1139
1140=head2 add_equivalence_edge
1141
176badfe 1142Add an edge in the equivalence graph corresponding to $source -> $target in the
1143collation. Should only be called by Collation.
359944f7 1144
1145=cut
1146
1147sub add_equivalence_edge {
1148 my( $self, $source, $target ) = @_;
1149 my $seq = $self->equivalence( $source );
1150 my $teq = $self->equivalence( $target );
359944f7 1151 $self->equivalence_graph->add_edge( $seq, $teq );
1152}
1153
176badfe 1154=head2 delete_equivalence_edge
359944f7 1155
176badfe 1156Remove an edge in the equivalence graph corresponding to $source -> $target in the
1157collation. Should only be called by Collation.
359944f7 1158
1159=cut
1160
1161sub delete_equivalence_edge {
1162 my( $self, $source, $target ) = @_;
1163 my $seq = $self->equivalence( $source );
1164 my $teq = $self->equivalence( $target );
359944f7 1165 $self->equivalence_graph->delete_edge( $seq, $teq );
1166}
1167
1168sub _is_disconnected {
1169 my $self = shift;
1170 return( scalar $self->equivalence_graph->predecessorless_vertices > 1
1171 || scalar $self->equivalence_graph->successorless_vertices > 1 );
1172}
1173
176badfe 1174# Equate two readings in the equivalence graph
1175sub _make_equivalence {
56772e8c 1176 my( $self, $source, $target ) = @_;
359944f7 1177 # Get the source equivalent readings
1178 my $seq = $self->equivalence( $source );
1179 my $teq = $self->equivalence( $target );
1180 # Nothing to do if they are already equivalent...
1181 return if $seq eq $teq;
56772e8c 1182 my $sourcepool = $self->eqreadings( $seq );
359944f7 1183 # and add them to the target readings.
56772e8c 1184 push( @{$self->eqreadings( $teq )}, @$sourcepool );
1185 map { $self->set_equivalence( $_, $teq ) } @$sourcepool;
359944f7 1186 # Then merge the nodes in the equivalence graph.
1187 foreach my $pred ( $self->equivalence_graph->predecessors( $seq ) ) {
56772e8c 1188 $self->equivalence_graph->add_edge( $pred, $teq );
359944f7 1189 }
1190 foreach my $succ ( $self->equivalence_graph->successors( $seq ) ) {
56772e8c 1191 $self->equivalence_graph->add_edge( $teq, $succ );
359944f7 1192 }
1193 $self->equivalence_graph->delete_vertex( $seq );
176badfe 1194 # TODO enable this after collation parsing is done
10943ab0 1195 throw( "Graph got disconnected making $source / $target equivalence" )
3579c22b 1196 if $self->_is_disconnected && $self->collation->tradition->_initialized;
359944f7 1197}
1198
1199=head2 test_equivalence
1200
176badfe 1201Test whether, if two readings were equated with a 'colocated' relationship,
1202the graph would still be valid.
359944f7 1203
1204=cut
1205
1206sub test_equivalence {
1207 my( $self, $source, $target ) = @_;
1208 # Try merging the nodes in the equivalence graph; return a true value if
1209 # no cycle is introduced thereby. Restore the original graph first.
1210
1211 # Keep track of edges we add
1212 my %added_pred;
1213 my %added_succ;
1214 # Get the reading equivalents
1215 my $seq = $self->equivalence( $source );
1216 my $teq = $self->equivalence( $target );
1217 # Maybe this is easy?
1218 return 1 if $seq eq $teq;
1219
1220 # Save the first graph
1221 my $checkstr = $self->equivalence_graph->stringify();
1222 # Add and save relevant edges
1223 foreach my $pred ( $self->equivalence_graph->predecessors( $seq ) ) {
1224 if( $self->equivalence_graph->has_edge( $pred, $teq ) ) {
1225 $added_pred{$pred} = 0;
1226 } else {
1227 $self->equivalence_graph->add_edge( $pred, $teq );
1228 $added_pred{$pred} = 1;
1229 }
1230 }
1231 foreach my $succ ( $self->equivalence_graph->successors( $seq ) ) {
1232 if( $self->equivalence_graph->has_edge( $teq, $succ ) ) {
1233 $added_succ{$succ} = 0;
1234 } else {
1235 $self->equivalence_graph->add_edge( $teq, $succ );
1236 $added_succ{$succ} = 1;
1237 }
1238 }
1239 # Delete source equivalent and test
1240 $self->equivalence_graph->delete_vertex( $seq );
1241 my $ret = !$self->equivalence_graph->has_a_cycle;
1242
1243 # Restore what we changed
1244 $self->equivalence_graph->add_vertex( $seq );
1245 foreach my $pred ( keys %added_pred ) {
1246 $self->equivalence_graph->add_edge( $pred, $seq );
1247 $self->equivalence_graph->delete_edge( $pred, $teq ) if $added_pred{$pred};
1248 }
1249 foreach my $succ ( keys %added_succ ) {
1250 $self->equivalence_graph->add_edge( $seq, $succ );
1251 $self->equivalence_graph->delete_edge( $teq, $succ ) if $added_succ{$succ};
1252 }
1253 unless( $self->equivalence_graph->eq( $checkstr ) ) {
c7bd2768 1254 throw( "GRAPH CHANGED after testing" );
359944f7 1255 }
1256 # Return our answer
1257 return $ret;
1258}
1259
176badfe 1260# Unmake an equivalence link between two readings. Should only be called internally.
1261sub _break_equivalence {
359944f7 1262 my( $self, $source, $target ) = @_;
1263
1264 # This is the hard one. Need to reconstruct the equivalence groups without
1265 # the given link.
1266 my( %sng, %tng );
1267 map { $sng{$_} = 1 } $self->_find_equiv_without( $source, $target );
1268 map { $tng{$_} = 1 } $self->_find_equiv_without( $target, $source );
1269 # If these groups intersect, they are still connected; do nothing.
1270 foreach my $el ( keys %tng ) {
10e4b1ac 1271 return if( exists $sng{$el} );
359944f7 1272 }
359944f7 1273 # If they don't intersect, then we split the nodes in the graph and in
1274 # the hashes. First figure out which group has which name
176badfe 1275 my $oldgroup = $self->equivalence( $source ); # same as $target
1276 my $keepsource = $sng{$oldgroup};
1277 my $newgroup = $keepsource ? $target : $source;
359944f7 1278 my( $oldmembers, $newmembers );
176badfe 1279 if( $keepsource ) {
359944f7 1280 $oldmembers = [ keys %sng ];
1281 $newmembers = [ keys %tng ];
1282 } else {
1283 $oldmembers = [ keys %tng ];
1284 $newmembers = [ keys %sng ];
1285 }
1286
1287 # First alter the old group in the hash
1288 $self->set_eqreadings( $oldgroup, $oldmembers );
176badfe 1289 foreach my $el ( @$oldmembers ) {
1290 $self->set_equivalence( $el, $oldgroup );
1291 }
359944f7 1292
1293 # then add the new group back to the hash with its new key
1294 $self->set_eqreadings( $newgroup, $newmembers );
1295 foreach my $el ( @$newmembers ) {
1296 $self->set_equivalence( $el, $newgroup );
1297 }
1298
1299 # Now add the new group back to the equivalence graph
1300 $self->equivalence_graph->add_vertex( $newgroup );
1301 # ...add the appropriate edges to the source group vertext
1302 my $c = $self->collation;
1303 foreach my $rdg ( @$newmembers ) {
1304 foreach my $rp ( $c->sequence->predecessors( $rdg ) ) {
beb47b16 1305 next unless $self->equivalence( $rp );
359944f7 1306 $self->equivalence_graph->add_edge( $self->equivalence( $rp ), $newgroup );
1307 }
1308 foreach my $rs ( $c->sequence->successors( $rdg ) ) {
beb47b16 1309 next unless $self->equivalence( $rs );
359944f7 1310 $self->equivalence_graph->add_edge( $newgroup, $self->equivalence( $rs ) );
1311 }
1312 }
1313
1314 # ...and figure out which edges on the old group vertex to delete.
1315 my( %old_pred, %old_succ );
1316 foreach my $rdg ( @$oldmembers ) {
1317 foreach my $rp ( $c->sequence->predecessors( $rdg ) ) {
beb47b16 1318 next unless $self->equivalence( $rp );
359944f7 1319 $old_pred{$self->equivalence( $rp )} = 1;
1320 }
1321 foreach my $rs ( $c->sequence->successors( $rdg ) ) {
beb47b16 1322 next unless $self->equivalence( $rs );
359944f7 1323 $old_succ{$self->equivalence( $rs )} = 1;
1324 }
1325 }
1326 foreach my $p ( $self->equivalence_graph->predecessors( $oldgroup ) ) {
1327 unless( $old_pred{$p} ) {
1328 $self->equivalence_graph->delete_edge( $p, $oldgroup );
1329 }
1330 }
1331 foreach my $s ( $self->equivalence_graph->successors( $oldgroup ) ) {
1332 unless( $old_succ{$s} ) {
1333 $self->equivalence_graph->delete_edge( $oldgroup, $s );
1334 }
1335 }
176badfe 1336 # TODO enable this after collation parsing is done
10943ab0 1337 throw( "Graph got disconnected breaking $source / $target equivalence" )
3579c22b 1338 if $self->_is_disconnected && $self->collation->tradition->_initialized;
359944f7 1339}
1340
1341sub _find_equiv_without {
1342 my( $self, $first, $second ) = @_;
1343 my %found = ( $first => 1 );
1344 my $check = [ $first ];
1345 my $iter = 0;
1346 while( @$check ) {
1347 my $more = [];
1348 foreach my $r ( @$check ) {
1349 foreach my $nr ( $self->graph->neighbors( $r ) ) {
1350 next if $r eq $second;
1351 if( $self->get_relationship( $r, $nr )->colocated ) {
1352 push( @$more, $nr ) unless exists $found{$nr};
1353 $found{$nr} = 1;
1354 }
1355 }
1356 }
1357 $check = $more;
1358 }
1359 return keys %found;
1360}
1361
e1083e99 1362=head2 rebuild_equivalence
1363
1364(Re)build the equivalence graph from scratch. Dumps the graph, makes a new one,
1365adds all readings and edges, then makes an equivalence for all relationships.
1366
1367=cut
1368
1369sub rebuild_equivalence {
1370 my $self = shift;
1371 my $newgraph = Graph->new();
04482188 1372 # Set this as the new equivalence graph
1373 $self->_reset_equivalence( $newgraph );
1374 # Clear out the data hashes
1375 $self->_clear_equivalence;
1376 $self->_clear_eqreadings;
1377
b6f13859 1378 $self->collation->tradition->_init_done(0);
04482188 1379 # Add the readings
e1083e99 1380 foreach my $r ( $self->collation->readings ) {
04482188 1381 my $rid = $r->id;
1382 $newgraph->add_vertex( $rid );
1383 $self->set_equivalence( $rid, $rid );
1384 $self->set_eqreadings( $rid, [ $rid ] );
e1083e99 1385 }
04482188 1386
1387 # Now add the edges
e1083e99 1388 foreach my $e ( $self->collation->paths ) {
04482188 1389 $self->add_equivalence_edge( @$e );
e1083e99 1390 }
04482188 1391
1392 # Now equate the colocated readings. This does no testing;
1393 # it assumes that all preexisting relationships are valid.
e1083e99 1394 foreach my $rel ( $self->relationships ) {
1395 my $relobj = $self->get_relationship( $rel );
1396 next unless $relobj && $relobj->colocated;
1397 $self->_make_equivalence( @$rel );
1398 }
b6f13859 1399 $self->collation->tradition->_init_done(1);
e1083e99 1400}
1401
56772e8c 1402=head2 equivalence_ranks
1403
1404Rank all vertices in the equivalence graph, and return a hash reference with
1405vertex => rank mapping.
1406
1407=cut
1408
1409sub equivalence_ranks {
1410 my $self = shift;
1411 my $eqstart = $self->equivalence( $self->collation->start );
1412 my $eqranks = { $eqstart => 0 };
1413 my $rankeqs = { 0 => [ $eqstart ] };
1414 my @curr_origin = ( $eqstart );
1415 # A little iterative function.
1416 while( @curr_origin ) {
1417 @curr_origin = $self->_assign_rank( $eqranks, $rankeqs, @curr_origin );
1418 }
1419 return( $eqranks, $rankeqs );
1420}
1421
1422sub _assign_rank {
1423 my( $self, $node_ranks, $rank_nodes, @current_nodes ) = @_;
1424 my $graph = $self->equivalence_graph;
1425 # Look at each of the children of @current_nodes. If all the child's
1426 # parents have a rank, assign it the highest rank + 1 and add it to
1427 # @next_nodes. Otherwise skip it; we will return when the highest-ranked
1428 # parent gets a rank.
1429 my @next_nodes;
1430 foreach my $c ( @current_nodes ) {
1431 warn "Current reading $c has no rank!"
1432 unless exists $node_ranks->{$c};
1433 foreach my $child ( $graph->successors( $c ) ) {
1434 next if exists $node_ranks->{$child};
1435 my $highest_rank = -1;
1436 my $skip = 0;
1437 foreach my $parent ( $graph->predecessors( $child ) ) {
1438 if( exists $node_ranks->{$parent} ) {
1439 $highest_rank = $node_ranks->{$parent}
1440 if $highest_rank <= $node_ranks->{$parent};
1441 } else {
1442 $skip = 1;
1443 last;
1444 }
1445 }
1446 next if $skip;
1447 my $c_rank = $highest_rank + 1;
1448 # print STDERR "Assigning rank $c_rank to node $child \n";
1449 $node_ranks->{$child} = $c_rank if $node_ranks;
1450 push( @{$rank_nodes->{$c_rank}}, $child ) if $rank_nodes;
1451 push( @next_nodes, $child );
1452 }
1453 }
1454 return @next_nodes;
1455}
1456
359944f7 1457### Output logic
1458
027d819c 1459sub _as_graphml {
2626f709 1460 my( $self, $graphml_ns, $xmlroot, $node_hash, $nodeid_key, $edge_keys ) = @_;
c84275ff 1461
1462 my $rgraph = $xmlroot->addNewChild( $graphml_ns, 'graph' );
1463 $rgraph->setAttribute( 'edgedefault', 'directed' );
1464 $rgraph->setAttribute( 'id', 'relationships', );
1465 $rgraph->setAttribute( 'parse.edgeids', 'canonical' );
cc31ebaa 1466 $rgraph->setAttribute( 'parse.edges', 0 );
c84275ff 1467 $rgraph->setAttribute( 'parse.nodeids', 'canonical' );
cc31ebaa 1468 $rgraph->setAttribute( 'parse.nodes', 0 );
c84275ff 1469 $rgraph->setAttribute( 'parse.order', 'nodesfirst' );
1470
1471 # Add the vertices according to their XML IDs
2626f709 1472 my %rdg_lookup = ( reverse %$node_hash );
cc31ebaa 1473 # my @nlist = sort _by_xmlid keys( %rdg_lookup ); ## CAUSES SEGFAULT
826d8773 1474 my @nlist = sort keys( %rdg_lookup );
414cc046 1475 foreach my $n ( @nlist ) {
c84275ff 1476 my $n_el = $rgraph->addNewChild( $graphml_ns, 'node' );
1477 $n_el->setAttribute( 'id', $n );
2626f709 1478 _add_graphml_data( $n_el, $nodeid_key, $rdg_lookup{$n} );
c84275ff 1479 }
cc31ebaa 1480 $rgraph->setAttribute( 'parse.nodes', scalar @nlist );
c84275ff 1481
1482 # Add the relationship edges, with their object information
1483 my $edge_ctr = 0;
1484 foreach my $e ( sort { $a->[0] cmp $b->[0] } $self->graph->edges ) {
1485 # Add an edge and fill in its relationship info.
a30ca502 1486 next unless( exists $node_hash->{$e->[0]} && exists $node_hash->{$e->[1]} );
c84275ff 1487 my $edge_el = $rgraph->addNewChild( $graphml_ns, 'edge' );
1488 $edge_el->setAttribute( 'source', $node_hash->{$e->[0]} );
1489 $edge_el->setAttribute( 'target', $node_hash->{$e->[1]} );
1490 $edge_el->setAttribute( 'id', 'e'.$edge_ctr++ );
1491
3ae5e2ad 1492 my $rel_obj = $self->get_relationship( @$e );
bbd064a9 1493 foreach my $key ( keys %$edge_keys ) {
1494 my $value = $rel_obj->$key;
1495 _add_graphml_data( $edge_el, $edge_keys->{$key}, $value )
1496 if defined $value;
1497 }
c84275ff 1498 }
cc31ebaa 1499 $rgraph->setAttribute( 'parse.edges', $edge_ctr );
c84275ff 1500}
1501
1502sub _by_xmlid {
2626f709 1503 my $tmp_a = $a;
1504 my $tmp_b = $b;
1505 $tmp_a =~ s/\D//g;
1506 $tmp_b =~ s/\D//g;
1507 return $tmp_a <=> $tmp_b;
c84275ff 1508}
1509
1510sub _add_graphml_data {
1511 my( $el, $key, $value ) = @_;
1512 return unless defined $value;
1513 my $data_el = $el->addNewChild( $el->namespaceURI, 'data' );
1514 $data_el->setAttribute( 'key', $key );
1515 $data_el->appendText( $value );
83d5ac3a 1516}
1517
c7bd2768 1518sub dump_segment {
1519 my( $self, $from, $to ) = @_;
1520 open( DUMP, ">debug.svg" ) or die "Could not open debug.svg";
1521 binmode DUMP, ':utf8';
1522 print DUMP $self->collation->as_svg({ from => $from, to => $to, nocalc => 1 });
1523 close DUMP;
1524}
1525
63778331 1526sub throw {
1527 Text::Tradition::Error->throw(
1528 'ident' => 'Relationship error',
1529 'message' => $_[0],
1530 );
1531}
1532
22222af9 1533no Moose;
1534__PACKAGE__->meta->make_immutable;
1535
15361;