       1 package FileIO::MDLMolFileIO;
       2 #
       3 # File: MDLMolFileIO.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 Exporter;
      29 use Scalar::Util ();
      30 use TextUtil ();
      31 use FileUtil ();
      32 use SDFileUtil ();
      33 use FileIO::FileIO;
      34 use Molecule;
      35
      36 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
      37
      38 @ISA = qw(FileIO::FileIO Exporter);
      39 @EXPORT = qw();
      40 @EXPORT_OK = qw(IsMDLMolFile);
      41
      42 %EXPORT_TAGS = (all  => [@EXPORT, @EXPORT_OK]);
      43
      44 # Setup class variables...
      45 my($ClassName);
      46 _InitializeClass();
      47
      48 # Class constructor...
      49 sub new {
      50   my($Class, %NamesAndValues) = @_;
      51
      52   # Initialize object...
      53   my $This = $Class->SUPER::new();
      54   bless $This, ref($Class) || $Class;
      55   $This->_InitializeMDLMolFileIO();
      56
      57   $This->_InitializeMDLMolFileIOProperties(%NamesAndValues);
      58
      59   return $This;
      60 }
      61
      62 # Initialize any local object data...
      63 #
      64 sub _InitializeMDLMolFileIO {
      65   my($This) = @_;
      66
      67   # Nothing to do: Base class FileIO handles default class variables...
      68
      69   return $This;
      70 }
      71
      72 # Initialize class ...
      73 sub _InitializeClass {
      74   #Class name...
      75   $ClassName = __PACKAGE__;
      76
      77 }
      78
      79 # Initialize object values...
      80 sub _InitializeMDLMolFileIOProperties {
      81   my($This, %NamesAndValues) = @_;
      82
      83   # All other property names and values along with all Set/Get<PropertyName> methods
      84   # are implemented on-demand using ObjectProperty class.
      85
      86   my($Name, $Value, $MethodName);
      87   while (($Name, $Value) = each  %NamesAndValues) {
      88     $MethodName = "Set${Name}";
      89     $This->$MethodName($Value);
      90   }
      91
      92   if (!exists $NamesAndValues{Name}) {
      93     croak "Error: ${ClassName}->New: Object can't be instantiated without specifying file name...";
      94   }
      95
      96   # Make sure it's a MDLMol file...
      97   $Name = $NamesAndValues{Name};
      98   if (!$This->IsMDLMolFile($Name)) {
      99     croak "Error: ${ClassName}->New: Object can't be instantiated: File, $Name, doesn't appear to be MDLMol format...";
     100   }
     101
     102   return $This;
     103 }
     104
     105 # Is it a MDLMol file?
     106 sub IsMDLMolFile ($;$) {
     107   my($FirstParameter, $SecondParameter) = @_;
     108   my($This, $FileName, $Status);
     109
     110   if ((@_ == 2) && (_IsMDLMolFileIO($FirstParameter))) {
     111     ($This, $FileName) = ($FirstParameter, $SecondParameter);
     112   }
     113   else {
     114     $FileName = $FirstParameter;
     115   }
     116
     117   # Check file extension...
     118   $Status = FileUtil::CheckFileType($FileName, "mol");
     119
     120   return $Status;
     121 }
     122
     123 # Read molecule from file and return molecule object...
     124 sub ReadMolecule {
     125   my($This) = @_;
     126   my($FileHandle);
     127
     128   $FileHandle = $This->GetFileHandle();
     129   return $This->ParseMoleculeString(SDFileUtil::ReadCmpdString($FileHandle));
     130 }
     131
     132 # Write compound data using Molecule object...
     133 sub WriteMolecule {
     134   my($This, $Molecule) = @_;
     135
     136   if (!(defined($Molecule) && $Molecule->IsMolecule())) {
     137     carp "Warning: ${ClassName}->WriteMolecule: No data written: Molecule object is not specified...";
     138     return $This;
     139   }
     140   my($FileHandle);
     141   $FileHandle = $This->GetFileHandle();
     142
     143   print $FileHandle $This->GenerateMoleculeString($Molecule) . "\n";
     144
     145   return $This;
     146 }
     147
     148 # Retrieve molecule string...
     149 sub ReadMoleculeString {
     150   my($This) = @_;
     151   my($FileHandle);
     152
     153   $FileHandle = $This->GetFileHandle();
     154   return SDFileUtil::ReadCmpdString($FileHandle);
     155 }
     156
     157 # Parse molecule string and return molecule object. ParseMoleculeString supports two invocation methods: class
     158 # method or a package function.
     159 #
     160 sub ParseMoleculeString {
     161   my($FirstParameter, $SecondParameter) = @_;
     162   my($This, $MoleculeString);
     163
     164   if ((@_ == 2) && (_IsMDLMolFileIO($FirstParameter))) {
     165     ($This, $MoleculeString) = ($FirstParameter, $SecondParameter);
     166   }
     167   else {
     168     $MoleculeString = $FirstParameter;
     169     $This = undef;
     170   }
     171   if (!$MoleculeString) {
     172     return undef;
     173   }
     174   my($LineIndex, @MoleculeLines);
     175   @MoleculeLines = split /\n/, $MoleculeString;
     176
     177   # Create molecule object and set molecule level native and MDL properties...
     178   #
     179   my($Molecule);
     180   $Molecule = new Molecule();
     181
     182   # Set valence model for calculating implicit hydrogens...
     183   $Molecule->SetValenceModel('MDLValenceModel');
     184
     185   # Process headers data...
     186   $LineIndex = 0;
     187   my($MoleculeName) = SDFileUtil::ParseCmpdMolNameLine($MoleculeLines[$LineIndex]);
     188   $MoleculeName = TextUtil::RemoveTrailingWhiteSpaces($MoleculeName);
     189   $Molecule->SetName($MoleculeName);
     190
     191   $LineIndex++;
     192   my($UserInitial, $ProgramName, $Date, $Code, $ScalingFactor1, $ScalingFactor2, $Energy, $RegistryNum) = SDFileUtil::ParseCmpdMiscInfoLine($MoleculeLines[$LineIndex]);
     193   $Molecule->SetProperties('MDLUserInitial' => $UserInitial, 'MDLProgramName' => $ProgramName, 'MDLDate' => $Date, 'MDLCode' => $Code, 'MDLScalingFactor1' => $ScalingFactor1, 'MDLScalingFactor2' => $ScalingFactor2, 'MDLEnergy' => $Energy, 'MDLRegistryNum' => $RegistryNum);
     194
     195   $LineIndex++;
     196   my($Comments) = SDFileUtil::ParseCmpdCommentsLine($MoleculeLines[$LineIndex]);
     197   $Molecule->SetProperties('MDLComments' => $Comments);
     198
     199   $LineIndex++;
     200   my($AtomCount, $BondCount, $ChiralFlag, $PropertyCount, $Version) = SDFileUtil::ParseCmpdCountsLine($MoleculeLines[$LineIndex]);
     201
     202   $Molecule->SetProperties('MDLChiralFlag' => $ChiralFlag, 'MDLPropertyCount' => $PropertyCount, 'MDLVersion' => $Version);
     203
     204   # Process atom data...
     205   my($FirstAtomLineIndex, $LastAtomLineIndex, $AtomNum, $AtomX, $AtomY, $AtomZ, $AtomSymbol, $MassDifference, $Charge, $StereoParity, $Atom, %AtomNumToAtomMap);
     206
     207   $AtomNum = 0;
     208   %AtomNumToAtomMap = ();
     209   $FirstAtomLineIndex = 4; $LastAtomLineIndex = $FirstAtomLineIndex + $AtomCount - 1;
     210
     211   for ($LineIndex = $FirstAtomLineIndex; $LineIndex <= $LastAtomLineIndex; $LineIndex++) {
     212     $AtomNum++;
     213     ($AtomSymbol, $AtomX, $AtomY, $AtomZ, $MassDifference, $Charge, $StereoParity) = SDFileUtil::ParseCmpdAtomLine($MoleculeLines[$LineIndex]);
     214
     215     $Atom = new Atom('AtomSymbol' => $AtomSymbol, 'XYZ' => [$AtomX, $AtomY, $AtomZ]);
     216
     217     if ($MassDifference && $MassDifference != 0) {
     218       _ProcessMassDifference($Atom, $MassDifference);
     219     }
     220     if ($Charge && $Charge != 0) {
     221       _ProcessCharge($Atom, $Charge);
     222     }
     223     if ($StereoParity && $StereoParity != 0) {
     224       _ProcessStereoParity($Atom, $StereoParity);
     225     }
     226
     227     $AtomNumToAtomMap{$AtomNum} = $Atom;
     228     $Molecule->AddAtom($Atom);
     229   }
     230
     231   # Process bond data...
     232   my($FirstBondLineIndex, $LastBondLineIndex, $FirstAtomNum, $SecondAtomNum, $BondType, $BondStereo, $InternalBondOrder, $InternalBondType, $Bond, $Atom1, $Atom2);
     233
     234   $FirstBondLineIndex = $FirstAtomLineIndex + $AtomCount;
     235   $LastBondLineIndex = $FirstAtomLineIndex + $AtomCount + $BondCount - 1;
     236
     237   for ($LineIndex = $FirstBondLineIndex; $LineIndex <= $LastBondLineIndex; $LineIndex++) {
     238     ($FirstAtomNum, $SecondAtomNum, $BondType, $BondStereo) = SDFileUtil::ParseCmpdBondLine($MoleculeLines[$LineIndex]);
     239
     240     $Atom1 = $AtomNumToAtomMap{$FirstAtomNum};
     241     $Atom2 = $AtomNumToAtomMap{$SecondAtomNum};
     242
     243     ($InternalBondOrder, $InternalBondType) = SDFileUtil::MDLBondTypeToInternalBondOrder($BondType);
     244     $Bond = new Bond('Atoms' => [$Atom1, $Atom2], 'BondOrder' => $InternalBondOrder);
     245     $Bond->SetBondType($InternalBondType);
     246
     247     if ($BondStereo && $BondStereo != 0) {
     248       _ProcessBondStereo($Bond, $BondStereo);
     249     }
     250
     251     $Molecule->AddBond($Bond);
     252   }
     253
     254   # Process available property block lines starting with A  aaa, M CHG, M ISO and M RAD. All other property blocks
     255   # lines are for query or specific display purposes and are ignored for now.
     256   #
     257   #
     258   my($PropertyLineIndex, $PropertyLine, $FirstChargeOrRadicalLine, @ValuePairs);
     259
     260   $PropertyLineIndex = $FirstAtomLineIndex + $AtomCount + $BondCount;
     261   $PropertyLine = $MoleculeLines[$PropertyLineIndex];
     262   $FirstChargeOrRadicalLine = 1;
     263
     264   PROPERTYLINE: while ($PropertyLine !~ /^M  END/i ) {
     265     if ($PropertyLine =~ /\$\$\$\$/) {
     266       last PROPERTYLINE;
     267     }
     268     if ($PropertyLine =~ /^(M  CHG|M  RAD)/i) {
     269       if ($FirstChargeOrRadicalLine) {
     270         $FirstChargeOrRadicalLine = 0;
     271         _ZeroOutAtomsChargeAndRadicalValues(\%AtomNumToAtomMap);
     272       }
     273       if ($PropertyLine =~ /^M  CHG/i) {
     274         @ValuePairs = SDFileUtil::ParseCmpdChargePropertyLine($PropertyLine);
     275         _ProcessChargeProperty(\@ValuePairs, \%AtomNumToAtomMap);
     276       }
     277       elsif ($PropertyLine =~ /^M  RAD/i) {
     278         @ValuePairs = SDFileUtil::ParseCmpdRadicalPropertyLine($PropertyLine);
     279         _ProcessRadicalProperty(\@ValuePairs, \%AtomNumToAtomMap);
     280       }
     281     }
     282     elsif ($PropertyLine =~ /^M  ISO/i) {
     283       @ValuePairs = SDFileUtil::ParseCmpdIsotopePropertyLine($PropertyLine);
     284       _ProcessIsotopeProperty(\@ValuePairs, \%AtomNumToAtomMap);
     285     }
     286     elsif ($PropertyLine =~ /^A  /i) {
     287       my($NextPropertyLine);
     288       $PropertyLineIndex++;
     289       $NextPropertyLine = $MoleculeLines[$PropertyLineIndex];
     290       @ValuePairs = SDFileUtil::ParseCmpdAtomAliasPropertyLine($PropertyLine, $NextPropertyLine);
     291       _ProcessAtomAliasProperty(\@ValuePairs, \%AtomNumToAtomMap);
     292     }
     293     $PropertyLineIndex++;
     294     $PropertyLine = $MoleculeLines[$PropertyLineIndex];
     295   }
     296   # Store input molecule string as generic property of molecule...
     297   $Molecule->SetInputMoleculeString($MoleculeString);
     298
     299   return $Molecule;
     300 }
     301
     302 # Generate molecule string using molecule object...
     303 sub GenerateMoleculeString {
     304   my($FirstParameter, $SecondParameter) = @_;
     305   my($This, $Molecule);
     306
     307   if ((@_ == 2) && (_IsMDLMolFileIO($FirstParameter))) {
     308     ($This, $Molecule) = ($FirstParameter, $SecondParameter);
     309   }
     310   else {
     311     $Molecule = $FirstParameter;
     312     $This = undef;
     313   }
     314   if (!defined($Molecule)) {
     315     return undef;
     316   }
     317   my(@MoleculeLines);
     318   @MoleculeLines = ();
     319
     320   # First line: Molname line...
     321   push @MoleculeLines, SDFileUtil::GenerateCmpdMolNameLine($Molecule->GetName());
     322
     323   # Second line: Misc info...
     324   my($ProgramName, $UserInitial, $Code);
     325   $ProgramName = ''; $UserInitial = ''; $Code = '';
     326
     327   $Code = $Molecule->IsThreeDimensional() ? '3D' : '2D';
     328
     329   push @MoleculeLines, SDFileUtil::GenerateCmpdMiscInfoLine($ProgramName, $UserInitial, $Code);
     330
     331   # Third line: Comments line...
     332   my($Comments);
     333   $Comments = $Molecule->HasProperty('MDLComments') ? $Molecule->GetMDLComments() : ($Molecule->HasProperty('Comments') ? $Molecule->GetComments() : '');
     334   push @MoleculeLines, SDFileUtil::GenerateCmpdCommentsLine($Comments);
     335
     336   # Fourth line: Counts line for V2000
     337   my($AtomCount, $BondCount, $ChiralFlag);
     338   $AtomCount = $Molecule->GetNumOfAtoms();
     339   $BondCount = $Molecule->GetNumOfBonds();
     340   $ChiralFlag = 0;
     341   push @MoleculeLines, SDFileUtil::GenerateCmpdCountsLine($AtomCount, $BondCount, $ChiralFlag);
     342
     343   # Atom lines...
     344   my($Atom, $AtomSymbol, $AtomX, $AtomY, $AtomZ, $MassDifference, $Charge, $StereoParity, $AtomNum, $AtomID, @Atoms, %AtomIDToNum);
     345   my($ChargePropertyValue, $IsotopePropertyValue, $RadicalPropertyValue, $AtomAliasPropertyValue, @IsotopePropertyValuePairs, @ChargePropertyValuePairs, @RadicalPropertyValuePairs, @AtomAliasPropertyValuePairs);
     346
     347   @ChargePropertyValuePairs = ();
     348   @IsotopePropertyValuePairs = ();
     349   @RadicalPropertyValuePairs = ();
     350   @AtomAliasPropertyValuePairs = ();
     351
     352   @Atoms = $Molecule->GetAtoms();
     353
     354   $AtomNum = 0;
     355   for $Atom (@Atoms) {
     356     $AtomNum++;
     357     $AtomID = $Atom->GetID();
     358     $AtomIDToNum{$AtomID} = $AtomNum;
     359
     360     $AtomSymbol = $Atom->GetAtomSymbol();
     361     ($AtomX, $AtomY, $AtomZ) = $Atom->GetXYZ();
     362
     363     # Setup mass difference...
     364     $MassDifference = _GetMassDifference($Atom);
     365     if ($MassDifference) {
     366       # Hold it for M  ISO property lines...
     367       $IsotopePropertyValue = _GetIsotopePropertyValue($Atom);
     368       if ($IsotopePropertyValue) {
     369         push @IsotopePropertyValuePairs, ($AtomNum, $IsotopePropertyValue);
     370       }
     371     }
     372
     373     # Setup charge...
     374     $Charge = _GetCharge($Atom);
     375     if ($Charge) {
     376       # Hold it for M  CHG property lines...
     377       $ChargePropertyValue = _GetChargePropertyValue($Atom);
     378       if ($ChargePropertyValue) {
     379         push @ChargePropertyValuePairs, ($AtomNum, $ChargePropertyValue);
     380       }
     381     }
     382
     383     # Hold any radical values for  for M  RAD property lines...
     384     $RadicalPropertyValue = _GetRadicalPropertyValue($Atom);
     385     if ($RadicalPropertyValue) {
     386       push @RadicalPropertyValuePairs, ($AtomNum, $RadicalPropertyValue);
     387     }
     388
     389     # Hold any atom alias value for A  xxx property lines....
     390     $AtomAliasPropertyValue = _GetAtomAliasPropertyValue($Atom);
     391     if ($AtomAliasPropertyValue) {
     392       push @AtomAliasPropertyValuePairs, ($AtomNum, $AtomAliasPropertyValue);
     393
     394       # Set AtomSymbol to carbon as atom alias would override its value during parsing...
     395       $AtomSymbol = "C";
     396     }
     397
     398     # Setup stereo parity...
     399     $StereoParity = _GetStereoParity($Atom);
     400
     401     push @MoleculeLines, SDFileUtil::GenerateCmpdAtomLine($AtomSymbol, $AtomX, $AtomY, $AtomZ, $MassDifference, $Charge, $StereoParity);
     402   }
     403
     404   # Bond lines...
     405   my($FirstAtomID, $FirstAtom, $FirstAtomNum, $SecondAtomID, $SecondAtom, $SecondAtomNum, $MDLBondType, $BondOrder, $BondType, $MDLBondStereo, $Bond, @Bonds);
     406   for $FirstAtom (@Atoms) {
     407     $FirstAtomID = $FirstAtom->GetID();
     408     $FirstAtomNum = $AtomIDToNum{$FirstAtomID};
     409
     410     @Bonds = ();
     411     @Bonds = $FirstAtom->GetBonds();
     412     BOND: for $Bond (@Bonds) {
     413       $SecondAtom = $Bond->GetBondedAtom($FirstAtom);
     414       $SecondAtomID = $SecondAtom->GetID();
     415       $SecondAtomNum = $AtomIDToNum{$SecondAtomID};
     416       if ($FirstAtomNum >= $SecondAtomNum) {
     417         next BOND;
     418       }
     419       # Setup BondType...
     420       $BondOrder = $Bond->GetBondOrder();
     421       $BondType = $Bond->GetBondType();
     422       $MDLBondType = SDFileUtil::InternalBondOrderToMDLBondType($BondOrder, $BondType);
     423
     424       # Setup BondStereo...
     425       $MDLBondStereo = _GetBondStereo($Bond);
     426
     427       push @MoleculeLines, SDFileUtil::GenerateCmpdBondLine($FirstAtomNum, $SecondAtomNum, $MDLBondType, $MDLBondStereo);
     428     }
     429   }
     430   # Property lines...
     431   if (@IsotopePropertyValuePairs) {
     432     push @MoleculeLines, SDFileUtil::GenerateCmpdIsotopePropertyLines(\@IsotopePropertyValuePairs);
     433   }
     434   if (@ChargePropertyValuePairs) {
     435     push @MoleculeLines, SDFileUtil::GenerateCmpdChargePropertyLines(\@ChargePropertyValuePairs);
     436   }
     437   if (@RadicalPropertyValuePairs) {
     438     push @MoleculeLines, SDFileUtil::GenerateCmpdRadicalPropertyLines(\@RadicalPropertyValuePairs);
     439   }
     440   if (@AtomAliasPropertyValuePairs) {
     441     push @MoleculeLines, SDFileUtil::GenerateCmpdAtomAliasPropertyLines(\@AtomAliasPropertyValuePairs);
     442   }
     443
     444   push @MoleculeLines, "M  END";
     445
     446   return join "\n", @MoleculeLines;
     447 }
     448
     449 # Process MassDifference value and set atom's mass number...
     450 #
     451 sub _ProcessMassDifference {
     452   my($Atom, $MassDifference) = @_;
     453   my($MassNumber, $NewMassNumber, $AtomicNumber);
     454
     455   $AtomicNumber = $Atom->GetAtomicNumber();
     456
     457   if (!$AtomicNumber) {
     458     carp "Warning: ${ClassName}->_ProcessMassDifference: Ignoring specified mass difference value, $MassDifference, in SD file: Assigned to non standard element...";
     459     return;
     460   }
     461   $MassNumber = $Atom->GetMassNumber();
     462   if (!$MassDifference) {
     463     carp "Warning: ${ClassName}->_ProcessMassDifference: Ignoring specified mass difference value, $MassDifference, in SD file: Unknown MassNumber value...";
     464     return;
     465   }
     466   $NewMassNumber = $MassNumber + $MassDifference;
     467   if (!PeriodicTable::IsElementNaturalIsotopeMassNumber($AtomicNumber, $NewMassNumber)) {
     468     my($AtomSymbol) = $Atom->GetAtomSymbol();
     469     carp "Warning: ${ClassName}->_ProcessMassDifference: Unknown mass number, $MassNumber, corresponding to specified mass difference value, $MassDifference, in SD for atom with atomic number, $AtomicNumber, and atomic symbol, $AtomSymbol. The mass number value has been assigned. Don't forget to Set ExactMass property explicitly; otherwise, GetExactMass method would return mass of most abundant isotope...\n";
     470   }
     471
     472   # Use SetProperty method instead of SetMassNumber to skip explicit checks on MassNumber value...
     473   $Atom->SetProperty('MassNumber', $NewMassNumber);
     474 }
     475
     476 # Get mass difference value...
     477 sub _GetMassDifference {
     478   my($Atom) = @_;
     479   my($MassDifference, $MassNumber, $MostAbundantMassNumber, $AtomicNumber);
     480
     481   $MassDifference = 0;
     482   $MassNumber = $Atom->GetMassNumber();
     483   if (defined $MassNumber) {
     484     $AtomicNumber = $Atom->GetAtomicNumber();
     485     if (defined $AtomicNumber) {
     486       $MostAbundantMassNumber = PeriodicTable::GetElementMostAbundantNaturalIsotopeMassNumber($AtomicNumber);
     487       if (defined($MostAbundantMassNumber) && $MassNumber != $MostAbundantMassNumber) {
     488         $MassDifference = $MassNumber - $MostAbundantMassNumber;
     489       }
     490     }
     491   }
     492   return $MassDifference;
     493 }
     494
     495 # Process formal charge value and assign it to atom as formal charge...
     496 sub _ProcessCharge {
     497   my($Atom, $Charge) = @_;
     498   my($InternalCharge);
     499
     500   $InternalCharge = SDFileUtil::MDLChargeToInternalCharge($Charge);
     501   $Atom->SetFormalCharge($InternalCharge);
     502 }
     503
     504 # Get MDL formal charge value ...
     505 sub _GetCharge {
     506   my($Atom) = @_;
     507   my($InternalCharge, $Charge);
     508
     509   $Charge = 0;
     510   if ($Atom->HasProperty('FormalCharge')) {
     511     $InternalCharge = $Atom->GetFormalCharge();
     512     if ($InternalCharge) {
     513       $Charge = SDFileUtil::InternalChargeToMDLCharge($InternalCharge);
     514     }
     515   }
     516   return $Charge;
     517 }
     518
     519 # Process stereo parity value and assign it to atom as MDL property...
     520 #
     521 # Notes:
     522 #   . Mark atom as chiral center
     523 #   . Assign any explicit Clockwise (parity 1), CounterClockwise (parity 2) or either value (parity 3) as property of atom.
     524 #   . MDL values of Clockwise and CounterClockwise don't correspond to priority assigned to ligands around
     525 #     stereo center using CIP scheme; consequently, these values can't be used to set internal Stereochemistry for
     526 #     an atom.
     527 #
     528 sub _ProcessStereoParity {
     529   my($Atom, $StereoParity) = @_;
     530
     531   $Atom->SetStereoCenter('1');
     532   $Atom->SetMDLStereoParity($StereoParity);
     533 }
     534
     535 # Set stereo parity value to zero for now: The current release of MayaChemTools hasn't implemented
     536 # functionality to determine chirality.
     537 #
     538 sub _GetStereoParity {
     539   my($Atom) = @_;
     540   my($StereoParity);
     541
     542   $StereoParity = 0;
     543
     544   return $StereoParity;
     545 }
     546
     547 # Process bond stereo value...
     548 sub _ProcessBondStereo {
     549   my($Bond, $BondStereo) = @_;
     550   my($InternalBondStereo);
     551
     552   $InternalBondStereo = SDFileUtil::MDLBondStereoToInternalBondStereochemistry($BondStereo);
     553   if ($InternalBondStereo) {
     554     $Bond->SetBondStereochemistry($InternalBondStereo);
     555   }
     556 }
     557
     558 # Get MDLBondStereo value...
     559 sub _GetBondStereo {
     560   my($Bond) = @_;
     561   my($InternalBondStereo, $BondStereo);
     562
     563   $BondStereo = 0;
     564
     565   $InternalBondStereo = '';
     566   BONDSTEREO: {
     567     if ($Bond->IsUp()) {
     568       $InternalBondStereo = 'Up';
     569       last BONDSTEREO;
     570     }
     571     if ($Bond->IsDown()) {
     572       $InternalBondStereo = 'Down';
     573       last BONDSTEREO;
     574     }
     575     if ($Bond->IsUpOrDown()) {
     576       $InternalBondStereo = 'UpOrDown';
     577       last BONDSTEREO;
     578     }
     579     if ($Bond->IsCisOrTrans() || $Bond->IsCis() || $Bond->IsTrans()) {
     580       $InternalBondStereo = 'CisOrTrans';
     581       last BONDSTEREO;
     582     }
     583     $InternalBondStereo = '';
     584   }
     585
     586   if ($InternalBondStereo) {
     587     $BondStereo = SDFileUtil::InternalBondStereochemistryToMDLBondStereo($InternalBondStereo);
     588   }
     589
     590   return $BondStereo;
     591 }
     592
     593 # Zero out charge and radical values specified for atoms...
     594 sub _ZeroOutAtomsChargeAndRadicalValues {
     595   my($AtomNumToAtomMapRef) = @_;
     596   my($Atom);
     597
     598   for $Atom (values %{$AtomNumToAtomMapRef}) {
     599     if ($Atom->HasProperty('FormalCharge')) {
     600       $Atom->DeleteProperty('FormalCharge');
     601     }
     602     elsif ($Atom->HasProperty('SpinMultiplicity')) {
     603       $Atom->DeleteProperty('SpinMultiplicity');
     604     }
     605   }
     606 }
     607
     608 # Process charge property value pairs...
     609 sub _ProcessChargeProperty {
     610   my($ValuePairsRef, $AtomNumToAtomMapRef) = @_;
     611
     612   if (!(defined($ValuePairsRef) && @{$ValuePairsRef})) {
     613     return;
     614   }
     615   my($Index, $ValuePairsCount, $AtomNum, $Charge, $Atom);
     616
     617   $ValuePairsCount = scalar @{$ValuePairsRef};
     618   VALUEPAIRS: for ($Index = 0; $Index < $ValuePairsCount; $Index +=2) {
     619     $AtomNum = $ValuePairsRef->[$Index]; $Charge = $ValuePairsRef->[$Index + 1];
     620     if (!$Charge) {
     621       next VALUEPAIRS;
     622     }
     623     if (!exists $AtomNumToAtomMapRef->{$AtomNum}) {
     624       next VALUEPAIRS;
     625     }
     626     $Atom = $AtomNumToAtomMapRef->{$AtomNum};
     627     if ($Atom->HasProperty('SpinMultiplicity')) {
     628       carp "Warning: ${ClassName}->_ProcessChargeProperty: Setting formal charge on atom number, $AtomNum,  with already assigned spin multiplicity value...";
     629     }
     630     $Atom->SetFormalCharge($Charge);
     631   }
     632 }
     633
     634 # Get charge property value for an atom...
     635 sub _GetChargePropertyValue {
     636   my($Atom) = @_;
     637   my($Charge);
     638
     639   $Charge = 0;
     640   if ($Atom->HasProperty('FormalCharge')) {
     641     $Charge = $Atom->GetFormalCharge();
     642   }
     643   return $Charge;
     644 }
     645
     646 # Process charge property value pairs...
     647 sub _ProcessRadicalProperty {
     648   my($ValuePairsRef, $AtomNumToAtomMapRef) = @_;
     649
     650   if (!(defined($ValuePairsRef) && @{$ValuePairsRef})) {
     651     return;
     652   }
     653   my($Index, $ValuePairsCount, $AtomNum, $Radical, $SpinMultiplicity, $Atom);
     654
     655   $ValuePairsCount = scalar @{$ValuePairsRef};
     656   VALUEPAIRS: for ($Index = 0; $Index < $ValuePairsCount; $Index +=2) {
     657     $AtomNum = $ValuePairsRef->[$Index]; $Radical = $ValuePairsRef->[$Index + 1];
     658     if (!$Radical) {
     659       next VALUEPAIRS;
     660     }
     661     if (!exists $AtomNumToAtomMapRef->{$AtomNum}) {
     662       next VALUEPAIRS;
     663     }
     664     $Atom = $AtomNumToAtomMapRef->{$AtomNum};
     665     if ($Atom->HasProperty('FormalCharge')) {
     666       carp "Warning: ${ClassName}->_ProcessRadicalProperty: Setting spin multiplicity on atom number, $AtomNum,  with already assigned formal charge value...";
     667     }
     668     $SpinMultiplicity = SDFileUtil::MDLRadicalToInternalSpinMultiplicity($Radical);
     669     $Atom->SetSpinMultiplicity($SpinMultiplicity);
     670   }
     671 }
     672
     673 # Get radical property value for an atom...
     674 sub _GetRadicalPropertyValue {
     675   my($Atom) = @_;
     676   my($Radical, $SpinMultiplicity);
     677
     678   $Radical = 0;
     679   if ($Atom->HasProperty('SpinMultiplicity')) {
     680     $SpinMultiplicity = $Atom->GetSpinMultiplicity();
     681     $Radical = SDFileUtil::InternalSpinMultiplicityToMDLRadical($SpinMultiplicity);
     682   }
     683   return $Radical;
     684 }
     685
     686 # Process isotope property value pairs...
     687 sub _ProcessIsotopeProperty {
     688   my($ValuePairsRef, $AtomNumToAtomMapRef) = @_;
     689
     690   if (!(defined($ValuePairsRef) && @{$ValuePairsRef})) {
     691     return;
     692   }
     693   my($Index, $ValuePairsCount, $AtomNum, $MassNumber, $Atom, $AtomicNumber);
     694
     695   $ValuePairsCount = scalar @{$ValuePairsRef};
     696   VALUEPAIRS: for ($Index = 0; $Index < $ValuePairsCount; $Index +=2) {
     697     $AtomNum = $ValuePairsRef->[$Index]; $MassNumber = $ValuePairsRef->[$Index + 1];
     698     if (!$MassNumber) {
     699       next VALUEPAIRS;
     700     }
     701     if (!exists $AtomNumToAtomMapRef->{$AtomNum}) {
     702       next VALUEPAIRS;
     703     }
     704     $Atom = $AtomNumToAtomMapRef->{$AtomNum};
     705     $AtomicNumber = $Atom->GetAtomicNumber();
     706
     707     if (!PeriodicTable::IsElementNaturalIsotopeMassNumber($AtomicNumber, $MassNumber)) {
     708       my($AtomSymbol) = $Atom->GetAtomSymbol();
     709       carp "Warning: ${ClassName}->_ProcessProcessIsotopeProperty: Unknown mass number, $MassNumber, specified on M  ISO property line for atom number, $AtomNum,  in SD for atom with atomic number, $AtomicNumber, and atomic symbol, $AtomSymbol. The mass number value has been assigned. Don't forget to Set ExactMass property explicitly; otherwise, GetExactMass method would return mass of most abundant isotope...\n";
     710     }
     711
     712     # Use SetProperty method instead of SetMassNumber to skip explicit checks on MassNumber value...
     713     $Atom->SetProperty('MassNumber', $MassNumber);
     714   }
     715 }
     716
     717 # Get isotope property value for an atom...
     718 sub _GetIsotopePropertyValue {
     719   my($Atom) = @_;
     720   my($MassNumber);
     721
     722   $MassNumber = 0;
     723   if ($Atom->HasProperty('MassNumber')) {
     724     $MassNumber = $Atom->GetMassNumber();
     725   }
     726   return $MassNumber;
     727 }
     728
     729 # Process atom alias property value pairs...
     730 sub _ProcessAtomAliasProperty {
     731   my($ValuePairsRef, $AtomNumToAtomMapRef) = @_;
     732
     733   if (!(defined($ValuePairsRef) && @{$ValuePairsRef})) {
     734     return;
     735   }
     736   my($Index, $ValuePairsCount, $AtomNum, $AtomAlias, $Atom);
     737
     738   $ValuePairsCount = scalar @{$ValuePairsRef};
     739   VALUEPAIRS: for ($Index = 0; $Index < $ValuePairsCount; $Index +=2) {
     740     $AtomNum = $ValuePairsRef->[$Index]; $AtomAlias = $ValuePairsRef->[$Index + 1];
     741     if (!$AtomNum) {
     742       next VALUEPAIRS;
     743     }
     744     if (!exists $AtomNumToAtomMapRef->{$AtomNum}) {
     745       next VALUEPAIRS;
     746     }
     747     $AtomAlias = TextUtil::RemoveLeadingAndTrailingWhiteSpaces($AtomAlias);
     748     if (TextUtil::IsEmpty($AtomAlias)) {
     749       carp("Warning: ${ClassName}->_ProcessAtomAliasProperty: Ignoring atom alias property line: No Atom alias value specified...");
     750       next VALUEPAIRS;
     751     }
     752
     753     # Set atom symbol to atom alias which sets atomic number automatically...
     754     $Atom = $AtomNumToAtomMapRef->{$AtomNum};
     755     $Atom->SetAtomSymbol($AtomAlias);
     756
     757     $Atom->SetProperty('AtomAlias', $AtomAlias);
     758   }
     759 }
     760
     761 # Get atom alias property value for an atom...
     762 sub _GetAtomAliasPropertyValue {
     763   my($Atom) = @_;
     764   my($AtomAlias);
     765
     766   $AtomAlias = undef;
     767   if ($Atom->HasProperty('AtomAlias')) {
     768     $AtomAlias = $Atom->GetAtomAlias();
     769   }
     770   return $AtomAlias;
     771 }
     772
     773 # Is it a MDLMolFileIO object?
     774 sub _IsMDLMolFileIO {
     775   my($Object) = @_;
     776
     777   return (Scalar::Util::blessed($Object) && $Object->isa($ClassName)) ? 1 : 0;
     778 }
     779
     780