File Coverage

blib/lib/Bio/RNA/Treekin/Record.pm
Criterion Covered Total %
statement 110 157 70.0
branch 28 40 70.0
condition 2 5 40.0
subroutine 24 34 70.5
pod 13 14 92.8
total 177 250 70.8


line stmt bran cond sub pod time code
1             # Bio/RNA/Treekin/Record.pm
2              
3             # Stores a data from a single row of the Treekin file, i.e. the populations of
4             # all minima at a given time point.
5             package Bio::RNA::Treekin::Record;
6             our $VERSION = '0.03';
7              
8 4     4   69 use 5.006;
  4         14  
9 4     4   20 use strict;
  4         10  
  4         119  
10 4     4   22 use warnings;
  4         7  
  4         112  
11              
12 4     4   2263 use Moose;
  4         1900539  
  4         25  
13 4     4   32021 use MooseX::StrictConstructor;
  4         123708  
  4         17  
14 4     4   39074 use namespace::autoclean;
  4         9  
  4         27  
15              
16 4     4   1980 use autodie qw(:all);
  4         47101  
  4         34  
17 4     4   83304 use Scalar::Util qw(reftype openhandle);
  4         11  
  4         236  
18 4     4   25 use List::Util qw(first pairmap max uniqnum all);
  4         8  
  4         263  
19 4     4   26 use Carp qw(croak);
  4         9  
  4         153  
20              
21 4     4   2230 use Bio::RNA::Treekin::PopulationDataRecord;
  4         17  
  4         280  
22              
23 4     4   59 use overload '""' => \&stringify;
  4         10  
  4         31  
