package ConvertWell;

#  ConvertWell.pm : conversion of well locations among various formats
#
#  Copyright (C) 1997, 2002 Michael C. Wendl
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

#__STANDARD PERL PACKAGES
   require 5.6.0;
   use strict;
   use Carp;

#__SET UP EXPORTING
   use vars qw( @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION );
   require Exporter;
   @ISA = qw(Exporter);
   @EXPORT = qw();
   @EXPORT_OK = qw( To384 To96 ToGel );
   %EXPORT_TAGS = ( all => [ qw( To384 To96 ToGel ) ] );
   $VERSION = '2.1';
   my $pkg = 'ConvertWell';

#__REGULAR EXPRESSION FOR PLATE (ARCHIVE) NAMES

#  match the standard GSC production convention of [a-z][a-z][a-z]?\d\d or
#  also match the automated prefinish "control" read convention

   my $archive = '(?:[a-z][a-z][a-z]?\d\d|\w+cntrl)';

#__ALPHABETIC-NUMERIC TABLES
   %ConvertWell::a2n = (
      'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7,
      'h' => 8, 'i' => 9, 'j' => 10, 'k' => 11, 'l' => 12, 'm' => 13, 'n' => 14,
      'o' => 15, 'p' => 16,
   );
   %ConvertWell::n2a = (
      1 => 'a', 2 => 'b', 3 => 'c', 4 => 'd', 5 => 'e', 6 => 'f', 7 => 'g',
      8 => 'h', 9 => 'i', 10 => 'j', 11 => 'k', 12 => 'l', 13 => 'm', 14 => 'n',
      15 => 'o', 16 => 'p',
   );

################################################################################
##                                                                            ##
##              P U B L I C   R O U T I N E S   A R E   H E R E               ##
##                                                                            ##
################################################################################

#  ======
#  TO 384   converts a 96 well location to a 384 well location
#  ======   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sub To384 {
   my ($name, $sub) = @_;
   my ($plate, $well, $col, $suffix);

#__SUBSET MUST BE LOWER CASE
   $sub = lc $sub;
   $sub =~ s/-//;

#__CHECK INTEGRITY OF FIRST ARGUMENT (THE ONE WITH THE 96 WELL LOCATION IN IT)
   unless ( $name =~ /
         ^($archive)?             # save plate name to $1 if it's present
         (                        # extract well name and save to $2
         [a-h]                    # only rows a to h in 96 format
         (0[1-9]|1[012])          # only cols 01 to 12 in 96 format - save to $3
         )
         (.*)$                    # save all chars after well name in $4
         /x ) {                   # x option allows these comments
      croak "'$name' does not contain a valid well name or 96 template name";
   } else {
      ($plate, $well, $col, $suffix) = ($1, $2, $3, $4);
      $well = lc $well;
      $plate = '' unless defined $plate;
      $suffix = '' unless defined $suffix;
   }

#__MUST BE EITHER A "QUADRANT" CONVERSION - I.E. A1, A2, B1, B2
   my ($nrow, $ncol);
   if ($sub =~ /^[ab][12]$/) {

   #__GET NUMERICAL VALUES FOR THE ROW (LETTER) & COLUMN (NUMBER) FOR WELL & SUB
      my $irow = $ConvertWell::a2n{ substr ($sub, 0, 1) };
      my $icol = substr ($sub, 1, 1);
      my $mrow = $ConvertWell::a2n{ substr ($well, 0, 1) };
      my $mcol = $col;

   #__DO THE ACTUAL CONVERSION FROM 96 TO 384
      $nrow = 2 * $mrow - 2 + $irow;
      $ncol = 2 * $mcol - 2 + $icol;

#__OR A "STREAK" CONVERSION
   } elsif ($sub eq 'ad' || $sub eq 'eh' || $sub eq 'il' || $sub eq 'mp') {

   #__OFFSET INTO 384
      my $irow = $ConvertWell::a2n{ substr ($sub, 0, 1) };
      my $mrow = $ConvertWell::a2n{ substr ($well, 0, 1) };
      my $icol = ($mrow - 1) % 2;
      my $mcol = $col;

   #__TWICE AS MANY COLUMNS PER ROW
      $nrow = int(($mrow - 1) / 2) + $irow;
      $ncol = $mcol + $icol * 12;

#__OR WE HAVE AN UNRECOGNIZED SUBSET
   } else {
      croak "'$sub' is not valid (a1, a2, b1, b2, ad, eh, il, mp)";
   }

