File Coverage

blib/lib/GOBO/Graph.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             =head1 NAME
2              
3             GOBO::Graph
4              
5             =head1 SYNOPSIS
6              
7             =head1 DESCRIPTION
8              
9             A collection of inter-related GOBO::Node objects. With a simple
10             ontology these are typically GOBO::TermNode objects, although other
11             graphs e.g. instance graphs are possible.
12              
13             This module deliberately omits any kind of graph traversal
14             functionality. This is done by an GOBO::InferenceEngine.
15              
16             =head2 DETAILS
17              
18             A GOBO::Graph consists of two collections: a node collection and a
19             link collection. Both types of collection are handled behind the
20             scenes using indexes (in future these can be transparently mapped to
21             databases).
22              
23             A graph keeps a reference of all nodes declared or referenced. We draw
24             a distinction here: a graph can reference a node that is not declared
25             in that graph. For example, consider an obo file with two stanzas:
26              
27             id: x
28             is_a: y
29              
30             id: y
31             is_a: z
32              
33             Here there are only two nodes declared (x and y) but there are a total
34             of three references.
35              
36             The noderef method can be used to access the full list of nodes that
37             are either declared or referenced. This is useful to avoid
38             instantiating multiple copies of the same object.
39              
40             Methods such as terms, relations and instances return only those nodes
41             declared to be in the graph
42              
43             =head1 SEE ALSO
44              
45             GOBO::Node
46              
47             GOBO::LinkStatement
48              
49             =cut
50              
51             package GOBO::Graph;
52 14     14   268100 use Moose;
  0            
  0            
