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