#__MAKE SURE COLUMN IS TWO-DIGIT -- PREPEND A ZERO FOR VALS ONE TO NINE
   substr ($ncol, 0, 0) = "0" if length ($ncol) == 1;

#__TRANSLATE BACK TO THE ALPHA-NUMERIC DESIGNATION (NOW IN 384 FORMAT)
   $well = join ("", $ConvertWell::n2a{$nrow}, $ncol);
   $name = join ("", $plate, $well, $suffix);

#__RETURN THE RESULTING 384 WELL LOCATION
   return $name;
}

#  =====
#  TO 96   converts a 384 well location to a 96 well location and sub-position
#  =====   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sub To96 {
   my ($name, $type, $loader) = @_;
   my ($plate, $well, $col, $suffix, $nrow, $ncol, $lane);

#__ARGUMENT MUST BE LOWER CASE
   $type = lc $type;

#__CHECK INTEGRITY OF ARGUMENT
   if ($type eq 'gel') {
      $lane = $name;

   #__SHOULD BE A NUMBER AND NOT A MARKER LANE
      unless ($lane =~ m/^\d+$/ && $lane < 120 && $lane % 5) {
         croak "'$name' does not contain a valid gel lane";
      }
      $loader ||= 8;		# 8-channel is default
   } else {

   #__SHOULD BE A 384 WELL LOCATION
      unless ( $name =~ /
         ^($archive)?             # save plate name to $1 if it's present
         (                        # extract well name and save to $2
         [a-p]                    # rows a to p in 384 format
         (0[1-9]|1[0-9]|2[0-4])   # cols 01 to 24 in 384 format - save to $3
         )
         (.*)$                    # save all chars after well name in $4
         /x ) {                   # x option allows these comments
         croak "'$name' does not contain a valid well name or 384 template name";
      } else {
         ($plate, $well, $col, $suffix) = ($1, $2, $3, $4);
         $well = lc $well;
      }

   #__GET NUMERICAL VALUES FOR THE ROW (LETTER) & COLUMN (NUMBER) FOR WELL
      $nrow = $ConvertWell::a2n{ substr ($well, 0, 1) };
      $ncol = $col;
   }
   $plate = '' unless defined $plate;
   $suffix = '' unless defined $suffix;

#__IS EITHER A "STREAK" CONVERSION
   my ($mrow, $mcol, $sub);
   if ($type && $type eq 'streak') {
      my $irow = ($nrow - 1) % 4;
      my $icol = ($ncol - 1) % 12;
      $mrow = $irow * 2 + 1 + (($ncol > 12) ? 1 : 0);
      $mcol = $icol + 1;

   #__DETERMINE SUBSET
      my $isub = int(($nrow - 1) / 4);
      $sub = (qw(ad eh il mp))[$isub];

#__OR A "GEL" CONVERSION
   } elsif ($type && $type eq 'gel') {
      if ($loader == 1) {
         $lane -= int($lane / 5); # remove marker lanes
         my $irow = int(($lane - 1) / 12);
         my $icol = ($lane - 1) % 12;
         $mrow = $irow + 1;
         $mcol = $icol + 1;
      } elsif ($loader == 12) {
         croak "$pkg:To96:12-channel conversion not yet implemented";
      } else {			# 8-channel
         my $irow = int(($lane % 40) / 5);
         $mrow = $ConvertWell::a2n{ h } - $irow;
         my $icol = $lane % 5;
         $mcol = int($lane / 40) * 4 + $icol;
      }

   #__NO SUBSET
      $sub = '';

#__OR A "QUADRANT" CONVERSION - I.E. A1, A2, B1, B2
   } else {

   #__DERIVE SUB-POSITION FROM ODD-EVEN PATTERNS
      my $irow = &SubPos ($nrow);
      my $icol = &SubPos ($ncol);

   #__DO THE ACTUAL CONVERSION FROM 384 TO 96
      $mrow = ($nrow + 2 - $irow) / 2;
      $mcol = ($ncol + 2 - $icol) / 2;

   #__ALSO MUST TRANSLATE SUB-POSITION BACK TO THE ALPHA-NUMERIC DESIGNATION
      $sub = join ("", $ConvertWell::n2a{$irow}, $icol);
  }

#__MAKE SURE COLUMN IS TWO DIGIT -- PREPEND A ZERO FOR VALS ONE TO NINE
   substr ($mcol, 0, 0) = "0" if length ($mcol) == 1;

