Skip to content

Commit 8f61792

Browse files
author
Vincent Landau
committed
reword filenames, add tests, update docs
1 parent 99d373d commit 8f61792

File tree

10 files changed

+247
-27
lines changed

10 files changed

+247
-27
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ run-dev.sh
33
Manifest.toml
44
docs/build
55
docs/make-local.jl
6+
local

Project.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1212

1313
[compat]
1414
ArchGDAL = "0.6"
15-
GeoData = "0.4.6"
1615
julia = "1.6"

docs/make.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ makedocs(
1515
authors = "Vincent A. Landau",
1616
sitename = "SpatialGraphs.jl",
1717
pages = ["About" => "index.md",
18-
"Graph Types" => "graphtypes.md"],
18+
"Graph Types" => "graphtypes.md",
19+
"User Guide" => "userguide.md"],
1920
)
2021

2122
deploydocs(

docs/src/graphtypes.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,19 @@ AbstractSpatialGraph
1515
The `AbstractRasterGraph` type is as subtype of `AbstractSpatialGraph`. All
1616
`AbstractRasterGraph` subtypes contain a field called `vertex_raster`, which is
1717
a `GeoData.GeoArray` that describes the spatial locations for the vertices in
18-
the graph. If your graph has 10 vertices, then the corresponding `GeoArray`
18+
the graph. If your graph has 10 vertices, then the corresponding `vertex_raster`
1919
must have 10 unique values, starting at 1 (which corresponds to the first vertex
2020
in the graph) and going up to 10 (which corresponds to the tenth vertex in the
2121
graph). To find the location of a given graph vertex, _n_, you simply identify
22-
the pixel(s) with a value of _n_.
22+
the pixel(s) with a value of _n_. For pixels/elements in `vertex_raster` for
23+
which there shouldn't be a graph vertex, use a value of 0.
2324
```@docs
2425
AbstractRasterGraph
2526
```
2627

2728
## AbstractRasterGraph Subtypes
2829
SpatialGraphs.jl has raster graph types for undirected, directed, unweighted,
29-
and weighted graphsm each detailed below.
30+
and weighted graphs, each detailed below.
3031

3132
```@docs
3233
SimpleRasterGraph

docs/src/userguide.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# User Guide
2+
3+
## Building Graphs from Rasters
4+
5+
SpatialGraphs.jl offers options methods for constructing graphs from raster
6+
data.
7+
8+
```@docs
9+
weightedrastergraph
10+
make_raster_graph
11+
```

src/SpatialGraphs.jl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@ using LightGraphs, SimpleWeightedGraphs, ArchGDAL, GeoData
44

55
include("structs.jl")
66
include("graph_interface.jl")
7+
include("rastergraphs.jl")
8+
include("utils.jl")
79
## Types and Structs
810
export AbstractSpatialGraph, AbstractRasterGraph, SimpleRasterGraph,
911
SimpleRasterDiGraph, WeightedRasterGraph, WeightedRasterDiGraph
1012

13+
## Raster graph
14+
export make_raster_graph, weightedrastergraph
15+
1116
end # module

src/graph_interface.jl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
## Methods for the LightGraphs ad SimpleWeightedGraphs interfaces
22
## With these methods defined, functions from LightGraphs should "just work"
3+
import LightGraphs:
4+
nv, ne, vertices, edges, eltype, edgetype, has_edge, has_vertex,
5+
inneighbors, outneighbors, is_directed, add_edge!
6+
7+
import SimpleWeightedGraphs:
8+
add_edge!, get_weight
9+
10+
import Base:
11+
eltype, zero
312

413
### LightGraphs interface
514
nv(g::AbstractSpatialGraph) = nv(g.graph)
@@ -14,3 +23,8 @@ inneighbors(g::AbstractSpatialGraph, v) = inneighbors(g.graph, v)
1423
outneighbors(g::AbstractSpatialGraph, v) = outneighbors(g.graph, v)
1524
is_directed(g::AbstractSpatialGraph) = is_directed(g.graph)
1625
Base.zero(g::AbstractSpatialGraph) = zero(g.graph)
26+
add_edge!(g::AbstractSpatialGraph, a::Integer, b::Integer, c::Number) = add_edge!(g.graph, a, b, c)
27+
28+
### SimpleWeightedGraphs
29+
get_weight(g::WeightedRasterGraph, a::Integer, b::Integer) = get_weight(g.graph, a, b)
30+
get_weight(g::WeightedRasterDiGraph, a::Integer, b::Integer) = get_weight(g.graph, a, b)

src/constructors.jl renamed to src/rastergraphs.jl

Lines changed: 134 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,44 @@
1+
"""
2+
make_vertex_raster(A::GeoArray)
3+
4+
Constuct a vertex raster (a raster where the value of each pixel corresponds
5+
to its ID in a graph, and 0s correspond to NoData). Returns a GeoArray. This
6+
function is recommended for internal use only.
7+
8+
## Parameters
9+
`A`: The GeoArray from which a graph will be built, which is used as the
10+
reference for building the vertex raster. Pixels with NoData (`A.missingval`)
11+
are skipped (no vertex is assigned). Pixels with NoData will get a value of 0 in
12+
the vertex raster.
13+
"""
14+
function make_vertex_raster(A::GeoData.GeoArray)
15+
# Make an array of unique node identifiers
16+
nodemap = zeros(Int64, size(A.data))
17+
is_node = (A.data .!= A.missingval) .&
18+
((!).(isnan.(A.data)))
19+
20+
nodemap[is_node] = 1:sum(is_node)
21+
22+
nodemap = GeoData.GeoArray(nodemap, dims(A))
23+
24+
nodemap
25+
end
26+
27+
128
"""
229
weightedrastergraph(
3-
A::GeoArray;
30+
weight_raster::GeoArray;
431
directed::Bool = false,
532
condition::Function = is_data,
633
cardinal_neighbors_only::Bool = false,
734
connect_using_avg_raster_val::Bool = true
835
)
936
1037
Construct a `WeightedRasterGraph` or `WeightedRasterDiGraph` (if
11-
`directed = true`) from a raster dataset.
38+
`directed = true`) from a raster dataset. The weight raster, denotes
39+
the edge weight correponding to each vertex. Since edges are between rather than
40+
on vertices, edge weights are calculated as the average of the weights for each
41+
vertex.
1242
1343
## Parameters
1444
`weight_raster`: A GeoData.GeoArray contained values that, where applicable
@@ -53,27 +83,33 @@ If `false`, the weight between two nodes with resistances R1 and R2 is
5383
calculated by converting resistance to conductances, taking the average, then
5484
taking the inverse of the result to convert back to resistance:
5585
`1 / ((1/R1 + 1/R2) / 2)`. `connect_using_avg_weights = false` correspondes to
56-
the default settings in Circuitscape. Defaults to `true', in which case the
57-
simple average of the weights (adjusted for distance in the case of diagonal
58-
neighbors) in `weight_raster` are used.
86+
the default settings in Circuitscape. Defaults to `true`, in which case the
87+
simple average (adjusted for distance in the case of diagonal
88+
neighbors) of the weights in `weight_raster` is used.
89+
90+
`combine`: In the case that there are multiple edges between a single pair
91+
of vertices, how should the weight be chosen? Defaults to `min`. See the docs
92+
for `SparseArrays.sparse()` for more information.
5993
"""
6094
function weightedrastergraph(
6195
weight_raster::GeoArray;
6296
directed::Bool = false,
63-
condition_raster::GeoArray = A,
97+
condition_raster::GeoArray = weight_raster,
6498
condition::Function = is_data,
6599
cardinal_neighbors_only::Bool = false,
66-
connect_using_avg_weights::Bool = true
100+
connect_using_avg_weights::Bool = true,
101+
combine::Function = min
67102
)
68103
v = make_vertex_raster(weight_raster)
69-
g = make_weighted_graph(
104+
g = make_raster_graph(
70105
weight_raster,
71-
vertex_raster,
106+
v,
72107
directed = directed,
73108
condition_raster = condition_raster,
74109
condition = condition,
75110
cardinal_neighbors_only = cardinal_neighbors_only,
76-
connect_using_avg_weights = connect_using_avg_weights
111+
connect_using_avg_weights = connect_using_avg_weights,
112+
combine = combine
77113
)
78114

79115
if directed
@@ -86,30 +122,106 @@ function weightedrastergraph(
86122

87123
end
88124

89-
function make_weighted_graph(
125+
"""
126+
make_raster_graph(
127+
weight_raster::GeoArray,
128+
vertex_raster::GeoArray;
129+
directed::Bool = false,
130+
condition_raster::GeoArray = weight_raster,
131+
condition::Function = is_data,
132+
cardinal_neighbors_only::Bool = false,
133+
connect_using_avg_weights::Bool = true,
134+
combine::Function = min
135+
)
136+
137+
Construct a `SimpleWeightedGraph` or `SimpleWeightedDiGraph` (if
138+
`directed = true`) based on vertex and weight rasters. This function is useful
139+
if you already have a custom vertex raster and don't want SpatialGraphs.jl to
140+
make one for you. The vertex raster denotes the spatial locations of each vertex
141+
in the graph, and the weight raster denotes the edge weight correponding to each
142+
vertex. Since edges are between rather than on vertices, edge weights are
143+
calculated as the average of the weights for each vertex being connected.
144+
145+
## Parameters
146+
`weight_raster`: A `GeoData.GeoArray` containing values that, where applicable
147+
based on other arguments, determine which pixels to connect and the edge
148+
weights between pixels. Any pixel in `weight_raster` with a value not equal to
149+
`weight_raster.missingval` will be assigned a vertex in the graph (corresponding
150+
to its centroid).
151+
152+
`vertex_raster`: A `GeoData.GeoArray` with integer values ranging from 1:n,
153+
where n is the number of unique vertices in the graph.
154+
155+
##Arguments
156+
`directed`: A `Bool` determining whether the graph should be directed.
157+
158+
`condition_raster`: A raster with values that can be used to determine whether
159+
two neighboring pixels should be connected. For example, an elevation raster
160+
can be used to create a hydologic flow graph.
161+
162+
`condition`: A function that compares the values in `condition_raster` for two
163+
neighbors to determine if those neighbors should be connected. The function must
164+
compare two values and return either `true` or `false`. Useful functions to use
165+
here include `<`, `<=`, `==`, etc. The first argument to `condition` corresponds
166+
to the source vertex, and the second argument corresponds to the destination
167+
vertex. So, if you only want to connect sources to destinations with a lower
168+
value in `condition_raster` (e.g. in the case of developing a hydrologic flow
169+
graph based on elevation), then you would use `<` for `condition`. Defaults to
170+
`is_data`, which results in neighbors being connected as long as they are not
171+
NoData (`condition_raster.missingval`) in `condition_raster`. Note that if using
172+
an inequality function (or any function where the result depends on argument
173+
position), the graph must be directed. For undirected graphs, you can use either
174+
`is_data` or `==`, or any other custom function where argument position doesn't
175+
matter, e.g. a function that determines whether the values in `condition_raster`
176+
are within a certain distance of each other.
177+
178+
`cardinal_neighbors_only`: A `Bool` stating whether only cardinal neighbors
179+
should be connected. By default, both cardinal _and_ diagonal neighbors are
180+
connected. Note that when determining weights between diagonal neighbors, the
181+
increased distance between them (as compared to the distance between cardinal
182+
neighbors) is accounted for.
183+
184+
`connect_using_avg_raster_val`: `Bool`. This is intended to offer methods that
185+
complement those used in Circuitscape.jl and Omniscape.jl. In this context,
186+
weights (the values in `weight_raster`) are in units of electrical resistance.
187+
If `false`, the weight between two nodes with resistances R1 and R2 is
188+
calculated by converting resistance to conductances, taking the average, then
189+
taking the inverse of the result to convert back to resistance:
190+
`1 / ((1/R1 + 1/R2) / 2)`. `connect_using_avg_weights = false` correspondes to
191+
the default settings in Circuitscape. Defaults to `true', in which case the
192+
simple average of the weights (adjusted for distance in the case of diagonal
193+
neighbors) in `weight_raster` are used.
194+
195+
`combine`: In the case that there are multiple edges defined for a single pair
196+
of vertices, how should the weight be chosen? Defaults to `min`. See the docs
197+
for `SparseArrays.sparse()` for more information.
198+
"""
199+
function make_raster_graph(
90200
weight_raster::GeoArray,
91201
vertex_raster::GeoArray;
92202
directed::Bool = false,
93203
condition_raster::GeoArray = weight_raster,
94204
condition::Function = is_data,
95205
cardinal_neighbors_only::Bool = false,
96-
connect_using_avg_weights::Bool = true
206+
connect_using_avg_weights::Bool = true,
207+
combine::Function = min
97208
)
98209
# Which averaging function to use
99210
card_avg = connect_using_avg_weights ? res_cardinal_avg : cond_cardinal_avg
100211
diag_avg = connect_using_avg_weights ? res_diagonal_avg : cond_diagonal_avg
101-
dims = size(cost)
102-
not_no_data = cost .!= no_data_val
212+
dims = size(weight_raster)
213+
no_data_val = weight_raster.missingval
214+
not_no_data = weight_raster .!= no_data_val
103215

104-
if sum((cost .<= 0) .& not_no_data) != 0
105-
@error("cost surface contains 0 or negative values (aside " *
106-
"from the provided no_data_val), which is not supported")
216+
if sum((weight_raster .<= 0) .& not_no_data) != 0
217+
@error("weight_raster contains 0 or negative values (aside " *
218+
"from weight_raster.missingval), which is not supported")
107219
end
108220

109221
sources = Vector{Int64}()
110222
destinations = Vector{Int64}()
111223
node_weights = Vector{Float64}()
112-
224+
113225
# Add the edges
114226
# Only need to do neighbors down or to the right for undirected graphs
115227
# because edge additions will be redundant.
@@ -120,7 +232,7 @@ function make_weighted_graph(
120232
for column in 1:dims[2]
121233
source_idx = CartesianIndex((row, column))
122234
if vertex_raster[source_idx] == 0 ||
123-
cost[source_idx] == weight_raster.missingval
235+
weight_raster[source_idx] == no_data_val
124236
continue
125237
else
126238
# Cardinal destination indices needed for undirected graph
@@ -269,16 +381,16 @@ function make_weighted_graph(
269381
sources,
270382
destinations,
271383
node_weights,
272-
combine = min
384+
combine = combine
273385
)
274386
else
275387
g = SimpleWeightedGraph(
276388
sources,
277389
destinations,
278390
node_weights,
279-
combine = min
391+
combine = combine
280392
)
281393
end
282394

283395
return g
284-
end
396+
end

src/utils.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,21 @@ Returns `true`
1111
function is_data(a::Number, b::Number)
1212
return true
1313
end
14+
15+
16+
# averaging functions, to operate on resistances
17+
# cond_* functions convert to conductance, calulate mean, then convert to resistance
18+
# res_* functions directly operate on the resistances
19+
cond_cardinal_avg(x, y) = 1 / ((1/x + 1/y) / 2)
20+
cond_diagonal_avg(x, y) = 1 / ((1/x + 1/y) / (2 * 2))
21+
res_cardinal_avg(x, y) = (x + y) / 2
22+
res_diagonal_avg(x, y) = ((x + y) * 2) / 2
23+
24+
25+
# function get_dims(A::GeoData.GeoArray)
26+
# y_first = dims(mytif)[1] isa XDim
27+
# first_dim_type = y_first ? YDim : XDim
28+
# second_dim_type = y_first ? XDim : YDim
29+
30+
# (dims(A, first_dim_type), dims(A, second_dim_type), Band(1:1, Categorical(order = Ordered())))
31+
# end

0 commit comments

Comments
 (0)