Skip to content

Commit 0e8f491

Browse files
committed
first
0 parents  commit 0e8f491

20 files changed

+1037
-0
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.html

DESCRIPTION

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
Package: conexus
2+
Type: Package
3+
Title: Network approaches for biological conservation assessments
4+
Description: This package implements network approaches to assess the biological conservation assessments of species. The background is described in: El Graoui, M., M.E. Ghanem, M. Amri, R.J. Hijmans, 2025. A distance-based framework for assessing the ex-situ conservation status of plants. Submitted for publication.
5+
Version: 1.1-0
6+
Date: 2025-04-02
7+
Depends: R (>= 3.5.0)
8+
Imports: terra, igraph
9+
Suggests: knitr, rmarkdown
10+
VignetteBuilder: knitr
11+
Maintainer: Robert Hijmans <[email protected]>
12+
Encoding: UTF-8
13+
License: GPL (>=3)
14+
BugReports: https://github.com/rspatial/terra/issues/
15+
LazyLoad: yes
16+
Authors@R: c(
17+
person("Marwa", "El Graoui", role="aut"),
18+
person("Robert J.", "Hijmans", role=c("cre", "aut"), email="[email protected]", comment=c(ORCID = "0000-0003-2392-6140")))

NAMESPACE

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export(
2+
get_range, get_samplesize, make_zones, XC, dst2svect, dst2graph, GRex, ERex, SRex, FCex
3+
)

R/FCSex.R

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#In GapAnalysis::GRSex the CA50 areas outside of the SDM range are considered for the genebank area and in GapAnalysis::ERSex the number of ecoregions considered for the genebank samples includes areas that are outside the SDM.
2+
# In both cases you could end up with a score > 1. That can be corrected for, but this is inconsistent. Areas outside the range (SDM) should not be included in the analysis.
3+
# They are not included in the analysis below (but can be with "inrange=FALSE").
4+
5+
6+
GRex <- function(srange, ca, inrange) {
7+
if (inrange) {
8+
bufr <- terra::mask(srange, ca)
9+
e <- terra::expanse(c(srange, bufr), unit="km")
10+
e[2,2] / e[1,2]
11+
} else {
12+
bufr <- terra::expanse(ca, unit="km")
13+
e <- terra::expanse(srange, unit="km")
14+
out <- bufr[1,2] / e[1,2]
15+
min(1, out)
16+
}
17+
}
18+
19+
ERex <- function(srange, ca, ecoregions, inrange) {
20+
eco <- terra::mask(ecoregions, srange)
21+
ueco <- nrow(terra::unique(eco))
22+
if (inrange) {
23+
seedeco <- terra::mask(eco, ca)
24+
} else {
25+
seedeco <- terra::mask(ecoregions, ca)
26+
}
27+
seco <- nrow(terra::unique(seedeco))
28+
if (is.null(seco)) return(0)
29+
out <- seco / ueco
30+
if (inrange) {
31+
out
32+
} else {
33+
min(1, out)
34+
}
35+
}
36+
37+
SRex <- function(s, h) {
38+
if (s == 0) return(0)
39+
if (h == 0) return(1)
40+
min(1, s / h)
41+
}
42+
43+
FCex <- function(seed, nherbarium, srange, ecoregions, bsize=50, nseed_nogeo=0, inrange=TRUE) {
44+
srange <- terra::subst(srange, 0, NA)
45+
ca <- terra::buffer(seed, bsize*1000) |> terra::aggregate()
46+
ca <- terra::rasterize(ca, srange)
47+
g <- GRex(srange, ca, inrange)
48+
e <- ERex(srange, ca, ecoregions, inrange)
49+
nseed <- NROW(seed) + nseed_nogeo[1]
50+
s <- SRex(nseed, nherbarium)
51+
c(nseed=NROW(seed), nherbarium=NROW(nherbarium), nseed_nogeo=nseed_nogeo, GRex=g, ERex=e, SRex=s, FCex=mean(c(g, e, s)))
52+
}
53+

R/XC.R