#__TRANSLATE BACK TO THE ALPHA-NUMERIC DESIGNATION (NOW IN 96 FORMAT)
   $well = join ("", $ConvertWell::n2a{$mrow}, $mcol);
   $name = join ("", $plate, $well, $suffix);

#__RETURN THE RESULTING 96 WELL LOCATION AND 384 SUB-POSITION
   return (wantarray) ? ($name, $sub) : $name;
}

#  ======
#  TO GEL   converts a 96 well location to a 96 lane gel lane
#  ======   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sub ToGel {
   my ($name, $loader) = @_;
   my ($plate, $well, $col, $suffix);

#__CHECK ARGUMENT
   $name = lc $name;
   my @values = &Parse96($name);
   croak "'$name' does not contain a valid well name or 96 template name"
       unless @values;
   ($plate, $well, $col, $suffix) = @values;
   $loader ||= 8;		# 8-channel is default

   my $lane;
   my $irow = $ConvertWell::a2n{ substr ($well, 0, 1) };
   if ($loader == 1) {

   #__GELS ARE LOADED A1->A12, B1->B12, ETC.
      my $clane = ($irow - 1) * 12 + $col;
      my ($mlane, $tlane) = (0, $clane);
      do {
         $mlane = int($tlane / 5); # marker lanes
         $tlane = $clane + $mlane;
      } while ($mlane != int($tlane / 5));
      $lane = $clane + $mlane;
   } elsif ($loader == 12) {
      croak "$pkg:ToGel:12-channel loading not implemented yet";
   } else {			# 8-channel

   #__GELS ARE LOADED H->A, 4 COLUMNS AT A TIME
      $irow = $ConvertWell::a2n{ h } - $irow;
      my $mcol = int(($col - 1)/ 4);
      my $icol = ($col - 1) % 4;
      $lane = $mcol * 40 + $icol + $irow * 5 + 1;
   }
   return $lane;
}

################################################################################
##                                                                            ##
##             P R I V A T E   R O U T I N E S   A R E   H E R E              ##
##                                                                            ##
################################################################################

#  =======
#  SUB POS   derive sub-position of 96 well on a 384 plate
#  =======   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sub SubPos {
   my ($num) = @_;
   my $mod = $num % 2;

#__ODD => POSITION 1
   return 1 if $mod;

#__EVEN => POSITION 2
   return 2;
}

#  ========
#  PARSE 96   parse a 96 well location from name
#  ========   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

sub Parse96 {
   my ($name) = @_;

#__CHECK INTEGRITY OF FIRST ARGUMENT (THE ONE WITH THE 96 WELL LOCATION IN IT)
   unless ( $name =~ /
         ^($archive)?             # save plate name to $1 if it's present
         (                        # extract well name and save to $2
         [a-h]                    # only rows a to h in 96 format
         (0[1-9]|1[012])          # only cols 01 to 12 in 96 format - save to $3
         )
         (.*)$                    # save all chars after well name in $4
         /x )                     # x option allows these comments
   {
       return ();
   }
   my ($plate, $well, $col, $suffix) = ($1, $2, $3, $4);
   $plate = '' unless defined $plate;
   $suffix = '' unless defined $suffix;
   return ($plate, $well, $col, $suffix);
}

################################################################################
##                                                                            ##
##              P O D   D O C U M E N T A T I O N   I S   H E R E             ##
##                                                                            ##
################################################################################

1;

__END__

=pod

=head1 NAME

ConvertWell - convert locations between 384 and 96 well plates and gels

=head1 SYNOPSIS

    use ConvertWell ':all';

    $well384 = &To384('C06', 'A1');
    $well384 = &To384('C06', 'E-I');

    ($well96, $subset) = &To96('N22');
    ($well96, $streak_subset) = &To96('N22', 'streak');
    $well96 = &To96(36, 'gel');

    $lane = &ToGel('E11', 8);

=head1 DESCRIPTION

This package implements mapping functions to interconvert well
locations between 384 well plates and 96 well plates.  It also can
interconvert between 96-well plate well locations and restriction
digest gel lanes.  No variables or methods are exported by default.
Therefore all method calls must be fully-qualified, I<e.g.>,
C<&ConvertWell::function_name()>, or the method must be imported
explicitly.

The package contains the following methods:

=over 4

=item C<To384>

    $well384 = &To384('C06', 'A1');
    $well384 = &To384('C06', 'E-H');

