       1 package PeriodicTable;
       2 #
       3 # $RCSfile: PeriodicTable.pm,v $
       4 # $Date: 2015/02/28 20:47:18 $
       5 # $Revision: 1.37 $
       6 #
       7 # Author: Manish Sud <msud@san.rr.com>
       8 #
       9 # Copyright (C) 2015 Manish Sud. All rights reserved.
      10 #
      11 # This file is part of MayaChemTools.
      12 #
      13 # MayaChemTools is free software; you can redistribute it and/or modify it under
      14 # the terms of the GNU Lesser General Public License as published by the Free
      15 # Software Foundation; either version 3 of the License, or (at your option) any
      16 # later version.
      17 #
      18 # MayaChemTools is distributed in the hope that it will be useful, but without
      19 # any warranty; without even the implied warranty of merchantability of fitness
      20 # for a particular purpose.  See the GNU Lesser General Public License for more
      21 # details.
      22 #
      23 # You should have received a copy of the GNU Lesser General Public License
      24 # along with MayaChemTools; if not, see <http://www.gnu.org/licenses/> or
      25 # write to the Free Software Foundation Inc., 59 Temple Place, Suite 330,
      26 # Boston, MA, 02111-1307, USA.
      27 #
      28
      29 use strict;
      30 use Carp;
      31 use Text::ParseWords;
      32 use TextUtil;
      33 use FileUtil;
      34
      35 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
      36
      37 @ISA = qw(Exporter);
      38 @EXPORT = qw();
      39 @EXPORT_OK = qw(GetElements GetElementsByGroupName GetElementsByGroupNumber GetElementsByAmericanStyleGroupLabel GetElementsByEuropeanStyleGroupLabel GetElementsByPeriodNumber GetElementMostAbundantNaturalIsotopeData GetElementNaturalIsotopeCount GetElementNaturalIsotopesData GetElementNaturalIsotopeAbundance GetElementMostAbundantNaturalIsotopeMass GetElementMostAbundantNaturalIsotopeMassNumber GetElementNaturalIsotopeMass GetElementPropertiesData GetElementPropertiesNames GetElementPropertiesNamesAndUnits GetElementPropertyUnits GetIUPACGroupNumberFromAmericanStyleGroupLabel GetIUPACGroupNumberFromEuropeanStyleGroupLabel IsElement IsElementNaturalIsotopeMassNumber IsElementProperty);
      40
      41 %EXPORT_TAGS = (all  => [@EXPORT, @EXPORT_OK]);
      42
      43 #
      44 # Load atomic properties and isotope data for elements...
      45 #
      46 my(%ElementDataMap, %ElementIsotopeDataMap, %ElementSymbolMap, @ElementPropertyNames, %ElementPropertyNamesMap, %ElementIsotopeDerivedDataMap);
      47 _LoadPeriodicTableElementData();
      48
      49 #
      50 # Get a list of all known element symbols...
      51 #
      52 sub GetElements {
      53   my($AtomicNumber, @ElementSymbols);
      54
      55   @ElementSymbols = ();
      56   for $AtomicNumber (sort {$a <=> $b} keys %ElementDataMap) {
      57       push @ElementSymbols, $ElementDataMap{$AtomicNumber}{ElementSymbol};
      58   }
      59   return (wantarray ? @ElementSymbols : \@ElementSymbols);
      60 }
      61
      62 #
      63 # Get element symbols of elements with a specific group name. Valid group
      64 # names are: Alkali metals, Alkaline earth metals, Coinage metals, Pnictogens,
      65 # Chalcogens, Halogens, Noble gases; Additionally, usage of Lanthanides (Lanthanoids)
      66 # and Actinides (Actinoids) is also supported.
      67 #
      68 sub GetElementsByGroupName {
      69   my($SpecifiedGroupName) = @_;
      70   my($AtomicNumber, @ElementSymbols, $GroupName);
      71
      72   if (IsEmpty($SpecifiedGroupName)) {
      73     return (wantarray ? () : undef);
      74   }
      75   if ($SpecifiedGroupName =~ /Lanthanide/i) {
      76     $SpecifiedGroupName = 'Lanthanoids';
      77   }
      78   elsif ($SpecifiedGroupName =~ /Actinide/i) {
      79     $SpecifiedGroupName = 'Actinoids';
      80   }
      81   @ElementSymbols = ();
      82   for $AtomicNumber (sort {$a <=> $b} keys %ElementDataMap) {
      83     $GroupName = $ElementDataMap{$AtomicNumber}{GroupName};
      84     if ($SpecifiedGroupName =~ /$GroupName/i ) {
      85       push @ElementSymbols, $ElementDataMap{$AtomicNumber}{ElementSymbol};
      86     }
      87   }
      88   return (wantarray ? @ElementSymbols : \@ElementSymbols);
      89 }
      90
      91 #
      92 # Get element symbols of elements in a specific IUPAC group number.
      93 # A reference to an array containing element symbols is returned.
      94 #
      95 sub GetElementsByGroupNumber {
      96   my($GroupNumber) = @_;
      97   my($AtomicNumber, @ElementSymbols);
      98
      99   if (!IsInteger($GroupNumber)) {
     100     return (wantarray ? () : undef);
     101   }
     102
     103   @ElementSymbols = ();
     104   for $AtomicNumber (sort {$a <=> $b} keys %ElementDataMap) {
     105     if ($GroupNumber eq $ElementDataMap{$AtomicNumber}{GroupNumber}) {
     106       push @ElementSymbols, $ElementDataMap{$AtomicNumber}{ElementSymbol};
     107     }
     108   }
     109   return (wantarray ? @ElementSymbols : \@ElementSymbols);
     110 }
     111
     112 #
     113 # Get element symbols of elements in a specific American style group label.
     114 # A reference to an array containing element symbols is returned.
     115 #
     116 sub GetElementsByAmericanStyleGroupLabel {
     117   my($GroupLabel) = @_;
     118
     119   return _GetElementsByGroupLabel($GroupLabel, 'AmericanStyle');
     120 }
     121
     122 #
     123 # Get element symbols of elements in a specific European style group label.
     124 # A reference to an array containing element symbols is returned.
     125 #
     126 sub GetElementsByEuropeanStyleGroupLabel {
     127   my($GroupLabel) = @_;
     128
     129   return _GetElementsByGroupLabel($GroupLabel, 'EuropeanStyle');
     130 }
     131
     132 #
     133 # Get IUPAC group number from American style group label. A comma delimited
     134 # string is returned for group VIII or VIIIB.
     135 #
     136 sub GetIUPACGroupNumberFromAmericanStyleGroupLabel {
     137   my($GroupLabel) = @_;
     138   my($GroupNumber);
     139
     140   if (IsEmpty($GroupLabel)) {
     141     return undef;
     142   }
     143   $GroupNumber = "";
     144   SWITCH: {
     145       if ($GroupLabel =~ /^IA$/) { $GroupNumber = 1; last SWITCH;}
     146       if ($GroupLabel =~ /^IIA$/) { $GroupNumber = 2; last SWITCH;}
     147       if ($GroupLabel =~ /^IIIB$/) { $GroupNumber = 3; last SWITCH;}
     148       if ($GroupLabel =~ /^IVB$/) { $GroupNumber = 4; last SWITCH;}
     149       if ($GroupLabel =~ /^VB$/) { $GroupNumber = 5; last SWITCH;}
     150       if ($GroupLabel =~ /^VIB$/) { $GroupNumber = 6; last SWITCH;}
     151       if ($GroupLabel =~ /^VIIB$/) { $GroupNumber = 7; last SWITCH;}
     152       if ($GroupLabel =~ /^(VIII|VIIIB)$/) { $GroupNumber = '8,9,10'; last SWITCH;}
     153       if ($GroupLabel =~ /^IB$/) { $GroupNumber = 11; last SWITCH;}
     154       if ($GroupLabel =~ /^IIB$/) { $GroupNumber = 12; last SWITCH;}
     155       if ($GroupLabel =~ /^IIIA$/) { $GroupNumber = 13; last SWITCH;}
     156       if ($GroupLabel =~ /^IVA$/) { $GroupNumber = 14; last SWITCH;}
     157       if ($GroupLabel =~ /^VA$/) { $GroupNumber = 15; last SWITCH;}
     158       if ($GroupLabel =~ /^VIA$/) { $GroupNumber = 16; last SWITCH;}
     159       if ($GroupLabel =~ /^VIIA$/) { $GroupNumber = 17; last SWITCH;}
     160       if ($GroupLabel =~ /^VIIIA$/) { $GroupNumber = 18; last SWITCH;}
     161       $GroupNumber = "";
     162   }
     163   if (!$GroupNumber) {
     164     return undef;
     165   }
     166   return $GroupNumber;
     167 }
     168
     169 #
     170 # Get IUPAC group number from European style group label. A comma delimited
     171 # string is returned for group VIII or VIIIA
     172 #
     173 sub GetIUPACGroupNumberFromEuropeanStyleGroupLabel {
     174   my($GroupLabel) = @_;
     175   my($GroupNumber);
     176
     177   if (IsEmpty($GroupLabel)) {
     178     return undef;
     179   }
     180   $GroupNumber = "";
     181   SWITCH: {
     182       if ($GroupLabel =~ /^IA$/) { $GroupNumber = 1; last SWITCH;}
     183       if ($GroupLabel =~ /^IIA$/) { $GroupNumber = 2; last SWITCH;}
     184       if ($GroupLabel =~ /^IIIA$/) { $GroupNumber = 3; last SWITCH;}
     185       if ($GroupLabel =~ /^IVA$/) { $GroupNumber = 4; last SWITCH;}
     186       if ($GroupLabel =~ /^VA$/) { $GroupNumber = 5; last SWITCH;}
     187       if ($GroupLabel =~ /^VIA$/) { $GroupNumber = 6; last SWITCH;}
     188       if ($GroupLabel =~ /^VIIA$/) { $GroupNumber = 7; last SWITCH;}
     189       if ($GroupLabel =~ /^(VIII|VIIIA)$/) { $GroupNumber = '8,9,10'; last SWITCH;}
     190       if ($GroupLabel =~ /^IB$/) { $GroupNumber = 11; last SWITCH;}
     191       if ($GroupLabel =~ /^IIB$/) { $GroupNumber = 12; last SWITCH;}
     192       if ($GroupLabel =~ /^IIIB$/) { $GroupNumber = 13; last SWITCH;}
     193       if ($GroupLabel =~ /^IVB$/) { $GroupNumber = 14; last SWITCH;}
     194       if ($GroupLabel =~ /^VB$/) { $GroupNumber = 15; last SWITCH;}
     195       if ($GroupLabel =~ /^VIB$/) { $GroupNumber = 16; last SWITCH;}
     196       if ($GroupLabel =~ /^VIIB$/) { $GroupNumber = 17; last SWITCH;}
     197       if ($GroupLabel =~ /^VIIIB$/) { $GroupNumber = 18; last SWITCH;}
     198       $GroupNumber = "";
     199   }
     200   if (!$GroupNumber) {
     201     return undef;
     202   }
     203   return $GroupNumber;
     204 }
     205
     206 #
     207 # Get element symbols of elements in a specific period number.
     208 # A reference to an array containing element symbols is returned.
     209 #
     210 sub GetElementsByPeriodNumber {
     211   my($SpecifiedPeriodNumber) = @_;
     212   my($AtomicNumber, $PeriodNumber, @ElementSymbols);
     213
     214   if (!IsInteger($SpecifiedPeriodNumber)) {
     215     return (wantarray ? () : undef);
     216   }
     217
     218   @ElementSymbols = ();
     219   for $AtomicNumber (sort {$a <=> $b} keys %ElementDataMap) {
     220     $PeriodNumber = $ElementDataMap{$AtomicNumber}{PeriodNumber};
     221     if ($PeriodNumber =~ /\(/) {
     222       # Lanthanides and Actinides...
     223       ($PeriodNumber) = split /\(/, $PeriodNumber;
     224     }
     225     if ($PeriodNumber == $SpecifiedPeriodNumber) {
     226       push @ElementSymbols, $ElementDataMap{$AtomicNumber}{ElementSymbol};
     227     }
     228   }
     229   return (wantarray ? @ElementSymbols : \@ElementSymbols);
     230 }
     231
     232 #
     233 # Get data for most abundant isotope of an element using element symbol or atomic number.
     234 #
     235 sub GetElementMostAbundantNaturalIsotopeData {
     236   my($ElementID) = @_;
     237   my($AtomicNumber);
     238
     239   if (!($AtomicNumber = _ValidateElementID($ElementID))) {
     240     return (wantarray ? () : undef);
     241   }
     242
     243   my(@IsotopeData, $IsotopeSymbol, $MassNumber, $RelativeAtomicMass, $NaturalAbundance);
     244   $MassNumber = $ElementIsotopeDerivedDataMap{$AtomicNumber}{MostAbundantMassNumber};
     245   $IsotopeSymbol = $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{IsotopeSymbol};
     246   $RelativeAtomicMass = $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{RelativeAtomicMass};
     247   $NaturalAbundance = $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{NaturalAbundance};
     248   @IsotopeData = ();
     249   @IsotopeData = ($AtomicNumber, $IsotopeSymbol, $MassNumber, $RelativeAtomicMass, $NaturalAbundance);
     250
     251   return (wantarray ? @IsotopeData : \@IsotopeData);
     252
     253 }
     254 #
     255 # Get natural isotope count for an element...
     256 #
     257 sub GetElementNaturalIsotopeCount {
     258   my($ElementID) = @_;
     259   my($AtomicNumber);
     260
     261   if ($AtomicNumber = _ValidateElementID($ElementID)) {
     262     return $ElementIsotopeDerivedDataMap{$AtomicNumber}{IsotopeCount};
     263   }
     264   else {
     265     return undef;
     266   }
     267 }
     268
     269 #
     270 # Get all available isotope data for an element using element symbol or atomic number or
     271 # data for a specific mass number using one of these two invocation methods:
     272 #
     273 # $HashRef = GetElementNaturalIsotopesData($ElementID);
     274 #
     275 # $HashRef = GetElementNaturalIsotopesData($ElementID, $MassNumber);
     276 #
     277 # In the first mode, a reference to a two-dimensional hash array is return where first
     278 # and second dimension keys correspond to mass number and isotope data labels.
     279 #
     280 # And in the second mode, a refernce to one-dimensional hash array is returned with
     281 # keys and values corresponding to isotope data label and values.
     282 #
     283 sub GetElementNaturalIsotopesData {
     284   my($ElementID, $MassNumber, $InvocationMode, $AtomicNumber);
     285
     286   if (@_ == 2) {
     287     ($ElementID, $MassNumber) = @_;
     288     $InvocationMode = 2;
     289   }
     290   else {
     291     ($ElementID) = @_;
     292     $InvocationMode = 1;
     293   }
     294   if (!($AtomicNumber = _ValidateElementID($ElementID))) {
     295     return undef;
     296   }
     297   if ($InvocationMode == 1) {
     298     return \%{$ElementIsotopeDataMap{$AtomicNumber}};
     299   }
     300   elsif ($InvocationMode == 2) {
     301     if (exists $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}) {
     302       return \%{$ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}};
     303     }
     304     else {
     305       return undef;
     306     }
     307   }
     308   else {
     309     return undef;
     310   }
     311 }
     312
     313 #
     314 # Get relative atomic mass for an element with specfic mass number.
     315 #
     316 sub GetElementNaturalIsotopeMass {
     317   my($ElementID, $MassNumber) = @_;
     318   my($AtomicNumber);
     319
     320   if (!($AtomicNumber = _ValidateElementID($ElementID))) {
     321     return undef;
     322   }
     323   if (exists $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}) {
     324     return $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{RelativeAtomicMass};
     325   }
     326   else {
     327     return undef;
     328   }
     329 }
     330
     331 #
     332 # Get relative atomic mass of most abundant isotope for an element...
     333 #
     334 sub GetElementMostAbundantNaturalIsotopeMass {
     335   my($ElementID) = @_;
     336   my($AtomicNumber);
     337
     338   if (!($AtomicNumber = _ValidateElementID($ElementID))) {
     339     return undef;
     340   }
     341   my($MassNumber, $RelativeAtomicMass);
     342
     343   $MassNumber = $ElementIsotopeDerivedDataMap{$AtomicNumber}{MostAbundantMassNumber};
     344   $RelativeAtomicMass = $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{RelativeAtomicMass};
     345
     346   return $RelativeAtomicMass;
     347 }
     348
     349 #
     350 # Get mass number of most abundant isotope for an element...
     351 #
     352 sub GetElementMostAbundantNaturalIsotopeMassNumber {
     353   my($ElementID) = @_;
     354   my($AtomicNumber);
     355
     356   if (!($AtomicNumber = _ValidateElementID($ElementID))) {
     357     return undef;
     358   }
     359   my($MassNumber);
     360
     361   $MassNumber = $ElementIsotopeDerivedDataMap{$AtomicNumber}{MostAbundantMassNumber};
     362
     363   return $MassNumber;
     364 }
     365 #
     366 # Get % natural abundance of natural isotope for an element with specfic mass number.
     367 #
     368 sub GetElementNaturalIsotopeAbundance {
     369   my($ElementID, $MassNumber) = @_;
     370   my($AtomicNumber);
     371
     372   if (!($AtomicNumber = _ValidateElementID($ElementID))) {
     373     return undef;
     374   }
     375   if (exists $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}) {
     376     return $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{NaturalAbundance};
     377   }
     378   else {
     379     return undef;
     380   }
     381 }
     382
     383 #
     384 # Get all available properties data for an element using element symbol or atomic number.
     385 # A reference to a hash array is returned with keys and values representing property
     386 # name and its values respectively.
     387 #
     388 sub GetElementPropertiesData {
     389   my($ElementID) = @_;
     390   my($AtomicNumber);
     391
     392   if ($AtomicNumber = _ValidateElementID($ElementID)) {
     393     return \%{$ElementDataMap{$AtomicNumber}};
     394   }
     395   else {
     396     return undef;
     397   }
     398 }
     399
     400 #
     401 # Get names of all available element properties. A reference to  an array containing
     402 # names of all available properties is returned.
     403 #
     404 sub GetElementPropertiesNames {
     405   my($Mode);
     406   my($PropertyName, @PropertyNames);
     407
     408   $Mode = 'ByGroup';
     409   if (@_ == 1) {
     410     ($Mode) = @_;
     411   }
     412
     413   @PropertyNames = ();
     414   if ($Mode =~ /^Alphabetical$/i) {
     415     # AtomicNumber, ElementSymbol and ElementName are always listed first...
     416     push @PropertyNames, qw(AtomicNumber ElementSymbol ElementName);
     417     for $PropertyName (sort keys %ElementPropertyNamesMap) {
     418       if ($PropertyName !~ /^(AtomicNumber|ElementSymbol|ElementName)$/i) {
     419         push @PropertyNames, $PropertyName;
     420       }
     421     }
     422   }
     423   else {
     424     push @PropertyNames, @ElementPropertyNames;
     425   }
     426   return (wantarray ? @PropertyNames : \@PropertyNames);
     427 }
     428
     429 #
     430 # Get names and units of all available element properties...
     431 # A reference to a hash array is returned with keys and values representing property
     432 # name and its units respectively. Names with no units contains empty strings as hash
     433 # values.
     434 #
     435 sub GetElementPropertiesNamesAndUnits {
     436
     437    return \%ElementPropertyNamesMap;
     438 }
     439
     440 #
     441 # Get units for a specific element property. An empty string is returned for a property
     442 # with no units.
     443 #
     444 sub GetElementPropertyUnits {
     445   my($PropertyName) = @_;
     446   my($PropertyUnits);
     447
     448   $PropertyUnits = (exists($ElementPropertyNamesMap{$PropertyName})) ? $ElementPropertyNamesMap{$PropertyName} : undef;
     449
     450   return $PropertyUnits;
     451 }
     452
     453 #
     454 # Is it a known element? Input is either an element symol or a atomic number.
     455 #
     456 sub IsElement {
     457   my($ElementID) = @_;
     458   my($Status);
     459
     460   $Status = (_ValidateElementID($ElementID)) ? 1 : 0;
     461
     462   return $Status;
     463 }
     464
     465 #
     466 # Is it a valid mass number for an element? Element ID is either an element symol or a atomic number.
     467 #
     468 sub IsElementNaturalIsotopeMassNumber {
     469   my($ElementID, $MassNumber) = @_;
     470   my($AtomicNumber, $Status);
     471
     472   $Status = 0;
     473   if (!($AtomicNumber = _ValidateElementID($ElementID))) {
     474     return $Status;
     475   }
     476   if (exists $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}) {
     477     $Status = 1;
     478   }
     479
     480   return $Status;
     481 }
     482
     483 #
     484 # Is it an available element property?
     485 #
     486 sub IsElementProperty {
     487   my($PropertyName) = @_;
     488   my($Status);
     489
     490   $Status = (exists($ElementPropertyNamesMap{$PropertyName})) ? 1 : 0;
     491
     492   return $Status;
     493 }
     494
     495 #
     496 # Implents GetElement<PropertyName> for a valid proprty name.
     497 #
     498 sub AUTOLOAD {
     499   my($ElementID) = @_;
     500   my($FunctionName, $PropertyName, $PropertyValue, $AtomicNumber);
     501
     502   $PropertyValue = undef;
     503
     504   use vars qw($AUTOLOAD);
     505   $FunctionName = $AUTOLOAD;
     506   $FunctionName =~ s/.*:://;
     507
     508   # Only Get<PropertyName> functions are supported...
     509   if ($FunctionName !~ /^GetElement/) {
     510     croak "Error: Function, PeriodicTable::$FunctionName, is not supported by AUTOLOAD in PeriodicTable module: Only GetElement<PropertyName> functions are implemented...";
     511   }
     512
     513   $PropertyName = $FunctionName;
     514   $PropertyName =~  s/^GetElement//;
     515   if (!exists $ElementPropertyNamesMap{$PropertyName}) {
     516     croak "Error: Function, PeriodicTable::$FunctionName, is not supported by AUTOLOAD in PeriodicTable module: Unknown element property name, $PropertyName, specified...";
     517   }
     518
     519   if (!($AtomicNumber = _ValidateElementID($ElementID))) {
     520     return undef;
     521   }
     522   $PropertyValue = $ElementDataMap{$AtomicNumber}{$PropertyName};
     523   return $PropertyValue;
     524 }
     525
     526 #
     527 # Get elements labels for group name specified using American or European style...
     528 #
     529 sub _GetElementsByGroupLabel {
     530   my($GroupLabel, $LabelStyle) = @_;
     531   my($GroupNumber);
     532
     533   if ($LabelStyle =~ /^AmericanStyle$/i) {
     534     $GroupNumber = GetIUPACGroupNumberFromAmericanStyleGroupLabel($GroupLabel);
     535   }
     536   elsif ($LabelStyle =~ /^EuropeanStyle$/i) {
     537     $GroupNumber = GetIUPACGroupNumberFromEuropeanStyleGroupLabel($GroupLabel);
     538   }
     539
     540   if (IsEmpty($GroupNumber)) {
     541     return (wantarray ? () : undef);
     542   }
     543
     544   my($AtomicNumber, @GroupElements, @ElementSymbols);
     545   @ElementSymbols = ();
     546   if ($GroupNumber =~ /\,/) {
     547     my(@GroupNumbers);
     548
     549     @GroupNumbers = split /\,/, $GroupNumber;
     550     for $GroupNumber (@GroupNumbers) {
     551       @GroupElements =  GetElementsByGroupNumber($GroupNumber);
     552       push @ElementSymbols, @GroupElements;
     553     }
     554   }
     555   else {
     556     @GroupElements =  GetElementsByGroupNumber($GroupNumber);
     557     push @ElementSymbols, @GroupElements;
     558   }
     559   return (wantarray ? @ElementSymbols : \@ElementSymbols);
     560 }
     561
     562 #
     563 # Load PeriodicTableElementData.csv and PeriodicTableIsotopeData.csv files from
     564 # <MayaChemTools>/lib directory...
     565 #
     566 sub _LoadPeriodicTableElementData {
     567   my($ElementDataFile, $ElementIsotopeDataFile, $MayaChemToolsLibDir);
     568
     569   $MayaChemToolsLibDir = GetMayaChemToolsLibDirName();
     570
     571   $ElementDataFile =  "$MayaChemToolsLibDir" . "/data/PeriodicTableElementData.csv";
     572   $ElementIsotopeDataFile = "$MayaChemToolsLibDir" . "/data/PeriodicTableIsotopeData.csv";
     573
     574   if (! -e "$ElementDataFile") {
     575     croak "Error: MayaChemTools package file, $ElementDataFile, is missing: Possible installation problems...";
     576   }
     577   if (! -e "$ElementIsotopeDataFile") {
     578     croak "Error: MayaChemTools package file, $ElementIsotopeDataFile, is missing: Possible installation problems...";
     579   }
     580
     581   _LoadElementData($ElementDataFile);
     582   _LoadElementIsotopeData($ElementIsotopeDataFile);
     583 }
     584
     585 #
     586 # Load PeriodicTableElementData.csv file from <MayaChemTools>/lib directory...
     587 #
     588 sub _LoadElementData {
     589   my($ElementDataFile) = @_;
     590
     591   %ElementDataMap = ();
     592   @ElementPropertyNames = ();
     593   %ElementPropertyNamesMap = ();
     594   %ElementSymbolMap = ();
     595
     596   # Load atomic properties data for all elements...
     597   #
     598   # File Format:
     599   #"AtomicNumber","ElementSymbol","ElementName","AtomicWeight","GroupNumber","GroupName","PeriodNumber","Block","GroundStateConfiguration","ValenceElectrons","GroundStateLevel","StandardState","CommonValences","LowestCommonValence","HighestCommonValence","CommonOxidationNumbers","LowestCommonOxidationNumber","HighestCommonOxidationNumber","BondLength(pm)","AtomicRadiusEmpirical(pm)","AtomicRadiusCalculated(pm)","CovalentRadiusEmpirical(pm)","VanderWaalsRadius(pm)","ElectronAffinity(kJ mol-1)","FirstIonizationEnergy(kJ mol-1)","PaulingElectronegativity(Pauling units)","SandersonElectronegativity(Pauling units)","AllredRochowElectronegativity(Pauling units)","MullikenJaffeElectronegativity(Pauling units)","AllenElectronegativity(Pauling units)","DensityOfSolid(kg m-3)","MolarVolume(cm3)","VelocityOfSound(m s-1)","YoungsModulus(GPa)","RigidityModulus(GPa)","BulkModulus(GPa)","PoissonsRatio(No units)","MineralHardness(No units)","BrinellHardness(MN m-2)","VickersHardness(MN m-2)","ElectricalResistivity(10-8 omega m)","Reflectivity(%)","RefractiveIndex(No units)","MeltingPoint(Celsius)","BoilingPoint(Celsius)","CriticalTemperature(Celsius)","SuperconductionTemperature(Celsius)","ThermalConductivity(W m-1 K-1)","CoefficientOfLinearExpansion(K-1 x 10^6)","EnthalpyOfFusion(kJ mol-1)","EnthalpyOfVaporization(kJ mol-1)","EnthalpyOfAtmization(kJ mol-1)","Color","Classification","DiscoveredBy","DiscoveredAt","DiscoveredWhen","OriginOfName"
     600   #
     601   #
     602   my($AtomicNumber, $ElementSymbol, $Line, $NumOfCols, $InDelim, $Index, $Name, $Value, $Units, @LineWords, @ColLabels);
     603
     604   $InDelim = "\,";
     605   open ELEMENTDATAFILE, "$ElementDataFile" or croak "Couldn't open $ElementDataFile: $! ...";
     606
     607   # Skip lines up to column labels...
     608   LINE: while ($Line = GetTextLine(\*ELEMENTDATAFILE)) {
     609     if ($Line !~ /^#/) {
     610       last LINE;
     611     }
     612   }
     613   @ColLabels= quotewords($InDelim, 0, $Line);
     614   $NumOfCols = @ColLabels;
     615
     616   # Extract property names from column labels - and unit names where appropriate...
     617   @ElementPropertyNames = ();
     618   for $Index (0 .. $#ColLabels) {
     619     $Name = $ColLabels[$Index];
     620     $Units = "";
     621     if ($Name =~ /\(/) {
     622       ($Name, $Units) = split /\(/,  $Name;
     623       $Units =~ s/\)//g;
     624     }
     625     push @ElementPropertyNames, $Name;
     626
     627     # Store element names and units...
     628     $ElementPropertyNamesMap{$Name} = $Units;
     629   }
     630
     631   # Process element data...
     632   LINE: while ($Line = GetTextLine(\*ELEMENTDATAFILE)) {
     633     if ($Line =~ /^#/) {
     634       next LINE;
     635     }
     636     @LineWords = ();
     637     @LineWords = quotewords($InDelim, 0, $Line);
     638     if (@LineWords != $NumOfCols) {
     639       croak "Error: The number of data fields, @LineWords, in $ElementDataFile must be $NumOfCols.\nLine: $Line...";
     640     }
     641     $AtomicNumber = $LineWords[0]; $ElementSymbol = $LineWords[1];
     642     if (exists $ElementDataMap{$AtomicNumber}) {
     643       carp "Warning: Ignoring data for element $ElementSymbol: It has already been loaded.\nLine: $Line....";
     644       next LINE;
     645     }
     646
     647     # Store all the values...
     648     %{$ElementDataMap{$AtomicNumber}} = ();
     649     for $Index (0 .. $#LineWords) {
     650       $Name = $ElementPropertyNames[$Index];
     651       $Value = $LineWords[$Index];
     652       $ElementDataMap{$AtomicNumber}{$Name} = $Value;
     653     }
     654   }
     655   close ELEMENTDATAFILE;
     656
     657   # Setup the element symbol map as well...
     658   _SetupElementSymbolMap();
     659 }
     660
     661 #
     662 # Load PeriodicTableIsotopeData.csv files from <MayaChemTools>/lib directory...
     663 #
     664 sub _LoadElementIsotopeData {
     665   my($ElementIsotopeDataFile) = @_;
     666
     667   %ElementIsotopeDataMap = ();
     668   %ElementIsotopeDerivedDataMap = ();
     669
     670   # Load isotope data for all elements...
     671   #
     672   # File format:
     673   # "Atomic Number","Isotope Symbol","Mass Number","Relative Atomic Mass","% Natural Abundnace"
     674   #
     675   # Empty values for "Relative Atomic Mass" and "% Natural Abundnace" imply absence of any
     676   # naturally occuring isotopes for the element.
     677   #
     678   my($InDelim, $Line, $NumOfCols, @ColLabels, @LineWords);
     679
     680   $InDelim = "\,";
     681   open ISOTOPEDATAFILE, "$ElementIsotopeDataFile" or croak "Couldn't open $ElementIsotopeDataFile: $! ...";
     682
     683   # Skip lines up to column labels...
     684   LINE: while ($Line = GetTextLine(\*ISOTOPEDATAFILE)) {
     685     if ($Line !~ /^#/) {
     686       last LINE;
     687     }
     688   }
     689   @ColLabels= quotewords($InDelim, 0, $Line);
     690   $NumOfCols = @ColLabels;
     691
     692   my($AtomicNumber, $IsotopeSymbol, $MassNumber, $RelativeAtomicMass, $NaturalAbundance, %ZeroNaturalAbundanceMap);
     693   %ZeroNaturalAbundanceMap = ();
     694
     695   # Process element data...
     696   LINE: while ($Line = GetTextLine(\*ISOTOPEDATAFILE)) {
     697     if ($Line =~ /^#/) {
     698       next LINE;
     699     }
     700     @LineWords = ();
     701     @LineWords = quotewords($InDelim, 0, $Line);
     702     if (@LineWords != $NumOfCols) {
     703       croak "Error: The number of data fields, @LineWords, in $ElementIsotopeDataFile must be $NumOfCols.\nLine: $Line...";
     704     }
     705     ($AtomicNumber, $IsotopeSymbol, $MassNumber, $RelativeAtomicMass, $NaturalAbundance) = @LineWords;
     706     if (exists $ZeroNaturalAbundanceMap{$AtomicNumber}) {
     707       # Only one isotope data line allowed for elements with no natural isotopes...
     708       carp "Warning: Ignoring isotope data for element with atomic number $AtomicNumber: Only one data line allowed for an element with no natural isotopes.\nLine: $Line...";
     709       next LINE;
     710     }
     711     if (IsEmpty($NaturalAbundance)) {
     712       $RelativeAtomicMass = 0;
     713       $NaturalAbundance = 0;
     714       $ZeroNaturalAbundanceMap{$AtomicNumber} = 1;
     715     }
     716     if (exists $ElementIsotopeDataMap{$AtomicNumber}) {
     717       # Additional data for an existing element...
     718       if (exists $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}) {
     719         carp "Warning: Ignoring isotope data for element with atomic number $AtomicNumber: It has already been loaded.\nLine: $Line...";
     720         next LINE;
     721       }
     722     }
     723     else {
     724       # Data for a new element...
     725       %{$ElementIsotopeDataMap{$AtomicNumber}} = ();
     726     }
     727     %{$ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}} = ();
     728     $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{IsotopeSymbol} = $IsotopeSymbol;
     729     $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{RelativeAtomicMass} = $RelativeAtomicMass;
     730     $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{NaturalAbundance} = $NaturalAbundance;
     731   }
     732   close ISOTOPEDATAFILE;
     733
     734   _SetupElementIsotopeDerivedDataMap();
     735 }
     736
     737 #
     738 # Map mass number of most abundant isotope for each element; additionally,
     739 # count number of isotopes as well.
     740 #
     741 sub _SetupElementIsotopeDerivedDataMap {
     742   my($AtomicNumber, $MassNumber, $NaturalAbundance, $MostNaturalAbundance, $MostAbundantMassNumber, $IsotopeCount);
     743
     744   %ElementIsotopeDerivedDataMap = ();
     745
     746   for $AtomicNumber (sort {$a <=> $b} keys %ElementIsotopeDataMap) {
     747     $IsotopeCount = 0;
     748     $MostAbundantMassNumber = 0;
     749     $MostNaturalAbundance = 0;
     750     MASSNUMBER: for $MassNumber (sort {$a <=> $b} keys %{$ElementIsotopeDataMap{$AtomicNumber}}) {
     751       $NaturalAbundance = $ElementIsotopeDataMap{$AtomicNumber}{$MassNumber}{NaturalAbundance};
     752       if (IsEmpty($NaturalAbundance)) {
     753         # No natural isotopes available...
     754         $MostAbundantMassNumber = $MassNumber;
     755         last MASSNUMBER;
     756       }
     757       if ($NaturalAbundance == 0) {
     758         # Not a natural isotope; Listed in periodic table data file to support non-natural
     759         # elements such as T. It's not included in natural isotope count...
     760         next MASSNUMBER;
     761       }
     762       $IsotopeCount++;
     763       if ($NaturalAbundance > $MostNaturalAbundance) {
     764         $MostAbundantMassNumber = $MassNumber;
     765         $MostNaturalAbundance = $NaturalAbundance;
     766       }
     767     }
     768     %{$ElementIsotopeDerivedDataMap{$AtomicNumber}} = ();
     769     $ElementIsotopeDerivedDataMap{$AtomicNumber}{IsotopeCount} = $IsotopeCount;
     770     $ElementIsotopeDerivedDataMap{$AtomicNumber}{MostAbundantMassNumber} = $MostAbundantMassNumber;
     771   }
     772 }
     773
     774 #
     775 # Setup element symbol map...
     776 #
     777 sub _SetupElementSymbolMap {
     778   my($AtomicNumber, $ElementSymbol);
     779
     780   %ElementSymbolMap = ();
     781
     782   for $AtomicNumber (keys %ElementDataMap) {
     783     $ElementSymbol = $ElementDataMap{$AtomicNumber}{ElementSymbol};
     784     $ElementSymbolMap{$ElementSymbol} = $AtomicNumber;
     785   }
     786 }
     787
     788 # Validate element ID...
     789 sub _ValidateElementID {
     790   my($ElementID) = @_;
     791   my($ElementSymbol, $AtomicNumber);
     792
     793   if ($ElementID =~ /[^0-9]/) {
     794     # Assume atomic symbol...
     795     $ElementSymbol = $ElementID;
     796     if (exists $ElementSymbolMap{$ElementSymbol}) {
     797       $AtomicNumber = $ElementSymbolMap{$ElementSymbol};
     798     }
     799     else {
     800       return undef;
     801     }
     802   }
     803   else {
     804     # Assume atomic number...
     805     $AtomicNumber = $ElementID;
     806     if (!exists $ElementDataMap{$AtomicNumber}) {
     807       return undef;
     808     }
     809   }
     810   return $AtomicNumber;
     811 }
     812