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