MayaChemTools

   1 package Graph::PathGraph;
   2 #
   3 # File: PathGraph.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 Graph;
  32 use Graph::Path;
  33 
  34 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
  35 
  36 @ISA = qw(Graph Exporter);
  37 @EXPORT = qw(IsPathGraph);
  38 @EXPORT_OK = qw();
  39 
  40 %EXPORT_TAGS = (all  => [@EXPORT, @EXPORT_OK]);
  41 
  42 # Setup class variables...
  43 my($ClassName, $PathsPropertyName, $CyclicPathsPropertyName);
  44 _InitializeClass();
  45 
  46 # Overload Perl functions...
  47 use overload '""' => 'StringifyPathGraph';
  48 
  49 # Class constructor...
  50 sub new {
  51   my($Class, $Graph) = @_;
  52 
  53   # Initialize object...
  54   my $This = $Class->SUPER::new();
  55   bless $This, ref($Class) || $Class;
  56   $This->_InitializePathGraph($Graph);
  57 
  58   $This->_ConvertGraphIntoPathGraph($Graph);
  59 
  60   return $This;
  61 }
  62 
  63 # Initialize object data...
  64 sub _InitializePathGraph {
  65   my($This, $Graph) = @_;
  66 
  67   if (!(defined($Graph) && Graph::IsGraph($Graph))) {
  68     croak "Error: ${ClassName}->new: PathGraph object can't be instantiated without a Graph object...";
  69   }
  70 
  71   $This->{Graph} = $Graph;
  72 
  73   # Maximum time allowed for cycles detection during collapse vertex cycles detection
  74   # methodology in seconds...
  75   $This->{MaxAllowedTime} = 30;
  76 
  77   # Starting time for cycles detection during collapse vertex cycles detection
  78   # methodology...
  79   $This->{StartTime} = time;
  80 
  81   return $This;
  82 }
  83 
  84 # Initialize class ...
  85 sub _InitializeClass {
  86   #Class name...
  87   $ClassName = __PACKAGE__;
  88 
  89   # Path edge property name...
  90   $PathsPropertyName = 'Paths';
  91 
  92   # Cyclic path vertex property name...
  93   $CyclicPathsPropertyName = 'CyclicPaths';
  94 }
  95 
  96 # Convert graph into a path graph...
  97 #
  98 sub _ConvertGraphIntoPathGraph {
  99   my($This, $Graph) = @_;
 100 
 101   # Copy graph vertices and edges without any associated properties data
 102   # from Graph to This: Graph properties data is available using Graph object reference
 103   # store in This object...
 104   #
 105   $Graph->CopyVerticesAndEdges($This);
 106 
 107   # . Attach Path property to each edge...
 108   #
 109   my($Index, $VertexID1, $VertexID2, $Path, @EdgesVertexIDs);
 110 
 111   @EdgesVertexIDs = ();
 112   @EdgesVertexIDs = $This->GetEdges();
 113   for ($Index = 0; $Index < $#EdgesVertexIDs; $Index += 2) {
 114     $VertexID1 = $EdgesVertexIDs[$Index]; $VertexID2 = $EdgesVertexIDs[$Index + 1];
 115     $Path = new Graph::Path($VertexID1, $VertexID2);
 116     my(@Paths) = ();
 117     push @Paths, $Path;
 118     $This->SetEdgeProperty($PathsPropertyName, \@Paths, $VertexID1, $VertexID2);
 119   }
 120   return $This;
 121 }
 122 
 123 # Collapse paths around a specified vertex by updating paths around the vertex
 124 # and adding any resulting cyclic paths to vertices attached to specified vertex.
 125 #
 126 # Notes:
 127 #   . Path object references are stored as a list attached to Paths property on edges.
 128 #     Usage of list allows multiple paths attached to the egde between a pair of vertices;
 129 #     Graph doesn't support multiple egdes between a pair of vertices.
 130 #
 131 #   . Cyclic path object references are stored as list on vertices as CyclicPaths graph property.
 132 #     List allows multiple Loop properties attached to a vertex.
 133 #
 134 #   . For topologically complex graphs containing large number of cycles, cycles detection algorithm
 135 #     [ Ref 31 ] as implemented implemented in CollapseVertexAndCollectCyclicPathsDetectCycles
 136 #     might not be able to find all the cycles in a reasonable amount of time and is designed to
 137 #     abandon cycles detection after MaxAllowedTime. Consequently, no cycles are detected
 138 #     or assigned.
 139 #
 140 sub CollapseVertexAndCollectCyclicPaths {
 141   my($This, $VertexID) = @_;
 142 
 143   if (!$This->HasVertex($VertexID)) {
 144     carp "Warning: ${ClassName}->CollapseVertexAndCollectCyclicPaths: Didn't collapse vertex $VertexID: Vertex $VertexID doesn't exist...";
 145     return undef;
 146   }
 147   # Collect all paths around specified VertexID by going over paths associated with its edges...
 148   my($Index, $EdgePathsRef, $EdgeVertexID1, $EdgeVertexID2, @Paths, @EdgesVertexIDs);
 149 
 150   @EdgesVertexIDs = ();
 151   @EdgesVertexIDs = $This->GetEdges($VertexID);
 152 
 153   @Paths = ();
 154   for ($Index = 0; $Index < $#EdgesVertexIDs; $Index += 2) {
 155     ($EdgeVertexID1, $EdgeVertexID2) = ($EdgesVertexIDs[$Index], $EdgesVertexIDs[$Index + 1]);
 156     $EdgePathsRef = $This->GetEdgeProperty($PathsPropertyName, $EdgeVertexID1, $EdgeVertexID2);
 157     push @Paths, @{$EdgePathsRef};
 158   }
 159 
 160   # Go over each pair of paths around the specified vertex, join paths and associate
 161   # joined path to appropriate edge...
 162   my($Index1, $Index2, $Path1, $Path2, $JoinedPath, $JoinedPathStartVertexID, $JoinedPathEndVertexID, @CommonVertices);
 163 
 164   for ($Index1 = 0; $Index1 < $#Paths; $Index1 +=1 ) {
 165     $Path1 = $Paths[$Index1];
 166 
 167     PATH2: for ($Index2 = $Index1 + 1; $Index2 <= $#Paths; $Index2 +=1 ) {
 168       $Path2 = $Paths[$Index2];
 169 
 170       # For JoinedPath to be valid cycle, Path1 and Path2 must have exactly two vertices in common.
 171       # Otherwise, joined path contains duplicate vertices besides the terminal vertices and
 172       # indicates a path from a different direction.
 173       #
 174       # For paths leading to cycles, it only makes sense to join paths with only one common vertex;
 175       # otherwise, it wouldn't lead to a cycle and can be ignored.
 176       #
 177       @CommonVertices = $Path1->GetCommonVertices($Path2);
 178       if (!(@CommonVertices <= 2 && ($CommonVertices[0] == $VertexID || $CommonVertices[1] == $VertexID))) {
 179         next PATH2;
 180       }
 181 
 182       $JoinedPath = $Path1->JoinAtVertex($Path2, $VertexID);
 183       ($JoinedPathStartVertexID, $JoinedPathEndVertexID) = $JoinedPath->GetTerminalVertices();
 184 
 185       if (!$JoinedPath->IsIndependentPath()) {
 186         next PATH2;
 187       }
 188 
 189       # Decide whether to give up or keep going...
 190       if ($This->_IsTimeToGiveUpCyclesDetection()) {
 191         warn "Warning: ${ClassName}->CollapseVertexAndCollectCyclicPaths: Cycles detection algorithm [ Ref 31 ] as implemented in the current release of MayaChemTools didn't finish with in the maximum allowed time of $This->{MaxAllowedTime} seconds; Cycles detection has been abandoned...";
 192         return undef;
 193       }
 194 
 195       if ($JoinedPathStartVertexID == $JoinedPathEndVertexID) {
 196         # It's a cycle. Attach it to the graph as CylicPaths property...
 197         if ($This->HasGraphProperty($CyclicPathsPropertyName)) {
 198           my($ExistingCyclicPathsRef);
 199           $ExistingCyclicPathsRef = $This->GetGraphProperty($CyclicPathsPropertyName);
 200           push @{$ExistingCyclicPathsRef}, $JoinedPath;
 201         }
 202         else {
 203           my(@NewCyclicPaths) = ();
 204           push @NewCyclicPaths, $JoinedPath;
 205           $This->SetGraphProperty($CyclicPathsPropertyName, \@NewCyclicPaths, $JoinedPathStartVertexID);
 206         }
 207       }
 208       else {
 209         if ($This->HasEdge($JoinedPathStartVertexID, $JoinedPathEndVertexID)) {
 210           # Append to the list of exisiting paths property of the edge...
 211           my($ExistingPathsRef);
 212           $ExistingPathsRef = $This->GetEdgeProperty($PathsPropertyName, $JoinedPathStartVertexID, $JoinedPathEndVertexID);
 213           push @{$ExistingPathsRef}, $JoinedPath;
 214         }
 215         else {
 216           # Create a new edge and associate path property...
 217           my(@NewPaths) = ();
 218           push @NewPaths, $JoinedPath;
 219           $This->AddEdge($JoinedPathStartVertexID, $JoinedPathEndVertexID);
 220           $This->SetEdgeProperty($PathsPropertyName, \@NewPaths, $JoinedPathStartVertexID, $JoinedPathEndVertexID);
 221         }
 222       }
 223     }
 224   }
 225   $This->DeleteVertex($VertexID);
 226 
 227   return $This;
 228 }
 229 
 230 # Decide whether to give up cycles detection using collapse vertex methodology...
 231 #
 232 sub _IsTimeToGiveUpCyclesDetection {
 233   my($This) = @_;
 234 
 235   return ((time - $This->{StartTime}) > $This->{MaxAllowedTime}) ? 1 : 0;
 236 }
 237 
 238 # Delete vertices with degree less than a specifed degree...
 239 #
 240 sub DeleteVerticesWithDegreeLessThan {
 241   my($This, $Degree) = @_;
 242   my($VertexID, @VertexIDs);
 243 
 244   while (@VertexIDs = $This->GetVerticesWithDegreeLessThan($Degree)) {
 245     for $VertexID (@VertexIDs) {
 246       $This->DeleteVertex($VertexID);
 247     }
 248   }
 249   return $This;
 250 }
 251 
 252 # Get paths associated with edges...
 253 #
 254 sub GetPaths {
 255   my($This) = @_;
 256   my($PathsRef, @Paths, @PathsList);
 257 
 258   @Paths = (); @PathsList = ();
 259   @PathsList = $This->GetEdgesProperty($PathsPropertyName);
 260   for $PathsRef (@PathsList) {
 261     push @Paths, @{$PathsRef};
 262   }
 263   return wantarray ? @Paths : scalar @Paths;
 264 }
 265 
 266 # Get paths associated with edges which make a cylce...
 267 #
 268 sub GetCyclicPaths {
 269   my($This) = @_;
 270   my($PathsRef, @Paths, @PathsList);
 271 
 272   @Paths = (); @PathsList = ();
 273   @PathsList = $This->GetGraphProperty($CyclicPathsPropertyName);
 274   PATHS: for $PathsRef (@PathsList) {
 275     if (!(defined($PathsRef) && @{$PathsRef})) {
 276       next PATHS;
 277     }
 278     push @Paths, @{$PathsRef};
 279   }
 280   return wantarray ? @Paths : scalar @Paths;
 281 }
 282 
 283 # Is it a path graph object?
 284 sub IsPathGraph ($) {
 285   my($Object) = @_;
 286 
 287   return _IsPathGraph($Object);
 288 }
 289 
 290 # Return a string containg data for PathGraph object...
 291 sub StringifyPathGraph {
 292   my($This) = @_;
 293   my($PathGraphString);
 294 
 295   $PathGraphString = 'PathGraph:' . $This->StringifyVerticesAndEdges() . '; ' . $This->StringifyProperties();
 296 
 297   return $PathGraphString;
 298 }
 299 
 300 # Is it a PathGraph object?
 301 sub _IsPathGraph {
 302   my($Object) = @_;
 303 
 304   return (Scalar::Util::blessed($Object) && $Object->isa($ClassName)) ? 1 : 0;
 305 }
 306