This methods converts a 96-well location and a I<subset> to a 384-well
location.  The subset is the segment of the 384-well plate from which
the clone in the 96 well originally came.  The 384-well position is
returned as a lowercase string (regardless of the case of the input).

The subset B<must> be one of the following: a1, a2, b1, b2, a-d, e-h,
i-l, or m-p (case does not matter).  However, the 96 well location can
be the well location itself, the template name, or the name of the
corresponding SCF or experiment file. For example, any of the
following would have worked in the above function call:

    $well_96     = "g08";            # just the well name works
    $template_96 = "jf23g08"         # template name is fine
    $scf_96      = "jf23g08.s1"      # SCF file name works too
    $exp_96      = "jf23g08.s1.exp"  # experiment file name also works

Note that the B<relevant> quantity is returned, I<i.e.> if you
specified an experiment file in the 96 well format, the full
experiment file name in the 384 well format is returned (although
conversion to lowercase is done).

=item C<To96>

    ($well96, $subset) = &To96('N22');
    ($well96, $streak_subset) = &To96('N22', 'streak');
    $well96 = &To96(36, 'gel', 8);

This method converts a 384-well location or a gel lane number to a
96-well location and, in the case of the 384-well location, a 384-well
subset.  Both return values are lowercase strings.  In scalar context,
only the well location is returned.

If only one argument is given, it is assumed to be a 384-well
location.  It is converted to a 96-well location using the standard
C<a1>, C<a2>, C<b1>, C<b2> format.  If a second argument is given, it
should spcify the origin of the clone: streak or gel (any other value
causes the method to assume 384-well, i.e., the same behavior as no
second argument at all).  If the second argument is 'streak', then the
96-well position is calculated as if the 96-well plate was loaded from
a streak plate.  The possible subsets returned in this case are C<ad>,
C<eh>, C<il> and C<mp>.

Finally, if the second argument is 'gel', then the first argument
should be a restriction-digest gel lane number.  The third argument
then specifies what type of loader was used to load the gel: 1-, 8-,
or 12-channel.  If no loader is specified, 8-channel is assumed.  The
arguments are then used to construct a 96-well location.  In array
context, the subset returned is an empty string.  See L<"ToGel"> for
information about valid gel lanes.

Like C<To384>, this routine handles the well location itself, the
template name, or SCF and experiment file names and returns the
corresponding result.

=item C<ToGel>

    $lane = &ToGel('E11', 8);

This method converts a 96-well location into a restriction-digest gel
lane number.  The lanes are assumed to be numbered from 0 to 120, with
every fifth lane being a marker lane (0, 5, 10, 15, ...).  The second
argument specifies the loader used to load the gel from the 96-well
plate.  If no loader is given, the 8-channel loader is assumed.

=head1 ERRORS

All methods C<croak> when an error is encountered.  Therefore, to
diaallow these methods from killing your application, execute all
calls to them in an eval block.  This behavior is less than optimal
and may change in the future.

=head1 EXAMPLES

To use this modules, simply add the following line in your
script/module before you call any of the methods in this module:

    use ConvertWell;

After doing the above, you will have to call all the methods with
fully-qualified domain names.  That is, to call C<To96>, you would
have to do the collowing:

    $well96 = &ConvertWell::To96('D20');

To avoid this extra typing, you can import all the (importable)
methods like so:

    use ConvertWell ':all';

Then you can call methods without the module prefix (as long as you
don't have other methods with the same name):

    $well96 = &To96('D20');

The following example converts well location "g08" in 96 format to
subset "b2" on the 384 format:

    $well_96 = "g08";
    $position = "b2";
    $well_384 = &To384($well_96, $position);

The return value will be "n16", I<i.e.>, the well location on the 384
plate.

The following line converts well location "n16" in 384 format to the
location on a 96 well plate and gives the sub-position:

    $well_384 = "n16";
    ($well_96, $sub) = &ConvertWell::To96 ( $well_384 );

The return values will be "g08", the location on a 96 well plate, and
"b2", the sub-position on a 384 well plate.

=head1 AUTHORS

Original package by Michael C. Wendl <mwendl@watson.wustl.edu>

Enhancements by David Dooling <ddooling@watson.wustl.edu>

=head1 HISTORY

	1.0   FEB 1997   Created module

	2.0   SEP 2002   Added streak and gel lane conversions

	2.1   SEP 2002   See CVS log

=cut

# $Id: ConvertWell.pm,v 1.5 2002/09/10 19:27:42 mwendl Exp $

