4. Networks 2: Shortest Paths and Road Networks¶

ISE 754, Fall 2024

Package Used: Functions from the following package are used in this notebook for the first time:

  • Graphs: Graphs.jl, "The goal of Graphs.jl is to offer a performant platform for network and graph analysis in Julia, following the example of libraries such as NetworkX in Python."
  • SimpleWeightedGraphs: [SimpleWeightedGraphs.jl (https://juliagraphs.org/SimpleWeightedGraphs.jl/stable/), used to create graphs with undirected (two-way) edges and digraphs with directed (one-way) edges, with all of the edges have and associated weight (e.g., distance, if the edge represents a road segment).
  • DelaunayTriangulation: DelaunayTriangulation.jl, computes Delaunay triangulations of 2-D points.

1. Network Representations¶

The terms network and graph can be used interchangeably. The terms graph/vertex/edge are typically used when the focus is on properties of the mathematical object, and the terms network/node/arc are used when the focus is on the properties of a real-work system. The terms graph and digraph (short for directed graph) are used in mathematics to describe networks in which the arcs are either all undirected or directed, respectively. In many network applications, both directed and undirected arcs are used (e.g., one- and two-way roads), and the arcs have associated weights that represent, for example, the distance or travel time of the road being represented.

Network Graph Digraph
Node Vertex Vertex
Arc (undirected/directed) Edge (undirected) Edge (directed)

In Julia, both graphs and digraphs can be created. If a network with a mix of directed and undirected arcs is needed, then a digraph can be used, with two directed arcs in opposite directions used to represent an undirected arc.

There are different network representations that make it easier to operate on the network for specific applications, and it is usually possible to translate from one representation to another. Given a network with $m$ nodes and $n$ arcs:

  • Arc list: $n$-element array of the source nodes, n-element array of destination nodes, and an n-element array of the weights of each arc in the network.
  • Adjacency matrix: $m \times m$ matrix of arc weights, with weights of $0$ indicating no arc.
    • Interlevel matrix: $m_1 \times m_2$ matrix of $m_1 \times m_2$ arc weights, where all nodes can be separated in two groups consisting of $m_1$ arc-source and $m_2$ arc-destination nodes, that can be either be used directly to specify embedded into an adjacency matrix.
  • Incidence matrix: $m \times n$ matrix, where values of $-1$ and $1$ in each column indicate the source and destination nodes of the arc represented by the column, that represents of coefficients of the flow-balance equations of the constraints in an LP.

Ex: Digraph (complete bipartite digraph)¶

image.png

Interlevel to Adjacency: Embed the $m \times n$ interlevel matrix C into a $(m + n) \times (m + n)$ adjacency matrix:

In [1]:
C = [6. 10.; 0. 13.; 9. 16.]       # m x n interlevel matrix from m nodes to n nodes
m,n = size(C)
W = [zeros(m,m) C; zeros(n,m+n)]   # (m+n) x (m+n) adjacency matrix
Out[1]:
5×5 Matrix{Float64}:
 0.0  0.0  0.0  6.0  10.0
 0.0  0.0  0.0  0.0  13.0
 0.0  0.0  0.0  9.0  16.0
 0.0  0.0  0.0  0.0   0.0
 0.0  0.0  0.0  0.0   0.0

The function SimpleWeightedDiGraph is used to create a directed network using an adjacency matrix, where each element ( i, j ) in the matrix represents an arc directed from a starting node ( i ) to an ending node ( j ):

In [2]:
using Graphs, SimpleWeightedGraphs

g = SimpleWeightedDiGraph(W)      # Create digraph from adjacency matrix
Out[2]:
{5, 5} directed simple Int64 graph with Float64 weights
In [3]:
adjacency_matrix(g)               # Arcs represented by non-zero elements in sparse matrix
Out[3]:
5×5 SparseArrays.SparseMatrixCSC{Float64, Int64} with 5 stored entries:
  ⋅    ⋅    ⋅   6.0  10.0
  ⋅    ⋅    ⋅    ⋅   13.0
  ⋅    ⋅    ⋅   9.0  16.0
  ⋅    ⋅    ⋅    ⋅     ⋅ 
  ⋅    ⋅    ⋅    ⋅     ⋅ 

Since the adjacency matrix is stored as a sparse matrix, covert it to a regular (dense) matrix for display purposes:

In [4]:
Matrix(adjacency_matrix(g))
Out[4]:
5×5 Matrix{Float64}:
 0.0  0.0  0.0  6.0  10.0
 0.0  0.0  0.0  0.0  13.0
 0.0  0.0  0.0  9.0  16.0
 0.0  0.0  0.0  0.0   0.0
 0.0  0.0  0.0  0.0   0.0

An array comprehension can be used to iterate over all of the edges in the graph:

In [5]:
[e for e in edges(g)]
Out[5]:
5-element Vector{SimpleWeightedEdge{Int64, Float64}}:
 Edge 1 => 4 with weight 6.0
 Edge 1 => 5 with weight 10.0
 Edge 2 => 5 with weight 13.0
 Edge 3 => 4 with weight 9.0
 Edge 3 => 5 with weight 16.0

Problem: Arc 2 => 4 of 0 weight not included

In [6]:
replace!(C, 0. => sqrt(eps()))     # Use small positive value for all 0 arcs
Out[6]:
3×2 Matrix{Float64}:
 6.0         10.0
 1.49012e-8  13.0
 9.0         16.0
In [7]:
W = [zeros(m,m) replace!(C, 0. => sqrt(eps())); zeros(n,m+n)]
g = SimpleWeightedDiGraph(W)
println.(edges(g));
Edge 1 => 4 with weight 6.0
Edge 1 => 5 with weight 10.0
Edge 2 => 4 with weight 1.4901161193847656e-8
Edge 2 => 5 with weight 13.0
Edge 3 => 4 with weight 9.0
Edge 3 => 5 with weight 16.0
In [8]:
src.(edges(g))                     # Source or starting nodes of arcs
Out[8]:
6-element Vector{Int64}:
 1
 1
 2
 2
 3
 3
In [9]:
dst.(edges(g))                     # Destination or ending nodes of arcs
Out[9]:
6-element Vector{Int64}:
 4
 5
 4
 5
 4
 5
In [10]:
weights(g)                         # Weights stored as adjacency matrix
Out[10]:
5×5 LinearAlgebra.Adjoint{Float64, SparseArrays.SparseMatrixCSC{Float64, Int64}} with 6 stored entries:
  ⋅    ⋅    ⋅   6.0         10.0
  ⋅    ⋅    ⋅   1.49012e-8  13.0
  ⋅    ⋅    ⋅   9.0         16.0
  ⋅    ⋅    ⋅    ⋅            ⋅ 
  ⋅    ⋅    ⋅    ⋅            ⋅ 

Adjacency to Arc list: Iterate over the edge and weight data in the graph to create the arc lists:

In [11]:
i = src.(edges(g))
j = dst.(edges(g))
w = [weights(g)[src(e), dst(e)] for e in edges(g)]
println("i: ", i, "\nj: ", j, "\nw: ", w)
i: [1, 1, 2, 2, 3, 3]
j: [4, 5, 4, 5, 4, 5]
w: [6.0, 10.0, 1.4901161193847656e-8, 13.0, 9.0, 16.0]
In [12]:
g = SimpleWeightedDiGraph(i, j, w) # Create digraph using arc lists
println.(edges(g));
Edge 1 => 4 with weight 6.0
Edge 1 => 5 with weight 10.0
Edge 2 => 4 with weight 1.4901161193847656e-8
Edge 2 => 5 with weight 13.0
Edge 3 => 4 with weight 9.0
Edge 3 => 5 with weight 16.0

Adjacency/Arc list to Incidence matrix: Output the incidence matrix from the graph, create either from the adjacency matrix or arc lists:

In [13]:
A = Matrix(incidence_matrix(g))
Out[13]:
5×6 Matrix{Int64}:
 -1  -1   0   0   0   0
  0   0  -1  -1   0   0
  0   0   0   0  -1  -1
  1   0   1   0   1   0
  0   1   0   1   0   1

Since the incidence matrix representation does not include arc weight information, the weights need to be determined separately:

In [14]:
w = [weights(g)[src(e), dst(e)] for e in edges(g)]
Matrix([w'; A])
Out[14]:
6×6 Matrix{Float64}:
  6.0  10.0   1.49012e-8  13.0   9.0  16.0
 -1.0  -1.0   0.0          0.0   0.0   0.0
  0.0   0.0  -1.0         -1.0   0.0   0.0
  0.0   0.0   0.0          0.0  -1.0  -1.0
  1.0   0.0   1.0          0.0   1.0   0.0
  0.0   1.0   0.0          1.0   0.0   1.0

Ex: (Undirected) Graph:¶

image.png

The only change is that each arc in the network is undirected from the starting node ( i ) to the ending node ( j ). The function SimpleWeightedGraph is used to create an undirected network, as opposed to using SimpleWeightedDiGraph to create a directed network:

In [15]:
g = SimpleWeightedGraph(i, j, w)   # Create graph using arc lists
println.(edges(g));
Edge 1 => 4 with weight 6.0
Edge 2 => 4 with weight 1.4901161193847656e-8
Edge 3 => 4 with weight 9.0
Edge 1 => 5 with weight 10.0
Edge 2 => 5 with weight 13.0
Edge 3 => 5 with weight 16.0
In [16]:
adjacency_matrix(g)                # Both directions represented for each arc
Out[16]:
5×5 SparseArrays.SparseMatrixCSC{Float64, Int64} with 12 stored entries:
   ⋅     ⋅            ⋅   6.0         10.0
   ⋅     ⋅            ⋅   1.49012e-8  13.0
   ⋅     ⋅            ⋅   9.0         16.0
  6.0   1.49012e-8   9.0   ⋅            ⋅ 
 10.0  13.0         16.0   ⋅            ⋅ 
In [17]:
W
Out[17]:
5×5 Matrix{Float64}:
 0.0  0.0  0.0  6.0         10.0
 0.0  0.0  0.0  1.49012e-8  13.0
 0.0  0.0  0.0  9.0         16.0
 0.0  0.0  0.0  0.0          0.0
 0.0  0.0  0.0  0.0          0.0
In [18]:
W + W'
Out[18]:
5×5 Matrix{Float64}:
  0.0   0.0          0.0  6.0         10.0
  0.0   0.0          0.0  1.49012e-8  13.0
  0.0   0.0          0.0  9.0         16.0
  6.0   1.49012e-8   9.0  0.0          0.0
 10.0  13.0         16.0  0.0          0.0
In [19]:
g = SimpleWeightedGraph(W + W')      # Create graph using adjacency matrix
println.(edges(g));
Edge 1 => 4 with weight 6.0
Edge 2 => 4 with weight 1.4901161193847656e-8
Edge 3 => 4 with weight 9.0
Edge 1 => 5 with weight 10.0
Edge 2 => 5 with weight 13.0
Edge 3 => 5 with weight 16.0
In [20]:
i, j, w = src.(edges(g)), dst.(edges(g)), [weights(g)[src(e), dst(e)] for e in edges(g)]
println("i: ", i, "\nj: ", j, "\nw: ", w)
i: [1, 2, 3, 1, 2, 3]
j: [4, 4, 4, 5, 5, 5]
w: [6.0, 1.4901161193847656e-8, 9.0, 10.0, 13.0, 16.0]
In [21]:
A = Matrix(incidence_matrix(g))    # Note: Undirected graph => 1,1 vs digraph => -1,1
Out[21]:
5×6 Matrix{Int64}:
 1  0  0  1  0  0
 0  1  0  0  1  0
 0  0  1  0  0  1
 1  1  1  0  0  0
 0  0  0  1  1  1

2. Dijkstra's Shortest Path Procedure¶

Dijkstra's procedure is used to find the shortest path in a network. It is a greedy procedure that can quickly find the (global) optimal solution.

Ex: Undirected Network¶

Determine the shortest path from node 1 to node 6 in this network:

image.png

image.png

image.png

image.png

Pred: $\quad 0 - 3 - 1 - 2 - 4 - 5 $

Path: $\quad 1 \leftarrow 3 \leftarrow 2 \leftarrow 4 \leftarrow 5 \leftarrow 6 $

Cost: $\quad 13$

The network can be created by defining the starting and ending nodes for each arc along with each arc's weight:

In [22]:
using Graphs, SimpleWeightedGraphs

i = [1, 1, 2, 2, 3, 3, 4, 4, 5]     # Index of starting node of each arc
j = [2, 3, 3, 4, 4, 5, 5, 6, 6]     # Index of ending node of each arc
w = [4, 2, 1, 5, 8, 10, 2, 6, 3]    # Weight (e.g., cost, distance, time) of each arc
g = SimpleWeightedGraph(i, j, w)    # Create undirected network
Out[22]:
{6, 9} undirected simple Int64 graph with Int64 weights

Using Dijkstra's procedure from Graphs (very efficient compared to the code example that follows):

In [23]:
ds = dijkstra_shortest_paths(g, 1)    # Shortest paths from node 1 to other all nodes
Out[23]:
Graphs.DijkstraState{Int64, Int64}([0, 3, 1, 2, 4, 5], [0, 3, 2, 8, 10, 13], [Int64[], Int64[], Int64[], Int64[], Int64[], Int64[]], [1.0, 1.0, 1.0, 1.0, 1.0, 1.0], Int64[])
In [24]:
ds.dists                              # Distance from node 1 to all nodes
Out[24]:
6-element Vector{Int64}:
  0
  3
  2
  8
 10
 13
In [25]:
ds.parents                            # Node predecessors (also called parents)
Out[25]:
6-element Vector{Int64}:
 0
 3
 1
 2
 4
 5
In [26]:
enumerate_paths(ds, 6)                # Path from node 1 to node 6
Out[26]:
6-element Vector{Int64}:
 1
 3
 2
 4
 5
 6
In [27]:
ds.dists[6]                           # Distance from node 1 to 6
Out[27]:
13

To show an implementation of Dijkstra's procedure, we will operate on the adjacency matrix representation of the network. Only inputs are the adjacency matrix and the starting node.

In [28]:
W = Array(float(adjacency_matrix(g)))
Out[28]:
6×6 Matrix{Float64}:
 0.0  4.0   2.0  0.0   0.0  0.0
 4.0  0.0   1.0  5.0   0.0  0.0
 2.0  1.0   0.0  8.0  10.0  0.0
 0.0  5.0   8.0  0.0   2.0  6.0
 0.0  0.0  10.0  2.0   0.0  3.0
 0.0  0.0   0.0  6.0   3.0  0.0
In [29]:
replace!(W, 0. => Inf)                # Set non-arcs to Inf so never selected
Out[29]:
6×6 Matrix{Float64}:
 Inf    4.0   2.0  Inf   Inf   Inf
  4.0  Inf    1.0   5.0  Inf   Inf
  2.0   1.0  Inf    8.0  10.0  Inf
 Inf    5.0   8.0  Inf    2.0   6.0
 Inf   Inf   10.0   2.0  Inf    3.0
 Inf   Inf   Inf    6.0   3.0  Inf

Dijkstra's Shortest Path Procedure:

In [30]:
s = 1                                 # Starting node
m = size(W, 1)                        # Number of nodes
d = fill(Inf, m)                      # Initial distance to all nodes from s = Inf
pred = Int.(zeros(m))                 # All predecessors set to 0
S̄ = [1:m;]                            # Initially, all nodes in set S̄ of unlabeled nodes
d[s] = 0.                             # Distance from s to s = 0
done = false
while !done
    i2 = argmin(d[S̄])                 # Find minimum distance unlabeled node (SLOW step!)
    i = S̄[i2]                         # Convert S̄ index (i2) to node index (i)
    splice!(S̄,i2)                     # Label node i (remove from S̄)
    pred[d .> d[i] .+ W[i,:]] .= i    # Update pred of children of i if dist from i shorter
    d = min.(d, d[i] .+ W[i,:])       # Update dist of children of i if dist from i shorter
    if isempty(S̄)
        done = true
    end
end
d, pred
Out[30]:
([0.0, 3.0, 2.0, 8.0, 10.0, 13.0], [0, 3, 1, 2, 4, 5])
In [31]:
ds.dists, ds.parents                   # Using dijkstra_shortest_paths(G, 1)
Out[31]:
([0, 3, 2, 8, 10, 13], [0, 3, 1, 2, 4, 5])

Ex: Directed Network¶

The only change is that each arc in the network is directed from the starting node ( i ) to the ending node ( j ). The function SimpleWeightedDiGraph is used to create a directed network, as opposed to using SimpleWeightedGraph to create an undirected network:

In [32]:
i = [1, 1, 2, 2, 3, 3, 4, 4, 5]
j = [2, 3, 3, 4, 4, 5, 5, 6, 6]
w = [4, 2, 1, 5, 8, 10, 2, 6, 3]
g = SimpleWeightedDiGraph(i, j, w)
Out[32]:
{6, 9} directed simple Int64 graph with Int64 weights
In [33]:
ds = dijkstra_shortest_paths(g, 1)
println("Min cost path: ", enumerate_paths(ds, 6), ", Cost: ", ds.dists[6])
Min cost path: [1, 2, 4, 5, 6], Cost: 14

Undirected path was [1, 3, 2, 4, 5, 6] at a cost of 13. In the directed graph, there is no edge from 3 to 2, resulting in the different path and higher cost.

Discussion¶

Dijkstra¶

The Dijkstra procedure requires that arcs have all nonnegative lengths on any cycles in the network:

  • Negative arc on a cycle would result in a path length $\rightarrow -\infty$.
  • It is a “label setting” algorithm since a step to the final solution is made as each node is labeled.
  • Can find the longest path (used, e.g., in CPM) by negating all arc lengths

Floyd-Warshall¶

Networks with only some negative arcs on a cycle require slower “label correcting” procedures that repeatedly check for optimality at all nodes or detect a negative cycle.

  • Requires $O(m^3)$ via Floyd-Warshall algorithm (cf., $O(m^2)$ Dijkstra), for $m$ nodes.
  • Negative arcs used in project scheduling to represent maximum lags between activities.

A* algorithm¶

A* algorithm adds to Dijkstra a heuristic LB estimate of each node's remaining distance to the destination:

  • Used in AI search for all types of applications (tic-tac-toe, chess).
  • In path planning applications, great circle distance from each node to destination could be used as an LB estimate of the remaining distance.

image.png

Time Complexity of Shortest Path Procedures¶

Procedure Problem Time Complexity
Simplex LP $O(2^m)$
Ellipsoid LP $O(m^4)$
Hungarian Transportation $O(m^3)$
Floyd-Warshall Shortest Path with Cycles $O(m^3)$
Dijkstra (linear min) Shortest Path without Cycles $O(m^2)$
Dijkstra (Fibonocci heap) Shortest Path without Cycles $O(n\,\mbox{log}\,m)$
Number of nodes $m$
Number of arcs $n$

The following compares the time complexity of determining the road distance from Raleigh, NC, to Portland, OR, using the U.S. Interstate Highway network:

image.png

In [34]:
m = 862                    # Number of nodes in Interstate road network
n = 1223                   # Number of arcs
@show m^4                  # LP for shortest path using ellipsoid
@show m^2                  # Dijkstra using linear search for minimum  
@show n*log(n)             # Dijkstra using Fibonocci heap for minimum
@show (m^4)/(n*log(m))     # Ratio LP to Fibonocci
@show (m^2)/(n*log(m));    # Ratio linear search to Fibonocci
m ^ 4 = 552114385936
m ^ 2 = 743044
n * log(n) = 8694.382991945411
m ^ 4 / (n * log(m)) = 6.6788818050625674e7
m ^ 2 / (n * log(m)) = 89.88541466000086

3. Road Networks¶

National Highway System (NHS) Road Network¶

The files nhs_nodes.csv and nhs_links.csv contain node and link data, respectively, for the U.S. National Highway System (NHS) road network. Each node is defined by an index ( IDX ) and its longitude ( LON ) and latitude ( LAT ), and each link is defined by its starting (source) node index ( SRC ), ending (destination) node index ( DST ), its distance ( DIST ) in miles, and other link attributes like state and county FIPS codes ( STFIP and COFIP ). The network is undirected because all of the arcs can be used for two-way travel. The file roadnetfun.jl contains functions used to work with the road network data.

Ex: Wake County NHS Roads¶

Convert CSV files to data frames and then use STFIP == 37 (NC) and COFIP == 183 (Wake County) to select NHS links, and prune_reindex to create node and link data frames that just contain Wake County roads.

In [35]:
using DataFrames, CSV

include("roadnetfun.jl")

dfnodes = DataFrame(CSV.File("nhs_nodes.csv"))
dflinks = DataFrame(CSV.File("nhs_links.csv"))
dfnodes, dflinks
Out[35]:
(369854×3 DataFrame
    Row │ IDX     LON        LAT     
        │ Int64   Float64    Float64 
────────┼────────────────────────────
      1 │      1  -150.289   67.018
      2 │      2  -150.21    67.2448
      3 │      3   -68.0043  46.8593
      4 │      4   -67.9938  46.8208
      5 │      5   -67.9952  46.8308
      6 │      6   -67.9964  46.8142
      7 │      7   -67.9965  46.8125
      8 │      8   -67.9961  46.8112
      9 │      9  -155.111   19.875
     10 │     10  -155.243   19.9873
     11 │     11  -155.813   20.0189
   ⋮    │   ⋮         ⋮         ⋮
 369845 │ 369845   -68.01    46.7009
 369846 │ 369846   -68.0122  46.694
 369847 │ 369847   -68.0161  46.6745
 369848 │ 369848   -68.0138  46.686
 369849 │ 369849   -68.0158  46.6735
 369850 │ 369850   -68.0127  46.689
 369851 │ 369851   -68.0087  46.7028
 369852 │ 369852   -68.008   46.7052
 369853 │ 369853   -68.008   46.7043
 369854 │ 369854   -68.0058  46.8537
                  369833 rows omitted, 387344×10 DataFrame
    Row │ SRC     DST     DIST       STFIP  COFIP  URBAN  SPEED    SIGN      N ⋯
        │ Int64   Int64   Float64    Int64  Int64  Int64  Float64  String7?  S ⋯
────────┼───────────────────────────────────────────────────────────────────────
      1 │      1       2  17.9124        2    290  99999     50.0  S11       J ⋯
      2 │      1    1145  24.2011        2    290  99999     50.0  S11       J
      3 │      2    1522   1.10347       2    290  99999     35.0  S11       J
      4 │      3      19   0.805146     23      3  99998     45.0  U1        m
      5 │      3  369854   0.403536     23      3  99998     35.0  U1        m ⋯
      6 │      4       5   0.700074     23      3  99999     55.0  U1        m
      7 │      4       6   0.459986     23      3  99999     55.0  U1        m
      8 │      5      13   1.33016      23      3  99999     55.0  U1        m
      9 │      6       7   0.100335     23      3  99999     45.0  U1        m ⋯
     10 │      7       8   0.090225     23      3  99999     45.0  U1        m
     11 │      8  369842   0.070219     23      3  99999     45.0  U1        m
   ⋮    │   ⋮       ⋮         ⋮        ⋮      ⋮      ⋮       ⋮        ⋮        ⋱
 387335 │ 369843  369844   4.8839       23      3  99999     55.0  U1        m
 387336 │ 369843  369852   0.180755     23      3  99999     35.0  U1        m ⋯
 387337 │ 369845  369846   0.486638     23      3  99998     35.0  U1        m
 387338 │ 369845  369851   0.149565     23      3  99998     35.0  U1        m
 387339 │ 369846  369850   0.344989     23      3  99998     25.0  U1        m
 387340 │ 369847  369848   0.808124     23      3  99998     25.0  U1        m ⋯
 387341 │ 369847  369849   0.072928     23      3  99999     25.0  U1        m
 387342 │ 369848  369850   0.214392     23      3  99998     25.0  U1        m
 387343 │ 369851  369853   0.109344     23      3  99999     35.0  U1        m
 387344 │ 369852  369853   0.059729     23      3  99999     35.0  U1        m ⋯
                                               2 columns and 387323 rows omitted)
In [36]:
dfL, dfN = prune_reindex(
    filter(r -> (r.STFIP == 37) && (r.COFIP == 183), dflinks), dfnodes)
Out[36]:
(446×10 DataFrame
 Row │ SRC    DST    DIST      STFIP  COFIP  URBAN  SPEED    SIGN      NAME    ⋯
     │ Int64  Int64  Float64   Int64  Int64  Int64  Float64  String7?  String? ⋯
─────┼──────────────────────────────────────────────────────────────────────────
   1 │     1      2  0.361991     37    183  73261     70.0  I40       missing ⋯
   2 │     1    220  3.11067      37    183  73261     70.0  I40       missing
   3 │     2    305  1.09278      37    183  73261     55.0  U70       Us Hwy
   4 │     2    306  0.446956     37    183  73261     70.0  I40       missing
   5 │     3      4  3.39827      37    183  99999     70.0  U1        missing ⋯
   6 │     4     54  3.85514      37    183  73261     70.0  U1        missing
   7 │     5     11  0.462193     37    183  73261     55.0  U401      missing
   8 │     5     27  0.972324     37    183  73261     55.0  U401      missing
   9 │     6     11  0.798938     37    183  73261     55.0  U401      missing ⋯
  10 │     6    122  1.25214      37    183  73261     55.0  U401      missing
  11 │     7      8  1.60154      37    183  73261     35.0  S55       missing
  ⋮  │   ⋮      ⋮       ⋮        ⋮      ⋮      ⋮       ⋮        ⋮          ⋮   ⋱
 437 │   298    299  0.728432     37    183  73261     35.0  U401      missing
 438 │   298    413  0.08392      37    183  99999     35.0  U401      missing ⋯
 439 │   299    300  0.069977     37    183  73261     35.0  U401      missing
 440 │   300    414  0.82933      37    183  99999     55.0  U401      missing
 441 │   301    415  0.716913     37    183  73261     70.0  U264      missing
 442 │   302    303  0.701596     37    183  73261     70.0  U64       missing ⋯
 443 │   303    416  2.59257      37    183  73261     70.0  U64       missing
 444 │   303    417  0.507954     37    183  73261     70.0  U264      missing
 445 │   304    415  1.43788      37    183  73261     70.0  U264      missing
 446 │   304    417  0.541973     37    183  73261     70.0  U264      missing ⋯
                                                  2 columns and 425 rows omitted, 417×3 DataFrame
 Row │ IDX    LON       LAT     
     │ Int64  Float64   Float64 
─────┼──────────────────────────
   1 │     1  -78.5649  35.6517
   2 │     2  -78.5653  35.6464
   3 │     3  -78.9831  35.6445
   4 │     4  -78.9307  35.6688
   5 │     5  -78.7081  35.6486
   6 │     6  -78.6994  35.6651
   7 │     7  -78.8331  35.6394
   8 │     8  -78.8485  35.6576
   9 │     9  -78.5663  35.6304
  10 │    10  -78.5066  35.7674
  11 │    11  -78.7055  35.6548
  ⋮  │   ⋮       ⋮         ⋮
 408 │   408  -78.6216  35.8017
 409 │   409  -78.5877  35.7878
 410 │   410  -78.5918  35.8207
 411 │   411  -78.5193  36.0089
 412 │   412  -78.5419  35.9653
 413 │   413  -78.4281  35.9375
 414 │   414  -78.4123  35.9578
 415 │   415  -78.2689  35.8204
 416 │   416  -78.275   35.859
 417 │   417  -78.2995  35.834
                396 rows omitted)
In [37]:
using Graphs, SimpleWeightedGraphs

g = SimpleWeightedGraph(dfL.SRC, dfL.DST, dfL.DIST)
Out[37]:
{417, 446} undirected simple Int64 graph with Float64 weights
In [38]:
using CairoMakie, Logjam.MapTools

fig, ax = makemap(dfN.LON, dfN.LAT)
lines!(ax, x2ln(g, dfN.LON), x2ln(g, dfN.LAT), color=:black, linewidth=.5)
fig
Out[38]:
No description has been provided for this image
In [39]:
XY = [-78.75864529722959 35.74329140697632;
    -78.70352644371913 35.866083946889844]
dfL2, dfN2 = addconnectors(dfL, dfN, XY[:, 1], XY[:, 2])
Out[39]:
(452×3 DataFrame
 Row │ SRC    DST    DIST     
     │ Int64  Int64  Float64  
─────┼────────────────────────
   1 │     3      4  0.361991
   2 │     3    222  3.11067
   3 │     4    307  1.09278
   4 │     4    308  0.446956
   5 │     5      6  3.39827
   6 │     6     56  3.85514
   7 │     7     13  0.462193
   8 │     7     29  0.972324
   9 │     8     13  0.798938
  10 │     8    124  1.25214
  11 │     9     10  1.60154
  ⋮  │   ⋮      ⋮       ⋮
 443 │   305    418  2.59257
 444 │   305    419  0.507954
 445 │   306    417  1.43788
 446 │   306    419  0.541973
 447 │     1     73  1.07142
 448 │     1      8  8.24161
 449 │     1    121  4.25563
 450 │     2    190  1.85777
 451 │     2    191  2.36375
 452 │     2    143  0.694065
              431 rows omitted, 419×3 DataFrame
 Row │ IDX    LON       LAT     
     │ Int64  Float64   Float64 
─────┼──────────────────────────
   1 │     1  -78.7586  35.7433
   2 │     2  -78.7035  35.8661
   3 │     3  -78.5649  35.6517
   4 │     4  -78.5653  35.6464
   5 │     5  -78.9831  35.6445
   6 │     6  -78.9307  35.6688
   7 │     7  -78.7081  35.6486
   8 │     8  -78.6994  35.6651
   9 │     9  -78.8331  35.6394
  10 │    10  -78.8485  35.6576
  11 │    11  -78.5663  35.6304
  ⋮  │   ⋮       ⋮         ⋮
 410 │   410  -78.6216  35.8017
 411 │   411  -78.5877  35.7878
 412 │   412  -78.5918  35.8207
 413 │   413  -78.5193  36.0089
 414 │   414  -78.5419  35.9653
 415 │   415  -78.4281  35.9375
 416 │   416  -78.4123  35.9578
 417 │   417  -78.2689  35.8204
 418 │   418  -78.275   35.859
 419 │   419  -78.2995  35.834
                398 rows omitted)
In [40]:
idxN = dfL2.DST[end-5:end]
scatter!(ax, dfN2.LON[idxN], dfN2.LAT[idxN], marker='.', markersize=24, color=:red)
scatter!(ax, XY[:, 1], XY[:, 2])
fig
Out[40]:
No description has been provided for this image
In [41]:
g = SimpleWeightedGraph(dfL2.SRC, dfL2.DST, dfL2.DIST)
Out[41]:
{419, 452} undirected simple Int64 graph with Float64 weights
In [42]:
ds = dijkstra_shortest_paths(g, 1)
println("Min cost path: ", enumerate_paths(ds, 2), ", Cost: ", ds.dists[2])
Min cost path: [1, 73, 340, 120, 123, 338, 68, 69, 175, 168, 320, 39, 321, 170, 169, 146, 145, 192, 364, 144, 143, 2], Cost: 13.689167136202915
In [43]:
dgc(XY[1,:], XY[2,:]) * 1.2
Out[43]:
10.834616964839176

Google Maps:

image.png

Ex: Road Distances between 100K NC Cities¶

In [44]:
using Logjam.DataTools, DataFrames, CSV

df = filter(r -> (r.STFIP == st2fips(:NC)) && (r.POP > 100_000), usplace())
select!(df, :NAME, :LAT, :LON, :POP)
Out[44]:
10×4 DataFrame
RowNAMELATLONPOP
StringFloat64Float64Int64
1Cary35.7814-78.819174721
2Charlotte35.209-80.831874579
3Concord35.3921-80.6355105240
4Durham35.98-78.901283506
5Fayetteville35.0828-78.9735208501
6Greensboro36.0951-79.827299035
7High Point35.9908-79.9927114059
8Raleigh35.8302-78.6415467665
9Wilmington34.2092-77.8858115451
10Winston-Salem36.1029-80.2608249545
In [45]:
using DataFrames, CSV
using Graphs, SimpleWeightedGraphs

include("roadnetfun.jl")

dfN = DataFrame(CSV.File("nhs_nodes.csv"))
dfL = DataFrame(CSV.File("nhs_links.csv"))
dfL, dfN = prune_reindex(filter(r -> (r.STFIP == st2fips(:NC)), dfL), dfN)
dfL, dfN = addconnectors(dfL, dfN, df.LON, df.LAT)

g = SimpleWeightedGraph(dfL.SRC, dfL.DST, dfL.DIST)
Out[45]:
{6893, 7329} undirected simple Int64 graph with Float64 weights
In [46]:
using CairoMakie, Logjam.MapTools

fig, ax = makemap(dfN.LON, dfN.LAT; xexpand=0.05)
lines!(ax, x2ln(g, dfN.LON), x2ln(g, dfN.LAT), color=:black, linewidth=.5)
scatter!(ax, df.LON, df.LAT)
text!(ax, df.LON, df.LAT, text=df.NAME; aligntext(df.LON, df.LAT)..., fontsize=12) 
fig
Out[46]:
No description has been provided for this image
In [47]:
n = nrow(df)
D = hcat([dijkstra_shortest_paths(g, i).dists[1:n] for i in 1:n]...)
Out[47]:
10×10 Matrix{Float64}:
   0.0     132.816   116.263    16.9174  …   14.4509  139.411   98.21
 132.816     0.0      19.5412  134.992      144.194   202.782   81.0455
 116.263    19.5412    0.0     118.44       127.641   214.328   62.9545
  16.9174  134.992   118.44      0.0         19.36    149.422   83.299
  67.853   133.231   133.693    78.0083      73.9081  112.846  116.307
  71.8229   92.6482   74.5571   56.9119  …   74.5521  204.614   28.8151
  81.9244   79.5903   61.4992   68.2141      85.8543  209.621   20.9838
  14.4509  144.194   127.641    19.36         0.0     131.984  100.939
 139.411   202.782   214.328   149.422      131.984     0.0    226.573
  98.21     81.0455   62.9545   83.299      100.939   226.573    0.0

Distance from Cary (1) to Charlotte (2):

In [48]:
D[1, 2]
Out[48]:
132.81556429475137
In [49]:
dgc([df.LON[1], df.LAT[1]],[df.LON[2], df.LAT[2]]) * 1.2
Out[49]:
143.86594537973932

Google Maps:

image.png

In [ ]: