MayaChemTools

   1 package FileIO::MDLMolFileIO;
   2 #
   3 # File: MDLMolFileIO.pm
   4 # Author: Manish Sud <msud@san.rr.com>
   5 #
   6 # Copyright (C) 2024 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