This is an example to showcase how rsppfp can be used along with other existing packages.

Preparing the Input

The first step is to define the graph and its forbidden paths. This is done in the following snippet, with a set of forbidden paths defined as F = {f_1, f_2, f_3, f_4}, where f_1 = {u, v, y, u}, f_2 = {w, u, y, u}, f_3 = {w, v, y} and f_4 = {x, w, v, y, t}. Though this example is defined arbitrarily, and the input data is hard-coded, it is worth noting that the input can be obtained from different sources such as databases, spreadsheet files, and others. However, that process is outside of rsppfp’s scope.

# Load the sample graph
graph <- data.frame(from = c("s", "s", "s", "u", "u", "w", "w", "x", "x", "v", "v", "y", "y"),
                    to = c("u", "w", "x", "w", "v", "v", "y", "w", "y", "y", "t", "t", "u"),
                    cost = c(1L, 4L, 1L, 2L, 7L, 1L, 2L, 5L, 1L, 4L, 1L, 3L, 9L),
                    stringsAsFactors = FALSE)

# Load the forbidden paths
fpaths <- data.frame(V1 = c("u", "u", "w", "x"), V2 = c("v", "w", "v","w"), V3 = c("y", "y", "y", "v"),
                     V4 = c("u", "u", NA, "y"), V5 = c(NA, NA, NA, "t"), stringsAsFactors = FALSE)

After this, it is possible to use rsppfp’s functions to transform the original graph, into G*. In this case, some forbidden paths have sub-paths that are part of others; particular examples are f_3 and f_4. As a result, the example makes use of Hsu’s backward construction function.

# Run the algorithm and transform the graph
gStar <- modify_graph_hsu(graph, fpaths)
gStar
#>     from    to cost
#> 1      s     u    1
#> 2      s     w    4
#> 3      s     x    1
#> 4      w     y    2
#> 5      x     y    1
#> 6      v     y    4
#> 7      v     t    1
#> 8      y     t    3
#> 9      y     u    9
#> 10     u   u|v    7
#> 11   u|v u|v|y    4
#> 12     u   u|w    2
#> 13   u|w u|v|y    2
#> 23     w   w|v    1
#> 24     x   x|w    5
#> 25   x|w x|w|v    1
#> 26   w|v     t    1
#> 27   x|w     y    2
#> 28 x|w|v     t    1
#> 29   u|v     t    1
#> 30 u|v|y     t    3
#> 31   u|w   w|v    1

Integration with igraph

The resulting data frame (named in the code as gStar) can be transformed to other data types, specific of particular libraries. For example, it is possible to use the function graph_from_data_frame(...) provided by igraph (Csárdi & Nepusz, 2006), to convert gStar in order to use iGraph shortest-path functionalities.

# Transform gStar to igraph's data format
gStar.igraph <- graph_from_data_frame(gStar)

Even more, both graphs can be plotted using tkplot(…) or plot(...) function. The following code shows an example of visualization using igraphs functions.

# This can be used to plot the graph
plot(gStar.igraph, edge.arrow.size = 0.5, vertex.size = 20, xlab = "Graph", 
     vertex.color = "#F1F1F1", vertex.label.color = "#050505")

As any path calculated in gStar will be given in terms of its nodes. Hence, as a result of the transformation process, gStar nodes nStar are equivalences of the original nodes. For example, a node labeled Argo123 in the original graph G can be divided into several new nStar_i, resulting in: Troya789|Argo123, Paris456|Troya789|Argo123 and Polux852|Argo123, besides the original node. Therefore, when searching for a path from a node to another node n_i, the algorithm must consider all of the nStar_i.

The package rsppfp provides an additional function to obtain this equivalences. A code example can be seen below:

# This can be used to plot the graph
get_all_nodes(gStar, "v")
#> [1] "v"     "u|v"   "w|v"   "x|w|v"

With this, the flow to obtain a shortest path with any algorithm, can be summarized as follows: 1. Obtain all of the target node’s equivalences. 2. For each target node found in (1): - Get the shortest path and its weight/cost from the origin node to it. 3. The less costly path is the solution.

This algorithm has been implemented for igraph in an example function, named get_shortest_path. However, although it can be used to simplify solving the shortest path, its aim is to provide guidance on how to impliment the previous logic.

Its code is the following:

get_shortest_path <- function(g, origin, dest, weightColName = NULL) {
  #If there is no weight column specified, assume equal weights
  if(is.null(weightColName)) {
    g$weight <- 1
    weightColName <- "weight"
  # If the column could not be found...
  } else if(!weightColName %in% colnames(g)) {
    #Show an error
    stop(weightColName, " is not a variable in `g`.")
  }
  
  # Convert the graph
  g.i <- graph_from_data_frame(g)
  
  # Get all nodes where for the destination is the destination
  destEq <- get_all_nodes(g, dest)
  
  # Find shortest paths from `origin` to all N* corresponding to `dest`
  # - suppress warning if not all destinations reachable
  sp <- suppressWarnings(shortest_paths(g.i, from = origin, to = destEq,
                                        weights = edge_attr(g.i, weightColName),
                                        output = "both"))
  
  # Filter out zero-length paths (return if nothing left)
  zero_length <- lengths(sp$epath) == 0
  if (all(zero_length)) {
    warning("There is no path from ", origin, " to ", dest, ".\n")
    return (character(0))
  } else {
    sp <- lapply(sp, function(element) element[!zero_length])
  }
  
  # Find shortest of remaining paths
  dist <- vapply(sp$epath, 
                 function(path) sum(edge_attr(g.i, weightColName, path)),
                 numeric(1)) 
  shortestPath <- sp$vpath[[which.min(dist)]]
  
  # Convert path with nodes from N* to path with nodes from N
  return( parse_vpath(names(shortestPath)) )
}

And it can be used as in the following example:

# Obtain the shortest path using the simplified function
shortestPath <- get_shortest_path(gStar, "u", "t", "cost")
shortestPath
#> [1] "u" "w" "v" "t"

Also, it is worth pointing out that a path can only be translated if it is presented as a list or vector of nodes, written sequentially. In igraph, this is known as vpaths (vertexes paths). However, this step is done inside the convenience function.