MayaChemTools

   1 package Molecule;
   2 #
   3 # File: Molecule.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 Storable ();
  30 use Scalar::Util ();
  31 use ObjectProperty;
  32 use MathUtil;
  33 use PeriodicTable;
  34 use Text::ParseWords;
  35 use TextUtil;
  36 use FileUtil;
  37 use Graph;
  38 use Atom;
  39 use Bond;
  40 use MolecularFormula;
  41 
  42 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  43 
  44 @ISA = qw(Graph ObjectProperty Exporter);
  45 @EXPORT = qw(IsMolecule);
  46 @EXPORT_OK = qw(FormatElementalCompositionInformation GetSupportedAromaticityModels IsSupportedAromaticityModel);
  47 
  48 %EXPORT_TAGS = (all  => [@EXPORT, @EXPORT_OK]);
  49 
  50 # Setup class variables...
  51 my($ClassName, $ObjectID, %AromaticityModelsDataMap, %CanonicalAromaticityModelNamesMap);
  52 _InitializeClass();
  53 
  54 # Overload Perl functions...
  55 use overload '""' => 'StringifyMolecule';
  56 
  57 # Class constructor...
  58 sub new {
  59   my($Class, %NamesAndValues) = @_;
  60 
  61   # Initialize object...
  62   my $This = $Class->SUPER::new();
  63   bless $This, ref($Class) || $Class;
  64   $This->_InitializeMolecule();
  65 
  66   if (keys %NamesAndValues) { $This->_InitializeMoleculeProperties(%NamesAndValues); }
  67 
  68   return $This;
  69 }
  70 
  71 # Initialize object data...
  72 sub _InitializeMolecule {
  73   my($This) = @_;
  74   my($ObjectID) = _GetNewObjectID();
  75 
  76   # All other property names and values along with all Set/Get<PropertyName> methods
  77   # are implemented on-demand using ObjectProperty class.
  78   $This->{ID} = $ObjectID;
  79   $This->{Name} = "Molecule ${ObjectID}";
  80 }
  81 
  82 # Initialize molecule properties...
  83 sub _InitializeMoleculeProperties {
  84   my($This, %NamesAndValues) = @_;
  85 
  86   my($Name, $Value, $MethodName);
  87   while (($Name, $Value) = each  %NamesAndValues) {
  88     $MethodName = "Set${Name}";
  89     $This->$MethodName($Value);
  90   }
  91 }
  92 
  93 # Initialize class ...
  94 sub _InitializeClass {
  95   #Class name...
  96   $ClassName = __PACKAGE__;
  97 
  98   # ID to keep track of objects...
  99   $ObjectID = 0;
 100 
 101   # Load molecule class data...
 102   _LoadMoleculeClassData();
 103 }
 104 
 105 # Setup an explicit SetID method to block setting of ID by AUTOLOAD function...
 106 sub SetID {
 107   my($This, $Value) = @_;
 108 
 109   carp "Warning: ${ClassName}->SetID: Object ID can't be changed: it's used for internal tracking...";
 110 
 111   return $This;
 112 }
 113 
 114 # Add an atom...
 115 sub AddAtom {
 116   my($This, $Atom) = @_;
 117 
 118   if (!defined $Atom) {
 119     carp "Warning: ${ClassName}->AddAtom: No atom added: Atom must be specified...";
 120     return undef;
 121   }
 122   if ($This->HasAtom($Atom)) {
 123     carp "Warning: ${ClassName}->AddAtom: No atom added: Atom already exists...";
 124     return undef;
 125   }
 126   return $This->_AddAtom($Atom);
 127 }
 128 
 129 # Add an atom...
 130 sub _AddAtom {
 131   my($This, $Atom) = @_;
 132 
 133   # Assign atom to this molecule...
 134   $Atom->_SetMolecule($This);
 135 
 136   # Add it to the graph as a vertex...
 137   my($AtomID);
 138   $AtomID = $Atom->GetID();
 139   $This->AddVertex($AtomID);
 140   $This->SetVertexProperty('Atom', $Atom, $AtomID);
 141 
 142   return $This;
 143 }
 144 
 145 # Add atoms...
 146 sub AddAtoms {
 147   my($This, @Atoms) = @_;
 148 
 149   if (!@Atoms) {
 150     carp "Warning: ${ClassName}->AddAtoms: No atoms added: Atoms list must be specified...";
 151     return undef;
 152   }
 153   my($Atom);
 154   for $Atom (@Atoms) {
 155     $This->AddAtom($Atom);
 156   }
 157   return $This;
 158 }
 159 
 160 # Create an atom and add it to molecule...
 161 sub NewAtom {
 162   my($This, %NamesAndValues) = @_;
 163   my($Atom);
 164 
 165   $Atom = new Atom(%NamesAndValues);
 166   $This->AddAtom($Atom);
 167 
 168   return $Atom;
 169 }
 170 
 171 # Delete an atom...
 172 sub DeleteAtom {
 173   my($This, $Atom) = @_;
 174 
 175   if (!defined $Atom) {
 176     carp "Warning: ${ClassName}->DeleteAtom: No atom deleted: Atom must be specified...";
 177     return undef;
 178   }
 179   # Does the atom exist in  molecule?
 180   if (!$This->HasAtom($Atom)) {
 181     carp "Warning: ${ClassName}->DeleteAtom: No atom deleted: Atom doesn't exist...";
 182     return undef;
 183   }
 184   return $This->_DeleteAtom($Atom);
 185 }
 186 
 187 # Delete atom...
 188 sub _DeleteAtom {
 189   my($This, $Atom) = @_;
 190 
 191   my($AtomID);
 192   $AtomID = $Atom->GetID();
 193   $This->DeleteVertex($AtomID);
 194 
 195   return $This;
 196 }
 197 
 198 # Delete atoms...
 199 sub DeleteAtoms {
 200   my($This, @Atoms) = @_;
 201 
 202   if (!@Atoms) {
 203     carp "Warning: ${ClassName}->DeleteAtoms: No atoms added: Atoms list must be specified...";
 204     return undef;
 205   }
 206   my($Atom);
 207   for $Atom (@Atoms) {
 208     $This->DeleteAtom($Atom);
 209   }
 210 
 211   return $This;
 212 }
 213 
 214 # Is this atom present?
 215 sub HasAtom {
 216   my($This, $Atom) = @_;
 217 
 218   if (!defined $Atom) {
 219     return 0;
 220   }
 221   if (!$Atom->HasProperty('Molecule')) {
 222     # It's not in molecule...
 223     return 0;
 224   }
 225   my($AtomID);
 226   $AtomID = $Atom->GetID();
 227   if (!$This->HasVertex($AtomID)) {
 228     # It's not in molecule...
 229     return 0;
 230   }
 231   my($Molecule);
 232   $Molecule = $Atom->GetProperty('Molecule');
 233 
 234   return ($This->HasVertex($AtomID) && $This->GetID() == $Molecule->GetID()) ? 1 : 0;
 235 }
 236 
 237 # Return an array of atoms. In scalar context,  it returns number of atoms. Additionally,
 238 # atoms array can be filtered by any user specifiable Atom class method...
 239 #
 240 sub GetAtoms {
 241   my($This, $AtomCheckMethodName, $NegateMethodResult) = @_;
 242   my(@Atoms, @AtomIDs);
 243 
 244   @Atoms = (); @AtomIDs = ();
 245 
 246   @AtomIDs = $This->GetVertices();
 247   if (!@AtomIDs) {
 248     return wantarray ? @Atoms : scalar @Atoms;
 249   }
 250 
 251   @Atoms = $This->GetVerticesProperty('Atom', @AtomIDs);
 252 
 253   if (!defined $AtomCheckMethodName) {
 254     return wantarray ? @Atoms : scalar @Atoms;
 255   }
 256   $NegateMethodResult = (defined($NegateMethodResult) &&  $NegateMethodResult) ? 1 : 0;
 257 
 258   # Filter out atoms...
 259   my($Atom, $KeepAtom, @FilteredAtoms);
 260   @FilteredAtoms = ();
 261   for $Atom (@Atoms) {
 262     $KeepAtom = $NegateMethodResult ? (!$Atom->$AtomCheckMethodName()) : $Atom->$AtomCheckMethodName();
 263     if ($KeepAtom) {
 264       push @FilteredAtoms, $Atom;
 265     }
 266   }
 267   return wantarray ? @FilteredAtoms : scalar @FilteredAtoms;
 268 }
 269 
 270 # Return an array of bonds. In scalar context, it returns number of bonds...
 271 sub GetBonds {
 272   my($This) = @_;
 273   my(@Bonds, @EdgesAtomsIDs);
 274 
 275   @Bonds = (); @EdgesAtomsIDs = ();
 276 
 277   @EdgesAtomsIDs = $This->GetEdges();
 278   if (@EdgesAtomsIDs) {
 279     @Bonds = $This->GetEdgesProperty('Bond', @EdgesAtomsIDs);
 280   }
 281   return wantarray ? @Bonds : scalar @Bonds;
 282 }
 283 
 284 # Get number of atoms in molecule...
 285 sub GetNumOfAtoms {
 286   my($This) = @_;
 287   my($NumOfAtoms);
 288 
 289   $NumOfAtoms = $This->GetAtoms();
 290 
 291   return $NumOfAtoms;
 292 }
 293 
 294 # Get number of bonds in molecule...
 295 sub GetNumOfBonds {
 296   my($This) = @_;
 297   my($NumOfBonds);
 298 
 299   $NumOfBonds = $This->GetBonds();
 300 
 301   return $NumOfBonds;
 302 }
 303 
 304 # Get number of heavy atoms in molecule...
 305 sub GetNumOfHeavyAtoms {
 306   my($This) = @_;
 307 
 308   return $This->GetNumOfNonHydrogenAtoms();
 309 }
 310 
 311 # Get number of non-hydrogen atoms in molecule...
 312 sub GetNumOfNonHydrogenAtoms {
 313   my($This) = @_;
 314   my($NumOfNonHydrogenAtoms, $Atom, @Atoms);
 315 
 316   @Atoms = $This->GetAtoms();
 317   $NumOfNonHydrogenAtoms = 0;
 318   for $Atom (@Atoms) {
 319     if (!$Atom->IsHydrogen()) {
 320       $NumOfNonHydrogenAtoms++;
 321     }
 322   }
 323   return $NumOfNonHydrogenAtoms;
 324 }
 325 
 326 # Get number of hydrogen atoms in molecule...
 327 sub GetNumOfHydrogenAtoms {
 328   my($This) = @_;
 329   my($NumOfHydrogenAtoms, $Atom, @Atoms);
 330 
 331   @Atoms = $This->GetAtoms();
 332   $NumOfHydrogenAtoms = 0;
 333   for $Atom (@Atoms) {
 334     if ($Atom->IsHydrogen()) {
 335       $NumOfHydrogenAtoms++;
 336     }
 337   }
 338   return $NumOfHydrogenAtoms;
 339 }
 340 
 341 # Get number of missing hydrogen atoms in molecule...
 342 sub GetNumOfMissingHydrogenAtoms {
 343   my($This) = @_;
 344   my($NumOfMissingHydrogenAtoms, $Atom, @Atoms);
 345 
 346   @Atoms = $This->GetAtoms();
 347   $NumOfMissingHydrogenAtoms = 0;
 348   for $Atom (@Atoms) {
 349     if (!$Atom->IsHydrogen()) {
 350       $NumOfMissingHydrogenAtoms += $Atom->GetNumOfMissingHydrogens();
 351     }
 352   }
 353   return $NumOfMissingHydrogenAtoms;
 354 }
 355 
 356 # Add bond...
 357 sub AddBond {
 358   my($This, $Bond) = @_;
 359   my($Atom1, $Atom2);
 360 
 361   ($Atom1, $Atom2) = $Bond->GetAtoms();
 362   if (!(defined($Atom1) && defined($Atom2))) {
 363     carp "Warning: ${ClassName}->AddBond: No bond added: Both atoms must be specified...";
 364     return undef;
 365   }
 366   if (!($This->HasAtom($Atom1) && $This->HasAtom($Atom2))) {
 367     carp "Warning: ${ClassName}->AddBond: No bond added: Both atoms must be present...";
 368     return undef;
 369   }
 370   if ($This->HasBond($Bond)) {
 371     carp "Warning: ${ClassName}->AddBond: No bond added: Bond already exists...";
 372     return undef;
 373   }
 374   return $This->_AddBond($Bond);
 375 }
 376 
 377 # Add bond...
 378 sub _AddBond {
 379   my($This, $Bond) = @_;
 380 
 381   # Assign bond to this molecule...
 382   $Bond->_SetMolecule($This);
 383 
 384   # Add it to the graph as an edge...
 385   my($Atom1, $Atom2, $AtomID1, $AtomID2);
 386   ($Atom1, $Atom2) = $Bond->GetAtoms();
 387   $AtomID1 = $Atom1->GetID(); $AtomID2 = $Atom2->GetID();
 388   $This->AddEdge($AtomID1, $AtomID2);
 389   $This->SetEdgeProperty('Bond', $Bond, $AtomID1, $AtomID2);
 390 
 391   return $This;
 392 }
 393 
 394 # Add Bonds...
 395 sub AddBonds {
 396   my($This, @Bonds) = @_;
 397 
 398   if (!@Bonds) {
 399     carp "Warning: ${ClassName}->AddBonds: No bonds added: Bonds list must be specified...";
 400     return undef;
 401   }
 402   my($Bond);
 403   for $Bond (@Bonds) {
 404     $This->AddBond($Bond);
 405   }
 406   return $This;
 407 }
 408 
 409 # Create a bond and add it to molecule...
 410 sub NewBond {
 411   my($This, %NamesAndValues) = @_;
 412   my($Bond);
 413 
 414   $Bond = new Bond(%NamesAndValues);
 415   $This->AddBond($Bond);
 416 
 417   return $Bond;
 418 }
 419 
 420 # Delete a bond...
 421 sub DeleteBond {
 422   my($This, $Bond) = @_;
 423 
 424   if (!defined $Bond) {
 425     carp "Warning: ${ClassName}->DeleteBond: No bond deleted: Bond must be specified...";
 426     return undef;
 427   }
 428   # Does the bond exist in molecule?
 429   if (!$This->HasBond($Bond)) {
 430     carp "Warning: ${ClassName}->DeleteBond: No bond deleted: Bond doesn't exist...";
 431     return undef;
 432   }
 433   return $This->_DeleteBond($Bond);
 434 }
 435 
 436 # Delete bond...
 437 sub _DeleteBond {
 438   my($This, $Bond) = @_;
 439 
 440   my($Atom1, $Atom2, $AtomID1, $AtomID2);
 441   ($Atom1, $Atom2) = $Bond->GetAtoms();
 442   $AtomID1 = $Atom1->GetID(); $AtomID2 = $Atom2->GetID();
 443   $This->DeleteEdge($AtomID1, $AtomID2);
 444 
 445   return $This;
 446 }
 447 
 448 # Delete bonds...
 449 sub DeleteBonds {
 450   my($This, @Bonds) = @_;
 451 
 452   if (!@Bonds) {
 453     carp "Warning: ${ClassName}->DeleteBonds: No bonds added: Bonds list must be specified...";
 454     return undef;
 455   }
 456   my($Bond);
 457   for $Bond (@Bonds) {
 458     $This->DeleteBond($Bond);
 459   }
 460 
 461   return $This;
 462 }
 463 
 464 # Has bond...
 465 sub HasBond {
 466   my($This, $Bond) = @_;
 467   my($Atom1, $Atom2);
 468 
 469   ($Atom1, $Atom2) = $Bond->GetAtoms();
 470   if (!($This->HasAtom($Atom1) && $This->HasAtom($Atom2))) {
 471     return 0;
 472   }
 473   if (!$Bond->HasProperty('Molecule')) {
 474     # It's not in molecule...
 475     return 0;
 476   }
 477   my($AtomID1, $AtomID2, $Molecule);
 478   $AtomID1 = $Atom1->GetID(); $AtomID2 = $Atom2->GetID();
 479   $Molecule = $Bond->GetMolecule();
 480 
 481   return ($This->HasEdge($AtomID1, $AtomID2) && $This->GetID() == $Molecule->GetID()) ? 1 : 0;
 482 }
 483 
 484 # Get atom neighbors...
 485 sub _GetAtomNeighbors {
 486   my($This, $Atom) = @_;
 487 
 488   my($AtomID, @Atoms, @AtomIDs);
 489 
 490   @Atoms = (); @AtomIDs = ();
 491   $AtomID = $Atom->GetID();
 492   @AtomIDs = $This->GetNeighbors($AtomID);
 493   if (@AtomIDs) {
 494     @Atoms = $This->GetVerticesProperty('Atom', @AtomIDs);
 495   }
 496   return wantarray ? @Atoms : scalar @Atoms;
 497 }
 498 
 499 # Get atom bonds...
 500 sub _GetAtomBonds {
 501   my($This, $Atom) = @_;
 502   my($AtomID, @AtomIDs, @Bonds);
 503 
 504   @Bonds = (); @AtomIDs = ();
 505   $AtomID = $Atom->GetID();
 506   @AtomIDs = $This->GetEdges($AtomID);
 507   if (@AtomIDs) {
 508     @Bonds = $This->GetEdgesProperty('Bond', @AtomIDs);
 509   }
 510   return wantarray ? @Bonds : scalar @Bonds;
 511 }
 512 
 513 # Get bond to specified atom...
 514 sub _GetBondToAtom {
 515   my($This, $Atom1, $Atom2) = @_;
 516   my($AtomID1, $AtomID2);
 517 
 518   $AtomID1 = $Atom1->GetID();
 519   $AtomID2 = $Atom2->GetID();
 520 
 521   return $This->GetEdgeProperty('Bond', $AtomID1, $AtomID2);
 522 }
 523 
 524 # Are two specified atoms bonded?
 525 sub _IsBondedToAtom {
 526   my($This, $Atom1, $Atom2) = @_;
 527   my($AtomID1, $AtomID2);
 528 
 529   $AtomID1 = $Atom1->GetID();
 530   $AtomID2 = $Atom2->GetID();
 531 
 532   return $This->HasEdgeProperty('Bond', $AtomID1, $AtomID2);
 533 }
 534 
 535 # Add hydrogens to each atoms in molecule and return total number of hydrogens added...
 536 sub AddHydrogens {
 537   my($This) = @_;
 538 
 539   return $This->_AddHydrogens();
 540 }
 541 
 542 # Add hydrogens to polar atoms (N, O, P, S) in molecule and return total number of hydrogens added...
 543 sub AddPolarHydrogens {
 544   my($This) = @_;
 545   my($PolarHydrogensOnly) = 1;
 546 
 547   return $This->_AddHydrogens($PolarHydrogensOnly);
 548 }
 549 
 550 # Add all the hydrogens or hydrogens for polar atoms only...
 551 #
 552 # Note:
 553 #   . The current release of MayaChemTools doesn't assign any hydrogen positions.
 554 #
 555 sub _AddHydrogens {
 556   my($This, $PolarHydrogensOnly) = @_;
 557   my($Atom, $NumOfHydrogensAdded, $HydrogenPositionsWarning, @Atoms);
 558 
 559   if (! defined $PolarHydrogensOnly) {
 560     $PolarHydrogensOnly = 0;
 561   }
 562 
 563   $NumOfHydrogensAdded = 0;
 564   @Atoms = $This->GetAtoms();
 565   $HydrogenPositionsWarning = 0;
 566 
 567   ATOM: for $Atom (@Atoms) {
 568     if ($PolarHydrogensOnly) {
 569       if (!$Atom->IsPolarAtom()) {
 570         next ATOM;
 571       }
 572     }
 573     $NumOfHydrogensAdded += $Atom->AddHydrogens($HydrogenPositionsWarning);
 574   }
 575   return $NumOfHydrogensAdded;
 576 }
 577 
 578 # Delete all hydrogens atoms in molecule and return total number of hydrogens removed...
 579 sub DeleteHydrogens {
 580   my($This) = @_;
 581 
 582   return $This->_DeleteHydrogens();
 583 }
 584 
 585 # Delete hydrogens to polar atoms (N, O, P, S) in molecule and return total number of hydrogens removed...
 586 sub DeletePolarHydrogens {
 587   my($This) = @_;
 588   my($PolarHydrogensOnly) = 1;
 589 
 590   return $This->_DeleteHydrogens($PolarHydrogensOnly);
 591 }
 592 
 593 # Delete all hydrogens atoms in molecule and return total number of hydrogens removed...
 594 sub _DeleteHydrogens {
 595   my($This, $PolarHydrogensOnly) = @_;
 596   my($Atom, $NumOfHydrogensRemoved, @Atoms);
 597 
 598   if (! defined $PolarHydrogensOnly) {
 599     $PolarHydrogensOnly = 0;
 600   }
 601 
 602   $NumOfHydrogensRemoved = 0;
 603   @Atoms = $This->GetAtoms();
 604 
 605   ATOM: for $Atom (@Atoms) {
 606     if ($PolarHydrogensOnly) {
 607       if (!$Atom->IsPolarHydrogen()) {
 608         next ATOM;
 609       }
 610     }
 611     elsif (!$Atom->IsHydrogen()) {
 612       next ATOM;
 613     }
 614     $This->_DeleteAtom($Atom);
 615     $NumOfHydrogensRemoved++;
 616   }
 617   return $NumOfHydrogensRemoved;
 618 }
 619 
 620 # Get molecular weight by summing up atomic weights of all the atoms...
 621 sub GetMolecularWeight {
 622   my($This, $IncludeMissingHydrogens) = @_;
 623   my($MolecularWeight, $AtomicWeight, @Atoms, $Atom);
 624 
 625   $IncludeMissingHydrogens = defined($IncludeMissingHydrogens) ? $IncludeMissingHydrogens : 1;
 626 
 627   $MolecularWeight = 0;
 628   @Atoms = $This->GetAtoms();
 629   for $Atom (@Atoms) {
 630     $AtomicWeight = $Atom->GetAtomicWeight();
 631     if (defined $AtomicWeight) {
 632       $MolecularWeight += $AtomicWeight;
 633     }
 634   }
 635 
 636   if (!$IncludeMissingHydrogens) {
 637     return $MolecularWeight;
 638   }
 639 
 640   # Account for missing hydrogen atoms...
 641   my($NumOfMissingHydrogenAtoms);
 642 
 643   $NumOfMissingHydrogenAtoms = $This->GetNumOfMissingHydrogenAtoms();
 644   if ($NumOfMissingHydrogenAtoms) {
 645     $MolecularWeight += $NumOfMissingHydrogenAtoms * PeriodicTable::GetElementAtomicWeight('H');
 646   }
 647 
 648   return $MolecularWeight;
 649 }
 650 
 651 # Get exact mass by summing up exact masses of all the atoms...
 652 sub GetExactMass {
 653   my($This, $IncludeMissingHydrogens) = @_;
 654   my($ExactMass, $AtomicMass, @Atoms, $Atom);
 655 
 656   $IncludeMissingHydrogens = defined($IncludeMissingHydrogens) ? $IncludeMissingHydrogens : 1;
 657 
 658   $ExactMass = 0;
 659   @Atoms = $This->GetAtoms();
 660   for $Atom (@Atoms) {
 661     $AtomicMass = $Atom->GetExactMass();
 662     if (defined $AtomicMass) {
 663       $ExactMass += $AtomicMass;
 664     }
 665   }
 666 
 667   if (!$IncludeMissingHydrogens) {
 668     return $ExactMass;
 669   }
 670 
 671   # Account for missing hydrogen atoms...
 672   my($NumOfMissingHydrogenAtoms);
 673 
 674   $NumOfMissingHydrogenAtoms = $This->GetNumOfMissingHydrogenAtoms();
 675   if ($NumOfMissingHydrogenAtoms) {
 676     $ExactMass += $NumOfMissingHydrogenAtoms * PeriodicTable::GetElementMostAbundantNaturalIsotopeMass('H');
 677   }
 678 
 679   return $ExactMass;
 680 }
 681 
 682 # Get net formal charge on the molecule using one of the following two methods:
 683 #   . Using explicitly set FormalCharge property
 684 #   . Adding up formal charge on each atom in the molecule
 685 #
 686 # Caveats:
 687 #   . FormalCharge is different from Charge property of the molecule which corresponds to
 688 #     sum of partial atomic charges explicitly set for each atom using a specific methodology.
 689 #
 690 sub GetFormalCharge {
 691   my($This) = @_;
 692 
 693   # Is FormalCharge property explicitly set?
 694   if ($This->HasProperty('FormalCharge')) {
 695     return $This->GetProperty('FormalCharge');
 696   }
 697   my($FormalCharge, $AtomicFormalCharge, @Atoms, $Atom);
 698 
 699   $FormalCharge = 0;
 700   @Atoms = $This->GetAtoms();
 701   for $Atom (@Atoms) {
 702     $AtomicFormalCharge = $Atom->GetFormalCharge();
 703     if (defined $AtomicFormalCharge) {
 704       $FormalCharge += $AtomicFormalCharge;
 705     }
 706   }
 707   return $FormalCharge;
 708 }
 709 
 710 # Get net charge on the molecule using one of the following two methods:
 711 #   . Using explicitly set Charge property
 712 #   . Adding up charge on each atom in the molecule
 713 #
 714 # Caveats:
 715 #   . FormalCharge is different from Charge property of the molecule which corresponds to
 716 #     sum of partial atomic charges explicitly set for each atom using a specific methodology.
 717 #
 718 sub GetCharge {
 719   my($This) = @_;
 720 
 721   # Is Charge property explicitly set?
 722   if ($This->HasProperty('Charge')) {
 723     return $This->GetProperty('Charge');
 724   }
 725   my($Charge, $AtomicCharge, @Atoms, $Atom);
 726 
 727   $Charge = 0;
 728   @Atoms = $This->GetAtoms();
 729   for $Atom (@Atoms) {
 730     $AtomicCharge = $Atom->GetCharge();
 731     if (defined $AtomicCharge) {
 732       $Charge += $AtomicCharge;
 733     }
 734   }
 735   return $Charge;
 736 }
 737 
 738 # Get total SpinMultiplicity for the molecule using one of the following two methods:
 739 #   . Using explicitly set SpinMultiplicity property
 740 #   . Adding up SpinMultiplicity on each atom in the molecule
 741 #
 742 #
 743 sub GetSpinMultiplicity {
 744   my($This) = @_;
 745 
 746   # Is SpinMultiplicity property explicitly set?
 747   if ($This->HasProperty('SpinMultiplicity')) {
 748     return $This->GetProperty('SpinMultiplicity');
 749   }
 750   my($AtomicSpinMultiplicity, $SpinMultiplicity, @Atoms, $Atom);
 751 
 752   $SpinMultiplicity = 0;
 753   @Atoms = $This->GetAtoms();
 754   for $Atom (@Atoms) {
 755     $AtomicSpinMultiplicity = $Atom->GetSpinMultiplicity();
 756     if (defined $AtomicSpinMultiplicity) {
 757       $SpinMultiplicity += $AtomicSpinMultiplicity;
 758     }
 759   }
 760   return $SpinMultiplicity;
 761 }
 762 
 763 # Get total FreeRadicalElectrons for the molecule using one of the following two methods:
 764 #   . Using explicitly set FreeRadicalElectrons property
 765 #   . Adding up FreeRadicalElectrons on each atom in the molecule
 766 #
 767 #
 768 sub GetFreeRadicalElectrons {
 769   my($This) = @_;
 770 
 771   # Is FreeRadicalElectrons property explicitly set?
 772   if ($This->HasProperty('FreeRadicalElectrons')) {
 773     return $This->GetProperty('FreeRadicalElectrons');
 774   }
 775   my($AtomicFreeRadicalElectrons, $FreeRadicalElectrons, @Atoms, $Atom);
 776 
 777   $FreeRadicalElectrons = 0;
 778   @Atoms = $This->GetAtoms();
 779   for $Atom (@Atoms) {
 780     $AtomicFreeRadicalElectrons = $Atom->GetFreeRadicalElectrons();
 781     if (defined $AtomicFreeRadicalElectrons) {
 782       $FreeRadicalElectrons += $AtomicFreeRadicalElectrons;
 783     }
 784   }
 785   return $FreeRadicalElectrons;
 786 }
 787 
 788 # Set valence model...
 789 #
 790 sub SetValenceModel {
 791   my($This, $ValenceModel) = @_;
 792 
 793   if ($ValenceModel !~ /^(MDLValenceModel|DaylightValenceModel|InternalValenceModel|MayaChemToolsValenceModel)$/i) {
 794     carp "Warning: ${ClassName}->SetValenceModel: The current release of MayaChemTools doesn't support the specified valence model $ValenceModel. Supported valence models: MDLValenceModel, DaylightValenceModel, InternalValenceModel or MayaChemToolsValenceModel. Using internal valence model...";
 795     $ValenceModel = 'InternalValenceModel';
 796   }
 797 
 798   $This->SetProperty('ValenceModel', $ValenceModel);
 799 
 800   return $This;
 801 }
 802 
 803 # Get valence model...
 804 #
 805 sub GetValenceModel {
 806   my($This) = @_;
 807 
 808   # Is ValenceModel property explicitly set?
 809   if ($This->HasProperty('ValenceModel')) {
 810     return $This->GetProperty('ValenceModel');
 811   }
 812 
 813   # Used internal valence model as default model...
 814   return 'InternalValenceModel';
 815 }
 816 
 817 # Get molecular formula by collecting information about all atoms in the molecule and
 818 # composing the formula using Hills ordering system:
 819 #   . C shows up first and H follows assuming C is present
 820 #   . All other standard elements are sorted alphanumerically
 821 #   . All other non-stanard atom symbols are also sorted alphanumerically and follow standard elements
 822 #
 823 # Caveats:
 824 #   . By default, missing hydrogens and nonelements are also included
 825 #   . Elements for disconnected fragments are combined into the same formula
 826 #
 827 # Handle formula generation for disconnected structures. e.g: molecule generated by
 828 # [Na+].[O-]c1ccccc1
 829 #
 830 sub GetMolecularFormula {
 831   my($This, $IncludeMissingHydrogens, $IncludeNonElements) = @_;
 832   my($MolecularFormula, $AtomSymbol, $ElementsCountRef, $NonElementsCountRef);
 833 
 834   $IncludeMissingHydrogens = defined($IncludeMissingHydrogens) ? $IncludeMissingHydrogens : 1;
 835   $IncludeNonElements = defined($IncludeNonElements) ? $IncludeNonElements : 1;
 836 
 837   # Get elements count and setup molecular formula...
 838   ($ElementsCountRef, $NonElementsCountRef) = $This->GetElementsAndNonElements($IncludeMissingHydrogens);
 839   $MolecularFormula = '';
 840 
 841   # Count C and H first...
 842   if (exists $ElementsCountRef->{C} ) {
 843     $MolecularFormula .= 'C' . ($ElementsCountRef->{C} > 1 ? $ElementsCountRef->{C} : '');
 844     delete $ElementsCountRef->{C};
 845 
 846     if (exists $ElementsCountRef->{H} ) {
 847       $MolecularFormula .= 'H' . ($ElementsCountRef->{H} > 1 ? $ElementsCountRef->{H} : '');
 848       delete $ElementsCountRef->{H};
 849     }
 850   }
 851 
 852   # Sort elements...
 853   for $AtomSymbol (sort {$a cmp $b} keys %{$ElementsCountRef}) {
 854     $MolecularFormula .= $AtomSymbol . ($ElementsCountRef->{$AtomSymbol} > 1 ? $ElementsCountRef->{$AtomSymbol} : '');
 855   }
 856 
 857   # Sort non-elements...
 858   if ($IncludeNonElements) {
 859     for $AtomSymbol (sort {$a cmp $b} keys %{$NonElementsCountRef}) {
 860       $MolecularFormula .= $AtomSymbol . ($NonElementsCountRef->{$AtomSymbol} > 1 ? $NonElementsCountRef->{$AtomSymbol} : '');
 861     }
 862   }
 863 
 864   # Check formal charge...
 865   my($FormalCharge);
 866   $FormalCharge = $This->GetFormalCharge();
 867   if ($FormalCharge) {
 868     # Setup formal charge string...
 869     my($FormalChargeString);
 870     if ($FormalCharge == 1 ) {
 871       $FormalChargeString =  "+";
 872     }
 873     elsif ($FormalCharge == -1 ) {
 874       $FormalChargeString =  "-";
 875     }
 876     else {
 877       $FormalChargeString = ($FormalCharge > 0) ? ("+" . abs($FormalCharge)) : ("-" . abs($FormalCharge));
 878     }
 879     $MolecularFormula = "${MolecularFormula}${FormalChargeString}";
 880   }
 881 
 882   return $MolecularFormula;
 883 }
 884 
 885 # Count elements and non-elements in molecule and return references to hashes
 886 # containing count of elements and non-elements respectively. By default, missing
 887 # hydrogens are not added to the element hash.
 888 #
 889 #
 890 sub GetElementsAndNonElements {
 891   my($This, $IncludeMissingHydrogens) = @_;
 892   my($Atom, $AtomicNumber, $AtomSymbol, $NumOfMissingHydrogens, @Atoms, %ElementsCount, %NonElementsCount);
 893 
 894   $IncludeMissingHydrogens = defined($IncludeMissingHydrogens) ? $IncludeMissingHydrogens : 0;
 895 
 896   %ElementsCount = (); %NonElementsCount = ();
 897   $NumOfMissingHydrogens = 0;
 898 
 899   # Count elements and non elements...
 900   @Atoms = $This->GetAtoms();
 901   for $Atom (@Atoms) {
 902     $AtomicNumber = $Atom->GetAtomicNumber();
 903     $AtomSymbol = $Atom->GetAtomSymbol();
 904     if ($AtomicNumber) {
 905       if (exists $ElementsCount{$AtomSymbol}) {
 906         $ElementsCount{$AtomSymbol} += 1;
 907       }
 908       else {
 909         $ElementsCount{$AtomSymbol} = 1;
 910       }
 911       if ($IncludeMissingHydrogens) {
 912         $NumOfMissingHydrogens += $Atom->GetNumOfMissingHydrogens();
 913       }
 914     }
 915     else {
 916       if (exists $NonElementsCount{$AtomSymbol}) {
 917         $NonElementsCount{$AtomSymbol} += 1;
 918       }
 919       else {
 920         $NonElementsCount{$AtomSymbol} = 1;
 921       }
 922     }
 923   }
 924   if ($IncludeMissingHydrogens && $NumOfMissingHydrogens) {
 925     $AtomSymbol = 'H';
 926     if (exists $ElementsCount{$AtomSymbol}) {
 927       $ElementsCount{$AtomSymbol} += $NumOfMissingHydrogens;
 928     }
 929     else {
 930       $ElementsCount{$AtomSymbol} = $NumOfMissingHydrogens;
 931     }
 932   }
 933 
 934   return (\%ElementsCount, \%NonElementsCount);
 935 }
 936 
 937 # Get number of element and non-elements in molecule. By default, missing
 938 # hydrogens are not added to element count.
 939 #
 940 sub GetNumOfElementsAndNonElements {
 941   my($This, $IncludeMissingHydrogens) = @_;
 942   my($ElementCount, $NonElementCount, $Atom);
 943 
 944   $IncludeMissingHydrogens = defined($IncludeMissingHydrogens) ? $IncludeMissingHydrogens : 0;
 945 
 946   ($ElementCount, $NonElementCount) = (0, 0);
 947 
 948   ATOM: for $Atom ($This->GetAtoms()) {
 949     if (!$Atom->GetAtomicNumber()) {
 950       $NonElementCount++;
 951       next ATOM;
 952     }
 953     # Process element...
 954     $ElementCount++;
 955     if ($IncludeMissingHydrogens) {
 956       if (!$Atom->IsHydrogen()) {
 957         $ElementCount += $Atom->GetNumOfMissingHydrogens();
 958       }
 959     }
 960   }
 961 
 962   return ($ElementCount, $NonElementCount);
 963 }
 964 
 965 # Calculate elemental composition and return reference to arrays
 966 # containing elements and their percent composition.
 967 #
 968 # Caveats:
 969 #   . By default, missing hydrogens are included
 970 #   . Non elemnents are ignored
 971 #   . Mass number are ignored
 972 #
 973 sub GetElementalComposition {
 974   my($This, $IncludeMissingHydrogens) = @_;
 975   my($MolecularFormula, $IncludeNonElements, $ElementsCountRef, $NonElementsCountRef, $ElementsRef, $ElementsCompositionRef);
 976 
 977   $IncludeMissingHydrogens = defined($IncludeMissingHydrogens) ? $IncludeMissingHydrogens : 1;
 978 
 979   $IncludeNonElements = 0;
 980   ($ElementsCountRef, $NonElementsCountRef) = $This->GetElementsAndNonElements($IncludeMissingHydrogens);
 981 
 982   $MolecularFormula = $This->GetMolecularFormula($IncludeMissingHydrogens, $IncludeNonElements);
 983 
 984   ($ElementsRef, $ElementsCompositionRef) = MolecularFormula::CalculateElementalComposition($MolecularFormula);
 985 
 986   return ($ElementsRef, $ElementsCompositionRef);
 987 }
 988 
 989 # Using refernece to element and its composition arrays, format composition information
 990 # as: Element: Composition;...
 991 #
 992 sub FormatElementalCompositionInformation {
 993   my($FirstParameter, $SecondParameter, $ThirdParameter, $FourthParameter) = @_;
 994   my($This, $ElementsRef, $ElementCompositionRef, $Precision);
 995 
 996   if (_IsMolecule($FirstParameter)) {
 997     ($This, $ElementsRef, $ElementCompositionRef, $Precision) = ($FirstParameter, $SecondParameter, $ThirdParameter, $FourthParameter);
 998   }
 999   else {
1000     ($ElementsRef, $ElementCompositionRef, $Precision) = ($FirstParameter, $SecondParameter, $ThirdParameter);
1001   }
1002   my($FormattedInfo) = '';
1003 
1004   if (!(defined($ElementsRef) && @{$ElementsRef})) {
1005     carp "Warning: ${ClassName}->FormatElementalCompositionInformation: Elements list is not defined or empty...";
1006     return undef;
1007   }
1008   if (!(defined($ElementCompositionRef) && @{$ElementCompositionRef})) {
1009     carp "Warning: ${ClassName}->FormatElementalCompositionInformation: Elements composition list is not defined or empty...";
1010     return undef;
1011   }
1012 
1013   if (!defined $Precision) {
1014     $Precision = 2;
1015   }
1016 
1017   $FormattedInfo = MolecularFormula::FormatCompositionInfomation($ElementsRef, $ElementCompositionRef, $Precision);
1018 
1019   return $FormattedInfo;
1020 }
1021 
1022 # Copy molecule and its associated data using Storable::dclone and update:
1023 #
1024 #   o Atom references corresponding atoms and bonds objects
1025 #   o Bond object references
1026 #
1027 # Object IDs for Atoms and Bonds don't get changed. So there is no need to clear
1028 # up any exisiting ring data attached to molecule via graph as vertex IDs.
1029 #
1030 sub Copy {
1031   my($This) = @_;
1032   my($NewMolecule, $Atom, $NewAtom, $AtomID, @Atoms, @AtomIDs, %AtomsIDsToNewAtoms);
1033 
1034   $NewMolecule = Storable::dclone($This);
1035 
1036   # Update atom references stored as vertex property...
1037 
1038   @Atoms = (); @AtomIDs = ();
1039   %AtomsIDsToNewAtoms = ();
1040 
1041   @AtomIDs = $This->GetVertices();
1042   if (@AtomIDs) {
1043     @Atoms = $This->GetVerticesProperty('Atom', @AtomIDs);
1044   }
1045 
1046   for $Atom (@Atoms) {
1047     $AtomID = $Atom->GetID();
1048 
1049     # Setup a reference to copied atom object...
1050     $NewAtom = $Atom->Copy();
1051     $AtomsIDsToNewAtoms{$AtomID} = $NewAtom;
1052 
1053     # Update atom reference to new atom object...
1054     $NewMolecule->UpdateVertexProperty('Atom', $NewAtom, $AtomID);
1055   }
1056 
1057   # Update bond object and bond atom references stored as edge property...
1058   my($Index, $AtomID1, $AtomID2, $Bond, $NewBond, $NewAtom1, $NewAtom2, @EdgesAtomsIDs);
1059   @EdgesAtomsIDs = ();
1060   @EdgesAtomsIDs = $This->GetEdges();
1061   for ($Index = 0; $Index < $#EdgesAtomsIDs; $Index += 2) {
1062     $AtomID1 = $EdgesAtomsIDs[$Index]; $AtomID2 = $EdgesAtomsIDs[$Index + 1];
1063 
1064     # Get reference to bond object...
1065     $Bond = $This->GetEdgeProperty('Bond', $AtomID1, $AtomID2);
1066 
1067     # Make a new bond object and update its atom references...
1068     $NewBond = $Bond->Copy();
1069     $NewAtom1 = $AtomsIDsToNewAtoms{$AtomID1};
1070     $NewAtom2 = $AtomsIDsToNewAtoms{$AtomID2};
1071     $NewBond->SetAtoms($NewAtom1, $NewAtom2);
1072 
1073     # Update bond object reference in the new molecule...
1074     $NewMolecule->UpdateEdgeProperty('Bond', $NewBond, $AtomID1, $AtomID2);
1075   }
1076 
1077   return $NewMolecule;
1078 }
1079 
1080 # Get number of connected components...
1081 #
1082 sub GetNumOfConnectedComponents {
1083   my($This) = @_;
1084   my($NumOfComponents);
1085 
1086   $NumOfComponents = $This->GetConnectedComponentsVertices();
1087 
1088   return $NumOfComponents;
1089 }
1090 
1091 # Return a reference to an array containing molecules corresponding
1092 # to connected components sorted in decreasing order of component size...
1093 #
1094 sub GetConnectedComponents {
1095   my($This) = @_;
1096   my($Index, @ComponentMolecules, @ConnectedComponents);
1097 
1098   @ConnectedComponents = ();
1099   @ConnectedComponents = $This->GetConnectedComponentsVertices();
1100   @ComponentMolecules = ();
1101 
1102   for $Index (0 .. $#ConnectedComponents) {
1103     push @ComponentMolecules, $This->_GetConnectedComponent(\@ConnectedComponents, $Index);
1104   }
1105   return @ComponentMolecules;
1106 }
1107 
1108 # Return a reference to largest connected component as a molecule object...
1109 #
1110 sub GetLargestConnectedComponent {
1111   my($This) = @_;
1112   my($LargestComponentIndex, @ConnectedComponents);
1113 
1114   $LargestComponentIndex = 0;
1115   @ConnectedComponents = ();
1116   @ConnectedComponents = $This->GetConnectedComponentsVertices();
1117 
1118   return $This->_GetConnectedComponent(\@ConnectedComponents, $LargestComponentIndex);
1119 }
1120 
1121 # Return connected component as a molecule...
1122 #
1123 sub _GetConnectedComponent {
1124   my($This, $ConnectedComponentsRef, $ComponentIndex) = @_;
1125   my($ComponentMolecule);
1126 
1127   # Copy existing molecule...
1128   $ComponentMolecule = $This->Copy();
1129 
1130   # Delete all atoms besides the ones in specified component...
1131   $ComponentMolecule->_DeleteConnectedComponents($ConnectedComponentsRef, $ComponentIndex);
1132 
1133   # Clear any deteced rings...
1134   if ($ComponentMolecule->HasRings()) {
1135     $ComponentMolecule->ClearRings();
1136   }
1137   return $ComponentMolecule;
1138 }
1139 
1140 # Delete atoms corresponding to all connected components except the one specified...
1141 #
1142 sub _DeleteConnectedComponents {
1143   my($This, $ConnectedComponentsRef, $KeepComponentIndex) = @_;
1144   my($Index, $AtomID);
1145 
1146   INDEX: for $Index (0 .. $#{$ConnectedComponentsRef}) {
1147     if ($Index == $KeepComponentIndex) {
1148       next INDEX;
1149     }
1150     for $AtomID (@{$ConnectedComponentsRef->[$Index]}) {
1151       $This->DeleteVertex($AtomID);
1152     }
1153   }
1154   return $This;
1155 }
1156 
1157 # Return an array containing references to atom arrays corresponding to atoms of
1158 # connected components sorted in order of their decreasing size...
1159 #
1160 sub GetConnectedComponentsAtoms {
1161   my($This) = @_;
1162   my($Index, @ComponentsAtoms, @ConnectedComponents);
1163 
1164   @ConnectedComponents = ();
1165   @ConnectedComponents = $This->GetConnectedComponentsVertices();
1166 
1167   @ComponentsAtoms = ();
1168   for $Index (0 .. $#ConnectedComponents) {
1169     my(@ComponentAtoms);
1170 
1171     @ComponentAtoms = ();
1172     @ComponentAtoms = $This->_GetConnectedComponentAtoms(\@ConnectedComponents, $Index);
1173     push @ComponentsAtoms, \@ComponentAtoms;
1174   }
1175   return @ComponentsAtoms;
1176 }
1177 
1178 # Return an array containing atoms correspondig to largest connected component...
1179 #
1180 sub GetLargestConnectedComponentAtoms {
1181   my($This) = @_;
1182   my($LargestComponentIndex, @ConnectedComponents);
1183 
1184   $LargestComponentIndex = 0;
1185   @ConnectedComponents = ();
1186   @ConnectedComponents = $This->GetConnectedComponentsVertices();
1187 
1188   return $This->_GetConnectedComponentAtoms(\@ConnectedComponents, $LargestComponentIndex);
1189 }
1190 
1191 # Return an array containing atoms corresponding to specified connected component...
1192 #
1193 sub _GetConnectedComponentAtoms {
1194   my($This, $ConnectedComponentsRef, $ComponentIndex) = @_;
1195   my($AtomID, @AtomIDs, @ComponentAtoms);
1196 
1197   @ComponentAtoms = ();
1198   @AtomIDs = ();
1199 
1200   for $AtomID (@{$ConnectedComponentsRef->[$ComponentIndex]}) {
1201     push @AtomIDs, $AtomID;
1202   }
1203   @ComponentAtoms = $This->_GetAtomsFromAtomIDs(@AtomIDs);
1204 
1205   return @ComponentAtoms;
1206 }
1207 
1208 # Except for the largest connected component, delete atoms corresponding to all other
1209 # connected components...
1210 #
1211 sub KeepLargestComponent {
1212   my($This) = @_;
1213   my($LargestComponentIndex, @ConnectedComponents);
1214 
1215   @ConnectedComponents = ();
1216   @ConnectedComponents = $This->GetConnectedComponentsVertices();
1217   if (@ConnectedComponents == 1) {
1218     return $This;
1219   }
1220   $LargestComponentIndex = 0;
1221   $This->_DeleteConnectedComponents(\@ConnectedComponents, $LargestComponentIndex);
1222 
1223   # Clear any deteced rings...
1224   if ($This->HasRings()) {
1225     $This->ClearRings();
1226   }
1227 
1228   return $This;
1229 }
1230 
1231 # Get an array of topologically sorted atoms starting from a specified atom or
1232 # an arbitrary atom in the molecule...
1233 #
1234 sub GetTopologicallySortedAtoms {
1235   my($This, $StartAtom) = @_;
1236   my(@SortedAtoms);
1237 
1238   @SortedAtoms = ();
1239   if (defined($StartAtom) && !$This->HasAtom($StartAtom)) {
1240     carp "Warning: ${ClassName}->_GetTopologicallySortedAtoms: No atoms retrieved: Start atom doesn't exist...";
1241     return @SortedAtoms;
1242   }
1243   my($StartAtomID, @AtomIDs);
1244 
1245   @AtomIDs = ();
1246   $StartAtomID = defined($StartAtom) ? $StartAtom->GetID() : undef;
1247 
1248   @AtomIDs = $This->GetTopologicallySortedVertices($StartAtomID);
1249   @SortedAtoms = $This->_GetAtomsFromAtomIDs(@AtomIDs);
1250 
1251   return @SortedAtoms;
1252 }
1253 
1254 # Detect rings in molecule...
1255 #
1256 sub DetectRings {
1257   my($This) = @_;
1258 
1259   # Use graph method to detect all cycles and associate 'em to graph as graph
1260   # and vertex properties...
1261   return $This->DetectCycles();
1262 }
1263 
1264 # Clear rings in molecule...
1265 #
1266 sub ClearRings {
1267   my($This) = @_;
1268 
1269   # Use graph method to clear all cycles...
1270   $This->ClearCycles();
1271 
1272   return $This;
1273 }
1274 
1275 # Setup rings type paths to use during all ring related methods. Possible values:
1276 # Independent or All. Default is to use Independent rings.
1277 #
1278 sub SetActiveRings {
1279   my($This, $RingsType) = @_;
1280 
1281   if (!defined $This->SetActiveCyclicPaths($RingsType)) {
1282     return undef;
1283   }
1284   return $This;
1285 }
1286 
1287 # Is it a supported aromaticity model?
1288 #
1289 sub IsSupportedAromaticityModel {
1290   my($FirstParameter, $SecondParameter) = @_;
1291   my($This, $AromaticityModel);
1292 
1293   if (_IsMolecule($FirstParameter)) {
1294     ($This, $AromaticityModel) = ($FirstParameter, $SecondParameter);
1295   }
1296   else {
1297     ($This, $AromaticityModel) = (undef, $FirstParameter);
1298   }
1299 
1300   return exists $CanonicalAromaticityModelNamesMap{lc($AromaticityModel)} ? 1 : 0;
1301 }
1302 
1303 # Get a list of supported aromaticity model names...
1304 #
1305 sub GetSupportedAromaticityModels {
1306   return (sort values %CanonicalAromaticityModelNamesMap);
1307 }
1308 
1309 # Set aromaticity model...
1310 #
1311 sub SetAromaticityModel {
1312   my($This, $AromaticityModel) = @_;
1313 
1314   if (!$This->IsSupportedAromaticityModel($AromaticityModel)) {
1315     my(@SupportedModels) = $This->GetSupportedAromaticityModels();
1316 
1317     carp "Warning: ${ClassName}->SetAromaticityModel: The current release of MayaChemTools doesn't support the specified aromaticity model $AromaticityModel. Supported aromaticity models defined in AromaticityModelsData.csv file are: @SupportedModels . Using MayaChemToolsAromaticityModel...";
1318     $AromaticityModel = 'MayaChemToolsAromaticityModel';
1319   }
1320 
1321   $This->SetProperty('AromaticityModel', $AromaticityModel);
1322 
1323   return $This;
1324 }
1325 
1326 # Get aromaticity model...
1327 #
1328 sub GetAromaticityModel {
1329   my($This) = @_;
1330 
1331   # Is ValenceModel property explicitly set?
1332   if ($This->HasProperty('AromaticityModel')) {
1333     return $This->GetProperty('AromaticityModel');
1334   }
1335 
1336   # Used internal aromaticity model as default model...
1337   return 'MayaChemToolsAromaticityModel';
1338 }
1339 
1340 # Identify aromatic rings and ring systems in a molecule and set aromaticity for
1341 # corresponding atoms and bonds.
1342 #
1343 # What is aromaticity? [ Ref 124 ] It's in the code of the implementer, did you
1344 # say? Agree. The implementation of aromaticity varies widely across different
1345 # packages [ Ref 125 ]; additionally, the implementation details are not always
1346 # completely available, and it's not possible to figure out the exact implementation
1347 # of aromaticity across various packages. Using the publicly available information,
1348 # however, one can try to reproduce the available results to the extent possible,
1349 # along with parameterizing all the control parameters used to implement different
1350 # aromaticity models, and that's exactly what the current release of MayaChemTools
1351 # does.
1352 #
1353 # The implementation of aromaticity corresponding to various aromaticity models in
1354 # MayaChemTools package is driven by an external CSV file AromaticityModelsData.csv,
1355 # which is distributed with the package and is available in lib/data directory. The CSV
1356 # files contains names of supported aromaticity models, along with various control
1357 # parameters and their values. This file is loaded and processed during instantiation
1358 # of Molecule class and data corresponding to specific aromaticity model are used
1359 # to detect aromaticity for that model. Any new aromaticity model added to the
1360 # aromaticity data file, using different combinations of values for existing control
1361 # parameters would work without any changes to the code; the addition of any new
1362 # control parameters, however, requires its implementation in the code used to
1363 # calculate number of pi electrons available towards delocalization in a ring or ring
1364 # systems.
1365 #
1366 # The current release of MayaChemTools package supports these aromaticity
1367 # models: MDLAromaticityModel, TriposAromaticityModel, MMFFAromaticityModel,
1368 # ChemAxonBasicAromaticityModel, ChemAxonGeneralAromaticityModel,
1369 # DaylightAromaticityModel, MayaChemToolsAromaticityModel.
1370 #
1371 # The current list of control parameters available to detect aromaticity corresponding
1372 # to different aromaticity models are: AllowHeteroRingAtoms, HeteroRingAtomsList,
1373 # AllowExocyclicDoubleBonds, AllowHomoNuclearExocyclicDoubleBonds,
1374 # AllowElectronegativeRingAtomExocyclicDoubleBonds, AllowRingAtomFormalCharge,
1375 # AllowHeteroRingAtomFormalCharge, MinimumRingSize. The values for these control
1376 # parameters are specified in AromaticityModelsData.csv file.
1377 #
1378 # Although definition of aromaticity differs across various aromaticity models, a ring
1379 # or a ring system containing 4n + 2 pi electrons (Huckel's rule) corresponding to
1380 # alternate single and double bonds, in general, is considered aromatic.
1381 #
1382 # The available valence free electrons on heterocyclic ring atoms, involved in two single
1383 # ring bonds, are also allowed to participate in pi electron delocalizaiton for most of
1384 # the supported aromaticity models.
1385 #
1386 # The presence of exocyclic terminal double bond on ring atoms involved in pi electron
1387 # delocalization is only allowed for some of the aromaticity models. Additionally, the type
1388 # atoms involved in exocyclic terminal double bonds may result in making a ring or ring
1389 # system non-aromatic.
1390 #
1391 sub DetectAromaticity {
1392   my($This) = @_;
1393 
1394   # Delete aromaticity property for atoms and bonds...
1395   $This->DeleteAromaticity();
1396 
1397   # Any ring out there...
1398   if (!$This->HasRings()) {
1399     return $This;
1400   }
1401 
1402   if ($This->HasFusedRings()) {
1403     $This->_DetectAromaticityUsingFusedRingSets();
1404   }
1405   else {
1406     $This->_DetectAromaticityUsingIndividualRings();
1407   }
1408   return $This;
1409 }
1410 
1411 # Go over all rings and set aromaticity property for corresponding ring atoms
1412 # and bonds involved in aromatic rings...
1413 #
1414 sub _DetectAromaticityUsingIndividualRings {
1415   my($This) = @_;
1416 
1417   return $This->_DetectRingsAromaticity($This->GetRings());
1418 }
1419 
1420 # For each fused ring set, detect aromaticity by treating all of its ring as one aromatic
1421 # system for counting pi electrons to satisfy Huckel's rule; In case of a failure, rings in
1422 # fused set are treated individually for aromaticity detection. Additionally, non-fused
1423 # rings are handled on their own during aromaticity detection.
1424 #
1425 # Note:
1426 #   . pi electrons in common bonds involved in fused ring sets are only counted once.
1427 #
1428 #
1429 sub _DetectAromaticityUsingFusedRingSets {
1430   my($This) = @_;
1431   my($Index, $RingAtomsRef, $FusedRingSetRef, $FusedRingSetsRef, $NonFusedRingsRef, @FusedRingSetIsAromatic);
1432 
1433   ($FusedRingSetsRef, $NonFusedRingsRef) = $This->GetFusedAndNonFusedRings();
1434 
1435   @FusedRingSetIsAromatic = ();
1436   RINGSET: for $Index (0 .. $#{$FusedRingSetsRef}) {
1437     $FusedRingSetRef = $FusedRingSetsRef->[$Index];
1438     $FusedRingSetIsAromatic[$Index] = 0;
1439 
1440     my($NumOfPiElectronsInRingSet, $NumOfPiElectronsInRing, $Bond, $BondID, %FusedRingSetsBondsMap, %FusedRingSetsBondsVisitedMap, %FusedRingBondsMap);
1441 
1442     $NumOfPiElectronsInRingSet = 0;
1443 
1444     %FusedRingSetsBondsMap = ();
1445     %FusedRingSetsBondsVisitedMap = ();
1446     %FusedRingBondsMap = ();
1447 
1448     # Setup a bond ID map for all bonds in fused ring set and another one
1449     # for bonds involved in more than one ring...
1450     #
1451     for $RingAtomsRef (@{$FusedRingSetRef}) {
1452       for $Bond ($This->GetRingBonds(@{$RingAtomsRef})) {
1453         $BondID = $Bond->GetID();
1454         $FusedRingSetsBondsMap{$BondID} = $BondID;
1455 
1456         if ($Bond->GetNumOfRings() == 2) {
1457           $FusedRingBondsMap{$BondID} = $BondID;
1458         }
1459       }
1460     }
1461 
1462     for $RingAtomsRef (@{$FusedRingSetRef}) {
1463       my(@RingBonds);
1464 
1465       @RingBonds = ();
1466       @RingBonds = $This->GetRingBonds(@{$RingAtomsRef});
1467       $NumOfPiElectronsInRing = $This->_GetNumOfPiElectronsAvailableForDelocalization($RingAtomsRef, \@RingBonds, \%FusedRingSetsBondsMap, \%FusedRingSetsBondsVisitedMap, \%FusedRingBondsMap);
1468 
1469       if (!$NumOfPiElectronsInRing) {
1470         next RINGSET;
1471       }
1472       $NumOfPiElectronsInRingSet += $NumOfPiElectronsInRing;
1473     }
1474     if ($This->_DoPiElectronSatifyHuckelsRule($NumOfPiElectronsInRingSet)) {
1475       $FusedRingSetIsAromatic[$Index] = 1;
1476     }
1477   }
1478 
1479   # Set atom and bond aromatic flags for ring sets whose pi electrons satisfy Huckel's rule; otherwise,
1480   # treat rings in a ring set as individual rings for detecting aromaticity...
1481   for $Index (0 .. $#{$FusedRingSetsRef}) {
1482     $FusedRingSetRef = $FusedRingSetsRef->[$Index];
1483     if ($FusedRingSetIsAromatic[$Index]) {
1484       $This->_SetRingsAromaticity(@{$FusedRingSetRef});
1485     }
1486     else {
1487       $This->_DetectRingsAromaticity(@{$FusedRingSetRef});
1488     }
1489   }
1490 
1491   $This->_DetectRingsAromaticity(@{$NonFusedRingsRef});
1492 
1493   return $This;
1494 }
1495 
1496 # Detect and set aromaticity for rings...
1497 #
1498 sub _DetectRingsAromaticity {
1499   my($This, @Rings) = @_;
1500   my($RingAtom, $RingBond, $RingAtomsRef);
1501 
1502   RING: for $RingAtomsRef (@Rings) {
1503     if (!$This->_CheckRingAromaticity(@{$RingAtomsRef})) {
1504       next RING;
1505     }
1506     $This->_SetRingAromaticity(@{$RingAtomsRef});
1507   }
1508   return $This;
1509 }
1510 
1511 # Set aromatic property for all all atoms and bonds involved in all specified rings..
1512 #
1513 sub _SetRingsAromaticity {
1514   my($This, @Rings) = @_;
1515   my($RingAtomsRef );
1516 
1517   for $RingAtomsRef (@Rings) {
1518     $This->_SetRingAromaticity(@{$RingAtomsRef});
1519   }
1520   return $This;
1521 }
1522 
1523 # Set aromatic property for all all atoms and bonds involved in ring..
1524 #
1525 sub _SetRingAromaticity {
1526   my($This, @RingAtoms) = @_;
1527   my($RingAtom, $RingBond);
1528 
1529   for $RingAtom (@RingAtoms) {
1530     $RingAtom->SetAromatic(1);
1531   }
1532   for $RingBond ($This->GetRingBonds(@RingAtoms)) {
1533     $RingBond->SetAromatic(1);
1534   }
1535   return $This;
1536 }
1537 
1538 
1539 # For a ring to be an aromatic ring, all of its atoms must have aromatic property
1540 # set.
1541 #
1542 sub IsRingAromatic {
1543   my($This, @RingAtoms) = @_;
1544   my($RingAtom);
1545 
1546   for $RingAtom (@RingAtoms) {
1547     if (!$RingAtom->IsAromatic()) {
1548       return 0;
1549     }
1550   }
1551   return 1;
1552 }
1553 
1554 # Delete aromatic property for all atoms and bonds...
1555 #
1556 sub DeleteAromaticity {
1557   my($This) = @_;
1558 
1559   return $This->_DeleteAtomsAndBondsAromaticity();
1560 }
1561 
1562 # Check ring aromaticity...
1563 #
1564 sub _CheckRingAromaticity {
1565   my($This, @RingAtoms) = @_;
1566   my($NumOfPiElectrons, $BondID, @RingBonds);
1567 
1568   @RingBonds = ();
1569   @RingBonds = $This->GetRingBonds(@RingAtoms);
1570 
1571   $NumOfPiElectrons = $This->_GetNumOfPiElectronsAvailableForDelocalization(\@RingAtoms, \@RingBonds);
1572 
1573   return $This->_DoPiElectronSatifyHuckelsRule($NumOfPiElectrons);
1574 }
1575 
1576 # Get number of pi electrons available for delocalizaiton in a ring or ring system...
1577 #
1578 sub _GetNumOfPiElectronsAvailableForDelocalization {
1579   my($This, $RingAtomsRef, $RingBondsRef, $FusedRingSetsBondsMapRef, $FusedRingSetsBondsVisitedMapRef, $FusedRingBondsMapRef) = @_;
1580   my($AromaticityModelName, $AromaticityModelDataRef, $ExocyclicDoubleBondsDataMapRef, $NumOfConjugatedDoubleBonds, $NumOfExplicitAromaticBonds, $NumOfRingAtomElectronPairs, $NumOfRingBondsProcessed, $NumOfPiElectrons, $Index, $RingBond, $RingAtom, $RingAtomSymbol, $BondOrder, $RingAtomID, $RingBondID, $PreviousIndex, $PreviousRingBond, $ExcludeFreeRadicalElectrons, %ElectronPairContributionProcessedMap);
1581 
1582   $AromaticityModelName = $CanonicalAromaticityModelNamesMap{lc($This->GetAromaticityModel())};
1583   $AromaticityModelDataRef = \%{$AromaticityModelsDataMap{$AromaticityModelName}};
1584 
1585   # Perform an intial check for potential atomatic ring atoms..
1586   if (!$This->_CheckRingAtomsForPotentialAromaticity($RingAtomsRef, $RingBondsRef, $AromaticityModelDataRef)) {
1587     return 0;
1588   }
1589 
1590   $ExocyclicDoubleBondsDataMapRef = undef;
1591   $ExcludeFreeRadicalElectrons = 1;
1592 
1593   %ElectronPairContributionProcessedMap = ();
1594 
1595   ($NumOfPiElectrons, $NumOfRingBondsProcessed, $NumOfConjugatedDoubleBonds, $NumOfExplicitAromaticBonds, $NumOfRingAtomElectronPairs) = ('0') x 5;
1596 
1597   # Go over ring atoms and bonds to check their participation in aromaticity and count
1598   # pi electrons available for delocalization corresponding to various aromaticity models...
1599   #
1600   RINGBOND: for $Index (0 .. $#{$RingBondsRef}) {
1601     $RingBond = $RingBondsRef->[$Index];
1602     $RingAtom = $RingAtomsRef->[$Index];
1603     $BondOrder = $RingBond->GetBondOrder();
1604 
1605     # Is this ring bond part of a fused ring system which has been already processed?
1606     if (defined($FusedRingSetsBondsVisitedMapRef) && $RingBond->GetNumOfRings() == 2) {
1607       $RingBondID = $RingBond->GetID();
1608       if (exists $FusedRingSetsBondsVisitedMapRef->{$RingBondID}) {
1609         next RINGBOND;
1610       }
1611       $FusedRingSetsBondsVisitedMapRef->{$RingBondID} = $RingBondID;
1612     }
1613     $NumOfRingBondsProcessed++;
1614 
1615     # For first ring, previous ring bond corrresponds to last ring bond...
1616     $PreviousIndex = $Index ? ($Index -1) : $#{$RingBondsRef};
1617     $PreviousRingBond = $RingBondsRef->[$PreviousIndex];
1618 
1619     # Check for presence of alternate single/double bond configuration, and pesence of
1620     # hetero atoms with two single ring bonds along with any exocyclic double bonds...
1621     #
1622     BONDORDER: {
1623       # Is current ring double bond in an alternate single/double bond configuration?
1624       if ($BondOrder == 2) {
1625         if ($PreviousRingBond->GetBondOrder() != 1) {
1626           return 0;
1627         }
1628         $NumOfConjugatedDoubleBonds += 1;
1629         last BONDORDER;
1630       }
1631 
1632       # Is current ring bond order correspond to an explicit aromatic bond?
1633       if ($BondOrder == 1.5) {
1634         if ($PreviousRingBond->GetBondOrder() != 1.5) {
1635           return 0;
1636         }
1637         $NumOfExplicitAromaticBonds += 1;
1638         last BONDORDER;
1639       }
1640 
1641       # Check for potential hetero atoms involved in two single ring bonds along
1642       # with any terminal exocyclic bonds...
1643       if ($BondOrder == 1) {
1644         if ($PreviousRingBond->GetBondOrder() != 1) {
1645           # Part of a conjugated system...
1646           last BONDORDER;
1647         }
1648 
1649         # Identify any exocylic bonds on rings atoms...
1650         if (!defined $ExocyclicDoubleBondsDataMapRef) {
1651           $ExocyclicDoubleBondsDataMapRef = $This->_IdentifyRingAtomsInvolvedInExocyclicDoubleBonds($RingAtomsRef, $RingBondsRef, $FusedRingSetsBondsMapRef);
1652         }
1653 
1654         # Is current ring atom part of an allowed exocyclic terminal bond?
1655         if (!$This->_CheckPotentialAromaticRingAtomForExocylicDoubleBonds($RingAtom, $AromaticityModelDataRef, $ExocyclicDoubleBondsDataMapRef)) {
1656           return 0;
1657         }
1658 
1659         # Is it allowed to have any formal charge?
1660         if (!$This->_CheckPotentialAromaticRingAtomForFormalCharge($RingAtom, $AromaticityModelDataRef)) {
1661           return 0;
1662         }
1663 
1664         # It it an allowed hetero ring atom or a carbon atom?
1665         if (!$This->_CheckPotentialAromaticRingAtomForAllowedHeteroAtoms($RingAtom, $AromaticityModelDataRef)) {
1666           return 0;
1667         }
1668 
1669         $RingAtomID = $RingAtom->GetID();
1670         $ElectronPairContributionProcessedMap{$RingAtomID} = $RingAtomID;
1671 
1672         # Is it able to donate a pair for electrons towards pi electron delocalization?
1673         if ($RingAtom->GetValenceFreeElectrons($ExcludeFreeRadicalElectrons) >= 2) {
1674           # Possibilites:
1675           #   . Hetero atom with or without formal charge and an available electron pair
1676           #   . Carbon atom with -ve formal charge and with an available electron pair
1677           #
1678           $NumOfRingAtomElectronPairs += 1;
1679         }
1680         else {
1681           # Is ring atom involved in two single bonds without any electron pair allowed?
1682           if (!$This->_AllowRingAtomInTwoSingleBondsWithoutElectronPair($RingAtom, $RingBond, $PreviousRingBond, $ExocyclicDoubleBondsDataMapRef, $FusedRingBondsMapRef)) {
1683             return 0;
1684           }
1685         }
1686         last BONDORDER;
1687       }
1688 
1689       # Any other type of ring atom/bond is not allowed to contribute towards pi electron count
1690       # and caused loss of aromaticity...
1691       return 0;
1692     }
1693   }
1694 
1695   # Check for any electron pair contributions towards pi electron delocalization due to
1696   # -ve formal charge on ring atoms which haven't been already processed and part of
1697   # conjugated single/double bond system...
1698   #
1699   $NumOfRingAtomElectronPairs += $This->_GetElectronPairsContributionFromConjugatedRingAtoms($RingAtomsRef, $RingBondsRef, $ExcludeFreeRadicalElectrons, $AromaticityModelDataRef, \%ElectronPairContributionProcessedMap);
1700 
1701   # Setup pi electron count available for delocalization...
1702   COUNT: {
1703     if ($NumOfExplicitAromaticBonds == $NumOfRingBondsProcessed) {
1704       # Each aromatic bond contribute one electron towards pi electron delocalization...
1705       $NumOfPiElectrons = $NumOfExplicitAromaticBonds;
1706       last COUNT;
1707     }
1708 
1709     # Each conjugated double bond contribute two electrons towards pi electron delocalization...
1710     $NumOfPiElectrons = 2*$NumOfConjugatedDoubleBonds + 2*$NumOfRingAtomElectronPairs;
1711   }
1712 
1713   return $NumOfPiElectrons;
1714 }
1715 
1716 # Check ring atoms for their potential participation in aromatic systems..
1717 #
1718 sub _CheckRingAtomsForPotentialAromaticity {
1719   my($This, $RingAtomsRef, $RingBondsRef, $AromaticityModelDataRef) = @_;
1720   my($Index, $RingBond, $RingAtom);
1721 
1722   # Check availability of ring atoms and bonds...
1723   if (!(defined($RingAtomsRef) && @{$RingBondsRef})) {
1724     return 0;
1725   }
1726 
1727   # Is there any minimum ring size limit?
1728   if ($AromaticityModelDataRef->{MinimumRingSize}) {
1729     if (@{$RingAtomsRef} < $AromaticityModelDataRef->{MinimumRingSize}) {
1730       return 0;
1731     }
1732   }
1733 
1734   # Make sure ring bond order is not greater than 2 and ring atom is not connected to more
1735   # than 3 other atoms to eliminate any non sp2 carbon atoms and still allow for hetero atoms
1736   # to contrbute towards electron delocalization...
1737   #
1738   for $Index (0 .. $#{$RingBondsRef}) {
1739     $RingBond = $RingBondsRef->[$Index];
1740     $RingAtom = $RingAtomsRef->[$Index];
1741 
1742     if (($RingBond->GetBondOrder() > 2) || ($RingAtom->GetNumOfBonds() + $RingAtom->GetNumOfMissingHydrogens()) > 3) {
1743       return 0;
1744     }
1745   }
1746 
1747   return 1;
1748 }
1749 
1750 # Identify any exocylic double bonds on ring atoms...
1751 #
1752 sub _IdentifyRingAtomsInvolvedInExocyclicDoubleBonds {
1753   my($This, $RingAtomsRef, $RingBondsRef, $FusedRingSetsBondsMapRef) = @_;
1754   my($Index, $RingAtom, $RingBond, $RingAtomID, $Bond, $BondID, $BondedAtom, $RingBondsMapRef, %RingBondsMap, %ExocyclicDoubleBondsDataMap);
1755 
1756   # Setup a ring bond map to process exocyclic bonds...
1757   $RingBondsMapRef = undef;
1758   %RingBondsMap = ();
1759 
1760   if (defined $FusedRingSetsBondsMapRef) {
1761     $RingBondsMapRef = $FusedRingSetsBondsMapRef;
1762   }
1763   else {
1764     for $BondID (map { $_->GetID() } @{$RingBondsRef}) {
1765       $RingBondsMap{$BondID} = $BondID;
1766     }
1767     $RingBondsMapRef = \%RingBondsMap;
1768   }
1769 
1770   # Intialize exocyclic terminal double bond data...
1771   %ExocyclicDoubleBondsDataMap = ();
1772   %{$ExocyclicDoubleBondsDataMap{RingAtomID}} = ();
1773 
1774   for $Index (0 .. $#{$RingBondsRef}) {
1775     $RingBond = $RingBondsRef->[$Index];
1776     $RingAtom = $RingAtomsRef->[$Index];
1777 
1778     $RingAtomID = $RingAtom->GetID();
1779 
1780     BOND: for $Bond ($RingAtom->GetBonds()) {
1781       if ($Bond->GetBondOrder != 2) {
1782         next BOND;
1783       }
1784 
1785       # Is it part of ring or ring system under consideration?
1786       if (exists $RingBondsMapRef->{$Bond->GetID()}) {
1787         next BOND;
1788       }
1789 
1790       # Is bonded atom in a ring or a non-terminal atom?
1791       $BondedAtom = $Bond->GetBondedAtom($RingAtom);
1792       if ($BondedAtom->IsInRing() || !$BondedAtom->IsTerminal() ) {
1793         next BOND;
1794       }
1795 
1796       # Track exocyclic terminal double bond information...
1797       if (!exists $ExocyclicDoubleBondsDataMap{RingAtomID}{$RingAtomID}) {
1798         @{$ExocyclicDoubleBondsDataMap{RingAtomID}{$RingAtomID}} = ();
1799       }
1800       push @{$ExocyclicDoubleBondsDataMap{RingAtomID}{$RingAtomID}}, $BondedAtom;
1801     }
1802   }
1803 
1804   return \%ExocyclicDoubleBondsDataMap;
1805 }
1806 
1807 # Check to see whether ring atoms are allowed to participate in exocyclic terminal double
1808 # bonds...
1809 #
1810 sub _CheckPotentialAromaticRingAtomForExocylicDoubleBonds {
1811   my($This, $RingAtom, $AromaticityModelDataRef, $ExocyclicDoubleBondsDataMapRef) = @_;
1812   my($RingAtomID, $ExocyclicTerminalAtom, $RingAtomElectronegativity, $TerminalAtomElectronagativity);
1813 
1814   $RingAtomID = $RingAtom->GetID();
1815 
1816   # Is it part of an exocyclic terminal double bond?
1817   if (!exists $ExocyclicDoubleBondsDataMapRef->{RingAtomID}{$RingAtomID}) {
1818     return 1;
1819   }
1820 
1821   # Are exocyclic terminal double bonds allowed?
1822   if (!$AromaticityModelDataRef->{AllowExocyclicDoubleBonds}) {
1823     return 0;
1824   }
1825 
1826   # Are there multiple exocyclic double bonds?
1827   if (@{$ExocyclicDoubleBondsDataMapRef->{RingAtomID}{$RingAtomID}} > 1) {
1828     return 0;
1829   }
1830   ($ExocyclicTerminalAtom) = @{$ExocyclicDoubleBondsDataMapRef->{RingAtomID}{$RingAtomID}};
1831 
1832   # Are homo nuclear exocyclic terminal double bonds allowed?
1833   if (!$AromaticityModelDataRef->{AllowHomoNuclearExocyclicDoubleBonds}) {
1834     if ($RingAtom->GetAtomicNumber() == $ExocyclicTerminalAtom->GetAtomicNumber()) {
1835       return 0;
1836     }
1837   }
1838 
1839   # Are ring atoms with higher electronegativity allowed in exocyclic double bonds?
1840   if (!$AromaticityModelDataRef->{AllowElectronegativeRingAtomExocyclicDoubleBonds}) {
1841     $RingAtomElectronegativity = PeriodicTable::GetElementPaulingElectronegativity($RingAtom->GetAtomicNumber());
1842     $TerminalAtomElectronagativity = PeriodicTable::GetElementPaulingElectronegativity($ExocyclicTerminalAtom->GetAtomicNumber());
1843 
1844     if ($RingAtomElectronegativity && $TerminalAtomElectronagativity) {
1845       if ($RingAtomElectronegativity > $TerminalAtomElectronagativity) {
1846         return 0;
1847       }
1848     }
1849   }
1850 
1851   return 1;
1852 }
1853 
1854 #
1855 # Check for any formal charge participation into electron delocalization...
1856 #
1857 sub _CheckPotentialAromaticRingAtomForFormalCharge {
1858   my($This, $RingAtom, $AromaticityModelDataRef) = @_;
1859   my($FormalCharge);
1860 
1861   # Does atom has any formal charge?
1862   $FormalCharge = $RingAtom->GetFormalCharge();
1863   if (!$FormalCharge) {
1864     return 1;
1865   }
1866 
1867   # Are ring atoms with formal charge allowed to participate in electron delocalization?
1868   if (!$AromaticityModelDataRef->{AllowRingAtomFormalCharge}) {
1869     return 0;
1870   }
1871 
1872   # Are hetero ring atoms with formal charge allowed to participate in electron delocalization?
1873   if (!$RingAtom->IsCarbon()) {
1874     if (!$AromaticityModelDataRef->{AllowHeteroRingAtomFormalCharge}) {
1875       return 0;
1876     }
1877   }
1878 
1879   return 1;
1880 }
1881 
1882 #
1883 # Check ring atoms for allowed hetero atoms...
1884 #
1885 sub _CheckPotentialAromaticRingAtomForAllowedHeteroAtoms {
1886   my($This, $RingAtom, $AromaticityModelDataRef) = @_;
1887   my($RingAtomSymbol);
1888 
1889   # Is it a Carbon atom?
1890   if ($RingAtom->IsCarbon()) {
1891     return 1;
1892   }
1893 
1894   # Are heteroatoms allowed?
1895   if (!$AromaticityModelDataRef->{AllowHeteroRingAtoms}) {
1896     return 0;
1897   }
1898 
1899   # Is it an allowed hetero atom?
1900   $RingAtomSymbol = $RingAtom->GetAtomSymbol();
1901   if (!exists $AromaticityModelDataRef->{HeteroRingAtomsListMapRef}->{$RingAtomSymbol}) {
1902     return 0;
1903   }
1904 
1905   return 1;
1906 }
1907 
1908 # Check for any electron pair contributions toward pi electron delocalization due to
1909 # -ve formal charge on ring atoms which haven't been already processed and part of
1910 # conjugated single/double bond system...
1911 #
1912 sub _GetElectronPairsContributionFromConjugatedRingAtoms {
1913   my($This, $RingAtomsRef, $RingBondsRef, $ExcludeFreeRadicalElectrons, $AromaticityModelDataRef, $ElectronPairContributionProcessedMapRef) = @_;
1914   my($Index, $RingBond, $RingAtom, $NumOfRingAtomElectronPairs, $RingAtomID);
1915 
1916   # Is formal charge allowed on ring atoms?
1917   if (!$AromaticityModelDataRef->{AllowRingAtomFormalCharge}) {
1918     return 0;
1919   }
1920 
1921   $NumOfRingAtomElectronPairs = 0;
1922 
1923   # Process ring atoms...
1924   RINGBOND: for $Index (0 .. $#{$RingBondsRef}) {
1925     $RingBond = $RingBondsRef->[$Index];
1926     $RingAtom = $RingAtomsRef->[$Index];
1927     $RingAtomID = $RingAtom->GetID();
1928 
1929     # Is is already processed?
1930     if (exists $ElectronPairContributionProcessedMapRef->{$RingAtomID}) {
1931       next RINGBOND;
1932     }
1933     $ElectronPairContributionProcessedMapRef->{$RingAtomID} = $RingAtomID;
1934 
1935     # Is it allowed to have any formal charge?
1936     if (!$This->_CheckPotentialAromaticRingAtomForFormalCharge($RingAtom, $AromaticityModelDataRef)) {
1937       next RINGBOND;
1938     }
1939 
1940     # It it an allowed hetero ring atom or a carbon atom?
1941     if (!$This->_CheckPotentialAromaticRingAtomForAllowedHeteroAtoms($RingAtom, $AromaticityModelDataRef)) {
1942       next RINGBOND;
1943     }
1944 
1945     # It is an atom with -ve formal charge?
1946     if ($RingAtom->GetFormalCharge() >= 0) {
1947       next RINGBOND;
1948     }
1949 
1950     # Is it able to donate a pair for electrons towards pi electron delocalization?
1951     if ($RingAtom->GetValenceFreeElectrons($ExcludeFreeRadicalElectrons) < 2) {
1952       next RINGBOND;
1953     }
1954     $NumOfRingAtomElectronPairs += 1;
1955   }
1956 
1957   return $NumOfRingAtomElectronPairs;
1958 }
1959 
1960 # Check for ring atoms involved in two single ring bonds without any available electron
1961 # pair which are allowed to participate in aromatic system, after all other checks
1962 # corresponding to specified aromaticity models have already been performed...
1963 #
1964 sub _AllowRingAtomInTwoSingleBondsWithoutElectronPair {
1965   my($This, $RingAtom, $RingBond, $PreviousRingBond, $ExocyclicDoubleBondsDataMapRef, $FusedRingBondsMapRef) = @_;
1966 
1967   ALLOWRINGATOM: {
1968     if (exists $ExocyclicDoubleBondsDataMapRef->{RingAtomID}{$RingAtom->GetID()}) {
1969       # Ring atom in an exocylic terminal double bond without any available electron pair...
1970       last ALLOWRINGATOM;
1971     }
1972 
1973     if ($RingAtom->GetFormalCharge() > 0) {
1974       # Ring atom with positive formal charge without any available electron pair...
1975       last ALLOWRINGATOM;
1976     }
1977 
1978     if (defined $FusedRingBondsMapRef && (exists $FusedRingBondsMapRef->{$RingBond->GetID()} || exists $FusedRingBondsMapRef->{$PreviousRingBond->GetID()})) {
1979       # Ring atom involved in fused ring bond, which might end up being part of a conjugated
1980       # system in another fused ring...
1981       last ALLOWRINGATOM;
1982     }
1983 
1984     # Ring atom in any other environment is not allowed...
1985     return 0;
1986   }
1987 
1988   return 1;
1989 }
1990 
1991 # Do pi electrons satify huckel's rule: Number of pi electrons correspond to 4n + 2 where
1992 # n is a positive integer...
1993 #
1994 sub _DoPiElectronSatifyHuckelsRule {
1995   my($This, $NumOfPiElectrons) = @_;
1996 
1997   $NumOfPiElectrons = $NumOfPiElectrons - 2;
1998 
1999   return ($NumOfPiElectrons > 0) ? (($NumOfPiElectrons % 4) ? 0 : 1) : 0;
2000 }
2001 
2002 # Delete aromatic property for all atoms and bonds...
2003 #
2004 sub _DeleteAtomsAndBondsAromaticity {
2005   my($This) = @_;
2006   my($Atom, $Bond);
2007 
2008   for $Atom ($This->GetAtoms()) {
2009     $Atom->DeleteAromatic();
2010   }
2011   for $Bond ($This->GetBonds()) {
2012     $Bond->DeleteAromatic();
2013   }
2014   return $This;
2015 }
2016 
2017 # Kekulize marked ring and non-ring aromatic atoms in a molecule...
2018 #
2019 sub KekulizeAromaticAtoms {
2020   my($This) = @_;
2021 
2022   if (!$This->_KekulizeAromaticAtomsInRings()) {
2023     return 0;
2024   }
2025 
2026   if (!$This->_KekulizeAromaticAtomsNotInRings()) {
2027     return 0;
2028   }
2029 
2030   return 1;
2031 }
2032 
2033 # Kekulize marked aromatic atoms in rings and fused ring sets...
2034 #
2035 sub _KekulizeAromaticAtomsInRings {
2036   my($This) = @_;
2037 
2038   if (!$This->HasRings()) {
2039     # Nothing to do...
2040     return 1;
2041   }
2042 
2043   if (!$This->HasAromaticAtomsInRings()) {
2044     # Nothing to do...
2045     return 1;
2046   }
2047 
2048   # Identify fully aromatic fused and individual rings along with any partially aromatic ring components
2049   # using marked aromatic atoms in a molecule and kekulize them as individual stes...
2050   #
2051   my($AromaticFusedRingSetsRef, $AromaticRingsRef, $PartiallyAromaticRingComponentsRef) = (undef) x 3;
2052   if ($This->HasFusedRings()) {
2053     ($AromaticFusedRingSetsRef, $AromaticRingsRef, $PartiallyAromaticRingComponentsRef) = $This->_GetFusedAndNonFusedRingsContainingAromaticAtoms();
2054   }
2055   else {
2056     ($AromaticRingsRef, $PartiallyAromaticRingComponentsRef) = $This->_GetIndividualRingsContainingAromaticAtoms();
2057   }
2058 
2059   return $This->_KekulizeCompleteAndPartialAromaticRings($AromaticFusedRingSetsRef, $AromaticRingsRef, $PartiallyAromaticRingComponentsRef);
2060 }
2061 
2062 # Identify fully aromatic fused and individual rings along with any partially aromatic ring components
2063 # using marked aromatic atoms in a molecule...
2064 #
2065 sub _GetFusedAndNonFusedRingsContainingAromaticAtoms {
2066   my($This)  = @_;
2067   my($Index, $SetAtomsCount, $SetAromaticAtomsCount, $FusedRingSetRef, $FusedRingSetsRef, $NonFusedRingsRef, $IndividualRingsRef, $RingAtomsRef, $RingAtomsCount, $AromaticAtomsCount, $RingAtom, $NonFusedFullyAromaticRingsRef, $NonFusedPartiallyAromaticRingComponentsRef, $PartiallyAromaticRingComponentsRef, @FullyAromaticFusedRingSets, @PotentialFullyAromaticRings, @FullyAromaticRings, @PotentialPartiallyAromaticRings, @PartiallyAromaticRingComponents);
2068 
2069   @FullyAromaticFusedRingSets = ();
2070 
2071   @PotentialFullyAromaticRings = ();
2072   @FullyAromaticRings = ();
2073 
2074   @PotentialPartiallyAromaticRings = ();
2075   @PartiallyAromaticRingComponents = ();
2076 
2077   ($FusedRingSetsRef, $NonFusedRingsRef) = $This->GetFusedAndNonFusedRings();
2078 
2079   # Go over fused ring sets...
2080   RINGSET: for $Index (0 .. $#{$FusedRingSetsRef}) {
2081     $FusedRingSetRef = $FusedRingSetsRef->[$Index];
2082 
2083     $SetAtomsCount = 0;
2084     $SetAromaticAtomsCount = 0;
2085 
2086     for $RingAtomsRef (@{$FusedRingSetRef}) {
2087       $SetAtomsCount += scalar @{$RingAtomsRef};
2088 
2089       for $RingAtom (@{$RingAtomsRef}) {
2090         if ($RingAtom->IsAromatic()) {
2091           $SetAromaticAtomsCount += 1;
2092         }
2093       }
2094     }
2095 
2096     if (!($SetAtomsCount && $SetAromaticAtomsCount)) {
2097       next RINGSET;
2098     }
2099 
2100     if ($SetAromaticAtomsCount == $SetAtomsCount) {
2101       push @FullyAromaticFusedRingSets, $FusedRingSetRef;
2102     }
2103     else {
2104       # Identify any individual rings in partial aromatic fused ring sets which might be
2105       # fully or partially aromatic...
2106       #
2107       RING: for $RingAtomsRef (@{$FusedRingSetRef}) {
2108         $RingAtomsCount = scalar @{$RingAtomsRef};
2109         $AromaticAtomsCount = 0;
2110 
2111         RINGATOM: for $RingAtom (@{$RingAtomsRef}) {
2112           if (!$RingAtom->IsAromatic()) {
2113             next RINGATOM;
2114           }
2115           $AromaticAtomsCount += 1;
2116         }
2117 
2118         if (!($RingAtomsCount && $AromaticAtomsCount)) {
2119           next RING;
2120         }
2121 
2122         if ($RingAtomsCount == $AromaticAtomsCount) {
2123           push @PotentialFullyAromaticRings, $RingAtomsRef;
2124         }
2125         else {
2126           #  Track partially aromatic rings in an different list before removing them for
2127           #  any overlap with other rings and then add to fully aromatic rings...
2128           push @PotentialPartiallyAromaticRings, $RingAtomsRef;
2129         }
2130       }
2131     }
2132   }
2133 
2134   if (@PotentialFullyAromaticRings > 1) {
2135     # Get any fully aromatic fused ring subsets from potentially fully aromatic rings...
2136     my($FullyAromaticFusedRingSetsRefs, $FullyAromaticNonFusedRingsRef);
2137     ($FullyAromaticFusedRingSetsRefs, $FullyAromaticNonFusedRingsRef) = $This->_GetFullyAromaticFusedAndNonFusedRingsInFusedSubset(\@PotentialFullyAromaticRings);
2138 
2139     if (@{$FullyAromaticFusedRingSetsRefs}) {
2140       push @FullyAromaticFusedRingSets, @{$FullyAromaticFusedRingSetsRefs};
2141     }
2142     if (@{$FullyAromaticNonFusedRingsRef}) {
2143       push @FullyAromaticRings, @{$FullyAromaticNonFusedRingsRef};
2144     }
2145   }
2146   else {
2147     push @FullyAromaticRings, @PotentialFullyAromaticRings;
2148   }
2149 
2150   # Go over partial aromatic ring components...
2151   if (@PotentialPartiallyAromaticRings) {
2152     $PartiallyAromaticRingComponentsRef = $This->_GetPartiallyAromaticRingComponents(\@PotentialPartiallyAromaticRings, \@PotentialFullyAromaticRings);
2153     if (@{$PartiallyAromaticRingComponentsRef}) {
2154       push @PartiallyAromaticRingComponents, @{$PartiallyAromaticRingComponentsRef};
2155     }
2156   }
2157 
2158   # Go over non-fused rings...
2159   if (@{$NonFusedRingsRef}) {
2160     ($NonFusedFullyAromaticRingsRef, $NonFusedPartiallyAromaticRingComponentsRef) = $This->_GetRingsContainingAromaticAtoms(@{$NonFusedRingsRef});
2161 
2162     if (@{$NonFusedFullyAromaticRingsRef}) {
2163       push @FullyAromaticRings, @{$NonFusedFullyAromaticRingsRef};
2164     }
2165     if (@{$NonFusedPartiallyAromaticRingComponentsRef}) {
2166       push @PartiallyAromaticRingComponents, @{$NonFusedPartiallyAromaticRingComponentsRef};
2167     }
2168   }
2169 
2170   return (\@FullyAromaticFusedRingSets, \@FullyAromaticRings, \@PartiallyAromaticRingComponents);
2171 }
2172 
2173 # Identify fully aromatic fused sets and non-fused rings in potentially fully aromatic
2174 # rings in fused ring sets...
2175 #
2176 # Fully aromatic rings in fused ring sets might contain fully aromatic fused subsets. These
2177 # fused subets need to be tracked and treated as fused sets.
2178 #
2179 # Note:
2180 #   . Fused ring sets share at least one common bond, which could be used to identify
2181 #     any multiple fully aromatic fused rings sets; absence of a shared ring bond implies
2182 #     there are no fused ring sets.
2183 #
2184 #
2185 sub _GetFullyAromaticFusedAndNonFusedRingsInFusedSubset {
2186   my($This, $PotentialFullyAromaticFusedRingsRef) = @_;
2187   my($RingIndex, $RingIndex1, $RingIndex2, $RingAtom, $RingAtomID, $RingIsFuesd, $RingIndicesGraph, $FusedRingSetIndicesRef, @RingIndices, @FusedRingPairIndices, @FusedRingSetIndicesRefs, @FullyAromaticFusedRingSets, @FullyAromaticRings, %RingIndexToAtomIDMap, %FullyAromaticFusedRingIndexMap);
2188 
2189   @FullyAromaticFusedRingSets = ();
2190   @FullyAromaticRings = ();
2191 
2192   # Setup a ring index map for ring atoms...
2193   #
2194   %RingIndexToAtomIDMap = ();
2195   for $RingIndex (0 .. $#{$PotentialFullyAromaticFusedRingsRef}) {
2196     %{$RingIndexToAtomIDMap{$RingIndex}} = ();
2197     for $RingAtom (@{$PotentialFullyAromaticFusedRingsRef->[$RingIndex]}) {
2198       $RingAtomID = $RingAtom->GetID();
2199       $RingIndexToAtomIDMap{$RingIndex}{$RingAtomID} = $RingAtomID;
2200     }
2201   }
2202 
2203   # Identify fused ring pairs...
2204   #
2205   @RingIndices = ();
2206   @FusedRingPairIndices = ();
2207 
2208   for $RingIndex1 (0 .. $#{$PotentialFullyAromaticFusedRingsRef}) {
2209     push @RingIndices, $RingIndex1;
2210     for $RingIndex2 (($RingIndex1 + 1) .. $#{$PotentialFullyAromaticFusedRingsRef}) {
2211       $RingIsFuesd = 0;
2212       RINGATOM: for $RingAtom (@{$PotentialFullyAromaticFusedRingsRef->[$RingIndex2]}) {
2213         $RingAtomID = $RingAtom->GetID();
2214         if (exists $RingIndexToAtomIDMap{$RingIndex1}{$RingAtomID}) {
2215           $RingIsFuesd = 1;
2216           last RINGATOM;
2217         }
2218       }
2219       if ($RingIsFuesd) {
2220         push @FusedRingPairIndices, ($RingIndex1, $RingIndex2);
2221       }
2222     }
2223   }
2224 
2225   if (!@FusedRingPairIndices) {
2226     # No fused ring subset out there...
2227     push @FullyAromaticRings, @{$PotentialFullyAromaticFusedRingsRef};
2228 
2229     return (\@FullyAromaticFusedRingSets, \@FullyAromaticRings);
2230   }
2231 
2232   # Identify fused ring sets...
2233   #
2234   $RingIndicesGraph = new Graph(@RingIndices);
2235   $RingIndicesGraph->AddEdges(@FusedRingPairIndices);
2236   @FusedRingSetIndicesRefs = $RingIndicesGraph->GetConnectedComponentsVertices();
2237 
2238   # Collect fully aromatic fused ring sets...
2239   #
2240   %FullyAromaticFusedRingIndexMap = ();
2241   for $FusedRingSetIndicesRef (@FusedRingSetIndicesRefs) {
2242     my(@FullyAromaticFusedRingSet) = ();
2243     for $RingIndex (@{$FusedRingSetIndicesRef}) {
2244       $FullyAromaticFusedRingIndexMap{$RingIndex} = $RingIndex;
2245       push @FullyAromaticFusedRingSet, $PotentialFullyAromaticFusedRingsRef->[$RingIndex];
2246     }
2247     if (@FullyAromaticFusedRingSet) {
2248       # Sort rings by size with in the fused ring set...
2249       @FullyAromaticFusedRingSet = sort { scalar @$a <=> scalar @$b } @FullyAromaticFusedRingSet;
2250       push @FullyAromaticFusedRingSets, \@FullyAromaticFusedRingSet;
2251     }
2252   }
2253 
2254   # Collect fully aromatic non-fused rings...
2255   #
2256   RINGINDEX: for $RingIndex (0 .. $#{$PotentialFullyAromaticFusedRingsRef}) {
2257     if (exists $FullyAromaticFusedRingIndexMap{$RingIndex}) {
2258       next RINGINDEX;
2259     }
2260     push @FullyAromaticRings, $PotentialFullyAromaticFusedRingsRef->[$RingIndex];
2261   }
2262 
2263   return (\@FullyAromaticFusedRingSets, \@FullyAromaticRings);
2264 }
2265 
2266 # Identify individual non-fused rings containing aromatic atoms...
2267 #
2268 sub _GetIndividualRingsContainingAromaticAtoms {
2269   my($This)  = @_;
2270 
2271   return $This->_GetRingsContainingAromaticAtoms($This->GetRings());
2272 }
2273 
2274 # Identify individual non-fused rings containing aromatic atoms...
2275 #
2276 sub _GetRingsContainingAromaticAtoms {
2277   my($This, @Rings)  = @_;
2278   my($RingAtom, $RingAtomsRef, $RingAtomsCount, $AromaticAtomsCount, $PartiallyAromaticRingComponentsRef, @FullyAromaticRings, @PartiallyAromaticRings);
2279 
2280   @FullyAromaticRings = ();
2281   @PartiallyAromaticRings = ();
2282 
2283   RING: for $RingAtomsRef (@Rings) {
2284     $RingAtomsCount = scalar @{$RingAtomsRef};
2285     $AromaticAtomsCount = 0;
2286 
2287     for $RingAtom (@{$RingAtomsRef}) {
2288       if ($RingAtom->IsAromatic()) {
2289         $AromaticAtomsCount += 1;
2290       }
2291     }
2292 
2293     if (!($AromaticAtomsCount && $RingAtomsCount)) {
2294       next RING;
2295     }
2296 
2297     if ($AromaticAtomsCount == $RingAtomsCount) {
2298       push @FullyAromaticRings, $RingAtomsRef;
2299     }
2300     else {
2301       push @PartiallyAromaticRings, $RingAtomsRef;
2302     }
2303   }
2304 
2305   $PartiallyAromaticRingComponentsRef = $This->_GetPartiallyAromaticRingComponents(\@PartiallyAromaticRings);
2306 
2307   return (\@FullyAromaticRings, $PartiallyAromaticRingComponentsRef);
2308 }
2309 
2310 # Get connected aromatic components with in partially aromatic rings...
2311 #
2312 sub _GetPartiallyAromaticRingComponents {
2313   my($This, $PotentialPartiallyAromaticRingsRef, $FullyAromaticRingsRef) = @_;
2314   my($RingAtomsRef, $RingAtom, $RingAtomID, $Index, @PartiallyAromaticRingComponents, %FullyAromaticRingAtomsMap);
2315 
2316   @PartiallyAromaticRingComponents = ();
2317 
2318   # Setup a map for atoms involve in fully aromatic rings to remove remove partial rings
2319   # containing only those atoms which are already part of some other fully aromatic ring
2320   # in fused ring scenarios or some other partially aromatic ring...
2321   #
2322   %FullyAromaticRingAtomsMap = ();
2323   if (defined $FullyAromaticRingsRef) {
2324     for $RingAtomsRef (@{$FullyAromaticRingsRef}) {
2325       for $RingAtom (@{$RingAtomsRef}) {
2326         $RingAtomID = $RingAtom->GetID();
2327         $FullyAromaticRingAtomsMap{$RingAtomID} = $RingAtomID;
2328       }
2329     }
2330   }
2331 
2332   # . Identify any connected components with in each partially aromatic ring.
2333   # . Use ring atom indices to figure out connnected components in rings: All ring atoms
2334   #   in a connected component have sequential indices and a difference by more than
2335   #   1 indicates a new component in the list.
2336   #
2337   RING: for $RingAtomsRef (@{$PotentialPartiallyAromaticRingsRef}) {
2338     my(@AromaticRingAtoms, @AromaticRingAtomsIndices);
2339 
2340     @AromaticRingAtoms = ();
2341     @AromaticRingAtomsIndices = ();
2342 
2343     RINGATOM: for $Index (0 .. $#{$RingAtomsRef}) {
2344       $RingAtom = $RingAtomsRef->[$Index];
2345       $RingAtomID = $RingAtom->GetID();
2346 
2347       if (defined $FullyAromaticRingsRef && exists $FullyAromaticRingAtomsMap{$RingAtomID}) {
2348         next RINGATOM;
2349       }
2350       if (!$RingAtom->IsAromatic()) {
2351         next RINGATOM;
2352       }
2353       push @AromaticRingAtoms, $RingAtom;
2354       push @AromaticRingAtomsIndices, $Index;
2355 
2356     }
2357     if (!@AromaticRingAtoms) {
2358       next RING;
2359     }
2360 
2361     # Start off with a new connected component...
2362     #
2363     my($ComponentNum);
2364     $ComponentNum = scalar @PartiallyAromaticRingComponents;
2365     @{$PartiallyAromaticRingComponents[$ComponentNum]} = ();
2366 
2367     $Index = 0;
2368     push @{$PartiallyAromaticRingComponents[$ComponentNum]}, $AromaticRingAtoms[$Index];
2369 
2370     for $Index (1 .. $#AromaticRingAtoms) {
2371       if (($AromaticRingAtomsIndices[$Index] - $AromaticRingAtomsIndices[$Index -1]) > 1) {
2372         # New connected component...
2373         $ComponentNum += 1;
2374         @{$PartiallyAromaticRingComponents[$ComponentNum]} = ();
2375       }
2376       push @{$PartiallyAromaticRingComponents[$ComponentNum]}, $AromaticRingAtoms[$Index];
2377     }
2378   }
2379 
2380   return (\@PartiallyAromaticRingComponents);
2381 }
2382 
2383 # Kekulize fully aromatic fused and individual rings along with any partially aromatic ring
2384 # components...
2385 #
2386 sub _KekulizeCompleteAndPartialAromaticRings {
2387   my($This, $AromaticFusedRingSetsRef, $AromaticRingsRef, $PartiallyAromaticRingComponentsRef) = @_;
2388   my($Status, $ConnectedPathsAtomsSetsRef, $ConnectedPathsBondsSetsRef, $ConnectdPathsSetsTypesRef, $PathSetIndex, $PathAtom, $AtomID, $BondID, $PathBondsRef, $DeleteAtomsAromaticity, $DeleteBondsAromaticity, %PathAtomsProcessingStatusMap, %PathBondsProcessingStatusMap);
2389 
2390   ($ConnectedPathsAtomsSetsRef, $ConnectedPathsBondsSetsRef, $ConnectdPathsSetsTypesRef) = $This->_SetupCompleteAndPartialAromaticRingsForKekulizaiton($AromaticFusedRingSetsRef, $AromaticRingsRef, $PartiallyAromaticRingComponentsRef);
2391 
2392   if (!@{$ConnectedPathsAtomsSetsRef}) {
2393     # Nothing to do...
2394     return 1;
2395   }
2396 
2397   # Delete any aromaticity property set for non-ring bonds connected any two ring
2398   # aromatic atoms...
2399   #
2400   $This->_ProcessNonRingAromaticBondsBetweenAromaticRingAtoms();
2401 
2402   %PathAtomsProcessingStatusMap = ();
2403   %PathBondsProcessingStatusMap = ();
2404 
2405   $Status = 1;
2406 
2407   PATHSET: for $PathSetIndex (0 .. $#{$ConnectedPathsAtomsSetsRef}) {
2408     my($AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $PathSetProcessingStatusRef);
2409 
2410     ($AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $PathSetProcessingStatusRef) = (undef) x 3;
2411 
2412     if ($ConnectdPathsSetsTypesRef->[$PathSetIndex] =~ /^FusedAromatic$/i) {
2413       # Fused set of connected paths...
2414       #
2415       my($FusedConnectedPathAtomsSetRef, $FusedConnectedPathBondsSetRef);
2416 
2417       $FusedConnectedPathAtomsSetRef = $ConnectedPathsAtomsSetsRef->[$PathSetIndex];
2418       $FusedConnectedPathBondsSetRef = $ConnectedPathsBondsSetsRef->[$PathSetIndex];
2419 
2420       # Prepare for kekulization...
2421       ($AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $PathSetProcessingStatusRef) = $This->_SetupConnectedPathSetsForKekulization($FusedConnectedPathAtomsSetRef, $FusedConnectedPathBondsSetRef);
2422 
2423       # Perform kekulization starting with the first path set...
2424       $PathSetProcessingStatusRef->[0] = 'Processed';
2425       if (!$This->_KekulizeConnectedPathSets($FusedConnectedPathAtomsSetRef->[0],  $FusedConnectedPathBondsSetRef->[0], $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $FusedConnectedPathAtomsSetRef, $FusedConnectedPathBondsSetRef, $PathSetProcessingStatusRef)) {
2426         # Kekulization failed for the current fused paths set...
2427         $Status = 0;
2428       }
2429     }
2430     else {
2431       # An individual connected path...
2432       #
2433       my(@ConnectedPathAtomsSet, @ConnectedPathBondsSet);
2434 
2435       @ConnectedPathAtomsSet = ($ConnectedPathsAtomsSetsRef->[$PathSetIndex]);
2436       @ConnectedPathBondsSet = ($ConnectedPathsBondsSetsRef->[$PathSetIndex]);
2437 
2438       # Prepare for kekulization...
2439       ($AtomProcessingStatusMapRef, $BondProcessingStatusMapRef) = $This->_SetupConnectedPathSetsForKekulization(\@ConnectedPathAtomsSet, \@ConnectedPathBondsSet);
2440 
2441       # Perform kekulization...
2442       if (!$This->_KekulizeConnectedPathSets($ConnectedPathsAtomsSetsRef->[$PathSetIndex], $ConnectedPathsBondsSetsRef->[$PathSetIndex], $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef)) {
2443         # Kekulization failed for the current path...
2444         $Status = 0;
2445       }
2446     }
2447 
2448     # Did kekulization succeed for the current path or path set?
2449     if (!$Status) {
2450       last PATHSET;
2451     }
2452 
2453     # Track atom and bond processing state for final assignment after kekulization
2454     # is successfully completed for all the paths and fused path sets...
2455     #
2456     for $AtomID (keys %{$AtomProcessingStatusMapRef}) {
2457       $PathAtomsProcessingStatusMap{$AtomID} = $AtomProcessingStatusMapRef->{$AtomID};
2458     }
2459 
2460     for $BondID (keys %{$BondProcessingStatusMapRef}) {
2461       $PathBondsProcessingStatusMap{$BondID} = $BondProcessingStatusMapRef->{$BondID};
2462     }
2463   }
2464 
2465   if (!$Status) {
2466     carp "Warning: ${ClassName}->_KekulizeCompleteAndPartialAromaticRings: Couldn't perform kekulization for marked ring aromatic atoms...";
2467     return 0;
2468   }
2469 
2470   # Use PathAtomsProcessingStatusMap and PathBondsProcessingStatusMap to set
2471   # single/double bonds in the molecule after successful kekulization along with modification of
2472   # any aromatic flags...
2473 
2474   for $PathSetIndex (0 .. $#{$ConnectedPathsAtomsSetsRef}) {
2475     $DeleteAtomsAromaticity = 0; $DeleteBondsAromaticity = 0;
2476 
2477     if ($ConnectdPathsSetsTypesRef->[$PathSetIndex] =~ /^FusedAromatic$/i) {
2478       for $PathBondsRef (@{$ConnectedPathsBondsSetsRef->[$PathSetIndex]}) {
2479         $This->_ProcessBondOrdersAssignedDuringSuccessfulKekulization($PathBondsRef, \%PathBondsProcessingStatusMap, $DeleteBondsAromaticity);
2480       }
2481     }
2482     else {
2483       if ($ConnectdPathsSetsTypesRef->[$PathSetIndex] =~ /^PartiallyAromatic$/i ) {
2484         $DeleteBondsAromaticity = 1; $DeleteAtomsAromaticity = 1;
2485       }
2486 
2487       if ($DeleteAtomsAromaticity) {
2488         for $PathAtom (@{$ConnectedPathsAtomsSetsRef->[$PathSetIndex]}) {
2489           $PathAtom->DeleteAromatic();
2490         }
2491       }
2492 
2493       $This->_ProcessBondOrdersAssignedDuringSuccessfulKekulization($ConnectedPathsBondsSetsRef->[$PathSetIndex], \%PathBondsProcessingStatusMap, $DeleteBondsAromaticity);
2494     }
2495   }
2496 
2497   return 1;
2498 }
2499 
2500 # Look for any aromatic bonds outside the rings between two ring aromatic atoms
2501 # and turn them into single non-aromatic bonds before kekulization; otherwise, kekulization
2502 # fails.
2503 #
2504 # Note:
2505 #   . Two atoms marked as aromatic atoms in two different rings, such as two rings
2506 #     connected through a single bond, are still aromatic, but the bond is outside
2507 #     the ring and shouldn't be marked as aromatic. It should be set to single bond without
2508 #     any aromatic property for kekulization to succeed.
2509 #
2510 #     For example, the molecule  generated by SMILES parser for biphenyl SMILES string
2511 #     "c1ccccc1c2ccccc2" sets up an aromatic bond between the two phenyl rings, as
2512 #     it's connected to two aromatic atoms.
2513 #
2514 sub _ProcessNonRingAromaticBondsBetweenAromaticRingAtoms {
2515   my($This) = @_;
2516   my($Bond, $Atom1, $Atom2);
2517 
2518   BOND: for $Bond ($This->GetBonds()) {
2519     if (!($Bond->IsAromatic() && $Bond->IsNotInRing())) {
2520       next BOND;
2521     }
2522 
2523     ($Atom1, $Atom2) = $Bond->GetAtoms();
2524     if (!($Atom1->IsAromatic() && $Atom2->IsAromatic() && $Atom1->IsInRing() && $Atom2->IsInRing())) {
2525       next BOND;
2526     }
2527 
2528     $Bond->SetBondOrder(1);
2529     $Bond->DeleteAromatic();
2530   }
2531 
2532   return $This;
2533 }
2534 
2535 # Setup completelty aromatic fused and individual rings along with partially aromatic ring
2536 # components as sets of connected paths...
2537 #
2538 sub _SetupCompleteAndPartialAromaticRingsForKekulizaiton {
2539   my($This, $AromaticFusedRingSetsRef, $AromaticRingsRef, $PartiallyAromaticRingComponentsRef) = @_;
2540   my(@ConnectedPathsSets, @ConnectedPathsBondsSets, @ConnectdPathsSetsTypes);
2541 
2542   @ConnectedPathsSets = ();
2543   @ConnectedPathsBondsSets = ();
2544   @ConnectdPathsSetsTypes = ();
2545 
2546   # Setup atoms and bonds for connected paths in fused aromatic ring sets...
2547   #
2548   if (defined $AromaticFusedRingSetsRef && @{$AromaticFusedRingSetsRef}) {
2549     my($RingSetIndex);
2550 
2551     push @ConnectdPathsSetsTypes, ('FusedAromatic') x scalar @{$AromaticFusedRingSetsRef};
2552     push @ConnectedPathsSets, @{$AromaticFusedRingSetsRef};
2553 
2554     for $RingSetIndex (0 .. $#{$AromaticFusedRingSetsRef}) {
2555       my(@AromaticFusedRingBondsSet);
2556 
2557       # Get ring bonds for each ring set...
2558       #
2559       @AromaticFusedRingBondsSet = $This->GetRingBondsFromRings(@{$AromaticFusedRingSetsRef->[$RingSetIndex]});
2560       push @ConnectedPathsBondsSets, \@AromaticFusedRingBondsSet;
2561     }
2562   }
2563 
2564   # Set up atoms and bonds for connected paths in aromatic rings...
2565   #
2566   if (defined $AromaticRingsRef && @{$AromaticRingsRef}) {
2567     my(@AromaticRingBondsSets);
2568 
2569     push @ConnectdPathsSetsTypes, ('Aromatic') x scalar @{$AromaticRingsRef};
2570     push @ConnectedPathsSets, @{$AromaticRingsRef};
2571 
2572     # Get ring bonds for each ring...
2573     @AromaticRingBondsSets = $This->GetRingBondsFromRings(@{$AromaticRingsRef});
2574     push @ConnectedPathsBondsSets, @AromaticRingBondsSets;
2575   }
2576 
2577   # Set up atoms and bonds for connected paths in partially aromatic rings...
2578   #
2579   if (defined $PartiallyAromaticRingComponentsRef && @{$PartiallyAromaticRingComponentsRef}) {
2580     my($ComponentIndex);
2581 
2582     push @ConnectedPathsSets, @{$PartiallyAromaticRingComponentsRef};
2583     push @ConnectdPathsSetsTypes, ('PartiallyAromatic') x scalar @{$PartiallyAromaticRingComponentsRef};
2584 
2585     for $ComponentIndex (0 .. $#{$PartiallyAromaticRingComponentsRef}) {
2586       my(@ComponentBonds);
2587       @ComponentBonds = $This->_GetPathBonds($This->_GetAtomsIDsFromAtoms(@{$PartiallyAromaticRingComponentsRef->[$ComponentIndex]}));
2588       push @ConnectedPathsBondsSets, \@ComponentBonds;
2589     }
2590   }
2591 
2592   return (\@ConnectedPathsSets, \@ConnectedPathsBondsSets, \@ConnectdPathsSetsTypes);
2593 }
2594 
2595 # Process non-ring connected atoms which are marked aromatic and set connected
2596 # bonds as alternate single/double bonds...
2597 #
2598 # Notes:
2599 #   . Atom and bond aromaticity is deleted during kekulization of non-ring atoms.
2600 #
2601 sub _KekulizeAromaticAtomsNotInRings {
2602   my($This) = @_;
2603   my($Status, $PathIndex, $PathAtom, $PathAtomID, $PathBondID, $ConnectedPathsAtomsRef, $ConnectedPathsBondsRef, $DeleteAtomsAromaticity, $DeleteBondsAromaticity, %PathAtomsProcessingStatusMap, %PathBondsProcessingStatusMap);
2604 
2605   if (!$This->HasAromaticAtomsNotInRings()) {
2606     # Nothing to do...
2607     return 1;
2608   }
2609 
2610   # Identify paths for connected components containing non-ring aromatic atoms...
2611   ($ConnectedPathsAtomsRef, $ConnectedPathsBondsRef) = $This->_GetConnectedComponentsPathsForNonRingAromaticAtoms();
2612 
2613   if (!@{$ConnectedPathsAtomsRef}) {
2614     carp "Warning: ${ClassName}->_KekulizeAromaticAtomsNotInRings: Couldn't perform kekulization for marked non-ring aromatic atoms...";
2615     return 0;
2616   }
2617 
2618   %PathAtomsProcessingStatusMap = ();
2619   %PathBondsProcessingStatusMap = ();
2620 
2621   $Status = 1;
2622 
2623   PATH: for $PathIndex (0 .. $#{$ConnectedPathsAtomsRef}) {
2624     my($AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, @ConnectedPathAtomsSet, @ConnectedPathBondsSet);
2625 
2626     ($AtomProcessingStatusMapRef, $BondProcessingStatusMapRef) = (undef) x 2;
2627 
2628     @ConnectedPathAtomsSet = ($ConnectedPathsAtomsRef->[$PathIndex]);
2629     @ConnectedPathBondsSet = ($ConnectedPathsBondsRef->[$PathIndex]);
2630 
2631     # Prepare for kekulization...
2632     ($AtomProcessingStatusMapRef, $BondProcessingStatusMapRef) = $This->_SetupConnectedPathSetsForKekulization(\@ConnectedPathAtomsSet, \@ConnectedPathBondsSet);
2633 
2634     # Perform kekulization...
2635     if (!$This->_KekulizeConnectedPathSets($ConnectedPathsAtomsRef->[$PathIndex], $ConnectedPathsBondsRef->[$PathIndex], $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef)) {
2636       # Kekulization failed for the current path...
2637       $Status = 0;
2638       last PATH;
2639     }
2640 
2641     # Track atom and bond processing state for final assignment after kekulization
2642     # is successfully completed for all the paths and fused path sets...
2643     #
2644     for $PathAtomID (keys %{$AtomProcessingStatusMapRef}) {
2645       $PathAtomsProcessingStatusMap{$PathAtomID} = $AtomProcessingStatusMapRef->{$PathAtomID};
2646     }
2647 
2648     for $PathBondID (keys %{$BondProcessingStatusMapRef}) {
2649       $PathBondsProcessingStatusMap{$PathBondID} = $BondProcessingStatusMapRef->{$PathBondID};
2650     }
2651   }
2652 
2653   if (!$Status) {
2654     carp "Warning: ${ClassName}->_KekulizeAromaticAtomsNotInRings: Couldn't perform kekulization for marked non-ring aromatic atoms...";
2655     return 0;
2656   }
2657 
2658   $DeleteAtomsAromaticity = 1; $DeleteBondsAromaticity = 1;
2659   for $PathIndex (0 .. $#{$ConnectedPathsAtomsRef}) {
2660     if ($DeleteAtomsAromaticity) {
2661       for $PathAtom (@{$ConnectedPathsAtomsRef->[$PathIndex]}) {
2662         $PathAtom->DeleteAromatic();
2663       }
2664     }
2665     $This->_ProcessBondOrdersAssignedDuringSuccessfulKekulization($ConnectedPathsBondsRef->[$PathIndex], \%PathBondsProcessingStatusMap, $DeleteBondsAromaticity);
2666   }
2667 
2668   return 1;
2669 }
2670 
2671 # Collect path atoms for connected components paths containing non-ring aromatic atoms...
2672 #
2673 sub _GetConnectedComponentsPathsForNonRingAromaticAtoms {
2674   my($This) = @_;
2675   my($ComponentRef, $AtomIDsRef, $AtomIDsMapRef, $ConnectedComponentsAtomIDsRef, $ConnectedComponentsAtomIDsMapRef, $ConnectedComponentsPathsAtomIDsRef, $ConnectedComponentsPathsAtomsRef, $ConnectedComponentsPathsBondsRef);
2676 
2677   # Retrieve information for marked aromatic atoms not in the rings...
2678   ($AtomIDsRef, $AtomIDsMapRef) = $This->_GetNonRingAromaticAtomIDs();
2679 
2680   # Identify connected components containing marked aromatic atoms not in the rings...
2681   ($ConnectedComponentsAtomIDsRef, $ConnectedComponentsAtomIDsMapRef) = $This->_GetConnectedComponentsForNonRingAromaticAtoms($AtomIDsRef);
2682 
2683   # Identify paths for connected components containing non-ring aromatic atoms...
2684   ($ConnectedComponentsPathsAtomsRef, $ConnectedComponentsPathsBondsRef) = $This->_GetConnectedComponentsPathsAtomsAndBondsForNonRingAromaticAtoms($AtomIDsMapRef, $ConnectedComponentsAtomIDsRef, $ConnectedComponentsAtomIDsMapRef);
2685 
2686   return ($ConnectedComponentsPathsAtomsRef, $ConnectedComponentsPathsBondsRef);
2687 }
2688 
2689 # Collect information for marked aromatic atoms not in the rings...
2690 #
2691 sub _GetNonRingAromaticAtomIDs {
2692   my($This) = @_;
2693   my($Atom, $AtomID, @AtomIDs, %AtomIDsMap);
2694 
2695   @AtomIDs = ();
2696   %AtomIDsMap = ();
2697 
2698   ATOM: for $Atom ($This->GetAtoms()) {
2699     if (!$Atom->IsAromatic()) {
2700       next ATOM;
2701     }
2702     if ($Atom->IsInRing()) {
2703       next ATOM;
2704     }
2705     $AtomID = $Atom->GetID();
2706 
2707     push @AtomIDs, $AtomID;
2708     $AtomIDsMap{$AtomID} = $Atom;
2709   }
2710 
2711   return (\@AtomIDs, \%AtomIDsMap);
2712 }
2713 
2714 # Retrieve connected non-ring atom components as a reference to an array of references
2715 # containing atom IDs of connecnted components...
2716 #
2717 sub _GetConnectedComponentsForNonRingAromaticAtoms {
2718   my($This, $AtomIDsRef) = @_;
2719   my($Index, $AtomID, $AtomIDsGraph, @BondedAtomPairIDs, @ComponentsAtomIDsRefs, @ComponentsAtomIDsMapRefs);
2720 
2721   @ComponentsAtomIDsRefs = ();
2722   @ComponentsAtomIDsMapRefs = ();
2723 
2724   # Get bonded atom pair IDs...
2725   @BondedAtomPairIDs = $This->_GetBondedAtomPairAtomIDsFromAtomIDs(@{$AtomIDsRef});
2726 
2727   if (!@BondedAtomPairIDs) {
2728     return (\@ComponentsAtomIDsRefs, \@ComponentsAtomIDsMapRefs);
2729   }
2730 
2731   $AtomIDsGraph = new Graph(@{$AtomIDsRef});
2732   $AtomIDsGraph->AddEdges(@BondedAtomPairIDs);
2733 
2734   @ComponentsAtomIDsRefs = $AtomIDsGraph->GetConnectedComponentsVertices();
2735 
2736   # Setup atom IDs map for each component...
2737   for $Index (0 .. $#ComponentsAtomIDsRefs) {
2738     %{$ComponentsAtomIDsMapRefs[$Index]} = ();
2739 
2740     for $AtomID (@{$ComponentsAtomIDsRefs[$Index]}) {
2741       $ComponentsAtomIDsMapRefs[$Index]{$AtomID} = $AtomID;
2742     }
2743   }
2744 
2745   return (\@ComponentsAtomIDsRefs, \@ComponentsAtomIDsMapRefs);
2746 }
2747 
2748 # Get linear paths for connected components starting and ending at terminal aromatic atoms,
2749 # which are connected to only one other aromatic atom in the connected component..
2750 #
2751 sub _GetConnectedComponentsPathsAtomsAndBondsForNonRingAromaticAtoms {
2752   my($This, $AtomIDsMapRef, $ComponentsAtomIDsRef, $ComponentsAtomIDsMapRef) = @_;
2753   my($Index, $AtomID, $Atom, $AtomNbr, $AtomNbrID, $NumOfNonRingAromaticNbrs, $AtomIndex1, $AtomIndex2, $AtomID1, $AtomID2, $Atom1, $Atom2, $AtomIDsGraph, $StartTerminalAtomID, $EndTerminalAtomID, @Paths, @PathAtomIDs, @PathsAtoms, @PathsBonds, @TerminalAtomIDs, @AtomIDs, @BondedAtomPairIDs);
2754 
2755   @PathsAtoms = ();
2756   @PathsBonds = ();
2757 
2758   @TerminalAtomIDs = ();
2759 
2760   $Index = 0;
2761   COMPONENT: for $Index (0 .. $#{$ComponentsAtomIDsRef}) {
2762     @{$TerminalAtomIDs[$Index]} = ();
2763 
2764     # Identify terminal atoms for connected components...
2765     #
2766     # Notes:
2767     #   . Terminal atoms are defined as atoms connected to only one marked
2768     #     aromatic atom.
2769     #   . Linear connected compoents contain only two terminal atoms.
2770     #
2771     ATOM: for $AtomID (@{$ComponentsAtomIDsRef->[$Index]}) {
2772       $Atom = $AtomIDsMapRef->{$AtomID};
2773       $NumOfNonRingAromaticNbrs = 0;
2774 
2775       ATOMNBRID: for $AtomNbr ($Atom->GetNeighbors()) {
2776         $AtomNbrID = $AtomNbr->GetID();
2777 
2778         # Is neighbor in the same connected components containing aromatic atoms?
2779         if (!exists $ComponentsAtomIDsMapRef->[$Index]{$AtomNbrID}) {
2780           next ATOMNBRID;
2781         }
2782         $NumOfNonRingAromaticNbrs++;
2783       }
2784 
2785       # Is it a terminal atom?
2786       if ($NumOfNonRingAromaticNbrs != 1) {
2787         next ATOM;
2788       }
2789       push @{$TerminalAtomIDs[$Index]}, $AtomID;
2790     }
2791 
2792     if (@{$TerminalAtomIDs[$Index]} != 2) {
2793       next COMPONENT;
2794     }
2795 
2796     # Setup bonded atom pair IDs for connected component...
2797     #
2798     @AtomIDs = @{$ComponentsAtomIDsRef->[$Index]};
2799     @BondedAtomPairIDs = ();
2800 
2801     for $AtomIndex1 ( 0 .. $#AtomIDs) {
2802       $AtomID1 = $AtomIDs[$AtomIndex1];
2803       $Atom1 = $AtomIDsMapRef->{$AtomID1};
2804 
2805       for $AtomIndex2 ( ($AtomIndex1 + 1) .. $#AtomIDs) {
2806         $AtomID2 = $AtomIDs[$AtomIndex2];
2807         $Atom2 = $AtomIDsMapRef->{$AtomID2};
2808 
2809         if ($Atom1->IsBondedToAtom($Atom2)) {
2810           push @BondedAtomPairIDs, ($AtomID1, $AtomID2);
2811         }
2812       }
2813     }
2814 
2815     if (!@BondedAtomPairIDs) {
2816       next COMPONENT;
2817     }
2818 
2819     # Get path for connected component...
2820     $AtomIDsGraph = new Graph(@AtomIDs);
2821     $AtomIDsGraph->AddEdges(@BondedAtomPairIDs);
2822 
2823     ($StartTerminalAtomID, $EndTerminalAtomID) = sort { $a <=> $b }  @{$TerminalAtomIDs[$Index]};
2824     @Paths = $AtomIDsGraph->GetPathsBetween($StartTerminalAtomID, $EndTerminalAtomID);
2825 
2826     if (@Paths != 1) {
2827       next COMPONENT;
2828     }
2829 
2830     @PathAtomIDs = $Paths[0]->GetVertices();
2831 
2832     my(@PathAtoms);
2833     @PathAtoms = $This->_GetAtomsFromAtomIDs(@PathAtomIDs);
2834     push @PathsAtoms, \@PathAtoms;
2835 
2836     my(@PathBonds);
2837     @PathBonds = $This->_GetPathBonds(@PathAtomIDs);
2838     push @PathsBonds, \@PathBonds;
2839 
2840   }
2841 
2842   return (\@PathsAtoms, \@PathsBonds);
2843 }
2844 
2845 # Setup initial processing status of atoms and bonds involved in connected paths
2846 # before starting kekulization...
2847 #
2848 # Possible atom processing status: DoubleBondPossible, DoubleBondAssigned, DoubleBondNotPossible
2849 # Initial status: DoubleBondPossible or DoubleBondNotPossible
2850 #
2851 # Possible bond processing status: DoubleBondAssigned, SingleBondAssigned, NotProcessed
2852 #
2853 # Possible paths processing status: Processed, NotProcessed
2854 # Initial status: NotProcessed
2855 #
2856 sub _SetupConnectedPathSetsForKekulization {
2857   my($This, $PathAtomsSetsRef, $PathBondsSetsRef) = @_;
2858   my($PathIndex, $PathAtomsRef, $PathBondsRef, $Atom, $AtomID, $Bond, $BondID, %AtomProcessingStatusMap, %BondProcessingStatusMap, @PathsProcessingStatus, %InitialPathBondOrderMap);
2859 
2860   # Possible path set status values: Processed, NotProcessed
2861   # Initial value: NotProcessed
2862   #
2863   @PathsProcessingStatus = ('NotProcessed') x scalar @{$PathAtomsSetsRef};
2864 
2865   # Collect initial bond order of path bonds before setting bond orders to 1
2866   # and use it to set the bond order back to intial value after it has been processed for
2867   # availability of double bonds...
2868   #
2869   %InitialPathBondOrderMap = ();
2870   for $PathBondsRef (@{$PathBondsSetsRef}) {
2871     BOND: for $Bond (@{$PathBondsRef}) {
2872       $BondID = $Bond->GetID();
2873       if (exists $InitialPathBondOrderMap{$BondID}) {
2874         next BOND;
2875       }
2876       $InitialPathBondOrderMap{$BondID} = $Bond->GetBondOrder();
2877       $Bond->SetBondOrder(1);
2878     }
2879   }
2880 
2881   %AtomProcessingStatusMap = ();
2882   %BondProcessingStatusMap = ();
2883 
2884   for $PathIndex (0 .. $#{$PathAtomsSetsRef}) {
2885 
2886     $PathAtomsRef = $PathAtomsSetsRef->[$PathIndex];
2887     ATOM: for $Atom (@{$PathAtomsRef}) {
2888       $AtomID = $Atom->GetID();
2889       if (exists $AtomProcessingStatusMap{$AtomID}) {
2890         next ATOM;
2891       }
2892       $AtomProcessingStatusMap{$AtomID} = ($Atom->GetNumOfBondsAvailableForNonHydrogenAtoms() >= 1) ? 'DoubleBondPossible' : 'DoubleBondNotPossible';
2893     }
2894 
2895     $PathBondsRef = $PathBondsSetsRef->[$PathIndex];
2896     BOND: for $Bond (@{$PathBondsRef}) {
2897       $BondID = $Bond->GetID();
2898       if (exists $BondProcessingStatusMap{$BondID}) {
2899         next BOND;
2900       }
2901       $BondProcessingStatusMap{$BondID} = 'NotProcessed';
2902     }
2903   }
2904 
2905   # Set bond orders back to initial bond orders...
2906   for $PathIndex (0 .. $#{$PathAtomsSetsRef}) {
2907     $PathBondsRef = $PathBondsSetsRef->[$PathIndex];
2908 
2909     for $Bond (@{$PathBondsRef}) {
2910       $BondID = $Bond->GetID();
2911       if (exists $InitialPathBondOrderMap{$BondID}) {
2912         $Bond->SetBondOrder($InitialPathBondOrderMap{$BondID});
2913       }
2914     }
2915   }
2916 
2917   return (\%AtomProcessingStatusMap, \%BondProcessingStatusMap, \@PathsProcessingStatus);
2918 }
2919 
2920 # Kekulize connected path sets corresponding to fused rings, individual rings, or any other
2921 # connected path...
2922 #
2923 # Note:
2924 #   . PathAtomsRef and PathBondsRef contain paths and bonds corresponding to path
2925 #     under consideration for kekulization
2926 #   . PathAtomsSetsRef and PathBondsSetsRef contain any other available paths fused
2927 #     to the path being kekulized
2928 #   . _KekulizeConnectedPathSets is invoked recursively to kekulize all available paths
2929 #
2930 sub _KekulizeConnectedPathSets {
2931   my($This, $PathAtomsRef, $PathBondsRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $PathAtomsSetsRef, $PathBondsSetsRef, $PathsProcessingStatusRef) = @_;
2932   my($PathBond);
2933 
2934   # Get next available path bond...
2935   $PathBond = $This->_GetNextAvailablePathBondForKekulization($PathBondsRef, $BondProcessingStatusMapRef);
2936 
2937   if ($PathBond) {
2938     return $This->_ProcessNextAvailablePathBondForKekulization($PathBond, $PathAtomsRef, $PathBondsRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $PathAtomsSetsRef, $PathBondsSetsRef, $PathsProcessingStatusRef);
2939   }
2940 
2941   # Did kekulization succeed for the current path bonds?
2942   if (!$This->_DidKekulizationSucceedForPathBonds($PathAtomsRef, $PathBondsRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef)) {
2943     return 0;
2944   }
2945 
2946   # Is there any other path available for kekulization?
2947   ($PathAtomsRef, $PathBondsRef) = $This->_GetNextAvailablePathForKekulization($PathAtomsSetsRef, $PathBondsSetsRef, $PathsProcessingStatusRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef);
2948 
2949   if ($PathAtomsRef && $PathBondsRef) {
2950     # Recursively call itself to kekulize next path, which could either be a new path or part
2951     # of a fused paths corresponding to fused ring sets...
2952     #
2953     return $This->_KekulizeConnectedPathSets($PathAtomsRef, $PathBondsRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $PathAtomsSetsRef, $PathBondsSetsRef, $PathsProcessingStatusRef);
2954   }
2955 
2956   return 1;
2957 }
2958 
2959 # Get next available path bond in a list of path bonds...
2960 #
2961 sub _GetNextAvailablePathBondForKekulization {
2962   my($This, $PathBondsRef, $BondProcessingStatusMapRef) = @_;
2963   my($AvailablePathBond, $PathBond, $PathBondID);
2964 
2965   $AvailablePathBond = undef;
2966 
2967   BOND: for $PathBond (@{$PathBondsRef}) {
2968     $PathBondID = $PathBond->GetID();
2969     if (!exists $BondProcessingStatusMapRef->{$PathBondID}) {
2970       next BOND;
2971     }
2972     if ($BondProcessingStatusMapRef->{$PathBondID} =~ /^NotProcessed$/i) {
2973       $AvailablePathBond = $PathBond;
2974       last BOND;
2975     }
2976   }
2977 
2978   return ($AvailablePathBond);
2979 }
2980 
2981 # Process next available path bond for kekulizaiton...
2982 #
2983 sub _ProcessNextAvailablePathBondForKekulization {
2984   my($This, $PathBond, $PathAtomsRef, $PathBondsRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $PathAtomsSetsRef, $PathBondsSetsRef, $PathsProcessingStatusRef) = @_;
2985   my($PathBondID, $PathAtom1, $PathAtom2, $PathAtomID1, $PathAtomID2, %CurrentAtomProcessingStatusMap, %CurrentBondProcessingStatusMap);
2986 
2987   $PathBondID = $PathBond->GetID();
2988 
2989   ($PathAtom1, $PathAtom2) = $PathBond->GetAtoms();
2990   ($PathAtomID1, $PathAtomID2) = ($PathAtom1->GetID(), $PathAtom2->GetID());
2991 
2992   %CurrentAtomProcessingStatusMap = %{$AtomProcessingStatusMapRef};
2993   %CurrentBondProcessingStatusMap = %{$BondProcessingStatusMapRef};
2994 
2995   # Is it possible to assign a double bond to the current path bond?
2996   if ($AtomProcessingStatusMapRef->{$PathAtomID1} =~ /^DoubleBondPossible$/i && $AtomProcessingStatusMapRef->{$PathAtomID2} =~ /^DoubleBondPossible$/i ) {
2997     # Set current bond to double bond by appropriately marking atom and bond process status...
2998     $AtomProcessingStatusMapRef->{$PathAtomID1} = 'DoubleBondAssigned';
2999     $AtomProcessingStatusMapRef->{$PathAtomID2} = 'DoubleBondAssigned';
3000 
3001     $BondProcessingStatusMapRef->{$PathBondID} = 'DoubleBondAssigned';
3002 
3003     # Recursively call  _KekulizeConnectedPathSets to kekulize next available bond...
3004     if ($This->_KekulizeConnectedPathSets($PathAtomsRef, $PathBondsRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $PathAtomsSetsRef, $PathBondsSetsRef, $PathsProcessingStatusRef)) {
3005       return 1;
3006     }
3007 
3008     # Double bond at the current ring bond position didn't lead to successful kekulization...
3009     %{$AtomProcessingStatusMapRef} = %CurrentAtomProcessingStatusMap;
3010     %{$BondProcessingStatusMapRef} = %CurrentBondProcessingStatusMap;
3011   }
3012 
3013   # Try single bond at the current ring bond position and recursively call _KekulizeConnectedPathSets to kekulize
3014   # rest of the ring bonds...
3015   #
3016   $BondProcessingStatusMapRef->{$PathBondID} = 'SingleBondAssigned';
3017 
3018   if ($This->_KekulizeConnectedPathSets($PathAtomsRef, $PathBondsRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef, $PathAtomsSetsRef, $PathBondsSetsRef, $PathsProcessingStatusRef)) {
3019     return 1;
3020   }
3021 
3022   %{$AtomProcessingStatusMapRef} = %CurrentAtomProcessingStatusMap;
3023   %{$BondProcessingStatusMapRef} = %CurrentBondProcessingStatusMap;
3024 
3025   # Kekulization didn't work out for path bonds...
3026 
3027   return 0;
3028 
3029 }
3030 
3031 # Get next available path for kekulization from a set of fused ring paths...
3032 #
3033 sub _GetNextAvailablePathForKekulization {
3034   my($This, $PathAtomsSetsRef, $PathBondsSetsRef, $PathsProcessingStatusRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef) = @_;
3035   my($PathIndex, $AvailablePathIndex, $PathAtomsRef, $PathBondsRef, $PathBond, $PathBondID, $MaxNumOfPathBondsProcessed, $NumOfPathBondsProcessed);
3036 
3037   ($PathAtomsRef, $PathBondsRef, $AvailablePathIndex) = (undef) x 3;
3038 
3039   if (!(defined($PathAtomsSetsRef)  && defined($PathBondsSetsRef) && defined($PathsProcessingStatusRef))) {
3040     return ($PathAtomsRef, $PathBondsRef);
3041   }
3042 
3043   $MaxNumOfPathBondsProcessed = -999;
3044   $AvailablePathIndex = undef;
3045 
3046   PATHINDEX: for $PathIndex (0 .. $#{$PathsProcessingStatusRef}) {
3047     if ($PathsProcessingStatusRef->[$PathIndex] =~ /^Processed$/i) {
3048       next PATHINDEX;
3049     }
3050 
3051     # Count of already processed bonds in an unprocessed path bonds through
3052     # their participation in any fused bonds sets...
3053     #
3054     $NumOfPathBondsProcessed = 0;
3055     PATHBOND: for $PathBond (@{$PathBondsSetsRef->[$PathIndex]}) {
3056       $PathBondID = $PathBond->GetID();
3057       if ($BondProcessingStatusMapRef->{$PathBondID} =~ /^NotProcessed$/i) {
3058         next PATHBOND;
3059       }
3060       $NumOfPathBondsProcessed++;
3061     }
3062 
3063     if ($NumOfPathBondsProcessed > $MaxNumOfPathBondsProcessed) {
3064       $AvailablePathIndex = $PathIndex;
3065       $MaxNumOfPathBondsProcessed = $NumOfPathBondsProcessed;
3066     }
3067 
3068   }
3069 
3070   # Is any path available?
3071   if (!$AvailablePathIndex) {
3072     return ($PathAtomsRef, $PathBondsRef);
3073   }
3074 
3075   $PathsProcessingStatusRef->[$AvailablePathIndex] = 'Processed';
3076 
3077   $PathAtomsRef = $PathAtomsSetsRef->[$AvailablePathIndex];
3078   $PathBondsRef = $PathBondsSetsRef->[$AvailablePathIndex];
3079 
3080   return ($PathAtomsRef, $PathBondsRef);
3081 }
3082 
3083 # Check for kekulization in a specific set of path bonds. For successful kekulization, all
3084 # all path atoms marked with DoubleBondPossible must be involved in a path double bond...
3085 #
3086 sub _DidKekulizationSucceedForPathBonds {
3087   my($This, $PathAtomsRef, $PathBondsRef, $AtomProcessingStatusMapRef, $BondProcessingStatusMapRef) = @_;
3088   my($PathAtom, $PathAtomID);
3089 
3090   for $PathAtom (@{$PathAtomsRef}) {
3091     $PathAtomID = $PathAtom->GetID();
3092     if (exists $AtomProcessingStatusMapRef->{$PathAtomID} && $AtomProcessingStatusMapRef->{$PathAtomID} =~ /^DoubleBondPossible$/i) {
3093       return 0;
3094     }
3095   }
3096   return 1;
3097 }
3098 
3099 # Assign bond orders to the bonds in a molecule which have been successfully
3100 # kekulized along with optional clearing of aromaticty property...
3101 #
3102 sub _ProcessBondOrdersAssignedDuringSuccessfulKekulization {
3103   my($This, $BondsRef, $BondsProcessingStatusMapRef, $DeleteBondsAromaticity) = @_;
3104   my($Bond, $BondID, $BondOrder);
3105 
3106   $DeleteBondsAromaticity = defined $DeleteBondsAromaticity ? $DeleteBondsAromaticity : 0;
3107 
3108   BOND: for $Bond (@{$BondsRef}) {
3109     $BondID = $Bond->GetID();
3110 
3111     if (!exists $BondsProcessingStatusMapRef->{$BondID}) {
3112       carp "Warning: ${ClassName}->_ProcessBondOrdersAssignedDuringSuccessfulKekulization: Couldn't process bond with bond ID, $BondID: It's not available in the list of bonds processed for kekulization...";
3113       next BOND;
3114     }
3115 
3116     $BondOrder = ($BondsProcessingStatusMapRef->{$BondID} =~ /^DoubleBondAssigned$/i) ? 2 : 1;
3117     $Bond->SetBondOrder($BondOrder);
3118 
3119     if ($DeleteBondsAromaticity) {
3120       $Bond->DeleteAromatic();
3121     }
3122   }
3123   return $This;
3124 }
3125 
3126 # Does molecule contains aromatic rings?
3127 #
3128 sub HasAromaticRings {
3129   my($This) = @_;
3130 
3131   return $This->GetNumOfAromaticRings() ? 1 : 0;
3132 }
3133 
3134 # Does molecule contains any aromatic atom in a ring?
3135 #
3136 sub HasAromaticAtomsInRings {
3137   my($This) = @_;
3138   my($Atom);
3139 
3140   ATOM: for $Atom ($This->GetAtoms()) {
3141     if (!$Atom->IsAromatic()) {
3142       next ATOM;
3143     }
3144     if ($Atom->IsInRing()) {
3145       return 1;
3146     }
3147   }
3148   return 0;
3149 }
3150 
3151 # Does molecule contains any aromatic atom not in a ring?
3152 #
3153 sub HasAromaticAtomsNotInRings {
3154   my($This) = @_;
3155   my($Atom);
3156 
3157   ATOM: for $Atom ($This->GetAtoms()) {
3158     if (!$Atom->IsAromatic()) {
3159       next ATOM;
3160     }
3161     if ($Atom->IsNotInRing()) {
3162       return 1;
3163     }
3164   }
3165   return 0;
3166 }
3167 
3168 # Does molecule contains rings?
3169 #
3170 sub HasRings {
3171   my($This) = @_;
3172 
3173   return $This->IsCyclic();
3174 }
3175 
3176 # Does molecule contains only one ring?
3177 #
3178 sub HasOnlyOneRing {
3179   my($This) = @_;
3180 
3181   return $This->IsUnicyclic();
3182 }
3183 
3184 # Does molecule contains any rings?
3185 #
3186 sub HasNoRings {
3187   my($This) = @_;
3188 
3189   return $This->IsAcyclic();
3190 }
3191 
3192 # Get size of smallest ring...
3193 #
3194 sub GetSizeOfSmallestRing {
3195   my($This) = @_;
3196 
3197   return $This->GetSizeOfSmallestCycle();
3198 }
3199 
3200 # Get size of largest ring...
3201 #
3202 sub GetSizeOfLargestRing {
3203   my($This) = @_;
3204 
3205   return $This->GetSizeOfLargestCycle();
3206 }
3207 
3208 # Get number of rings...
3209 #
3210 sub GetNumOfRings {
3211   my($This) = @_;
3212 
3213   return $This->GetNumOfCycles();
3214 }
3215 
3216 # Get number of aromatic rings...
3217 #
3218 sub GetNumOfAromaticRings {
3219   my($This) = @_;
3220   my($NumOfRings);
3221 
3222   $NumOfRings = scalar $This->GetAromaticRings();
3223 
3224   return $NumOfRings;
3225 }
3226 
3227 # Get num of rings with odd size...
3228 #
3229 sub GetNumOfRingsWithOddSize {
3230   my($This) = @_;
3231 
3232   return $This->GetNumOfCyclesWithOddSize();
3233 }
3234 
3235 # Get num of rings with even size...
3236 #
3237 sub GetNumOfRingsWithEvenSize {
3238   my($This) = @_;
3239 
3240   return $This->GetNumOfCyclesWithEvenSize();
3241 }
3242 
3243 # Get num of rings with specified size...
3244 #
3245 sub GetNumOfRingsWithSize {
3246   my($This, $RingSize) = @_;
3247 
3248   return $This->GetNumOfCyclesWithSize($RingSize);
3249 }
3250 
3251 # Get num of rings with size less than a specified size...
3252 #
3253 sub GetNumOfRingsWithSizeLessThan {
3254   my($This, $RingSize) = @_;
3255 
3256   return $This->GetNumOfCyclesWithSizeLessThan($RingSize);
3257 }
3258 
3259 # Get num of rings with size greater than a specified size...
3260 #
3261 sub GetNumOfRingsWithSizeGreaterThan {
3262   my($This, $RingSize) = @_;
3263 
3264   return $This->GetNumOfCyclesWithSizeGreaterThan($RingSize);
3265 }
3266 
3267 # Get largest ring as an array containing ring atoms...
3268 #
3269 sub GetLargestRing {
3270   my($This) = @_;
3271 
3272   return $This->_GetRing($This->GetLargestCycle());
3273 }
3274 
3275 # Get smallest ring as an array containing ring atoms...
3276 #
3277 sub GetSmallestRing {
3278   my($This) = @_;
3279 
3280   return $This->_GetRing($This->GetSmallestCycle());
3281 }
3282 
3283 # Get rings as an array containing references to arrays with ring atoms...
3284 #
3285 sub GetRings {
3286   my($This) = @_;
3287 
3288   return $This->_GetRings($This->GetCycles());
3289 }
3290 
3291 # Get aromatic rings as an array containing references to arrays with ring atoms...
3292 #
3293 sub GetAromaticRings {
3294   my($This) = @_;
3295 
3296   return $This->_GetAromaticRings($This->GetCycles());
3297 }
3298 
3299 # Get odd size rings as an array containing references to arrays with ring atoms...
3300 #
3301 sub GetRingsWithOddSize {
3302   my($This) = @_;
3303 
3304   return $This->_GetRings($This->GetCyclesWithOddSize());
3305 }
3306 
3307 # Get even size rings as an array containing references to arrays with ring atoms...
3308 #
3309 sub GetRingsWithEvenSize {
3310   my($This) = @_;
3311 
3312   return $This->_GetRings($This->GetCyclesWithEvenSize());
3313 }
3314 
3315 # Get rings with a specific size as an array containing references to arrays with ring atoms...
3316 #
3317 sub GetRingsWithSize {
3318   my($This, $RingSize) = @_;
3319 
3320   return $This->_GetRings($This->GetCyclesWithSize($RingSize));
3321 }
3322 
3323 # Get rings with size less than a specific size as an array containing references to arrays with ring atoms...
3324 #
3325 sub GetRingsWithSizeLessThan {
3326   my($This, $RingSize) = @_;
3327 
3328   return $This->_GetRings($This->GetCyclesWithSizeLessThan($RingSize));
3329 }
3330 
3331 # Get rings with size greater than a specific size as an array containing references to arrays with ring atoms...
3332 #
3333 sub GetRingsWithSizeGreaterThan {
3334   my($This, $RingSize) = @_;
3335 
3336   return $This->_GetRings($This->GetCyclesWithSizeGreaterThan($RingSize));
3337 }
3338 
3339 # Generate an array of bond objects for an array of ring atoms and return an array
3340 # of bond objects...
3341 #
3342 sub GetRingBonds {
3343   my($This, @RingAtoms) = @_;
3344   my(@Bonds);
3345 
3346   @Bonds = ();
3347   if (!@RingAtoms) {
3348     # Return an empty ring bonds list...
3349     return @Bonds;
3350   }
3351 
3352   my(@RingAtomIDs);
3353 
3354   @RingAtomIDs = ();
3355   @RingAtomIDs = $This->_GetAtomsIDsFromAtoms(@RingAtoms);
3356   if (!@RingAtomIDs) {
3357     carp "Warning: ${ClassName}->GetRingBonds: No ring bonds retrieved: Atom IDs couldn't be retrieved for specified atoms...";
3358     return @Bonds;
3359   }
3360 
3361   # Add start atom to the end to make it a cyclic path for ring: It's taken out during conversion
3362   # of cyclic path to a ring...
3363   push @RingAtomIDs, $RingAtomIDs[0];
3364 
3365   return $This->_GetPathBonds(@RingAtomIDs);
3366 }
3367 
3368 # Generate an array containing references to arrays of ring bond objects for rings specified
3369 # in an array of references to ring atoms...
3370 #
3371 sub GetRingBondsFromRings {
3372   my($This, @RingAtomsSets) = @_;
3373   my($RingAtomsRef, @RingBondsSets);
3374 
3375   @RingBondsSets = ();
3376   for $RingAtomsRef  (@RingAtomsSets) {
3377     my(@RingBonds);
3378     @RingBonds = $This->GetRingBonds(@{$RingAtomsRef});
3379 
3380     push @RingBondsSets, \@RingBonds;
3381   }
3382 
3383   return @RingBondsSets;
3384 }
3385 
3386 # Does molecule has any fused rings?
3387 #
3388 sub HasFusedRings {
3389   my($This) = @_;
3390 
3391   return $This->HasFusedCycles();
3392 }
3393 
3394 # Get references to array of fused ring sets and non-fused rings. Fused ring sets array reference
3395 # contains refernces to arrays of rings; Non-fused rings array reference contains references to
3396 # arrays of ring atoms...
3397 # rings.
3398 #
3399 sub GetFusedAndNonFusedRings {
3400   my($This) = @_;
3401   my($FusedCyclesSetsRef, $NonFusedCyclesRef, @FusedRingSets, @NonFusedRings);
3402 
3403   @FusedRingSets = (); @NonFusedRings = ();
3404   ($FusedCyclesSetsRef, $NonFusedCyclesRef) = $This->GetFusedAndNonFusedCycles();
3405   if (!(defined($FusedCyclesSetsRef) && defined($NonFusedCyclesRef))) {
3406     return (\@FusedRingSets, \@NonFusedRings);
3407   }
3408   my($FusedCyclesSetRef);
3409 
3410   for $FusedCyclesSetRef (@{$FusedCyclesSetsRef}) {
3411     my(@FusedRingSet);
3412     @FusedRingSet = ();
3413     @FusedRingSet = $This->_GetRings(@{$FusedCyclesSetRef});
3414     push @FusedRingSets, \@FusedRingSet;
3415   }
3416 
3417   @NonFusedRings = $This->_GetRings(@{$NonFusedCyclesRef});
3418 
3419   return (\@FusedRingSets, \@NonFusedRings);
3420 }
3421 
3422 # Get rings as an array containing references to arrays with ring atoms...
3423 #
3424 sub _GetRings {
3425   my($This, @CyclicPaths) = @_;
3426   my($CyclicPath, @Rings);
3427 
3428   @Rings = ();
3429   if (!@CyclicPaths) {
3430     return @Rings;
3431   }
3432   if (!@CyclicPaths) {
3433     # Return an empty ring list...
3434     return @Rings;
3435   }
3436 
3437   for $CyclicPath (@CyclicPaths) {
3438     my(@RingAtoms);
3439     @RingAtoms = ();
3440     push @RingAtoms, $This->_GetRing($CyclicPath);
3441 
3442     push @Rings, \@RingAtoms;
3443   }
3444   return @Rings;
3445 }
3446 
3447 # Get aromatic rings as an array containing references to arrays with ring atoms...
3448 #
3449 sub _GetAromaticRings {
3450   my($This, @CyclicPaths) = @_;
3451   my($RingAtomsRef, @Rings, @AromaticRings);
3452 
3453   @AromaticRings = ();
3454   @Rings = $This->_GetRings(@CyclicPaths);
3455 
3456   if (!@Rings) {
3457     return @AromaticRings;
3458   }
3459   RING: for $RingAtomsRef (@Rings) {
3460     if (!$This->IsRingAromatic(@{$RingAtomsRef})) {
3461       next RING;
3462     }
3463     my(@RingAtoms);
3464     @RingAtoms = ();
3465     push @RingAtoms, @{$RingAtomsRef};
3466 
3467     push @AromaticRings, \@RingAtoms;
3468   }
3469   return @AromaticRings;
3470 }
3471 
3472 # Map atom IDs in cyclic path to atoms and return a reference to an array containing ring atoms...
3473 #
3474 # Note:
3475 #   . Start and end vertex is same for cyclic paths. So end atom is removed before
3476 #     returning atoms array as ring atoms...
3477 #
3478 sub _GetRing {
3479   my($This, $CyclicPath) = @_;
3480   my(@RingAtoms);
3481 
3482   @RingAtoms = ();
3483   if (!defined $CyclicPath) {
3484     # Return an empty atoms list...
3485     return @RingAtoms;
3486   }
3487 
3488   @RingAtoms = $This->_GetPathAtoms($CyclicPath);
3489   if (@RingAtoms) {
3490     pop @RingAtoms;
3491   }
3492   return @RingAtoms;
3493 }
3494 
3495 # Map atom IDs to atoms and return a reference to an array containing these atoms...
3496 #
3497 sub _GetPathAtoms {
3498   my($This, $Path) = @_;
3499   my(@PathAtoms);
3500 
3501   @PathAtoms = ();
3502   if (!defined $Path) {
3503     carp "Warning: ${ClassName}->_GetPathAtoms: No path atoms retrieved: Path must be defined...";
3504     return @PathAtoms;
3505   }
3506   my(@AtomIDs);
3507 
3508   @AtomIDs = ();
3509   @AtomIDs = $Path->GetVertices();
3510 
3511   @PathAtoms = $This->_GetAtomsFromAtomIDs(@AtomIDs);
3512 
3513   return @PathAtoms;
3514 }
3515 
3516 # Get bonds for a path specified by atom IDs...
3517 #
3518 sub _GetPathBonds {
3519   my($This, @AtomIDs) = @_;
3520   my($Index, $AtomID1, $AtomID2, @Bonds, @EdgesAtomIDs);
3521 
3522   @Bonds = (); @EdgesAtomIDs = ();
3523 
3524   if (!@AtomIDs || @AtomIDs == 1) {
3525     return @Bonds;
3526   }
3527 
3528   # Setup edges...
3529   for $Index (0 .. ($#AtomIDs - 1) ) {
3530     $AtomID1 = $AtomIDs[$Index];
3531     $AtomID2 = $AtomIDs[$Index + 1];
3532     push @EdgesAtomIDs, ($AtomID1, $AtomID2);
3533   }
3534   @Bonds =  $This->GetEdgesProperty('Bond', @EdgesAtomIDs);
3535 
3536   return @Bonds;
3537 }
3538 
3539 # Map atom ID to an atom...
3540 #
3541 sub _GetAtomFromAtomID {
3542   my($This, $AtomID) = @_;
3543 
3544   return $This->GetVertexProperty('Atom', $AtomID);
3545 }
3546 
3547 # Map atom IDs to atoms and return an array containing these atoms...
3548 #
3549 sub _GetAtomsFromAtomIDs {
3550   my($This, @AtomIDs) = @_;
3551 
3552   return $This->GetVerticesProperty('Atom', @AtomIDs);
3553 }
3554 
3555 # Map atoms to atom IDs and return an array containing these atoms...
3556 #
3557 sub _GetAtomsIDsFromAtoms {
3558   my($This, @Atoms) = @_;
3559 
3560   return map { $_->GetID() } @Atoms;
3561 }
3562 
3563 # Get bonded atom pair atom IDs for specified list of atom IDs...
3564 #
3565 sub _GetBondedAtomPairAtomIDsFromAtomIDs {
3566   my($This, @AtomIDs) = @_;
3567   my($AtomIndex1, $AtomID1, $Atom1, $AtomIndex2, $AtomID2, $Atom2, @Atoms, @BondedAtomPairIDs);
3568 
3569   @BondedAtomPairIDs = ();
3570   @Atoms = $This->_GetAtomsFromAtomIDs(@AtomIDs);
3571 
3572   for $AtomIndex1 ( 0 .. $#Atoms) {
3573     $Atom1 = $Atoms[$AtomIndex1];
3574     $AtomID1 = $Atom1->GetID();
3575 
3576     ATOMINDEX2: for $AtomIndex2 ( ($AtomIndex1 + 1) .. $#Atoms) {
3577       $Atom2 = $Atoms[$AtomIndex2];
3578       if (!$Atom1->IsBondedToAtom($Atom2)) {
3579         next ATOMINDEX2;
3580        }
3581       $AtomID2 = $Atom2->GetID();
3582 
3583       push @BondedAtomPairIDs, ($AtomID1, $AtomID2);
3584     }
3585   }
3586 
3587   return @BondedAtomPairIDs;
3588 }
3589 
3590 # Get bonded atom pair atoms for specified list of atoms...
3591 #
3592 sub _GetBondedAtomPairAtomsFromAtoms {
3593   my($This, @Atoms) = @_;
3594   my($AtomIndex1, $Atom1, $AtomIndex2, $Atom2, @BondedAtomPairAtoms);
3595 
3596   @BondedAtomPairAtoms = ();
3597 
3598   for $AtomIndex1 ( 0 .. $#Atoms) {
3599     $Atom1 = $Atoms[$AtomIndex1];
3600 
3601     ATOMINDEX2: for $AtomIndex2 ( ($AtomIndex1 + 1) .. $#Atoms) {
3602       $Atom2 = $Atoms[$AtomIndex2];
3603       if ($Atom1->IsBondedToAtom($Atom2)) {
3604         next ATOMINDEX2;
3605        }
3606 
3607       push @BondedAtomPairAtoms, ($Atom1, $Atom2);
3608     }
3609   }
3610 
3611   return @BondedAtomPairAtoms;
3612 }
3613 
3614 # Is atom in a ring?
3615 #
3616 sub _IsAtomInRing {
3617   my($This, $Atom) = @_;
3618 
3619   return $This->IsCyclicVertex($Atom->GetID());
3620 }
3621 
3622 # Is atom not in a ring?
3623 #
3624 sub _IsAtomNotInRing {
3625   my($This, $Atom) = @_;
3626 
3627   return $This->IsAcyclicVertex($Atom->GetID());
3628 }
3629 
3630 # Is atom only in one ring?
3631 #
3632 sub _IsAtomInOnlyOneRing {
3633   my($This, $Atom) = @_;
3634 
3635   return $This->IsUnicyclicVertex($Atom->GetID());
3636 }
3637 
3638 # Is atom in a ring of specified size?
3639 #
3640 sub _IsAtomInRingOfSize {
3641   my($This, $Atom, $RingSize) = @_;
3642 
3643   return $This->GetNumOfVertexCyclesWithSize($Atom->GetID(), $RingSize) ? 1 : 0;
3644 }
3645 
3646 # Get size of smallest ring containing specified atom...
3647 #
3648 sub _GetSizeOfSmallestAtomRing {
3649   my($This, $Atom) = @_;
3650 
3651   return $This->GetSizeOfSmallestVertexCycle($Atom->GetID());
3652 }
3653 
3654 # Get size of largest ring containing specified atom...
3655 #
3656 sub _GetSizeOfLargestAtomRing {
3657   my($This, $Atom) = @_;
3658 
3659   return $This->GetSizeOfLargestVertexCycle($Atom->GetID());
3660 }
3661 
3662 # Get number of  rings containing specified atom...
3663 #
3664 sub _GetNumOfAtomRings {
3665   my($This, $Atom) = @_;
3666 
3667   return $This->GetNumOfVertexCycles($Atom->GetID());
3668 }
3669 
3670 # Get number of  rings with odd size containing specified atom...
3671 #
3672 sub _GetNumOfAtomRingsWithOddSize {
3673   my($This, $Atom) = @_;
3674 
3675   return $This->GetNumOfVertexCyclesWithOddSize($Atom->GetID());
3676 }
3677 
3678 # Get number of  rings with even size containing specified atom...
3679 #
3680 sub _GetNumOfAtomRingsWithEvenSize {
3681   my($This, $Atom) = @_;
3682 
3683   return $This->GetNumOfVertexCyclesWithEvenSize($Atom->GetID());
3684 }
3685 
3686 # Get number of  rings with specified size containing specified atom...
3687 #
3688 sub _GetNumOfAtomRingsWithSize {
3689   my($This, $Atom, $RingSize) = @_;
3690 
3691   return $This->GetNumOfVertexCyclesWithSize($Atom->GetID(), $RingSize);
3692 }
3693 
3694 # Get number of  rings with size less than specified containing specified atom...
3695 #
3696 sub _GetNumOfAtomRingsWithSizeLessThan {
3697   my($This, $Atom, $RingSize) = @_;
3698 
3699   return $This->GetNumOfVertexCyclesWithSizeLessThan($Atom->GetID(), $RingSize);
3700 }
3701 
3702 # Get number of  rings with size greater than specified containing specified atom...
3703 #
3704 sub _GetNumOfAtomRingsWithSizeGreaterThan {
3705   my($This, $Atom, $RingSize) = @_;
3706 
3707   return $This->GetNumOfVertexCyclesWithSizeGreaterThan($Atom->GetID(), $RingSize);
3708 }
3709 
3710 # Get smallest ring as an array containing ring atoms...
3711 #
3712 sub _GetSmallestAtomRing {
3713   my($This, $Atom) = @_;
3714 
3715   return $This->_GetRing($This->GetSmallestVertexCycle($Atom->GetID()));
3716 }
3717 
3718 # Get odd size rings an array of references to arrays containing ring atoms...
3719 #
3720 sub _GetLargestAtomRing {
3721   my($This, $Atom) = @_;
3722 
3723   return $This->_GetRing($This->GetLargestVertexCycle($Atom->GetID()));
3724 }
3725 
3726 # Get all rings an array of references to arrays containing ring atoms...
3727 #
3728 sub _GetAtomRings {
3729   my($This, $Atom) = @_;
3730 
3731   return $This->_GetRings($This->GetVertexCycles($Atom->GetID()));
3732 }
3733 
3734 # Get odd size rings an array of references to arrays containing ring atoms...
3735 #
3736 sub _GetAtomRingsWithOddSize {
3737   my($This, $Atom) = @_;
3738 
3739   return $This->_GetRings($This->GetVertexCyclesWithOddSize($Atom->GetID()));
3740 }
3741 
3742 # Get even size rings an array of references to arrays containing ring atoms...
3743 #
3744 sub _GetAtomRingsWithEvenSize {
3745   my($This, $Atom) = @_;
3746 
3747   return $This->_GetRings($This->GetVertexCyclesWithEvenSize($Atom->GetID()));
3748 }
3749 
3750 # Get rings with specified size  an array of references to arrays containing ring atoms...
3751 #
3752 sub _GetAtomRingsWithSize {
3753   my($This, $Atom, $RingSize) = @_;
3754 
3755   return $This->_GetRings($This->GetVertexCyclesWithSize($Atom->GetID(), $RingSize));
3756 }
3757 
3758 # Get rings with size less than specfied size as an array of references to arrays containing ring atoms...
3759 #
3760 sub _GetAtomRingsWithSizeLessThan {
3761   my($This, $Atom, $RingSize) = @_;
3762 
3763   return $This->_GetRings($This->GetVertexCyclesWithSizeLessThan($Atom->GetID(), $RingSize));
3764 }
3765 
3766 # Get rings with size less than specfied size as an array of references to arrays containing ring atoms...
3767 #
3768 sub _GetAtomRingsWithSizeGreaterThan {
3769   my($This, $Atom, $RingSize) = @_;
3770 
3771   return $This->_GetRings($This->GetVertexCyclesWithSizeGreaterThan($Atom->GetID(), $RingSize));
3772 }
3773 
3774 # Is bond in a ring?
3775 #
3776 sub _IsBondInRing {
3777   my($This, $Bond) = @_;
3778   my($Atom1, $Atom2);
3779 
3780   ($Atom1, $Atom2) = $Bond->GetAtoms();
3781 
3782   return $This->IsCyclicEdge($Atom1->GetID(), $Atom2->GetID());
3783 }
3784 
3785 # Is bond not in a ring?
3786 #
3787 sub _IsBondNotInRing {
3788   my($This, $Bond) = @_;
3789   my($Atom1, $Atom2);
3790 
3791   ($Atom1, $Atom2) = $Bond->GetAtoms();
3792 
3793   return $This->IsAcyclicEdge($Atom1->GetID(), $Atom2->GetID());
3794 }
3795 
3796 # Is bond only in one ring?
3797 #
3798 sub _IsBondInOnlyOneRing {
3799   my($This, $Bond) = @_;
3800   my($Atom1, $Atom2);
3801 
3802   ($Atom1, $Atom2) = $Bond->GetAtoms();
3803 
3804   return $This->IsUnicyclicEdge($Atom1->GetID(), $Atom2->GetID());
3805 }
3806 
3807 # Is bond in a ring of specified size?
3808 #
3809 sub _IsBondInRingOfSize {
3810   my($This, $Bond, $RingSize) = @_;
3811   my($Atom1, $Atom2);
3812 
3813   ($Atom1, $Atom2) = $Bond->GetAtoms();
3814 
3815   return $This->GetNumOfEdgeCyclesWithSize($Atom1->GetID(), $Atom2->GetID(), $RingSize) ? 1 : 0;
3816 }
3817 
3818 # Get size of smallest ring containing specified bond...
3819 #
3820 sub _GetSizeOfSmallestBondRing {
3821   my($This, $Bond) = @_;
3822   my($Atom1, $Atom2);
3823 
3824   ($Atom1, $Atom2) = $Bond->GetAtoms();
3825 
3826   return $This->GetSizeOfSmallestEdgeCycle($Atom1->GetID(), $Atom2->GetID());
3827 }
3828 
3829 # Get size of largest ring containing specified bond...
3830 #
3831 sub _GetSizeOfLargestBondRing {
3832   my($This, $Bond) = @_;
3833   my($Atom1, $Atom2);
3834 
3835   ($Atom1, $Atom2) = $Bond->GetAtoms();
3836 
3837   return $This->GetSizeOfLargestEdgeCycle($Atom1->GetID(), $Atom2->GetID());
3838 }
3839 
3840 # Get number of  rings containing specified bond...
3841 #
3842 sub _GetNumOfBondRings {
3843   my($This, $Bond) = @_;
3844   my($Atom1, $Atom2);
3845 
3846   ($Atom1, $Atom2) = $Bond->GetAtoms();
3847 
3848   return $This->GetNumOfEdgeCycles($Atom1->GetID(), $Atom2->GetID());
3849 }
3850 
3851 # Get number of  rings with odd size containing specified bond...
3852 #
3853 sub _GetNumOfBondRingsWithOddSize {
3854   my($This, $Bond) = @_;
3855   my($Atom1, $Atom2);
3856 
3857   ($Atom1, $Atom2) = $Bond->GetAtoms();
3858 
3859   return $This->GetNumOfEdgeCyclesWithOddSize($Atom1->GetID(), $Atom2->GetID());
3860 }
3861 
3862 # Get number of  rings with even size containing specified bond...
3863 #
3864 sub _GetNumOfBondRingsWithEvenSize {
3865   my($This, $Bond) = @_;
3866   my($Atom1, $Atom2);
3867 
3868   ($Atom1, $Atom2) = $Bond->GetAtoms();
3869 
3870   return $This->GetNumOfEdgeCyclesWithEvenSize($Atom1->GetID(), $Atom2->GetID());
3871 }
3872 
3873 # Get number of  rings with specified size containing specified bond...
3874 #
3875 sub _GetNumOfBondRingsWithSize {
3876   my($This, $Bond, $RingSize) = @_;
3877   my($Atom1, $Atom2);
3878 
3879   ($Atom1, $Atom2) = $Bond->GetAtoms();
3880 
3881   return $This->GetNumOfEdgeCyclesWithSize($Atom1->GetID(), $Atom2->GetID(), $RingSize);
3882 }
3883 
3884 # Get number of  rings with size less than specified containing specified bond...
3885 #
3886 sub _GetNumOfBondRingsWithSizeLessThan {
3887   my($This, $Bond, $RingSize) = @_;
3888   my($Atom1, $Atom2);
3889 
3890   ($Atom1, $Atom2) = $Bond->GetAtoms();
3891 
3892   return $This->GetNumOfEdgeCyclesWithSizeLessThan($Atom1->GetID(), $Atom2->GetID(), $RingSize);
3893 }
3894 
3895 # Get number of  rings with size greater than specified containing specified bond...
3896 #
3897 sub _GetNumOfBondRingsWithSizeGreaterThan {
3898   my($This, $Bond, $RingSize) = @_;
3899   my($Atom1, $Atom2);
3900 
3901   ($Atom1, $Atom2) = $Bond->GetAtoms();
3902 
3903   return $This->GetNumOfEdgeCyclesWithSizeGreaterThan($Atom1->GetID(), $Atom2->GetID(), $RingSize);
3904 }
3905 
3906 # Get smallest ring as an array containing ring atoms...
3907 #
3908 sub _GetSmallestBondRing {
3909   my($This, $Bond) = @_;
3910   my($Atom1, $Atom2);
3911 
3912   ($Atom1, $Atom2) = $Bond->GetAtoms();
3913 
3914   return $This->_GetRing($This->GetSmallestEdgeCycle($Atom1->GetID(), $Atom2->GetID()));
3915 }
3916 
3917 # Get odd size rings an array of references to arrays containing ring atoms...
3918 #
3919 sub _GetLargestBondRing {
3920   my($This, $Bond) = @_;
3921   my($Atom1, $Atom2);
3922 
3923   ($Atom1, $Atom2) = $Bond->GetAtoms();
3924 
3925   return $This->_GetRing($This->GetLargestEdgeCycle($Atom1->GetID(), $Atom2->GetID()));
3926 }
3927 
3928 # Get all rings an array of references to arrays containing ring atoms...
3929 #
3930 sub _GetBondRings {
3931   my($This, $Bond) = @_;
3932   my($Atom1, $Atom2);
3933 
3934   ($Atom1, $Atom2) = $Bond->GetAtoms();
3935 
3936   return $This->_GetRings($This->GetEdgeCycles($Atom1->GetID(), $Atom2->GetID()));
3937 }
3938 
3939 # Get odd size rings an array of references to arrays containing ring atoms...
3940 #
3941 sub _GetBondRingsWithOddSize {
3942   my($This, $Bond) = @_;
3943   my($Atom1, $Atom2);
3944 
3945   ($Atom1, $Atom2) = $Bond->GetAtoms();
3946 
3947   return $This->_GetRings($This->GetEdgeCyclesWithOddSize($Atom1->GetID(), $Atom2->GetID()));
3948 }
3949 
3950 # Get even size rings an array of references to arrays containing ring atoms...
3951 #
3952 sub _GetBondRingsWithEvenSize {
3953   my($This, $Bond) = @_;
3954   my($Atom1, $Atom2);
3955 
3956   ($Atom1, $Atom2) = $Bond->GetAtoms();
3957 
3958   return $This->_GetRings($This->GetEdgeCyclesWithEvenSize($Atom1->GetID(), $Atom2->GetID()));
3959 }
3960 
3961 # Get rings with specified size  an array of references to arrays containing ring atoms...
3962 #
3963 sub _GetBondRingsWithSize {
3964   my($This, $Bond, $RingSize) = @_;
3965   my($Atom1, $Atom2);
3966 
3967   ($Atom1, $Atom2) = $Bond->GetAtoms();
3968 
3969   return $This->_GetRings($This->GetEdgeCyclesWithSize($Atom1->GetID(), $Atom2->GetID(), $RingSize));
3970 }
3971 
3972 # Get rings with size less than specfied size as an array of references to arrays containing ring atoms...
3973 #
3974 sub _GetBondRingsWithSizeLessThan {
3975   my($This, $Bond, $RingSize) = @_;
3976   my($Atom1, $Atom2);
3977 
3978   ($Atom1, $Atom2) = $Bond->GetAtoms();
3979 
3980   return $This->_GetRings($This->GetEdgeCyclesWithSizeLessThan($Atom1->GetID(), $Atom2->GetID(), $RingSize));
3981 }
3982 
3983 # Get rings with size less than specfied size as an array of references to arrays containing ring atoms...
3984 #
3985 sub _GetBondRingsWithSizeGreaterThan {
3986   my($This, $Bond, $RingSize) = @_;
3987   my($Atom1, $Atom2);
3988 
3989   ($Atom1, $Atom2) = $Bond->GetAtoms();
3990 
3991   return $This->_GetRings($This->GetEdgeCyclesWithSizeGreaterThan($Atom1->GetID(), $Atom2->GetID(), $RingSize));
3992 }
3993 
3994 
3995 # Get atom paths starting from a specified atom as a reference to an array containing references
3996 # to arrays with path atoms.
3997 #
3998 # Path atoms atoms correspond to to all possible paths for specified atom in molecule with length
3999 # upto a specified length and sharing of bonds in paths traversed. By default, rings are
4000 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4001 #
4002 # Note:
4003 #    . For molecule without any rings, this method returns the same set of atom paths
4004 #      as GetAtomPathsStartingAtWithLengthUpto method.
4005 #
4006 sub GetAllAtomPathsStartingAtWithLengthUpto {
4007   my($This, $StartAtom, $Length, $AllowCycles) = @_;
4008 
4009   return $This->_GetAtomPathsStartingAt('AllAtomPathsWithLengthUpto', $StartAtom, $Length, $AllowCycles);
4010 }
4011 
4012 # Get atom paths starting from a specified atom as a reference to an array containing references
4013 # to arrays with path atoms.
4014 #
4015 # Path atoms atoms correspond to to all possible paths for specified atom in molecule with
4016 # specified length and sharing of bonds in paths traversed. By default, rings are
4017 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4018 #
4019 # Note:
4020 #    . For molecule without any rings, this method returns the same set of atom paths
4021 #      as GetAtomPathsStartingAtWithLengthUpto method.
4022 #
4023 sub GetAllAtomPathsStartingAtWithLength {
4024   my($This, $StartAtom, $Length, $AllowCycles) = @_;
4025 
4026   return $This->_GetAtomPathsStartingAt('AllAtomPathsWithLength', $StartAtom, $Length, $AllowCycles);
4027 }
4028 
4029 # Get atom paths starting from a specified atom as a reference to an array containing references
4030 # to arrays with path atoms.
4031 #
4032 # Path atoms atoms correspond to to all possible paths for specified atom in molecule with all
4033 # possible lengths and sharing of bonds in paths traversed. By default, rings are
4034 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4035 #
4036 # Note:
4037 #    . For molecule without any rings, this method returns the same set of atom paths
4038 #      as GetAtomPathsStartingAt method.
4039 #
4040 sub GetAllAtomPathsStartingAt {
4041   my($This, $StartAtom, $AllowCycles) = @_;
4042 
4043   return $This->_GetAtomPathsStartingAt('AllAtomPathsWithAllLengths', $StartAtom, undef, $AllowCycles);
4044 }
4045 
4046 # Get atom paths starting from a specified atom as a reference to an array containing references
4047 # to arrays with path atoms.
4048 #
4049 # Path atoms atoms correspond to to all possible paths for specified atom in molecule with length
4050 # upto a specified length and no sharing of bonds in paths traversed. By default, rings are
4051 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4052 #
4053 sub GetAtomPathsStartingAtWithLengthUpto {
4054   my($This, $StartAtom, $Length, $AllowCycles) = @_;
4055 
4056   return $This->_GetAtomPathsStartingAt('AtomPathsWithLengthUpto', $StartAtom, $Length, $AllowCycles);
4057 }
4058 
4059 # Get atom paths starting from a specified atom as a reference to an array containing references
4060 # to arrays with path atoms.
4061 #
4062 # Path atoms atoms correspond to to all possible paths for specified atom in molecule with
4063 # specified length and no sharing of bonds in paths traversed. By default, rings are
4064 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4065 #
4066 sub GetAtomPathsStartingAtWithLength {
4067   my($This, $StartAtom, $Length, $AllowCycles) = @_;
4068 
4069   return $This->_GetAtomPathsStartingAt('AtomPathsWithLength', $StartAtom, $Length, $AllowCycles);
4070 }
4071 
4072 # Get atom paths starting from a specified atom as a reference to an array containing references
4073 # to arrays with path atoms.
4074 #
4075 # Path atoms atoms correspond to to all possible paths for specified atom in molecule with all
4076 # possible lengths and no sharing of bonds in paths traversed. By default, rings are
4077 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4078 #
4079 #
4080 sub GetAtomPathsStartingAt {
4081   my($This, $StartAtom, $AllowCycles) = @_;
4082 
4083   return $This->_GetAtomPathsStartingAt('AtomPathsWithAllLengths', $StartAtom, undef, $AllowCycles);
4084 }
4085 
4086 # Get atom paths as an array containing references to arrays with path atoms...
4087 #
4088 sub _GetAtomPathsStartingAt {
4089   my($This, $Mode, $StartAtom, $Length, $AllowCycles) = @_;
4090   my(@AtomPaths);
4091 
4092   @AtomPaths = ();
4093   if (!defined $StartAtom) {
4094     carp "Warning: ${ClassName}->_GetAtomPathsStartingAt: No atom paths retrieved: Start atom is not defined...";
4095     return @AtomPaths;
4096   }
4097   if (!$This->HasAtom($StartAtom)) {
4098     carp "Warning: ${ClassName}->_GetAtomPathsStartingAt: No atom paths retrieved: Start atom doesn't exist...";
4099     return @AtomPaths;
4100   }
4101   my($StartAtomID, @Paths);
4102 
4103   $StartAtomID = $StartAtom->GetID();
4104   @Paths = ();
4105 
4106   # Collect appropriate atom paths...
4107   MODE: {
4108     if ($Mode =~ /^AtomPathsWithLengthUpto$/i) { @Paths = $This->GetPathsStartingAtWithLengthUpto($StartAtomID, $Length, $AllowCycles); last MODE; }
4109     if ($Mode =~ /^AtomPathsWithLength$/i) { @Paths = $This->GetPathsStartingAtWithLength($StartAtomID, $Length, $AllowCycles); last MODE; }
4110     if ($Mode =~ /^AtomPathsWithAllLengths$/i) { @Paths = $This->GetPathsStartingAt($StartAtomID, $AllowCycles); last MODE; }
4111 
4112     if ($Mode =~ /^AllAtomPathsWithLengthUpto$/i) { @Paths = $This->GetAllPathsStartingAtWithLengthUpto($StartAtomID, $Length, $AllowCycles); last MODE; }
4113     if ($Mode =~ /^AllAtomPathsWithLength$/i) { @Paths = $This->GetAllPathsStartingAtWithLength($StartAtomID, $Length, $AllowCycles); last MODE; }
4114     if ($Mode =~ /^AllAtomPathsWithAllLengths$/i) { @Paths = $This->GetAllPathsStartingAt($StartAtomID, $AllowCycles); last MODE; }
4115 
4116     print "Warn: ${ClassName}->_GetAtomPathsStartingAt: No atom paths retrieved: Mode, $Mode, is not supported...";
4117     return @AtomPaths;
4118   }
4119   return $This->_GetAtomPathsFromPaths(\@Paths);
4120 }
4121 
4122 # Get atom paths for all atoms as a reference to an array containing references to arrays with
4123 # path atoms.
4124 #
4125 # Path atoms correspond to to all possible paths for each atom in molecule with length
4126 # upto a specified length and sharing of bonds in paths traversed. By default, rings are
4127 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4128 #
4129 # Notes:
4130 #    . For molecule without any rings, this method returns the same set of atom paths
4131 #      as GetAtomPathsWithLengthUpto method.
4132 #
4133 sub GetAllAtomPathsWithLengthUpto {
4134   my($This, $Length, $AllowCycles) = @_;
4135 
4136   return $This->_GetAtomPaths('AllAtomPathsWithLengthUpto', $Length, $AllowCycles);
4137 }
4138 
4139 # Get atom paths for all atoms as a reference to an array containing references to arrays with
4140 # path atoms.
4141 #
4142 # Path atoms correspond to to all possible paths for each atom in molecule with
4143 # a specified length and sharing of bonds in paths traversed. By default, rings are
4144 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4145 #
4146 # Notes:
4147 #    . For molecule without any rings, this method returns the same set of atom paths
4148 #      as GetAtomPathsWithLengthUpto method.
4149 #
4150 sub GetAllAtomPathsWithLength {
4151   my($This, $Length, $AllowCycles) = @_;
4152 
4153   return $This->_GetAtomPaths('AllAtomPathsWithLength', $Length, $AllowCycles);
4154 }
4155 
4156 # Get atom paths for all atoms as a reference to an array containing references to arrays with
4157 # path atoms.
4158 #
4159 # Path atoms correspond to to all possible paths for each atom in molecule with all
4160 # possible lengths and sharing of bonds in paths traversed. By default, rings are
4161 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4162 #
4163 # Notes:
4164 #    . For molecule without any rings, this method returns the same set of atom paths
4165 #      as GetAtomPaths method.
4166 #
4167 sub GetAllAtomPaths {
4168   my($This, $AllowCycles) = @_;
4169 
4170   return $This->_GetAtomPaths('AllAtomPathsWithAllLengths', undef, $AllowCycles);
4171 }
4172 
4173 # Get atom paths for all atoms as a reference to an array containing references to arrays with
4174 # path atoms.
4175 #
4176 # Path atoms correspond to to all possible paths for each atom in molecule with length
4177 # upto a specified length and no sharing of bonds in paths traversed. By default, rings are
4178 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4179 #
4180 sub GetAtomPathsWithLengthUpto {
4181   my($This, $Length, $AllowCycles) = @_;
4182 
4183   return $This->_GetAtomPaths('AtomPathsWithLengthUpto', $Length, $AllowCycles);
4184 }
4185 
4186 # Get atom paths for all atoms as a reference to an array containing references to arrays with
4187 # path atoms.
4188 #
4189 # Path atoms correspond to to all possible paths for each atom in molecule with
4190 # a specified length and no sharing of bonds in paths traversed. By default, rings are
4191 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4192 #
4193 sub GetAtomPathsWithLength {
4194   my($This, $Length, $AllowCycles) = @_;
4195 
4196   return $This->_GetAtomPaths('AtomPathsWithLength', $Length, $AllowCycles);
4197 }
4198 
4199 
4200 # Get atom paths for all atoms as a reference to an array containing references to arrays with
4201 # path atoms.
4202 #
4203 # Path atoms correspond to to all possible paths for each atom in molecule with all
4204 # possible lengths and no sharing of bonds in paths traversed. By default, rings are
4205 # included in paths. A path containing a ring is terminated at an atom completing the ring.
4206 #
4207 sub GetAtomPaths {
4208   my($This, $AllowCycles) = @_;
4209 
4210   return $This->_GetAtomPaths('AtomPathsWithAllLengths', undef, $AllowCycles);
4211 }
4212 
4213 # Get atom paths for all atoms as a reference to an array containing references to arrays with
4214 # path atoms.
4215 #
4216 sub _GetAtomPaths {
4217   my($This, $Mode, $Length, $AllowCycles) = @_;
4218   my($PathsRef, @AtomPaths);
4219 
4220   @AtomPaths = ();
4221   # Collect appropriate atom paths...
4222   MODE: {
4223     if ($Mode =~ /^AtomPathsWithLengthUpto$/i) { $PathsRef = $This->GetPathsWithLengthUpto($Length, $AllowCycles); last MODE; }
4224     if ($Mode =~ /^AtomPathsWithLength$/i) { $PathsRef = $This->GetPathsWithLength($Length, $AllowCycles); last MODE; }
4225     if ($Mode =~ /^AtomPathsWithAllLengths$/i) { $PathsRef = $This->GetPaths($AllowCycles); last MODE; }
4226 
4227     if ($Mode =~ /^AllAtomPathsWithLengthUpto$/i) { $PathsRef = $This->GetAllPathsWithLengthUpto($Length, $AllowCycles); last MODE; }
4228     if ($Mode =~ /^AllAtomPathsWithLength$/i) { $PathsRef = $This->GetAllPathsWithLength($Length, $AllowCycles); last MODE; }
4229     if ($Mode =~ /^AllAtomPathsWithAllLengths$/i) { $PathsRef = $This->GetAllPaths($AllowCycles); last MODE; }
4230 
4231     print "Warn: ${ClassName}->_GetAtomPaths: No atom paths retrieved: Mode, $Mode, is not supported...";
4232     return \@AtomPaths;
4233   }
4234   return $This->_GetAtomPathsFromPaths($PathsRef);
4235 }
4236 
4237 # Get atom paths as an array reference containing references to arrays with path atoms...
4238 #
4239 sub _GetAtomPathsFromPaths {
4240   my($This, $PathsRef) = @_;
4241   my($Path, @AtomPaths);
4242 
4243   @AtomPaths = ();
4244   if (!defined $PathsRef) {
4245     return \@AtomPaths;
4246   }
4247   if (!@{$PathsRef}) {
4248     # Return an empty atom paths list...
4249     return \@AtomPaths;
4250   }
4251   for $Path (@{$PathsRef}) {
4252     my(@PathAtoms);
4253     @PathAtoms = ();
4254     @PathAtoms = $This->_GetAtomPathFromPath($Path);
4255 
4256     push @AtomPaths, \@PathAtoms;
4257   }
4258   return \@AtomPaths;
4259 }
4260 
4261 # Generate an array of bond objects for an array of path atoms and return an array
4262 # of bond objects...
4263 #
4264 sub GetAtomPathBonds {
4265   my($This, @PathAtoms) = @_;
4266   my(@Bonds);
4267 
4268   if (!@PathAtoms) {
4269     # Return an empty ring bonds list...
4270     return @Bonds;
4271   }
4272   my(@PathAtomIDs);
4273 
4274   @PathAtomIDs = ();
4275   @PathAtomIDs = $This->_GetAtomsIDsFromAtoms(@PathAtoms);
4276 
4277   return $This->_GetPathBonds(@PathAtomIDs);
4278 }
4279 
4280 # Map atom IDs in path to atoms and return a reference to an array containing ring atoms...
4281 #
4282 sub _GetAtomPathFromPath {
4283   my($This, $Path) = @_;
4284   my(@PathAtoms);
4285 
4286   @PathAtoms = ();
4287   if (!defined $Path) {
4288     # Return an empty atoms list...
4289     return @PathAtoms;
4290   }
4291 
4292   return $This->_GetPathAtoms($Path);
4293 }
4294 
4295 # Get atom paths between two specified atoms as a reference to an array containing references
4296 # to arrays with path atoms. For molecules with rings, atom paths array contains may contain
4297 # two paths.
4298 #
4299 sub GetAtomPathsBetween {
4300   my($This, $StartAtom, $EndAtom) = @_;
4301   my(@AtomPaths);
4302 
4303   @AtomPaths = ();
4304   if (!(defined($StartAtom) && $This->HasAtom($StartAtom))) {
4305     carp "Warning: ${ClassName}->_GetAtomPathsBetween: No atom paths retrieved: Start atom is not defined  or it doesn't exist...";
4306     return @AtomPaths;
4307   }
4308   if (!(defined($EndAtom) && $This->HasAtom($EndAtom))) {
4309     carp "Warning: ${ClassName}->_GetAtomPathsBetween: No atom paths retrieved: End atom is not defined  or it doesn't exist...";
4310     return @AtomPaths;
4311   }
4312   return $This->_GetAtomPathsBetween($StartAtom, $EndAtom);
4313 }
4314 
4315 # Get atom paths between two specified atoms as a reference to an array containing references
4316 # to arrays with path atoms.
4317 #
4318 sub _GetAtomPathsBetween {
4319   my($This, $StartAtom, $EndAtom) = @_;
4320   my($StartAtomID, $EndAtomID, @Paths);
4321 
4322   $StartAtomID = $StartAtom->GetID();
4323   $EndAtomID = $EndAtom->GetID();
4324 
4325   @Paths = ();
4326   @Paths = $This->GetPathsBetween($StartAtomID, $EndAtomID);
4327 
4328   return $This->_GetAtomPathsFromPaths(\@Paths);
4329 }
4330 
4331 # Get atom neighborhoods around a specified atom as an array containing references
4332 # to arrays with neighborhood atoms at different radii upto specified radius...
4333 #
4334 sub GetAtomNeighborhoodsWithRadiusUpto {
4335   my($This, $StartAtom, $Radius) = @_;
4336 
4337   return $This->_GetAtomNeighborhoods('RadiusUpto', $StartAtom, $Radius);
4338 }
4339 
4340 # Get atom neighborhoods around a specified atom as an array containing references
4341 # to arrays with neighborhood atoms at possible radii...
4342 #
4343 sub GetAtomNeighborhoods {
4344   my($This, $StartAtom) = @_;
4345 
4346   return $This->_GetAtomNeighborhoods('AllRadii', $StartAtom, undef);
4347 }
4348 
4349 # Get atom neighborhood around a specified atom, along with their successor connected atoms, collected
4350 # with in a specified radius as a list containing references to lists with first value corresponding to neighborhood
4351 # atom at a specific radius and second value as reference to a list containing its successor connected atoms.
4352 #
4353 # For a neighborhood atom at each radius level, the successor connected atoms correspond to the
4354 # neighborhood atoms at the next radius level. Consequently, the neighborhood atoms at the last
4355 # radius level don't contain any successor atoms which fall outside the range of specified radius.
4356 #
4357 sub GetAtomNeighborhoodsWithSuccessorAtomsAndRadiusUpto {
4358   my($This, $StartAtom, $Radius) = @_;
4359 
4360   return $This->_GetAtomNeighborhoods('WithSuccessorsAndRadiusUpto', $StartAtom, $Radius);
4361 }
4362 
4363 # Get atom neighborhood around a specified atom, along with their successor connected atoms, collected
4364 # at all radii as a list containing references to lists with first value corresponding to neighborhood
4365 # atom at a specific radius and second value as reference to a list containing its successor connected atoms.
4366 #
4367 # For a neighborhood atom at each radius level, the successor connected atoms correspond to the
4368 # neighborhood atoms at the next radius level. Consequently, the neighborhood atoms at the last
4369 # radius level don't contain any successor atoms which fall outside the range of specified radius.
4370 #
4371 #
4372 sub GetAtomNeighborhoodsWithSuccessorAtoms {
4373   my($This, $StartAtom) = @_;
4374 
4375   return $This->_GetAtomNeighborhoods('WithSuccessorsAndAllRadii', $StartAtom, undef);
4376 }
4377 
4378 # Get atom neighborhoods...
4379 #
4380 sub _GetAtomNeighborhoods {
4381   my($This, $Mode, $StartAtom, $Radius) = @_;
4382   my(@AtomNeighborhoods);
4383 
4384   @AtomNeighborhoods = ();
4385 
4386   if (!(defined($StartAtom) && $This->HasAtom($StartAtom))) {
4387     carp "Warning: ${ClassName}->_GetAtomNeighborhoods: No atom neighborhoods retrieved: Start atom is not defined  or it doesn't exist...";
4388     return @AtomNeighborhoods;
4389   }
4390   if ($Mode =~ /^(RadiusUpto|WithSuccessorsAndRadiusUpto)$/i) {
4391     if (!(defined($Radius) && $Radius > 0)) {
4392       carp "Warning: ${ClassName}->_GetAtomNeighborhoods: No atom neighborhoods retrieved: Radius is not defined or it's <= 0 ...";
4393       return @AtomNeighborhoods;
4394     }
4395   }
4396 
4397   # Collect neighborhood atom IDs...
4398   my($StartAtomID, @NeighborhoodAtomIDs, @NeighborhoodAtomIDsWithSuccessors);
4399 
4400   @NeighborhoodAtomIDs = (); @NeighborhoodAtomIDsWithSuccessors = ();
4401   $StartAtomID = $StartAtom->GetID();
4402 
4403   MODE: {
4404     if ($Mode =~ /^RadiusUpto$/i) { @NeighborhoodAtomIDs = $This->GetNeighborhoodVerticesWithRadiusUpto($StartAtomID, $Radius); last MODE; }
4405     if ($Mode =~ /^AllRadii$/i) { @NeighborhoodAtomIDs = $This->GetNeighborhoodVertices($StartAtomID); last MODE; }
4406 
4407     if ($Mode =~ /^WithSuccessorsAndRadiusUpto$/i) { @NeighborhoodAtomIDsWithSuccessors = $This->GetNeighborhoodVerticesWithSuccessorsAndRadiusUpto($StartAtomID, $Radius); last MODE; }
4408     if ($Mode =~ /^WithSuccessorsAndAllRadii$/i) { @NeighborhoodAtomIDsWithSuccessors = $This->GetNeighborhoodVerticesWithSuccessors($StartAtomID); last MODE; }
4409 
4410     print "Warn: ${ClassName}->_GetAtomNeighborhood: No atom neighborhoods retrieved: Mode, $Mode, is not supported...";
4411     return @AtomNeighborhoods;
4412   }
4413   if ($Mode =~ /^(RadiusUpto|AllRadii)$/i) {
4414     return $This->_GetNeighborhoodAtomsFromAtomIDs(\@NeighborhoodAtomIDs);
4415   }
4416   elsif ($Mode =~ /^(WithSuccessorsAndRadiusUpto|WithSuccessorsAndAllRadii)$/i) {
4417     return $This->_GetNeighborhoodAtomsWithSuccessorsFromAtomIDs(\@NeighborhoodAtomIDsWithSuccessors);
4418   }
4419 
4420   return @AtomNeighborhoods;
4421 }
4422 
4423 # Map neighborhood atom IDs to atoms...
4424 #
4425 sub _GetNeighborhoodAtomsFromAtomIDs {
4426   my($This, $NeighborhoodsAtomIDsRef) = @_;
4427   my($NeighborhoodAtomIDsRef, @AtomNeighborhoods);
4428 
4429   @AtomNeighborhoods = ();
4430   for $NeighborhoodAtomIDsRef (@{$NeighborhoodsAtomIDsRef}) {
4431     my(@AtomNeighborhood);
4432 
4433     @AtomNeighborhood = ();
4434     @AtomNeighborhood = $This->_GetAtomsFromAtomIDs(@{$NeighborhoodAtomIDsRef});
4435     push @AtomNeighborhoods, \@AtomNeighborhood;
4436   }
4437   return @AtomNeighborhoods;
4438 }
4439 
4440 # Map neighborhood atom IDs with successors to atoms...
4441 #
4442 sub _GetNeighborhoodAtomsWithSuccessorsFromAtomIDs {
4443   my($This, $NeighborhoodsAtomIDsWithSuccessorsRef) = @_;
4444   my($Depth, $NeighborhoodAtomIDsWithSuccessorsRef, $NeighborhoodAtomIDWithSuccessorsRef, $NeighborhoodAtomID, $NeighborhoodAtomSuccessorsIDsRef, @AtomNeighborhoods);
4445 
4446   $Depth = 0;
4447   @AtomNeighborhoods = ();
4448 
4449   # Go over neighborhoods at each level...
4450   for $NeighborhoodAtomIDsWithSuccessorsRef (@{$NeighborhoodsAtomIDsWithSuccessorsRef}) {
4451     @{$AtomNeighborhoods[$Depth]} = ();
4452 
4453     # Go over the neighborhood atoms and their successors at a specific level..
4454     for $NeighborhoodAtomIDWithSuccessorsRef (@{$NeighborhoodAtomIDsWithSuccessorsRef}) {
4455       my($NeighborhoodAtom, @NeighborhoodAtomWithSuccessors, @NeighborhoodAtomSuccessorAtoms);
4456 
4457       @NeighborhoodAtomWithSuccessors = (); @NeighborhoodAtomSuccessorAtoms = ();
4458       ($NeighborhoodAtomID, $NeighborhoodAtomSuccessorsIDsRef) = @{$NeighborhoodAtomIDWithSuccessorsRef};
4459 
4460       # Map atom IDs to atoms...
4461       $NeighborhoodAtom = $This->_GetAtomFromAtomID($NeighborhoodAtomID);
4462       if (@{$NeighborhoodAtomSuccessorsIDsRef}) {
4463         @NeighborhoodAtomSuccessorAtoms = $This->_GetAtomsFromAtomIDs(@{$NeighborhoodAtomSuccessorsIDsRef});
4464       }
4465 
4466       # Store an atom and its successors at each level in an array...
4467       push @NeighborhoodAtomWithSuccessors, ($NeighborhoodAtom, \@NeighborhoodAtomSuccessorAtoms);
4468 
4469       push @{$AtomNeighborhoods[$Depth]} , \@NeighborhoodAtomWithSuccessors;
4470     }
4471       $Depth++;
4472   }
4473   return @AtomNeighborhoods;
4474 }
4475 
4476 # Get next object ID...
4477 sub _GetNewObjectID {
4478   $ObjectID++;
4479   return $ObjectID;
4480 }
4481 
4482 # Is aromatic property set for the molecule?
4483 sub IsAromatic {
4484   my($This) = @_;
4485   my($Aromatic);
4486 
4487   $Aromatic = $This->GetAromatic();
4488 
4489   return (defined($Aromatic) && $Aromatic) ? 1 : 0;
4490 }
4491 
4492 # Does molecule contains any atoms with non-zero Z coordiantes?
4493 sub IsThreeDimensional {
4494   my($This) = @_;
4495   my($Atom, @Atoms);
4496 
4497   @Atoms = $This->GetAtoms();
4498   ATOM: for $Atom (@Atoms) {
4499       if ($Atom->GetZ() != 0) {
4500         return 1;
4501       }
4502   }
4503   return 0;
4504 }
4505 
4506 # Does molecule contains any atoms with non-zero X or Y coordinates
4507 # and only zero Z-coordinates?
4508 sub IsTwoDimensional {
4509   my($This) = @_;
4510   my($Atom, @Atoms);
4511 
4512   @Atoms = $This->GetAtoms();
4513   ATOM: for $Atom (@Atoms) {
4514       if ($Atom->GetZ() != 0) {
4515         return 0;
4516       }
4517       if ($Atom->GetX() != 0 || $Atom->GetY() != 0) {
4518         return 1;
4519       }
4520   }
4521   return 0;
4522 }
4523 
4524 # Get dimensionality of the molecule using one of the following two methods:
4525 #   . Using explicitly set Dimensionality
4526 #   . Going over atomic coordinates
4527 #
4528 # The valid dimensionality values are:
4529 #   . 3D - Three dimensional: One of X, Y or Z coordinate is non-zero
4530 #   . 2D - Two dimensional: One of X or Y coordinate is non-zero; All Z coordinates are zero
4531 #   . 0D - Zero dimensional: All atomic coordinates are zero
4532 #
4533 sub GetDimensionality {
4534   my($This) = @_;
4535 
4536   # Is Dimensionality property explicitly set?
4537   if ($This->HasProperty('Dimensionality')) {
4538     return $This->GetProperty('Dimensionality');
4539   }
4540   my($Atom, @Atoms);
4541 
4542   @Atoms = $This->GetAtoms();
4543   ATOM: for $Atom (@Atoms) {
4544       if ($Atom->GetZ() != 0) {
4545         return '3D';
4546       }
4547       if ($Atom->GetX() != 0 || $Atom->GetY() != 0) {
4548         return '2D';
4549       }
4550   }
4551   return '0D';
4552 }
4553 
4554 # Is it a molecule object?
4555 sub IsMolecule ($) {
4556   my($Object) = @_;
4557 
4558   return _IsMolecule($Object);
4559 }
4560 
4561 # Return a string containing vertices, edges and other properties...
4562 sub StringifyMolecule {
4563   my($This) = @_;
4564   my($MoleculeString, $ID, $Name, $NumOfAtoms, $NumOfBonds, $MolecularFormula, $NumOfRings, $MolecularWeight, $ExactMass, $FormalCharge, $SpinMultiplicity, $FreeRadicalElectrons, $Charge, $ElementsRef, $ElementsCompositionRef, $ElementalComposition);
4565 
4566   $ID = $This->GetID();
4567   $Name = $This->GetName();
4568   $NumOfAtoms = $This->GetNumOfAtoms();
4569   $NumOfBonds = $This->GetNumOfBonds();
4570 
4571   $NumOfRings = $This->GetNumOfRings();
4572   if (!defined $NumOfRings) {
4573     $NumOfRings = 'undefined';
4574   }
4575 
4576   $MolecularFormula = $This->GetMolecularFormula();
4577 
4578   $MolecularWeight = $This->GetMolecularWeight();
4579   $MolecularWeight = round($MolecularWeight, 4) + 0;
4580 
4581   $ExactMass = $This->GetExactMass();
4582   $ExactMass = round($ExactMass, 4) + 0;
4583 
4584   $FormalCharge = $This->GetFormalCharge();
4585   $Charge = $This->GetCharge();
4586 
4587   $SpinMultiplicity = $This->GetSpinMultiplicity();
4588   $FreeRadicalElectrons = $This->GetFreeRadicalElectrons();
4589 
4590   ($ElementsRef, $ElementsCompositionRef) = $This->GetElementalComposition();
4591   $ElementalComposition = 'None';
4592   if (defined($ElementsRef) && @{$ElementsRef}) {
4593     $ElementalComposition = "[ " . FormatElementalCompositionInformation($ElementsRef, $ElementsCompositionRef) . " ]";
4594   }
4595 
4596   $MoleculeString = "Molecule: ID: $ID; Name: \"$Name\"; NumOfAtoms: $NumOfAtoms; NumOfBonds: $NumOfBonds; NumOfRings: $NumOfRings; MolecularFormula: $MolecularFormula; MolecularWeight: $MolecularWeight; ExactMass: $ExactMass; FormalCharge: $FormalCharge; Charge: $Charge; SpinMultiplicity: $SpinMultiplicity; FreeRadicalElectrons: $FreeRadicalElectrons; ElementalComposition: $ElementalComposition";
4597 
4598   return $MoleculeString;
4599 }
4600 
4601 # Load appropriate atom data files from <MayaChemTools>/lib directory used by various
4602 # object methods in the current class...
4603 #
4604 sub _LoadMoleculeClassData {
4605   my($MayaChemToolsLibDir);
4606 
4607   $MayaChemToolsLibDir = GetMayaChemToolsLibDirName();
4608 
4609   # Load and process data for aromaticity models...
4610   _LoadAromaticityModelsData($MayaChemToolsLibDir);
4611   _ProcessAromaticityModelsData();
4612 }
4613 
4614 #
4615 # Load data for supported aromaticity models...
4616 #
4617 sub _LoadAromaticityModelsData {
4618   my($MayaChemToolsLibDir) = @_;
4619   my($DataFile, $Index, $InDelim, $Line, $NumOfCols, $ParameterName, $ParameterValue, $ModelName, @ColLabels, @LineWords, %ParameterNames, %ColIndexToModelName, %SupportedParameterNames);
4620 
4621   %AromaticityModelsDataMap = ();
4622   %CanonicalAromaticityModelNamesMap = ();
4623 
4624   # File format:
4625   #
4626   # "ParameterName","MDLAromaticityModel","TriposAromaticityModel","MMFFAromaticityModel","ChemAxonBasicAromaticityModel","ChemAxonGeneralAromaticityModel","DaylightAromaticityModel","MayaChemToolsAromaticityModel"
4627   # "AllowHeteroRingAtoms","No","No","Yes","Yes","Yes","Yes","Yes"
4628   #
4629   $DataFile = $MayaChemToolsLibDir . "/data/AromaticityModelsData.csv";
4630   if (! -e "$DataFile") {
4631     croak "Error: ${ClassName}::_LoadAromaticityModelsData: MayaChemTools package file, $DataFile, is missing: Possible installation problems...";
4632   }
4633 
4634   # Setup a list of currently supported aromaticity parameters...
4635   #
4636   my(@KnownNames);
4637   @KnownNames = qw(AllowHeteroRingAtoms HeteroRingAtomsList AllowExocyclicDoubleBonds AllowHomoNuclearExocyclicDoubleBonds AllowElectronegativeRingAtomExocyclicDoubleBonds AllowRingAtomFormalCharge AllowHeteroRingAtomFormalCharge MinimumRingSize);
4638 
4639   %SupportedParameterNames = ();
4640   for $ParameterName (@KnownNames) {
4641     $SupportedParameterNames{$ParameterName} = $ParameterName;
4642   }
4643 
4644   $InDelim = "\,";
4645   open DATAFILE, "$DataFile" or croak "Couldn't open $DataFile: $! ...";
4646 
4647   # Skip lines up to column labels...
4648   LINE: while ($Line = GetTextLine(\*DATAFILE)) {
4649     if ($Line !~ /^#/) {
4650       last LINE;
4651     }
4652   }
4653   @ColLabels= quotewords($InDelim, 0, $Line);
4654   $NumOfCols = @ColLabels;
4655 
4656   %ColIndexToModelName = ();
4657 
4658   # Process names of aromaticity models...
4659   for $Index (1 .. $#ColLabels) {
4660     $ModelName = $ColLabels[$Index];
4661     $ModelName =~ s/ //g;
4662 
4663     if (exists $AromaticityModelsDataMap{$ModelName}) {
4664       croak "Error: ${ClassName}::_LoadAromaticityModelsData: The aromaticity model name, $ModelName, in $DataFile has already exists.\nLine: $Line...";
4665     }
4666     %{$AromaticityModelsDataMap{$ModelName}} = ();
4667 
4668     # Cannonicalize aromatic model name by converting into all lowercase...
4669     $CanonicalAromaticityModelNamesMap{lc($ModelName)} = $ModelName;
4670 
4671     $ColIndexToModelName{$Index} = $ModelName;
4672   }
4673 
4674   # Process paramater name and their values for specified aromaticity models...
4675   #
4676   %ParameterNames = ();
4677   LINE: while ($Line = GetTextLine(\*DATAFILE)) {
4678     if ($Line =~ /^#/) {
4679       next LINE;
4680     }
4681     @LineWords = ();
4682     @LineWords = quotewords($InDelim, 0, $Line);
4683     if (@LineWords != $NumOfCols) {
4684       croak "Error: ${ClassName}::_LoadAromaticityModelsData: The number of data fields, @LineWords, in $DataFile must be $NumOfCols.\nLine: $Line...";
4685     }
4686 
4687     # Process parameter name and values for aromaticity models...
4688     #
4689     $ParameterName = $LineWords[0];
4690 
4691     if (!exists $SupportedParameterNames{$ParameterName}) {
4692       carp "Warning: ${ClassName}::_LoadAromaticityModelsData: The current release of MayaChemTools doesn't support aromaticity model parameter name, $ParameterName, specified in $DataFile. It would be ignore during aromaticity detection.\nLine: $Line...";
4693     }
4694 
4695     if (exists $ParameterNames{$ParameterName}) {
4696       carp "Warning: ${ClassName}::_LoadAromaticityModelsData: Ignoring aromaticity model data for parameter name, $ParameterName, in $DataFile. It has already been loaded.\nLine: $Line...";
4697       next LINE;
4698     }
4699     $ParameterNames{$ParameterName} = $ParameterName;
4700 
4701     for $Index (1 .. $#LineWords) {
4702       $ModelName = $ColIndexToModelName{$Index};
4703       $ParameterValue = $LineWords[$Index];
4704       $AromaticityModelsDataMap{$ModelName}{$ParameterName} = $ParameterValue;
4705     }
4706   }
4707   close DATAFILE;
4708 }
4709 
4710 # Process already loaded aromaticity model data...
4711 #
4712 sub _ProcessAromaticityModelsData {
4713   my($ParameterName, $ParameterValue, $ModelName, $NewParameterValue);
4714 
4715   for $ModelName (keys %AromaticityModelsDataMap) {
4716     for $ParameterName (keys %{$AromaticityModelsDataMap{$ModelName}}) {
4717       $ParameterValue = $AromaticityModelsDataMap{$ModelName}{$ParameterName};
4718       $ParameterValue =~ s/ //g;
4719 
4720       VALUE: {
4721         if ($ParameterValue =~ /^Yes$/i) {
4722           $NewParameterValue = 1;
4723           last VALUE;
4724         }
4725         if ($ParameterValue =~ /^(NA|No)$/i) {
4726           $NewParameterValue = 0;
4727           last VALUE;
4728         }
4729         if ($ParameterValue =~ /^None$/i) {
4730           $NewParameterValue = '';
4731           last VALUE;
4732         }
4733         $NewParameterValue = $ParameterValue;
4734       }
4735       $AromaticityModelsDataMap{$ModelName}{$ParameterName} = $NewParameterValue;
4736 
4737       if ($ParameterName =~ /List/i) {
4738         # Setup a new parameter conatining a reference to a hash for the specified values...
4739         my($DataMapRefName, $DataValue, %DataMap);
4740 
4741         $DataMapRefName = "${ParameterName}MapRef";
4742 
4743         %DataMap = ();
4744         for $DataValue (split /\,/, $NewParameterValue) {
4745           $DataMap{$DataValue} = $DataValue;
4746         }
4747         $AromaticityModelsDataMap{$ModelName}{$DataMapRefName} = \%DataMap;
4748       }
4749     }
4750   }
4751 }
4752 
4753 # Is it a molecule object?
4754 sub _IsMolecule {
4755   my($Object) = @_;
4756 
4757   return (Scalar::Util::blessed($Object) && $Object->isa($ClassName)) ? 1 : 0;
4758 }
4759