       1 #!/usr/bin/perl -w
       2 #
       3 # File: ElementalAnalysis.pl
       4 # Author: Manish Sud <msud@san.rr.com>
       5 #
       6 # Copyright (C) 2018 Manish Sud. All rights reserved.
       7 #
       8 # This file is part of MayaChemTools.
       9 #
      10 # MayaChemTools is free software; you can redistribute it and/or modify it under
      11 # the terms of the GNU Lesser General Public License as published by the Free
      12 # Software Foundation; either version 3 of the License, or (at your option) any
      13 # later version.
      14 #
      15 # MayaChemTools is distributed in the hope that it will be useful, but without
      16 # any warranty; without even the implied warranty of merchantability of fitness
      17 # for a particular purpose.  See the GNU Lesser General Public License for more
      18 # details.
      19 #
      20 # You should have received a copy of the GNU Lesser General Public License
      21 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
      22 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
      23 # Boston, MA, 02111-1307, USA.
      24 #
      25
      26 use strict;
      27 use FindBin; use lib "$FindBin::Bin/../lib";
      28 use Getopt::Long;
      29 use File::Basename;
      30 use Text::ParseWords;
      31 use Benchmark;
      32 use FileUtil;
      33 use TextUtil;
      34 use MolecularFormula;
      35
      36 my($ScriptName, %Options, $StartTime, $EndTime, $TotalTime);
      37
      38 # Autoflush STDOUT
      39 $| = 1;
      40
      41 # Starting message...
      42 $ScriptName = basename($0);
      43 print "\n$ScriptName: Starting...\n\n";
      44 $StartTime = new Benchmark;
      45
      46 # Get the options and setup script...
      47 SetupScriptUsage();
      48 if ($Options{help}) {
      49   die GetUsageFromPod("$FindBin::Bin/$ScriptName");
      50 }
      51
      52 print "Processing options...\n";
      53 my(%OptionsInfo);
      54 ProcessOptions();
      55
      56 PerformElementalAnalysis();
      57
      58 print "\n$ScriptName:Done...\n\n";
      59
      60 $EndTime = new Benchmark;
      61 $TotalTime = timediff ($EndTime, $StartTime);
      62 print "Total time: ", timestr($TotalTime), "\n";
      63
      64 ###############################################################################
      65
      66 # Elemental analysis...
      67 sub PerformElementalAnalysis {
      68   my($Formula, $FormulaDataRef, $ValueLabel, $CalculationType, $CalculatedValue, $ElementsRef, $ElementCompositionRef, $Status, $ErrorMsg, @ValueLabels, @CalculatedValues);
      69
      70   print "Performing elemental analysis...\n";
      71
      72   if ($OptionsInfo{FileOutput}) {
      73     print "Generating file $OptionsInfo{OutFileName}...\n";
      74     open OUTFILE, ">$OptionsInfo{OutFileName}" or die "Couldn't open $OptionsInfo{OutFileName}: $!\n";
      75   }
      76
      77   # Setup value labels...
      78   @ValueLabels = ();
      79   if ($OptionsInfo{RowsOutput}) {
      80     push @ValueLabels, "Formula";
      81   }
      82   for $CalculationType (@{$OptionsInfo{SpecifiedCalculations}}) {
      83     push @ValueLabels, $OptionsInfo{ValueLabelsMap}{$CalculationType};
      84   }
      85
      86   if ($OptionsInfo{RowsOutput}) {
      87     ListHeaderRowData(\@ValueLabels);
      88   }
      89
      90   # Go over specified properties...
      91   FORMULA: for $Formula (@{$OptionsInfo{SpecifiedFormulas}}) {
      92     if (!$OptionsInfo{RowsOutput}) {
      93       if ($OptionsInfo{FileOutput}) {
      94         print OUTFILE "\nPerforming elemental analysis using formula $Formula...\n\n";
      95       }
      96       else {
      97         print "\nPerforming elemental analysis using formula  $Formula...\n\n";
      98       }
      99     }
     100     # Calculate appropriate values and write 'em out...
     101     if ($OptionsInfo{CheckFormula}) {
     102       ($Status, $ErrorMsg) = MolecularFormula::IsMolecularFormula($Formula);
     103       if (!$Status) {
     104         warn("Warning: Ignoring formula $Formula: It's not a valid value: $ErrorMsg\n");
     105         next FORMULA;
     106       }
     107     }
     108     @CalculatedValues = ();
     109     if ($OptionsInfo{RowsOutput}) {
     110       push @CalculatedValues, $Formula;
     111     }
     112     for $CalculationType (@{$OptionsInfo{SpecifiedCalculations}}) {
     113       if ($CalculationType =~ /^ElementalAnalysis$/i) {
     114         ($ElementsRef, $ElementCompositionRef) = MolecularFormula::CalculateElementalComposition($Formula);
     115         $CalculatedValue = (defined($ElementsRef) && defined($ElementCompositionRef)) ? MolecularFormula::FormatCompositionInfomation($ElementsRef, $ElementCompositionRef, $OptionsInfo{Precision}) : '';
     116       }
     117       elsif ($CalculationType =~ /^MolecularWeight$/i) {
     118         $CalculatedValue = MolecularFormula::CalculateMolecularWeight($Formula);
     119         $CalculatedValue = (defined($CalculatedValue) && length($CalculatedValue)) ? (sprintf("%.$OptionsInfo{Precision}f", $CalculatedValue)) : "";
     120       }
     121       elsif ($CalculationType =~ /^ExactMass$/i) {
     122         $CalculatedValue = MolecularFormula::CalculateExactMass($Formula);
     123         $CalculatedValue = (defined($CalculatedValue) && length($CalculatedValue)) ? (sprintf("%.$OptionsInfo{Precision}f", $CalculatedValue)) : "";
     124       }
     125       else {
     126         $CalculatedValue = '';
     127       }
     128       push @CalculatedValues, $CalculatedValue;
     129     }
     130     # List data...
     131     ListFormulaData(\@ValueLabels, \@CalculatedValues);
     132   }
     133   if ($OptionsInfo{FileOutput}) {
     134     close OUTFILE;
     135   }
     136   print "\n";
     137 }
     138
     139 # List calculated data values...
     140 sub ListFormulaData {
     141   my($DataLabelRef, $DataValueRef) = @_;
     142   my($Index, $Line, $Value);
     143
     144   if ($OptionsInfo{RowsOutput}) {
     145     $Line = '';
     146     # Format data...
     147     if ($OptionsInfo{OutQuote} || $Options{outdelim} !~ /^comma$/i) {
     148       $Line = JoinWords($DataValueRef, $OptionsInfo{OutDelim}, $OptionsInfo{OutQuote});
     149     }
     150     else {
     151       # Always quote values containing commas...
     152       $Line = ($DataValueRef->[0] =~ /\,/) ? qq("$DataValueRef->[0]") : $DataValueRef->[0];
     153       for $Index (1 .. $#{$DataValueRef} ) {
     154         $Value = $DataValueRef->[$Index];
     155         if ($Value =~ /\,/) {
     156           $Value = qq("$Value");
     157         }
     158         $Line .= $OptionsInfo{OutDelim} . $Value;
     159       }
     160     }
     161     if ($OptionsInfo{FileOutput}) {
     162       print OUTFILE "$Line\n";
     163     }
     164     else {
     165       print "$Line\n";
     166     }
     167   }
     168   else {
     169     # Format and list data...
     170     $Line = '';
     171     for $Index (0 .. $#{$DataLabelRef} ) {
     172       $Line = $DataLabelRef->[$Index] . ': ' . $DataValueRef->[$Index];
     173       if ($OptionsInfo{FileOutput}) {
     174         print OUTFILE "$Line\n";
     175       }
     176       else {
     177         print "$Line\n";
     178       }
     179     }
     180   }
     181 }
     182
     183 # List calculated data for a formula...
     184 sub ListHeaderRowData {
     185   my($DataLabelRef) = @_;
     186   my($Line);
     187
     188   # Format data...
     189   $Line = JoinWords($DataLabelRef, $OptionsInfo{OutDelim}, $OptionsInfo{OutQuote});
     190   $Line =~ s/\://g;
     191   # List data...
     192   if ($OptionsInfo{FileOutput}) {
     193     print OUTFILE "$Line\n";
     194   }
     195   else {
     196     print "$Line\n";
     197   }
     198 }
     199
     200 # Process option values...
     201 sub ProcessOptions {
     202   %OptionsInfo = ();
     203
     204   $OptionsInfo{Mode} = $Options{mode};
     205
     206   $OptionsInfo{OutDelim} = ($Options{outdelim} =~ /^tab$/i ) ? "\t" : (($Options{outdelim} =~ /^semicolon$/i) ? "\;" : "\,");
     207   $OptionsInfo{OutQuote} = ($Options{quote} =~ /^yes$/i) ? 1 : 0;
     208
     209   $OptionsInfo{Output} = $Options{output};
     210   $OptionsInfo{OutputStyle} = $Options{outputstyle};
     211
     212   $OptionsInfo{RowsOutput} = ($Options{outputstyle} =~ /^FormulaRows$/i) ? 1 : 0;
     213   $OptionsInfo{FileOutput} = ($Options{output} =~ /^File$/i) ? 1 : 0;
     214   $OptionsInfo{CheckFormula} = $Options{fast} ? 0 : 1;
     215
     216   $OptionsInfo{Precision} = $Options{precision};
     217
     218   $OptionsInfo{OutFileRoot} = defined $Options{root} ? $Options{root} : undef;
     219   $OptionsInfo{ValueLabels} = defined $Options{valuelabels} ? $Options{valuelabels} : undef;
     220
     221   # Setup formulas...
     222   @{$OptionsInfo{SpecifiedFormulas}} = ();
     223   if (@ARGV >= 1) {
     224     push @{$OptionsInfo{SpecifiedFormulas}}, @ARGV;
     225   }
     226   else {
     227     # Setup mode specified default values...
     228     push @{$OptionsInfo{SpecifiedFormulas}}, 'H2O';
     229   }
     230
     231   # Setup what to calculate...
     232   @{$OptionsInfo{SpecifiedCalculations}} = ();
     233   if ($Options{mode} =~ /^All$/i) {
     234     @{$OptionsInfo{SpecifiedCalculations}} = qw(ElementalAnalysis MolecularWeight ExactMass);
     235   }
     236   else {
     237     my($Mode, $ModeValue, @SpecifiedModeValues);
     238     $Mode = $Options{mode};
     239     $Mode =~ s/ //g;
     240     @SpecifiedModeValues = split /\,/, $Mode;
     241     for $ModeValue (@SpecifiedModeValues) {
     242       if ($ModeValue !~ /^(ElementalAnalysis|MolecularWeight|ExactMass)$/i) {
     243         if ($ModeValue =~ /^All$/i) {
     244           die "Error: All value for option \"-m --mode\" is not allowed with other valid values.\n";
     245         }
     246         else {
     247           die "Error: The value specified, $ModeValue, for option \"-m --mode\" is not valid. Allowed values: ElementalAnalysis, MolecularWeight, or ExactMass\n";
     248         }
     249       }
     250       push @{$OptionsInfo{SpecifiedCalculations}}, $ModeValue;
     251     }
     252   }
     253   my($Index, $Value, $Label);
     254   # Set up labels for calculated values...
     255   @{$OptionsInfo{SpecifiedValueLabels}} = ();
     256   if ($Options{valuelabels}) {
     257     my($Value, $Label, @ValueLabels);
     258     @ValueLabels = split /\,/, $Options{valuelabels};
     259     if (@ValueLabels % 2) {
     260       die "Error: The value specified, $Options{valuelabels}, for option \"-v --valuelabels\" is not valid: It must contain even number of comma delimited values\n";
     261     }
     262     for ($Index = 0; $Index < @ValueLabels; $Index +=2) {
     263       $Value = $ValueLabels[$Index];
     264       $Value =~ s/ //g;
     265       $Label = $ValueLabels[$Index + 1];
     266       if ($Value !~ /^(ElementalAnalysis|MolecularWeight|ExactMass)$/i) {
     267         die "Error: The value specified, $Value, using option \"-v --valuelabels\" is not valid. Allowed values: ElementalAnalysis, MolecularWeight, or ExactMass\n";
     268       }
     269       push @{$OptionsInfo{SpecifiedValueLabels}}, ($Value, $Label);
     270     }
     271   }
     272
     273   # Set up calculation type to label map...
     274   %{$OptionsInfo{ValueLabelsMap}} = (ElementalAnalysis => 'ElementalAnalysis', MolecularWeight => 'MolecularWeight', ExactMass => 'ExactMass');
     275   if (@{$OptionsInfo{SpecifiedValueLabels}}) {
     276     for ($Index = 0; $Index < @{$OptionsInfo{SpecifiedValueLabels}}; $Index +=2) {
     277       $Value = $OptionsInfo{SpecifiedValueLabels}[$Index];
     278       $Label = $OptionsInfo{SpecifiedValueLabels}[$Index + 1];
     279       if (exists $OptionsInfo{ValueLabelsMap}{$Value}) {
     280         $OptionsInfo{ValueLabelsMap}{$Value} = $Label;
     281       }
     282     }
     283   }
     284
     285   # Setup output file name...
     286   $OptionsInfo{OutFileName} = '';
     287   if ($OptionsInfo{FileOutput}) {
     288     my($OutFileRoot, $OutFileExt);
     289
     290     $OutFileRoot = '';
     291     $OutFileExt = "csv";
     292     if ($Options{outdelim} =~ /^tab$/i) {
     293       $OutFileExt = "tsv";
     294     }
     295     if ($Options{root}) {
     296       my ($RootFileDir, $RootFileName, $RootFileExt) = ParseFileName($Options{root});
     297       if ($RootFileName && $RootFileExt) {
     298         $OutFileRoot = $RootFileName;
     299       }
     300       else {
     301         $OutFileRoot = $Options{root};
     302       }
     303     }
     304     else {
     305       $OutFileRoot = 'FormulasElementalAnalysis';
     306     }
     307     $OptionsInfo{OutFileName} = $OutFileRoot . '.' . $OutFileExt;
     308     if (!$Options{overwrite}) {
     309       if (-e $OptionsInfo{OutFileName}) {
     310         die "Error: Output file, $OptionsInfo{OutFileName}, already exists.\nUse \-o --overwrite\ option or specify a different name using \"-r --root\" option.\n";
     311       }
     312     }
     313   }
     314 }
     315
     316
     317 # Setup script usage  and retrieve command line arguments specified using various options...
     318 sub SetupScriptUsage {
     319
     320   # Retrieve all the options...
     321   %Options = ();
     322   $Options{mode} = "All";
     323   $Options{outdelim} = "comma";
     324   $Options{output} = "STDOUT";
     325   $Options{outputstyle} = "FormulaBlock";
     326   $Options{precision} = 2;
     327   $Options{quote} = "yes";
     328
     329   if (!GetOptions(\%Options, "help|h", "fast", "mode|m=s", "outdelim=s", "output=s", "outputstyle=s", "overwrite|o", "precision=i", "quote|q=s", "root|r=s", "valuelabels|v=s", "workingdir|w=s")) {
     330     die "\nTo get a list of valid options and their values, use \"$ScriptName -h\" or\n\"perl -S $ScriptName -h\" command and try again...\n";
     331   }
     332   if ($Options{workingdir}) {
     333     if (! -d $Options{workingdir}) {
     334       die "Error: The value specified, $Options{workingdir}, for option \"-w --workingdir\" is not a directory name.\n";
     335     }
     336     chdir $Options{workingdir} or die "Error: Couldn't chdir $Options{workingdir}: $! \n";
     337   }
     338   if ($Options{outdelim} !~ /^(comma|semicolon|tab)$/i) {
     339     die "Error: The value specified, $Options{outdelim}, for option \"--outdelim\" is not valid. Allowed values: comma, tab, or semicolon\n";
     340   }
     341   if ($Options{output} !~ /^(STDOUT|File)$/i) {
     342     die "Error: The value specified, $Options{output}, for option \"--output\" is not valid. Allowed values: STDOUT or File\n";
     343   }
     344   if ($Options{outputstyle} !~ /^(FormulaBlock|FormulaRows)$/i) {
     345     die "Error: The value specified, $Options{outputstyle}, for option \"--outputstyle\" is not valid. Allowed values: FormulaBlock or FormulaRows\n";
     346   }
     347   if (!IsPositiveInteger($Options{precision})) {
     348     die "Error: The value specified, $Options{precision}, for option \"-p --precision\" is not valid. Allowed values: > 0 \n";
     349   }
     350   if ($Options{quote} !~ /^(yes|no)$/i) {
     351     die "Error: The value specified, $Options{quote}, for option \"-q --quote\" is not valid. Allowed values: yes or no\n";
     352   }
     353 }
     354