+297
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
2+
3+
get_range <- function(x, sp, land, include=10, exclude=100) {
4+
5+
stopifnot(include <= exclude)
6+
7+
# km to m
8+
CAmin <- include * 1000
9+
CAmax <- exclude * 1000
10+
11+
if ((CAmax > 0) && (CAmax < Inf)) {
12+
ca_remove <- terra::buffer(sp, CAmax)
13+
x <- terra::mask(x, ca_remove, updatevalue=NA)
14+
}
15+
if (CAmin > 0) {
16+
ca_add <- terra::buffer(sp, CAmin, quadsegs=12)
17+
x <- terra::rasterize(ca_add, x, update=TRUE)
18+
}
19+
if (!is.null(land)) {
20+
x <- terra::mask(x, land)
21+
}
22+
# terra::ifel(x==1, 1, NA)
23+
x
24+
}
25+
26+
27+
28+
get_EnvDist <- function(env, regions, envfun) {
29+
e <- terra::extract(env, regions, fun=mean, na.rm=TRUE, ID=FALSE)
30+
31+
dst <- lapply(1:ncol(e), \(i) stats::dist(e[,i]))
32+
x <- do.call(data.frame, dst)
33+
names(x) <- names(env)
34+
# express environmental distance expressed as geographic distance
35+
envd <- envfun(x)
36+
structure(envd, class = 'dist', Size=attr(dst[[1]], "Size"))
37+
}
38+
39+
40+
.n_zones <- function(x, min_area, m=3) {
41+
round(pmax(1, pmin(x, m*sqrt(x))))
42+
}
43+
44+
45+
make_zones <- function(x, range, n, spread=TRUE) {
46+
47+
if (spread) { # spread sample across polygons
48+
drange <- terra::disagg(range)
49+
rr <- terra::rasterize(drange, terra::rast(x), 1:nrow(drange))
50+
p <- terra::as.polygons(rr)
51+
p$area <- terra::expanse(p, "km") / 1000
52+
avga <- sum(p$area) / n
53+
p$n <- round(p$area / avga)
54+
totn <- sum(p$n)
55+
while (totn > n) {
56+
p$error <- p$area - p$n * avga
57+
i <- which.max(p$error)
58+
p$n[i] <- p$n[i] - 1
59+
totn <- sum(p$n)
60+
}
61+
while (totn < n) {
62+
p$error <- p$area - p$n * avga
63+
i <- which.min(p$error)
64+
p$n[i] <- p$n[i] + 1
65+
totn <- sum(p$n)
66+
}
67+
p <- p[p$n > 0, ]
68+
seeds <- lapply(1:nrow(p), function(i) {
69+
out <- terra::spatSample(p[i,], p$n[i])
70+
if (nrow(out) < p$n[i]) {
71+
out <- terra::spatSample(p[i,], p$n[i] * 10)
72+
out <- out[sample(nrow(out), p$n[i]), ]
73+
}
74+
out
75+
})
76+
77+
seeds <- terra::crds(terra::vect(seeds))
78+
79+
km <- terra::k_means(terra::mask(x, rr), seeds, iter.max = 25)
80+
} else {
81+
82+
xm <- terra::mask(x, range)
83+
km <- terra::k_means(xm, n, iter.max = 25)
84+
}
85+
86+
terra::as.polygons(km)
87+
}
88+
89+
get_samplesize <- function(x, fun=NULL, ...) {
90+
if (inherits(x, "SpatRaster")) {
91+
x <- terra::as.polygons(x)
92+
x <- x[x[,1,drop=TRUE]==1]
93+
} else if (!inherits(x, "SpatVector")) {
94+
stop("x should be a SpatVector")
95+
}
96+
a <- sum(terra::expanse(x, unit="km"))
97+
if (is.null(fun)) {
98+
fun <- function(A, omega) max(1, round(omega * sqrt(A/pi)))
99+
}
100+
n <- fun(a, ...)
101+
#n <- max(nmin, n)
102+
#z <- max(1, min(n, round(a/min_area)))
103+
return(list(range=x, area=a, n=n))
104+
}
105+
106+
107+
108+
small_ssize_penalty <- function(ssize, minssize) {
109+
ifelse(ssize > minssize[1], 1, ssize / minssize[1])
110+
}
111+
112+
113+
114+
get_network <- function(regions, sample, maxdist=1500) {
115+
116+
terra::values(regions) <- data.frame(id=1:nrow(regions))
117+
patches <- terra::disagg(terra::aggregate(regions))
118+
patches$pid <- 1:nrow(patches) # pid = patch id
119+
120+
x <- terra::centroids(regions, inside=TRUE)
121+
122+
x$pid <- terra::extract(patches, x)$pid
123+
patches <- patches[unique(x$pid), ]
124+
np <- nrow(patches)
125+
126+
x <- terra::round(x, 5) # to help merge
127+
xy <- terra::crds(x)
128+
129+
adj <- terra::adjacent(regions)
130+
adj <- data.frame(unique(t(apply(adj, 1, sort))))
131+
if (ncol(adj) == 0) {
132+
adj <- data.frame(from=NA, to=NA, adj=NA)
133+
adj <- adj[0,]
134+
} else {
135+
colnames(adj) <- c("from", "to")
136+
adj$adj <- 1
137+
}
138+
if (np > 1) {
139+
up <- sort(unique(x$pid))
140+
dx <- as.matrix(terra::distance(x))
141+
diag(dx) <- NA
142+
i <- match(colnames(dx), x$id)
143+
pid <- x$pid[i]
144+
for (p in up) {
145+
s <- dx[pid != p, pid == p, drop=FALSE]
146+
rid <- pid[pid != p]
147+
upp <- sort(unique(rid))
148+
for (pp in upp) {
149+
ss <- s[rid == pp, ,drop=FALSE]
150+
j <- which.min(apply(ss, 1, min))
151+
k <- which.min(ss[j, ])
152+
add <- c(sort(as.integer(c(rownames(ss)[j], colnames(ss)[k]))), 0)
153+
add <- data.frame(from=add[1], to=add[2], adj=add[3])
154+
adj <- rbind(adj, add)
155+
}
156+
}
157+
adj <- unique(adj)
158+
}
159+
160+
colnames(xy) <- c("xf", "yf")
161+
adj <- cbind(adj, xy[adj$from, , drop=FALSE])
162+
colnames(xy) <- c("xt", "yt")
163+
adj <- cbind(adj, xy[adj$to, , drop=FALSE])
164+
165+
adj <- cbind(adj, w=1, dst=terra::distance(x[adj$from, ], x[adj$to, ], pairwise=TRUE, unit="m")/1000)
166+
167+
if (np > 2) {
168+
padj <- adj[adj$adj == 0, ]
169+
adj <- adj[adj$adj != 0, ]
170+
nx <- nrow(x)
171+
padj <- padj[order(padj$dst), ]
172+
g <- igraph::components( igraph::graph_from_data_frame(adj, directed = FALSE) )
173+
for (i in 1:nrow(padj)) {
174+
adj2 <- rbind(adj, padj[i,])
175+
gg <- igraph::components( igraph::graph_from_data_frame(adj2, directed = FALSE) )
176+
if ((gg$no < g$no) | (length(gg$membership) > length(g$membership))) {
177+
g <- gg
178+
adj <- adj2
179+
}
180+
}
181+
}
182+
183+
# mxd <- median(adj$dst) * 3
184+
# adj$dst[adj$dst > mxd]] <- mxd
185+
186+
adj$dst[adj$dst > maxdist] <- maxdist
187+
adj
188+
}
189+
190+
191+
dst2svect <- function(x) {
192+
m <- as.matrix(x[, c("xf", "yf", "xt", "yt")])
193+
a <- lapply(1:nrow(m), \(i) cbind(i, matrix(m[i,], nrow=2, byrow=2)))
194+
b <- do.call(rbind, a)
195+
v <- terra::vect(b, "lines", crs="lonlat")
196+
terra::values(v) <- x
197+
v
198+
}
199+
200+
dst2graph <- function(x) {
201+
gg <- igraph::graph_from_data_frame(x, directed = FALSE)
202+
igraph::E(gg)$weight <- x$dst * x$w
203+
gg
204+
}
205+
206+
207+
add2rr <- function(rr, pair) {
208+
add <- rr[0,]
209+
add[1,1:2] <- as.integer(pair)
210+
j <- which(rr$from == add[1,1])[1]
211+
if (is.na(j)) {
212+
j <- which(rr$to == add[1,1])[1]
213+
if (!is.na(j)) {
214+
add[, c("xf", "yf")] <- rr[j, c("xt", "yt")]
215+
}
216+
} else {
217+
add[, c("xf", "yf")] <- rr[j, c("xf", "yf")]
218+
}
219+
j <- which(rr$from == add[1,2])[1]
220+
if (is.na(j)) {
221+
j <- which(rr$to == add[1,2])[1]
222+
if (!is.na(j)) {
223+
add[, c("xt", "yt")] <- rr[j, c("xt", "yt")]
224+
}
225+
} else {
226+
add[, c("xt", "yt")] <- rr[j, c("xf", "yf")]
227+
}
228+
add$w <- add$dst <- 0
229+
rbind(rr, add)
230+
}
231+
232+
XC <- function(regions, seed, env=NULL, envfun=NULL, minssize=10, maxdist=1500) {
233+
234+
## TODO RH
235+
# refine the adjust effect such that when you have many observations in one zones
236+
# they can only contribute to their neighbors. Do not increase branch length to avoid that
237+
# one region does not compensate for another
238+
239+
rr <- get_network(regions, seed, maxdist)
240+
241+
if (!is.null(env)) {
242+
envd <- as.matrix(get_EnvDist(env, regions, envfun))
243+
rr$envdst <- envd[as.matrix(rr[,1:2])]
244+
rr$geodst <- rr$dst
245+
# sum geo and env dist
246+
rr$dst <- rr$dst + rr$envdst
247+
}
248+
gg <- igraph::graph_from_data_frame(rr, directed = FALSE)
249+
igraph::E(gg)$weight <- rr$dst * rr$w
250+
y <- unique(terra::extract(regions, seed)[,2])
251+
rr$w <- rowSums(!matrix(as.matrix(rr[,1:2]) %in% y, ncol=2)) / 2
252+
igraph::E(gg)$weight2 <- rr$dst * rr$w
253+
254+
if (nrow(seed) <= 0) {
255+
rr$weight <- rr$w * rr$dst
256+
return(list(XC=0, dist=rr))
257+
}
258+
259+
260+
n <- igraph::count_components(gg)
261+
score <- nodes <- rep(NA, n)
262+
dg <- igraph::decompose(gg)
263+
for (k in 1:n) {
264+
g <- dg[[k]]
265+
dst <- igraph::distances(g)
266+
d1 <- sum(dst)
267+
268+
igraph::E(g)$weight <- igraph::E(g)$weight2
269+
270+
if (length(y) > 1) {
271+
b <- utils::combn(as.character(y), 2)
272+
nms <- igraph::V(g)$name
273+
haveb <- apply(matrix(b %in% nms, nrow=2), 2, all)
274+
b <- b[,haveb,drop=FALSE]
275+
if (ncol(b) > 0) {
276+
for (i in 1:ncol(b)) {
277+
if (!igraph::are_adjacent(g, b[1,i], b[2,i])) {
278+
g <- igraph::add_edges(g, b[,i], weight=0)
279+
rr <- add2rr(rr, b[,i])
280+
}
281+
}
282+
}
283+
}
284+
d2 <- igraph::distances(g)
285+
score[k] <- 1 - (sum(d2) / d1)
286+
nodes[k] <- length(g)
287+
}
288+
score <- stats::weighted.mean(score, nodes)
289+
290+
if (minssize > 0) {
291+
score <- score * small_ssize_penalty(length(seed), minssize)
292+
}
293+
294+
rr$weight <- rr$w * rr$dst
295+
list(XC=score, dist=rr)
296+
}
297+

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# connexus
2+
3+
This is the repo for *R* package connexus.
4+
5+
This package implements network approaches to assess the biological conservation assessments of species. The background is described in: El Graoui, M., M.E. Ghanem, M. Amri, R.J. Hijmans, 2025. A distance-based framework for assessing the ex-situ conservation status of plants. Submitted for publication.
6+

inst/ex/env.tif

63.4 KB
Binary file not shown.

inst/ex/land.rds

2.34 KB
Binary file not shown.

inst/ex/m_prc.rds

104 KB
Binary file not shown.

inst/ex/m_tmp.rds

102 KB
Binary file not shown.

inst/ex/sp.rds

1.31 KB
Binary file not shown.

inst/ex/suitable.tif

3.67 KB
Binary file not shown.

0 commit comments

Comments
 (0)