24              
25              
26             has '_population_data' => (
27             is => 'ro',
28             required => 1,
29             init_arg => 'population_data',
30             );
31              
32             has 'date' => (is => 'ro', required => 1);
33             has 'sequence' => (is => 'ro', required => 1);
34             has 'method' => (is => 'ro', required => 1);
35             has 'start_time' => (is => 'ro', required => 1);
36             has 'stop_time' => (is => 'ro', required => 1);
37             has 'temperature' => (is => 'ro', required => 1);
38             has 'basename' => (is => 'ro', required => 1);
39             has 'time_increment' => (is => 'ro', required => 1);
40             has 'degeneracy' => (is => 'ro', required => 1);
41             has 'absorbing_state' => (is => 'ro', required => 1);
42             has 'states_limit' => (is => 'ro', required => 1);
43              
44             # Add optional attributes including predicate.
45             has $_ => (
46             is => 'ro',
47             required => 0,
48             predicate => "has_$_",
49             )
50             foreach qw(
51             info
52             init_population
53             rates_file
54             file_index
55             cmd
56             of_iterations
57             );
58              
59             # Get number of population data rows stored.
60             sub population_data_count {
61 2     2 1 4492 my ($self) = @_;
62              
63 2         5 my $data_count = @{ $self->_population_data };
  2         129  
64 2         12 return $data_count;
65             }
66              
67             # Number of states / minima in this simulation.
68             # Get number of mins in the first population record; it should be the
69             # same for all records.
70             sub min_count {
71 15     15 1 28 my $self = shift;
72              
73 15         39 my $first_pop = $self->population(0);
74 15 50       36 confess 'min_count: no population data present'
75             unless defined $first_pop;
76              
77 15         63 my $min_count = $first_pop->min_count;
78              
79 15         36 return $min_count;
80             }
81              
82             # Return a list of all minima, i. e. 1..n, where n is the total number of
83             # minima.
84             sub mins {
85 0     0 1 0 my ($self) = @_;
86 0         0 my @mins = 1..$self->min_count;
87              
88 0         0 return @mins;
89             }
90              
91             # Keep only the population data for the selected minima, remove all other.
92             # Will NOT rescale populations, so they may no longer sum up to 1.
93             # Arguments:
94             # mins: List of mins to keep. Will be sorted and uniq'ed (cf. splice()).
95             # Returns the return value of splice().
96             sub keep_mins {
97 0     0 1 0 my ($self, @kept_mins) = @_;
98 0         0 @kept_mins = uniqnum sort {$a <=> $b} @kept_mins; # sort / uniq'ify
  0         0  
99 0         0 return $self->splice_mins(@kept_mins);
100             }
101              
102             # Keep only the population data for the selected minima, remove all other.
103             # May duplicate and re-order.
104             # mins: List of mins to keep. Will be used as is.
105             # Returns itself.
106             sub splice_mins {
107 0     0 1 0 my ($self, @kept_mins) = @_;
108              
109 0         0 my $min_count = $self->min_count;
110             confess 'Cannot splice, minimum out of bounds'
111 0 0   0   0 unless all {$_ >= 1 and $_ <= $min_count} @kept_mins;
  0 0       0  
112              
113             # Directly update raw population data here instead of doing tons of
114             # calls passing the same min array.
115 0         0 my @kept_indices = map {$_ - 1} @kept_mins;
  0         0  
116 0         0 for my $pop_data (@{$self->_population_data}) { # each point in time
  0         0  
117 0         0 my $raw_pop_data = $pop_data->_populations;
118 0         0 @{$raw_pop_data} = @{$raw_pop_data}[@kept_indices];
  0         0  
  0         0  
119             }
120              
121 0         0 return $self;
122             }
123              
124             # Get the maximal population for the given minimum over all time points.
125             sub max_pop_of_min {
126 0     0 1 0 my ($self, $min) = @_;
127 0         0 my $max_pop = '-Inf';
128 0         0 for my $pop_data (@{$self->_population_data}) { # each point in time
  0         0  
129 0         0 $max_pop = max $max_pop, $pop_data->of_min($min); # update max
130             }
131 0         0 return $max_pop;
132             }
133              
134             # For a given minimum, return all population values in chronological
135             # order.
136             # Arguments:
137             # min: Minimum for which to collect the population data.
138             # Returns a list of population values in chronological order.
139             sub pops_of_min {
140 0     0 1 0 my ($self, $min) = @_;
141              
142 0         0 my @pops_of_min = map { $_->of_min($min) } $self->populations;
  0         0  
143              
144 0         0 return @pops_of_min;
145             }
146              
147             # Final population data record, i.e. the result of the simulation.
148             sub final_population {
149 0     0 1 0 my ($self) = @_;
150              
151 0         0 my $final_population_data
152             = $self->population($self->population_data_count - 1);
153              
154 0         0 return $final_population_data;
155             }
156              
157             # Get the i-th population data record (0-based indexing).
158             sub population {
159 52     52 1 23109 my ($self, $i) = @_;
160              
161 52         1953 my $population_record = $self->_population_data->[$i];
162 52         134 return $population_record;
163             }
164              
165             # Return all population data records.
166             sub populations {
167 0     0 1 0 return @{ $_[0]->_population_data };
  0         0  
168             }
169              
170             # Add a new minimum with all-zero entries. Data can then be appended to
171             # this new min.
172             # Returns the index of the new minimum.
173             sub add_min {
174 0     0 1 0 my $self = shift;
175 0         0 my $new_min_count = $self->min_count + 1;
176              
177             # Increase the min count of all population data records by one.
178             $_->set_min_count($new_min_count)
179 0         0 foreach @{ $self->_population_data }, $self->init_population;
  0         0  
180              
181 0         0 return $new_min_count; # count == highest index
182             }
183              
184             # Given a list of population data records, append them to the population data
185             # of this record. The columns of the added data can be re-arranged on the fly by
186             # providing a mapping (hash ref) giving for each minimum in the population
187             # data to be added (key) a minimum in the current population data
188             # (value) to which the new minimum should be swapped. If no data is provided
189             # for some minimum of this record, its population is set to zero in the
190             # newly added entries.
191             # Arguments:
192             # pop_data_ref: ref to the array of population data to be added
193             # append_to_min_ref:
194             # hash ref describing which mininum in pop_data_ref (key)
195             # should be mapped to which minimum in this record (value)
196             # The passed population data objects are modified.
197             sub append_pop_data {
198 0     0 1 0 my ($self, $pop_data_ref, $append_to_min_ref) = @_;
199              
200 0 0       0 if (defined $append_to_min_ref) {
201 0         0 my $min_count = $self->min_count;
202 0         0 $_->transform($append_to_min_ref, $min_count) foreach @$pop_data_ref;
203             }
204              
205 0         0 push @{ $self->_population_data }, @$pop_data_ref;
  0         0  
206              
207 0         0 return;
208             }
209              
210             # Decode a single header line into a key and a value, which are returned.
211             sub _get_header_line_key_value {
212 150     150   363 my ($class, $header_line) = @_;
213              
214             # key and value separated by first ':' (match non-greedy!)
215 150         679 my ($key, $value) = $header_line =~ m{ ^ ( .+? ) : [ ] ( .* ) $ }x;
216              
217 150 50       331 confess "Invalid key in header line:\n$header_line"
218             unless defined $key;
219              
220             # Convert key to lower case and replace spaces by underscores.
221 150         436 $key = (lc $key) =~ s/\s+/_/gr;
222              
223 150         403 return ($key, $value);
224             }
225              
226             # Decode the initial population from the Treekin command line. The
227             # population is given as multiple --p0 a=x switches, where a is the state
228             # index and x is the fraction of population initially present in this
229             # state.
230             # Arguments:
231             # command: the command line string used to call treekin
232             # Returns a hash ref containing the initial population of each state a at
233             # position a (1-based).
234             sub _parse_init_population_from_cmd {
235 7     7   18 my ($class, $command) = @_;
236              
237 7         94 my @command_parts = split /\s+/, $command;
238              
239             # Extract the initial population strings given as (multiple) arguments
240             # --p0 to Treekin from the Treekin command.
241 7         18 my @init_population_strings;
242 7         20 while (@command_parts) {
243 116 100       251 if (shift @command_parts eq '--p0') {
244             # Next value should be a population value.
245 32 50       58 confess 'No argument following a --p0 switch'
246             unless @command_parts;
247 32         65 push @init_population_strings, shift @command_parts;
248             }
249             }
250              
251             # Store population of state i in index i-1.
252 7         14 my @init_population;
253 7         15 foreach my $init_population_string (@init_population_strings) {
254 32         112 my ($state, $population) = split /=/, $init_population_string;
255 32         77 $init_population[$state-1] = $population;
256             }
257              
258             # If no population was specified on the cmd line, init 100% in state 1
259 7 100       21 $init_population[0] = 1 unless @init_population_strings;
260              
261             # Set undefined states to zero.
262 7   50     44 $_ //= 0. foreach @init_population;
263              
264 7         346 my $init_population_record
265             = Bio::RNA::Treekin::PopulationDataRecord->new(
266             time => 0,
267             populations => \@init_population,
268             );
269 7         22 return $init_population_record;
270             }
271              
272             sub _parse_header_lines {
273 10     10   26 my ($class, $header_lines_ref) = @_;
274              
275 10         18 my @header_args;
276 10         26 foreach my $line (@$header_lines_ref) {
277 150         279 my ($key, $value) = $class->_get_header_line_key_value($line);
278              
279             # Implement special handling for certain keys.
280 150 100       345 if ($key eq 'rates_file') {
    100          
281             # remove (#index) from file name and store the value
282 7         37 my ($file_name, $file_index)
283             = $value =~ m{ ^ (.+) [ ] [(] [#] (\d+) [)] $ }x;
284 7         26 push @header_args, (
285             rates_file => $file_name,
286             file_index => $file_index,
287             );
288             }
289             elsif ($key eq 'cmd') {
290             # Extract initial population from Treekin command.
291 7         20 my $init_population_ref
292             = $class->_parse_init_population_from_cmd($value);
293 7         24 push @header_args, (
294             cmd => $value,
295             init_population => $init_population_ref,
296             );
297             }
298             else {
299             # For the rest, just push key and value as constructor args.
300 136         313 push @header_args, ($key => $value);
301             }
302             }
303 10         95 return @header_args;
304             }
305              
306              
307             # Read all lines from the given handle and separate it into header lines
308             # and data lines.
309             sub _read_record_lines {
310 10     10   27 my ($class, $record_handle) = @_;
311              
312             # Separate lines into header and population data. All header lines
313             # begin with a '# ' (remove it!)
314             # Note: Newer versions of treekin also add header info *below* data lines.
315 10         23 my ($current_line, @header_lines, @population_data_lines);
316 10         210 while (defined ($current_line = <$record_handle>)) {
317 342 50 33     3114 next if $current_line =~ /^@/ # drop xmgrace annotations
318             or $current_line =~ m{ ^ \s* $ }x; # or empty lines
319              
320             # Header lines start with '# ', remove it.
321 342 100       962 if ($current_line =~ s/^# //) { # header line
322 150         694 push @header_lines, $current_line;
323             }
324             else { # data line
325 192         698 push @population_data_lines, $current_line;
326             }
327             }
328              
329             # Sanity checks.
330 10 50       90 confess 'No header lines found in Treekin file'
331             unless @header_lines;
332 10         44 chomp @header_lines;
333              
334 10 50       30 confess 'No population data lines found in Treekin file'
335             unless @population_data_lines;
336 10         36 chomp @population_data_lines;
337              
338             ###################### Old implementation #################
339              
340             # # Separate lines into header and population data. All header lines
341             # # begin with a '# ' (remove it!)
342             # my ($current_line, @header_lines);
343             # while (defined ($current_line = <$record_handle>)) {
344             # next if $current_line =~ /^@/; # drop xmgrace annotations
345             # # header lines start with '# ', remove it
346             # last unless $current_line =~ s/^# //;
347             # push @header_lines, $current_line;
348             # }
349             # chomp @header_lines;
350              
351             # confess 'Unexpected end of record while parsing header'
352             # unless defined $current_line;
353              
354             # my @population_data_lines = ($current_line);
355             # while (defined ($current_line = <$record_handle>)) {
356             # push @population_data_lines, $current_line;
357             # }
358             # chomp @population_data_lines;
359              
360 10         49 return \@header_lines, \@population_data_lines;
361             }
362              
363             sub _parse_population_data_lines {
364 10     10   22 my ($class, $population_data_lines_ref) = @_;
365              
366             my @population_data
367 10         28 = map { Bio::RNA::Treekin::PopulationDataRecord->new($_) }
  161         5533  
368             @$population_data_lines_ref
369             ;
370              
371 9         33 return (population_data => \@population_data);
372             }
373              
374             around BUILDARGS => sub {
375             my $orig = shift;
376             my $class = shift;
377              
378             # Call original constructor if passed more than one arg.
379             return $class->$orig(@_) unless @_ == 1;
380              
381             # Retrive file handle or pass on hash ref to constructor.
382             my $record_handle;
383             if (reftype $_[0]) {
384             if (reftype $_[0] eq reftype {}) { # arg hash passed,
385             return $class->$orig(@_); # pass on as is
386             }
387             elsif (reftype $_[0] eq reftype \*STDIN) { # file handle passed
388             $record_handle = shift;
389             }
390             else {
391             croak 'Invalid ref type passed to constructor';
392             }
393             }
394             else { # file name passed
395             my $record_file = shift;
396             open $record_handle, '<', $record_file;
397             }
398              
399             # Read in file.
400             my ($header_lines_ref, $population_data_lines_ref)
401             = $class->_read_record_lines($record_handle);
402              
403             # Parse file.
404             my @header_args = $class->_parse_header_lines($header_lines_ref);
405             my @data_args
406             = $class->_parse_population_data_lines($population_data_lines_ref);
407              
408             my %args = (@header_args, @data_args);
409             return $class->$orig(\%args);
410             };
411              
412             sub BUILD {
413 9     9 0 17 my $self = shift;
414              
415             # Force construction despite laziness.
416 9         31 $self->min_count;
417              
418             # Adjust min count of initial population as it was not known when
419             # initial values were extracted from Treekin cmd.
420 9 100       335 $self->init_population->set_min_count( $self->min_count )
421             if $self->has_init_population;
422             }
423              
424             sub stringify {
425 8     8 1 172 my $self = shift;
426              
427             # Format header line value of rates file entry.
428             my $make_rates_file_val = sub {
429 6     6   169 $self->rates_file . ' (#' . $self->file_index . ')';
430 8         47 };
431              
432             # Header
433 8 100       309 my @header_entries = (
    100          
    100          
434             $self->has_rates_file ? ('Rates file' => $make_rates_file_val->()) : (),
435             $self->has_info ? ('Info' => $self->info) : (),
436             $self->has_cmd ? ('Cmd' => $self->cmd) : (),
437             'Date' => $self->date,
438             'Sequence' => $self->sequence,
439             'Method' => $self->method,
440             'Start time' => $self->start_time,
441             'Stop time' => $self->stop_time,
442             'Temperature' => $self->temperature,
443             'Basename' => $self->basename,
444             'Time increment' => $self->time_increment,
445             'Degeneracy' => $self->degeneracy,
446             'Absorbing state' => $self->absorbing_state,
447             'States limit' => $self->states_limit,
448             );
449              
450 8     102   60 my $header_str = join "\n", pairmap { "# $a: $b" } @header_entries;
  102         215  
451              
452             # Population data
453             my $population_str
454 8         37 = join "\n", map { "$_" } @{ $self->_population_data };
  132         383  
  8         253  
455              
456             # Footer (new Treekin versions only).
457 8 100       324 my $footer_str = $self->has_of_iterations
458             ? '# of iterations: ' . $self->of_iterations
459             : q{};
460              
461 8         75 my $self_as_str = $header_str . "\n" . $population_str;
462 8 100       29 $self_as_str .= "\n" . $footer_str if $footer_str;
463              
464 8         114 return $self_as_str;
465             }
466              
467             __PACKAGE__->meta->make_immutable;
468              
469             1; # End of Bio::RNA::Treekin::Record
470              
471              
472             __END__
473              
474              
475             =pod
476              
477             =encoding UTF-8
478              
479             =head1 NAME
480              
481             Bio::RNA::Treekin::Record - Parse, query, and manipulate I<Treekin> output.
482              
483             =head1 SYNOPSIS
484              
485             use Bio::RNA::Treekin;
486              
487             =head1 DESCRIPTION
488              
489             Parses a regular output file of I<Treekin>. Allows to query population data
490             as well as additional info from the header. New minima can be generated. The
491             stringification returns, again, a valid I<Treekin> file which can be, e. g.,
492             visualized using I<Grace>.
493              
494             =head1 ATTRIBUTES
495              
496             These attributes of the class allow to query various data from the header of
497             the input file.
498              
499             =head2 date
500              
501             The time and date of the I<Treekin> run.
502              
503             =head2 sequence
504              
505             The RNA sequence for which the simulation was computed.
506              
507             =head2 method
508              
509             The method used to build the transition matrix as documented for the
510             C<--method> switch of I<Treekin>.
511              
512             =head2 start_time
513              
514             Initial time of the simulation.
515              
516             =head2 stop_time
517              
518             Time at which the simulation stops.
519              
520             =head2 temperature
521              
522             Temperature of the simulation in degrees Celsius.
523              
524             =head2 basename
525              
526             Name of the input file. May be C<< <stdin> >> if data was read from standard
527             input.
528              
529             =head2 time_increment
530              
531             Factor by which the time is multiplied in each simulation step (roughly, the
532             truth is more complicated).
533              
534             =head2 degeneracy
535              
536             Whether to consider degeneracy in transition rates.
537              
538             =head2 absorbing_state
539              
540             The states specified as absorbing do not have any outgoing transitions and
541             thus serve as "population sinks" during the simulation.
542              
543             =head2 states_limit
544              
545             Maximum number of states (???). Value is always (?) 2^31 = 2147483647.
546              
547             =head2 info
548              
549             A free text field containing additional comments.
550              
551             Only available in I<some> of the records of a I<BarMap> multi-record file. Use
552             predicate C<has_info> to check whether this attribute is available.
553              
554             =head2 init_population
555              
556             The initial population specified by the user. This information is extracted
557             from the C<cmd> attribute.
558              
559             Only available in I<BarMap>'s multi-record files. Use predicate
560             C<has_init_population> to check whether this attribute is available.
561              
562             =head2 rates_file
563              
564             The file that the rate matrix was read from.
565              
566             Only available in I<BarMap>'s multi-record files. Use predicate
567             C<has_rates_file> to check whether this attribute is available.
568              
569             =head2 file_index
570              
571             Zero-based index given to the input files in the order they were read.
572             Extracted from the C<rates_file> attribute.
573              
574             Use predicate C<has_file_index> to check whether this attribute is available.
575              
576             =head2 cmd
577              
578             The command used to invoke I<Treekin>. Only available in I<BarMap>'s
579             multi-record files.
580              
581             Use predicate C<has_cmd> to check whether this attribute is available.
582              
583              
584             =head1 METHODS
585              
586             These methods allow the construction, querying and manipulation of the record
587             objects and its population data.
588              
589             =head2 Bio::RNA::Treekin::Record->new($treekin_file)
590              
591             =head2 Bio::RNA::Treekin::Record->new($treekin_handle)
592              
593             Construct a new record from a (single) I<Treekin> file.
594              
595             =head2 $record->population_data_count
596              
597             Return the number of population data records, i. e. the number of simulated
598             time steps, including the start time.
599              
600             =head2 $record->min_count
601              
602             Return the number of minima.
603              
604             =head2 $record->mins
605              
606             Return the list of all contained minima, i. e. C<< 1...$record->min_count >>
607              
608             =head2 $record->keep_mins(@kept_minima)
609              
610             Remove all minima but the ones from C<@kept_minima>. The list is sorted and
611             de-duplicated first.
612              
613             =head2 $record->splice_mins(@kept_minima)
614              
615             Like C<keep_mins()>, but do not sort / de-duplicate, but use C<@kept_minima>
616             as is. This can be used to remove, duplicate or reorder minima.
617              
618             =head2 $record->max_pop_of_min($minimum)
619              
620             Get the maximum population value of all time points for a specific C<$minimum>.
621              
622             =head2 $record->pops_of_min($minimum)
623              
624             Get a list of the populations at all time points (in chronological order) for
625             a single C<$minimum>.
626              
627             =head2 $record->final_population
628              
629             Get the last population data record, an object of class
630             L<Bio::RNA::Treekin::PopulationDataRecord>. It contains the population data
631             for all minima at the C<stop_time>.
632              
633             =head2 $record->population($i)
634              
635             Get the C<$i>-th population data record, an object of class
636             L<Bio::RNA::Treekin::PopulationDataRecord>. C<$i> is a zero-based index in
637             chronological order.
638              
639             =head2 $record->populations
640              
641             Returns the list of all population data records. Useful for iterating.
642              
643             =head2 $record->add_min
644              
645             Add a single new minimum with all-zero entries. Data can then be appended to
646             this new min using C<append_pop_data()>.
647              
648             Returns the index of the new minimum.
649              
650             =head2 $record->append_pop_data($pop_data_ref, $append_to_min_ref)
651              
652             Given a list of population data records C<$pop_data_ref>, append them to the
653             population data of this record.
654              
655             The columns of the added data can be
656             re-arranged on the fly by providing a mapping C<$append_to_min_ref> (a hash
657             ref) giving for each minimum in C<$pop_data_ref> (key) a
658             minimum in the current population data (value) to which the new minimum should
659             be swapped. If no data is provided for some minimum of this record, its
660             population is set to zero in the newly added entries.
661              
662             =head2 $record->stringify
663              
664             =head2 "$record"
665              
666             Returns the record as a I<Treekin> file.
667              
668             =head1 AUTHOR
669              
670             Felix Kuehnl, C<< <felix@bioinf.uni-leipzig.de> >>
671              
672              
673             =head1 BUGS
674              
675             Please report any bugs or feature requests by raising an issue at
676             L<https://github.com/xileF1337/Bio-RNA-Treekin/issues>.
677              
678             You can also do so by mailing to C<bug-bio-rna-treekin at rt.cpan.org>,
679             or through the web interface at
680             L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Bio-RNA-Treekin>. I will be
681             notified, and then you'll automatically be notified of progress on your bug as
682             I make changes.
683              
684              
685             =head1 SUPPORT
686              
687             You can find documentation for this module with the perldoc command.
688              
689             perldoc Bio::RNA::Treekin
690              
691              
692             You can also look for information at:
693              
694             =over 4
695              
696             =item * Github: the official repository
697              
698             L<https://github.com/xileF1337/Bio-RNA-Treekin>
699              
700             =item * RT: CPAN's request tracker (report bugs here)
701              
702             L<https://rt.cpan.org/NoAuth/Bugs.html?Dist=Bio-RNA-Treekin>
703              
704             =item * AnnoCPAN: Annotated CPAN documentation
705              
706             L<http://annocpan.org/dist/Bio-RNA-Treekin>
707              
708             =item * CPAN Ratings
709              
710             L<https://cpanratings.perl.org/d/Bio-RNA-Treekin>
711              
712             =item * Search CPAN
713              
714             L<https://metacpan.org/release/Bio-RNA-Treekin>
715              
716             =back
717              
718              
719             =head1 LICENSE AND COPYRIGHT
720              
721             Copyright 2019-2021 Felix Kuehnl.
722              
723             This program is free software: you can redistribute it and/or modify
724             it under the terms of the GNU General Public License as published by
725             the Free Software Foundation, either version 3 of the License, or
726             (at your option) any later version.
727              
728             This program is distributed in the hope that it will be useful,
729             but WITHOUT ANY WARRANTY; without even the implied warranty of
730             MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
731             GNU General Public License for more details.
732              
733             You should have received a copy of the GNU General Public License
734             along with this program. If not, see L<http://www.gnu.org/licenses/>.
735              
736              
737             =cut
738              
739             # End of Bio/RNA/Treekin/Record.pm