53             with 'GOBO::Attributed';
54             use strict;
55             use GOBO::Annotation;
56             use GOBO::ClassExpression::Union;
57             use GOBO::ClassExpression;
58             use GOBO::Formula;
59             use GOBO::Indexes::NodeIndex;
60             use GOBO::Indexes::StatementIndex;
61             use GOBO::InstanceNode;
62             use GOBO::LinkStatement;
63             use GOBO::LiteralStatement;
64             #use GOBO::Node;
65             use GOBO::RelationNode;
66             use GOBO::Statement;
67             use GOBO::Subset;
68             use GOBO::Synonym;
69             use GOBO::TermNode;
70              
71             use overload ('""' => 'as_string');
72              
73             has 'relation_h' => (is => 'rw', isa => 'HashRef[GOBO::TermNode]', default=>sub{{}});
74             has 'term_h' => (is => 'rw', isa => 'HashRef[GOBO::TermNode]', default=>sub{{}});
75             has 'instance_h' => (is => 'rw', isa => 'HashRef[GOBO::InstanceNode]', default=>sub{{}});
76             has 'link_ix' => (is => 'rw', isa => 'GOBO::Indexes::StatementIndex',
77             default=>sub{ new GOBO::Indexes::StatementIndex() },
78             handles => { links => 'statements', add_link => 'add_statement', add_links => 'add_statements', remove_links => 'remove_statements', remove_link => 'remove_statement' },
79             );
80             has 'annotation_ix' => (is => 'rw', isa => 'GOBO::Indexes::StatementIndex',
81             default=>sub{ new GOBO::Indexes::StatementIndex() },
82             handles => { annotations => 'statements', add_annotation => 'add_statement', add_annotations => 'add_statements', annotated_entities => 'referenced_nodes', remove_annotations => 'remove_statements', remove_annotation => 'remove_statement' },
83             );
84             #has 'node_index' => (is => 'rw', isa => 'HashRef[GOBO::Node]', default=>sub{{}});
85             has 'node_index' => (is => 'rw', isa => 'GOBO::Indexes::NodeIndex',
86             default=>sub{ new GOBO::Indexes::NodeIndex() },
87             handles => [ 'nodes' ],
88             );
89             has 'subset_index' => (is => 'rw', isa => 'HashRef[GOBO::Subset]', default=>sub{{}});
90             has 'formulae' => (is => 'rw', isa => 'ArrayRef[GOBO::Formula]', default=>sub{[]});
91              
92              
93             #sub nodes {
94             # my $self = shift;
95             # return $self->node_index->nodes;
96             #}
97              
98             sub referenced_nodes {
99             my $self = shift;
100             return $self->node_index->nodes;
101             }
102              
103             #sub links { shift->link_ix->statements(@_) }
104             #sub add_link { shift->link_ix->add_statement(@_) }
105             #sub add_links { shift->link_ix->add_statements(@_) }
106             #sub remove_link { shift->link_ix->remove_statements([@_]) }
107              
108             #sub annotations { shift->annotation_ix->statements(@_) }
109             #sub add_annotation { shift->annotation_ix->add_statement(@_) }
110             #sub add_annotations { shift->annotation_ix->add_statements(@_) }
111             #sub remove_annotation { shift->annotation_ix->remove_statements([@_]) }
112             #sub annotated_entities { shift->annotation_ix->referenced_nodes }
113              
114             sub has_terms {
115             my $self = shift;
116             return 1 if scalar @{$self->terms};
117             return undef;
118             }
119              
120             sub has_relations {
121             my $self = shift;
122             return 1 if scalar @{$self->relations};
123             return undef;
124             }
125              
126             sub has_instances {
127             my $self = shift;
128             return 1 if scalar @{$self->instances};
129             return undef;
130             }
131              
132             sub has_subsets {
133             my $self = shift;
134             return 1 if scalar @{$self->declared_subsets};
135             return undef;
136             }
137             *has_declared_subsets = \&has_subsets;
138              
139             sub has_formulae {
140             my $self = shift;
141             return 1 if scalar @{$self->formulae};
142             return undef;
143             }
144              
145             sub has_links {
146             my $self = shift;
147             return 1 if scalar @{$self->links};
148             return undef;
149             }
150              
151             sub has_annotations {
152             my $self = shift;
153             return 1 if scalar @{$self->annotations};
154             return undef;
155             }
156              
157             sub has_nodes {
158             my $self = shift;
159             return 1 if scalar @{$self->nodes};
160             return undef;
161             }
162              
163              
164              
165             =head2 declared_subsets
166              
167             - returns ArrayRef[GOBO::Subset]
168              
169             returns the subsets declared in this graph.
170              
171             See also: GOBO::TermNode->subsets() - this returns the subsets a term belongs to
172              
173             =cut
174              
175             # @Override
176             sub declared_subsets {
177             my $self = shift;
178             if (@_) {
179             my $ssl = shift;
180             $self->subset_index->{$_->id} = $_ foreach @$ssl;
181             }
182             return [values %{$self->subset_index()}];
183             }
184              
185             =head2 terms
186              
187             - Returns: ArrayRef[GOBO::TermNode], where each member is a term belonging to this graph
188              
189             =cut
190              
191             sub terms {
192             my $self = shift;
193             #$self->node_index->nodes_by_metaclass('term');
194             return [values %{$self->term_h}];
195             }
196              
197             =head2 get_term
198              
199             - Argument: id Str
200             - Returns: GOBO::TermNode, if term is declared in this graph
201              
202             =cut
203              
204             sub get_term {
205             my $self = shift;
206             my $id = shift;
207             return $self->term_h->{$id};
208             }
209              
210             =head2 get_relation
211              
212             - Argument: id Str
213             - Returns: GOBO::RelationNode, if relation is declared in this graph
214              
215             =cut
216              
217             sub get_relation {
218             my $self = shift;
219             my $id = shift;
220             return $self->relation_h->{$id};
221             }
222              
223             =head2 get_instance
224              
225             - Argument: id Str
226             - Returns: GOBO::InstanceNode, if instance is declared in this graph
227              
228             =cut
229              
230             sub get_instance {
231             my $self = shift;
232             my $id = shift;
233             return $self->instance_h->{$id};
234             }
235              
236              
237             =head2 relations
238              
239             - Returns: ArrayRef[GOBO::RelationNode], where each member is a relation belonging to this graph
240              
241             =cut
242              
243             sub relations {
244             my $self = shift;
245             #$self->node_index->nodes_by_metaclass('relation');
246             return [values %{$self->relation_h}];
247             }
248              
249             =head2 instances
250              
251             - Returns: ArrayRef[GOBO::InstanceNode], where each member is an instance belonging to this graph
252              
253             =cut
254              
255             sub instances {
256             my $self = shift;
257             #$self->node_index->nodes_by_metaclass('instance');
258             return [values %{$self->instance_h}];
259             }
260              
261             =head2 add_term
262              
263             - Arguments: Str or GOBO::Node
264             - Returns: GOBO::TermNode
265             - Side effects: adds the object to the list of terms referenced in this graph. Forces the class to be GOBO::TermNode
266              
267             =cut
268              
269             sub add_term {
270             my $self = shift;
271             my $n = $self->term_noderef(@_);
272             $self->term_h->{$n->id} = $n;
273             return $n;
274             }
275              
276             =head2 add_relation
277              
278             - Arguments: Str or GOBO::Node
279             - Returns: GOBO::RelationNode
280             - Side effects: adds the object to the list of relations referenced in this graph. Forces the class to be GOBO::RelationNode
281              
282             =cut
283              
284             sub add_relation {
285             my $self = shift;
286             my $n = $self->relation_noderef(@_);
287             $self->relation_h->{$n->id} = $n;
288             return $n;
289             }
290              
291             =head2 add_instance
292              
293             - Arguments: Str or GOBO::Node
294             - Returns: GOBO::InstanceNode
295              
296             adds the object to the list of instances referenced in this
297             graph. Forces the class to be GOBO::InstanceNode
298              
299             =cut
300              
301             sub add_instance {
302             my $self = shift;
303             my $n = $self->instance_noderef(@_);
304             $self->instance_h->{$n->id} = $n;
305             return $n;
306             }
307              
308             =head2 remove_node
309              
310             - Arguments: node GOBO::Node, cascade Bool[OPT]
311              
312             unlinks the node from this graph
313              
314             If cascade is 0 or undef, any links to or from this node will remain as dangling links.
315              
316             If cascade is set, then links to and from this node will also be deleted
317              
318             =cut
319              
320             sub remove_node {
321             my $self = shift;
322             my $n = shift;
323             my $cascade = shift;
324             #my $id = ref($n) ? $n->id : $n;
325             my $id = $n->id;
326              
327             if ($self->term_h->{$id}) {
328             delete $self->term_h->{$id};
329             }
330             if ($self->instance_h->{$id}) {
331             delete $self->instance_h->{$id};
332             }
333             if ($self->relation_h->{$id}) {
334             delete $self->relation_h->{$id};
335             }
336             if ($cascade) {
337             $self->remove_link($_) foreach @{$self->get_outgoing_links($n)};
338             $self->remove_link($_) foreach @{$self->get_incoming_links($n)};
339             }
340              
341             return $self->node_index->remove_node($n);
342             }
343              
344             sub add_formula { my $self = shift; push(@{$self->formulae},@_) }
345              
346             =head2 get_outgoing_links (subject GOBO::Node, relation GOBO::RelationNode OPTIONAL)
347              
348             given a subject (child), get target (parent) links
349              
350             if relation is specified, also filters results on relation
351              
352             =cut
353              
354             sub get_outgoing_links {
355             my $self = shift;
356             my $n = shift;
357             my $rel = shift;
358             my @sl = @{$self->link_ix->statements_by_node_id(ref($n) ? $n->id : $n) || []};
359             # if x = a AND r(b), then x r b
360             if (ref($n) && $n->isa('GOBO::ClassExpression::Intersection')) {
361             foreach (@{$n->arguments}) {
362             if ($_->isa('GOBO::ClassExpression::RelationalExpression')) {
363             push(@sl, new GOBO::LinkStatement(node=>$n,relation=>$_->relation,target=>$_->target));
364             }
365             else {
366             push(@sl, new GOBO::LinkStatement(node=>$n,relation=>'is_a',target=>$_));
367             }
368             }
369             }
370             if ($rel) {
371             # TODO: use indexes to make this faster
372             my $rid = ref($rel) ? $rel->id : $rel;
373             @sl = grep {$_->relation->id eq $rid} @sl;
374             }
375             return \@sl;
376             }
377              
378             # @Deprecated
379             *get_target_links = \&get_outgoing_links;
380              
381             =head2 get_incoming_links (subject GOBO::Node, relation GOBO::RelationNode OPTIONAL)
382              
383             given a subject (child), get target (parent) links
384              
385             if relation is specified, also filters results on relation
386              
387             =cut
388              
389             sub get_incoming_links {
390             my $self = shift;
391             my $n = shift;
392             my $rel = shift;
393             my @sl = @{$self->link_ix->statements_by_target_id(ref($n) ? $n->id : $n) || []};
394             if ($rel) {
395             # TODO: use indexes to make this faster
396             my $rid = ref($rel) ? $rel->id : $rel;
397             @sl = grep {$_->relation->id eq $rid} @sl;
398             }
399             return \@sl;
400             }
401              
402             =head2 get_is_a_roots
403              
404             - Argument: none
405             - Returns: ArrayRef[GOBO::TermNode]
406              
407             returns terms that lack an is_a parent
408              
409             =cut
410              
411             sub get_is_a_roots {
412             my $self = shift;
413             return $self->get_roots('is_a');
414             }
415              
416             =head2 get_roots
417              
418             - Argument: relation Str or OBO::RelationNode [OPTIONAL]
419             - Returns: ArrayRef[GOBO::TermNode]
420              
421             returns terms that lack a parent by the given relation. If no relation
422             specified, then returns terms that lack a parent by any relation
423              
424             =cut
425              
426             sub get_roots {
427             my $self = shift;
428             my $rel = shift;
429             my @roots = ();
430             foreach my $term (@{$self->terms || []}) {
431             if (!{@$self->get_outgoing_links($term, $rel)}) {
432             push(@roots,$term);
433             }
434             }
435             return \@roots;
436             }
437              
438             # given a node ID or a node object, returns the corresponding
439             # node in the graph. If no such node exists, one will be created.
440             sub noderef {
441             my $self = shift;
442             my $id = shift; # Str or GOBO::Node
443             my $ix = $self->node_index;
444              
445             my $n_obj;
446             if (ref($id)) {
447             # $id is actually a GOBO::Node
448             $n_obj = $id;
449             $id = $id->id;
450             }
451             else {
452             if ($id =~ /\s/) {
453             confess("attempted to noderef '$id' -- no whitespace allowed in ID.");
454             }
455             }
456              
457             if ($ix->node_by_id($id)) { # already in the index
458             $n_obj = $ix->node_by_id($id);
459             }
460             else {
461             if (! $n_obj) {
462             $n_obj = new GOBO::Node(id=>$id);
463             }
464             $ix->add_node( $n_obj );
465             }
466             return $n_obj;
467             }
468              
469             # given a node ID or a node object, returns the corresponding
470             # node in the graph. If no such node exists, one will be created.
471             # Forces the resulting object to be a TermNode.
472             sub term_noderef {
473             my $self = shift;
474             my $n = $self->noderef(@_);
475             if (!$n->isa('GOBO::TermNode')) {
476             bless $n, 'GOBO::TermNode';
477             }
478             return $n;
479             }
480              
481             # given a node ID or a node object, returns the corresponding
482             # node in the graph. If no such node exists, one will be created.
483             # Forces the resulting object to be a RelationNode.
484             sub relation_noderef {
485             my $self = shift;
486             my $n = $self->noderef(@_);
487             if (!$n->isa('GOBO::RelationNode')) {
488             bless $n, 'GOBO::RelationNode';
489             }
490             return $n;
491             }
492              
493             # given a node ID or a node object, returns the corresponding
494             # node in the graph. If no such node exists, one will be created.
495             # Forces the resulting object to be an InstanceNode.
496             sub instance_noderef {
497             my $self = shift;
498             my $n = $self->noderef(@_);
499             if (!$n->isa('GOBO::InstanceNode')) {
500             bless $n, 'GOBO::InstanceNode';
501             }
502             return $n;
503             }
504              
505             # given a node ID or a node object, returns the corresponding
506             # node in the graph. If no such node exists, one will be created.
507             # Forces the resulting object to be a Subset.
508             sub subset_noderef {
509             my $self = shift;
510             my $ssid = shift;
511             my $n = $self->subset_index->{$ssid};
512             if (!$n) {
513             # TODO: fail?
514             # warn "creating subset $ssid";
515             $n = new GOBO::Subset(id=>$ssid);
516             $self->subset_index->{$ssid} = $n;
517             }
518             if (!$n->isa('GOBO::Subset')) {
519             bless $n, 'GOBO::Subset';
520             }
521             return $n;
522             }
523              
524             sub parse_idexprs {
525             my $self = shift;
526             my @nodes = @{$self->node_index->nodes};
527             my %done = ();
528             while (my $n = shift @nodes) {
529             next if $done{$n->id};
530             if ($n->id =~ /\^/) {
531             my $ce = new GOBO::ClassExpression->parse_idexpr($self,$n->id);
532             #printf STDERR "$n => $ce\n";
533             if (!$n->can('logical_definition')) {
534             bless $n, 'GOBO::Term';
535             }
536             $n->logical_definition($ce);
537             foreach my $arg (@{$ce->arguments}) {
538             push(@nodes,$n);
539             printf STDERR "n=$n\n";
540             }
541             }
542             $done{$n->id} = 1;
543             }
544             }
545              
546             # logical definitions can be directly attached to TermNodes, or they can be
547             # present in the graph as intersection links
548             # TBD : move to utility class?
549             use GOBO::ClassExpression::RelationalExpression;
550             use GOBO::ClassExpression::Intersection;
551             use GOBO::ClassExpression::Union;
552             sub convert_intersection_links_to_logical_definitions {
553             my $self = shift;
554             my @xplinks = ();
555             my @nlinks = ();
556             my %xpnodeh = ();
557             foreach (@{$self->links}) {
558             if($_->is_intersection) {
559             push(@xplinks, $_);
560             push(@{$xpnodeh{$_->node->id}}, $_);
561             }
562             else {
563             push(@nlinks, $_);
564             }
565             }
566             if (@xplinks) {
567             $self->links(\@nlinks);
568             foreach my $nid (keys %xpnodeh) {
569             my $n = $self->noderef($nid);
570             my @exprs =
571             map {
572             if ($_->relation->is_subsumption) {
573             $_->target;
574             }
575             else {
576             new GOBO::ClassExpression::RelationalExpression(relation=>$_->relation, target=>$_->target);
577             }
578             } @{$xpnodeh{$nid}};
579             if (@exprs < 2) {
580             $self->throw("invalid intersection links for $nid. Need at least 2, you have @exprs");
581             }
582             $n->logical_definition(new GOBO::ClassExpression::Intersection(arguments=>\@exprs));
583             }
584             }
585             return;
586             }
587              
588             sub as_string {
589             my $self = shift;
590             return
591             join('',
592             (map { "$_\n" } @{$self->links}),
593             (map { "$_\n" } @{$self->annotations}),
594             );
595             }
596              
597             1;
598