1 package Graph::Path; 2 # 3 # File: Path.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 32 use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS); 33 34 @ISA = qw(Exporter); 35 @EXPORT = qw(); 36 @EXPORT_OK = qw(); 37 38 %EXPORT_TAGS = (all => [@EXPORT, @EXPORT_OK]); 39 40 # Setup class variables... 41 my($ClassName, $ObjectID); 42 _InitializeClass(); 43 44 # Overload Perl functions... 45 use overload '""' => 'StringifyPath', 46 47 '==' => '_PathEqualOperator', 48 'eq' => '_PathEqualOperator', 49 50 'fallback' => undef; 51 52 # Class constructor... 53 sub new { 54 my($Class, @VertexIDs) = @_; 55 56 # Initialize object... 57 my $This = {}; 58 bless $This, ref($Class) || $Class; 59 $This->_InitializePath(); 60 61 if (@VertexIDs) { $This->AddVertices(@VertexIDs); } 62 63 return $This; 64 } 65 66 # Initialize object data... 67 # 68 sub _InitializePath { 69 my($This) = @_; 70 71 @{$This->{Vertices}} = (); 72 } 73 74 # Initialize class ... 75 sub _InitializeClass { 76 #Class name... 77 $ClassName = __PACKAGE__; 78 } 79 80 # Add a vertex to path after the end vertex... 81 # 82 sub AddVertex { 83 my($This, $VertexID) = @_; 84 85 if (!defined $VertexID ) { 86 carp "Warning: ${ClassName}->AddVertex: No vertex added: Vertex ID must be specified..."; 87 return undef; 88 } 89 push @{$This->{Vertices}}, $VertexID; 90 91 return $This; 92 } 93 94 # Add vertices to the path after the end vertex... 95 # 96 sub AddVertices { 97 my($This, @VertexIDs) = @_; 98 99 if (!@VertexIDs) { 100 carp "Warning: ${ClassName}->AddVertices: No vertices added: Vertices list is empty..."; 101 return undef; 102 } 103 push @{$This->{Vertices}}, @VertexIDs; 104 105 return $This; 106 } 107 108 # Add a vertex to path after the end vertex... 109 # 110 sub PushVertex { 111 my($This, $VertexID) = @_; 112 113 return $This->AddVertex($VertexID); 114 } 115 116 # Add vertices to the path after the end vertex... 117 # 118 sub PushVertices { 119 my($This, @VertexIDs) = @_; 120 121 return $This->AddVertices(@VertexIDs); 122 } 123 124 # Remove end vertex from path... 125 # 126 sub PopVertex { 127 my($This) = @_; 128 129 if (!@{$This->{Vertices}}) { 130 carp "Warning: ${ClassName}->PopVertex: No vertex removed: Path is empty..."; 131 return undef; 132 } 133 pop @{$This->{Vertices}}; 134 135 return $This; 136 } 137 138 # Remove start vertex from path... 139 # 140 sub ShiftVertex { 141 my($This) = @_; 142 143 if (!@{$This->{Vertices}}) { 144 carp "Warning: ${ClassName}->ShiftVertex: No vertex removed: Path is empty..."; 145 return undef; 146 } 147 shift @{$This->{Vertices}}; 148 149 return $This; 150 } 151 152 # Add a vertex to path before the start vertex... 153 # 154 sub UnshiftVertex { 155 my($This, $VertexID) = @_; 156 157 if (!defined $VertexID ) { 158 carp "Warning: ${ClassName}->UnshiftVertex: No vertex added: Vertex ID must be specified..."; 159 return undef; 160 } 161 unshift @{$This->{Vertices}}, $VertexID; 162 163 return $This; 164 } 165 166 # Add vertices to the path before the start vertex... 167 # 168 sub UnshiftVertices { 169 my($This, @VertexIDs) = @_; 170 171 if (!@VertexIDs) { 172 carp "Warning: ${ClassName}->UnshiftVertices: No vertices added: Vertices list is empty..."; 173 return undef; 174 } 175 unshift @{$This->{Vertices}}, @VertexIDs; 176 177 return $This; 178 } 179 180 # Get length... 181 # 182 sub GetLength { 183 my($This) = @_; 184 185 return scalar @{$This->{Vertices}}; 186 } 187 188 # Get start vertex... 189 # 190 sub GetStartVertex { 191 my($This) = @_; 192 193 if (!$This->GetLength()) { 194 return undef; 195 } 196 my($Index) = 0; 197 return $This->_GetVertex($Index); 198 } 199 200 # Get end vertex... 201 # 202 sub GetEndVertex { 203 my($This) = @_; 204 205 if (!$This->GetLength()) { 206 return undef; 207 } 208 my($Index); 209 210 $Index = $This->GetLength() - 1; 211 return $This->_GetVertex($Index); 212 } 213 214 # Get start and end vertices... 215 # 216 sub GetTerminalVertices { 217 my($This) = @_; 218 219 return ( $This->GetStartVertex(), $This->GetEndVertex() ), 220 } 221 222 # Get path vertices... 223 # 224 sub GetVertices { 225 my($This) = @_; 226 227 return wantarray ? @{$This->{Vertices}} : scalar @{$This->{Vertices}}; 228 } 229 230 # Get a specific vertex from path with indicies starting from 0... 231 # 232 sub GetVertex { 233 my($This, $Index) = @_; 234 235 if ($Index < 0) { 236 croak "Error: ${ClassName}->GetValue: Index value must be a positive number..."; 237 } 238 if ($Index >= $This->GetLength()) { 239 croak "Error: ${ClassName}->GetValue: Index vaue must be less than length of path..."; 240 } 241 if (!$This->GetLength()) { 242 return undef; 243 } 244 return $This->_GetVertex($Index); 245 } 246 247 # Get a vertex... 248 # 249 sub _GetVertex { 250 my($This, $Index) = @_; 251 252 return $This->{Vertices}[$Index]; 253 } 254 255 # Get path edges as pair of vertices or number of edges... 256 # 257 sub GetEdges { 258 my($This) = @_; 259 260 if ($This->GetLength < 1) { 261 return undef; 262 } 263 # Set up edges... 264 my($Index, $VertexID1, $VertexID2, @Vertices, @Edges); 265 266 @Edges = (); 267 for $Index (0 .. ($#{$This->{Vertices}} - 1) ) { 268 $VertexID1 = $This->{Vertices}[$Index]; 269 $VertexID2 = $This->{Vertices}[$Index + 1]; 270 push @Edges, ($VertexID1, $VertexID2); 271 } 272 273 return wantarray ? @Edges : ((scalar @Edges)/2); 274 } 275 276 # Is it a cycle? 277 # 278 sub IsCycle { 279 my($This) = @_; 280 my($StartVertex, $EndVertex); 281 282 ($StartVertex, $EndVertex) = $This->GetTerminalVertices(); 283 284 return ($StartVertex == $EndVertex) ? 1 : 0; 285 } 286 287 # For a path to be an independent path, it must meet the following conditions: 288 # . All other vertices are unique. 289 # 290 sub IsIndependentPath { 291 my($This) = @_; 292 293 # Make sure it has at least two vertices... 294 if ($This->GetLength() < 2) { 295 return 0; 296 } 297 298 # Check frequency of occurence for non-terminal vertices... 299 my($VertexID, $IndependenceStatus, @Vertices, %VerticesMap); 300 301 @Vertices = $This->GetVertices(); 302 shift @Vertices; pop @Vertices; 303 304 %VerticesMap = (); 305 $IndependenceStatus = 1; 306 307 VERTEXID: for $VertexID (@Vertices) { 308 if (exists $VerticesMap{$VertexID} ) { 309 $IndependenceStatus = 0; 310 last VERTEXID; 311 } 312 $VerticesMap{$VertexID} = $VertexID; 313 } 314 return $IndependenceStatus; 315 } 316 317 # For a path to be an independent cyclic path, it must meet the following conditions: 318 # . Termimal vertices are the same 319 # . All other vertices are unique. 320 # 321 sub IsIndependentCyclicPath { 322 my($This) = @_; 323 324 # Make sure it's a cycle... 325 if (!($This->GetLength() >= 3 && $This->IsCycle())) { 326 return 0; 327 } 328 return $This->IsIndependentPath(); 329 } 330 331 # Is it a path object? 332 sub IsPath ($) { 333 my($Object) = @_; 334 335 return _IsPath($Object); 336 } 337 338 # Copy path... 339 # 340 sub Copy { 341 my($This) = @_; 342 my($NewPath); 343 344 $NewPath = Storable::dclone($This); 345 346 return $NewPath; 347 } 348 349 # Reverse order of vertices in path... 350 # 351 sub Reverse { 352 my($This) = @_; 353 my(@VertexIDs); 354 355 @VertexIDs = (); push @VertexIDs, @{$This->{Vertices}}; 356 357 @{$This->{Vertices}} = (); push @{$This->{Vertices}}, reverse @VertexIDs; 358 359 return $This; 360 } 361 362 # Get vertices common between two paths... 363 # 364 sub GetCommonVertices { 365 my($This, $Other) = @_; 366 my($VertexID, @CommonVertices, %OtherVerticesMap); 367 368 # Setup a vertices hash for a quick look up... 369 %OtherVerticesMap = (); 370 for $VertexID ($Other->GetVertices()) { 371 $OtherVerticesMap{$VertexID} = $VertexID; 372 } 373 374 @CommonVertices = (); 375 for $VertexID ($This->GetVertices()) { 376 if ($OtherVerticesMap{$VertexID}) { 377 push @CommonVertices, $VertexID 378 } 379 } 380 return wantarray ? @CommonVertices : scalar @CommonVertices; 381 } 382 383 # Join the existing path with a new path specifed using a path object of a list of 384 # verticies. 385 # 386 sub Join { 387 my($This, @Values) = @_; 388 389 return $This->_Join(@Values); 390 } 391 392 # Join the existing path with a new path specifed using a path object at a specified 393 # vertex. 394 # 395 sub JoinAtVertex { 396 my($This, $Other, $CenterVertexID) = @_; 397 398 # Make sure CenterVertexID is end vertex in This and start vertex in Other before 399 # joining them... 400 if ($This->GetEndVertex() != $CenterVertexID) { 401 $This->Reverse(); 402 } 403 if ($Other->GetStartVertex() != $CenterVertexID) { 404 $Other->Reverse(); 405 } 406 return $This->_Join($Other); 407 } 408 409 # Join the existing path with a new path specifed using a path object of a list of 410 # verticies. 411 # 412 # Notes: 413 # . Paths must have a common terminal vertex. 414 # . Based on the common terminal vertex found, new path vertices are added to the 415 # current path in one of the four ways: 416 # . New path at end of current path with same vertices order : EndVertex = NewStartVertex 417 # . New path at end of current path with reversed vertices order: EndVertex = NewEndVertex 418 # . New path at front of current path with same vertices order: StartVertex = NewEndVertex 419 # . New path at front of current path with reversed vertices order: StartVertex = NewStartVertex 420 # 421 sub _Join { 422 my($This, @Values) = @_; 423 424 if (!@Values) { 425 return; 426 } 427 428 # Get a list of new vertex IDs.. 429 my($NewPath, $FirstValue, $TypeOfFirstValue, @NewVertexIDs); 430 431 $NewPath = $This->Copy(); 432 433 @NewVertexIDs = (); 434 $FirstValue = $Values[0]; 435 $TypeOfFirstValue = ref $FirstValue; 436 if ($TypeOfFirstValue =~ /^(SCALAR|HASH|CODE|REF|GLOB)/) { 437 croak "Error: ${ClassName}->JoinPath: Trying to add vertices to path object with a reference to unsupported value format..."; 438 } 439 440 if (_IsPath($FirstValue)) { 441 # It's another path object... 442 push @NewVertexIDs, @{$FirstValue->{Vertices}}; 443 } 444 elsif ($TypeOfFirstValue =~ /^ARRAY/) { 445 # It's array reference... 446 push @NewVertexIDs, @{$FirstValue}; 447 } 448 else { 449 # It's a list of values... 450 push @NewVertexIDs, @Values; 451 } 452 my($StartVertex, $EndVertex, $NewStartVertex, $NewEndVertex); 453 454 ($StartVertex, $EndVertex) = $NewPath->GetTerminalVertices(); 455 ($NewStartVertex, $NewEndVertex) = ($NewVertexIDs[0], $NewVertexIDs[$#NewVertexIDs]); 456 457 if (!($EndVertex == $NewStartVertex || $EndVertex == $NewEndVertex || $StartVertex == $NewEndVertex || $StartVertex == $NewStartVertex)) { 458 carp "Warning: ${ClassName}->JoinPath: Paths can't be joined: No common terminal vertex found..."; 459 return undef; 460 } 461 462 if ($EndVertex == $NewStartVertex) { 463 # Take out EndVertex and add new path at the end... 464 pop @{$NewPath->{Vertices}}; 465 push @{$NewPath->{Vertices}}, @NewVertexIDs; 466 } 467 elsif ($EndVertex == $NewEndVertex) { 468 # Take out EndVertex and add new path at the end with reversed vertex order... 469 pop @{$NewPath->{Vertices}}; 470 push @{$NewPath->{Vertices}}, reverse @NewVertexIDs; 471 } 472 elsif ($StartVertex == $NewEndVertex) { 473 # Take out NewEndVertex and add new path at the front... 474 pop @NewVertexIDs; 475 unshift @{$NewPath->{Vertices}}, @NewVertexIDs; 476 } 477 elsif ($StartVertex == $NewStartVertex) { 478 # Take out NewStartVertex and add new path at the front... 479 shift @NewVertexIDs; 480 unshift @{$NewPath->{Vertices}}, reverse @NewVertexIDs; 481 } 482 483 return $NewPath, 484 } 485 486 # Compare two paths... 487 # 488 sub _PathEqualOperator { 489 my($This, $Other) = @_; 490 491 if (!(defined($This) && _IsPath($This) && defined($Other) && _IsPath($Other))) { 492 croak "Error: ${ClassName}->_PathEqualOperator: Path equal comparison failed: Both object must be paths..."; 493 } 494 495 if ($This->GetLength() != $Other->GetLength()) { 496 return 0; 497 } 498 my($ThisID, $OtherID, $ReverseOtherID); 499 500 $ThisID = join('-', @{$This->{Vertices}}); 501 $OtherID = join('-', @{$Other->{Vertices}}); 502 $ReverseOtherID = join('-', reverse(@{$Other->{Vertices}})); 503 504 return ($ThisID =~ /^($OtherID|$ReverseOtherID)$/) ? 1 : 0; 505 } 506 507 # Return a string containing vertices in the path... 508 sub StringifyPath { 509 my($This) = @_; 510 my($PathString); 511 512 $PathString = "Path: " . join('-', @{$This->{Vertices}}); 513 514 return $PathString; 515 } 516 517 # Is it a path object? 518 sub _IsPath { 519 my($Object) = @_; 520 521 return (Scalar::Util::blessed($Object) && $Object->isa($ClassName)) ? 1 : 0; 522 } 523