1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * bezierCurve() |
|
4 | ||
5 |
#' Calculate the coordinates of a Bezier curve from control points |
|
6 |
#' |
|
7 |
#' @param x,y Coordinates of the control points |
|
8 |
#' @param n Integer, number of steps to use (larger is smoother) |
|
9 |
#' |
|
10 |
#' @return A two-column matrix with the coordinates of the Bezier curve |
|
11 |
#' |
|
12 |
#' @examples |
|
13 |
#' bezierCurve <- isotracer:::bezierCurve |
|
14 |
#' set.seed(9) |
|
15 |
#' x = runif(6) |
|
16 |
#' y = runif(6) |
|
17 |
#' plot(x, y, type = "l", col = "grey") |
|
18 |
#' lines(bezierCurve(x, y, 2), col = "cornflowerblue", lwd = 1) |
|
19 |
#' lines(bezierCurve(x, y, 4), col = "cornflowerblue", lwd = 1) |
|
20 |
#' lines(bezierCurve(x, y, 8), col = "purple", lwd = 1.5) |
|
21 |
#' lines(bezierCurve(x, y, 64), col = "indianred", lwd = 4) |
|
22 |
#' |
|
23 |
#' @keywords internal |
|
24 |
#' @noRd |
|
25 | ||
26 |
bezierCurve = function(x, y, n) { |
|
27 | 67x |
stopifnot(length(x) == length(y)) |
28 | 67x |
stopifnot(length(x) > 1) |
29 | 67x |
n = as.integer(n) |
30 | 67x |
stopifnot(n > 0) |
31 | 67x |
xOut = rep(NA, n + 2) |
32 | 67x |
yOut = rep(NA, n + 2) |
33 | 67x |
weights = seq(0, 1, length.out = n + 2) |
34 |
# Calculate coordinates |
|
35 | 67x |
for (i in seq_along(weights)) { |
36 | 2278x |
xControl = x |
37 | 2278x |
yControl = y |
38 | 2278x |
nControl = length(xControl) |
39 | 2278x |
weight = weights[i] |
40 | 2278x |
while(nControl > 1) { |
41 | 11390x |
xControl = (1 - weight) * xControl[1:(nControl - 1)] + weight * xControl[2:nControl] |
42 | 11390x |
yControl = (1 - weight) * yControl[1:(nControl - 1)] + weight * yControl[2:nControl] |
43 | 11390x |
nControl = length(xControl) |
44 |
} |
|
45 | 2278x |
stopifnot(nControl == 1) |
46 | 2278x |
xOut[i] = xControl |
47 | 2278x |
yOut[i] = yControl |
48 |
} |
|
49 |
# Return |
|
50 | 67x |
return(as.matrix(cbind(xOut, yOut))) |
51 |
} |
|
52 | ||
53 |
### * ribbonFromTrajectory() |
|
54 | ||
55 |
#' Calculate a ribbon polygon spanning a trajectory |
|
56 |
#' |
|
57 |
#' @param x Two-column object giving the coordinates defining the indicating |
|
58 |
#' trajectory. |
|
59 |
#' @param width Numeric, width of the returned segment on the x scale. |
|
60 |
#' @param constantWidth.y Boolean, if TRUE the ribbon coordinates are corrected |
|
61 |
#' for the aspect ratio of the plot so that the ribbon has a constant y |
|
62 |
#' width. |
|
63 |
#' |
|
64 |
#' @return A two-columnm matrix containing the coordinates of the ribbon, ready |
|
65 |
#' to be used with \code{\link{polygon}} |
|
66 |
#' |
|
67 |
#' @examples |
|
68 |
#' bezierCurve <- isotracer:::bezierCurve |
|
69 |
#' ribbonFromTrajectory <- isotracer:::ribbonFromTrajectory |
|
70 |
#' |
|
71 |
#' set.seed(8) |
|
72 |
#' x = runif(4) |
|
73 |
#' y = runif(4) |
|
74 |
#' b = bezierCurve(x, y, 64) |
|
75 |
#' |
|
76 |
#' plot(x, y, type = "l", col = "grey", asp = 1, main = "Plot asp = 1") |
|
77 |
#' lines(b, col = "indianred", lwd = 2) |
|
78 |
#' bvp <- gridBase::baseViewports() |
|
79 |
#' do.call(grid::pushViewport, bvp) |
|
80 |
#' polygon(ribbonFromTrajectory(b, width = 0.05), |
|
81 |
#' col = adjustcolor("cornflowerblue", alpha.f = 0.5)) |
|
82 |
#' points(ribbonFromTrajectory(b, width = 0.05), pch = 19, cex = 0.5) |
|
83 |
#' points(ribbonFromTrajectory(b, width = 0.10), pch = 19, cex = 0.5, col = "red") |
|
84 |
#' |
|
85 |
#' plot(x, y, type = "l", col = "grey", asp = 3, main = "Plot asp = 3, constant y width") |
|
86 |
#' lines(b, col = "indianred", lwd = 2) |
|
87 |
#' polygon(ribbonFromTrajectory(b, width = 0.05), |
|
88 |
#' col = adjustcolor("cornflowerblue", alpha.f = 0.5)) |
|
89 |
#' |
|
90 |
#' plot(x, y, type = "l", col = "grey", asp = 3, main = "Plot asp = 3, no constant y width") |
|
91 |
#' lines(b, col = "indianred", lwd = 2) |
|
92 |
#' polygon(ribbonFromTrajectory(b, width = 0.05, constantWidth.y = FALSE), |
|
93 |
#' col = adjustcolor("cornflowerblue", alpha.f = 0.5)) |
|
94 |
#' |
|
95 |
#' @keywords internal |
|
96 |
#' @noRd |
|
97 | ||
98 |
ribbonFromTrajectory = function(x, width, constantWidth.y = TRUE) { |
|
99 | 134x |
xIn = x |
100 | 134x |
x = xIn[,1] |
101 | 134x |
y = xIn[,2] |
102 | 134x |
n = length(x) |
103 |
# Get the plot aspect ratio (this will probably fail if no plot exists yet) |
|
104 | 134x |
if (constantWidth.y) { |
105 |
## # https://stat.ethz.ch/pipermail/r-help/2005-October/080598.html |
|
106 |
## wRes = par("pin")[1] / diff(par("usr")[c(1, 2)]) |
|
107 |
## hRes = par("pin")[2] / diff(par("usr")[c(3, 4)]) |
|
108 |
## aspectRatio = hRes / wRes |
|
109 | 134x |
aspectRatio <- grid_get_asp() |
110 |
} else { |
|
111 |
# Use default value of one |
|
112 | ! |
aspectRatio = 1 |
113 |
} |
|
114 |
# Convert x values to asp=1 space |
|
115 | 134x |
xCenter = mean(x) |
116 | 134x |
x = (x - xCenter) / aspectRatio |
117 |
# Calculate one way |
|
118 | 134x |
x0 = x |
119 | 134x |
y0 = y |
120 | 134x |
xNormal = diff(x0) |
121 | 134x |
yNormal = diff(y0) |
122 | 134x |
xyNorm = sqrt(xNormal^2 + yNormal^2) |
123 | 134x |
thetas = acos(xNormal / xyNorm) |
124 | 134x |
thetas[yNormal < 0] = - thetas[yNormal < 0] |
125 | 134x |
out = matrix(ncol = 4, nrow = n) # Columns: xleft, yleft, xright, yright |
126 | 134x |
for (i in 1:(n-1)) { |
127 | 4422x |
initLeft = c(-width/2, 0) |
128 | 4422x |
initRight = c(width/2, 0) |
129 |
# Rotation |
|
130 | 4422x |
myAngle = thetas[i] + pi/2 |
131 | 4422x |
rotationMatrix = matrix(c(cos(myAngle), - sin(myAngle), |
132 | 4422x |
sin(myAngle), cos(myAngle)), |
133 | 4422x |
ncol = 2, byrow = T) |
134 | 4422x |
rotLeft = rotationMatrix %*% initLeft |
135 | 4422x |
rotRight = rotationMatrix %*% initRight |
136 | 4422x |
left = rotLeft + c(x0[i], y0[i]) |
137 | 4422x |
right = rotRight + c(x0[i], y0[i]) |
138 | 4422x |
out[i, ] = c(left, right) |
139 |
} |
|
140 |
# Add the last segment |
|
141 | 134x |
out[n, ] = c(rotLeft + c(x0[n], y0[n]), rotRight + c(x0[n], y0[n])) |
142 |
# Calculate the other way |
|
143 | 134x |
x0 = rev(x) |
144 | 134x |
y0 = rev(y) |
145 | 134x |
xNormal = diff(x0) |
146 | 134x |
yNormal = diff(y0) |
147 | 134x |
xyNorm = sqrt(xNormal^2 + yNormal^2) |
148 | 134x |
thetas = acos(xNormal / xyNorm) |
149 | 134x |
thetas[yNormal < 0] = - thetas[yNormal < 0] |
150 | 134x |
out2 = matrix(ncol = 4, nrow = n) # Columns: xleft, yleft, xright, yright |
151 | 134x |
for (i in 1:(n-1)) { |
152 | 4422x |
initLeft = c(-width/2, 0) |
153 | 4422x |
initRight = c(width/2, 0) |
154 |
# Rotation |
|
155 | 4422x |
myAngle = thetas[i] + pi/2 |
156 | 4422x |
rotationMatrix = matrix(c(cos(myAngle), - sin(myAngle), |
157 | 4422x |
sin(myAngle), cos(myAngle)), |
158 | 4422x |
ncol = 2, byrow = T) |
159 | 4422x |
rotLeft = rotationMatrix %*% initLeft |
160 | 4422x |
rotRight = rotationMatrix %*% initRight |
161 | 4422x |
left = rotLeft + c(x0[i], y0[i]) |
162 | 4422x |
right = rotRight + c(x0[i], y0[i]) |
163 | 4422x |
out2[i, ] = c(left, right) |
164 |
} |
|
165 |
# Add the last segment |
|
166 | 134x |
out2[n, ] = c(rotLeft + c(x0[n], y0[n]), rotRight + c(x0[n], y0[n])) |
167 |
# Average both out matrices |
|
168 | 134x |
out2 = out2[nrow(out2):1, ] |
169 | 134x |
outTmp = out2 |
170 | 134x |
out2[, 1:2] = out2[, 3:4] |
171 | 134x |
out2[, 3:4] = outTmp[, 1:2] |
172 | 134x |
for (i in 1:nrow(out)) { |
173 | 4556x |
for (j in 1:ncol(out)) { |
174 | 18224x |
out2[i, j] = (out[i, j] + out2[i, j]) / 2 |
175 |
} |
|
176 |
} |
|
177 |
# Arrange format |
|
178 | 134x |
out = rbind(out2[, 1:2], out2[nrow(out2):1, 3:4]) |
179 |
# Back-convert to plot aspect ratio space |
|
180 | 134x |
out[,1] = out[,1]*aspectRatio + xCenter |
181 |
# Return |
|
182 | 134x |
return(out) |
183 |
} |
|
184 | ||
185 |
### * grid_get_asp() |
|
186 | ||
187 |
#' Get the aspect ratio of the current grid viewport |
|
188 |
#' |
|
189 |
#' @examples |
|
190 |
#' grid_get_asp <- isotracer:::grid_get_asp |
|
191 |
#' |
|
192 |
#' library(grid) |
|
193 |
#' grid.newpage() |
|
194 |
#' x <- mtcars$wt |
|
195 |
#' y <- mtcars$drat |
|
196 |
#' vp <- dataViewport(xData = x, yData = y) |
|
197 |
#' pushViewport(vp) |
|
198 |
#' grid.points(x, y) |
|
199 |
#' |
|
200 |
#' # Run this snippet several times, and resize the plot window in-between |
|
201 |
#' # The newly drawn rectangle should always be exactly square. |
|
202 |
#' width <- 1 |
|
203 |
#' asp <- grid_get_asp() |
|
204 |
#' height <- width / asp |
|
205 |
#' col <- sample(colors(), 1) |
|
206 |
#' grid.rect(width = unit(width, "native"), height = unit(height, "native")) |
|
207 |
#' |
|
208 |
#' @keywords internal |
|
209 |
#' @noRd |
|
210 | ||
211 |
grid_get_asp <- function() { |
|
212 |
# Get x and y ranges |
|
213 | 134x |
vp <- grid::current.viewport() |
214 | 134x |
xrange <- diff(vp$xscale) |
215 | 134x |
yrange <- diff(vp$yscale) |
216 | 134x |
span <- min(xrange, yrange) / 2.5 |
217 | 134x |
xmid <- mean(vp$xscale) |
218 | 134x |
ymid <- mean(vp$yscale) |
219 | 134x |
loc_mid <- grid::deviceLoc(grid::unit(xmid, "native"), |
220 | 134x |
grid::unit(ymid, "native")) |
221 | 134x |
loc_right <- grid::deviceLoc(grid::unit(xmid + span, "native"), |
222 | 134x |
grid::unit(ymid, "native")) |
223 | 134x |
loc_top <- grid::deviceLoc(grid::unit(xmid, "native"), |
224 | 134x |
grid::unit(ymid + span, "native")) |
225 | 134x |
height <- as.numeric(loc_top$y) - as.numeric(loc_mid$y) |
226 | 134x |
width <- as.numeric(loc_right$x) - as.numeric(loc_mid$x) |
227 | 134x |
return(height/width) |
228 |
} |
|
229 | ||
230 |
### * topo_has_loop() |
|
231 | ||
232 |
#' Test if a topology has a loop |
|
233 |
#' |
|
234 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
235 |
#' is used in this case). |
|
236 |
#' |
|
237 |
#' @return Boolean |
|
238 |
#' |
|
239 |
#' @examples |
|
240 |
#' topo_has_loop <- isotracer:::topo_has_loop |
|
241 |
#' |
|
242 |
#' topo_has_loop(aquarium_mod) |
|
243 |
#' topo_has_loop(trini_mod) |
|
244 |
#' |
|
245 |
#' @keywords internal |
|
246 |
#' @noRd |
|
247 | ||
248 |
topo_has_loop <- function(x) { |
|
249 | 10x |
if (is(x, "networkModel")) { |
250 | ! |
x <- unique(topo(x, simplify = FALSE)) |
251 | ! |
stopifnot(length(x) == 1) |
252 | ! |
x <- x[[1]] |
253 |
} |
|
254 | 10x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
255 | 10x |
n_comps <- ncol(x) |
256 |
# Calculate powers of the transition matrix |
|
257 | 10x |
iters <- list() |
258 | 10x |
iters[[1]] <- x |
259 | 10x |
if (n_comps == 1) { |
260 | ! |
stop("Only one compartment present in the network topology.") |
261 |
} |
|
262 | 10x |
for (i in 2:n_comps) { |
263 | 73x |
iters[[i]] <- iters[[i-1]] %*% x |
264 |
} |
|
265 |
# Look for loops |
|
266 | 10x |
loops <- lapply(iters, diag) |
267 | 10x |
any_loop <- sapply(loops, function(l) any(l!=0)) |
268 |
# Return |
|
269 | 10x |
return(any(any_loop)) |
270 |
} |
|
271 | ||
272 |
is_dag <- function(x) { |
|
273 | 6x |
!topo_has_loop(x) |
274 |
} |
|
275 | ||
276 |
### * not_implemented_for_topology_with_loop() |
|
277 | ||
278 |
#' Check and throw an error if appropriate |
|
279 |
#' |
|
280 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
281 |
#' is used in this case). |
|
282 |
#' |
|
283 |
#' @keywords internal |
|
284 |
#' @noRd |
|
285 | ||
286 |
not_implemented_for_topology_with_loop <- function(x) { |
|
287 | 4x |
if (topo_has_loop(x)) { |
288 | ! |
stop("Function not implemented for topologies containing loop(s).") |
289 |
} |
|
290 |
} |
|
291 | ||
292 |
### * sankey_get_layout() |
|
293 | ||
294 |
#' Process layout option for a given topology |
|
295 |
#' |
|
296 |
#' @param x A topology. |
|
297 |
#' @param layout String or NULL, node-placing algorithm to use from the ggraph |
|
298 |
#' package (e.g. "stress" or "sugiyama"). If NULL, a default layout is |
|
299 |
#' returned, which depends on the topology. The ggraph package itself uses |
|
300 |
#' some algoritms from the igraph package. See the Details in the help of |
|
301 |
#' \code{\link[ggraph]{layout_tbl_graph_igraph}} for available |
|
302 |
#' algorithms. The ggraph package must be installed for this argument to be |
|
303 |
#' taken into account. |
|
304 |
#' |
|
305 |
#' @return A string |
|
306 |
#' |
|
307 |
#' @keywords internal |
|
308 |
#' @noRd |
|
309 | ||
310 |
sankey_get_layout <- function(x, layout) { |
|
311 | 8x |
if (is.null(layout)) { |
312 | 6x |
if (is_dag(x)) { |
313 | ! |
layout <- "left2right" |
314 |
} else { |
|
315 | 6x |
layout <- "stress" |
316 |
} |
|
317 |
} |
|
318 | 8x |
return(layout) |
319 |
} |
|
320 | ||
321 | ||
322 |
### * sankey_calc_nodes_locations() |
|
323 | ||
324 |
#' Calculate nodes locations for a topology |
|
325 |
#' |
|
326 |
#' @param x A topology. |
|
327 |
#' @param layout String, one of "in-house" or the layout options accepted by |
|
328 |
#' \code{\link[ggraph]{create_layout}}. |
|
329 |
#' @param minimize_simple (For in-house algorithm.) Boolean, apply simple |
|
330 |
#' energy minimization by adjusting consumers location based on their |
|
331 |
#' sources y location? |
|
332 |
#' @param minimize_E Boolean, use \code{\link{sankey_minimize_energy_sim}} to |
|
333 |
#' adjust node locations using an energy minimization algorithm? |
|
334 |
#' @param k1 Numeric, stiffness for springs between nodes on the same x |
|
335 |
#' position (used if minimize_E = TRUE). |
|
336 |
#' @param k2 Numeric, sitffness for springs between connected nodes (used if |
|
337 |
#' minimize_E = TRUE). |
|
338 |
#' @param l1 Numeric, rest length for springs with k1 stiffness (used if |
|
339 |
#' minimize_E = TRUE). |
|
340 |
#' @param l2 Numeric, rest length for springs with k2 stiffness (used if |
|
341 |
#' minimize_E = TRUE). |
|
342 |
#' |
|
343 |
#' @return A tibble with the nodes and ordered x and y values. The columns are |
|
344 |
#' "comp", "x", and "y". |
|
345 |
#' |
|
346 |
#' @examples |
|
347 |
#' sankey_calc_nodes_locations <- isotracer:::sankey_calc_nodes_locations |
|
348 |
#' |
|
349 |
#' sankey_calc_nodes_locations(topo(trini_mod), "left2right") |
|
350 |
#' sankey_calc_nodes_locations(topo(aquarium_mod), "stress") |
|
351 |
#' |
|
352 |
#' @keywords internal |
|
353 |
#' @noRd |
|
354 | ||
355 |
sankey_calc_nodes_locations <- function(x, layout, |
|
356 |
minimize_simple = TRUE, |
|
357 |
minimize_E = FALSE, k1 = 1, k2 = 1, l1 = 1, l2 = 1) { |
|
358 | 8x |
if (layout %in% c("left2right", "left-to-right")) { |
359 |
# In-house algorithm for topologies without loops |
|
360 | 2x |
not_implemented_for_topology_with_loop(x) |
361 | 2x |
nodes <- sankey_calc_nodes_locations_inhouse(x, minimize_simple = minimize_simple) |
362 | 2x |
if (minimize_E) { |
363 | ! |
nodes <- sankey_minimize_energy_sim(x, nodes, k1 = k1, k2 = k2, l1 = l1, |
364 | ! |
l2 = l2) |
365 |
} |
|
366 |
} else { |
|
367 | 6x |
if (!requireNamespace("ggraph", quietly = TRUE)) { |
368 | ! |
stop("Package \"ggraph\" needed when specifying a layout argument. Please install it.", |
369 | ! |
call. = FALSE) |
370 |
} |
|
371 |
# Check that the random seed is not reset by graphlayouts functions |
|
372 | 6x |
if (exists(".Random.seed", .GlobalEnv)) { |
373 | 6x |
prev_seed <- .GlobalEnv[[".Random.seed"]] |
374 | ! |
} else { prev_seed <- NULL } |
375 | 6x |
x <- as_tbl_graph(x) |
376 | 6x |
nodes <- ggraph::create_layout(graph = x, layout = layout) |
377 | 6x |
if (exists(".Random.seed", .GlobalEnv)) { |
378 | 6x |
new_seed <- .GlobalEnv[[".Random.seed"]] |
379 | ! |
} else { new_seed <- NULL } |
380 | 6x |
if (!identical(prev_seed, new_seed)) { |
381 | ! |
stop("Random seed was reset when calling ggraph::create_layout() in sankey_calc_nodes_locations().\n", |
382 | ! |
"This is unwanted behaviour, please report it as a bug to the isotracer authors.") |
383 |
} |
|
384 | 6x |
nodes <- tibble::as_tibble(nodes[, c("name", "x", "y")]) |
385 | 6x |
names(nodes) <- c("comp", "x", "y") |
386 |
} |
|
387 | 8x |
attr(nodes, "layout") <- layout |
388 | 8x |
return(nodes) |
389 |
} |
|
390 | ||
391 |
### * sankey_calc_nodes_locations_inhouse() |
|
392 | ||
393 |
#' Calculate roughly optimized nodes locations for a topology (in-house algorithm) |
|
394 |
#' |
|
395 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
396 |
#' is used in this case). |
|
397 |
#' @param minimize_simple Boolean, apply simple energy minimization by |
|
398 |
#' adjusting consumers location based on their sources y location. |
|
399 |
#' |
|
400 |
#' @return A tibble with the nodes and ordered x and y values. |
|
401 |
#' |
|
402 |
#' @keywords internal |
|
403 |
#' @noRd |
|
404 | ||
405 |
sankey_calc_nodes_locations_inhouse <- function(x, minimize_simple) { |
|
406 | 2x |
if (is(x, "networkModel")) { |
407 | ! |
x <- unique(topo(x, simplify = FALSE)) |
408 | ! |
stopifnot(length(x) == 1) |
409 | ! |
x <- x[[1]] |
410 |
} |
|
411 | 2x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
412 | 2x |
not_implemented_for_topology_with_loop(x) |
413 | 2x |
z <- topo_x_ordering(x) |
414 | 2x |
comps <- names(z) |
415 | 2x |
d <- tibble::tibble(comp = comps, x = z) |
416 | 2x |
nodes <- topo_y_ordering(x, d) |
417 |
# Center y locations |
|
418 | 2x |
x_ranks <- unique(nodes$x) |
419 | 2x |
for (xi in x_ranks) { |
420 | 8x |
y_shift <- mean(nodes$y[nodes$x == xi]) |
421 | 8x |
nodes$y[nodes$x == xi] <- nodes$y[nodes$x == xi] - y_shift |
422 |
} |
|
423 |
# Adjust y locations |
|
424 | 2x |
if (minimize_simple) { |
425 | 2x |
for (xi in x_ranks) { |
426 | 8x |
comps <- nodes$comp[nodes$x == xi] |
427 | 8x |
sources <- colnames(x)[apply(x[comps, , drop = FALSE], 2, sum) > 0] |
428 | 8x |
if (length(sources) > 0) { |
429 | 6x |
mean_y_sources <- mean(nodes$y[nodes$comp %in% sources]) |
430 | 6x |
nodes$y[nodes$x == xi] <- nodes$y[nodes$x == xi] + mean_y_sources |
431 |
} |
|
432 |
} |
|
433 |
} |
|
434 | 2x |
return(nodes) |
435 |
} |
|
436 | ||
437 |
### * topo_x_ordering() |
|
438 | ||
439 |
#' Order compartments from left to right |
|
440 |
#' |
|
441 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
442 |
#' is used in this case). |
|
443 |
#' |
|
444 |
#' @examples |
|
445 |
#' topo_x_ordering <- isotracer:::topo_x_ordering |
|
446 |
#' |
|
447 |
#' sort(topo_x_ordering(trini_mod)) |
|
448 |
#' |
|
449 |
#' @keywords internal |
|
450 |
#' @noRd |
|
451 | ||
452 |
topo_x_ordering <- function(x) { |
|
453 | 2x |
if (is(x, "networkModel")) { |
454 | ! |
x <- unique(topo(x, simplify = FALSE)) |
455 | ! |
stopifnot(length(x) == 1) |
456 | ! |
x <- x[[1]] |
457 |
} |
|
458 | 2x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
459 | 2x |
n_comps <- ncol(x) |
460 | 2x |
max_iters <- 2 * n_comps |
461 | 2x |
links <- which(x > 0) |
462 | 2x |
from <- links %/% n_comps + 1 |
463 | 2x |
to <- links %% n_comps |
464 | 2x |
links <- tibble::tibble(from = from, to = to) |
465 | 2x |
for (i in seq_len(nrow(links))) { |
466 | 34x |
if (links$to[i] == 0) { |
467 | 2x |
links$from[i] <- links$from[i] - 1 |
468 | 2x |
links$to[i] <- n_comps |
469 |
} |
|
470 | 34x |
stopifnot(x[links$to[i], links$from[i]] > 0) |
471 |
} |
|
472 | 2x |
ranks <- rep(0, n_comps) |
473 | 2x |
stable <- FALSE |
474 | 2x |
iter <- 0 |
475 | 2x |
while (!stable & iter <= max_iters) { |
476 | 8x |
prev_ranks <- ranks |
477 | 8x |
for (i in seq_len(nrow(links))) { |
478 | 136x |
if (ranks[links$from[i]] >= ranks[links$to[i]]) { |
479 | 44x |
ranks[links$to[i]] <- ranks[links$to[i]] + 1 |
480 |
} |
|
481 |
} |
|
482 | 8x |
stable <- all(prev_ranks == ranks) |
483 | 8x |
iter <- iter + 1 |
484 |
} |
|
485 | 2x |
if (iter == (max_iters + 1)) { |
486 | ! |
stop("Maximum number of iterations reached.") |
487 |
} |
|
488 | 2x |
out <- setNames(ranks, colnames(x)) |
489 | 2x |
return(out) |
490 |
} |
|
491 | ||
492 |
### * topo_y_ordering() |
|
493 | ||
494 |
#' Order compartment vertically |
|
495 |
#' |
|
496 |
#' This function tries to minimize the number of edges crossings for graphical |
|
497 |
#' display of the topology. |
|
498 |
#' |
|
499 |
#' This function assumes that the x locations given in \code{nodes} have been |
|
500 |
#' established using \code{\link{topo_x_ordering}}. |
|
501 |
#' |
|
502 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
503 |
#' is used in this case). |
|
504 |
#' @param nodes A tibble giving the x locations for each node. It must contain |
|
505 |
#' columns "comp" and "x". |
|
506 |
#' @param n_iters Number of iterations used to try to optimize the compartment |
|
507 |
#' ordering. |
|
508 |
#' |
|
509 |
#' @return An updated tibble similar to the \code{nodes} argument. |
|
510 |
#' |
|
511 |
#' @examples |
|
512 |
#' topo_x_ordering <- isotracer:::topo_x_ordering |
|
513 |
#' topo_y_ordering <- isotracer:::topo_y_ordering |
|
514 |
#' |
|
515 |
#' z <- topo(trini_mod) |
|
516 |
#' x <- topo_x_ordering(z) |
|
517 |
#' comps <- names(x) |
|
518 |
#' d <- tibble::tibble(comp = comps, x = x) |
|
519 |
#' nodes <- topo_y_ordering(z, d) |
|
520 |
#' |
|
521 |
#' @keywords internal |
|
522 |
#' @noRd |
|
523 | ||
524 |
topo_y_ordering <- function(x, nodes, n_iters = 2) { |
|
525 | 2x |
if (is(x, "networkModel")) { |
526 | ! |
x <- unique(topo(x, simplify = FALSE)) |
527 | ! |
stopifnot(length(x) == 1) |
528 | ! |
x <- x[[1]] |
529 |
} |
|
530 | 2x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
531 | 2x |
nodes$y <- seq_len(nrow(nodes)) |
532 | 2x |
nodes <- nodes[order(nodes$x, nodes$y), ] |
533 | 2x |
nodes$id <- seq_len(nrow(nodes)) |
534 | 2x |
comp_id <- setNames(nodes$id, nm = nodes$comp) |
535 | 2x |
counts <- setNames(topo_count_crossings(x, nodes), |
536 | 2x |
paste0(nodes$id, collapse = "-")) |
537 | 2x |
x_ranks <- unique(nodes$x) |
538 | 2x |
stopifnot(length(x_ranks) > 1) |
539 | 2x |
attempts <- 0 |
540 | 2x |
for (iter in seq_len(n_iters)) { |
541 |
# Forward walk |
|
542 | 4x |
for (i in 1:length(x_ranks)) { |
543 | 16x |
focal_comps <- nodes$comp[nodes$x == x_ranks[i]] |
544 | 16x |
focal_y <- nodes$y[nodes$x == x_ranks[i]] |
545 | 16x |
this_rank_counts <- vector() |
546 | 16x |
for (ci in focal_comps) { |
547 | 56x |
swaps <- insert_in_between(ci, focal_comps[focal_comps != ci]) |
548 | 56x |
for (k in seq_along(swaps)) { |
549 | 240x |
new_focal_comps <- swaps[[k]] |
550 | 240x |
nodes$y[match(new_focal_comps, nodes$comp)] <- focal_y |
551 | 240x |
count_name <- paste0(nodes$id[order(nodes$x, nodes$y)], |
552 | 240x |
collapse = "-") |
553 | 240x |
counts[count_name] <- topo_count_crossings(x, nodes) |
554 | 240x |
this_rank_counts[count_name] <- counts[count_name] |
555 |
} |
|
556 |
} |
|
557 | 16x |
this_rank_best <- names(this_rank_counts)[which.min(this_rank_counts)] |
558 | 16x |
this_rank_best_order <- as.numeric(strsplit(this_rank_best, "-")[[1]]) |
559 | 16x |
nodes <- nodes[match(this_rank_best_order, nodes$id), ] |
560 | 16x |
nodes$y <- seq_len(nrow(nodes)) |
561 |
} |
|
562 |
# Backward walk |
|
563 | 4x |
for (i in length(x_ranks):1) { |
564 | 16x |
focal_comps <- nodes$comp[nodes$x == x_ranks[i]] |
565 | 16x |
focal_y <- nodes$y[nodes$x == x_ranks[i]] |
566 | 16x |
this_rank_counts <- vector() |
567 | 16x |
for (ci in focal_comps) { |
568 | 56x |
swaps <- insert_in_between(ci, focal_comps[focal_comps != ci]) |
569 | 56x |
for (k in seq_along(swaps)) { |
570 | 240x |
new_focal_comps <- swaps[[k]] |
571 | 240x |
nodes$y[match(new_focal_comps, nodes$comp)] <- focal_y |
572 | 240x |
count_name <- paste0(nodes$id[order(nodes$x, nodes$y)], |
573 | 240x |
collapse = "-") |
574 | 240x |
counts[count_name] <- topo_count_crossings(x, nodes) |
575 | 240x |
this_rank_counts[count_name] <- counts[count_name] |
576 |
} |
|
577 |
} |
|
578 | 16x |
this_rank_best <- names(this_rank_counts)[which.min(this_rank_counts)] |
579 | 16x |
this_rank_best_order <- as.numeric(strsplit(this_rank_best, "-")[[1]]) |
580 | 16x |
nodes <- nodes[match(this_rank_best_order, nodes$id), ] |
581 | 16x |
nodes$y <- seq_len(nrow(nodes)) |
582 |
} |
|
583 |
} |
|
584 |
# Return |
|
585 | 2x |
return(nodes[, c("comp", "x", "y")]) |
586 |
} |
|
587 | ||
588 |
### * insert_in_between() |
|
589 | ||
590 |
#' Insert an element in all possible positions of another vector |
|
591 |
#' |
|
592 |
#' @param x Element to insert |
|
593 |
#' @param into Target vector |
|
594 |
#' |
|
595 |
#' @return A list of vectors with the x element inserted in all posssible |
|
596 |
#' positions of the target. |
|
597 |
#' |
|
598 |
#' @examples |
|
599 |
#' isotracer:::insert_in_between("a", 1:5) |
|
600 |
#' |
|
601 |
#' @keywords internal |
|
602 |
#' @noRd |
|
603 | ||
604 |
insert_in_between <- function(x, into) { |
|
605 | 112x |
l <- length(into) |
606 | 112x |
out <- list() |
607 | 112x |
for (i in 0:l) { |
608 | 480x |
z <- c(into[0:i], x) |
609 | 480x |
if (i+1 <= l) { |
610 | 368x |
z <- c(z, into[(i+1):l]) |
611 |
} |
|
612 | 480x |
out[[i+1]] <- z |
613 |
} |
|
614 | 112x |
return(out) |
615 |
} |
|
616 | ||
617 |
### * topo_count_crossings() |
|
618 | ||
619 |
#' Count edges crossings given a topology and a proposed node arrangement |
|
620 |
#' |
|
621 |
#' This function assumes that the x locations given in \code{nodes} have been |
|
622 |
#' established using \code{\link{topo_x_ordering}}. |
|
623 |
#' |
|
624 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
625 |
#' is used in this case). |
|
626 |
#' @param nodes A tibble giving the x and y locations for each node. It must |
|
627 |
#' contain columns "comp", "x" and "y". |
|
628 |
#' |
|
629 |
#' @return Integer, the number of crossing edges when drawing the nodes |
|
630 |
#' and the connecting edges. |
|
631 |
#' |
|
632 |
#' @examples |
|
633 |
#' topo_x_ordering <- isotracer:::topo_x_ordering |
|
634 |
#' topo_count_crossings <- isotracer:::topo_count_crossings |
|
635 |
#' |
|
636 |
#' z <- topo(trini_mod) |
|
637 |
#' x <- topo_x_ordering(z) |
|
638 |
#' comps <- names(x) |
|
639 |
#' y <- 1:length(comps) |
|
640 |
#' d <- tibble::tibble(comp = comps, x = x, y = y) |
|
641 |
#' topo_count_crossings(z, d) |
|
642 |
#' |
|
643 |
#' @keywords internal |
|
644 |
#' @noRd |
|
645 | ||
646 |
topo_count_crossings <- function(x, nodes) { |
|
647 | 482x |
if (is(x, "networkModel")) { |
648 | ! |
x <- unique(topo(x, simplify = FALSE)) |
649 | ! |
stopifnot(length(x) == 1) |
650 | ! |
x <- x[[1]] |
651 |
} |
|
652 | 482x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
653 | 482x |
crossings <- 0 |
654 | 482x |
nodes <- nodes[order(nodes$x, nodes$y), ] |
655 | 482x |
x_ranks <- unique(nodes$x) |
656 | 482x |
stopifnot(length(x_ranks) > 1) |
657 | 482x |
for (i in 2:length(x_ranks)) { |
658 |
# Get the edges for gap i |
|
659 | 1446x |
n <- nodes[nodes$x %in% x_ranks[c(i-1, i)], ] |
660 | 1446x |
e <- x[n$comp[n$x == x_ranks[i]], n$comp[n$x == x_ranks[i-1]], drop = FALSE] |
661 | 1446x |
if (ncol(e)>1) { |
662 | 1446x |
for (j in 2:ncol(e)) { |
663 | 4338x |
k <- which(e[, j] > 0) |
664 | 4338x |
for (w in 1:(j-1)) { |
665 | 10604x |
for (ki in k) { |
666 | 11950x |
if (ki < nrow(e)) { |
667 | 7946x |
crossings <- crossings + sum(e[, w][(ki+1):nrow(e)]) |
668 |
} |
|
669 |
} |
|
670 |
} |
|
671 |
} |
|
672 |
} |
|
673 |
} |
|
674 | 482x |
return(crossings) |
675 |
} |
|
676 | ||
677 |
### * sankey_minimize_energy() |
|
678 | ||
679 |
#' Adjust nodes y positions using a spring/energy minimization model |
|
680 |
#' |
|
681 |
#' WIP: This function does not seem to working properly at the moment. |
|
682 |
#' |
|
683 |
#' @param x A topology. |
|
684 |
#' @param nodes The output of \code{\link{sankey_calc_nodes_locations}} run on |
|
685 |
#' \code{x}. |
|
686 |
#' @param k1 Numeric, stiffness for springs between nodes on the same x position. |
|
687 |
#' @param k2 Numeric, sitffness for springs between connected nodes. |
|
688 |
#' @param l1 Numeric, rest length for springs with k1 stiffness. |
|
689 |
#' @param l2 Numeric, rest length for springs with k2 stiffness. |
|
690 |
#' |
|
691 |
#' @return An updated \code{nodes} tibble. |
|
692 |
#' |
|
693 |
#' @importFrom stats optim |
|
694 |
#' |
|
695 |
#' @examples |
|
696 |
#' sankey_calc_nodes_locations <- isotracer:::sankey_calc_nodes_locations |
|
697 |
#' sankey_minimize_energy <- isotracer:::sankey_minimize_energy |
|
698 |
#' |
|
699 |
#' nodes <- sankey_calc_nodes_locations(trini_mod, layout = "left2right") |
|
700 |
#' sankey_minimize_energy(topo(trini_mod), nodes) |
|
701 |
#' |
|
702 |
#' @keywords internal |
|
703 |
#' @noRd |
|
704 | ||
705 |
sankey_minimize_energy <- function(x, nodes, k1 = 100, k2 = 10, l1 = 1, l2 = 1) { |
|
706 | ! |
n_comps <- ncol(x) |
707 | ! |
links <- which(unclass(x) > 0) |
708 | ! |
from <- links %/% n_comps + 1 |
709 | ! |
to <- links %% n_comps |
710 | ! |
links <- tibble::tibble(from = from, to = to) |
711 | ! |
for (i in seq_len(nrow(links))) { |
712 | ! |
if (links$to[i] == 0) { |
713 | ! |
links$from[i] <- links$from[i] - 1 |
714 | ! |
links$to[i] <- n_comps |
715 |
} |
|
716 | ! |
stopifnot(x[links$to[i], links$from[i]] > 0) |
717 |
} |
|
718 | ! |
links$from <- colnames(x)[links$from] |
719 | ! |
links$to <- colnames(x)[links$to] |
720 |
# The y location of the first node is fixed. |
|
721 | ! |
stopifnot(nrow(nodes) > 1) |
722 | ! |
nodes <- nodes[order(nodes$x, nodes$y), ] |
723 | ! |
links$from <- match(links$from, nodes$comp) |
724 | ! |
links$to <- match(links$to, nodes$comp) |
725 | ! |
energy <- function(...) { |
726 | ! |
nodes$y <- c(nodes$y[1], unlist(list(...))) |
727 | ! |
energy <- 0 |
728 |
# Energy due to springs between nodes on same x_rank |
|
729 | ! |
x_ranks <- unique(nodes$x) |
730 | ! |
for (xi in x_ranks) { |
731 | ! |
ni <- nodes[nodes$x == xi, ] |
732 | ! |
if (nrow(ni) > 1) { |
733 | ! |
for (i in 2:nrow(ni)) { |
734 | ! |
delta_l <- ni$y[i] - ni$y[i-1] - l1 |
735 | ! |
energy <- energy + 1/2 * k1 * delta_l^2 |
736 |
} |
|
737 |
} |
|
738 |
} |
|
739 |
# Energy due to springs along connections between nodes of different x_rank |
|
740 | ! |
for (i in 1:nrow(links)) { |
741 | ! |
delta_l <- abs(nodes$y[links$to[i]] - nodes$y[links$from[i]]) - l2 |
742 | ! |
energy <- energy + 1/2 * k2 * delta_l^2 |
743 |
} |
|
744 | ! |
return(energy) |
745 |
} |
|
746 | ! |
start_values <- nodes$y[2:nrow(nodes)] |
747 | ! |
y_locs <- optim(start_values, energy, method = "BFGS")$par |
748 | ! |
nodes$y <- c(nodes$y[1], y_locs) |
749 | ! |
return(nodes) |
750 |
} |
|
751 | ||
752 |
### * sankey_minimize_energy_sim() |
|
753 | ||
754 |
#' Adjust nodes y positions using a spring/energy minimization model |
|
755 |
#' |
|
756 |
#' @param x A topology. |
|
757 |
#' @param nodes The output of \code{\link{sankey_calc_nodes_locations}} run on |
|
758 |
#' \code{x}. |
|
759 |
#' @param k1 Numeric, stiffness for springs between nodes on the same x |
|
760 |
#' position. |
|
761 |
#' @param k2 Numeric, sitffness for springs between connected nodes. |
|
762 |
#' @param l1 Numeric, rest length for springs with k1 stiffness. |
|
763 |
#' @param l2 Numeric, rest length for springs with k2 stiffness. |
|
764 |
#' @param cd Numeric, dampening constant. |
|
765 |
#' @param debug Boolean, if TRUE return trajectories instead of the node |
|
766 |
#' tibble. |
|
767 |
#' @param return_acc Boolean, if TRUE also return velocity and acceleration |
|
768 |
#' values in the node tibble (useful for debugging). |
|
769 |
#' @param n_iters Number of iterations in the simulation loop. |
|
770 |
#' @param dt Time step used in the simulation loop. |
|
771 |
#' |
|
772 |
#' @return An updated \code{nodes} tibble. |
|
773 |
#' |
|
774 |
#' @examples |
|
775 |
#' sankey_calc_nodes_locations <- isotracer:::sankey_calc_nodes_locations |
|
776 |
#' sankey_minimize_energy_sim <- isotracer:::sankey_minimize_energy_sim |
|
777 |
#' |
|
778 |
#' nodes <- sankey_calc_nodes_locations(trini_mod, layout = "left2right") |
|
779 |
#' nodes <- nodes[nodes$x < 2, ] |
|
780 |
#' z <- sankey_minimize_energy_sim(topo(trini_mod)[nodes$comp, nodes$comp], nodes, |
|
781 |
#' n_iters = 200, cd = 0.8, dt = 0.1, debug = TRUE) |
|
782 |
#' z <- do.call(rbind, z) |
|
783 |
#' lattice::xyplot(ts(z)) |
|
784 |
#' |
|
785 |
#' (z <- sankey_minimize_energy_sim(topo(trini_mod)[nodes$comp, nodes$comp], nodes, |
|
786 |
#' n_iters = 200, cd = 0.8, dt = 0.1, return_acc = TRUE)) |
|
787 |
#' plot(z$x, z$y) |
|
788 |
#' text(z$x, z$y, z$comp) |
|
789 |
#' |
|
790 |
#' @keywords internal |
|
791 |
#' @noRd |
|
792 | ||
793 |
sankey_minimize_energy_sim <- function(x, nodes, k1 = 1, k2 = 1, l1 = 1, l2 = 1, |
|
794 |
cd = 0.8, n_iters = 25, dt = 0.5, debug = FALSE, |
|
795 |
return_acc = FALSE) { |
|
796 | ! |
n_comps <- ncol(x) |
797 | ! |
links <- which(x > 0) |
798 | ! |
from <- links %/% n_comps + 1 |
799 | ! |
to <- links %% n_comps |
800 | ! |
links <- tibble::tibble(from = from, to = to) |
801 | ! |
for (i in seq_len(nrow(links))) { |
802 | ! |
if (links$to[i] == 0) { |
803 | ! |
links$from[i] <- links$from[i] - 1 |
804 | ! |
links$to[i] <- n_comps |
805 |
} |
|
806 | ! |
stopifnot(x[links$to[i], links$from[i]] > 0) |
807 |
} |
|
808 | ! |
links$from <- colnames(x)[links$from] |
809 | ! |
links$to <- colnames(x)[links$to] |
810 | ! |
stopifnot(nrow(nodes) > 1) |
811 | ! |
nodes <- nodes[order(nodes$x, nodes$y), ] |
812 | ! |
nodes$v <- 0 |
813 | ! |
links$from <- match(links$from, nodes$comp) |
814 | ! |
links$to <- match(links$to, nodes$comp) |
815 | ! |
traj <- list() |
816 | ! |
x_ranks <- unique(nodes$x) |
817 |
# Dynamic simulation |
|
818 | ! |
for (i in seq_len(n_iters)) { |
819 | ! |
nodes$a <- 0 |
820 | ! |
traj[[i]] <- nodes$y |
821 |
# Add forces due to springs between nodes on same x_rank |
|
822 | ! |
for (xi in x_ranks) { |
823 | ! |
ni <- nodes[nodes$x == xi, ] |
824 | ! |
if (nrow(ni) > 1) { |
825 | ! |
for (i in 2:nrow(ni)) { |
826 | ! |
delta_l <- ni$y[i] - ni$y[i-1] - l1 |
827 | ! |
force <- k1 * delta_l |
828 | ! |
ni$a[i] <- ni$a[i] - force |
829 | ! |
ni$a[i-1] <- ni$a[i-1] + force |
830 |
} |
|
831 |
} |
|
832 | ! |
nodes[nodes$x == xi, ] <- ni |
833 |
} |
|
834 |
# Add forces due to springs along connections between nodes of different x_rank |
|
835 | ! |
for (i in 1:nrow(links)) { |
836 | ! |
delta_l <- abs(nodes$y[links$to[i]] - nodes$y[links$from[i]]) - l2 |
837 | ! |
force <- k2 * delta_l |
838 | ! |
if (nodes$y[links$to[i]] > nodes$y[links$from[i]]) { |
839 | ! |
nodes$a[links$to[i]] <- nodes$a[links$to[i]] - force |
840 | ! |
nodes$a[links$from[i]] <- nodes$a[links$from[i]] + force |
841 |
} else { |
|
842 | ! |
nodes$a[links$to[i]] <- nodes$a[links$to[i]] + force |
843 | ! |
nodes$a[links$from[i]] <- nodes$a[links$from[i]] - force |
844 |
} |
|
845 |
} |
|
846 |
# Dampening |
|
847 | ! |
for (i in seq_len(nrow(nodes))) { |
848 | ! |
nodes$a[i] <- nodes$a[i] - cd * nodes$v[i] |
849 |
} |
|
850 |
# Integration |
|
851 | ! |
for (i in seq_len(nrow(nodes))) { |
852 | ! |
nodes$y[i] <- nodes$y[i] + dt * nodes$v[i] |
853 | ! |
nodes$v[i] <- nodes$v[i] + dt * nodes$a[i] |
854 |
} |
|
855 |
} |
|
856 | ! |
if (debug) { |
857 | ! |
return(traj) |
858 |
} |
|
859 | ! |
if (return_acc) { |
860 | ! |
return(nodes) |
861 |
} |
|
862 | ! |
return(nodes[, c("comp", "x", "y")]) |
863 |
} |
|
864 | ||
865 |
### * sankey_draw_node() |
|
866 | ||
867 |
#' Draw a rounded rectangle with a label |
|
868 |
#' |
|
869 |
#' @param x,y Locations of the center of the rectangle. |
|
870 |
#' @param label String, label added inside the rectangle. |
|
871 |
#' @param fill Fill color. |
|
872 |
#' @param col Border color. |
|
873 |
#' @param text_col Text color. |
|
874 |
#' @param style One of "roundrect", "square". |
|
875 |
#' @param padding Padding value. |
|
876 |
#' @param padding_factor Adjustment factor for padding. |
|
877 |
#' @param r_factor Adjustment factor for rounded corner. |
|
878 |
#' @param side Vector of length 1 or 2 giving node width and height values |
|
879 |
#' (used for style = "square"). |
|
880 |
#' @param factor Adjustment factor for node dimensions (used for style = |
|
881 |
#' "square"). |
|
882 |
#' |
|
883 |
#' @keywords internal |
|
884 |
#' @noRd |
|
885 | ||
886 |
sankey_draw_node <- function(x, y, label, |
|
887 |
fill = NULL, col = "black", text_col = NULL, |
|
888 |
style = "roundrect", |
|
889 |
padding = NULL, padding_factor = 1, r_factor = 1, # For roundrect |
|
890 |
side = NULL, factor = 1) { |
|
891 |
# Convert x and y coordinates |
|
892 | ! |
x <- grid::unit(x, "native") |
893 | ! |
y <- grid::unit(y, "native") |
894 |
# Draw box |
|
895 | ! |
if (is.null(fill)) { |
896 | ! |
gp <- grid::gpar(col = col) |
897 |
} else { |
|
898 | ! |
gp <- grid::gpar(col = col, fill = fill) |
899 |
} |
|
900 | ! |
if (style == "roundrect") { |
901 | ! |
r <- grid::unit(0.1, "snpc") * r_factor |
902 | ! |
if (is.null(padding)) { |
903 | ! |
padding <- grid::unit(1, "strwidth", "o") |
904 |
} |
|
905 | ! |
width <- grid::unit(1, "strwidth", label) + 2 * padding * padding_factor |
906 | ! |
height <- grid::unit(1, "strheight", label) + 2 * padding * padding_factor |
907 | ! |
grid::grid.roundrect(x = x, y = y, width = width, height = height, |
908 | ! |
gp = gp, r = r) |
909 | ! |
} else if (style == "square") { |
910 | ! |
if (is.null(side)) { |
911 | ! |
side <- grid::unit(1, "strwidth", "toto") |
912 |
} |
|
913 | ! |
if (length(side) == 1) { |
914 | ! |
side <- grid::unit.c(side, side) |
915 |
} |
|
916 | ! |
grid::grid.rect(x = x, y = y, width = side[1] * factor, |
917 | ! |
height = side[2] * factor, gp = gp) |
918 |
} |
|
919 |
# Draw label |
|
920 | ! |
if (is.null(text_col)) { |
921 | ! |
text_col <- col |
922 |
} |
|
923 | ! |
gp <- grid::gpar(col = text_col) |
924 | ! |
grid::grid.text(label = label, x = x, y = y, gp = gp) |
925 |
} |
|
926 | ||
927 |
### * sankey_draw_edge() |
|
928 | ||
929 |
#' Draw a constant-width ribbon between nodes |
|
930 |
#' |
|
931 |
#' @param backbone Two-column table containing the x and y coordinates of the |
|
932 |
#' points making the mid-line of the edge ribbon. |
|
933 |
#' @param width Width of the edge ribbon. |
|
934 |
#' @param fill Color for ribbon fill. |
|
935 |
#' @param col Color for ribbon border. |
|
936 |
#' @param factor Adjustment factor for ribbon width. |
|
937 |
#' |
|
938 |
#' @keywords internal |
|
939 |
#' @noRd |
|
940 | ||
941 |
sankey_draw_edge <- function(backbone, width = 0.1, fill = NULL, col = "black", |
|
942 |
factor = 1) { |
|
943 | ! |
rb <- ribbonFromTrajectory(backbone, width * factor) |
944 | ! |
if (is.null(fill)) { |
945 | ! |
gp <- grid::gpar(col = col) |
946 |
} else { |
|
947 | ! |
gp <- grid::gpar(col = col, fill = fill) |
948 |
} |
|
949 | ! |
grid::grid.polygon(x = rb[, 1], y = rb[, 2], default.units = "native", gp = gp) |
950 |
} |
|
951 | ||
952 |
### * make_and_push_ortho_vp() |
|
953 | ||
954 |
#' Make an orthonormal data viewport |
|
955 |
#' |
|
956 |
#' This function must be run when a parent viewport already exists. It adds a |
|
957 |
#' container viewport, centered, with specified width and height, and fills |
|
958 |
#' this container viewport with an orthonormal data viewport (based on the |
|
959 |
#' physical dimensions of the container viewport). |
|
960 |
#' |
|
961 |
#' Note that this function creates and push two viewports on the stack (a |
|
962 |
#' container filling the parent viewport completely and a data viewport filling |
|
963 |
#' the container, with an orthonormal cartesian coordinate system), so that two |
|
964 |
#' viewports must be popped in order to come back to the parent viewport in use |
|
965 |
#' before the function call. |
|
966 |
#' |
|
967 |
#' @param width,height Numeric between 0 and 1, fraction of the parent viewport |
|
968 |
#' width and height filled by the new viewport. |
|
969 |
#' @param debug Boolean. |
|
970 |
#' |
|
971 |
#' @return An orthonormal data viewport which bounds (0, 1) on one axis. |
|
972 |
#' |
|
973 |
#' @examples |
|
974 |
#' library(grid) |
|
975 |
#' grid.newpage() |
|
976 |
#' pushViewport(viewport()) |
|
977 |
#' ortho_vp <- isotracer:::make_and_push_ortho_vp(width = 0.9, height = 0.9, |
|
978 |
#' debug = TRUE) |
|
979 |
#' |
|
980 |
#' @keywords internal |
|
981 |
#' @noRd |
|
982 | ||
983 |
make_and_push_ortho_vp <- function(width = 1, height = 1, debug = FALSE) { |
|
984 | 8x |
debug_bg_col <- "#095ba7" |
985 | 8x |
debug_line_col <- "#9fccfa" # "#baecfa" |
986 | 8x |
parent_vp <- grid::current.viewport() |
987 | 8x |
if (debug) { |
988 | 5x |
grid::grid.rect(gp = grid::gpar(col = debug_line_col, |
989 | 5x |
fill = debug_bg_col)) |
990 |
} |
|
991 | 8x |
container_vp <- grid::viewport(width = width, height = height, |
992 | 8x |
name = "ortho_container") |
993 | 8x |
grid::pushViewport(container_vp) |
994 | 8x |
if (debug) { |
995 | 5x |
grid::grid.rect(gp = grid::gpar(col = debug_line_col, |
996 | 5x |
fill = debug_bg_col)) |
997 |
} |
|
998 | 8x |
container_dims <- grid::deviceDim(container_vp$width, |
999 | 8x |
container_vp$height, |
1000 | 8x |
valueOnly = TRUE) |
1001 | 8x |
canvas_dims <- container_dims |
1002 | 8x |
if (canvas_dims$w >= canvas_dims$h) { |
1003 | 8x |
y_scale <- c(0, 1) |
1004 | 8x |
x_scale <- y_scale * canvas_dims$w / canvas_dims$h |
1005 | 8x |
x_scale <- x_scale - (x_scale[2] - 1) / 2 |
1006 |
} else { |
|
1007 | ! |
x_scale <- c(0, 1) |
1008 | ! |
y_scale <- x_scale * canvas_dims$h / canvas_dims$w |
1009 | ! |
y_scale <- y_scale - (y_scale[2] - 1) / 2 |
1010 |
} |
|
1011 | 8x |
canvas_vp <- grid::dataViewport(xscale = x_scale, yscale = y_scale, |
1012 | 8x |
name = "ortho_canvas") |
1013 | 8x |
grid::pushViewport(canvas_vp) |
1014 | 8x |
if (debug) { |
1015 | 5x |
shift <- 0.05 |
1016 | 5x |
arrow_style <- grid::arrow(length = grid::unit(0.01, "native")) |
1017 | 5x |
grid::grid.rect(width = 1, height = 1, |
1018 | 5x |
default.units = "native", |
1019 | 5x |
gp = grid::gpar(col = debug_line_col, lty = 2)) |
1020 | 5x |
grid::grid.lines(x = c(0, 0.1) + shift, y = shift, default.units = "native", |
1021 | 5x |
gp = grid::gpar(col = debug_line_col), |
1022 | 5x |
arrow = arrow_style) |
1023 | 5x |
grid::grid.lines(x = shift, y = c(0, 0.1) + shift, default.units = "native", |
1024 | 5x |
gp = grid::gpar(col = debug_line_col), |
1025 | 5x |
arrow = arrow_style) |
1026 | 5x |
grid::grid.points(x = 0.5, y = 0.5, default.units = "native", |
1027 | 5x |
pch = 3, gp = grid::gpar(col = debug_line_col)) |
1028 | 5x |
grid::grid.text("0.1 x", x = grid::unit(0.05 + shift, "native"), |
1029 | 5x |
y = grid::unit(shift, "native") + grid::unit(0.7, "strheight", "x"), |
1030 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1031 | 5x |
grid::grid.text("0.1 y", x = grid::unit(shift, "native") + grid::unit(0.7, "strheight", "x"), |
1032 | 5x |
y = grid::unit(0.05 + shift, "native"), |
1033 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col), |
1034 | 5x |
rot = -90) |
1035 | 5x |
grid::grid.text("(0, 0)", |
1036 | 5x |
x = grid::unit(0, "native") + grid::unit(0.1, "strwidth", "x"), |
1037 | 5x |
y = grid::unit(0, "native") + grid::unit(0.4, "strheight", "x"), |
1038 | 5x |
hjust = 0, vjust = 0, |
1039 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1040 | 5x |
grid::grid.text("(1, 1)", |
1041 | 5x |
x = grid::unit(1, "native") - grid::unit(1, "strwidth", "(1, 1)"), |
1042 | 5x |
y = grid::unit(1, "native") - grid::unit(0.4, "strheight", "x"), |
1043 | 5x |
hjust = 0, vjust = 1, |
1044 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1045 | 5x |
topleft <- signif(c(canvas_vp$xscale[1], canvas_vp$yscale[2]), 3) |
1046 | 5x |
topleft_char <- paste0("(", topleft[1], ", ", topleft[2], ")") |
1047 | 5x |
grid::grid.text(topleft_char, |
1048 | 5x |
x = grid::unit(topleft[1], "native") + grid::unit(0.1, "strwidth", "x"), |
1049 | 5x |
y = grid::unit(topleft[2], "native") - grid::unit(0.4, "strheight", "x"), |
1050 | 5x |
hjust = 0, vjust = 1, |
1051 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1052 | 5x |
bottomright <- signif(c(canvas_vp$xscale[2], canvas_vp$yscale[1]), 3) |
1053 | 5x |
bottomright_char <- paste0("(", bottomright[1], ", ", bottomright[2], ")") |
1054 | 5x |
grid::grid.text(bottomright_char, |
1055 | 5x |
x = grid::unit(bottomright[1], "native") - grid::unit(1, "strwidth", bottomright_char), |
1056 | 5x |
y = grid::unit(bottomright[2], "native") + grid::unit(0.4, "strheight", "x"), |
1057 | 5x |
hjust = 0, vjust = 0, |
1058 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1059 | 5x |
grid::grid.text("(0.5, 0.5)", |
1060 | 5x |
x = grid::unit(0.5, "native") + grid::unit(0.1, "strwidth", "x"), |
1061 | 5x |
y = grid::unit(0.5, "native") + grid::unit(0.4, "strheight", "x"), |
1062 | 5x |
hjust = 0, vjust = 0, |
1063 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1064 |
} |
|
1065 | 8x |
return(canvas_vp) |
1066 |
} |
|
1067 | ||
1068 |
### * sankey_draw_edges() |
|
1069 | ||
1070 |
#' @keywords internal |
|
1071 |
#' @noRd |
|
1072 | ||
1073 |
sankey_draw_edges <- function(edges, defaults = list(col = adjustcolor("black", alpha.f = 0.5), |
|
1074 |
fill = adjustcolor("grey", alpha.f = 0.5), |
|
1075 |
lwd = 1, lty = 1), |
|
1076 |
debug = FALSE) { |
|
1077 | 8x |
debug_bg_col <- "#095ba7" |
1078 | 8x |
debug_line_col <- "#9fccfa" # "#baecfa" |
1079 | 8x |
if (is.null(edges) || nrow(edges) == 0) { |
1080 | ! |
return(NULL) |
1081 |
} |
|
1082 | 8x |
for (property in names(defaults)) { |
1083 | 32x |
if (! property %in% colnames(edges)) { |
1084 | 32x |
edges[[property]] <- defaults[[property]] |
1085 |
} |
|
1086 |
} |
|
1087 | 8x |
for (i in seq_len(nrow(edges))) { |
1088 | 67x |
rb <- ribbonFromTrajectory(edges[["backbone"]][[i]], |
1089 | 67x |
width = edges[["width"]][i]) |
1090 | 67x |
gp <- grid::gpar(col = edges[["col"]][i], |
1091 | 67x |
fill = edges[["fill"]][i], |
1092 | 67x |
lwd = edges[["lwd"]][i], |
1093 | 67x |
lty = edges[["lty"]][i]) |
1094 | 67x |
grid::grid.polygon(x = rb[, 1], y = rb[, 2], default.units = "native", |
1095 | 67x |
gp = gp) |
1096 | 67x |
if (debug) { |
1097 | 34x |
grid::grid.lines(x = edges[["control_points"]][[i]][, 1], |
1098 | 34x |
y = edges[["control_points"]][[i]][, 2], |
1099 | 34x |
default.units = "native", |
1100 | 34x |
gp = grid::gpar(col = debug_line_col, |
1101 | 34x |
lty = 2)) |
1102 | 34x |
grid::grid.lines(x = edges[["backbone"]][[i]][, 1], |
1103 | 34x |
y = edges[["backbone"]][[i]][, 2], |
1104 | 34x |
default.units = "native", |
1105 | 34x |
gp = grid::gpar(col = debug_line_col)) |
1106 | 34x |
grid::grid.points(x = edges[["control_points"]][[i]][, 1], |
1107 | 34x |
y = edges[["control_points"]][[i]][, 2], |
1108 | 34x |
default.units = "native", |
1109 | 34x |
pch = 1, |
1110 | 34x |
gp = grid::gpar(col = debug_line_col, |
1111 | 34x |
cex = 0.75)) |
1112 | 34x |
grid::grid.points(x = edges[["backbone"]][[i]][, 1], |
1113 | 34x |
y = edges[["backbone"]][[i]][, 2], |
1114 | 34x |
default.units = "native", |
1115 | 34x |
pch = 21, |
1116 | 34x |
gp = grid::gpar(col = debug_line_col, |
1117 | 34x |
fill = debug_line_col, |
1118 | 34x |
cex = 0.5)) |
1119 |
} |
|
1120 |
} |
|
1121 | 8x |
return(NULL) |
1122 |
} |
|
1123 | ||
1124 |
### * sankey_draw_nodes() |
|
1125 | ||
1126 |
#' @keywords internal |
|
1127 |
#' @noRd |
|
1128 | ||
1129 |
sankey_draw_nodes <- function(nodes, defaults = list(label = "", |
|
1130 |
col = adjustcolor("black", alpha.f = 0.5), |
|
1131 |
fill = adjustcolor("grey", alpha.f = 0.5), |
|
1132 |
lwd = 1, lty = 1), |
|
1133 |
debug = FALSE, node_s = "default") { |
|
1134 | 8x |
debug_bg_col <- "#095ba7" |
1135 | 8x |
debug_line_col <- "#9fccfa" # "#baecfa" |
1136 | 8x |
if (is.null(nodes) || nrow(nodes) == 0) { |
1137 | ! |
return(NULL) |
1138 |
} |
|
1139 | 8x |
for (property in names(defaults)) { |
1140 | 40x |
if (! property %in% colnames(nodes)) { |
1141 | 32x |
nodes[[property]] <- defaults[[property]] |
1142 |
} |
|
1143 |
} |
|
1144 | 8x |
for (i in seq_len(nrow(nodes))) { |
1145 | 55x |
gp <- grid::gpar(col = nodes$col[i], |
1146 | 55x |
fill = nodes$fill[i], |
1147 | 55x |
lwd = nodes$lwd[i], |
1148 | 55x |
lty = nodes$lty[i]) |
1149 | 55x |
if (node_s == "roundsquare") { |
1150 | ! |
grid::grid.roundrect(x = nodes$x[i], y = nodes$y[i], |
1151 | ! |
width = nodes$width[[i]], |
1152 | ! |
height = nodes$height[[i]], |
1153 | ! |
default.units = "native", |
1154 | ! |
gp = gp) |
1155 |
} else { |
|
1156 | 55x |
grid::grid.rect(x = nodes$x[i], y = nodes$y[i], |
1157 | 55x |
width = nodes$width[[i]], |
1158 | 55x |
height = nodes$height[[i]], |
1159 | 55x |
default.units = "native", |
1160 | 55x |
gp = gp) |
1161 |
} |
|
1162 | 55x |
if (debug) { |
1163 | 29x |
gp <- grid::gpar(col = debug_line_col, |
1164 | 29x |
lty = 5) |
1165 | 29x |
if (node_s == "roundrect") { |
1166 | ! |
grid::grid.roundrect(x = nodes$x[i], y = nodes$y[i], |
1167 | ! |
width = nodes$width[[i]], |
1168 | ! |
height = nodes$height[[i]], |
1169 | ! |
default.units = "native", |
1170 | ! |
gp = gp) |
1171 |
} else { |
|
1172 | 29x |
grid::grid.rect(x = nodes$x[i], y = nodes$y[i], |
1173 | 29x |
width = nodes$width[[i]], |
1174 | 29x |
height = nodes$height[[i]], |
1175 | 29x |
default.units = "native", |
1176 | 29x |
gp = gp) |
1177 |
} |
|
1178 | 29x |
left <- nodes$x[i] - grid::convertWidth(nodes$width[[i]], unitTo = "native", |
1179 | 29x |
valueOnly = TRUE) / 2 |
1180 | 29x |
right <- nodes$x[i] + grid::convertWidth(nodes$width[[i]], unitTo = "native", |
1181 | 29x |
valueOnly = TRUE) / 2 |
1182 | 29x |
bottom <- nodes$y[i] - grid::convertHeight(nodes$height[[i]], unitTo = "native", |
1183 | 29x |
valueOnly = TRUE) / 2 |
1184 | 29x |
top <- nodes$y[i] + grid::convertHeight(nodes$height[[i]], unitTo = "native", |
1185 | 29x |
valueOnly = TRUE) / 2 |
1186 | 29x |
gp <- grid::gpar(col = debug_line_col, |
1187 | 29x |
lty = "26") |
1188 | 29x |
grid::grid.lines(x = c(left, right), y = c(top, bottom), |
1189 | 29x |
default.units = "native", gp = gp) |
1190 | 29x |
grid::grid.lines(x = c(left, right), y = c(bottom, top), |
1191 | 29x |
default.units = "native", gp = gp) |
1192 | 29x |
gp <- grid::gpar(col = debug_line_col, |
1193 | 29x |
fill = debug_bg_col) |
1194 | 29x |
grid::grid.points(x = nodes[["x"]][i], y = nodes[["y"]][i], |
1195 | 29x |
pch = 5, size = grid::unit(0.8, "char"), |
1196 | 29x |
default.units = "native", gp = gp) |
1197 | 29x |
grid::grid.text(x = nodes[["x"]][i], |
1198 | 29x |
y = grid::unit(nodes[["y"]][i], "native") + |
1199 | 29x |
grid::unit(1.4, "strheight", "x"), |
1200 | 29x |
default.units = "native", gp = gp, |
1201 | 29x |
label = nodes[["comp"]][i]) |
1202 |
} |
|
1203 |
} |
|
1204 | 8x |
return(NULL) |
1205 |
} |
|
1206 | ||
1207 |
### * sankey_draw_labels() |
|
1208 | ||
1209 |
#' @keywords internal |
|
1210 |
#' @noRd |
|
1211 | ||
1212 |
sankey_draw_labels <- function(labels, defaults = list(label = "", |
|
1213 |
cex = 1, |
|
1214 |
col = adjustcolor("black", alpha.f = 0.5), |
|
1215 |
fill = adjustcolor("grey", alpha.f = 0.5), |
|
1216 |
lwd = 1, lty = 1), |
|
1217 |
debug = FALSE) { |
|
1218 | 8x |
debug_bg_col <- "#095ba7" |
1219 | 8x |
debug_line_col <- "#9fccfa" # "#baecfa" |
1220 | 8x |
if (is.null(labels) || nrow(labels) == 0) { |
1221 | ! |
return(NULL) |
1222 |
} |
|
1223 | 8x |
for (property in names(defaults)) { |
1224 | 48x |
if (! property %in% colnames(labels)) { |
1225 | 32x |
labels[[property]] <- defaults[[property]] |
1226 |
} |
|
1227 |
} |
|
1228 | 8x |
for (i in seq_len(nrow(labels))) { |
1229 | 55x |
if (debug) { |
1230 | 29x |
gp <- grid::gpar(col = debug_line_col, |
1231 | 29x |
lwd = 0.5) |
1232 | 29x |
grid::grid.points(x = labels$label_x[i], |
1233 | 29x |
y = labels$label_y[[i]], |
1234 | 29x |
default.units = "native", |
1235 | 29x |
pch = 3, gp = gp) |
1236 | 29x |
half_height <- grid::convertHeight(grid::unit(1/2 * labels$cex[i], "strheight", labels$label[[i]]), |
1237 | 29x |
unitTo = "native", valueOnly = TRUE) |
1238 | 29x |
top <- labels$label_y[i] + half_height |
1239 | 29x |
bottom <- labels$label_y[i] - half_height |
1240 | 29x |
left <- grid::convertWidth(grid::unit(labels$label_x[i], "native") - |
1241 | 29x |
grid::unit(0.525 * labels$cex[i], "strwidth", labels$label[[i]]), |
1242 | 29x |
unitTo = "native", valueOnly = TRUE) |
1243 | 29x |
right <- grid::convertWidth(grid::unit(labels$label_x[i], "native") + |
1244 | 29x |
grid::unit(0.525 * labels$cex[i], "strwidth", labels$label[[i]]), |
1245 | 29x |
unitTo = "native", valueOnly = TRUE) |
1246 | 29x |
grid::grid.lines(c(left, right), c(top, top), |
1247 | 29x |
default.units = "native", gp = gp) |
1248 | 29x |
grid::grid.lines(c(left, right), c(bottom, bottom), |
1249 | 29x |
default.units = "native", gp = gp) |
1250 |
} |
|
1251 | 55x |
gp <- grid::gpar(col = labels$col[i], |
1252 | 55x |
cex = labels$cex[i]) |
1253 | 55x |
grid::grid.text(label = labels$label[[i]], |
1254 | 55x |
x = labels$label_x[i], |
1255 | 55x |
y = labels$label_y[[i]], |
1256 | 55x |
default.units = "native", gp = gp) |
1257 |
} |
|
1258 | 8x |
return(NULL) |
1259 |
} |
|
1260 | ||
1261 |
### * (1) sankey_place_nodes() |
|
1262 | ||
1263 |
#' Perform initial placement of the nodes |
|
1264 |
#' |
|
1265 |
#' @param topo Topology. |
|
1266 |
#' @param nodes Node tibble. |
|
1267 |
#' @param flows Flows tibble. |
|
1268 |
#' @param layout Layout string. |
|
1269 |
#' @param xlim,ylim Limits of the x and y scales. |
|
1270 |
#' |
|
1271 |
#' @examples |
|
1272 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1273 |
#' |
|
1274 |
#' topo <- topo(trini_mod) |
|
1275 |
#' nodes <- tibble::tibble(comp = colnames(topo), size = 1, col = "red") |
|
1276 |
#' nodes$label <- letters[1:nrow(nodes)] |
|
1277 |
#' sankey_place_nodes(topo, nodes, layout = "left2right") |
|
1278 |
#' sankey_place_nodes(topo, nodes, layout = "stress") |
|
1279 |
#' |
|
1280 |
#' @keywords internal |
|
1281 |
#' @noRd |
|
1282 | ||
1283 |
sankey_place_nodes <- function(topo, nodes = NULL, flows, layout, xlim = c(0, 1), |
|
1284 |
ylim = c(0, 1)) { |
|
1285 | 8x |
nodes_arg <- nodes |
1286 |
# Calculate node locations |
|
1287 | 8x |
nodes <- sankey_calc_nodes_locations(topo, layout) |
1288 |
# Adjust node locations to fill the canvas |
|
1289 | 8x |
nodes$x <- (nodes$x - min(nodes$x)) / (max(nodes$x) - min(nodes$x)) * diff(xlim) + xlim[1] |
1290 | 8x |
nodes$y <- (nodes$y - min(nodes$y)) / (max(nodes$y) - min(nodes$y)) * diff(ylim) + ylim[1] |
1291 | 8x |
nodes$label <- nodes$comp |
1292 |
# Return |
|
1293 | 8x |
if (!is.null(nodes_arg)) { |
1294 | 8x |
if (any(colnames(nodes_arg) %in% c("x", "y"))) { |
1295 | ! |
stop("Provided `nodes` tibble cannot have `x` or `y` column.") |
1296 |
} |
|
1297 | 8x |
if (! setequal(nodes$comp, nodes_arg$comp)) { |
1298 | ! |
stop("Provided `nodes` tibble must have a `comp` column with exactly the same entries as the topology compartments.") |
1299 |
} |
|
1300 | 8x |
if ("label" %in% colnames(nodes_arg)) { |
1301 | 4x |
nodes$label <- NULL |
1302 |
} |
|
1303 | 8x |
nodes <- dplyr::left_join(nodes, nodes_arg, by = "comp") |
1304 |
} |
|
1305 | 8x |
out <- list(nodes = nodes, edges = NULL) |
1306 | 8x |
attr(out, "layout") <- layout |
1307 | 8x |
return(out) |
1308 |
} |
|
1309 | ||
1310 |
### * (2) sankey_place_edge_sockets_on_nodes() |
|
1311 | ||
1312 |
#' Determine relative location of edge sockets on nodes |
|
1313 |
#' |
|
1314 |
#' @param scene A list with a "nodes" tibble. |
|
1315 |
#' @param topo A topology. |
|
1316 |
#' @param nodes A node tibble. |
|
1317 |
#' @param flows A tibble giving the flow rates between connected nodes. |
|
1318 |
#' @param layout String specifying the plot layout. |
|
1319 |
#' |
|
1320 |
#' @examples |
|
1321 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1322 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1323 |
#' |
|
1324 |
#' topo <- topo(trini_mod) |
|
1325 |
#' flows <- flows_from_topo(topo) |
|
1326 |
#' flows$width <- 1 |
|
1327 |
#' layout <- "stress" |
|
1328 |
#' scene <- sankey_place_nodes(topo, flows = flows, layout = layout) |
|
1329 |
#' z <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1330 |
#' |
|
1331 |
#' @keywords internal |
|
1332 |
#' @noRd |
|
1333 | ||
1334 |
sankey_place_edge_sockets_on_nodes <- function(scene, topo, nodes = NULL, flows, layout) { |
|
1335 | 8x |
nodes <- scene$nodes |
1336 | 8x |
edges <- flows |
1337 | 8x |
stopifnot(is.null(scene[["edges"]])) |
1338 | 8x |
edges$from_x <- nodes$x[match(edges$from, nodes$comp)] |
1339 | 8x |
edges$to_x <- nodes$x[match(edges$to, nodes$comp)] |
1340 | 8x |
edges$from_y <- nodes$y[match(edges$from, nodes$comp)] |
1341 | 8x |
edges$to_y <- nodes$y[match(edges$to, nodes$comp)] |
1342 | 8x |
sockets <- list() |
1343 |
# Layout left2right |
|
1344 | 8x |
if (layout == "left2right") { |
1345 | 2x |
edges <- edges[order(edges$from_x, edges$from_y), ] |
1346 | 2x |
nodes$left_accumulator <- 0 |
1347 | 2x |
nodes$right_accumulator <- 0 |
1348 | 2x |
for (i in seq_len(nrow(edges))) { |
1349 | 34x |
from <- edges$from[i] |
1350 | 34x |
to <- edges$to[i] |
1351 | 34x |
edge_id <- c(from = from, to = to) |
1352 |
# Socket from |
|
1353 | 34x |
socket_from <- list() |
1354 | 34x |
socket_from[["node_id"]] <- from |
1355 | 34x |
socket_from[["node_side"]] <- "right" |
1356 | 34x |
socket_from[["edge_id"]] <- list(edge_id) |
1357 | 34x |
socket_from[["edge_end"]] <- "from" |
1358 | 34x |
socket_from[["rel_loc"]] <- (nodes$right_accumulator[nodes$comp == from] + |
1359 | 34x |
edges$width[i]/2) |
1360 | 34x |
nodes$right_accumulator[nodes$comp == from] <- (nodes$right_accumulator[nodes$comp == from] + |
1361 | 34x |
edges$width[i]) |
1362 | 34x |
socket_from[["width"]] <- edges$width[i] |
1363 |
# Socket to |
|
1364 | 34x |
socket_to <- list() |
1365 | 34x |
socket_to[["node_id"]] <- to |
1366 | 34x |
socket_to[["node_side"]] <- "left" |
1367 | 34x |
socket_to[["edge_id"]] <- list(edge_id) |
1368 | 34x |
socket_to[["edge_end"]] <- "to" |
1369 | 34x |
socket_to[["rel_loc"]] <- (nodes$left_accumulator[nodes$comp == to] + |
1370 | 34x |
edges$width[i]/2) |
1371 | 34x |
nodes$left_accumulator[nodes$comp == to] <- (nodes$left_accumulator[nodes$comp == to] + |
1372 | 34x |
edges$width[i]) |
1373 | 34x |
socket_to[["width"]] <- edges$width[i] |
1374 |
# Store sockets |
|
1375 | 34x |
sockets[[2*i-1]] <- socket_from |
1376 | 34x |
sockets[[2*i]] <- socket_to |
1377 |
} |
|
1378 | 6x |
} else if (layout %in% c("stress", "kk", "lgl", "fr", "dh", "mds")) { |
1379 |
# Layout stress or kk or ... |
|
1380 | 6x |
edges <- edges[order(edges$from_x, edges$from_y), ] |
1381 | 6x |
sides <- c("right", "top", "left", "bottom") |
1382 |
# Place sockets (node side and angular location) |
|
1383 | 6x |
for (i in seq_len(nrow(edges))) { |
1384 | 33x |
from <- edges$from[i] |
1385 | 33x |
to <- edges$to[i] |
1386 | 33x |
edge_id <- c(from = from, to = to) |
1387 | 33x |
edge_id_char <- paste(from, to, sep = "->") |
1388 | 33x |
recip_edge_id <- paste(to, from, sep = "->") |
1389 |
# Socket from |
|
1390 | 33x |
quadrant <- sankey_stress_quadrant(y = edges$to_y[i] - edges$from_y[i], |
1391 | 33x |
x = edges$to_x[i] - edges$from_x[i]) |
1392 | 33x |
socket_from <- list() |
1393 | 33x |
socket_from[["node_id"]] <- from |
1394 | 33x |
socket_from[["node_side"]] <- sides[quadrant["q"]] |
1395 | 33x |
socket_from[["edge_id"]] <- list(edge_id) |
1396 | 33x |
socket_from[["edge_id_char"]] <- edge_id_char |
1397 | 33x |
socket_from[["recip_edge_id"]] <- recip_edge_id |
1398 | 33x |
socket_from[["edge_end"]] <- "from" |
1399 | 33x |
socket_from[["rel_quadrant_angle"]] <- quadrant["a"] |
1400 | 33x |
socket_from[["width"]] <- edges$width[i] |
1401 |
# Socket to |
|
1402 | 33x |
quadrant <- sankey_stress_quadrant(y = -(edges$to_y[i] - edges$from_y[i]), |
1403 | 33x |
x = -(edges$to_x[i] - edges$from_x[i])) |
1404 | 33x |
socket_to <- list() |
1405 | 33x |
socket_to[["node_id"]] <- to |
1406 | 33x |
socket_to[["node_side"]] <- sides[quadrant["q"]] |
1407 | 33x |
socket_to[["edge_id"]] <- list(edge_id) |
1408 | 33x |
socket_to[["edge_id_char"]] <- edge_id_char |
1409 | 33x |
socket_to[["recip_edge_id"]] <- recip_edge_id |
1410 | 33x |
socket_to[["edge_end"]] <- "to" |
1411 | 33x |
socket_to[["rel_quadrant_angle"]] <- quadrant["a"] |
1412 | 33x |
socket_to[["width"]] <- edges$width[i] |
1413 |
# Give a little nudge if a reciprocal edge exists |
|
1414 | 33x |
if (recip_edge_id %in% sapply(sockets, "[[", "edge_id_char")) { |
1415 | 9x |
socket_to[["rel_quadrant_angle"]] <- socket_to[["rel_quadrant_angle"]] - 1e-8 |
1416 |
} |
|
1417 |
# Store sockets |
|
1418 | 33x |
sockets[[2*i-1]] <- socket_from |
1419 | 33x |
sockets[[2*i]] <- socket_to |
1420 |
} |
|
1421 | 6x |
sockets <- dplyr::bind_rows(sockets) |
1422 | 6x |
sockets <- sockets[order(sockets$node_id, sockets$node_side, |
1423 | 6x |
sockets$rel_quadrant_angle), ] |
1424 | ||
1425 |
# Update the node side accumulators and sockets relative locations |
|
1426 | 6x |
nodes$left_accumulator <- 0 |
1427 | 6x |
nodes$right_accumulator <- 0 |
1428 | 6x |
nodes$top_accumulator <- 0 |
1429 | 6x |
nodes$bottom_accumulator <- 0 |
1430 | 6x |
sockets[["rel_loc"]] <- 0 |
1431 | 6x |
for (i in seq_len(nrow(sockets))) { |
1432 | 66x |
ni <- which(nodes$comp == sockets$node_id[i]) |
1433 | 66x |
side <- sockets$node_side[i] |
1434 | 66x |
if (side == "left") { |
1435 | 12x |
sockets$rel_loc[i] <- nodes$left_accumulator[ni] - sockets$width[i]/2 |
1436 | 12x |
nodes$left_accumulator[ni] <- nodes$left_accumulator[ni] - sockets$width[i] |
1437 |
} |
|
1438 | 66x |
if (side == "bottom") { |
1439 | 21x |
sockets$rel_loc[i] <- nodes$bottom_accumulator[ni] + sockets$width[i]/2 |
1440 | 21x |
nodes$bottom_accumulator[ni] <- nodes$bottom_accumulator[ni] + sockets$width[i] |
1441 |
} |
|
1442 | 66x |
if (side == "right") { |
1443 | 12x |
sockets$rel_loc[i] <- nodes$right_accumulator[ni] + sockets$width[i]/2 |
1444 | 12x |
nodes$right_accumulator[ni] <- nodes$right_accumulator[ni] + sockets$width[i] |
1445 |
} |
|
1446 | 66x |
if (side == "top") { |
1447 | 21x |
sockets$rel_loc[i] <- nodes$top_accumulator[ni] - sockets$width[i]/2 |
1448 | 21x |
nodes$top_accumulator[ni] <- nodes$top_accumulator[ni] - sockets$width[i] |
1449 |
} |
|
1450 |
} |
|
1451 | ||
1452 |
# Optimize sockets distribution around a given node |
|
1453 | 6x |
for (n in nodes$comp) { |
1454 | 27x |
node_sockets <- sockets[sockets$node_id == n, ] |
1455 | 27x |
current_imbalance <- sankey_stress_socket_imbalance(node_sockets) |
1456 | 27x |
possible_moves <- sankey_stress_socket_moves(node_sockets) |
1457 | 27x |
if (length(possible_moves) > 0) { |
1458 | 18x |
possible_imbalances <- sapply(possible_moves, |
1459 | 18x |
sankey_stress_socket_imbalance) |
1460 | 18x |
possible_moves <- possible_moves[order(possible_imbalances)] |
1461 | 18x |
possible_imbalances <- sort(possible_imbalances) |
1462 | 18x |
if (possible_imbalances[1] < current_imbalance) { |
1463 |
# Do the move |
|
1464 | 18x |
sockets[sockets$node_id == n, ] <- possible_moves[[1]] |
1465 |
} |
|
1466 |
} |
|
1467 |
} |
|
1468 | ||
1469 |
# Update nodes accumulators and sockets rel_loc |
|
1470 | 6x |
sockets <- sockets[order(sockets$node_id, sockets$node_side, |
1471 | 6x |
sockets$rel_quadrant_angle), ] |
1472 | 6x |
nodes$left_accumulator <- 0 |
1473 | 6x |
nodes$right_accumulator <- 0 |
1474 | 6x |
nodes$top_accumulator <- 0 |
1475 | 6x |
nodes$bottom_accumulator <- 0 |
1476 | 6x |
sockets[["rel_loc"]] <- 0 |
1477 | 6x |
for (i in seq_len(nrow(sockets))) { |
1478 | 66x |
ni <- which(nodes$comp == sockets$node_id[i]) |
1479 | 66x |
side <- sockets$node_side[i] |
1480 | 66x |
if (side == "left") { |
1481 | 15x |
sockets$rel_loc[i] <- nodes$left_accumulator[ni] - sockets$width[i]/2 |
1482 | 15x |
nodes$left_accumulator[ni] <- nodes$left_accumulator[ni] - sockets$width[i] |
1483 |
} |
|
1484 | 66x |
if (side == "bottom") { |
1485 | 18x |
sockets$rel_loc[i] <- nodes$bottom_accumulator[ni] + sockets$width[i]/2 |
1486 | 18x |
nodes$bottom_accumulator[ni] <- nodes$bottom_accumulator[ni] + sockets$width[i] |
1487 |
} |
|
1488 | 66x |
if (side == "right") { |
1489 | 15x |
sockets$rel_loc[i] <- nodes$right_accumulator[ni] + sockets$width[i]/2 |
1490 | 15x |
nodes$right_accumulator[ni] <- nodes$right_accumulator[ni] + sockets$width[i] |
1491 |
} |
|
1492 | 66x |
if (side == "top") { |
1493 | 18x |
sockets$rel_loc[i] <- nodes$top_accumulator[ni] - sockets$width[i]/2 |
1494 | 18x |
nodes$top_accumulator[ni] <- nodes$top_accumulator[ni] - sockets$width[i] |
1495 |
} |
|
1496 |
} |
|
1497 |
} else { |
|
1498 |
# Default sockets |
|
1499 | ! |
for (i in seq_len(nrow(edges))) { |
1500 | ! |
from <- edges$from[i] |
1501 | ! |
to <- edges$to[i] |
1502 | ! |
edge_id <- c(from = from, to = to) |
1503 |
# Socket from |
|
1504 | ! |
socket_from <- list() |
1505 | ! |
socket_from[["node_id"]] <- from |
1506 | ! |
socket_from[["node_side"]] <- "center" |
1507 | ! |
socket_from[["edge_id"]] <- list(edge_id) |
1508 | ! |
socket_from[["edge_end"]] <- "from" |
1509 | ! |
socket_from[["rel_loc"]] <- 0 |
1510 | ! |
socket_from[["width"]] <- edges$width[i] |
1511 |
# Socket to |
|
1512 | ! |
socket_to <- list() |
1513 | ! |
socket_to[["node_id"]] <- to |
1514 | ! |
socket_to[["node_side"]] <- "center" |
1515 | ! |
socket_to[["edge_id"]] <- list(edge_id) |
1516 | ! |
socket_to[["edge_end"]] <- "to" |
1517 | ! |
socket_to[["rel_loc"]] <- 0 |
1518 | ! |
socket_to[["width"]] <- edges$width[i] |
1519 |
# Store sockets |
|
1520 | ! |
sockets[[2*i-1]] <- socket_from |
1521 | ! |
sockets[[2*i]] <- socket_to |
1522 |
} |
|
1523 |
} |
|
1524 |
# Return |
|
1525 | 8x |
sockets <- dplyr::bind_rows(sockets) |
1526 | 8x |
out <- scene |
1527 | 8x |
out[["nodes"]] <- nodes |
1528 | 8x |
out[["edges"]] <- edges |
1529 | 8x |
out[["edge_sockets"]] <- sockets |
1530 | 8x |
return(out) |
1531 |
} |
|
1532 | ||
1533 |
### * (3) sankey_calc_node_shape() |
|
1534 | ||
1535 |
#' Determine node shape |
|
1536 |
#' |
|
1537 |
#' @importFrom stats aggregate |
|
1538 |
#' |
|
1539 |
#' @param scene Scene list. |
|
1540 |
#' @param topo Topology. |
|
1541 |
#' @param nodes Node tibble. |
|
1542 |
#' @param flows Flows tibble. |
|
1543 |
#' @param layout Layout string. |
|
1544 |
#' @param node_f Multiplicative factor used to adjust node size. |
|
1545 |
#' @param xlim Limits of the x scale. |
|
1546 |
#' @param node_s String defining how node size is calculated. The effect of the |
|
1547 |
#' string also depends on the chosen layout. |
|
1548 |
#' |
|
1549 |
#' @examples |
|
1550 |
#' library(magrittr) |
|
1551 |
#' |
|
1552 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1553 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1554 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
1555 |
#' |
|
1556 |
#' topo <- topo(trini_mod) |
|
1557 |
#' flows <- flows_from_topo(topo) |
|
1558 |
#' flows$width <- 1 |
|
1559 |
#' layout <- "left2right" |
|
1560 |
#' scene <- sankey_place_nodes(topo, flows = flows, layout = layout) |
|
1561 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1562 |
#' z <- sankey_calc_node_shape(scene, topo, flows = flows, layout = layout) |
|
1563 |
#' |
|
1564 |
#' topo <- topo(trini_mod) |
|
1565 |
#' flows <- flows_from_topo(topo) |
|
1566 |
#' flows$width <- 1 |
|
1567 |
#' layout <- "stress" |
|
1568 |
#' scene <- sankey_place_nodes(topo, flows = flows, layout = layout) |
|
1569 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1570 |
#' z <- sankey_calc_node_shape(scene, topo, flows = flows, layout = layout) |
|
1571 |
#' |
|
1572 |
#' y <- new_networkModel() %>% |
|
1573 |
#' set_topo(c("subs -> NH3 -> subs", |
|
1574 |
#' "NH3 -> Q, E", "E -> Q -> E", |
|
1575 |
#' "E -> D, M")) %>% |
|
1576 |
#' set_steady("subs") %>% |
|
1577 |
#' set_prop_family("normal_sd") |
|
1578 |
#' y <- topo(y) |
|
1579 |
#' nodes <- nodes_from_topo(y) |
|
1580 |
#' nodes$size <- runif(nrow(nodes), 1, 2) |
|
1581 |
#' flows <- flows_from_topo(y) |
|
1582 |
#' flows$width <- runif(nrow(flows), 0.2, 5) |
|
1583 |
#' layout <- "stress" |
|
1584 |
#' scene <- sankey_place_nodes(y, nodes = nodes, flows = flows, layout = layout) |
|
1585 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, y, flows = flows, layout = layout) |
|
1586 |
#' z <- sankey_calc_node_shape(scene, y, flows = flows, layout = layout) |
|
1587 |
#' |
|
1588 |
#' @keywords internal |
|
1589 |
#' @noRd |
|
1590 | ||
1591 |
sankey_calc_node_shape <- function(scene, topo, nodes = NULL, flows, layout, |
|
1592 |
node_f = 1, xlim = c(0, 1), node_s = "auto") { |
|
1593 | 8x |
nodes <- scene$nodes |
1594 | 8x |
sockets <- scene$edge_sockets |
1595 | 8x |
nodes$shape <- NA |
1596 | 8x |
nodes$width <- NA |
1597 | 8x |
nodes$height <- NA |
1598 | 8x |
if (!"size" %in% colnames(nodes)) { |
1599 | ! |
nodes[["size"]] <- 1 |
1600 |
} |
|
1601 |
# Layout left2right |
|
1602 | 8x |
if (layout == "left2right") { |
1603 | 2x |
for (i in seq_len(nrow(nodes))) { |
1604 | 28x |
nodes$shape[i] <- "rect" |
1605 | 28x |
nodes$height[i] <- list(grid::unit(max(nodes$left_accumulator[i], |
1606 | 28x |
nodes$right_accumulator[i]), |
1607 | 28x |
"native")) |
1608 | 28x |
nodes$width[i] <- nodes$size[i] / as.numeric(nodes$height[[i]]) |
1609 |
} |
|
1610 |
# Adjust nodes width |
|
1611 | 2x |
max_width_per_x <- aggregate(width ~ x, data = nodes, FUN = max) |
1612 | 2x |
total_width <- sum(max_width_per_x$width) |
1613 | 2x |
max_allowed_width <- diff(xlim) * 0.5 |
1614 | 2x |
adj_factor <- max_allowed_width / total_width |
1615 | 2x |
nodes$width <- lapply(nodes$width * adj_factor * node_f, |
1616 | 2x |
function(x) grid::unit(x, "native") ) |
1617 | 6x |
} else if (layout %in% c("stress", "kk", "lgl", "fr", "dh", "mds")) { |
1618 |
# Layout stress or kk or ... |
|
1619 | 6x |
min_width <- grid::convertWidth(grid::unit(1, "strwidth", "x"), unitTo = "native", |
1620 | 6x |
valueOnly = TRUE) |
1621 | 6x |
min_height <- abs(grid::convertHeight(grid::unit(1, "strheight", "x"), unitTo = "native", |
1622 | 6x |
valueOnly = TRUE)) |
1623 | 6x |
for (i in seq_len(nrow(nodes))) { |
1624 | 27x |
node_sockets <- sockets[sockets$node_id == nodes$comp[i], ] |
1625 | 27x |
side_sockets <- lapply(c("top", "left", "right", "bottom"), function(s) { |
1626 | 108x |
z <- node_sockets[node_sockets$node_side == s, ] |
1627 | 48x |
if (nrow(z) == 0) return(0) |
1628 | 60x |
return(sum(z$width)) |
1629 |
}) |
|
1630 | 27x |
names(side_sockets) <- c("top", "left", "right", "bottom") |
1631 | 27x |
nodes$shape[i] <- "rect" |
1632 | 27x |
nodes$width[i] <- list(grid::unit(max(min_width, side_sockets[["top"]], |
1633 | 27x |
side_sockets[["bottom"]]), |
1634 | 27x |
units = "native")) |
1635 | 27x |
nodes$height[i] <- list(grid::unit(max(min_height, side_sockets[["left"]], |
1636 | 27x |
side_sockets[["right"]]), |
1637 | 27x |
units = "native")) |
1638 |
} |
|
1639 | 6x |
if (node_s == "constant") { |
1640 | 2x |
max_width <- do.call(max, nodes$width) |
1641 | 2x |
max_height <- do.call(max, nodes$height) |
1642 | 2x |
nodes$width <- rep(list(max_width), nrow(nodes)) |
1643 | 2x |
nodes$height <- rep(list(max_height), nrow(nodes)) |
1644 |
} |
|
1645 | 6x |
if (node_s == "square" | node_s == "roundsquare") { |
1646 | ! |
ext_factor <- ifelse(node_s == "roundsquare", 1.10, 1.05) |
1647 | ! |
max_width <- do.call(max, nodes$width) |
1648 | ! |
max_height <- do.call(max, nodes$height) |
1649 | ! |
side_dim <- max(max_width, max_height) * ext_factor |
1650 | ! |
nodes$width <- rep(list(side_dim), nrow(nodes)) |
1651 | ! |
nodes$height <- rep(list(side_dim), nrow(nodes)) |
1652 |
} |
|
1653 | 6x |
if (node_s == "prop") { |
1654 | 2x |
nodes$min_area <- as.numeric(nodes$width) * as.numeric(nodes$height) |
1655 | 2x |
nodes$dim_adj <- nodes$size / nodes$min_area |
1656 | 2x |
nodes$dim_adj <- nodes$dim_adj / min(nodes$dim_adj) |
1657 | 2x |
for (i in seq_len(nrow(nodes))) { |
1658 | 9x |
a <- max(as.numeric(nodes$width[[i]]), as.numeric(nodes$height[[i]])) |
1659 | 9x |
b <- min(as.numeric(nodes$width[[i]]), as.numeric(nodes$height[[i]])) |
1660 | 9x |
adj <- nodes$dim_adj[i] |
1661 | 9x |
if (adj <= a/b) { |
1662 | 5x |
beta <- adj |
1663 | 5x |
alpha <- 1 |
1664 |
} else { |
|
1665 | 4x |
beta <- sqrt(adj * a/b) |
1666 | 4x |
alpha <- sqrt(adj * b/a) |
1667 |
} |
|
1668 | 9x |
if (as.numeric(nodes$width[[i]]) >= as.numeric(nodes$height[[i]])) { |
1669 | 7x |
nodes$width[[i]] <- grid::unit(alpha * as.numeric(nodes$width[[i]]), "native") |
1670 | 7x |
nodes$height[[i]] <- grid::unit(beta * as.numeric(nodes$height[[i]]), "native") |
1671 |
} else { |
|
1672 | 2x |
nodes$width[[i]] <- grid::unit(beta * as.numeric(nodes$width[[i]]), "native") |
1673 | 2x |
nodes$height[[i]] <- grid::unit(alpha * as.numeric(nodes$height[[i]]), "native") |
1674 |
} |
|
1675 |
} |
|
1676 |
} |
|
1677 |
} else { |
|
1678 |
# Default node shape |
|
1679 | ! |
width <- list(grid::unit(1, "strwidth", "toto")) |
1680 | ! |
height <- list(grid::unit(1, "strheight", "toto")) |
1681 | ! |
for (i in seq_len(nrow(nodes))) { |
1682 | ! |
nodes$shape[i] <- "rect" |
1683 | ! |
nodes$width[i] <- width |
1684 | ! |
nodes$height[i] <- height |
1685 |
} |
|
1686 |
} |
|
1687 |
# Return |
|
1688 | 8x |
out <- scene |
1689 | 8x |
out$nodes <- nodes |
1690 | 8x |
return(out) |
1691 |
} |
|
1692 | ||
1693 |
### * (4) sankey_adjust_node_locations() |
|
1694 | ||
1695 |
#' Adjust node location |
|
1696 |
#' |
|
1697 |
#' @param scene Scene list. |
|
1698 |
#' @param topo Topology. |
|
1699 |
#' @param nodes Node tibble. |
|
1700 |
#' @param flows Flows tibble. |
|
1701 |
#' @param layout Layout string. |
|
1702 |
#' |
|
1703 |
#' @examples |
|
1704 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1705 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1706 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
1707 |
#' sankey_adjust_node_locations <- isotracer:::sankey_adjust_node_locations |
|
1708 |
#' |
|
1709 |
#' topo <- topo(trini_mod) |
|
1710 |
#' flows <- flows_from_topo(topo) |
|
1711 |
#' flows$width <- 1 |
|
1712 |
#' layout <- "left2right" |
|
1713 |
#' scene <- sankey_place_nodes(topo, flows = flows, layout = layout) |
|
1714 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1715 |
#' scene <- sankey_calc_node_shape(scene, topo, flows = flows, layout = layout) |
|
1716 |
#' z <- sankey_adjust_node_locations(scene, topo, flows = flows, layout = layout) |
|
1717 |
#' |
|
1718 |
#' @keywords internal |
|
1719 |
#' @noRd |
|
1720 | ||
1721 |
sankey_adjust_node_locations <- function(scene, topo, nodes = NULL, flows, layout) { |
|
1722 |
# Layout left2right |
|
1723 | 8x |
if (layout == "left2right") { |
1724 | 2x |
nodes <- scene$nodes |
1725 | 2x |
nodes <- nodes[order(nodes$x, nodes$y), ] |
1726 | 2x |
ylim <- range(nodes$y) |
1727 | 2x |
x_locs <- sort(unique(nodes$x)) |
1728 |
# Fix collision |
|
1729 | 2x |
for (xi in x_locs) { |
1730 | 8x |
ni <- nodes[nodes$x == xi, ] |
1731 | 8x |
previous_mean_y <- mean(ni$y) |
1732 | 8x |
total_height <- sum(as.numeric(unlist(ni$height))) |
1733 | 8x |
available_height <- diff(ylim) - total_height |
1734 | 8x |
min_spacer <- min(0.08, 0.1 * total_height, available_height / (nrow(ni) + 1)) |
1735 | 8x |
if (nrow(ni) > 1) { |
1736 | 8x |
for (i in 2:nrow(ni)) { |
1737 | 20x |
top_a <- ni$y[i-1] + as.numeric(ni$height[i-1]) / 2 |
1738 | 20x |
bottom_b <- ni$y[i] - as.numeric(ni$height[i]) / 2 |
1739 | 20x |
if (bottom_b - top_a < min_spacer) { |
1740 |
# Collision or too close, shift all remaining nodes upwards |
|
1741 | 2x |
shift <- top_a - bottom_b + min_spacer |
1742 | 2x |
ni$y[i] <- ni$y[i] + shift |
1743 |
} |
|
1744 |
} |
|
1745 |
} |
|
1746 | 8x |
new_mean_y <- mean(nodes$y) |
1747 | 8x |
ni$y <- ni$y - new_mean_y + previous_mean_y |
1748 | 8x |
nodes[nodes$x == xi, ] <- ni |
1749 |
} |
|
1750 |
# Quick minimization |
|
1751 | 2x |
for (xi in x_locs) { |
1752 | 8x |
comps <- nodes$comp[nodes$x == xi] |
1753 | 8x |
sources <- colnames(topo)[apply(topo[comps,, drop = FALSE], 2, sum) > 0] |
1754 | 8x |
if (length(sources) > 0) { |
1755 | 6x |
mean_y_self <- mean(nodes$y[nodes$x == xi]) |
1756 | 6x |
mean_y_sources <- mean(nodes$y[nodes$comp %in% sources]) |
1757 | 6x |
nodes$y[nodes$x == xi] <- nodes$y[nodes$x == xi] - mean_y_self + mean_y_sources |
1758 |
} |
|
1759 |
} |
|
1760 |
# Adjust node location so that none is outside the canvas |
|
1761 | 2x |
for (xi in x_locs) { |
1762 | 8x |
ni <- nodes[nodes$x == xi, ] |
1763 | 8x |
if (nrow(ni) > 1) { |
1764 | 8x |
previous_mean_y <- mean(ni$y) |
1765 | 8x |
total_height <- sum(as.numeric(unlist(ni$height))) |
1766 | 8x |
available_height <- diff(ylim) - total_height |
1767 | 8x |
min_spacer <- min(0.08, 0.1 * total_height, available_height / (nrow(ni) + 1)) |
1768 | 8x |
lowest <- ni$y[1] - as.numeric(ni$height[1]) / 2 |
1769 | 8x |
if (lowest < ylim[1]) { |
1770 | 2x |
extra <- ylim[1] - lowest |
1771 | 2x |
total_spacing <- (ni$y[nrow(ni)] - as.numeric(ni$height[nrow(ni)]) / 2) - lowest |
1772 | 2x |
if (total_spacing > extra) { |
1773 |
# The extra height can be accommodated in the spacing |
|
1774 | 2x |
spacing_adj_factor <- 1 - extra / total_spacing |
1775 |
# Apply this factor on the nodes from top to bottom |
|
1776 | 2x |
for (i in (nrow(ni)-1):1) { |
1777 | 10x |
current_spacing <- (ni$y[i+1] - as.numeric(ni$height[i+1]) / 2 - |
1778 | 10x |
ni$y[i] + as.numeric(ni$height[i]) / 2) |
1779 | 10x |
shift <- current_spacing * (1 - spacing_adj_factor) |
1780 | 10x |
for (j in i:1) { |
1781 | 30x |
ni$y[j] <- ni$y[j] + shift |
1782 |
} |
|
1783 |
} |
|
1784 |
} |
|
1785 |
} |
|
1786 | 8x |
highest <- ni$y[nrow(ni)] + as.numeric(ni$height[nrow(ni)]) / 2 |
1787 | 8x |
if (highest > ylim[2]) { |
1788 | 2x |
extra <- highest - ylim[2] |
1789 | 2x |
total_spacing <- highest - ni$y[1] - as.numeric(ni$height[1]) / 2 |
1790 | 2x |
if (total_spacing > extra) { |
1791 |
# The extra height can be accommodated in the spacing |
|
1792 | 2x |
spacing_adj_factor <- 1 - extra / total_spacing |
1793 |
# Apply this factor on the nodes from bottom to top |
|
1794 | 2x |
for (i in 2:nrow(ni)) { |
1795 | 10x |
current_spacing <- (ni$y[i] - as.numeric(ni$height[i]) / 2 - |
1796 | 10x |
ni$y[i-1] + as.numeric(ni$height[i-1]) / 2) |
1797 | 10x |
shift <- current_spacing * (1 - spacing_adj_factor) |
1798 | 10x |
for (j in i:nrow(ni)) { |
1799 | 30x |
ni$y[j] <- ni$y[j] - shift |
1800 |
} |
|
1801 |
} |
|
1802 |
} |
|
1803 |
} |
|
1804 |
} |
|
1805 | 8x |
nodes[nodes$x == xi, ] <- ni |
1806 |
} |
|
1807 | 2x |
out <- scene |
1808 | 2x |
out$nodes <- nodes |
1809 |
} else { |
|
1810 |
# Default is to do nothing |
|
1811 | 6x |
out <- scene |
1812 |
} |
|
1813 | 8x |
return(out) |
1814 |
} |
|
1815 | ||
1816 |
### * (5) sankey_adjust_edge_sockets() |
|
1817 | ||
1818 |
#' Adjust edge sockets relative location on nodes |
|
1819 |
#' |
|
1820 |
#' @param scene Scene list. |
|
1821 |
#' @param topo Topology. |
|
1822 |
#' @param nodes Node tibble. |
|
1823 |
#' @param flows Flows tibble. |
|
1824 |
#' @param layout Layout string. |
|
1825 |
#' |
|
1826 |
#' @keywords internal |
|
1827 |
#' @noRd |
|
1828 | ||
1829 |
sankey_adjust_edge_sockets <- function(scene, topo, nodes = NULL, flows, layout) { |
|
1830 | 8x |
nodes <- scene$nodes |
1831 | 8x |
edges <- scene$edges |
1832 | 8x |
edge_sockets <- scene$edge_sockets |
1833 |
# Layout left2right |
|
1834 | 8x |
if (layout == "left2right") { |
1835 | 2x |
for (i in seq_len(nrow(edge_sockets))) { |
1836 |
# Slide sockets so that each node face has its group of sockets centered |
|
1837 | 68x |
node_i <- which(nodes$comp == edge_sockets$node_id[i]) |
1838 | 68x |
if (edge_sockets$edge_end[i] == "from") { |
1839 | 34x |
shift <- nodes$right_accumulator[node_i] / 2 |
1840 |
} else { |
|
1841 | 34x |
shift <- nodes$left_accumulator[node_i] / 2 |
1842 |
} |
|
1843 | 68x |
edge_sockets$rel_loc[i] <- edge_sockets$rel_loc[i] - shift |
1844 |
} |
|
1845 | 2x |
out <- scene |
1846 | 2x |
out[["edge_sockets"]] <- edge_sockets |
1847 | 6x |
} else if (layout %in% c("stress", "kk", "lgl", "fr", "dh", "mds")) { |
1848 | 6x |
for (i in seq_len(nrow(edge_sockets))) { |
1849 |
# Slide sockets so that each node face has its group of sockets centered |
|
1850 | 66x |
node_i <- which(nodes$comp == edge_sockets$node_id[i]) |
1851 | 66x |
side <- edge_sockets$node_side[i] |
1852 | 66x |
if (side == "left") { |
1853 | 15x |
shift <- nodes$left_accumulator[node_i] / 2 |
1854 | 51x |
} else if (side == "top") { |
1855 | 18x |
shift <- nodes$top_accumulator[node_i] / 2 |
1856 | 33x |
} else if (side == "right") { |
1857 | 15x |
shift <- nodes$right_accumulator[node_i] / 2 |
1858 |
} else { |
|
1859 | 18x |
shift <- nodes$bottom_accumulator[node_i] / 2 |
1860 |
} |
|
1861 | 66x |
edge_sockets$rel_loc[i] <- edge_sockets$rel_loc[i] - shift |
1862 |
} |
|
1863 | 6x |
out <- scene |
1864 | 6x |
out[["edge_sockets"]] <- edge_sockets |
1865 |
} else { |
|
1866 |
# Default is to do nothing |
|
1867 | ! |
out <- scene |
1868 |
} |
|
1869 |
# Return |
|
1870 | 8x |
return(out) |
1871 |
} |
|
1872 | ||
1873 |
### * (6) sankey_calc_edge_socket_coordinates() |
|
1874 | ||
1875 |
#' Calculate absolute coordinates of edge sockets |
|
1876 |
#' |
|
1877 |
#' This function calculates the absolute (x, y) coordinates of the center of |
|
1878 |
#' each edge socket, as well as the normal vector of each socket (which is |
|
1879 |
#' actually the normal vector of the receiving node face). |
|
1880 |
#' |
|
1881 |
#' @param scene Scene list. |
|
1882 |
#' @param topo Topology. |
|
1883 |
#' @param nodes Node tibble. |
|
1884 |
#' @param flows Flows tibble. |
|
1885 |
#' @param layout Layout string. |
|
1886 |
#' |
|
1887 |
#' @examples |
|
1888 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1889 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1890 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
1891 |
#' sankey_adjust_edge_sockets <- isotracer:::sankey_adjust_edge_sockets |
|
1892 |
#' sankey_calc_edge_socket_coordinates <- isotracer:::sankey_calc_edge_socket_coordinates |
|
1893 |
#' |
|
1894 |
#' topo <- topo(trini_mod) |
|
1895 |
#' nodes <- nodes_from_topo(topo) |
|
1896 |
#' nodes$size <- 1 |
|
1897 |
#' flows <- flows_from_topo(topo) |
|
1898 |
#' flows$width <- 1 |
|
1899 |
#' layout <- "left2right" |
|
1900 |
#' scene <- sankey_place_nodes(topo, nodes = nodes, flows = flows, layout = layout) |
|
1901 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1902 |
#' scene <- sankey_calc_node_shape(scene, topo, flows = flows, layout = layout) |
|
1903 |
#' scene <- sankey_adjust_edge_sockets(scene, topo, flows = flows, layout = layout) |
|
1904 |
#' z <- sankey_calc_edge_socket_coordinates(scene, topo, flows = flows, layout = layout) |
|
1905 |
#' |
|
1906 |
#' @keywords internal |
|
1907 |
#' @noRd |
|
1908 | ||
1909 |
sankey_calc_edge_socket_coordinates <- function(scene, topo, nodes = NULL, flows, layout) { |
|
1910 | 8x |
nodes <- scene$nodes |
1911 | 8x |
sockets <- scene$edge_sockets |
1912 | 8x |
sockets$center_x <- NA |
1913 | 8x |
sockets$center_y <- NA |
1914 | 8x |
sockets$normal <- rep(list(NA), nrow(sockets)) |
1915 |
# Process the sockets |
|
1916 | 8x |
for (i in seq_len(nrow(sockets))) { |
1917 | 134x |
side <- sockets$node_side[i] |
1918 | 134x |
ni <- which(nodes$comp == sockets$node_id[i]) |
1919 | 134x |
nw <- as.numeric(grid::convertWidth(nodes$width[[ni]], unitTo = "native")) |
1920 | 134x |
nh <- as.numeric(grid::convertHeight(nodes$height[[ni]], unitTo = "native")) |
1921 | 134x |
stopifnot(side %in% c("center", "left", "right", "top", "bottom")) |
1922 | 134x |
if (side == "center") { |
1923 | ! |
sockets$center_x[i] <- nodes$x[ni] |
1924 | ! |
sockets$center_y[i] <- nodes$y[ni] |
1925 | ! |
sockets$normal[[i]] <- c(0, 0) |
1926 | 134x |
} else if (side %in% c("left", "right")) { |
1927 | 98x |
sockets$center_x[i] <- nodes$x[ni] |
1928 | 98x |
sockets$center_y[i] <- (nodes$y[ni] + |
1929 | 98x |
sockets$rel_loc[i]) |
1930 | 98x |
if (side == "left") { |
1931 | 49x |
sockets$normal[[i]] <- c(-1, 0) |
1932 | 49x |
sockets$center_x[i] <- sockets$center_x[i] - nw / 2 |
1933 |
} else { |
|
1934 | 49x |
sockets$normal[[i]] <- c(1, 0) |
1935 | 49x |
sockets$center_x[i] <- sockets$center_x[i] + nw / 2 |
1936 |
} |
|
1937 | 36x |
} else if (side %in% c("top", "bottom")) { |
1938 | 36x |
sockets$center_x[i] <- (nodes$x[ni] + |
1939 | 36x |
sockets$rel_loc[i]) |
1940 | 36x |
sockets$center_y[i] <- nodes$y[ni] |
1941 | 36x |
if (side == "top") { |
1942 | 18x |
sockets$normal[[i]] <- c(0, 1) |
1943 | 18x |
sockets$center_y[i] <- sockets$center_y[i] + nh / 2 |
1944 |
} else { |
|
1945 | 18x |
sockets$normal[[i]] <- c(0, -1) |
1946 | 18x |
sockets$center_y[i] <- sockets$center_y[i] - nh / 2 |
1947 |
} |
|
1948 |
} |
|
1949 |
} |
|
1950 |
# Return |
|
1951 | 8x |
out <- scene |
1952 | 8x |
out[["edge_sockets"]] <- sockets |
1953 | 8x |
return(out) |
1954 |
} |
|
1955 | ||
1956 |
### * (7) sankey_place_edge_backbones() |
|
1957 | ||
1958 |
#' Calculate absolute coordinates of edge backbones |
|
1959 |
#' |
|
1960 |
#' @param scene Scene list. |
|
1961 |
#' @param topo Topology. |
|
1962 |
#' @param nodes Node tibble. |
|
1963 |
#' @param flows Flows tibble. |
|
1964 |
#' @param layout Layout string. |
|
1965 |
#' @param n Integer, number of steps used for Bézier curve interpolation. |
|
1966 |
#' |
|
1967 |
#' @examples |
|
1968 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1969 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1970 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
1971 |
#' sankey_calc_edge_socket_coordinates <- isotracer:::sankey_calc_edge_socket_coordinates |
|
1972 |
#' sankey_place_edge_backbones <- isotracer:::sankey_place_edge_backbones |
|
1973 |
#' |
|
1974 |
#' t <- topo(trini_mod) |
|
1975 |
#' nodes <- tibble::tibble(comp = colnames(t), size = 1, col = "red") |
|
1976 |
#' flows <- flows_from_topo(t) |
|
1977 |
#' flows$width <- 1 |
|
1978 |
#' layout <- "left2right" |
|
1979 |
#' scene <- sankey_place_nodes(t, nodes = nodes, flows = flows, layout = layout) |
|
1980 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, t, flows = flows, layout = layout) |
|
1981 |
#' scene <- sankey_calc_node_shape(scene, t, flows = flows, layout = layout) |
|
1982 |
#' scene <- sankey_calc_edge_socket_coordinates(scene, t, flows = flows, layout = layout) |
|
1983 |
#' z <- sankey_place_edge_backbones(scene, t, flows = flows, layout = layout) |
|
1984 |
#' |
|
1985 |
#' @keywords internal |
|
1986 |
#' @noRd |
|
1987 | ||
1988 |
sankey_place_edge_backbones <- function(scene, topo, nodes = NULL, flows, layout, |
|
1989 |
n = 32) { |
|
1990 | 8x |
nodes <- scene$nodes |
1991 | 8x |
edges <- scene$edges |
1992 | 8x |
edges$backbone <- rep(list(NA), nrow(edges)) |
1993 | 8x |
edges$control_points <- rep(list(NA), nrow(edges)) |
1994 | 8x |
edge_sockets <- scene$edge_sockets |
1995 |
# Default |
|
1996 | 8x |
for (i in seq_len(nrow(edges))) { |
1997 | 67x |
edge_id <- c(from = edges$from[i], to = edges$to[i]) |
1998 | 67x |
sockets <- edge_sockets[sapply(edge_sockets$edge_id, function(x) identical(edge_id, x)), ] |
1999 | 67x |
s_from <- as.list(sockets[sockets$edge_end == "from", ]) |
2000 | 67x |
s_to <- as.list(sockets[sockets$edge_end == "to", ]) |
2001 | 67x |
xy_start <- c(s_from$center_x, s_from$center_y) |
2002 | 67x |
xy_end <- c(s_to$center_x, s_to$center_y) |
2003 | 67x |
if (identical(s_from$normal[[1]], c(0, 0))) { |
2004 | ! |
v <- xy_end - xy_start |
2005 | ! |
v <- v / sqrt(sum(v^2)) |
2006 | ! |
s_from$normal[[1]] <- v |
2007 |
} |
|
2008 | 67x |
if (identical(s_to$normal[[1]], c(0, 0))) { |
2009 | ! |
v <- xy_start - xy_end |
2010 | ! |
v <- v / sqrt(sum(v^2)) |
2011 | ! |
s_to$normal[[1]] <- v |
2012 |
} |
|
2013 | 67x |
if (layout %in% c("left2right")) { |
2014 | 34x |
len_straight <- sqrt(sum((xy_end[1] - xy_start[1])^2)) |
2015 | 34x |
xy_start2 <- xy_start + c(s_from$normal[[1]][1], 0) * len_straight * 0.15 |
2016 | 34x |
xy_end2 <- xy_end + c(s_to$normal[[1]][1], 0) * len_straight * 0.15 |
2017 | 34x |
xy_start3 <- xy_start + c(s_from$normal[[1]][1], 0) * len_straight * 0.3 |
2018 | 34x |
xy_end3 <- xy_end + c(s_to$normal[[1]][1], 0) * len_straight * 0.3 |
2019 | 34x |
bb <- cbind(xy_start, xy_start2, xy_start3, |
2020 | 34x |
xy_end3, xy_end2, xy_end) |
2021 | 33x |
} else if (layout %in% c("stress", "kk", "lgl", "fr", "dh", "mds")) { |
2022 | 33x |
len_straight <- sqrt(sum((xy_end - xy_start)^2)) |
2023 | 33x |
xy_start2 <- xy_start + s_from$normal[[1]] * len_straight * 0.15 |
2024 | 33x |
xy_end2 <- xy_end + s_to$normal[[1]] * len_straight * 0.15 |
2025 | 33x |
xy_start3 <- xy_start + s_from$normal[[1]] * len_straight * 0.3 |
2026 | 33x |
xy_end3 <- xy_end + s_to$normal[[1]] * len_straight * 0.3 |
2027 | 33x |
bb <- cbind(xy_start, xy_start2, xy_start3, |
2028 | 33x |
xy_end3, xy_end2, xy_end) |
2029 |
} else { |
|
2030 | ! |
len_straight <- sqrt(sum((xy_end - xy_start)^2)) |
2031 | ! |
xy_start2 <- xy_start + s_from$normal[[1]] * len_straight * 0.2 |
2032 | ! |
xy_end2 <- xy_end + s_to$normal[[1]] * len_straight * 0.2 |
2033 | ! |
bb <- cbind(xy_start, xy_start2, xy_end2, xy_end) |
2034 |
} |
|
2035 | 67x |
edges$control_points[[i]] <- t(bb) |
2036 | 67x |
edges$backbone[[i]] <- bezierCurve(x = bb[1, ], y = bb[2, ], n = n) |
2037 |
} |
|
2038 |
# Return |
|
2039 | 8x |
out <- scene |
2040 | 8x |
out[["edges"]] <- edges |
2041 | 8x |
return(out) |
2042 |
} |
|
2043 | ||
2044 |
### * (8) sankey_place_labels() |
|
2045 | ||
2046 |
#' Place node labels |
|
2047 |
#' |
|
2048 |
#' @param scene Scene list. |
|
2049 |
#' @param topo Topology. |
|
2050 |
#' @param nodes Node tibble. |
|
2051 |
#' @param flows Flows tibble. |
|
2052 |
#' @param layout Layout string. |
|
2053 |
#' @param cex_lab Expansion factor for label size. |
|
2054 |
#' |
|
2055 |
#' @examples |
|
2056 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
2057 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
2058 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
2059 |
#' sankey_calc_edge_socket_coordinates <- isotracer:::sankey_calc_edge_socket_coordinates |
|
2060 |
#' sankey_place_edge_backbones <- isotracer:::sankey_place_edge_backbones |
|
2061 |
#' sankey_place_labels <- isotracer:::sankey_place_labels |
|
2062 |
#' |
|
2063 |
#' t <- topo(trini_mod) |
|
2064 |
#' nodes <- tibble::tibble(comp = colnames(t), size = 1, col = "red") |
|
2065 |
#' flows <- flows_from_topo(t) |
|
2066 |
#' flows$width <- 1 |
|
2067 |
#' layout <- "left2right" |
|
2068 |
#' scene <- sankey_place_nodes(t, nodes = nodes, flows = flows, layout = layout) |
|
2069 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, t, flows = flows, layout = layout) |
|
2070 |
#' scene <- sankey_calc_node_shape(scene, t, flows = flows, layout = layout) |
|
2071 |
#' scene <- sankey_calc_edge_socket_coordinates(scene, t, flows = flows, layout = layout) |
|
2072 |
#' scene <- sankey_place_edge_backbones(scene, t, flows = flows, layout = layout) |
|
2073 |
#' z <- sankey_place_labels(scene, t, flows = flows, layout = layout) |
|
2074 |
#' |
|
2075 |
#' @keywords internal |
|
2076 |
#' @noRd |
|
2077 | ||
2078 |
sankey_place_labels <- function(scene, topo, nodes, flows, layout, cex_lab = 1) { |
|
2079 | 8x |
nodes <- scene$nodes |
2080 | 8x |
labels <- nodes[, c("comp", "label", "x", "y", "width", "height")] |
2081 | 8x |
names(labels) <- c("comp", "label", "node_x", "node_y", "node_width", "node_height") |
2082 |
# Convert node dimensions to native units |
|
2083 | 8x |
labels$node_width <- sapply(labels$node_width, function(x) { |
2084 | 55x |
grid::convertWidth(x, unitTo = "native", valueOnly = TRUE) |
2085 |
}) |
|
2086 | 8x |
labels$node_height <- sapply(labels$node_height, function(x) { |
2087 | 55x |
grid::convertHeight(x, unitTo = "native", valueOnly = TRUE) |
2088 |
}) |
|
2089 |
# Calculate label coordinates |
|
2090 | 8x |
labels <- tibble::add_column(labels, cex = cex_lab, label_x = NA, label_y = NA) |
2091 | 8x |
for (i in seq_len(nrow(labels))) { |
2092 | 55x |
node_bottom <- labels$node_y[i] - labels$node_height[i]/2 |
2093 | 55x |
labels$label_x[i] <- labels$node_x[i] |
2094 | 55x |
labels$label_y[i] <- (node_bottom - |
2095 | 55x |
grid::convertHeight(grid::unit(0.5, "strheight", "X"), |
2096 | 55x |
unitTo = "native", valueOnly = TRUE) - |
2097 | 55x |
grid::convertHeight(grid::unit(cex_lab, "strheight", "X"), |
2098 | 55x |
unitTo = "native", valueOnly = TRUE) / 2) |
2099 |
} |
|
2100 |
# Return |
|
2101 | 8x |
out <- scene |
2102 | 8x |
out[["labels"]] <- labels |
2103 | 8x |
return(out) |
2104 |
} |
|
2105 | ||
2106 |
### * sankey_stress_quadrant() |
|
2107 | ||
2108 |
#' From an absolute angle, calculate the quadrant and relative angle in this quadrant |
|
2109 |
#' |
|
2110 |
#' In the "stress" layout, each node has four quadrants defined from its (x, y) |
|
2111 |
#' centre: 1 = east (-pi/4, pi/4), 2 = north (pi/4, 3pi/4), 3 = west (3pi/4, |
|
2112 |
#' 5pi/4), and 4 = south (-3pi/4, -pi/4). |
|
2113 |
#' |
|
2114 |
#' This function takes (y, x) in the same way as \code{\link{atan2}}. |
|
2115 |
#' |
|
2116 |
#' @param y,x Same as for \code{\link{atan2}}. From atan2 help: "The arc-tangent |
|
2117 |
#' of two arguments atan2(y, x) returns the angle between the x-axis and |
|
2118 |
#' the vector from the origin to (x, y), i.e., for positive arguments |
|
2119 |
#' atan2(y, x) == atan(y/x)." |
|
2120 |
#' |
|
2121 |
#' @return A vector with two elements: the quadrant and the relative |
|
2122 |
#' anti-clockwise angle from this quadrant origin. |
|
2123 |
#' |
|
2124 |
#' @examples |
|
2125 |
#' sankey_stress_quadrant <- isotracer:::sankey_stress_quadrant |
|
2126 |
#' |
|
2127 |
#' theta <- seq(- pi, pi, length.out = 128) |
|
2128 |
#' x <- cos(theta) |
|
2129 |
#' y <- sin(theta) |
|
2130 |
#' q <- sapply(seq_along(x), function(i) sankey_stress_quadrant(y[i], x[i])) |
|
2131 |
#' plot(theta, q[2,], col = c("blue", "green", "purple", "red")[q[1,]], |
|
2132 |
#' pch = 19) |
|
2133 |
#' |
|
2134 |
#' @keywords internal |
|
2135 |
#' @noRd |
|
2136 | ||
2137 |
sankey_stress_quadrant <- function(y, x) { |
|
2138 | 66x |
angle <- as.vector(atan2(y, x)) |
2139 | 66x |
stopifnot(angle <= pi & angle >= -pi) |
2140 | 66x |
if (angle >= -pi/4 & angle <= pi/4) { |
2141 |
# east |
|
2142 | 12x |
q <- 1 |
2143 | 12x |
a <- angle + pi/4 |
2144 | 54x |
} else if (angle >= pi/4 & angle <= 3*pi/4) { |
2145 |
# north |
|
2146 | 21x |
q <- 2 |
2147 | 21x |
a <- angle - pi/4 |
2148 | 33x |
} else if (angle >= 3*pi/4 | angle <= -3*pi/4 ) { |
2149 |
# west |
|
2150 | 12x |
q <- 3 |
2151 | 12x |
if (angle >= 3*pi/4) { |
2152 | 3x |
a <- angle - 3*pi/4 |
2153 |
} else { |
|
2154 | 9x |
angle <- angle + 2 * pi |
2155 | 9x |
a <- angle - 3*pi/4 |
2156 |
} |
|
2157 |
} else { |
|
2158 |
# south |
|
2159 | 21x |
q <- 4 |
2160 | 21x |
a <- angle + 3*pi/4 |
2161 |
} |
|
2162 | 66x |
return(c(q = q, a = a)) |
2163 |
} |
|
2164 | ||
2165 |
### * sankey_stress_socket_imbalance() |
|
2166 | ||
2167 |
#' Calculate an imbalance score for socket distribution around a node |
|
2168 |
#' |
|
2169 |
#' @param node_sockets A tibble containing the (at most 4) rows describing the |
|
2170 |
#' sockets related to one given node. |
|
2171 |
#' |
|
2172 |
#' @return The variance of the total socket width per node side (or NA if the |
|
2173 |
#' input has zero row). This can be used as an imbalance score to minimize |
|
2174 |
#' when optimizing the distribution of sockets around a node. |
|
2175 |
#' |
|
2176 |
#' @importFrom stats aggregate |
|
2177 |
#' @importFrom stats var |
|
2178 |
#' |
|
2179 |
#' @examples |
|
2180 |
#' sankey_stress_socket_imbalance <- isotracer:::sankey_stress_socket_imbalance |
|
2181 |
#' |
|
2182 |
#' z <- structure(list(node_id = c("arg", "arg"), node_side = c("bottom", "top"), |
|
2183 |
#' edge_id = list(c(from = "petro", to = "arg"), c(from = "tricor", to = "arg")), |
|
2184 |
#' edge_id_char = c("petro->arg", "tricor->arg"), |
|
2185 |
#' recip_edge_id = c("arg->petro", "arg->tricor"), edge_end = c("to", "to"), |
|
2186 |
#' rel_quadrant_angle = c(0.00537724841543286, 1.26282578153344), width = c(1, 1), |
|
2187 |
#' rel_loc = c(0.5, -0.5)), row.names = c(NA, -2L), |
|
2188 |
#' class = c("tbl_df", "tbl", "data.frame")) |
|
2189 |
#' sankey_stress_socket_imbalance(z) |
|
2190 |
#' |
|
2191 |
#' @keywords internal |
|
2192 |
#' @noRd |
|
2193 | ||
2194 |
sankey_stress_socket_imbalance <- function(node_sockets) { |
|
2195 | 51x |
if (nrow(node_sockets) > 0) { |
2196 | 51x |
width_per_side <- tibble::deframe(aggregate(width ~ node_side, |
2197 | 51x |
data = node_sockets, |
2198 | 51x |
FUN = sum)) |
2199 | 51x |
width_per_side <- c(width_per_side, rep(0, 4 - length(width_per_side))) |
2200 | 51x |
imbalance <- var(width_per_side) |
2201 |
} else { |
|
2202 | ! |
imbalance <- NA |
2203 |
} |
|
2204 | 51x |
return(imbalance) |
2205 |
} |
|
2206 | ||
2207 |
### * sankey_stress_socket_moves() |
|
2208 | ||
2209 |
#' Determine all the possible socket arrangements one move away from input |
|
2210 |
#' |
|
2211 |
#' @param node_sockets A tibble containing the (at most 4) rows describing the |
|
2212 |
#' sockets related to one given node. |
|
2213 |
#' |
|
2214 |
#' @keywords internal |
|
2215 |
#' @noRd |
|
2216 | ||
2217 |
sankey_stress_socket_moves <- function(node_sockets) { |
|
2218 | 27x |
if (nrow(node_sockets) == 1) { |
2219 | 9x |
return(list()) |
2220 |
} |
|
2221 | 18x |
moves <- list() |
2222 | 18x |
moves_i <- 1 |
2223 | 18x |
ref <- node_sockets |
2224 | 18x |
cclock_move <- c("top" = "left", "left" = "bottom", "bottom" = "right", |
2225 | 18x |
"right" = "top") |
2226 | 18x |
clock_move <- c("bottom" = "left", "left" = "top", "top" = "right", |
2227 | 18x |
"right" = "bottom") |
2228 | 18x |
for (side in unique(ref$node_side)) { |
2229 | 33x |
side_sockets <- ref[ref$node_side == side, ] |
2230 | 33x |
side_sockets <- side_sockets[order(side_sockets$rel_quadrant_angle), ] |
2231 | 33x |
if (nrow(side_sockets) > 1) { |
2232 | 18x |
if (side_sockets$rel_quadrant_angle[1] <= pi/4) { |
2233 |
# Clockwise move |
|
2234 | 12x |
new_side <- clock_move[side] |
2235 | 12x |
if (!any(side_sockets$node_side == new_side)) { |
2236 | 12x |
new_angle <- pi/2 - 0.01 |
2237 |
} else { |
|
2238 | ! |
new_angle <- pi/2 - (pi/2 - max(side_sockets$rel_quadrant_angle[side_sockets$node_side == new_side])) / 2 |
2239 |
} |
|
2240 |
# Update |
|
2241 | 12x |
side_sockets$node_side[1] <- new_side |
2242 | 12x |
side_sockets$rel_quadrant_angle[1] <- new_angle |
2243 | 12x |
z <- ref |
2244 | 12x |
z[z$node_side == side, ] <- side_sockets |
2245 | 12x |
moves[[moves_i]] <- z |
2246 | 12x |
moves_i <- moves_i + 1 |
2247 |
} |
|
2248 | 18x |
side_sockets <- ref[ref$node_side == side, ] |
2249 | 18x |
side_sockets <- side_sockets[order(side_sockets$rel_quadrant_angle), ] |
2250 | 18x |
if (side_sockets$rel_quadrant_angle[nrow(side_sockets)] > pi/4) { |
2251 |
# Counter-clockwise move |
|
2252 | 12x |
new_side <- cclock_move[side] |
2253 | 12x |
if (!any(side_sockets$node_side == new_side)) { |
2254 | 12x |
new_angle <- 0.01 |
2255 |
} else { |
|
2256 | ! |
new_angle <- min(side_sockets$rel_quadrant_angle[side_sockets$node_side == new_side]) / 2 |
2257 |
} |
|
2258 |
# Update |
|
2259 | 12x |
side_sockets$node_side[nrow(side_sockets)] <- new_side |
2260 | 12x |
side_sockets$rel_quadrant_angle[nrow(side_sockets)] <- new_angle |
2261 | 12x |
z <- ref |
2262 | 12x |
z[z$node_side == side, ] <- side_sockets |
2263 | 12x |
moves[[moves_i]] <- z |
2264 | 12x |
moves_i <- moves_i + 1 |
2265 |
} |
|
2266 |
} |
|
2267 |
} |
|
2268 | 18x |
return(moves) |
2269 |
} |
|
2270 | ||
2271 |
### * sankey_get_elements_lims() |
|
2272 | ||
2273 |
#' Return the xlim and ylim to contain all the graphical elements |
|
2274 |
#' |
|
2275 |
#' @param scene Scene list. |
|
2276 |
#' |
|
2277 |
#' @return A list with "xlim" and "ylim" elements. |
|
2278 |
#' |
|
2279 |
#' @keywords internal |
|
2280 |
#' @noRd |
|
2281 | ||
2282 |
sankey_get_elements_lims <- function(scene) { |
|
2283 | 8x |
nodes <- scene[["nodes"]] |
2284 | 8x |
nodes_min_x <- min(nodes$x - as.numeric(nodes$width)/2) |
2285 | 8x |
nodes_max_x <- max(nodes$x + as.numeric(nodes$width)/2) |
2286 | 8x |
nodes_min_y <- min(nodes$y - as.numeric(nodes$height)/2) |
2287 | 8x |
nodes_max_y <- max(nodes$y + as.numeric(nodes$height)/2) |
2288 | 8x |
edges <- scene[["edges"]] |
2289 | 8x |
edges$ribbon <- lapply(seq_len(nrow(edges)), function(i) { |
2290 | 67x |
ribbonFromTrajectory(edges[["backbone"]][[i]], |
2291 | 67x |
width = edges[["width"]][i]) |
2292 |
}) |
|
2293 | 8x |
ribbons <- do.call(rbind, edges$ribbon) |
2294 | 8x |
edges_min_x <- min(ribbons[, 1]) |
2295 | 8x |
edges_max_x <- max(ribbons[, 1]) |
2296 | 8x |
edges_min_y <- min(ribbons[, 2]) |
2297 | 8x |
edges_max_y <- max(ribbons[, 2]) |
2298 | 8x |
xlim <- c(min(nodes_min_x, edges_min_x), |
2299 | 8x |
max(nodes_max_x, edges_max_x)) |
2300 | 8x |
ylim <- c(min(nodes_min_y, edges_min_y), |
2301 | 8x |
max(nodes_max_y, edges_max_y)) |
2302 | 8x |
return(list(xlim = xlim, ylim = ylim)) |
2303 |
} |
1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * describe_z_eta() |
|
4 | ||
5 |
#' Print a message describing the role of eta or zeta in a distribution family |
|
6 |
#' |
|
7 |
#' @param param_name Name of the parameter (e.g. "eta" or "zeta"). |
|
8 |
#' @param family Family string. |
|
9 |
#' @param prefix,suffix Strings appended to the message. |
|
10 |
#' |
|
11 |
#' @keywords internal |
|
12 |
#' @noRd |
|
13 | ||
14 |
describe_z_eta <- function(param_name, family, prefix = " (", suffix = ")") { |
|
15 | ! |
msg <- list( |
16 | ! |
"gamma_cv" = " is the coefficient of variation of gamma distributions.", |
17 | ! |
"normal_cv" = " is the coefficient of variation of normal distributions.", |
18 | ! |
"normal_sd" = " is the standard deviation of normal distributions.", |
19 | ! |
"beta_phi" = " is the precision (phi) of beta distributions.") |
20 | ! |
if (!family %in% names(msg)) { |
21 | ! |
stop("The provided value for the family argument is not allowed.") |
22 |
} |
|
23 | ! |
message(prefix, param_name, msg[[family]], suffix, sep = "") |
24 |
} |
|
25 | ||
26 |
### * valid_prior_tbl() |
|
27 | ||
28 |
#' Test if the input is a valid prior tibble |
|
29 |
#' |
|
30 |
#' @param x Some input to test. |
|
31 |
#' |
|
32 |
#' @return Boolean. |
|
33 |
#' |
|
34 |
#' @examples |
|
35 |
#' valid_prior_tbl(priors(aquarium_mod)) |
|
36 |
#' valid_prior_tbl(mtcars) |
|
37 |
#' |
|
38 |
#' @keywords internal |
|
39 |
#' @noRd |
|
40 | ||
41 |
valid_prior_tbl <- function(x) { |
|
42 |
# Is tibble? |
|
43 | ! |
if (!is(x, "tbl_df")) return(FALSE) |
44 |
# Has at least columns "in_model" and "prior"? |
|
45 | ! |
if (!all(c("in_model", "prior") %in% colnames(x))) return(FALSE) |
46 |
# "in_model" is a character vector? |
|
47 | ! |
if (!is(x[["in_model"]], "character")) return(FALSE) |
48 |
# "prior" is a list? |
|
49 | ! |
if (!is(x[["prior"]], "list")) return(FALSE) |
50 |
# Are all the entries in the "prior" column NULL or priors? |
|
51 | ! |
non_null <- x[["prior"]][!is.null(x[["prior"]])] |
52 | ! |
if (!all(sapply(non_null, is, "prior"))) return(FALSE) |
53 |
# End of tests |
|
54 | ! |
return(TRUE) |
55 |
} |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * new_networkModel() |
|
4 | ||
5 |
#' Create an empty network model |
|
6 |
#' |
|
7 |
#' The first step in building a network model is to create a new, empty |
|
8 |
#' \code{networkModel} object. This model can then be completed using functions |
|
9 |
#' such as \code{set_topo()}, \code{set_init()}, etc... |
|
10 |
#' |
|
11 |
#' @param quiet Boolean, if \code{FALSE} print a message indicating which |
|
12 |
#' distribution family is used for proportions. |
|
13 |
#' |
|
14 |
#' @return An empty \code{networkModel} object. It is basically a zero-row |
|
15 |
#' tibble with the appropriate columns. |
|
16 |
#' |
|
17 |
#' @examples |
|
18 |
#' m <- new_networkModel() |
|
19 |
#' m |
|
20 |
#' class(m) |
|
21 |
#' |
|
22 |
#' @export |
|
23 | ||
24 |
new_networkModel <- function(quiet = FALSE) { |
|
25 | 38x |
verbose <- !quiet |
26 | 38x |
x <- tibble::tibble(topology = list(), |
27 | 38x |
initial = list(), |
28 | 38x |
observations = list()) |
29 | 38x |
attr(x, "prop_family") <- "gamma_cv" |
30 | 38x |
if (verbose) { |
31 | ! |
message("Using default distribution family for proportions (\"gamma_cv\").") |
32 | ! |
describe_z_eta("eta", attr(x, "prop_family")) |
33 |
} |
|
34 | 38x |
attr(x, "size_family") <- "normal_cv" |
35 | 38x |
if (verbose) { |
36 | ! |
message("Using default distribution family for sizes (\"normal_cv\").") |
37 | ! |
describe_z_eta("zeta", attr(x, "size_family")) |
38 |
} |
|
39 | 38x |
attr(x, "size_zeta_per_compartment") <- FALSE |
40 | 38x |
x <- structure(x, class = c("networkModel", class(x))) |
41 | 38x |
return(x) |
42 |
} |
|
43 | ||
44 |
### * set_prop_family() |
|
45 | ||
46 |
#' Set the distribution family for observed proportions |
|
47 |
#' |
|
48 |
#' @param nm A \code{networkModel} object (output from |
|
49 |
#' \code{\link{new_networkModel}}). |
|
50 |
#' @param family Allowed values are "gamma_cv", "beta_phi", "normal_cv", and |
|
51 |
#' "normal_sd". |
|
52 |
#' @param quiet Boolean, if \code{FALSE} print a message indicating which |
|
53 |
#' distribution family is used for proportions. |
|
54 |
#' |
|
55 |
#' @return A \code{networkModel} object. |
|
56 |
#' |
|
57 |
#' @examples |
|
58 |
#' library(magrittr) |
|
59 |
#' |
|
60 |
#' m <- new_networkModel() %>% |
|
61 |
#' set_topo(links = "NH4, NO3 -> epi -> pseph, tricor") |
|
62 |
#' m <- m %>% set_prop_family("beta_phi") |
|
63 |
#' m |
|
64 |
#' attr(m, "prop_family") |
|
65 |
#' |
|
66 |
#' @export |
|
67 | ||
68 |
set_prop_family <- function(nm, family, quiet = FALSE) { |
|
69 | 5x |
verbose <- !quiet |
70 | 5x |
known_families <- c("gamma_cv" = 1, "normal_cv" = 2, "normal_sd" = 3, |
71 | 5x |
"beta_phi" = 4) |
72 | 5x |
if (!family %in% names(known_families)) { |
73 | ! |
stop("Unknown distribution family for proportions. Got value: \"", |
74 | ! |
family, "\"\n", |
75 | ! |
"Allowed values are: ", paste0(sapply(names(known_families), function(x) paste0("\"", x, "\"")), |
76 | ! |
collapse = ", ")) |
77 |
} |
|
78 | 5x |
attr(nm, "prop_family") <- family |
79 | 5x |
if (verbose) { |
80 | ! |
message("Using distribution family for proportions: \"", family, "\".", |
81 | ! |
sep = "") |
82 | ! |
describe_z_eta("eta", family) |
83 |
} |
|
84 | 5x |
return(nm) |
85 |
} |
|
86 | ||
87 |
### * set_size_family() |
|
88 | ||
89 |
#' Set the distribution family for observed sizes |
|
90 |
#' |
|
91 |
#' @param nm A \code{networkModel} object (output from |
|
92 |
#' \code{\link{new_networkModel}}). |
|
93 |
#' @param family Allowed values are "normal_cv" and "normal_sd". |
|
94 |
#' @param by_compartment Boolean, if TRUE then zeta is compartment-specific. |
|
95 |
#' @param quiet Boolean, if \code{FALSE} print a message indicating which |
|
96 |
#' distribution family is used for proportions. |
|
97 |
#' @param quiet_reset Boolean, write a message when model parameters (and |
|
98 |
#' covariates and priors) are reset? |
|
99 |
#' |
|
100 |
#' @return A \code{networkModel} object. |
|
101 |
#' |
|
102 |
#' @examples |
|
103 |
#' library(magrittr) |
|
104 |
#' |
|
105 |
#' m <- new_networkModel() %>% |
|
106 |
#' set_topo(links = "NH4, NO3 -> epi -> pseph, tricor") |
|
107 |
#' m <- m %>% set_size_family("normal_sd") |
|
108 |
#' m |
|
109 |
#' attr(m, "size_family") |
|
110 |
#' |
|
111 |
#' m <- m %>% set_size_family(by_compartment = TRUE) |
|
112 |
#' attr(m, "size_zeta_per_compartment") |
|
113 |
#' |
|
114 |
#' @export |
|
115 | ||
116 |
set_size_family <- function(nm, family, by_compartment, quiet = FALSE, |
|
117 |
quiet_reset = FALSE) { |
|
118 | ! |
verbose <- !quiet |
119 | ! |
verbose_reset <- !quiet_reset |
120 | ! |
if (!missing(family)) { |
121 | ! |
known_families <- c("normal_cv" = 1, "normal_sd" = 2) |
122 | ! |
if (!family %in% names(known_families)) { |
123 | ! |
stop("Unknown distribution family for proportions. Got value: \"", |
124 | ! |
family, "\"\n", |
125 | ! |
"Allowed values are: ", paste0(sapply(names(known_families), function(x) paste0("\"", x, "\"")), |
126 | ! |
collapse = ", ")) |
127 |
} |
|
128 | ! |
attr(nm, "size_family") <- family |
129 | ! |
if (verbose) { |
130 | ! |
message("Using distribution family for sizes: \"", family, "\".", |
131 | ! |
sep = "") |
132 | ! |
describe_z_eta("zeta", family) |
133 |
} |
|
134 |
} |
|
135 | ! |
if (!missing(by_compartment)) { |
136 | ! |
stopifnot(by_compartment %in% c(FALSE, TRUE)) |
137 | ! |
attr(nm, "size_zeta_per_compartment") <- by_compartment |
138 | ! |
if(verbose) { |
139 | ! |
message("Compartment-specific zeta set to ", by_compartment, ".", |
140 | ! |
sep = "") |
141 |
} |
|
142 | ! |
if (!is.null(nm[["parameters"]])) { |
143 |
# nm already had parameters |
|
144 |
# Reset to update the zeta parameters |
|
145 | ! |
nm <- add_param_mapping(nm) |
146 | ! |
if (verbose_reset) { |
147 | ! |
message("Parameters priors and covariates were reset because `by_compartment` was specified.") |
148 | ! |
message("(This means that no priors are set and no covariates are used in the returned networkModel.)") |
149 |
} |
|
150 |
} |
|
151 |
} |
|
152 | ! |
return(nm) |
153 |
} |
|
154 | ||
155 |
### * set_topo() |
|
156 | ||
157 |
### ** Doc |
|
158 | ||
159 |
#' Set the topology in a network model. |
|
160 |
#' |
|
161 |
#' @param nm A \code{networkModel} object (output from |
|
162 |
#' \code{\link{new_networkModel}}). |
|
163 |
#' @param ... One or more strings describing the links defining the network |
|
164 |
#' topology. Optionally, links can be given as a data frame. See the |
|
165 |
#' examples for more details about acceptable input formats. |
|
166 |
#' @param from Optional, string containing the column name for sources if |
|
167 |
#' links are provided as a data frame. |
|
168 |
#' @param to Optional, string containing the column name for destinations if |
|
169 |
#' links are provided as a data frame. |
|
170 |
#' |
|
171 |
#' @return A \code{networkModel} object. |
|
172 |
#' |
|
173 |
#' @examples |
|
174 |
#' # A single string can describe several links in one go. |
|
175 |
#' m <- new_networkModel() %>% |
|
176 |
#' set_topo("NH4, NO3 -> epi -> pseph, tricor") |
|
177 |
#' m |
|
178 |
#' topo(m) |
|
179 |
#' |
|
180 |
#' # Several strings can be given as distinct arguments. |
|
181 |
#' m2 <- new_networkModel() %>% |
|
182 |
#' set_topo("NH4, NO3 -> epi -> pseph, tricor", |
|
183 |
#' "NH4 -> FBOM, CBOM", "CBOM <- NO3") |
|
184 |
#' m2 |
|
185 |
#' topo(m2) |
|
186 |
#' |
|
187 |
#' # Multiple strings can be also be combined into a single argument with `c()`. |
|
188 |
#' links <- c("NH4, NO3 -> epi -> pseph, tricor", "NH4 -> FBOM, CBOM", |
|
189 |
#' "CBOM <- NO3") |
|
190 |
#' m3 <- new_networkModel() %>% |
|
191 |
#' set_topo(links) |
|
192 |
#' m3 |
|
193 |
#' topo(m3) |
|
194 |
#' |
|
195 |
#' # A data frame can be used to specify the links. |
|
196 |
#' links <- data.frame(source = c("NH4", "NO3", "epi"), |
|
197 |
#' consumer = c("epi", "epi", "petro")) |
|
198 |
#' links |
|
199 |
#' m4 <- new_networkModel() %>% |
|
200 |
#' set_topo(links, from = "source", to = "consumer") |
|
201 |
#' m4 |
|
202 |
#' m4$topology[[1]] |
|
203 |
#' |
|
204 |
#' @export |
|
205 | ||
206 |
### ** Code |
|
207 | ||
208 |
set_topo <- function(nm, ..., from = NULL, to = NULL) { |
|
209 |
# Parse the ... argument |
|
210 | 38x |
args <- list(...) |
211 | 38x |
if (length(args) > 1) { |
212 | 2x |
if (!all(sapply(args, is.character))) { |
213 | 1x |
stop("Only strings can be used when providing multiple link arguments.\n", |
214 | 1x |
"(Maybe you provided both string(s) and data frame(s)?)") |
215 |
} |
|
216 | 1x |
links <- unlist(args) |
217 |
} else { |
|
218 | 36x |
links <- args[[1]] |
219 |
} |
|
220 |
# Build the topology object |
|
221 | 37x |
topology <- make_topology(links = links, from = from, to = to) |
222 |
# If the network model is empty, create a row of NULLs |
|
223 | 37x |
message_reset <- TRUE |
224 | 37x |
if (nrow(nm) == 0) { |
225 | 36x |
nm[1, 1] <- NA |
226 | 36x |
message_reset <- FALSE |
227 |
} |
|
228 |
# Assign all the cells in the "topology" column |
|
229 | 37x |
for (i in seq_len(nrow(nm))) { |
230 | 37x |
nm$topology[[i]] <- topology |
231 |
} |
|
232 |
# Add parameter mapping |
|
233 | 37x |
nm <- add_param_mapping(nm) |
234 | 37x |
if (message_reset) { |
235 | 1x |
msg <- c("`set_topo()` was called on a non-empty networkModel object.", |
236 | 1x |
"As a result, the parameter mapping and the priors of the model were reset.") |
237 | 1x |
message(paste0(msg, collapse = " ")) |
238 |
} |
|
239 | 37x |
return(nm) |
240 |
} |
|
241 | ||
242 |
### * set_steady() |
|
243 | ||
244 |
#' Flag some network compartments as being in a steady state |
|
245 |
#' |
|
246 |
#' @param nm A \code{networkModel} object. |
|
247 |
#' @param comps Vector of strings, names of the compartments to set steady. |
|
248 |
#' @param which Vector of integers giving the nm rows to update. Default is to |
|
249 |
#' update all rows. |
|
250 |
#' |
|
251 |
#' @return A \code{networkModel} object. |
|
252 |
#' |
|
253 |
#' @examples |
|
254 |
#' library(magrittr) |
|
255 |
#' x <- new_networkModel() %>% |
|
256 |
#' set_topo("NH4 -> algae -> daphnia") %>% |
|
257 |
#' set_steady("NH4") |
|
258 |
#' topo(x) |
|
259 |
#' |
|
260 |
#' @export |
|
261 | ||
262 |
set_steady <- function(nm, comps = NULL, which = NULL) { |
|
263 | 6x |
if (is.null(which)) { |
264 | 6x |
which <- seq_len(nrow(nm)) |
265 |
} |
|
266 | 6x |
stopifnot(all(which %in% seq_len(nrow(nm)))) |
267 |
# Update row by row |
|
268 | 6x |
for (i in which) { |
269 | 7x |
topo <- nm[["topology"]][[i]] |
270 | 7x |
steady <- attr(topo, "steadyState") |
271 | 7x |
for (comp in comps) { |
272 | 8x |
stopifnot(comp %in% colnames(topo)) |
273 | 8x |
steady <- c(steady, comp) |
274 |
} |
|
275 | 7x |
attr(nm[["topology"]][[i]], "steadyState") <- steady |
276 |
} |
|
277 |
# Return |
|
278 | 6x |
return(nm) |
279 |
} |
|
280 | ||
281 |
### * set_split() |
|
282 | ||
283 |
#' Flag some network compartments as being split compartments |
|
284 |
#' |
|
285 |
#' This function automatically adds a default prior (uniform on [0,1]) for the |
|
286 |
#' active portion of split compartments. |
|
287 |
#' |
|
288 |
#' @param nm A \code{networkModel} object. |
|
289 |
#' @param comps Vector of strings, the names of the compartments to set split. |
|
290 |
#' @param which Vector of integers giving the nm rows to update. Default is to |
|
291 |
#' update all rows. |
|
292 |
#' |
|
293 |
#' @return A \code{networkModel} object. |
|
294 |
#' |
|
295 |
#' @examples |
|
296 |
#' library(magrittr) |
|
297 |
#' x <- new_networkModel() %>% |
|
298 |
#' set_topo("NH4 -> algae -> daphnia") %>% |
|
299 |
#' set_split("algae") |
|
300 |
#' topo(x) |
|
301 |
#' |
|
302 |
#' @export |
|
303 | ||
304 |
set_split <- function(nm, comps = NULL, which = NULL) { |
|
305 | 6x |
if (is.null(which)) { |
306 | 6x |
which <- seq_len(nrow(nm)) |
307 |
} |
|
308 | 6x |
stopifnot(all(which %in% seq_len(nrow(nm)))) |
309 |
# Update row by row |
|
310 | 6x |
for (i in which) { |
311 | 6x |
topo <- nm[["topology"]][[i]] |
312 | 6x |
split <- attr(topo, "split") |
313 | 6x |
for (comp in comps) { |
314 | 6x |
stopifnot(comp %in% colnames(topo)) |
315 | 6x |
split <- c(split, comp) |
316 |
} |
|
317 | 6x |
attr(nm[["topology"]][[i]], "split") <- split |
318 |
# Add the corresponding parameters |
|
319 | 6x |
params <- nm[["parameters"]][[i]] |
320 | 6x |
for (comp in comps) { |
321 | 6x |
paramName <- paste0("portion.act_", comp) |
322 | 6x |
params <- tibble::add_row(params, in_replicate = paramName, |
323 | 6x |
in_model = paramName) |
324 |
} |
|
325 | 6x |
nm[["parameters"]][[i]] <- params |
326 |
} |
|
327 |
# Update priors |
|
328 | 6x |
for (comp in comps) { |
329 | 6x |
paramName <- paste0("portion.act_", comp) |
330 | 6x |
attr(nm, "priors") <- tibble::add_row(attr(nm, "priors"), |
331 | 6x |
in_model = paramName, |
332 | 6x |
prior = list(uniform_p(0, 1))) |
333 |
} |
|
334 | 6x |
return(nm) |
335 |
} |
|
336 | ||
337 |
### * set_half_life() |
|
338 | ||
339 |
#' Set the half-life for radioactive tracers |
|
340 |
#' |
|
341 |
#' Indicating a non-zero value for half-life will add a decay to the marked |
|
342 |
#' portion of the tracer element. The decay constant is calculated from the |
|
343 |
#' half-life value as: |
|
344 |
#' |
|
345 |
#' lambda_decay = log(2) / half_life |
|
346 |
#' |
|
347 |
#' Note that for correct calculations the half-life value should be given in |
|
348 |
#' the same time unit (e.g. hour, day) that the time unit used for |
|
349 |
#' observations. |
|
350 |
#' |
|
351 |
#' @param nm A \code{networkModel} object. |
|
352 |
#' @param hl Half-life value, in the same time unit as the observations are (or |
|
353 |
#' will be) given. Setting half-life to zero is equivalent to using a |
|
354 |
#' stable isotope (no decay used in the model). |
|
355 |
#' @param quiet Boolean for verbosity. |
|
356 |
#' |
|
357 |
#' @return A \code{networkModel} object. |
|
358 |
#' |
|
359 |
#' @examples |
|
360 |
#' library(magrittr) |
|
361 |
#' x <- new_networkModel() %>% |
|
362 |
#' set_topo("32P -> root -> leaf") %>% |
|
363 |
#' set_half_life(hl = 14.268) |
|
364 |
#' x |
|
365 |
#' |
|
366 |
#' @export |
|
367 | ||
368 |
set_half_life <- function(nm, hl, quiet = FALSE) { |
|
369 | ! |
verbose <- !quiet |
370 | ! |
if (hl == 0) { |
371 | ! |
if (verbose) { |
372 | ! |
message("Half-life set to zero; the model will assume stable isotopes.") |
373 |
} |
|
374 | ! |
attr(nm, "lambda_hl") <- NULL |
375 |
} else { |
|
376 | ! |
lambda_hl <- log(2) / hl |
377 | ! |
if (verbose) { |
378 | ! |
message("Half-life set to ", hl, " time units.\n", |
379 | ! |
"(equivalent to a decay rate of ", signif(lambda_hl, 3), " per time unit).") |
380 |
} |
|
381 | ! |
attr(nm, "lambda_hl") <- lambda_hl |
382 |
} |
|
383 | ! |
return(nm) |
384 |
} |
|
385 | ||
386 |
### * add_pulse_event() |
|
387 | ||
388 |
#' Register a pulse event on one of the compartment of a topology |
|
389 |
#' |
|
390 |
#' When applied to a steady-state compartment, this is equivalent to changing |
|
391 |
#' the steady state. Negative values are allowed, so one can add a "pulse" to a |
|
392 |
#' steady-state compartment and then later add a similar but negative "pulse" |
|
393 |
#' to simulate a drip in a stream for example. |
|
394 |
#' |
|
395 |
#' @param nm A \code{networkModel} object. |
|
396 |
#' @param time Numeric, time at which the pulse is happening. |
|
397 |
#' @param comp One compartment name only. |
|
398 |
#' @param unmarked Numeric, quantity of unmarked marker added. |
|
399 |
#' @param marked Numeric, quantity of marked marker added. |
|
400 |
#' @param which Vector of integers giving the nm rows to update. Default is to |
|
401 |
#' update all rows. |
|
402 |
#' @param pulses Optionally, a tibble containing the pulse information in |
|
403 |
#' columns. If provided, `comp`, `time`, `unmarked` and `marked` must be |
|
404 |
#' strings giving the corresponding column names. |
|
405 |
#' |
|
406 |
#' @return A \code{networkModel} object. |
|
407 |
#' |
|
408 |
#' @examples |
|
409 |
#' m <- trini_mod |
|
410 |
#' m$events <- NULL |
|
411 |
#' pulses <- tibble::tribble( |
|
412 |
#' ~ stream, ~ transect, ~ comp, ~ time, ~ qty_14N, ~ qty_15N, |
|
413 |
#' "UL", "transect.1", "NH4", 11, 0, -0.00569, |
|
414 |
#' "UL", "transect.2", "NH4", 11, 0, -0.00264, |
|
415 |
#' "UL", "transect.3", "NH4", 11, 0, -0.000726, |
|
416 |
#' "UL", "transect.1", "NO3", 11, 0, -0.00851, |
|
417 |
#' "UL", "transect.2", "NO3", 11, 0, -0.01118, |
|
418 |
#' "UL", "transect.3", "NO3", 11, 0, -0.01244, |
|
419 |
#' ) |
|
420 |
#' m <- add_pulse_event(m, pulses = pulses, comp = "comp", time = "time", |
|
421 |
#' unmarked = "qty_14N", marked = "qty_15N") |
|
422 |
#' m |
|
423 |
#' |
|
424 |
#' |
|
425 |
#' @export |
|
426 | ||
427 |
add_pulse_event <- function(nm, time, comp = NULL, unmarked, marked, |
|
428 |
which = NULL, pulses) { |
|
429 | 2x |
if (!missing(pulses)) { |
430 |
# Table syntax |
|
431 | ! |
if (is.null(groups(nm))) { |
432 |
# No groups |
|
433 | ! |
for (i in seq_len(nrow(pulses))) { |
434 | ! |
args <- list(nm = nm, |
435 | ! |
time = pulses[[time]][i], |
436 | ! |
comp = pulses[[comp]][i], |
437 | ! |
unmarked = pulses[[unmarked]][i], |
438 | ! |
marked = pulses[[marked]][i]) |
439 | ! |
nm <- do.call(add_pulse_event, args) |
440 |
} |
|
441 | ! |
return(nm) |
442 |
} else { |
|
443 |
# Groups |
|
444 | ! |
grps <- groups(nm) |
445 | ! |
gp_vars <- colnames(grps) |
446 | ! |
if (!all(gp_vars %in% colnames(pulses))) { |
447 | ! |
gp_vars_str <- paste0("\"", gp_vars, "\"") |
448 | ! |
stop("The `pulses` argument must have columns specifying the group variables of the network model.\n", |
449 | ! |
" (Model grouped by: ", paste(gp_vars_str, collapse = ", "), ")\n", |
450 | ! |
" (Missing column(s) in `pulses` table: ", paste(gp_vars_str[!gp_vars %in% colnames(pulses)], collapse = ", "), ")\n") |
451 |
} |
|
452 | ! |
pulses_labels <- apply(as.matrix(pulses[, gp_vars]), 1, paste, collapse = " ") |
453 | ! |
for (i in seq_len(nrow(pulses))) { |
454 | ! |
grps <- groups(nm) |
455 | ! |
grps_labels <- apply(as.matrix(grps), 1, paste, collapse = " ") |
456 | ! |
row_i <- which(grps_labels == pulses_labels[i]) |
457 | ! |
stopifnot(length(row_i) == 1) |
458 | ! |
args <- list(nm = nm, |
459 | ! |
time = pulses[[time]][i], |
460 | ! |
comp = pulses[[comp]][i], |
461 | ! |
unmarked = pulses[[unmarked]][i], |
462 | ! |
marked = pulses[[marked]][i], |
463 | ! |
which = row_i) |
464 | ! |
nm <- do.call(add_pulse_event, args) |
465 |
} |
|
466 | ! |
return(nm) |
467 |
} |
|
468 |
} |
|
469 |
# Single pulse |
|
470 | 2x |
if (is.null(which)) { |
471 | 2x |
which <- seq_len(nrow(nm)) |
472 |
} |
|
473 | 2x |
stopifnot(all(which %in% seq_len(nrow(nm)))) |
474 |
# Create an empty "events" column if needed |
|
475 | 2x |
if (!"events" %in% colnames(nm)) { |
476 | 1x |
nm[["events"]] <- rep(list(NULL), nrow(nm)) |
477 |
} |
|
478 |
# Update row by row |
|
479 | 2x |
for (i in which) { |
480 | 4x |
topo <- nm[["topology"]][[i]] |
481 | 4x |
stopifnot(comp %in% colnames(topo)) |
482 | 4x |
this_event <- tibble::tibble(event = "pulse", |
483 | 4x |
time = time, |
484 | 4x |
compartment = comp, |
485 | 4x |
characteristics = list(list(unmarked = unmarked, |
486 | 4x |
marked = marked))) |
487 | 4x |
nm[["events"]][[i]] <- dplyr::bind_rows(nm[["events"]][[i]], |
488 | 4x |
this_event) |
489 | 4x |
nm[["events"]][[i]] <- nm[["events"]][[i]][order(nm[["events"]][[i]][["time"]], |
490 | 4x |
nm[["events"]][[i]][["compartment"]]), ] |
491 |
} |
|
492 |
# Return |
|
493 | 2x |
return(nm) |
494 |
} |
|
495 | ||
496 |
### * set_init() |
|
497 | ||
498 |
#' Set initial conditions in a network model |
|
499 |
#' |
|
500 |
#' @param nm A \code{networkModel} object (e.g. output from |
|
501 |
#' \code{\link{new_networkModel}}) |
|
502 |
#' @param data A tibble containing the initial conditions |
|
503 |
#' @param comp String, name of the \code{data} column with the compartment names |
|
504 |
#' @param size String, name of the \code{data} column with the compartment sizes |
|
505 |
#' @param prop String, name of the \code{data} column with the compartment |
|
506 |
#' proportions of marked tracer |
|
507 |
#' @param group_by Optional vector of string giving the names of the columns to |
|
508 |
#' use for grouping the data into replicates |
|
509 |
#' |
|
510 |
#' @return A \code{networkModel} object. |
|
511 |
#' |
|
512 |
#' @examples |
|
513 |
#' # Using the topology from the Trinidad case study |
|
514 |
#' m <- new_networkModel() %>% |
|
515 |
#' set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph", |
|
516 |
#' "FBOM -> tricor", "petro, tricor -> arg") |
|
517 |
#' |
|
518 |
#' # Taking initial condtions from the 'lalaja' dataset at t=0 |
|
519 |
#' inits <- lalaja[lalaja[["time.days"]] == 0, ] |
|
520 |
#' inits |
|
521 |
#' m <- set_init(m, inits, comp = "compartment", size = "mgN.per.m2", |
|
522 |
#' prop = "prop15N", group_by = "transect") |
|
523 |
#' m |
|
524 |
#' |
|
525 |
#' @export |
|
526 | ||
527 |
set_init <- function(nm, data, comp, size, prop, group_by = NULL) { |
|
528 |
# Trim extra compartments |
|
529 | 29x |
topo_comps <- unique(unlist(comps(nm))) |
530 | 29x |
init_comps <- unique(data[[comp]]) |
531 | 29x |
extra_comps <- init_comps[!init_comps %in% topo_comps] |
532 | 29x |
if (length(extra_comps) > 0) { |
533 | ! |
message(paste0(strwrap("Some compartments are not present in the network topology and are removed from the initial data:"), |
534 | ! |
collapse = "\n"), "\n", |
535 | ! |
" - ", paste0(extra_comps, collapse = "\n - "), "\n") |
536 | ! |
data <- data[data[[comp]] %in% topo_comps, ] |
537 |
} |
|
538 |
# Build init |
|
539 | 29x |
grouped_init <- make_init(data = data, comp = comp, |
540 | 29x |
size = size, prop = prop, |
541 | 29x |
group_by = group_by) |
542 | 29x |
out <- merge_nm_by_group(nm = nm, tib = grouped_init, |
543 | 29x |
destination = "initial", |
544 | 29x |
tib_name = "initial conditions") |
545 | 29x |
attr(out, "prop_family") <- attr(nm, "prop_family") |
546 | 29x |
attr(out, "default_columns") <- list(comp = comp, |
547 | 29x |
size = size, |
548 | 29x |
prop = prop, |
549 | 29x |
group_by = group_by) |
550 |
# Check that each compartment gets exactly one initial condition |
|
551 | 29x |
for (i in seq_len(nrow(out))) { |
552 | 47x |
z <- out$initial[[i]] |
553 | 47x |
if (nrow(z) != nrow(na.omit(z))) { |
554 | ! |
stop("Some NAs are present in the initial conditions.") |
555 |
} |
|
556 | 47x |
if (!all(table(z$compartment) == 1)) { |
557 | ! |
stop("Some compartments are present twice in the initial conditions data.") |
558 |
} |
|
559 | 47x |
if (!all(topo_comps %in% z[["compartment"]])) { |
560 | ! |
stop("Some compartments are present in the topology but do not have initial conditions.") |
561 |
} |
|
562 |
} |
|
563 | 29x |
return(out) |
564 |
} |
|
565 | ||
566 |
### * set_obs() |
|
567 | ||
568 |
#' Set observations in a network model |
|
569 |
#' |
|
570 |
#' @param nm A \code{networkModel} object (e.g. output from |
|
571 |
#' \code{\link{new_networkModel}}) |
|
572 |
#' @param data A tibble containing the observations. If NULL, remove |
|
573 |
#' observations from the model. |
|
574 |
#' @param comp String, name of the \code{data} column with the compartment |
|
575 |
#' names |
|
576 |
#' @param size String, name of the \code{data} column with the compartment |
|
577 |
#' sizes |
|
578 |
#' @param prop String, name of the \code{data} column with the compartment |
|
579 |
#' proportions of heavy tracer |
|
580 |
#' @param time String, name of the \code{data} column with the sampling times |
|
581 |
#' @param group_by Optional vector of string giving the names of the columns to |
|
582 |
#' use for grouping the data into replicates |
|
583 |
#' |
|
584 |
#' @return A \code{networkModel} object. |
|
585 |
#' |
|
586 |
#' @examples |
|
587 |
#' # Using the topology from the Trinidad case study |
|
588 |
#' m <- new_networkModel() %>% |
|
589 |
#' set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph", |
|
590 |
#' "FBOM -> tricor", "petro, tricor -> arg") |
|
591 |
#' |
|
592 |
#' # Taking initial condtions from the 'lalaja' dataset at t=0 |
|
593 |
#' inits <- lalaja[lalaja[["time.days"]] == 0, ] |
|
594 |
#' inits |
|
595 |
#' m <- set_init(m, inits, comp = "compartment", size = "mgN.per.m2", |
|
596 |
#' prop = "prop15N", group_by = "transect") |
|
597 |
#' m |
|
598 |
#' |
|
599 |
#' # Taking observations from 'lalaja' |
|
600 |
#' m <- set_obs(m, lalaja[lalaja[["time.days"]] > 0, ], time = "time.days") |
|
601 |
#' m |
|
602 |
#' plot(m) |
|
603 |
#' |
|
604 |
#' @export |
|
605 | ||
606 |
set_obs <- function(nm, data, comp, size, prop, time, group_by) { |
|
607 | 25x |
if (is.null(data)) { |
608 | ! |
nm[["observations"]] <- rep(list(NULL), nrow(nm)) |
609 | ! |
return(nm) |
610 |
} |
|
611 |
# Get default columns |
|
612 | 25x |
default_columns <- attr(nm, "default_columns") |
613 | 25x |
columns_from_attr <- c() |
614 | 25x |
if (missing(comp)) { |
615 | 2x |
comp <- default_columns[["comp"]] |
616 | 2x |
if (!is.null(comp)) { |
617 | 2x |
columns_from_attr <- c(columns_from_attr, "comp") |
618 |
} |
|
619 |
} |
|
620 | 25x |
if (missing(size)) { |
621 | 2x |
size <- default_columns[["size"]] |
622 | 2x |
if (!is.null(size)) { |
623 | 2x |
columns_from_attr <- c(columns_from_attr, "size") |
624 |
} |
|
625 |
} |
|
626 | 25x |
if (missing(prop)) { |
627 | 2x |
prop <- default_columns[["prop"]] |
628 | 2x |
if (!is.null(prop)) { |
629 | 2x |
columns_from_attr <- c(columns_from_attr, "prop") |
630 |
} |
|
631 |
} |
|
632 | 25x |
if (missing(group_by)) { |
633 | 18x |
group_by <- default_columns[["group_by"]] |
634 | 18x |
if (!is.null(group_by)) { |
635 | 5x |
columns_from_attr <- c(columns_from_attr, "group_by") |
636 |
} |
|
637 |
} |
|
638 | 25x |
if (length(columns_from_attr) > 0) { |
639 | 5x |
msg <- "Using the same columns by default as the ones used with `set_init()`:\n" |
640 | 5x |
for (i in columns_from_attr) { |
641 | 11x |
msg <- c(msg, paste0(" ", i, " = \"", default_columns[[i]], "\"\n")) |
642 |
} |
|
643 | 5x |
msg <- paste0(msg, collapse = "") |
644 | 5x |
message(msg) |
645 |
} |
|
646 |
# Trim extra compartments |
|
647 | 25x |
topo_comps <- unique(unlist(comps(nm))) |
648 | 25x |
obs_comps <- unique(data[[comp]]) |
649 | 25x |
extra_comps <- obs_comps[!obs_comps %in% topo_comps] |
650 | 25x |
if (length(extra_comps) > 0) { |
651 | ! |
message(paste0(strwrap("Some compartments are not present in the network topology and are removed from the observed data:"), |
652 | ! |
collapse = "\n"), "\n", |
653 | ! |
" - ", paste0(extra_comps, collapse = "\n - "), "\n") |
654 | ! |
data <- data[data[[comp]] %in% topo_comps, ] |
655 |
} |
|
656 |
# Build observations |
|
657 | 25x |
grouped_obs <- make_obs(data = data, comp = comp, size = size, prop = prop, |
658 | 25x |
time = time, group_by = group_by) |
659 | 25x |
out <- merge_nm_by_group(nm = nm, tib = grouped_obs, |
660 | 25x |
destination = "observations", |
661 | 25x |
tib_name = "observations") |
662 | 25x |
default_columns[["time"]] <- time |
663 | 25x |
attr(out, "default_columns") <- default_columns |
664 | 25x |
attr(out, "prop_family") <- attr(nm, "prop_family") |
665 | 25x |
return(out) |
666 |
} |
|
667 | ||
668 |
### * set_params() |
|
669 | ||
670 |
#' Set the parameters in a network model |
|
671 |
#' |
|
672 |
#' @param nm A \code{networkModel} object. |
|
673 |
#' @param params A named vector or a tibble with columns c("parameter", |
|
674 |
#' "value") containing the (global) parameter values. |
|
675 |
#' @param force Boolean, if FALSE will not overwrite already set parameters. |
|
676 |
#' @param quick Boolean, if TRUE take some shortcuts for faster parameter |
|
677 |
#' settings when called by another function. This should usually be left to |
|
678 |
#' the default (FALSE) by a regular package user. |
|
679 |
#' |
|
680 |
#' @return A \code{networkModel} object. |
|
681 |
#' |
|
682 |
#' @examples |
|
683 |
#' m <- aquarium_mod |
|
684 |
#' p <- sample_params(m) |
|
685 |
#' m2 <- set_params(m, p) |
|
686 |
#' m2$parameters |
|
687 |
#' |
|
688 |
#' @export |
|
689 | ||
690 |
set_params <- function(nm, params, force = TRUE, quick = FALSE) { |
|
691 | 579x |
if (is.vector(params)) { |
692 | 579x |
params <- data.frame(value = params, parameter = names(params), |
693 | 579x |
stringsAsFactors = FALSE) |
694 |
} |
|
695 | 579x |
prev_params <- attr(nm, "parameterValues") |
696 | 579x |
if (!is.null(prev_params) & !force) { |
697 | ! |
kept_indices <- !(prev_params$parameter %in% params$parameter) |
698 | ! |
kept_params <- prev_params[kept_indices, ] |
699 | ! |
new_params <- dplyr::bind_rows(kept_params, params) |
700 |
} else { |
|
701 | 579x |
new_params <- params |
702 |
} |
|
703 | 579x |
if (!quick) { |
704 | 299x |
new_params <- new_params[order(new_params[["parameter"]]), ] |
705 | 299x |
attr(nm, "parameterValues") <- tibble::as_tibble(new_params) |
706 |
} else { |
|
707 | 280x |
attr(nm, "parameterValues") <- new_params |
708 |
} |
|
709 | 579x |
for (i in seq_len(nrow(nm))) { |
710 | 581x |
nm[["parameters"]][[i]]$value <- new_params[["value"]][match(nm[["parameters"]][[i]]$in_model, |
711 | 581x |
new_params[["parameter"]])] |
712 |
} |
|
713 | 579x |
return(nm) |
714 |
} |
|
715 | ||
716 |
### * set_prior() | set_priors() |
|
717 | ||
718 |
#' Set prior(s) for a network model |
|
719 |
#' |
|
720 |
#' @param x A \code{networkModel} object. |
|
721 |
#' @param prior A prior built with e.g. uniform_p() or hcauchy_p(). Call |
|
722 |
#' \code{available_priors()} to see a table of implemented |
|
723 |
#' priors. Alternatively, if \code{prior} is a tibble, the function will |
|
724 |
#' try to use it to set parameter priors. The format of such an argument is |
|
725 |
#' the same as the format of the output of the getter function |
|
726 |
#' \code{priors()} (see examples). Note that if `prior` is given as a |
|
727 |
#' tibble, all other arguments (except `x`) are disregarded. |
|
728 |
#' @param param String, target parameter or regexp to target several |
|
729 |
#' parameters. Default is the empty string \code{""}, which will match all |
|
730 |
#' parameters. |
|
731 |
#' @param use_regexp Boolean, if \code{TRUE} (the default) then \code{param} is |
|
732 |
#' used as a regular expression to match one or several parameter names. |
|
733 |
#' @param quiet Boolean, if \code{FALSE} print a message indicating which |
|
734 |
#' parameters had their prior modified. |
|
735 |
#' |
|
736 |
#' @return A \code{networkModel} object. |
|
737 |
#' |
|
738 |
#' @examples |
|
739 |
#' # Copy `aquarium_mod` |
|
740 |
#' m <- aquarium_mod |
|
741 |
#' priors(m) |
|
742 |
#' |
|
743 |
#' # Modify the priors of `m` |
|
744 |
#' m <- set_priors(m, exponential_p(0.5), "lambda") |
|
745 |
#' priors(m) |
|
746 |
#' |
|
747 |
#' # Re-apply priors from the original `aquarium_mod` |
|
748 |
#' prev_priors <- priors(aquarium_mod) |
|
749 |
#' prev_priors |
|
750 |
#' m <- set_priors(m, prev_priors) |
|
751 |
#' priors(m) |
|
752 |
#' |
|
753 |
#' @export |
|
754 | ||
755 |
set_prior <- function(x, prior, param = "", use_regexp = TRUE, quiet = FALSE) { |
|
756 | 33x |
verbose <- !quiet |
757 | 33x |
params <- params(x, simplify = TRUE) |
758 | 33x |
priors <- priors(x) |
759 | 33x |
stopifnot(setequal(params, priors[["in_model"]])) |
760 | 33x |
params <- priors[["in_model"]] |
761 |
# If `prior` is a tibble |
|
762 | 33x |
if (is(prior, "tbl_df")) { |
763 | ! |
if (!valid_prior_tbl(prior)) { |
764 | ! |
stop("`prior` is not a valid prior tibble.") |
765 |
} |
|
766 | ! |
prior <- prior[, c("in_model", "prior")] |
767 | ! |
extra_params <- which(!prior[["in_model"]] %in% params) |
768 | ! |
if (length(extra_params) > 0) { |
769 | ! |
message("Some parameter names in `prior` are not used in the network model and will be ignored.\n", |
770 | ! |
paste0(paste0("\"", prior[["in_model"]][extra_params], "\""), collapse = ", ")) |
771 |
} |
|
772 | ! |
prior <- prior[which(prior[["in_model"]] %in% params), ] |
773 | ! |
param_indices <- match(prior[["in_model"]], params) |
774 | ! |
for (i in seq_along(param_indices)) { |
775 | ! |
priors[["prior"]][[param_indices[i]]] <- prior[["prior"]][[i]] |
776 |
} |
|
777 | ! |
if (verbose) { |
778 | ! |
message("Prior modified for parameter(s): \n - ", |
779 | ! |
paste0(prior[["in_model"]], collapse = "\n - ")) |
780 |
} |
|
781 | ! |
attr(x, "priors") <- priors |
782 | ! |
return(x) |
783 |
} |
|
784 |
# If `prior` is not a tibble |
|
785 | 33x |
if (!is(prior, "prior")) { |
786 | ! |
stop("`prior` must be a valid prior. See `available_priors()`.") |
787 |
} |
|
788 | 33x |
if (use_regexp) { |
789 | 32x |
param_indices <- which(grepl(pattern = param, x = params)) |
790 |
} else { |
|
791 | 1x |
param_indices <- which(params == param) |
792 |
} |
|
793 | 33x |
if (length(param_indices) == 0) { |
794 | ! |
stop("No matching parameter found for: ", param, ".\n", |
795 | ! |
"Available parameters are: \n- ", |
796 | ! |
paste0(params, collapse = "\n- ")) |
797 |
} |
|
798 | 33x |
for (i in param_indices) { |
799 | 225x |
priors[["prior"]][[i]] <- prior |
800 |
} |
|
801 | 33x |
if (verbose) { |
802 | 17x |
message("Prior modified for parameter(s): \n - ", |
803 | 17x |
paste0(params[param_indices], collapse = "\n - ")) |
804 |
} |
|
805 | 33x |
attr(x, "priors") <- priors |
806 | 33x |
return(x) |
807 |
} |
|
808 | ||
809 |
#' @rdname set_prior |
|
810 |
#' @export |
|
811 | ||
812 |
set_priors <- set_prior |
|
813 |
|
|
814 |
### * add_covariates() |
|
815 | ||
816 |
#' Add fixed effects of one or several covariates to some parameters. |
|
817 |
#' |
|
818 |
#' Note that new global parameters are not given any default prior. |
|
819 |
#' |
|
820 |
#' @param nm A \code{networkModel} object. |
|
821 |
#' @param ... One or several formulas defining the covariates. |
|
822 |
#' @param use_regexpr Boolean, use regular expression to match the parameters |
|
823 |
#' affected by the formulas? |
|
824 |
#' |
|
825 |
#' @return A \code{networkModel} object. |
|
826 |
#' |
|
827 |
#' @examples |
|
828 |
#' # Using a subset of the topology from the Trinidad case study |
|
829 |
#' m <- new_networkModel() %>% |
|
830 |
#' set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph") |
|
831 |
#' |
|
832 |
#' # Taking initial condtions from the 'lalaja' dataset at t=0 |
|
833 |
#' # Grouping by transect id |
|
834 |
#' inits <- lalaja[lalaja[["time.days"]] == 0, ] |
|
835 |
#' inits |
|
836 |
#' m <- set_init(m, inits, comp = "compartment", size = "mgN.per.m2", |
|
837 |
#' prop = "prop15N", group_by = "transect") |
|
838 |
#' m |
|
839 |
#' |
|
840 |
#' # Default model |
|
841 |
#' params(m, simplify = TRUE) |
|
842 |
#' |
|
843 |
#' # Adding an effect of the "transect" covariate on some parameters |
|
844 |
#' m <- add_covariates(m, upsilon_epi_to_pseph ~ transect) |
|
845 |
#' params(m, simplify = TRUE) |
|
846 |
#' |
|
847 |
#' @export |
|
848 | ||
849 |
add_covariates <- function(nm, ..., use_regexpr = TRUE) { |
|
850 | 5x |
formula <- list(...) |
851 |
# Apply formula |
|
852 | 5x |
for (eachFormula in formula) { |
853 | 5x |
nm <- refresh_param_mapping(nm, eachFormula, use_regexpr = use_regexpr) |
854 |
} |
|
855 |
# Refresh priors |
|
856 | 5x |
current_priors <- priors(nm) |
857 | 5x |
global_params <- params(nm, simplify = TRUE) |
858 | 5x |
default_priors <- tibble::tibble(in_model = global_params) |
859 | 5x |
default_priors$prior <- rep(list(NULL), nrow(default_priors)) |
860 | 5x |
kept_priors <- current_priors[current_priors$in_model %in% global_params, ] |
861 | 5x |
new_priors <- default_priors[!default_priors$in_model %in% |
862 | 5x |
current_priors$in_model, ] |
863 | 5x |
priors <- dplyr::bind_rows(kept_priors, new_priors) |
864 | 5x |
priors <- priors[order(priors$in_model), ] |
865 |
# Return |
|
866 | 5x |
attr(nm, "priors") <- priors |
867 | 5x |
return(nm) |
868 |
} |
|
869 | ||
870 |
### * print.networkModel() |
|
871 | ||
872 |
#' Print method for \code{networkModel} objects |
|
873 |
#' |
|
874 |
#' @param x A \code{networkModel} object. |
|
875 |
#' @param ... Passsed to the next method. |
|
876 |
#' |
|
877 |
#' @return Called for the side effect of printing a network model object. |
|
878 |
#' |
|
879 |
#' @method print networkModel |
|
880 |
#' |
|
881 |
#' @export |
|
882 |
#' |
|
883 | ||
884 |
print.networkModel <- function(x, ...) { |
|
885 | ! |
NextMethod() |
886 | ! |
lambda_hl <- attr(x, "lambda_hl") |
887 | ! |
if (!is.null(lambda_hl)) { |
888 | ! |
hl <- signif(log(2) / lambda_hl, 5) |
889 | ! |
cat("# Half-life of marked tracer:", hl, "time units.\n") |
890 |
} |
|
891 |
} |
1 |
### * TODO |
|
2 | ||
3 |
# Clean-up this file |
|
4 | ||
5 |
### * None of the functions in this file is exported |
|
6 | ||
7 |
### * plot_nm() |
|
8 | ||
9 |
#' Plot a network model object |
|
10 |
#' |
|
11 |
#' Reminder: A network model object is basically a tibble. Each row is one |
|
12 |
#' experimental replicate (also called "group"). The tibble might have the |
|
13 |
#' columns "observations", "prediction" and "trajectory". Those columns contain |
|
14 |
#' the data that can be plotted. |
|
15 |
#' |
|
16 |
#' @param x A network model object. Alternatively, it can also be the output |
|
17 |
#' from \code{split_to_unit_plot()} that the user has filtered themselves. |
|
18 |
#' @param facet_row,facet_column Optional, either can be "group" or |
|
19 |
#' "compartment". Define how facetting is performed. |
|
20 |
#' @param scale Define how y-scaling is performed within each panel (or |
|
21 |
#' cell). Can be one of "auto" (scaling on a per-panel basis), "all" |
|
22 |
#' (scaling shared by all panels) and "row" (scaling per row). Note that |
|
23 |
#' the x-scaling is always done for "all" (i.e. shared across all panels). |
|
24 |
#' @param type Either "prop", "size" or "both" |
|
25 |
#' @param newpage If FALSE, add the plot to the current page. |
|
26 |
#' @param xlab,ylab Labels for x and y axes. |
|
27 |
#' @param margins Figure margins. |
|
28 |
#' @param grid Boolean, draw a grid? |
|
29 |
#' @param ylab.size Y-axis label for size data. |
|
30 |
#' @param ylab.prop Y-axis label for proportion data. |
|
31 |
#' @param legend Boolean, draw legend? |
|
32 |
#' @param log Boolean, apply log transform to y axis? |
|
33 |
#' @param .colComps Colors for compartments. |
|
34 |
#' @param comps Optional, vector of compartment names giving the order in which |
|
35 |
#' they should be shown in the plot. |
|
36 |
#' @param keep Optional, vector of names of compartments to keep. |
|
37 |
#' @param drop Optional, vector of names of compartments to drop. |
|
38 |
#' |
|
39 |
#' @importFrom grDevices dev.hold |
|
40 |
#' @importFrom grDevices dev.flush |
|
41 |
#' |
|
42 |
#' @return NULL |
|
43 |
#' |
|
44 |
#' @keywords internal |
|
45 |
#' @noRd |
|
46 | ||
47 |
plot_nm <- function(x, facet_row = NULL, facet_column = NULL, scale = "auto", |
|
48 |
type = "both", newpage = TRUE, xlab = NULL, ylab = NULL, |
|
49 |
margins = c(0.5, 0.5, 1, 1), grid = TRUE, |
|
50 |
ylab.size = NULL, ylab.prop = NULL, legend = TRUE, |
|
51 |
log = FALSE, .colComps = NULL, comps, keep, drop) { |
|
52 | 90x |
if (!"group" %in% colnames(x)) { |
53 | 17x |
x[["group"]] <- rep(list(NULL), nrow(x)) |
54 |
} |
|
55 |
# Default geometries for each type of data to plot |
|
56 | 90x |
geometries <- c("observations" = "points", |
57 | 90x |
"initial" = "points", |
58 | 90x |
"prediction" = "envelope", |
59 | 90x |
"trajectory" = "line") |
60 | 90x |
colors <- c("#66C2A5", "#FC8D62", "#8DA0CB", "#E78AC3", "#A6D854", "#FFD92F", |
61 | 90x |
"#E5C494", "#B3B3B3") |
62 |
# Colors are taken from RColorBrewer::brewer.pal(8, "Set2") |
|
63 | 90x |
facet_variables <- c("group", "compartment", "type") |
64 | 90x |
nature <- type |
65 | 30x |
if (nature == "both") nature <- c("size", "prop") |
66 | 88x |
if (length(facet_row) == 0) facet_row <- NULL |
67 | 84x |
if (length(facet_column) == 0) facet_column <- NULL |
68 | 90x |
stopifnot(is.null(facet_row) | all(facet_row %in% facet_variables)) |
69 | 90x |
stopifnot(is.null(facet_column) | all(facet_column %in% facet_variables)) |
70 | 90x |
stopifnot(all(nature %in% c("prop", "size", "both"))) |
71 |
# Prepare the data |
|
72 | 90x |
if (!"ready_for_unit_plot" %in% class(x)) { |
73 | 90x |
if (log) { |
74 | 3x |
transform <- log10 |
75 |
} else { |
|
76 | 87x |
transform <- NULL |
77 |
} |
|
78 | 90x |
z <- split_to_unit_plot(x, transform) |
79 |
} else { |
|
80 | ! |
z <- x |
81 |
} |
|
82 |
# Determine what to draw |
|
83 | 90x |
if (!missing(comps)) { |
84 | ! |
z <- z[z[["compartment"]] %in% comps, ] |
85 |
} |
|
86 | 90x |
if (!missing(keep)) { |
87 | ! |
z <- z[z[["compartment"]] %in% keep, ] |
88 |
} |
|
89 | 90x |
if (!missing(drop)) { |
90 | ! |
z <- z[!z[["compartment"]] %in% drop, ] |
91 |
} |
|
92 | 90x |
if ("observations" %in% colnames(x) && !is.null(x$observations[[1]])) { |
93 | 66x |
draw_obs <- TRUE |
94 |
} else { |
|
95 | 24x |
draw_obs <- FALSE |
96 |
} |
|
97 | 90x |
if ("prediction" %in% colnames(x) && !is.null(x$prediction[[1]])) { |
98 | 39x |
draw_pred <- TRUE |
99 |
} else { |
|
100 | 51x |
draw_pred <- FALSE |
101 |
} |
|
102 |
# Process the data |
|
103 | 90x |
if (!draw_pred) { |
104 | 51x |
z <- dplyr::filter(z, type != "prediction") |
105 |
} |
|
106 | 90x |
if (!draw_obs) { |
107 | 24x |
z <- dplyr::filter(z, type != "observations") |
108 |
} |
|
109 | 90x |
if (nrow(z) == 0) { |
110 | ! |
stop("Nothing to plot") |
111 |
} |
|
112 | 90x |
z <- z[z$nature %in% nature, ] |
113 | 90x |
z$geometry <- geometries[z$type] |
114 | 90x |
z$groupLabel <- purrr::map_chr(z$group, function(x) { |
115 | 1044x |
paste0(x, collapse = ", ") |
116 |
}) |
|
117 | 90x |
groupLabels <- sort(unique(z$groupLabel)) |
118 | 90x |
compLabels <- sort(unique(z$compartment)) |
119 | 90x |
if (!missing(comps)) { |
120 | ! |
compLabels <- compLabels[order(match(compLabels, comps))] |
121 |
} |
|
122 | 90x |
colComps <- rep(colors, length(compLabels)) |
123 | 90x |
colComps <- colComps[1:length(compLabels)] |
124 | 90x |
colComps <- setNames(colComps, nm = compLabels) |
125 | 90x |
if (!is.null(.colComps)) { |
126 | 60x |
.colComps <- sort(.colComps) |
127 | 60x |
.colComps <- .colComps[names(sort(colComps))] |
128 | 60x |
stopifnot(all(.colComps == sort(colComps))) |
129 |
} |
|
130 | 30x |
if (newpage) grid::grid.newpage() |
131 | 90x |
dev.hold() |
132 | 90x |
on.exit(dev.flush()) |
133 | 90x |
if (legend) { |
134 | 30x |
labels <- c("compartment", names(x)) |
135 | 30x |
longest <- labels[which.max(nchar(labels))] |
136 | 30x |
longest <- paste0(longest, "aaaaaaaa", collapse = " ") |
137 | 30x |
lo <- grid::grid.layout(ncol = 2, |
138 | 30x |
widths = grid::unit(c(1, 1), c("null", "strwidth"), list(NULL, longest))) |
139 | 30x |
vp_top_with_legend <- grid::viewport(layout = lo) |
140 | 30x |
grid::pushViewport(vp_top_with_legend) |
141 | 30x |
vp_legend <- grid::viewport(layout.pos.col = 2) |
142 | 30x |
grid::pushViewport(vp_legend) |
143 | 30x |
draw_legend(colComps) |
144 | 30x |
grid::upViewport(1) |
145 | 30x |
vp_plot <- grid::viewport(layout.pos.col = 1) |
146 | 30x |
grid::pushViewport(vp_plot) |
147 |
} |
|
148 |
# Plot both size and proportion |
|
149 | 90x |
if (type == "both") { |
150 |
# Plot size and prop |
|
151 |
# Facetting for type |
|
152 | 30x |
facet_type <- "column" |
153 | 30x |
if ("type" %in% facet_row) { |
154 | 2x |
facet_row <- facet_row[facet_row != "type"] |
155 | 2x |
facet_type <- "row" |
156 |
} |
|
157 | 30x |
if ("type" %in% facet_column) { |
158 | ! |
facet_column <- facet_column[facet_column != "type"] |
159 | ! |
facet_type <- "column" |
160 |
} |
|
161 |
# Top vp |
|
162 | 30x |
if (facet_type == "column") { |
163 | 28x |
top_vp <- grid::viewport(layout = grid::grid.layout(ncol = 2)) |
164 |
} else { |
|
165 | 2x |
top_vp <- grid::viewport(layout = grid::grid.layout(nrow = 2)) |
166 |
} |
|
167 | 30x |
grid::pushViewport(top_vp) |
168 | 30x |
if (facet_type == "column") { |
169 | 28x |
size_vp <- grid::viewport(layout.pos.col = 1) |
170 |
} else { |
|
171 | 2x |
size_vp <- grid::viewport(layout.pos.row = 1) |
172 |
} |
|
173 | 30x |
grid::pushViewport(size_vp) |
174 | 30x |
plot_nm(x = x, facet_row = facet_row, facet_column = facet_column, |
175 | 30x |
scale = scale, type = "size", newpage = FALSE, |
176 | 30x |
xlab = xlab, ylab = ylab.size, margins = margins, grid = grid, |
177 | 30x |
.colComps = colComps, legend = FALSE, log = log, |
178 | 30x |
comps = comps, keep = keep, drop = drop) |
179 | 30x |
grid::upViewport(1) |
180 | 30x |
if (facet_type == "column") { |
181 | 28x |
prop_vp <- grid::viewport(layout.pos.col = 2) |
182 |
} else { |
|
183 | 2x |
prop_vp <- grid::viewport(layout.pos.row = 2) |
184 |
} |
|
185 | 30x |
grid::pushViewport(prop_vp) |
186 | 30x |
plot_nm(x = x, facet_row = facet_row, facet_column = facet_column, |
187 | 30x |
scale = scale, type = "prop", newpage = FALSE, |
188 | 30x |
xlab = xlab, ylab = ylab.prop, margins = margins, grid = grid, |
189 | 30x |
.colComps = colComps, legend = FALSE, log = log, |
190 | 30x |
comps = comps, keep = keep, drop = drop) |
191 | 30x |
grid::upViewport(1) |
192 | 30x |
grid::upViewport(1) |
193 | 30x |
if (legend) { |
194 | 30x |
grid::upViewport(2) |
195 |
} |
|
196 | 30x |
return(invisible(NULL)) |
197 |
} |
|
198 | 60x |
stopifnot(scale %in% c("auto", "all", "row")) |
199 | 60x |
if (is.null(xlab)) xlab <- "Time" |
200 | 60x |
if (is.null(ylab)) { |
201 | 60x |
ylab <- ifelse(nature == "size", "Size", "Proportion") |
202 |
} |
|
203 |
# Facetting |
|
204 | 60x |
if (is.null(facet_row)) { |
205 | 60x |
z$row_id <- 1 |
206 |
} else { |
|
207 | ! |
if (facet_row == "group") { z$row_id <- match(z$groupLabel, groupLabels) } |
208 | ! |
if (facet_row == "compartment") { z$row_id <- match(z$compartment, compLabels) } |
209 |
} |
|
210 | 60x |
if (is.null(facet_column)) { |
211 | 56x |
z$col_id <- 1 |
212 |
} else { |
|
213 | ! |
if (facet_column == "group") { z$col_id <- match(z$groupLabel, groupLabels) } |
214 | 4x |
if (facet_column == "compartment") { z$col_id <- match(z$compartment, compLabels) } |
215 |
} |
|
216 |
# Functions to get the scales |
|
217 | 60x |
xscaling <- function(z) { |
218 | 60x |
x <- unlist(lapply(z$data, function(x) x[["time"]])) |
219 | 60x |
return(extendrange(range(x, na.rm = TRUE))) |
220 |
} |
|
221 | 60x |
yscaling <- function(z) { |
222 | 188x |
x <- unlist(lapply(z$data, function(x) { |
223 | 1566x |
d <- colnames(x)[startsWith(colnames(x), "quantity")] |
224 | 1566x |
unlist(x[, d]) |
225 |
})) |
|
226 | 188x |
range_x <- range(x, na.rm = TRUE) |
227 | 188x |
if (range_x[1] == range_x[2]) { |
228 | ! |
delta <- 0.01 * range_x[1] |
229 | ! |
range_x <- range_x + c(-delta, delta) |
230 |
} |
|
231 | 188x |
return(extendrange(range_x)) |
232 |
} |
|
233 | 60x |
xscale <- xscaling(z) |
234 | 60x |
yscale_all <- yscaling(z) |
235 |
# Plot layout |
|
236 | 60x |
grid::pushViewport(grid::viewport(x = grid::unit(margins[2], "lines"), |
237 | 60x |
y = grid::unit(margins[1], "lines"), |
238 | 60x |
width = grid::unit(1, "npc") - grid::unit(sum(margins[c(2, 4)]), "lines"), |
239 | 60x |
height = grid::unit(1, "npc") - grid::unit(sum(margins[c(1, 3)]), "lines"), |
240 | 60x |
just =c("left", "bottom"))) |
241 |
# Top container |
|
242 | 60x |
vp_top <- grid::viewport(layout = grid::grid.layout(nrow = 2, ncol = 2, |
243 | 60x |
width = grid::unit(c(1.3, 1), c("lines", "null")), |
244 | 60x |
height = grid::unit(c(1, 1.3), c("null", "lines")))) |
245 | 60x |
grid::pushViewport(vp_top) |
246 | 60x |
vp_xlab <- grid::viewport(layout.pos.row = 2, layout.pos.col = 2) |
247 | 60x |
grid::pushViewport(vp_xlab) |
248 | 60x |
grid::grid.text(xlab, gp = grid::gpar(cex = 1)) |
249 | 60x |
grid::upViewport(1) |
250 | 60x |
vp_ylab <- grid::viewport(layout.pos.row = 1, layout.pos.col = 1) |
251 | 60x |
grid::pushViewport(vp_ylab) |
252 | 60x |
grid::grid.text(ylab, gp = grid::gpar(cex = 1), rot = 90) |
253 | 60x |
grid::upViewport(1) |
254 |
## Widths |
|
255 | 60x |
if (scale %in% c("all", "row")) { |
256 |
# One y-axis per row |
|
257 | 2x |
widths <- rep((grid::unit(1, "npc") - grid::unit(3, "lines"))*(1/max(z$col_id)), max(z$col_id)) |
258 | 2x |
widths[1] <- widths[1] + grid::unit(3, "lines") |
259 |
} else { |
|
260 |
# One y-axis per panel |
|
261 | 58x |
widths <- rep(grid::unit(1, "npc")*(1/max(z$col_id)), max(z$col_id)) |
262 |
} |
|
263 |
## Heights |
|
264 | 60x |
heights <- rep((grid::unit(1, "npc") - grid::unit(3, "lines"))*(1/max(z$row_id)), max(z$row_id)) |
265 | 60x |
heights[length(heights)] <- heights[length(heights)] + grid::unit(3, "lines") |
266 | 60x |
layout <- grid::grid.layout(nrow = max(z$row_id), ncol = max(z$col_id), |
267 | 60x |
width = widths, height = heights) |
268 | 60x |
vp_master <- grid::viewport(layout.pos.row = 1, layout.pos.col = 2, |
269 | 60x |
layout = layout) |
270 | 60x |
grid::pushViewport(vp_master) |
271 |
# Draw each panel |
|
272 | 60x |
for (i in 1:max(z$row_id)) { |
273 | 60x |
z_row <- z[z$row_id == i, ] |
274 | 60x |
yscale_row <- yscaling(z_row) |
275 | 60x |
for (j in 1:max(z$col_id)) { |
276 | 68x |
z_ij <- z[z$row_id == i & z$col_id == j, ] |
277 | 68x |
margins <- rep(0, 4) |
278 | 68x |
if (i == max(z$row_id)) margins[1] <- 3 |
279 | 64x |
if (j == 1 | scale == "auto") margins[2] <- 3 |
280 | 68x |
layout <- grid::grid.layout(nrow = 3, ncol = 2, |
281 | 68x |
widths = grid::unit(c(margins[2], 1), |
282 | 68x |
units = c("lines", "null")), |
283 | 68x |
heights = grid::unit(c(1.5, 1, margins[1]), |
284 | 68x |
units = c("lines", "null", "lines"))) |
285 | 68x |
vp_panel <- grid::viewport(layout.pos.row = i, |
286 | 68x |
layout.pos.col = j, |
287 | 68x |
layout = layout) |
288 | 68x |
grid::pushViewport(vp_panel) |
289 |
# Title |
|
290 | 68x |
vp_title <- grid::viewport(layout.pos.row = 1, |
291 | 68x |
layout.pos.col = 2) |
292 | 68x |
grid::pushViewport(vp_title) |
293 | 68x |
grid::grid.rect(gp = grid::gpar(fill = grey(0.95))) |
294 | 68x |
label <- sapply(seq_len(nrow(z_ij)), function(i) { |
295 | 522x |
if (z_ij$groupLabel[i] != "") { |
296 | 72x |
paste(z_ij$groupLabel[i]) |
297 |
} else { |
|
298 | 450x |
z_ij$compartment[i] |
299 |
} |
|
300 |
}) |
|
301 | 68x |
label <- paste(unique(label), collapse = " + ") |
302 | 68x |
grid::grid.text(label) |
303 | 68x |
grid::upViewport(1) |
304 |
# Plot |
|
305 | 68x |
vp_plot <- grid::viewport(layout.pos.row = 2, |
306 | 68x |
layout.pos.col = 2) |
307 | 68x |
grid::pushViewport(vp_plot) |
308 | 68x |
yscale <- yscaling(z_ij) |
309 | 6x |
if (scale == "all") yscale <- yscale_all |
310 | ! |
if (scale == "row") yscale <- yscale_row |
311 | 68x |
draw_nm_panel(z_ij, colors = colComps[z_ij$compartment], |
312 | 68x |
xscale = xscale, yscale = yscale, |
313 | 68x |
x.axis = i == max(z$row_id), |
314 | 68x |
y.axis = j == 1 | scale == "auto", |
315 | 68x |
grid = grid, log = log) |
316 | 68x |
grid::grid.rect(gp = grid::gpar(fill = NA)) |
317 | 68x |
grid::upViewport(1) |
318 |
# Out vp_panel |
|
319 | 68x |
grid::upViewport(1) |
320 |
} |
|
321 |
} |
|
322 |
# Up viewports |
|
323 | 60x |
grid::upViewport(1) |
324 | 60x |
grid::upViewport(1) |
325 | 60x |
grid::upViewport(1) |
326 | 60x |
if (legend) { |
327 | ! |
grid::upViewport(2) |
328 |
} |
|
329 |
# Return |
|
330 | 60x |
return(invisible(NULL)) |
331 |
} |
|
332 | ||
333 |
### * split_to_unit_plot() |
|
334 | ||
335 |
#' Separate network model object into plot chunks |
|
336 |
#' |
|
337 |
#' Function to split enriched network model object (i.e. network model that can |
|
338 |
#' have a trajectory or a prediction columns) into unit plot data. |
|
339 |
#' |
|
340 |
#' @param x A network model object, with obs/traj/prediction data. |
|
341 |
#' @param transform Optional, function to apply to all "quantities" columns |
|
342 |
#' (e.g. log). |
|
343 |
#' |
|
344 |
#' @return A tibble which can be e.g. filtered and used for plotting. |
|
345 |
#' |
|
346 |
#' @keywords internal |
|
347 |
#' @noRd |
|
348 | ||
349 |
split_to_unit_plot <- function(x, transform = NULL) { |
|
350 | 90x |
`!!` <- rlang::`!!` |
351 |
# Convert trajectory column to a light, tidy version |
|
352 | 90x |
if ("trajectory" %in% colnames(x)) { |
353 | 84x |
x$trajectory <- lapply(x$trajectory, function(z) { |
354 | 84x |
stopifnot(nrow(z) == 1) |
355 | 84x |
sizes <- tibble::as_tibble(as.data.frame(z$sizes)) |
356 | 84x |
sizes$time <- z$timepoints[[1]] |
357 | 84x |
sizes <- tidyr::pivot_longer(sizes, cols = -"time", |
358 | 84x |
names_to = "compartment", values_to = "size") |
359 | 84x |
props <- tibble::as_tibble(as.data.frame(z$proportions)) |
360 | 84x |
props$time <- z$timepoints[[1]] |
361 | 84x |
props <- tidyr::pivot_longer(props, cols = -"time", |
362 | 84x |
names_to = "compartment", values_to = "prop") |
363 | 84x |
out <- dplyr::left_join(sizes, props, by = c("time", "compartment")) |
364 | 84x |
return(out) |
365 |
}) |
|
366 |
} |
|
367 |
# For now implemented for a network model with prediction column |
|
368 | 90x |
if (!"prediction" %in% colnames(x)) { |
369 | 51x |
x$prediction <- rep(list(NULL), nrow(x)) |
370 |
} |
|
371 | 90x |
target_cols <- c("observations", "prediction", "trajectory") |
372 | 90x |
target_cols <- target_cols[target_cols %in% colnames(x)] |
373 | 90x |
if (length(target_cols) < 1) { |
374 | ! |
stop("x should have at least one of \"observations\", \"prediction\", or \"trajectory\" columns.") |
375 |
} |
|
376 | 90x |
y <- x[, c("initial", "group", target_cols)] |
377 | 90x |
y <- tidyr::pivot_longer(y, cols = tidyselect::all_of(target_cols), |
378 | 90x |
names_to = "type", |
379 | 90x |
values_to = "data") |
380 |
# Drop rows with NULL data |
|
381 | 90x |
y <- y[!sapply(y[["data"]], is.null), ] |
382 |
# Split by compartment |
|
383 | 90x |
z <- y |
384 | 90x |
z[["data"]] <- lapply(z[["data"]], function(x) { |
385 | 201x |
tidyr::nest(dplyr::group_by(x, `!!`(rlang::sym("compartment")))) |
386 |
}) |
|
387 | 90x |
z$row_id <- 1:nrow(z) |
388 | 90x |
for (i in seq_len(nrow(z))) { |
389 | 201x |
z$data[[i]]$row_id <- i |
390 | 201x |
z$data[[i]]$data <- as.list(z$data[[i]]$data) |
391 |
} |
|
392 | 90x |
y <- dplyr::select(z, - "data") |
393 |
# Note: in the call, I add `multiple = "all"` because of a new warning |
|
394 |
# in dplyr 1.1.0 (and because in this case rows in `y` can match multiple |
|
395 |
# rows in `dplyr::bind_rows(z$data)`). |
|
396 | 90x |
y <- dplyr::full_join(y, dplyr::bind_rows(z$data), by = "row_id", |
397 | 90x |
multiple = "all") |
398 | 90x |
y <- dplyr::select(y, - "row_id") |
399 |
# Separate size and proportion data |
|
400 | 90x |
y$size <- as.list(rep(NA, nrow(y))) |
401 | 90x |
y$prop <- as.list(rep(NA, nrow(y))) |
402 | 90x |
for (i in seq_len(nrow(y))) { |
403 | 531x |
d <- y$data[[i]] |
404 | 531x |
ds <- dplyr::select(d, tidyselect::starts_with("size"), "time") |
405 | 531x |
n <- names(ds) |
406 | 531x |
n <- gsub("size", "quantity", n) |
407 | 531x |
names(ds) <- n |
408 | 531x |
dp <- dplyr::select(d, tidyselect::starts_with("prop"), "time") |
409 | 531x |
n <- names(dp) |
410 | 531x |
n <- gsub("proportion", "quantity", n) |
411 | 531x |
n <- gsub("prop", "quantity", n) |
412 | 531x |
names(dp) <- n |
413 | 531x |
y$size[[i]] <- na.omit(ds) |
414 | 531x |
y$prop[[i]] <- na.omit(dp) |
415 |
} |
|
416 | 90x |
y <- dplyr::select(y, - "data") |
417 | 90x |
z <- tidyr::pivot_longer(y, cols = c("size", "prop"), |
418 | 90x |
names_to = "nature", |
419 | 90x |
values_to = "data") |
420 | 90x |
z <- dplyr::select(z, "group", "compartment", "nature", "type", "data", "initial") |
421 |
# Separate initial values |
|
422 | 90x |
init <- dplyr::select(z, "group", "compartment", "initial") |
423 | 90x |
init$size <- rep(list(NA), nrow(init)) |
424 | 90x |
init$prop <- rep(list(NA), nrow(init)) |
425 | 90x |
for (i in seq_len(nrow(init))) { |
426 | 1062x |
comp_i <- init$compartment[i] |
427 | 1062x |
init$initial[[i]] <- init$initial[[i]][init$initial[[i]][["compartment"]] == comp_i, ] |
428 | 1062x |
stopifnot(nrow(init$initial[[i]]) == 1) |
429 | 1062x |
init$size[[i]] <- tibble::tibble(quantity = init$initial[[i]]$size[1], |
430 | 1062x |
time = 0) |
431 | 1062x |
init$prop[[i]] <- tibble::tibble(quantity = init$initial[[i]]$proportion[1], |
432 | 1062x |
time = 0) |
433 |
} |
|
434 | 90x |
init$initial <- NULL |
435 | 90x |
init$type <- "initial" |
436 | 90x |
init <- tidyr::pivot_longer(init, cols = c("size", "prop"), |
437 | 90x |
names_to = "nature", |
438 | 90x |
values_to = "data") |
439 | 90x |
init <- unique(init) |
440 |
# Merge |
|
441 | 90x |
z$initial <- NULL |
442 | 90x |
z <- dplyr::bind_rows(z, init) |
443 |
# Apply transform |
|
444 | 90x |
if (!is.null(transform)) { |
445 | 3x |
for (i in seq_len(nrow(z))) { |
446 | 72x |
d <- z$data[[i]] |
447 | 72x |
cols <- colnames(d)[grepl("quantity", colnames(d))] |
448 | 72x |
for (col in cols) { |
449 | 108x |
d[[col]] <- transform(d[[col]]) |
450 |
} |
|
451 | 72x |
z$data[[i]] <- d |
452 |
} |
|
453 |
} |
|
454 |
# Return |
|
455 | 90x |
return(structure(z, class = c("ready_for_unit_plot", class(z)))) |
456 |
} |
|
457 | ||
458 |
### * draw_nm_panel() |
|
459 | ||
460 |
#' Draw a panel of the data (obs/traj/pred) from a network model |
|
461 |
#' |
|
462 |
#' @param x Output from \code{split_to_unit_plot}. |
|
463 |
#' @param colors Passed to \code{draw_nm_layer}. |
|
464 |
#' @param xscale,yscale X and y scales. |
|
465 |
#' @param x.axis,y.axis Booleans, draw the axes? |
|
466 |
#' @param grid Boolean, draw a grid? |
|
467 |
#' @param log Boolean, log-transform the y axis? |
|
468 |
#' |
|
469 |
#' @keywords internal |
|
470 |
#' @noRd |
|
471 | ||
472 |
draw_nm_panel <- function(x, colors = "grey", xscale = NULL, yscale = NULL, |
|
473 |
x.axis = FALSE, y.axis = FALSE, grid = FALSE, |
|
474 |
log = FALSE) { |
|
475 |
# Get lims |
|
476 | 68x |
x_range <- range(unlist(lapply(x$data, function(i) i[["time"]])), |
477 | 68x |
na.rm = TRUE) |
478 | 68x |
y_range <- range(unlist(lapply(x$data, function(i) { |
479 | 522x |
unlist(i[, grepl("quantity", colnames(i))]) |
480 | 68x |
})), na.rm = TRUE) |
481 |
# Create viewport |
|
482 | ! |
if (is.null(xscale)) xscale <- x_range |
483 | ! |
if (is.null(yscale)) yscale <- y_range |
484 | 68x |
vp <- grid::dataViewport(xscale = xscale, |
485 | 68x |
yscale = yscale) |
486 | 68x |
grid::pushViewport(vp) |
487 |
# Draw grid |
|
488 | 68x |
xat <- pretty(xscale) |
489 | 68x |
xat <- xat[xat >= xscale[1] & xat <= xscale[2]] |
490 | 68x |
yat <- pretty(yscale) |
491 | 68x |
if (log) { |
492 | 6x |
yat <- log10(prettyLog(yscale)) |
493 |
} |
|
494 | 68x |
yat <- yat[yat >= yscale[1] & yat <= yscale[2]] |
495 | 68x |
ylabel <- yat |
496 | 68x |
if (log) { |
497 | 6x |
ylabel <- prettyLogLabels(yscale) |
498 | 6x |
ylabel <- ylabel[log10(prettyLog(yscale)) >= yscale[1] & log10(prettyLog(yscale)) <= yscale[2]] |
499 |
} |
|
500 | 68x |
if (grid) { |
501 | 68x |
grid.col <- grey(0.95) |
502 | 68x |
for (xi in xat) { |
503 | 408x |
grid::grid.lines(x = grid::unit(c(xi, xi), "native"), |
504 | 408x |
y = grid::unit(yscale, "native"), |
505 | 408x |
gp =grid::gpar(col = grid.col)) |
506 |
} |
|
507 | 68x |
for (yi in yat) { |
508 | 402x |
grid::grid.lines(x = grid::unit(xscale, "native"), |
509 | 402x |
y = grid::unit(c(yi, yi), "native"), |
510 | 402x |
gp =grid::gpar(col = grid.col)) |
511 |
} |
|
512 |
} |
|
513 |
# Draw layers |
|
514 | 68x |
colors <- rep(colors, nrow(x)) |
515 | 68x |
for (i in seq_len(nrow(x))) { |
516 | 522x |
draw_nm_layer(x[i, ], color = colors[i]) |
517 |
} |
|
518 | 68x |
if (x.axis) grid::grid.xaxis() |
519 | 64x |
if (y.axis) grid::grid.yaxis(at = yat, |
520 | 64x |
label = ylabel) |
521 |
# Up viewports |
|
522 | 68x |
grid::upViewport(1) |
523 |
} |
|
524 | ||
525 |
### * draw_nm_layer() |
|
526 | ||
527 |
#' @importFrom grDevices adjustcolor |
|
528 |
#' |
|
529 |
#' @keywords internal |
|
530 |
#' @noRd |
|
531 | ||
532 |
draw_nm_layer <- function(x, color = "grey") { |
|
533 | 522x |
stopifnot(nrow(x) == 1) |
534 | 522x |
geometry <- x$geometry[[1]] |
535 | 522x |
stopifnot(geometry %in% c("points", "envelope", "line")) |
536 | 522x |
if (geometry == "points") { |
537 | 296x |
if (nrow(x$data[[1]]) > 0) { |
538 | 296x |
grid::grid.points(x = x$data[[1]]$time, |
539 | 296x |
y = x$data[[1]]$quantity, |
540 | 296x |
default.unit = "native", |
541 | 296x |
pch = 21, |
542 | 296x |
gp = grid::gpar(col = "black", |
543 | 296x |
fill = color)) |
544 |
} |
|
545 |
} |
|
546 | 522x |
if (geometry == "envelope") { |
547 | 82x |
if (nrow(x$data[[1]]) > 0) { |
548 | 82x |
grid::grid.polygon(x = c(x$data[[1]]$time, rev(x$data[[1]]$time)), |
549 | 82x |
y = c(x$data[[1]]$quantity_low, rev(x$data[[1]]$quantity_high)), |
550 | 82x |
default.unit = "native", |
551 | 82x |
gp = grid::gpar(col = color, |
552 | 82x |
fill = adjustcolor(color, alpha.f = 0.25))) |
553 |
} |
|
554 |
} |
|
555 | 522x |
if (geometry == "line") { |
556 | 144x |
if (nrow(x$data[[1]]) > 0) { |
557 | 144x |
grid::grid.lines(x = x$data[[1]]$time, |
558 | 144x |
y = x$data[[1]]$quantity, |
559 | 144x |
default.unit = "native", |
560 | 144x |
gp = grid::gpar(col = color)) |
561 |
} |
|
562 |
} |
|
563 |
} |
|
564 | ||
565 |
### * draw_legend() |
|
566 | ||
567 |
#' Draw a legend for plot_nm() |
|
568 |
#' |
|
569 |
#' @importFrom grDevices adjustcolor |
|
570 |
#' |
|
571 |
#' @param x A named vector of colors |
|
572 |
#' |
|
573 |
#' @keywords internal |
|
574 |
#' @noRd |
|
575 | ||
576 |
draw_legend <- function(x) { |
|
577 | 30x |
lo <- grid::grid.layout(nrow = 2, |
578 | 30x |
heights = grid::unit(c(2, 1.5 * length(x)), |
579 | 30x |
c("lines", "lines"))) |
580 | 30x |
vp_legend <- grid::viewport(layout = lo) |
581 | 30x |
grid::pushViewport(vp_legend) |
582 |
# Title |
|
583 | 30x |
vp_title <- grid::viewport(layout.pos.row = 1) |
584 | 30x |
grid::pushViewport(vp_title) |
585 | 30x |
grid::grid.text("Compartments", x = grid::unit(2, "strwidth", list("a")), |
586 | 30x |
just = "left", |
587 | 30x |
gp = grid::gpar(fontface = 2)) |
588 | 30x |
grid::upViewport(1) |
589 |
# Label stack |
|
590 | 30x |
lo <- grid::grid.layout(nrow = length(x), |
591 | 30x |
heights = grid::unit(rep(1.5, length(x)), |
592 | 30x |
rep("lines", length(x)))) |
593 | 30x |
vp_label_stack_top <- grid::viewport(layout.pos.row = 2) |
594 | 30x |
grid::pushViewport(vp_label_stack_top) |
595 | 30x |
vp_label_stack <- grid::viewport(x = grid::unit(3, "strwidth", list("a")), |
596 | 30x |
just = "left", |
597 | 30x |
layout = lo) |
598 | 30x |
grid::pushViewport(vp_label_stack) |
599 |
# Individual labels |
|
600 | 30x |
for (i in seq_along(x)) { |
601 |
# Colored square + label |
|
602 | 78x |
lo <- grid::grid.layout(ncol = 2, |
603 | 78x |
widths = grid::unit(c(0.9, 1), c("lines", "null"))) |
604 | 78x |
vp_compartment <- grid::viewport(layout.pos.row = i, |
605 | 78x |
layout = lo) |
606 | 78x |
grid::pushViewport(vp_compartment) |
607 | 78x |
vp_square_top <- grid::viewport(layout.pos.col = 1) |
608 | 78x |
grid::pushViewport(vp_square_top) |
609 | 78x |
vp_square <- grid::viewport(width = grid::unit(1.1, "lines"), |
610 | 78x |
height = grid::unit(1.1, "lines")) |
611 | 78x |
grid::pushViewport(vp_square) |
612 | 78x |
grid::grid.rect(gp = grid::gpar(col = x[i], |
613 | 78x |
fill = adjustcolor(x[i], alpha.f = 0.5))) |
614 | 78x |
grid::upViewport(2) |
615 | 78x |
vp_label <- grid::viewport(layout.pos.col = 2) |
616 | 78x |
grid::pushViewport(vp_label) |
617 | 78x |
grid::grid.text(names(x)[i], x = grid::unit(2, "strwidth", list("a")), |
618 | 78x |
just = "left") |
619 | 78x |
grid::upViewport(2) |
620 |
} |
|
621 | 30x |
grid::upViewport(3) |
622 |
} |
|
623 | ||
624 |
### * prettyLog |
|
625 | ||
626 |
#' Find pretty ticks for log scale |
|
627 |
#' |
|
628 |
#' @param range Range of the axis, in log10-transformed scale |
|
629 |
#' |
|
630 |
#' @return A vector containing the tick values in natural scale |
|
631 |
#' |
|
632 |
#' @keywords internal |
|
633 |
#' @noRd |
|
634 | ||
635 |
prettyLog <- function(range) { |
|
636 | 24x |
x <- range |
637 | 24x |
min_x <- 10^min(x) |
638 | 24x |
max_x <- 10^max(x) |
639 | 24x |
z <- list(c(1), c(1, 5), c(1, 2, 5)) |
640 | 24x |
o <- lapply(seq_along(z), function(j) { |
641 | 72x |
k <- c() |
642 | 72x |
for (i in -10:10) { |
643 | 1512x |
k <- c(k, z[[j]] * 10^i) |
644 |
} |
|
645 | 72x |
k <- k[k >= min_x & k <= max_x] |
646 | 72x |
return(k) |
647 |
}) |
|
648 | 24x |
if (length(o[[1]]) >= 3) { |
649 | 24x |
return(o[[1]]) |
650 | ! |
} else if (length(o[[2]]) >= 3) { |
651 | ! |
return(o[[2]]) |
652 |
} else { |
|
653 | ! |
return(o[[3]]) |
654 |
} |
|
655 |
} |
|
656 | ||
657 |
### * prettyLogLabels |
|
658 | ||
659 |
#' Same as prettyLog but return expression for labels |
|
660 |
#' |
|
661 |
#' TODO For now the function just returns its input. |
|
662 |
#' |
|
663 |
#' @param range Range of the axis, in log10-transformed scale |
|
664 |
#' |
|
665 |
#' @return A vector containing the tick values in natural scale |
|
666 |
#' |
|
667 |
#' @keywords internal |
|
668 |
#' @noRd |
|
669 | ||
670 |
prettyLogLabels <- function(range) { |
|
671 | 6x |
ticks <- prettyLog(range) |
672 | 6x |
labels <- ticks |
673 | 6x |
return(labels) |
674 |
} |
|
675 | ||
676 |
### * plot_traces() |
|
677 | ||
678 |
#' Plot nice traces from a mcmc list |
|
679 |
#' |
|
680 |
#' @param x A \code{coda::mcmc.list} object |
|
681 |
#' @param variables Optional, a vector specifying the ordered variables to plot |
|
682 |
#' @param regexp If TRUE (the default), strings in \code{variables} are used to |
|
683 |
#' select traces to draw by pattern matching with the variables names. |
|
684 |
#' @param loglik Boolean, if \code{TRUE} additionally plot the loglik trace. |
|
685 |
#' @param ratio Aspect ratio for the overall plot that the function thrives to |
|
686 |
#' respect (this will have an effect on the balance between numbers of rows |
|
687 |
#' and colums) |
|
688 |
#' @param transform Function to use to transform the values before plotting |
|
689 |
#' (e.g. log). The function is applied directly to \code{x}, so it should |
|
690 |
#' be able to handle \code{mcmc.list} object (and return a similar object). |
|
691 |
#' @param lty Line type for the density plots |
|
692 |
#' @param hist Boolean, if TRUE draw histograms instead of density profiles |
|
693 |
#' @param nbins Number of histogram bins, used only if \code{drawHist} is TRUE. |
|
694 |
#' @param pdf Filename to save the plot as a pdf. If \code{NULL}, the plot is |
|
695 |
#' displayed. |
|
696 |
#' @param png Filename to save the plot as a png. If \code{NULL}, the plot is |
|
697 |
#' displayed. |
|
698 |
#' @param width,height Width and height of the pdf file in inches. Also used |
|
699 |
#' for the size of the png file, using a RES value of 150. |
|
700 |
#' |
|
701 |
#' @importFrom grDevices dev.hold |
|
702 |
#' @importFrom grDevices dev.flush |
|
703 |
#' @importFrom grDevices dev.off |
|
704 |
#' |
|
705 |
#' @keywords internal |
|
706 |
#' @noRd |
|
707 | ||
708 |
plot_traces <- function(x, variables = NULL, regexp = TRUE, loglik = FALSE, |
|
709 |
ratio = 4/3, transform = NULL, lty = 1, |
|
710 |
hist = TRUE, nbins = 32, |
|
711 |
pdf = NULL, png = NULL, width = 12, height = 10) { |
|
712 | 10x |
x_original <- x |
713 | 10x |
if (is.null(transform)) { |
714 | 10x |
x <- x |
715 |
} else { |
|
716 | ! |
x <- transform(x) |
717 |
} |
|
718 | 10x |
drawHist <- hist |
719 | 10x |
nBars <- nbins |
720 |
# Parse pdf/png arguments |
|
721 | 10x |
stopifnot(is.null(pdf) | is.null(png)) |
722 | 10x |
if (!is.null(pdf)) { |
723 | ! |
pdf(file = pdf, width = width, height = height) |
724 |
} |
|
725 | 10x |
if (!is.null(png)) { |
726 | ! |
RES = 150 |
727 | ! |
png(file = png, width = width * RES, height = height * RES, res = RES, |
728 | ! |
type = "cairo-png") |
729 |
} |
|
730 | 10x |
dev.hold() |
731 | 10x |
on.exit(dev.flush()) |
732 |
# Prepare data |
|
733 | 10x |
if (loglik) { |
734 | ! |
prev_varnames <- coda::varnames(x) |
735 |
# Add loglik trace to each mcmc object |
|
736 | ! |
mcmcList <- lapply(seq_along(x), function(i) { |
737 | ! |
y <- cbind(x[[i]], attr(x, "loglik")[[i]]) |
738 | ! |
colnames(y) <- c(prev_varnames, "loglik") |
739 | ! |
chainI <- coda::mcmc(y) |
740 | ! |
return(chainI) |
741 |
}) |
|
742 | ! |
x <- coda::as.mcmc.list(mcmcList) |
743 | ! |
for (i in seq_along(x)) { |
744 | ! |
attr(x[[i]], "mcpar") <- coda::mcpar(x_original) |
745 |
} |
|
746 |
} |
|
747 | 10x |
if (regexp & !is.null(variables)) { |
748 | ! |
vars <- coda::varnames(x) |
749 | ! |
selectedVars <- vars[unlist(sapply(variables, grep, x = vars))] |
750 | ! |
selectedVars <- unique(selectedVars) |
751 | ! |
if (length(selectedVars) == 0) { |
752 | ! |
stop("No variables found matching: ", variables) |
753 |
} |
|
754 | ! |
variables <- selectedVars |
755 |
} |
|
756 | 10x |
drawTraces(mcmc.list = x, variables = variables, |
757 | 10x |
ratio = ratio, lty = lty, |
758 | 10x |
drawHist = drawHist, nBars = nBars) |
759 | 10x |
if (!is.null(pdf) | !is.null(png)) { |
760 | ! |
invisible(dev.off()) |
761 |
} |
|
762 | 10x |
return(invisible(x_original)) |
763 |
} |
|
764 | ||
765 |
### * drawTraces |
|
766 | ||
767 |
#' Draw the traces for a mcmc.list object |
|
768 |
#' |
|
769 |
#' @param mcmc.list A coda mcmc.list object |
|
770 |
#' @param drawXAxis Boolean |
|
771 |
#' @param variables A vector containing the names of the variables to include |
|
772 |
#' in the plot. If NULL, all variables are used. |
|
773 |
#' @param ratio Aspect ratio for the overall plot that the function thrives to |
|
774 |
#' respect (this will have an effect on the balance between numbers of rows |
|
775 |
#' and colums) |
|
776 |
#' @param drawHist Boolean, if TRUE draw histograms instead of density profiles |
|
777 |
#' @param nBars Number of histogram bars, used only if \code{drawHist} is TRUE. |
|
778 |
#' @param newpage Boolean, run grid.newpage() before drawing? |
|
779 |
#' @param ... Arguments passed to \code{\link{drawTraceOneVar}} |
|
780 |
#' |
|
781 |
#' @keywords internal |
|
782 |
#' @noRd |
|
783 | ||
784 |
drawTraces = function(mcmc.list, drawXAxis = FALSE, variables = NULL, |
|
785 |
ratio = 4/3, drawHist = FALSE, nBars = 64, |
|
786 |
newpage = TRUE, ...) { |
|
787 | 10x |
nchains = coda::nchain(mcmc.list) |
788 | 10x |
nvars = coda::nvar(mcmc.list) |
789 | 10x |
varsToPlot = 1:nvars |
790 | 10x |
if (!is.null(variables)) { |
791 | ! |
nvars = length(variables) |
792 | ! |
varsToPlot = variables |
793 |
} |
|
794 | 10x |
colors = c("deeppink", "green3", "darkmagenta", "dodgerblue") |
795 | 10x |
colors = rep(colors, nchains) |
796 | 10x |
mfrow = n2mfrowByRatio(nvars, ratio = ratio) |
797 |
# New page |
|
798 | 10x |
if (newpage) grid::grid.newpage() |
799 |
# Viewport for traces |
|
800 | 10x |
if (drawXAxis) { |
801 | ! |
vpTraces = grid::viewport(layout = grid::grid.layout(nrow = mfrow[1] + 1, ncol = mfrow[2], |
802 | ! |
heights = grid::unit(c(rep(1, mfrow[1]), 4), c(rep("null", mfrow[1]), "lines"))), |
803 | ! |
name = "vpTraces", clip = "on") |
804 |
} else { |
|
805 | 10x |
vpTraces = grid::viewport(layout = grid::grid.layout(nrow = mfrow[1], ncol = mfrow[2]), |
806 | 10x |
name = "vpTraces", clip = "on") |
807 |
} |
|
808 | 10x |
grid::pushViewport(vpTraces) |
809 |
# Draw panel for each parameter |
|
810 | 10x |
lapply(seq_len(nvars), function(v) { |
811 | 68x |
row = 1 + (v-1) %/% mfrow[2] |
812 | 68x |
column = 1 + (v-1) %% mfrow[2] |
813 | 68x |
vpCell = grid::viewport(layout.pos.row = row, |
814 | 68x |
layout.pos.col = column, |
815 | 68x |
name = paste("vpCell", row, column, sep = ".")) |
816 | 68x |
grid::pushViewport(vpCell) |
817 | 68x |
grid::grid.rect(gp = grid::gpar(col = grey(0.5))) |
818 | 68x |
if (drawHist) { |
819 | 60x |
drawTraceOneVarHist(mcmc.list = mcmc.list, varname = varsToPlot[v], colors = colors, |
820 | 60x |
drawXAxis = ifelse(drawXAxis, |
821 | 60x |
(row == mfrow[1] | (row == (mfrow[1] - 1) & column == mfrow[2])), |
822 | 60x |
FALSE), nBars = nBars, ...) |
823 |
} else { |
|
824 | 8x |
drawTraceOneVar(mcmc.list = mcmc.list, varname = varsToPlot[v], colors = colors, |
825 | 8x |
drawXAxis = ifelse(drawXAxis, |
826 | 8x |
(row == mfrow[1] | (row == (mfrow[1] - 1) & column == mfrow[2])), |
827 | 8x |
FALSE), ...) |
828 |
} |
|
829 | 68x |
grid::upViewport(1) |
830 |
}) |
|
831 |
# Back to parent viewport |
|
832 | 10x |
grid::upViewport(1) |
833 |
} |
|
834 | ||
835 |
### * drawTraceOneVar |
|
836 | ||
837 |
#' Draw the chain traces for one variable, using a density profile for the density panel |
|
838 |
#' |
|
839 |
#' This function is mostly useful when it is called by other functions as a |
|
840 |
#' buildign block to draw the traces of several variables. |
|
841 |
#' |
|
842 |
#' @param mcmc.list A coda mcmc.list object |
|
843 |
#' @param varname Name of the variable of which the trace is to be drawn |
|
844 |
#' @param colors Vector of colors for the chain traces |
|
845 |
#' @param drawXAxis Boolean |
|
846 |
#' @param lty Line type for the density plot |
|
847 |
#' |
|
848 |
#' @importFrom stats density |
|
849 |
#' @importFrom grDevices grey |
|
850 |
#' @importFrom grDevices extendrange |
|
851 |
#' @importFrom grDevices adjustcolor |
|
852 |
#' |
|
853 |
#' @keywords internal |
|
854 |
#' @noRd |
|
855 | ||
856 |
drawTraceOneVar = function(mcmc.list, varname, colors, drawXAxis = FALSE, lty = 1) { |
|
857 | 8x |
EXTENSION = c(0.025, 0.1) |
858 | 8x |
ALPHA_FILL = 0.2 |
859 | 8x |
ALPHA_TRACES = 0.6 |
860 | 8x |
DENSITY_MAX_RELATIVE_SHIFT = 0 # Can be set to e.g. 0.25 for slight shift in density plots |
861 | 8x |
MODE_COL = "white" |
862 | 8x |
CI_LWD = c(0.25, 0.15) # Unit: char |
863 |
# Process the data |
|
864 | 8x |
varnameOriginal <- varname |
865 | 8x |
if (is.numeric(varname)) { |
866 | 8x |
varname = coda::varnames(mcmc.list)[varname] |
867 | 8x |
if (is.null(varname)) { |
868 | ! |
varname <- "unnamed_variable" |
869 |
} |
|
870 |
} |
|
871 | 8x |
mcpars = coda::mcpar(mcmc.list[[1]]) |
872 | 8x |
nchains = coda::nchain(mcmc.list) |
873 | 8x |
if (!is.null(dim(mcmc.list[[1]]))) { |
874 | 8x |
varData = lapply(mcmc.list, function(mcmc) mcmc[, varnameOriginal]) |
875 |
} else { |
|
876 |
# There is only one variable |
|
877 | ! |
varData = lapply(mcmc.list, function(mcmc) as.vector(mcmc)) |
878 |
} |
|
879 | 8x |
xData = seq(from = mcpars[1], to = mcpars[2], by = mcpars[3]) |
880 | 8x |
yData = unlist(varData) |
881 | 8x |
densities = lapply(varData, density) |
882 | 8x |
densitiesData = unlist(lapply(densities, "[[", "y")) |
883 | 8x |
densitiesRange = range(densitiesData) |
884 | 8x |
if (nchains > 1) { |
885 | 8x |
stepDensity = diff(densitiesRange) * DENSITY_MAX_RELATIVE_SHIFT / (nchains - 1) |
886 |
} else { |
|
887 | ! |
stepDensity = 0 |
888 |
} |
|
889 | 8x |
densitiesRange[2] = densitiesRange[2] + stepDensity * (nchains - 1) + diff(densitiesRange) * 0.1 |
890 |
# Viewport for the parameter panel |
|
891 | 8x |
vpParamPanel = grid::viewport(layout = grid::grid.layout(nrow = 2, ncol = 1, |
892 | 8x |
heights = grid::unit(c(1.5, 1), c("lines", "null"))), |
893 | 8x |
name = paste("vpParamPanel", varname, sep = ".")) |
894 | 8x |
grid::pushViewport(vpParamPanel) |
895 |
# Viewport for the title |
|
896 | 8x |
vpTitle = grid::viewport(name = "vpTitle", layout.pos.row = 1) |
897 | 8x |
grid::pushViewport(vpTitle) |
898 | 8x |
grid::grid.rect(gp = grid::gpar(fill = grey(0.95), col = grey(0.5))) |
899 | 8x |
grid::grid.text(varnameToExp(varname), |
900 | 8x |
gp = grid::gpar(col = grey(0.5), cex = 1.2)) |
901 | 8x |
grid::upViewport(1) |
902 |
# Viewport for the graph |
|
903 | 8x |
vpGraph = grid::viewport(layout = grid::grid.layout(nrow = 1, ncol = 3, |
904 | 8x |
widths = grid::unit(c(3, 1, 0.25), c("lines", "null", "null"))), |
905 | 8x |
name = "vpGraph", layout.pos.row = 2, clip = ifelse(drawXAxis, "off", "on")) |
906 | 8x |
grid::pushViewport(vpGraph) |
907 |
# Viewport for the y axis |
|
908 | 8x |
vpYAxis = grid::viewport(name = "vpYAxis", layout.pos.col = 1) |
909 | 8x |
grid::pushViewport(vpYAxis) |
910 | 8x |
grid::upViewport(1) |
911 |
# Viewport for the trace plot |
|
912 | 8x |
vpTracePlot = grid::dataViewport(xData = xData, yData = yData, extension = EXTENSION, |
913 | 8x |
name = "vpTracePlot", layout.pos.col = 2) |
914 | 8x |
grid::pushViewport(vpTracePlot) |
915 |
## Draw the chains |
|
916 | 8x |
lapply(seq_len(nchains), function(i) { |
917 | 16x |
grid::grid.lines(x = xData, y = varData[[i]], default.units = "native", |
918 | 16x |
gp = grid::gpar(col = adjustcolor(colors[i], alpha.f = ALPHA_TRACES), |
919 | 16x |
lwd = 1)) |
920 |
}) |
|
921 |
## Draw axes |
|
922 | 8x |
grid::grid.yaxis(gp = grid::gpar(cex = 0.7)) |
923 | 8x |
if (drawXAxis) { |
924 |
# https://stackoverflow.com/questions/8816456/rotate-labels-in-grid-xaxis |
|
925 | ! |
grid::grid.xaxis(edits = grid::gEdit(gPath="labels", rot=90)) |
926 |
} |
|
927 | 8x |
grid::upViewport(1) |
928 |
# Viewport for the density plot |
|
929 | 8x |
vpDensityPlot = grid::dataViewport(xscale = extendrange(densitiesRange, f = 0.025), |
930 | 8x |
yData = yData, extension = EXTENSION, |
931 | 8x |
name = "vpDensityPlot", layout.pos.col = 3, |
932 | 8x |
clip = "on") |
933 | 8x |
grid::pushViewport(vpDensityPlot) |
934 | 8x |
lapply(seq_len(nchains), function(i) { |
935 | 16x |
grid::grid.polygon(x = c(densities[[i]]$y, densities[[i]]$y[1]) + stepDensity * (i - 1), |
936 | 16x |
y = c(densities[[i]]$x, densities[[i]]$x[1]), |
937 | 16x |
default.unit = "native", |
938 | 16x |
gp = grid::gpar(col = colors[i], |
939 | 16x |
fill = adjustcolor(colors[i], alpha.f = ALPHA_FILL), |
940 | 16x |
lty = lty)) |
941 |
}) |
|
942 | 8x |
grid::upViewport(1) |
943 |
# Back to vpParamPanel |
|
944 | 8x |
grid::upViewport(1) |
945 |
# Back to original (parent) viewport |
|
946 | 8x |
grid::upViewport(1) |
947 |
} |
|
948 | ||
949 |
### * drawTraceOneVarHist |
|
950 | ||
951 |
#' Draw the chain traces for one variable, using a histogram for density panel |
|
952 |
#' |
|
953 |
#' This function is mostly useful when it is called by other functions as a |
|
954 |
#' buildign block to draw the traces of several variables. |
|
955 |
#' |
|
956 |
#' @param mcmc.list A coda mcmc.list object |
|
957 |
#' @param varname Name of the variable of which the trace is to be drawn |
|
958 |
#' @param colors Vector of colors for the chain traces |
|
959 |
#' @param drawXAxis Boolean |
|
960 |
#' @param lty Line type for the density plot |
|
961 |
#' @param nBars Number of histogram bars per trace |
|
962 |
#' |
|
963 |
#' @importFrom stats density |
|
964 |
#' @importFrom graphics hist |
|
965 |
#' @importFrom grDevices grey |
|
966 |
#' @importFrom grDevices extendrange |
|
967 |
#' @importFrom grDevices adjustcolor |
|
968 |
#' |
|
969 |
#' @keywords internal |
|
970 |
#' @noRd |
|
971 | ||
972 |
drawTraceOneVarHist = function(mcmc.list, varname, colors, drawXAxis = FALSE, lty = 1, nBars = 128) { |
|
973 | 60x |
EXTENSION = c(0.025, 0.1) |
974 | 60x |
ALPHA_FILL = 0.2 |
975 | 60x |
ALPHA_TRACES = 0.6 |
976 | 60x |
DENSITY_MAX_RELATIVE_SHIFT = 0 # Can be set to e.g. 0.25 for slight shift in density plots |
977 | 60x |
MODE_COL = "white" |
978 | 60x |
CI_LWD = c(0.25, 0.15) # Unit: char |
979 |
# Process the data |
|
980 | 60x |
varnameOriginal <- varname |
981 | 60x |
if (is.numeric(varname)) { |
982 | 60x |
varname = coda::varnames(mcmc.list)[varname] |
983 | 60x |
if (is.null(varname)) { |
984 | ! |
varname <- "unnamed_variable" |
985 |
} |
|
986 |
} |
|
987 | 60x |
mcpars = coda::mcpar(mcmc.list[[1]]) |
988 | 60x |
nchains = coda::nchain(mcmc.list) |
989 | 60x |
if (!is.null(dim(mcmc.list[[1]]))) { |
990 | 60x |
varData = lapply(mcmc.list, function(mcmc) mcmc[, varnameOriginal]) |
991 |
} else { |
|
992 |
# There is only one variable |
|
993 | ! |
varData = lapply(mcmc.list, function(mcmc) as.vector(mcmc)) |
994 |
} |
|
995 | 60x |
xData = seq(from = mcpars[1], to = mcpars[2], by = mcpars[3]) |
996 | 60x |
yData = unlist(varData) |
997 | 60x |
densities = lapply(varData, density) |
998 | 60x |
densitiesData = unlist(lapply(densities, "[[", "y")) |
999 | 60x |
densitiesRange = range(densitiesData) |
1000 | 60x |
histRange <- range(unlist(varData)) |
1001 | 60x |
histBreaks <- seq(histRange[1], histRange[2], length.out = nBars + 1) |
1002 | 60x |
histograms <- lapply(seq_along(varData), function(i) { |
1003 | 120x |
d <- varData[[i]] |
1004 | 120x |
hist(d, breaks = histBreaks, plot = FALSE)$density |
1005 |
}) |
|
1006 | 60x |
if (nchains > 1) { |
1007 | 60x |
stepDensity = diff(densitiesRange) * DENSITY_MAX_RELATIVE_SHIFT / (nchains - 1) |
1008 |
} else { |
|
1009 | ! |
stepDensity = 0 |
1010 |
} |
|
1011 | 60x |
densitiesRange[2] = densitiesRange[2] + stepDensity * (nchains - 1) + diff(densitiesRange) * 0.1 |
1012 |
# Viewport for the parameter panel |
|
1013 | 60x |
vpParamPanel = grid::viewport(layout = grid::grid.layout(nrow = 2, ncol = 1, |
1014 | 60x |
heights = grid::unit(c(1.5, 1), c("lines", "null"))), |
1015 | 60x |
name = paste("vpParamPanel", varname, sep = ".")) |
1016 | 60x |
grid::pushViewport(vpParamPanel) |
1017 |
# Viewport for the title |
|
1018 | 60x |
vpTitle = grid::viewport(name = "vpTitle", layout.pos.row = 1) |
1019 | 60x |
grid::pushViewport(vpTitle) |
1020 | 60x |
grid::grid.rect(gp = grid::gpar(fill = grey(0.95), col = grey(0.5))) |
1021 | 60x |
grid::grid.text(varnameToExp(varname), |
1022 | 60x |
gp = grid::gpar(col = grey(0.5), cex = 1.2)) |
1023 | 60x |
grid::upViewport(1) |
1024 |
# Viewport for the graph |
|
1025 | 60x |
vpGraph = grid::viewport(layout = grid::grid.layout(nrow = 1, ncol = 3, |
1026 | 60x |
widths = grid::unit(c(3, 1, 0.25), c("lines", "null", "null"))), |
1027 | 60x |
name = "vpGraph", layout.pos.row = 2, clip = ifelse(drawXAxis, "off", "on")) |
1028 | 60x |
grid::pushViewport(vpGraph) |
1029 |
# Viewport for the y axis |
|
1030 | 60x |
vpYAxis = grid::viewport(name = "vpYAxis", layout.pos.col = 1) |
1031 | 60x |
grid::pushViewport(vpYAxis) |
1032 | 60x |
grid::upViewport(1) |
1033 |
# Viewport for the trace plot |
|
1034 | 60x |
if (length(unique(yData)) == 1) { |
1035 | ! |
if (yData[1] == 0) { |
1036 | ! |
adjYdata <- c(-0.1, 0.1) |
1037 |
} else { |
|
1038 | ! |
adjYdata <- c(0.9 * unique(yData), yData, 1.1 * unique(yData)) |
1039 |
} |
|
1040 |
} else { |
|
1041 | 60x |
adjYdata <- yData |
1042 |
} |
|
1043 | 60x |
vpTracePlot = grid::dataViewport(xData = xData, yData = adjYdata, extension = EXTENSION, |
1044 | 60x |
name = "vpTracePlot", layout.pos.col = 2) |
1045 | 60x |
grid::pushViewport(vpTracePlot) |
1046 |
## Draw the chains |
|
1047 | 60x |
lapply(seq_len(nchains), function(i) { |
1048 | 120x |
grid::grid.lines(x = xData, y = varData[[i]], default.units = "native", |
1049 | 120x |
gp = grid::gpar(col = adjustcolor(colors[i], alpha.f = ALPHA_TRACES), |
1050 | 120x |
lwd = 1)) |
1051 |
}) |
|
1052 |
## Draw axes |
|
1053 | 60x |
grid::grid.yaxis(gp = grid::gpar(cex = 0.7)) |
1054 | 60x |
if (drawXAxis) { |
1055 |
# https://stackoverflow.com/questions/8816456/rotate-labels-in-grid-xaxis |
|
1056 | ! |
grid::grid.xaxis(edits = grid::gEdit(gPath="labels", rot=90)) |
1057 |
} |
|
1058 | 60x |
grid::upViewport(1) |
1059 |
# Viewport for the density plot |
|
1060 | 60x |
if (!any(is.na(unlist(histograms)))) { |
1061 | 60x |
if (length(unique(yData)) == 1) { |
1062 | ! |
adjYdata <- c(0.9 * unique(yData), yData, 1.1 * unique(yData)) |
1063 |
} else { |
|
1064 | 60x |
adjYdata <- yData |
1065 |
} |
|
1066 | 60x |
vpDensityPlot = grid::dataViewport(xscale = extendrange(c(0, max(unlist(histograms))), f = 0.025), |
1067 | 60x |
yData = adjYdata, extension = EXTENSION, |
1068 | 60x |
name = "vpDensityPlot", layout.pos.col = 3, |
1069 | 60x |
clip = "on") |
1070 | 60x |
grid::pushViewport(vpDensityPlot) |
1071 | 60x |
lapply(seq_len(nchains), function(i) { |
1072 | 120x |
grid::grid.polygon(x = c(0, rep(histograms[[i]], each = 2), 0), #c(densities[[i]]$y, densities[[i]]$y[1]) + stepDensity * (i - 1), |
1073 | 120x |
y = rep(histBreaks, each = 2), #c(densities[[i]]$x, densities[[i]]$x[1]), |
1074 | 120x |
default.unit = "native", |
1075 | 120x |
gp = grid::gpar(col = colors[i], |
1076 | 120x |
fill = adjustcolor(colors[i], alpha.f = ALPHA_FILL), |
1077 | 120x |
lty = lty)) |
1078 |
}) |
|
1079 | 60x |
grid::upViewport(1) |
1080 |
} |
|
1081 |
# Back to vpParamPanel |
|
1082 | 60x |
grid::upViewport(1) |
1083 |
# Back to original (parent) viewport |
|
1084 | 60x |
grid::upViewport(1) |
1085 |
} |
|
1086 | ||
1087 |
### * n2mfrowByRatio |
|
1088 | ||
1089 |
#' Provide a good mfrow argument given a number of cells and a target ratio |
|
1090 |
#' |
|
1091 |
#' @param n Number of cells to fit in the plot |
|
1092 |
#' @param ratio Aspect ratio for the overall plot that the function thrives to |
|
1093 |
#' respect (this will have an effect on the balance between numbers of rows |
|
1094 |
#' and colums) |
|
1095 |
#' |
|
1096 |
#' @importFrom grDevices dev.size |
|
1097 |
#' |
|
1098 |
#' @keywords internal |
|
1099 |
#' @noRd |
|
1100 | ||
1101 |
n2mfrowByRatio = function(n, ratio = 4/3) { |
|
1102 |
# Build all the reasonable combinations of nRows x nColumns |
|
1103 | 10x |
nOne = seq_len(n) |
1104 | 10x |
nTheOther = ceiling(n / nOne) |
1105 | 10x |
nTheOtherKept = unique(nTheOther) |
1106 | 10x |
nOneKept = sapply(nTheOtherKept, function(k) { |
1107 | 46x |
min(nOne[nTheOther == k]) |
1108 |
}) |
|
1109 |
# Note: one can do nRows = c(nOneKept, nTheOtherKept) and nColumns = |
|
1110 |
# c(nTheOtherKept, nOneKept) if it is important to minimize the number of |
|
1111 |
# empty cells |
|
1112 | 10x |
nRows = c(nOne, nTheOther) |
1113 | 10x |
nColumns = c(nTheOther, nOne) |
1114 | 10x |
nRbyC = as.data.frame(unique(cbind(nRows, nColumns))) |
1115 | 10x |
names(nRbyC) = c("nRows", "nColumns") |
1116 |
# Get device size |
|
1117 | 10x |
devSize = dev.size() |
1118 |
# Calculate ratios |
|
1119 | 10x |
nRbyC$rowHeight = devSize[2] / nRbyC$nRows |
1120 | 10x |
nRbyC$colWidth = devSize[1] / nRbyC$nColumns |
1121 | 10x |
nRbyC$ratio = nRbyC$colWidth / nRbyC$rowHeight |
1122 | 10x |
nRbyC$dist = abs(log(nRbyC$ratio / ratio)) |
1123 | 10x |
i = which(nRbyC$dist == min(nRbyC$dist)) |
1124 |
# Return |
|
1125 | 10x |
return(c(nRbyC$nRows[i], nRbyC$nColumns[i])) |
1126 |
} |
|
1127 | ||
1128 |
### * varnameToExp |
|
1129 | ||
1130 |
#' Convert a variable name to the appropriate mathematical expression |
|
1131 |
#' |
|
1132 |
#' @param varname Variable name to be converted |
|
1133 |
#' |
|
1134 |
#' @keywords internal |
|
1135 |
#' @noRd |
|
1136 | ||
1137 |
varnameToExp = function(varname) { |
|
1138 | 68x |
rm_underscore <- function(x) { |
1139 | 66x |
gsub("_", ".", x) |
1140 |
} |
|
1141 |
# Try to split with "|" to find replication variables |
|
1142 | 68x |
elements = strsplit(varname, split = "[|]")[[1]] |
1143 | 68x |
varname = elements[1] |
1144 | 68x |
replVar = "" |
1145 | 68x |
if (length(elements) > 1) { |
1146 | ! |
replVar = paste(elements[2:length(elements)], collapse = ",") |
1147 | ! |
replVar = paste(" |", rm_underscore(replVar)) |
1148 |
} |
|
1149 |
# Uptake rate |
|
1150 | 68x |
if (grepl("^upsilon_", varname)) { |
1151 | 18x |
tmp = strsplit(varname, "^upsilon_")[[1]][2] |
1152 | 18x |
tmp = strsplit(tmp, "_to_")[[1]] |
1153 | 18x |
from = rm_underscore(tmp[1]) |
1154 | 18x |
to = rm_underscore(tmp[2]) |
1155 | 18x |
return(latex2exp::TeX(paste0("$\\upsilon_{", from, " \\rightarrow ", to, "}$", replVar))) |
1156 |
} |
|
1157 |
# Loss rate |
|
1158 | 50x |
if (grepl("^lambda_", varname)) { |
1159 | 26x |
comp = rm_underscore(strsplit(varname, "^lambda_")[[1]][2]) |
1160 | 26x |
return(latex2exp::TeX(paste0("$\\lambda_{", comp, "}$", replVar))) |
1161 |
} |
|
1162 |
# Active fraction |
|
1163 | 24x |
if (grepl("^portion\\.act_", varname)) { |
1164 | 4x |
comp = rm_underscore(strsplit(varname, "^portion\\.act_")[[1]][2]) |
1165 | 4x |
if (nchar(comp) > 4) { |
1166 | ! |
comp = substr(comp, start = 1, stop = nchar(comp) - 4) |
1167 |
} |
|
1168 | 4x |
return(latex2exp::TeX(paste0("$\\pi_{", comp, "}$", replVar))) |
1169 |
} |
|
1170 |
# propCv |
|
1171 | 20x |
if (varname == "eta") { |
1172 | 10x |
if (replVar == "") { |
1173 | 10x |
return(latex2exp::TeX(paste0("$\\eta$"))) |
1174 |
} |
|
1175 | ! |
return(latex2exp::TeX(paste0("$\\eta_{", "}$", replVar))) |
1176 |
} |
|
1177 |
# sizeCv |
|
1178 | 10x |
if (varname == "zeta") { |
1179 | 10x |
if (replVar == "") { |
1180 | 10x |
return(latex2exp::TeX(paste0("$\\zeta$"))) |
1181 |
} |
|
1182 | ! |
return(latex2exp::TeX(paste0("$\\zeta_{", "}$", replVar))) |
1183 |
} |
|
1184 | ! |
if (grepl("^zeta_", varname)) { |
1185 | ! |
comp = rm_underscore(strsplit(varname, "^zeta_")[[1]][2]) |
1186 | ! |
if (replVar == "") { |
1187 | ! |
return(latex2exp::TeX(paste0("$\\zeta_{", comp, "}$"))) |
1188 |
} |
|
1189 | ! |
return(latex2exp::TeX(paste0("$\\zeta_{", comp, "}$", replVar))) |
1190 |
} |
|
1191 |
# Return |
|
1192 | ! |
if (replVar == "") { |
1193 | ! |
return(varname) |
1194 |
} |
|
1195 | ! |
return(paste0(varname, replVar)) |
1196 |
} |
|
1197 | ||
1198 |
### * capture_msg() |
|
1199 | ||
1200 |
#' Capture a message generated by running an expression |
|
1201 |
#' |
|
1202 |
#' This function is inspired by the code from testthat::capture_message(). Note |
|
1203 |
#' that an expression such as `m <- new_networkModel()` will not have an effect |
|
1204 |
#' on the calling environment if a message is caught (i.e. no `m` object is |
|
1205 |
#' created in the calling environment of `capture_msg()`). In my understanding, |
|
1206 |
#' this is because the emission of a message interrupts the expression |
|
1207 |
#' evaluation. |
|
1208 |
#' |
|
1209 |
#' @param expr Expression to evaluate. |
|
1210 |
#' |
|
1211 |
#' @return NULL if no message was produced, the first message produced otherwise. |
|
1212 |
#' |
|
1213 |
#' @examples |
|
1214 |
#' isotracer:::capture_msg(new_networkModel()) |
|
1215 |
#' |
|
1216 |
#' @keywords internal |
|
1217 |
#' @noRd |
|
1218 | ||
1219 |
capture_msg <- function(expr) { |
|
1220 | 3x |
captured <- tryCatch({ |
1221 | 3x |
expr |
1222 | 1x |
NULL |
1223 | 3x |
}, message = function(msg) { |
1224 | 2x |
return(msg) |
1225 |
}) |
|
1226 | 3x |
return(captured) |
1227 |
} |
|
1228 | ||
1229 |
### * flows_from_topo() |
|
1230 | ||
1231 |
#' Build a flow tibble from a topology |
|
1232 |
#' |
|
1233 |
#' @param x A topology. |
|
1234 |
#' |
|
1235 |
#' @return A tibble with columns "from" and "to". |
|
1236 |
#' |
|
1237 |
#' @examples |
|
1238 |
#' flows_from_topo(topo(trini_mod)) |
|
1239 |
#' |
|
1240 |
#' @keywords internal |
|
1241 |
#' @noRd |
|
1242 | ||
1243 |
flows_from_topo <- function(x) { |
|
1244 | 5x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
1245 | 5x |
n_comps <- ncol(x) |
1246 | 5x |
links <- which(x > 0) |
1247 | 5x |
from <- links %/% n_comps + 1 |
1248 | 5x |
to <- links %% n_comps |
1249 | 5x |
links <- tibble::tibble(from = from, to = to) |
1250 | 5x |
for (i in seq_len(nrow(links))) { |
1251 | 34x |
if (links$to[i] == 0) { |
1252 | 8x |
links$from[i] <- links$from[i] - 1 |
1253 | 8x |
links$to[i] <- n_comps |
1254 |
} |
|
1255 | 34x |
stopifnot(x[links$to[i], links$from[i]] > 0) |
1256 |
} |
|
1257 | 5x |
flows <- tibble::tibble(from = colnames(x)[links$from], |
1258 | 5x |
to = rownames(x)[links$to]) |
1259 | 5x |
return(flows) |
1260 |
} |
|
1261 | ||
1262 |
### * nodes_from_topo() |
|
1263 | ||
1264 |
#' Build a node tibble from a topology |
|
1265 |
#' |
|
1266 |
#' @param x A topology. |
|
1267 |
#' |
|
1268 |
#' @return A tibble with columns "comp" and "label". |
|
1269 |
#' |
|
1270 |
#' @examples |
|
1271 |
#' nodes_from_topo(topo(trini_mod)) |
|
1272 |
#' |
|
1273 |
#' @keywords internal |
|
1274 |
#' @noRd |
|
1275 | ||
1276 |
nodes_from_topo <- function(x) { |
|
1277 | 2x |
nodes <- tibble::tibble(comp = colnames(x), |
1278 | 2x |
label = colnames(x)) |
1279 | 2x |
return(nodes) |
1280 |
} |
1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * encode_priors() |
|
4 | ||
5 |
#' Prepare the prior encoding for stan data |
|
6 |
#' |
|
7 |
#' @param params_nm Output from params(nm, simplify = TRUE). |
|
8 |
#' @param priors_nm Output from priors(nm) (must have the same parameter order |
|
9 |
#' as params_nm). |
|
10 |
#' |
|
11 |
#' @keywords internal |
|
12 |
#' @noRd |
|
13 | ||
14 |
encode_priors <- function(params_nm, priors_nm) { |
|
15 | 27x |
if (any(sapply(priors_nm$prior, is.null))) { |
16 | ! |
stop("One or several parameters are missing a prior.\n", |
17 | ! |
"You can list the current model priors with `priors(...)`.\n", |
18 | ! |
"You can list the missing model priors with `missing_priors(...)`.") |
19 |
} |
|
20 | 27x |
d <- list() |
21 | 27x |
d[["nPriorConstant_code0"]] <- 0 |
22 | 27x |
d[["nPriorUniform_code1"]] <- 0 |
23 | 27x |
d[["nPriorHcauchy_code2"]] <- 0 |
24 | 27x |
d[["nPriorBeta_code3"]] <- 0 |
25 | 27x |
d[["nPriorTrNormal_code4"]] <- 0 |
26 | 27x |
d[["nPriorExponential_code5"]] <- 0 |
27 | 27x |
d[["nPriorGamma_code6"]] <- 0 |
28 |
### Prior types (used to map the correct prior in the stan model) |
|
29 | 27x |
priorTypes <- c("constant" = 0, "uniform" = 1, "hcauchy" = 2, "scaled_beta" = 3, |
30 | 27x |
"trun_normal" = 4, "exponential" = 5, "gamma" = 6) |
31 |
### Parameter priors |
|
32 | 27x |
d[["mappingParamPriorType"]] <- rep(NA, length(params_nm)) |
33 | 27x |
d[["mappingParamPriorID"]] <- rep(NA, length(params_nm)) |
34 | 27x |
prior_i <- c("constant" = 1, "uniform" = 1, "hcauchy" = 1, |
35 | 27x |
"scaled_beta" = 1, "trun_normal" = 1, "exponential" = 1, |
36 | 27x |
"gamma" = 1) # Counts for each prior type |
37 |
# Set some defaults |
|
38 | 27x |
for (priorDistParam in c("constantParams", "lowerParams", "upperParams", "hcauchyScaleParams", |
39 | 27x |
"rawBetaAlpha", "rawBetaBeta", "betaScaleParams", |
40 | 27x |
"trNormMeanParams", "trNormSdParams", "exponentialRateParams", |
41 | 27x |
"gammaAlphaParams", "gammaBetaParams")) { |
42 |
# Set zero as default for all prior distribution parameters |
|
43 | 324x |
d[[priorDistParam]] <- rep(0, length(params_nm)) |
44 |
} |
|
45 |
# Go through each param prior |
|
46 | 27x |
for (i in seq_along(params_nm)) { |
47 | 258x |
p <- priors_nm[["prior"]][[i]] |
48 | 258x |
stopifnot(p[["type"]] %in% names(priorTypes)) |
49 | 258x |
d[["mappingParamPriorType"]][i] <- priorTypes[p[["type"]]] |
50 | 258x |
d[["mappingParamPriorID"]][i] <- prior_i[p[["type"]]] |
51 | 258x |
prior_i[p[["type"]]] <- prior_i[p[["type"]]] + 1 |
52 | 258x |
if (p[["type"]] == "constant") { |
53 | 24x |
d[["constantParams"]][i] <- p[["parameters"]][["value"]] |
54 | 24x |
d[["nPriorConstant_code0"]] <- d[["nPriorConstant_code0"]] + 1 |
55 |
} |
|
56 | 258x |
if (p[["type"]] == "uniform") { |
57 | 2x |
d[["lowerParams"]][i] <- p[["parameters"]]["min"] |
58 | 2x |
d[["upperParams"]][i] <- p[["parameters"]]["max"] |
59 | 2x |
d[["nPriorUniform_code1"]] <- d[["nPriorUniform_code1"]] + 1 |
60 |
} |
|
61 | 258x |
if (p[["type"]] == "hcauchy") { |
62 | 62x |
d[["hcauchyScaleParams"]][i] <- p[["parameters"]]["scale"] |
63 | 62x |
d[["nPriorHcauchy_code2"]] <- d[["nPriorHcauchy_code2"]] + 1 |
64 |
} |
|
65 | 258x |
if (p[["type"]] == "scaled_beta") { |
66 | ! |
d[["rawBetaAlpha"]][i] <- p[["parameters"]]["alpha"] |
67 | ! |
d[["rawBetaBeta"]][i] <- p[["parameters"]]["beta"] |
68 | ! |
d[["betaScaleParams"]][i] <- p[["parameters"]]["scale"] |
69 | ! |
d[["nPriorBeta_code3"]] <- d[["nPriorBeta_code3"]] + 1 |
70 |
} |
|
71 | 258x |
if (p[["type"]] == "trun_normal") { |
72 | 166x |
d[["trNormMeanParams"]][i] <- p[["parameters"]][["mean"]] |
73 | 166x |
d[["trNormSdParams"]][i] <- p[["parameters"]][["sd"]] |
74 | 166x |
d[["nPriorTrNormal_code4"]] <- d[["nPriorTrNormal_code4"]] + 1 |
75 |
} |
|
76 | 258x |
if (p[["type"]] == "exponential") { |
77 | 2x |
d[["exponentialRateParams"]][i] <- p[["parameters"]][["lambda"]] |
78 | 2x |
d[["nPriorExponential_code5"]] <- d[["nPriorExponential_code5"]] + 1 |
79 |
} |
|
80 | 258x |
if (p[["type"]] == "gamma") { |
81 | 2x |
d[["gammaAlphaParams"]][i] <- p[["parameters"]][["alpha"]] |
82 | 2x |
d[["gammaBetaParams"]][i] <- p[["parameters"]][["beta"]] |
83 | 2x |
d[["nPriorGamma_code6"]] <- d[["nPriorGamma_code6"]] + 1 |
84 |
} |
|
85 |
} |
|
86 |
# Count non-constant priors |
|
87 | 27x |
d[["nNonConstantPriors"]] <- length(params_nm) - d[["nPriorConstant_code0"]] |
88 |
# Return |
|
89 | 27x |
return(d) |
90 |
} |
|
91 | ||
92 |
### * encode_steady() |
|
93 | ||
94 |
#' Encode steady compartments for stan data |
|
95 |
#' |
|
96 |
#' @param nm A \code{networkModel} object. |
|
97 |
#' |
|
98 |
#' @keywords internal |
|
99 |
#' @noRd |
|
100 | ||
101 |
encode_steady <- function(nm) { |
|
102 | 27x |
d <- list() |
103 | 27x |
steady <- lapply(nm[["topology"]], function(x) { |
104 | 40x |
match(attr(x, "steadyState"), colnames(x)) |
105 |
}) |
|
106 | 27x |
d[["maxNsteady"]] <- max(sapply(steady, length)) |
107 | 27x |
d[["nSteady"]] <- setNames(c(sapply(steady, length), 0), # Padded |
108 | 27x |
nm = c(paste0("grp", seq_len(nrow(nm))), "padding")) |
109 | 27x |
d[["steadyIndices"]] <- array(0, dim = c(d[["maxNsteady"]], nrow(nm)), |
110 | 27x |
dimnames = list(seq_len(d[["maxNsteady"]]), |
111 | 27x |
paste0("grp", seq_len(nrow(nm))))) |
112 | 27x |
for (i in seq_len(nrow(nm))) { |
113 | 40x |
d[["steadyIndices"]][seq_along(steady[[i]]),i] <- steady[[i]] |
114 |
} |
|
115 |
# Return |
|
116 | 27x |
return(d) |
117 |
} |
|
118 | ||
119 |
### * encode_split() |
|
120 | ||
121 |
#' Encode split compartments for stan data |
|
122 |
#' |
|
123 |
#' @param nm A \code{networkModel} object. |
|
124 |
#' @param allParams Parameters of the network model. |
|
125 |
#' |
|
126 |
#' @keywords internal |
|
127 |
#' @noRd |
|
128 | ||
129 |
encode_split <- function(nm, allParams) { |
|
130 | 27x |
d <- list() |
131 | 27x |
split <- lapply(nm[["topology"]], function(x) { |
132 | 40x |
match(attr(x, "split"), colnames(x)) |
133 |
}) |
|
134 | 27x |
d[["splitPresent"]] <- as.numeric(max(sapply(split, length)) > 0) |
135 | 27x |
nGroups <- nrow(nm) |
136 | 27x |
splitComps <- array(0, dim = c(length(comps(nm)[[1]]), nGroups), |
137 | 27x |
dimnames = list(seq_along(comps(nm)[[1]]), |
138 | 27x |
paste0("grp", seq_len(nrow(nm))))) |
139 | 27x |
for (i in seq_along(split)) { |
140 | 40x |
splitComps[split[[i]], i] <- 1 |
141 |
} |
|
142 | 27x |
d[["splitComps"]] <- splitComps |
143 |
# Parameter mapping |
|
144 | 27x |
piMapping <- array(0, dim = c(length(comps(nm)[[1]]), nGroups), |
145 | 27x |
dimnames = list(seq_along(comps(nm)[[1]]), |
146 | 27x |
paste0("grp", seq_len(nrow(nm))))) |
147 | 27x |
for (g in seq_len(nGroups)) { |
148 | 40x |
compNames <- colnames(nm$topology[[g]]) |
149 | 40x |
stopifnot(length(compNames) == length(comps(nm)[[1]])) |
150 | 40x |
for (k in seq_along(compNames)) { |
151 | 112x |
if (splitComps[k, g] > 0) { |
152 | 9x |
paramName <- paste0("portion.act_", compNames[k]) |
153 | 9x |
paramGlobal <- nm$parameters[[g]]$in_model[nm$parameters[[g]]$in_replicate == paramName] |
154 | 9x |
stopifnot(length(paramGlobal) == 1) |
155 | 9x |
piMapping[k, g] <- match(paramGlobal, allParams) |
156 |
} |
|
157 |
} |
|
158 |
} |
|
159 | 27x |
d[["piMapping"]] <- piMapping |
160 |
# Return |
|
161 | 27x |
return(d) |
162 |
} |
|
163 | ||
164 |
### * encode_init() |
|
165 | ||
166 |
#' Encode initial conditions for a network model |
|
167 |
#' |
|
168 |
#' For now this function assumes that each row has the same number of |
|
169 |
#' compartments. |
|
170 |
#' |
|
171 |
#' @param nm A \code{networkModel} object. |
|
172 |
#' |
|
173 |
#' @keywords internal |
|
174 |
#' @noRd |
|
175 | ||
176 |
encode_init <- function(nm) { |
|
177 | 27x |
nComps <- sapply(comps(nm), length) |
178 | 27x |
stopifnot(all(nComps == nComps[1])) |
179 | 27x |
nGroups <- nrow(nm) |
180 | 27x |
d <- array(0, dim = c(nComps[1], 2, nGroups), |
181 | 27x |
dimnames = list(1:nComps[1], c("unmarked", "marked"), |
182 | 27x |
c(paste0("grp", seq_len(nGroups))))) |
183 | 27x |
for (i in seq_len(nGroups)) { |
184 | 40x |
comps <- colnames(nm$topology[[i]]) |
185 | 40x |
stopifnot(nrow(nm$initial[[i]]) == length(comps)) |
186 | 40x |
stopifnot(setequal(nm$initial[[i]][["compartment"]], comps)) |
187 | 40x |
comps <- nm$initial[[i]][match(comps, nm$initial[[i]][["compartment"]]), ] |
188 | 40x |
stopifnot(all(comps$compartment == colnames(nm$topology[[i]]))) |
189 | 40x |
unmarked <- comps$size * (1 - comps$proportion) |
190 | 40x |
marked <- comps$size * comps$proportion |
191 | 40x |
d[,,i] <- cbind(unmarked, marked) |
192 |
} |
|
193 | 27x |
return(list(initialQuantities = d)) |
194 |
} |
|
195 | ||
196 |
### * encode_distrib_families() |
|
197 | ||
198 |
#' Encode the families for proportions and sizes distributions |
|
199 |
#' |
|
200 |
#' @param nm A \code{networkModel} object. |
|
201 |
#' |
|
202 |
#' @keywords internal |
|
203 |
#' @noRd |
|
204 | ||
205 |
encode_distrib_families <- function(nm) { |
|
206 | 27x |
o <- list() |
207 |
# Encode distribution family for proportions |
|
208 | 27x |
known_families <- c("gamma_cv" = 1, "normal_cv" = 2, "normal_sd" = 3, |
209 | 27x |
"beta_phi" = 4) |
210 | 27x |
prop_family <- attr(nm, "prop_family") |
211 | 27x |
if (!prop_family %in% names(known_families)) { |
212 | ! |
stop("Unknown distribution family for proportions. Got value: ", |
213 | ! |
prop_family, "\n", |
214 | ! |
"Allowed values are: ", names(known_families)) |
215 |
} |
|
216 | 27x |
o[["propFamily"]] <- known_families[prop_family] |
217 |
# Encode distribution family for sizes |
|
218 | 27x |
size_known_families <- c("normal_cv" = 1, "normal_sd" = 2) |
219 | 27x |
size_family <- attr(nm, "size_family") |
220 | 27x |
if (!size_family %in% names(size_known_families)) { |
221 | ! |
stop("Unknown distribution family for sizes. Got value: ", |
222 | ! |
size_family, "\n", |
223 | ! |
"Allowed values are: ", names(size_known_families)) |
224 |
} |
|
225 | 27x |
o[["sizeFamily"]] <- size_known_families[size_family] |
226 | 27x |
return(o) |
227 |
} |
|
228 | ||
229 |
### * encode_upsilons() |
|
230 | ||
231 |
#' Encode the uptake rates for stan data |
|
232 |
#' |
|
233 |
#' @param nm A \code{networkModel} object. |
|
234 |
#' @param allParams Parameters of the network model. |
|
235 |
#' |
|
236 |
#' @keywords internal |
|
237 |
#' @noRd |
|
238 | ||
239 |
encode_upsilons <- function(nm, allParams) { |
|
240 | 27x |
nGroups <- nrow(nm) |
241 | 27x |
upsilons <- nm_get_upsilons(nm, allParams) |
242 | 27x |
nUpsilons <- setNames(c(upsilons[["nUpsilons"]], 0), # Padding |
243 | 27x |
nm = c(paste0("grp", seq_len(nGroups)), "padding")) |
244 | 27x |
maxNupsilons <- max(nUpsilons) |
245 | 27x |
upsilonMapping <- array(0, dim = c(maxNupsilons, 3, nGroups), |
246 | 27x |
dimnames = list(1:maxNupsilons, |
247 | 27x |
c("from", "to", "param"), |
248 | 27x |
paste0("grp", seq_len(nGroups)))) |
249 | 27x |
for (i in seq_len(nGroups)) { |
250 | 40x |
upsilonMapping[1:nUpsilons[i], 1:3, i] <- as.matrix(upsilons[["upsilons"]][[i]]) |
251 |
} |
|
252 | 27x |
return(list(nUpsilons = nUpsilons, |
253 | 27x |
maxNupsilons = maxNupsilons, |
254 | 27x |
upsilonMapping = upsilonMapping)) |
255 |
} |
|
256 | ||
257 |
### ** nm_get_upsilons() |
|
258 | ||
259 |
#' Get upsilons for a network model |
|
260 |
#' |
|
261 |
#' @param nm A \code{networkModel} object. |
|
262 |
#' @param allParams Parameters of the network model. |
|
263 |
#' |
|
264 |
#' @keywords internal |
|
265 |
#' @noRd |
|
266 | ||
267 |
nm_get_upsilons <- function(nm, allParams) { |
|
268 | 27x |
upsilons <- lapply(seq_len(nrow(nm)), function(i) { |
269 | 40x |
z <- nm_row_get_upsilons(nm[i, ], allParams = allParams) |
270 | 40x |
tibble::tibble(upsilons = list(z), nUpsilons = nrow(z)) |
271 |
}) |
|
272 | 27x |
return(dplyr::bind_rows(upsilons)) |
273 |
} |
|
274 | ||
275 |
### ** nm_row_get_upsilons() |
|
276 | ||
277 |
#' Get upsilons for one row of a network model |
|
278 |
#' |
|
279 |
#' @param nm_row A row from a \code{networkModel} object. |
|
280 |
#' @param allParams Parameters of the network model. |
|
281 |
#' |
|
282 |
#' @keywords internal |
|
283 |
#' @noRd |
|
284 | ||
285 |
nm_row_get_upsilons <- function(nm_row, allParams) { |
|
286 | 40x |
nmRow <- nm_row |
287 | 40x |
stopifnot(nrow(nmRow) == 1) |
288 |
# Get data |
|
289 | 40x |
topo <- nmRow[["topology"]][[1]] |
290 | 40x |
mapping <- nmRow[["parameters"]][[1]][, c("in_replicate", "in_model")] |
291 | 40x |
mapping <- tibble::deframe(mapping) |
292 |
# Build output |
|
293 | 40x |
from <- vector() |
294 | 40x |
to <- vector() |
295 | 40x |
param <- vector() |
296 | 40x |
compartments <- colnames(topo) |
297 | 40x |
stopifnot(all(compartments == rownames(topo))) |
298 | 40x |
for (j in seq_len(ncol(topo))) { |
299 | 112x |
for (i in seq_len(nrow(topo))) { |
300 | 320x |
if (topo[i,j] == 1) { |
301 | 100x |
from <- c(from, j) |
302 | 100x |
to <- c(to, i) |
303 | 100x |
paramName <- paste0("upsilon_", compartments[j], "_to_", |
304 | 100x |
compartments[i]) |
305 | 100x |
param <- c(param, match(mapping[paramName], allParams)) |
306 |
} |
|
307 |
} |
|
308 |
} |
|
309 | 40x |
return(tibble::tibble(from = from, to = to, param = param)) |
310 |
} |
|
311 | ||
312 |
### * encode_lambdas() |
|
313 | ||
314 |
#' Encode the loss rates for stan data |
|
315 |
#' |
|
316 |
#' @param nm A \code{networkModel} object. |
|
317 |
#' @param allParams Parameters of the network model. |
|
318 |
#' |
|
319 |
#' @keywords internal |
|
320 |
#' @noRd |
|
321 | ||
322 |
encode_lambdas <- function(nm, allParams) { |
|
323 | 27x |
nGroups <- nrow(nm) |
324 | 27x |
lambdas <- nm_get_lambdas(nm, allParams) |
325 | 27x |
nLambdas <- setNames(c(lambdas[["nLambdas"]], 0), # Padding |
326 | 27x |
nm = c(paste0("grp", seq_len(nGroups)), "padding")) |
327 | 27x |
maxNlambdas <- max(nLambdas) |
328 | 27x |
lambdaMapping <- array(0, dim = c(maxNlambdas, 2, nGroups), |
329 | 27x |
dimnames = list(1:maxNlambdas, |
330 | 27x |
c("from", "param"), |
331 | 27x |
paste0("grp", seq_len(nGroups)))) |
332 | 27x |
for (i in seq_len(nGroups)) { |
333 | 40x |
lambdaMapping[1:nLambdas[i], 1:2, i] <- as.matrix(lambdas[["lambdas"]][[i]]) |
334 |
} |
|
335 | 27x |
return(list(nLambdas = nLambdas, |
336 | 27x |
maxNlambdas = maxNlambdas, |
337 | 27x |
lambdaMapping = lambdaMapping)) |
338 |
} |
|
339 | ||
340 |
### ** nm_get_lambdas() |
|
341 | ||
342 |
#' Get lambdas for a network model |
|
343 |
#' |
|
344 |
#' @param nm A \code{networkModel} object. |
|
345 |
#' @param allParams Parameters of the network model. |
|
346 |
#' |
|
347 |
#' @keywords internal |
|
348 |
#' @noRd |
|
349 | ||
350 |
nm_get_lambdas <- function(nm, allParams) { |
|
351 | 27x |
lambdas <- lapply(seq_len(nrow(nm)), function(i) { |
352 | 40x |
z <- nm_row_get_lambdas(nm[i, ], allParams = allParams) |
353 | 40x |
tibble::tibble(lambdas = list(z), nLambdas = nrow(z)) |
354 |
}) |
|
355 | 27x |
return(dplyr::bind_rows(lambdas)) |
356 |
} |
|
357 | ||
358 |
### ** nm_row_get_lambdas() |
|
359 | ||
360 |
#' Get lambdas for one row of a network model |
|
361 |
#' |
|
362 |
#' @param nm_row A row from a \code{networkModel} object. |
|
363 |
#' @param allParams Parameters of the network model. |
|
364 |
#' |
|
365 |
#' @keywords internal |
|
366 |
#' @noRd |
|
367 | ||
368 |
nm_row_get_lambdas <- function(nm_row, allParams) { |
|
369 | 40x |
nmRow <- nm_row |
370 | 40x |
stopifnot(nrow(nmRow) == 1) |
371 |
# Get data |
|
372 | 40x |
topo <- nmRow[["topology"]][[1]] |
373 | 40x |
mapping <- nmRow[["parameters"]][[1]][, c("in_replicate", "in_model")] |
374 | 40x |
mapping <- tibble::deframe(mapping) |
375 |
# Build output |
|
376 | 40x |
from <- vector() |
377 | 40x |
param <- vector() |
378 | 40x |
compartments <- colnames(topo) |
379 | 40x |
stopifnot(all(compartments == rownames(topo))) |
380 | 40x |
for (j in seq_len(ncol(topo))) { |
381 | 112x |
paramName <- paste0("lambda_", compartments[j]) |
382 | 112x |
if (paramName %in% names(mapping)) { |
383 | 112x |
from <- c(from, j) |
384 | 112x |
param <- c(param, match(mapping[paramName], allParams)) |
385 |
} |
|
386 |
} |
|
387 | 40x |
return(tibble::tibble(from = from, param = param)) |
388 |
} |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * run_mcmc() |
|
4 | ||
5 |
### ** Doc |
|
6 | ||
7 |
#' Run a MCMC sampler on a network model using Stan |
|
8 |
#' |
|
9 |
#' @param model A \code{networkModel}. |
|
10 |
#' @param iter A positive integer specifying the number of iterations for each |
|
11 |
#' chain (including warmup). The default is 2000. |
|
12 |
#' @param chains A positive integer specifying the number of Markov chains. |
|
13 |
#' The default is 4. |
|
14 |
#' @param method A character string indicating the method to use to solve ODE |
|
15 |
#' in the Stan model; available methods are "matrix_exp" and "euler". The |
|
16 |
#' default is "matrix_exp", which uses matrix exponential and is reasonably |
|
17 |
#' fast for small networks. For large networks, the "euler" method can be |
|
18 |
#' used. It implements a simple forward Euler method to solve the ODE and can |
|
19 |
#' be faster than the matrix exponential approach, but extra caution must be |
|
20 |
#' taken to check for numerical accuracy (e.g. testing different \code{dt} |
|
21 |
#' time step values, ensuring that the product between \code{dt} and the |
|
22 |
#' largest transfer rates expected from the priors is always very small |
|
23 |
#' compared to 1). |
|
24 |
#' @param euler_control An optional list containing extra parameters when using |
|
25 |
#' \code{method = "euler"}. Allowed list elements are \code{"dt"} and |
|
26 |
#' \code{"grid_size"}, which are respectively the time step size for |
|
27 |
#' trajectory calculations (\code{"dt"}) or the number of points for the |
|
28 |
#' calculation (\code{"grid_size"}). Only one of "dt" or "grid_size" can be |
|
29 |
#' specified, not both. If none is provided, a default grid size of 256 steps |
|
30 |
#' is used. |
|
31 |
#' @param cores Number of cores to use for parallel use. Default is |
|
32 |
#' \code{NULL}, which means to use the value stored in |
|
33 |
#' \code{options()[["mc.cores"]]} (or 1 if this value is not set). |
|
34 |
#' @param stanfit If TRUE, returns a `stanfit` object instead of the more |
|
35 |
#' classical `mcmc.list` object. Note that when an `mcmc.list` object is |
|
36 |
#' returned, the original `stanfit` object is still accessible as an |
|
37 |
#' attribute of that object (see Examples). |
|
38 |
#' @param vb Boolean, if TRUE will use \code{rstan::vb} for a quick approximate |
|
39 |
#' sampling of the posterior. Important note from \code{?rstan::vb}: |
|
40 |
#' "This is still considered an experimental feature. We recommend calling |
|
41 |
#' \code{stan} or \code{sampling} for final inferences and only using ‘vb’ to |
|
42 |
#' get a rough idea of the parameter distributions." |
|
43 |
#' @param ... Arguments passed to `rstan::sampling` (e.g. iter, chains). |
|
44 |
#' |
|
45 |
#' @return An object of class `stanfit` returned by `rstan::sampling` if |
|
46 |
#' \code{stanfit = TRUE}, otherwise the result of converting this |
|
47 |
#' \code{stanfit} object with \code{stanfit_to_named_mcmclist} (i.e. an object |
|
48 |
#' of class \code{networkModelStanfit} and \code{mcmc.list}, which still |
|
49 |
#' carries the original `stanfit` object stored as an attribute). |
|
50 |
#' |
|
51 |
#' @examples |
|
52 |
#' aquarium_mod |
|
53 |
#' \dontrun{ |
|
54 |
#' # The 'aquarium_run' object is shipped with the package, so you don't |
|
55 |
#' # actually need to run the line below to obtain it |
|
56 |
#' aquarium_run <- run_mcmc(aquarium_mod) |
|
57 |
#' |
|
58 |
#' plot(aquarium_run) |
|
59 |
#' summary(aquarium_run) |
|
60 |
#' |
|
61 |
#' # The original stanfit object returned by Stan |
|
62 |
#' sfit <- attr(aquarium_run, "stanfit") |
|
63 |
#' sfit |
|
64 |
#' |
|
65 |
#' # The stanfit object can be used for diagnostics, LOO cross-validation, etc. |
|
66 |
#' rstan::loo(sfit) |
|
67 |
#' } |
|
68 |
#' |
|
69 |
#' @export |
|
70 | ||
71 |
### ** Code |
|
72 | ||
73 |
run_mcmc <- function(model, iter = 2000, chains = 4, method = "matrix_exp", |
|
74 |
euler_control = list(), |
|
75 |
cores = NULL, stanfit = FALSE, vb = FALSE, ...) { |
|
76 | 27x |
stopifnot(method %in% c("matrix_exp", "euler")) |
77 | 27x |
if (method != "euler" & length(euler_control) != 0) { |
78 | ! |
stop("The `euler_control` parameter is not empty, but `method` is not set to \"euler\".") |
79 |
} |
|
80 | 27x |
if (method == "euler") { |
81 | 8x |
if (vb) { |
82 | ! |
stop("vb not implemented for euler method") |
83 |
} |
|
84 | 8x |
fit <- mugen_stan(nm = model, iter = iter, chains = chains, |
85 | 8x |
euler_control = euler_control, |
86 | 8x |
cores = cores, stanfit = stanfit, ...) |
87 |
} |
|
88 | 27x |
if (method == "matrix_exp") { |
89 | 19x |
fit <- matrix_exp_stan(nm = model, iter = iter, chains = chains, |
90 | 19x |
cores = cores, stanfit = stanfit, |
91 | 19x |
vb = vb, ...) |
92 |
} |
|
93 | 27x |
return(fit) |
94 |
} |
|
95 | ||
96 |
### * stanfit_to_named_mcmclist() |
|
97 | ||
98 |
#' Convert a Stanfit object to a nicely named mcmc.list object |
|
99 |
#' |
|
100 |
#' When running \code{run_mcmc} with \code{stanfit = FALSE} (typically for |
|
101 |
#' debugging purposes), the parameters in the returned \code{stanfit} object |
|
102 |
#' are named using a base label and an indexing system. This function provides |
|
103 |
#' a way to convert this \code{stanfit} object into a more conventional |
|
104 |
#' \code{mcmc.list} object where parameters are named according to their role |
|
105 |
#' in the original network model used when running \code{run_mcmc}. |
|
106 |
#' |
|
107 |
#' @param stanfit A stanfit object returned by \code{rstan::sampling}. |
|
108 |
#' |
|
109 |
#' @return An \code{mcmc.list} object. It also has the original stanfit object |
|
110 |
#' stored as an attribute \code{"stanfit"}. |
|
111 |
#' |
|
112 |
#' @export |
|
113 | ||
114 |
stanfit_to_named_mcmclist <- function(stanfit) { |
|
115 | 26x |
stan_data <- attr(stanfit, "isotracer_stan_data") |
116 | 26x |
fit <- stanfit |
117 |
# Get mcpars |
|
118 | 26x |
start <- fit@sim[["warmup"]] + 1 |
119 | 26x |
end <- fit@sim[["iter"]] |
120 | 26x |
thin <- fit@sim[["thin"]] |
121 | 26x |
n_kept <- fit@sim[["n_save"]] - fit@sim[["warmup2"]] |
122 | 26x |
mcpars <- c(start, end, thin) |
123 |
# Prepare the mcmc.list object |
|
124 | 26x |
out <- rstan::As.mcmc.list(fit) |
125 | 26x |
for (i in seq_along(out)) { |
126 | 52x |
stopifnot(nrow(out[[i]]) == n_kept) |
127 |
} |
|
128 | 26x |
attr(out, "mcpar") <- mcpars |
129 | 26x |
rawNames <- coda::varnames(out) |
130 | 26x |
nonConstantParamNames <- rawNames[grepl("^nonConstantParams[\\[\\.]", |
131 | 26x |
rawNames)] |
132 | 26x |
loglikNames <- rawNames[grepl("^log_lik[\\[\\.]", rawNames)] |
133 | 26x |
ll <- out[, loglikNames] |
134 | 26x |
out <- out[, nonConstantParamNames] |
135 | 26x |
coda::varnames(out) <- stan_data[["allParams"]][stan_data[["mappingParamPriorType"]] != 0] |
136 | 26x |
llTrace <- lapply(ll, function(x) { |
137 | 52x |
out <- coda::as.mcmc(apply(as.matrix(x), 1, sum)) |
138 | 52x |
attr(out, "mcpar") <- attr(x, "mcpar") |
139 | 52x |
return(out) |
140 |
}) |
|
141 | 26x |
llTrace <- coda::as.mcmc.list(llTrace) |
142 | 26x |
attr(out, "loglik") <- llTrace |
143 | 26x |
attr(out, "mcpar") <- mcpars |
144 |
# Add values of constant parameters (if any) |
|
145 | 26x |
n_constant_params <- sum(stan_data[["mappingParamPriorType"]] == 0) |
146 | 26x |
if (n_constant_params > 0) { |
147 | 5x |
constant_params <- stan_data[["allParams"]][stan_data[["mappingParamPriorType"]] == 0] |
148 | 5x |
constant_values <- stan_data[["constantParams"]][stan_data[["mappingParamPriorType"]] == 0] |
149 | 5x |
constant_params <- setNames(constant_values, nm = constant_params) |
150 | 5x |
attr(out, "constant_params") <- constant_params |
151 |
} |
|
152 |
# Return the mcmc.list object |
|
153 | 26x |
outClass <- c("networkModelStanfit", class(out)) |
154 | 26x |
attr(out, "stanfit") <- stanfit |
155 | 26x |
return(structure(out, class = outClass)) |
156 |
} |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * delta2prop() |
|
4 | ||
5 |
#' Convert delta notation to proportion of heavy isotope |
|
6 |
#' |
|
7 |
#' For details and references about quantities used in expressing isotopic |
|
8 |
#' ratios, see: |
|
9 |
#' |
|
10 |
#' - Figure 1 in Coplen, Tyler B. “Guidelines and Recommended Terms for |
|
11 |
#' Expression of Stable-Isotope-Ratio and Gas-Ratio Measurement Results.” Rapid |
|
12 |
#' Communications in Mass Spectrometry 25, no. 17 (September 15, 2011): |
|
13 |
#' 2538–60. https://doi.org/10.1002/rcm.5129. |
|
14 |
#' |
|
15 |
#' - Table 2.1 in Fry, Brian. Stable Isotope Ecology. New York: |
|
16 |
#' Springer-Verlag, 2006. //www.springer.com/gp/book/9780387305134. |
|
17 |
#' |
|
18 |
#' @section Ratios for reference standards: |
|
19 |
#' |
|
20 |
#' The ratios for reference standards are taken from the Table 2.1 from Fry |
|
21 |
#' 2006. Note that the values used for oxygen isotopes are from the standard |
|
22 |
#' mean ocean water (SMOW). |
|
23 |
#' |
|
24 |
#' Standards recognized by this function are: \code{c("d15N", "d2H", "d13C", |
|
25 |
#' "d17O.SMOW", "d18O.SMOW", "d33S", "d34S", "d36S")} |
|
26 |
#' |
|
27 |
#' @param x Vector of delta values. |
|
28 |
#' @param Rstandard String describing the isotopic measurement, e.g. "d15N", |
|
29 |
#' "d13C" and used to set automatically Rstandards (see the Section |
|
30 |
#' "Ratios for reference standards" for more details). Alternatively, a |
|
31 |
#' numeric value to use for Rstandard, e.g. 0.0036765. |
|
32 |
#' |
|
33 |
#' @return A vector of same length of x, containing the proportion (numeric |
|
34 |
#' between 0 and 1) of heavy isotope based on the delta values and the |
|
35 |
#' Rstandard provided. |
|
36 |
#' |
|
37 |
#' @examples |
|
38 |
#' deltas <- c(78, 5180, 263, 1065, NA, 153, 345) |
|
39 |
#' |
|
40 |
#' # Rstandard can be specified with a string for some preset references |
|
41 |
#' prop15N <- delta2prop(deltas, "d15N") |
|
42 |
#' prop13C <- delta2prop(deltas, "d13C") |
|
43 |
#' |
|
44 |
#' # Rstandard can also be specified manually for non-preset references |
|
45 |
#' prop15N_manual <- delta2prop(deltas, 0.0036765) |
|
46 |
#' prop13C_manual <- delta2prop(deltas, 0.011180) |
|
47 |
#' |
|
48 |
#' # Call delta2prop() to get the detail of available references |
|
49 |
#' delta2prop() |
|
50 |
#' |
|
51 |
#' @export |
|
52 |
#' |
|
53 | ||
54 |
delta2prop <- function(x = NULL, Rstandard = NULL) { |
|
55 |
# Known standards |
|
56 | 10x |
Rstandards <- list("d15N" = 0.0036765, |
57 | 10x |
"d2H" = 0.00015576, |
58 | 10x |
"d13C" = 0.011180, |
59 | 10x |
"d17O.SMOW" = 0.0003799, |
60 | 10x |
"d18O.SMOW" = 0.0020052, |
61 | 10x |
"d33S" = 0.0078772, |
62 | 10x |
"d34S" = 0.0441626, |
63 | 10x |
"d36S" = 0.0001533) |
64 |
# Message about available standards |
|
65 | 10x |
msg <- paste0("Available standards are:") |
66 | 10x |
for (i in seq_along(Rstandards)) { |
67 | 80x |
label <- names(Rstandards)[i] |
68 | 80x |
string <- paste(label, paste(rep("_", 13 - nchar(label)), collapse = "")) |
69 | 80x |
value <- Rstandards[[i]] |
70 | 80x |
msg <- paste(msg, paste(string, value, collapse = ""), sep = "\n ") |
71 |
} |
|
72 |
# Parse argument |
|
73 | 10x |
if (is.null(Rstandard)) { |
74 | 2x |
Rstandard <- "non-specified" |
75 | 2x |
if (is.null(x)) { |
76 | 2x |
message(paste(msg, sep = "\n")) |
77 | 2x |
return(invisible(NULL)) |
78 |
} |
|
79 |
} |
|
80 | 8x |
if (length(Rstandard) > 1) { |
81 | 3x |
msg <- paste0( |
82 | 3x |
"The Rstandard argument must be of length 1 but the argument provided was ", |
83 | 3x |
ifelse(is.numeric(Rstandard), "a numeric vector", "an object"), |
84 | 3x |
" of length ", length(Rstandard), ".") |
85 | 3x |
if (is.numeric(Rstandard)) { |
86 | 2x |
msg <- paste0( |
87 | 2x |
msg, "\n", |
88 | 2x |
"If you did not want to pass a numeric vector but rather wanted to ", |
89 | 2x |
"pass a string defining the standard to use (such as \"d15N\"), maybe ", |
90 | 2x |
"you forgot the quotes?") |
91 |
} |
|
92 | 3x |
stop(msg) |
93 |
} |
|
94 | 5x |
if (is.character(Rstandard)) { |
95 | 3x |
if (!Rstandard %in% names(Rstandards)) { |
96 | 1x |
unk_std <- paste0("Rstandard argument (", Rstandard, "): unknown. ", |
97 | 1x |
collapse = "") |
98 | 1x |
msg <- paste0(unk_std, msg, collapse = "") |
99 | 1x |
stop(msg) |
100 |
} |
|
101 | 2x |
Rstandard <- Rstandards[[Rstandard]] |
102 | 2x |
} else if (!is.numeric(Rstandard)) { |
103 | ! |
stop("Provided value must the name of a known measurement type or a numeric.") |
104 |
} |
|
105 |
# Calculate proportions and return |
|
106 | 4x |
R0 <- Rstandard |
107 | 4x |
props <- R0 * (x/1000 + 1) / (R0 * (x/1000 + 1) + 1) |
108 | 4x |
return(props) |
109 |
} |
|
110 | ||
111 |
### * prop2delta() |
|
112 | ||
113 |
#' Convert isotopic proportions to delta values |
|
114 |
#' |
|
115 |
#' This function performs the inverse of the operation performed by |
|
116 |
#' \code{delta2prop()}. |
|
117 |
#' |
|
118 |
#' @param x Vector of proportions values. |
|
119 |
#' @param Rstandard String describing the isotopic measurement, e.g. "d15N", |
|
120 |
#' "d13C" and used to set automatically Rstandards (see the Section |
|
121 |
#' "Ratios for reference standards" for more details). Alternatively, a |
|
122 |
#' numeric value to use for Rstandard, e.g. 0.0036765. |
|
123 |
#' |
|
124 |
#' @return A vector of same length of x, containing the delta values based on |
|
125 |
#' the proportions of heavy isotope provided as x and the Rstandard provided. |
|
126 |
#' |
|
127 |
#' @examples |
|
128 |
#' prop15N <- c(0.00395, 0.02222, 0.00462, 0.00753, NA, 0.00422, 0.00492) |
|
129 |
#' |
|
130 |
#' # Rstandard can be specified with a string for some preset references |
|
131 |
#' d15N <- prop2delta(prop15N, "d15N") |
|
132 |
#' d15N |
|
133 |
#' |
|
134 |
#' # Rstandard can also be specified manually for non-preset references |
|
135 |
#' d15N_manual <- prop2delta(prop15N, 0.0036765) |
|
136 |
#' d15N_manual |
|
137 |
#' |
|
138 |
#' # Call delta2prop() to get the detail of available references |
|
139 |
#' delta2prop() |
|
140 |
#' |
|
141 |
#' @export |
|
142 | ||
143 |
prop2delta <- function(x = NULL, Rstandard = NULL) { |
|
144 |
# Known standards |
|
145 | ! |
Rstandards <- list("d15N" = 0.0036765, |
146 | ! |
"d2H" = 0.00015576, |
147 | ! |
"d13C" = 0.011180, |
148 | ! |
"d17O.SMOW" = 0.0003799, |
149 | ! |
"d18O.SMOW" = 0.0020052, |
150 | ! |
"d33S" = 0.0078772, |
151 | ! |
"d34S" = 0.0441626, |
152 | ! |
"d36S" = 0.0001533) |
153 |
# Message about available standards |
|
154 | ! |
msg <- paste0("Available standards are:") |
155 | ! |
for (i in seq_along(Rstandards)) { |
156 | ! |
label <- names(Rstandards)[i] |
157 | ! |
string <- paste(label, paste(rep("_", 13 - nchar(label)), collapse = "")) |
158 | ! |
value <- Rstandards[[i]] |
159 | ! |
msg <- paste(msg, paste(string, value, collapse = ""), sep = "\n ") |
160 |
} |
|
161 |
# Parse argument |
|
162 | ! |
if (is.null(Rstandard)) { |
163 | ! |
Rstandard <- "non-specified" |
164 | ! |
if (is.null(x)) { |
165 | ! |
message(paste(msg, sep = "\n")) |
166 | ! |
return(invisible(NULL)) |
167 |
} |
|
168 |
} |
|
169 | ! |
if (length(Rstandard) > 1) { |
170 | ! |
msg <- paste0( |
171 | ! |
"The Rstandard argument must be of length 1 but the argument provided was ", |
172 | ! |
ifelse(is.numeric(Rstandard), "a numeric vector", "an object"), |
173 | ! |
" of length ", length(Rstandard), ".") |
174 | ! |
if (is.numeric(Rstandard)) { |
175 | ! |
msg <- paste0( |
176 | ! |
msg, "\n", |
177 | ! |
"If you did not want to pass a numeric vector but rather wanted to ", |
178 | ! |
"pass a string defining the standard to use (such as \"d15N\"), maybe ", |
179 | ! |
"you forgot the quotes?") |
180 |
} |
|
181 | ! |
stop(msg) |
182 |
} |
|
183 | ! |
if (is.character(Rstandard)) { |
184 | ! |
if (!Rstandard %in% names(Rstandards)) { |
185 | ! |
unk_std <- paste0("Rstandard argument (", Rstandard, "): unknown. ", |
186 | ! |
collapse = "") |
187 | ! |
msg <- paste0(unk_std, msg, collapse = "") |
188 | ! |
stop(msg) |
189 |
} |
|
190 | ! |
Rstandard <- Rstandards[[Rstandard]] |
191 | ! |
} else if (!is.numeric(Rstandard)) { |
192 | ! |
stop("Provided value must the name of a known measurement type or a numeric.") |
193 |
} |
|
194 |
# Calculate deltas and return |
|
195 | ! |
R0 <- Rstandard |
196 | ! |
deltas <- (1/R0 * x / (1 - x) - 1) * 1000 |
197 | ! |
return(deltas) |
198 |
} |
|
199 | ||
200 |
### * filter_by_group |
|
201 | ||
202 |
#' Filter a tibble based on the "group" column |
|
203 |
#' |
|
204 |
#' This function can be used to filter any tibble (e.g. network model object) |
|
205 |
#' that has a "group" column. See the Examples for more details and syntax. |
|
206 |
#' |
|
207 |
#' @param .data A tibble that has a `group` column, such as a `networkModel` |
|
208 |
#' object. |
|
209 |
#' @param ... Conditional expressions for filtering (see the Examples). |
|
210 |
#' |
|
211 |
#' @return A tibble similar to the input object, but with rows filtered based |
|
212 |
#' on \code{...}. |
|
213 |
#' |
|
214 |
#' @examples |
|
215 |
#' trini_mod |
|
216 |
#' trini_mod$group |
|
217 |
#' groups(trini_mod) |
|
218 |
#' filter_by_group(trini_mod, stream == "LL", transect == "transect.1") |
|
219 |
#' filter_by_group(trini_mod, transect == "transect.1") |
|
220 |
#' \dontrun{ |
|
221 |
#' # The code below would raise an error because there is no "color" grouping variable. |
|
222 |
#' filter_by_group(trini_mod, color == "red") |
|
223 |
#' } |
|
224 |
#' |
|
225 |
#' @export |
|
226 |
#' |
|
227 | ||
228 |
filter_by_group <- function(.data, ...) { |
|
229 | ! |
if (dplyr::is_grouped_df(.data)) { |
230 | ! |
warning("\"groups\" attribute is not kept in returned object.") |
231 |
} |
|
232 | ! |
is_grouped <- TRUE |
233 | ! |
if (!"group" %in% colnames(.data)) { |
234 | ! |
is_grouped <- FALSE |
235 |
} |
|
236 | ! |
if (nrow(.data) == 0 | |
237 | ! |
(nrow(.data) == 1 && is.null(.data$group[[1]]))) { |
238 | ! |
is_grouped <- FALSE |
239 |
} |
|
240 | ! |
if (!is_grouped) { |
241 | ! |
stop("Input data does not have a valid \"group\" column.") |
242 |
} |
|
243 | ! |
if (!length(unique(lapply(.data$group, names))) == 1) { |
244 | ! |
stop("Variable names are not consistent in \"group\" column.") |
245 |
} |
|
246 | ! |
grp <- tibble::as_tibble(do.call(rbind, .data$group)) |
247 | ! |
if (".my_index" %in% names(grp)) { |
248 | ! |
stop("\"group\" column already has the reserved \".my_index\" field.") |
249 |
} |
|
250 | ! |
grp$.my_index <- seq_len(nrow(grp)) |
251 | ! |
filtered <- dplyr::filter(grp, ...) |
252 | ! |
return(.data[filtered$.my_index, ]) |
253 |
} |
|
254 | ||
255 |
### * dic() |
|
256 | ||
257 |
#' Calculate DIC from a model output |
|
258 |
#' |
|
259 |
#' Note that DIC might not be indicated for network models, as the posteriors |
|
260 |
#' are often not multinormal distributions. |
|
261 |
#' |
|
262 |
#' LOO is probably not a good choice either since the data is akin to a time |
|
263 |
#' series (so data points are not independent). Maybe WAIC could be an option? |
|
264 |
#' (TODO: read about this.) |
|
265 |
#' |
|
266 |
#' DIC is calculated as: |
|
267 |
#' |
|
268 |
#' DIC = Dbar + pD |
|
269 |
#' |
|
270 |
#' where D are deviance values calculated as -2 * loglik for each MCMC |
|
271 |
#' iteration, Dbar is the mean deviance value and pD is the effective number of |
|
272 |
#' parameters in the model and can be calculated as var(D)/2 (Gelman 2003). |
|
273 |
#' |
|
274 |
#' @param ... One or several \code{mcmc.list} objects, output(s) from |
|
275 |
#' \code{\link{run_mcmc}}. |
|
276 |
#' @param weight Boolean, if TRUE calculate DIC weights based on Link and |
|
277 |
#' Barker 2010 (Link, W. A., and R. J. Barker. 2010. Bayesian Inference |
|
278 |
#' With Ecological Applications. Amsterdam Boston Heidelberg London: |
|
279 |
#' Elsevier/Academic Press). |
|
280 |
#' |
|
281 |
#' @return A tibble with one row per \code{mcmc.list} object provided in |
|
282 |
#' \code{...}. This tibble is sorted by DIC, so the row order might be |
|
283 |
#' different from the \code{mcmc.list} objects order. |
|
284 |
#' |
|
285 |
#' @examples |
|
286 |
#' \donttest{ |
|
287 |
#' # Define two different models |
|
288 |
#' m1 <- aquarium_mod |
|
289 |
#' m2 <- set_topo(m1, c("NH4 -> algae -> daphnia -> NH4", "algae -> NH4")) |
|
290 |
#' m2 <- set_priors(m2, priors(m1)) |
|
291 |
#' m2 <- set_priors(m2, normal_p(0, 0.5), "upsilon_algae_to_NH4") |
|
292 |
#' # Run the models |
|
293 |
#' r1 <- run_mcmc(m1, chains = 2) |
|
294 |
#' r2 <- run_mcmc(m2, chains = 2) |
|
295 |
#' # Model comparison with DIC |
|
296 |
#' dic(r1, r2) |
|
297 |
#' } |
|
298 |
#' |
|
299 |
#' @export |
|
300 |
#' |
|
301 | ||
302 |
# TODO add formula for DIC calculation in function doc. |
|
303 | ||
304 |
dic <- function(..., weight = TRUE) { |
|
305 |
# Deparse based on https://stackoverflow.com/questions/51259346/how-to-get-names-of-dot-dot-dot-arguments-in-r |
|
306 |
# (but I don't understand how it works) |
|
307 | ! |
names <- sapply(substitute(list(...))[-1], deparse) |
308 | ! |
logliks <- lapply(list(...), function(x) attr(x, "loglik")) |
309 | ! |
if (any(sapply(logliks, is.null))) { |
310 | ! |
stop("No \"loglik\" attribute found for at least one input object.") |
311 |
} |
|
312 | ! |
logliks <- lapply(logliks, unlist) |
313 | ! |
D <- lapply(logliks, function(x) -2 * x) |
314 | ! |
Dbar <- sapply(D, mean) |
315 | ! |
pD <- sapply(D, function(x) var(x) / 2) |
316 | ! |
out <- tibble::tibble(fit = names, |
317 | ! |
Dbar = Dbar, |
318 | ! |
pD = pD) |
319 | ! |
out[["DIC"]] <- out$Dbar + out$pD |
320 | ! |
min_DIC <- min(out[["DIC"]]) |
321 | ! |
out[["delta_DIC"]] <- out[["DIC"]] - min_DIC |
322 | ! |
num <- exp(- out[["delta_DIC"]] / 2) |
323 | ! |
denom <- sum(num) |
324 | ! |
out[["weight"]] <- num / denom |
325 | ! |
out <- out[order(out$delta_DIC), ] |
326 | ! |
return(out) |
327 |
} |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * ggtopo (generic) |
|
4 | ||
5 |
#' Plot a topology |
|
6 |
#' |
|
7 |
#' A quick plot using ggraph |
|
8 |
#' |
|
9 |
#' @param x A network model or a topology matrix. |
|
10 |
#' @param layout Optional, layout to use (e.g. "sugiyama", "kk", "stress") |
|
11 |
#' @param edge "fan" (the default) or "line" or "curve". |
|
12 |
#' @param ... Passed to the methods. |
|
13 |
#' |
|
14 |
#' @return A ggplot2 plot. |
|
15 |
#' |
|
16 |
#' @examples |
|
17 |
#' if (requireNamespace("ggraph")) { |
|
18 |
#' ggtopo(aquarium_mod, edge = "line") |
|
19 |
#' } |
|
20 |
#' |
|
21 |
#' @export |
|
22 | ||
23 |
ggtopo <- function(x, layout = "auto", edge = "fan", ...) { |
|
24 | 6x |
UseMethod("ggtopo") |
25 |
} |
|
26 | ||
27 |
### * ggtopo.networkModel |
|
28 | ||
29 |
#' Plot a network topology |
|
30 |
#' |
|
31 |
#' A quick plot using ggraph |
|
32 |
#' |
|
33 |
#' @param x A topology matrix. |
|
34 |
#' @param layout Optional, layout to use (e.g. "sugiyama", "kk", "stress") |
|
35 |
#' @param edge "curve" (the default) or "line". |
|
36 |
#' @param ... Not used for now. |
|
37 |
#' |
|
38 |
#' @return A ggplot2 plot. |
|
39 |
#' |
|
40 |
#' @examples |
|
41 |
#' if (requireNamespace("ggraph")) { |
|
42 |
#' ggtopo(aquarium_mod, edge = "line") |
|
43 |
#' ggtopo(trini_mod) |
|
44 |
#' } |
|
45 |
#' |
|
46 |
#' @export |
|
47 | ||
48 |
ggtopo.networkModel <- function(x, layout = "auto", edge = "fan", ...) { |
|
49 | 3x |
topos <- topo(x, simplify = FALSE) |
50 | 3x |
if (length(unique(topos)) > 1) { |
51 | ! |
message("Plotting the topology of the first row of the networkModel.") |
52 |
} |
|
53 | 3x |
topo <- topos[[1]] |
54 | 3x |
ggtopo(topo, layout = layout, edge = edge) |
55 |
} |
|
56 | ||
57 |
### * ggtopo.topology |
|
58 | ||
59 |
#' Plot a topology |
|
60 |
#' |
|
61 |
#' A quick plot using ggraph |
|
62 |
#' |
|
63 |
#' @param x A topology matrix. |
|
64 |
#' @param layout Optional, layout to use (e.g. "sugiyama", "kk", "stress") |
|
65 |
#' @param edge "curve" (the default), "line" or "fan". |
|
66 |
#' @param ... Not used for now. |
|
67 |
#' |
|
68 |
#' @return A ggplot2 plot. |
|
69 |
#' |
|
70 |
#' @examples |
|
71 |
#' if (requireNamespace("ggraph")) { |
|
72 |
#' z <- topo(aquarium_mod) |
|
73 |
#' ggtopo(z) |
|
74 |
#' ggtopo(z, edge = "line") |
|
75 |
#' |
|
76 |
#' z <- topo(trini_mod) |
|
77 |
#' ggtopo(z) |
|
78 |
#' |
|
79 |
#' # For finer control, one can build a tbl_graph from the topology and |
|
80 |
#' # use ggraph directly |
|
81 |
#' x <- as_tbl_graph(z) |
|
82 |
#' library(ggraph) |
|
83 |
#' ggraph(x) + geom_edge_link() |
|
84 |
#' } |
|
85 |
#' |
|
86 |
#' @export |
|
87 | ||
88 |
ggtopo.topology <- function(x, layout = "auto", edge = "fan", ...) { |
|
89 | 3x |
`!!` <- rlang::`!!` |
90 | 3x |
topo <- x |
91 | 3x |
if (!edge %in% c("curve", "line", "fan")) { |
92 | ! |
stop("edge should be either \"curve\", \"fan\", or \"line\".") |
93 |
} |
|
94 | 3x |
if (!requireNamespace("ggraph", quietly = TRUE)) { |
95 | ! |
stop("Package \"ggraph\" needed for this function to work. Please install it.", |
96 | ! |
call. = FALSE) |
97 |
} |
|
98 | 3x |
x <- as_tbl_graph(topo) |
99 |
# https://stackoverflow.com/questions/49989158/how-to-have-a-removed-from-a-ggraph-plot-legend |
|
100 | 3x |
GeomLabel <- ggplot2::GeomLabel |
101 | 3x |
GeomLabel$draw_key <- function(data, params, size) { |
102 | 4x |
ggplot2::draw_key_rect(data) |
103 |
} |
|
104 |
# Check that the random seed is not reset by graphlayouts functions |
|
105 | 3x |
if (exists(".Random.seed", .GlobalEnv)) { |
106 | 3x |
prev_seed <- .GlobalEnv[[".Random.seed"]] |
107 | ! |
} else { prev_seed <- NULL } |
108 | 3x |
msg_ggraph <- capture_msg(ggraph::ggraph(x, layout = layout)) |
109 | 3x |
if (!is.null(msg_ggraph) && msg_ggraph$message == "Multiple parents. Unfolding graph\n") { |
110 | ! |
message("\"stress\" layout cannot handle this topology. ", |
111 | ! |
"Using \"kk\" layout instead.") |
112 | ! |
layout <- "kk" |
113 |
} |
|
114 | 3x |
g <- ggraph::ggraph(x, layout = layout) |
115 | 3x |
if (exists(".Random.seed", .GlobalEnv)) { |
116 | 3x |
new_seed <- .GlobalEnv[[".Random.seed"]] |
117 | ! |
} else { new_seed <- NULL } |
118 | 3x |
if (!identical(prev_seed, new_seed)) { |
119 | ! |
stop("Random seed was reset when calling ggraph::ggraph() in ggtopo().\n", |
120 | ! |
"This is unwanted behaviour, please report it as a bug to the isotracer authors.") |
121 |
} |
|
122 | 3x |
if (edge == "curve") { |
123 | ! |
g <- g + |
124 | ! |
ggraph::geom_edge_diagonal(ggplot2::aes(start_cap = ggraph::label_rect(`!!`(rlang::sym("node1.name"))), |
125 | ! |
end_cap = ggraph::label_rect(`!!`(rlang::sym("node2.name")))), |
126 | ! |
arrow = grid::arrow(length = grid::unit(1, "line"))) |
127 | 3x |
} else if (edge == "fan") { |
128 | 2x |
g <- g + |
129 | 2x |
ggraph::geom_edge_fan(ggplot2::aes(start_cap = ggraph::label_rect(`!!`(rlang::sym("node1.name"))), |
130 | 2x |
end_cap = ggraph::label_rect(`!!`(rlang::sym("node2.name")))), |
131 | 2x |
arrow = grid::arrow(length = grid::unit(1, "line"))) |
132 |
} else { |
|
133 | 1x |
g <- g + |
134 | 1x |
ggraph::geom_edge_link(ggplot2::aes(start_cap = ggraph::label_rect(`!!`(rlang::sym("node1.name"))), |
135 | 1x |
end_cap = ggraph::label_rect(`!!`(rlang::sym("node2.name")))), |
136 | 1x |
arrow = grid::arrow(length = grid::unit(1, "line"))) |
137 |
} |
|
138 | 3x |
g <- g + |
139 | 3x |
ggraph::geom_node_label(ggplot2::aes(label = `!!`(rlang::sym("name")), |
140 | 3x |
fill = `!!`(rlang::sym("steady_state"))), |
141 | 3x |
col = "black", |
142 | 3x |
fontface = 1, |
143 | 3x |
label.padding = grid::unit(0.5, "line"), |
144 | 3x |
label.r = grid::unit(0.5, "line")) + |
145 | 3x |
ggplot2::coord_cartesian(clip = "off") + |
146 | 3x |
ggraph::theme_graph(base_family = "sans") # https://github.com/thomasp85/ggraph/issues/67 |
147 | 3x |
g <- g + ggplot2::scale_fill_manual(values = c("FALSE" = "#a6cee3", |
148 | 3x |
"TRUE" = "#b2df8a"), |
149 | 3x |
labels = c("FALSE" = "No", |
150 | 3x |
"TRUE" = "Yes")) + |
151 | 3x |
ggplot2::labs(fill = "Steady state") |
152 | 3x |
if (length(attr(topo, "steadyState")) == 0) { |
153 | 1x |
g <- g + ggplot2::theme(legend.position = "none") |
154 |
} |
|
155 | 3x |
return(g) |
156 |
} |
|
157 | ||
158 |
### * ggflows() |
|
159 | ||
160 |
#' A quick-and-dirty way of visualizing relative flows in a network |
|
161 |
#' |
|
162 |
#' @param x A tibble with the flow estimates, with columns "from", "to", and |
|
163 |
#' "flow". |
|
164 |
#' @param layout Optional, layout to use (e.g. "sugiyama", "kk", "stress") |
|
165 |
#' @param edge "curve" (the default), "line" or "fan". |
|
166 |
#' @param max_width Optional, numeric giving the maximum edge width (minimum |
|
167 |
#' width is always 1). |
|
168 |
#' @param legend Boolean, display edge width legend? |
|
169 |
#' @param ... Not used. |
|
170 |
#' |
|
171 |
#' @return A ggplot2 plot. |
|
172 |
#' |
|
173 |
#' @examples |
|
174 |
#' if (requireNamespace("ggraph")) { |
|
175 |
#' z <- tibble::tribble( |
|
176 |
#' ~from, ~to, ~flow, |
|
177 |
#' "leavesAndStem", "rootsAndRhizome", 333.929866077124, |
|
178 |
#' "lowerWater", "rootsAndRhizome", 4425.15780019304, |
|
179 |
#' "rootsAndRhizome", "leavesAndStem", 525.208837577916, |
|
180 |
#' "upperWater", "leavesAndStem", 11224.0814971855 |
|
181 |
#' ) |
|
182 |
#' ggflows(z) |
|
183 |
#' ggflows(z, max_width = 15) |
|
184 |
#' } |
|
185 |
#' |
|
186 |
#' @export |
|
187 |
#' |
|
188 | ||
189 |
ggflows <- function(x, layout = "auto", edge = "fan", max_width, legend = TRUE, ...) { |
|
190 | ! |
`!!` <- rlang::`!!` |
191 | ! |
if (! all(c("from", "to", "flow") %in% colnames(x))) { |
192 | ! |
stop("`x` must have at least columns \"from\", \"to\", and \"flow\".") |
193 |
} |
|
194 | ! |
if (!requireNamespace("ggraph", quietly = TRUE)) { |
195 | ! |
stop("Package \"ggraph\" needed for this function to work. Please install it.", |
196 | ! |
call. = FALSE) |
197 |
} |
|
198 | ! |
if (!requireNamespace("tidygraph", quietly = TRUE)) { |
199 | ! |
stop("Package \"tidygraph\" needed for this function to work. Please install it.", |
200 | ! |
call. = FALSE) |
201 |
} |
|
202 | ! |
x <- x[, c("from", "to", "flow")] |
203 | ! |
if (!edge %in% c("curve", "line", "fan")) { |
204 | ! |
stop("edge should be either \"curve\", \"fan\", or \"line\".") |
205 |
} |
|
206 |
# https://stackoverflow.com/questions/49989158/how-to-have-a-removed-from-a-ggraph-plot-legend |
|
207 |
# https://stackoverflow.com/questions/56173310/how-to-adjust-the-width-of-edges-by-weight-in-ggraph-network-in-r |
|
208 | ! |
GeomLabel <- ggplot2::GeomLabel |
209 | ! |
GeomLabel$draw_key <- function(data, params, size) { |
210 | ! |
ggplot2::draw_key_rect(data) |
211 |
} |
|
212 | ! |
g <- ggraph::ggraph(x, layout = layout) |
213 | ! |
if (edge == "curve") { |
214 | ! |
g <- g + |
215 | ! |
ggraph::geom_edge_diagonal(ggplot2::aes(start_cap = ggraph::label_rect(`!!`(rlang::sym("node1.name"))), |
216 | ! |
end_cap = ggraph::label_rect(`!!`(rlang::sym("node2.name"))), |
217 | ! |
width = `!!`(rlang::sym("flow"))), |
218 | ! |
arrow = grid::arrow(length = grid::unit(1, "line"))) |
219 | ! |
} else if (edge == "fan") { |
220 | ! |
g <- g + |
221 | ! |
ggraph::geom_edge_fan(ggplot2::aes(start_cap = ggraph::label_rect(`!!`(rlang::sym("node1.name"))), |
222 | ! |
end_cap = ggraph::label_rect(`!!`(rlang::sym("node2.name"))), |
223 | ! |
width = `!!`(rlang::sym("flow"))), |
224 | ! |
arrow = grid::arrow(length = grid::unit(1, "line"))) |
225 |
} else { |
|
226 | ! |
g <- g + |
227 | ! |
ggraph::geom_edge_link(ggplot2::aes(start_cap = ggraph::label_rect(`!!`(rlang::sym("node1.name"))), |
228 | ! |
end_cap = ggraph::label_rect(`!!`(rlang::sym("node2.name"))), |
229 | ! |
width = `!!`(rlang::sym("flow"))), |
230 | ! |
arrow = grid::arrow(length = grid::unit(1, "line"))) |
231 |
} |
|
232 | ! |
g <- g + |
233 | ! |
ggraph::geom_node_label(ggplot2::aes(label = `!!`(rlang::sym("name"))), |
234 | ! |
fill = grey(0.9), |
235 | ! |
fontface = 1, |
236 | ! |
label.padding = grid::unit(0.5, "line"), |
237 | ! |
label.r = grid::unit(0.5, "line")) + |
238 | ! |
ggplot2::coord_cartesian(clip = "off") + |
239 | ! |
ggraph::theme_graph(base_family = "sans") # https://github.com/thomasp85/ggraph/issues/67 |
240 | ! |
if (!missing(max_width)) { |
241 | ! |
if (max_width < 1) { |
242 | ! |
stop("`max_width` must be >= 1.") |
243 |
} |
|
244 | ! |
g <- g + |
245 | ! |
ggraph::scale_edge_width(range = c(1, max_width)) |
246 |
} |
|
247 | ! |
if (!legend) { |
248 | ! |
g <- g + ggplot2::theme(legend.position = "none") |
249 |
} |
|
250 | ! |
return(g) |
251 |
} |
1 |
### * TODO |
|
2 | ||
3 |
# Clean-up this file |
|
4 | ||
5 |
### * None of the functions in this file is exported |
|
6 | ||
7 |
### * group2string() |
|
8 | ||
9 |
#' Convert a group entry to a string |
|
10 |
#' |
|
11 |
#' @param x A vector describing a group. Can be NULL. |
|
12 |
#' |
|
13 |
#' @return A string. |
|
14 |
#' |
|
15 |
#' @keywords internal |
|
16 |
#' @noRd |
|
17 | ||
18 |
group2string <- function(x) { |
|
19 | ! |
if (is.null(x)) { |
20 | ! |
return("NULL") |
21 |
} |
|
22 | ! |
n <- names(x) |
23 | ! |
s <- paste(paste(n, x, sep = "="), collapse = "; ") |
24 | ! |
return(s) |
25 |
} |
|
26 | ||
27 |
### * Alias for tidy_mcmc() |
|
28 | ||
29 |
#' @keywords internal |
|
30 |
#' @noRd |
|
31 | ||
32 |
tidy_mcmc_list <- function(...) { |
|
33 | 22x |
tidy_mcmc(...) |
34 |
} |
1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * mugen_stan() |
|
4 | ||
5 |
#' Run a stan model from a network model (temporary name) |
|
6 |
#' |
|
7 |
#' Incorporate a loglik trace: https://mc-stan.org/loo/reference/extract_log_lik.html |
|
8 |
#' |
|
9 |
#' @param nm A \code{networkModel} object. |
|
10 |
#' @param iter A positive integer specifying the number of iterations for each |
|
11 |
#' chain (including warmup). The default is 2000. |
|
12 |
#' @param chains A positive integer specifying the number of Markov chains. |
|
13 |
#' The default is 4. |
|
14 |
#' @param euler_control An optional list containing extra parameters for the |
|
15 |
#' Euler method. The list elements can be \code{"dt"} or |
|
16 |
#' \code{"grid_size"}, which are respectively the time step size for |
|
17 |
#' trajectory calculations (\code{"dt"}) or the number of points for the |
|
18 |
#' calculation (\code{"grid_size"}). If none is provided, a default grid |
|
19 |
#' size of 256 steps is used. |
|
20 |
#' @param cores Number of cores to use for parallel run. Default is |
|
21 |
#' \code{NULL}, which means to use the value stored in |
|
22 |
#' \code{options()[["mc.cores"]]} (or 1 if this value is not set). |
|
23 |
#' @param stanfit If TRUE, returns a `stanfit` object instead of the more |
|
24 |
#' classical `mcmc.list` object. |
|
25 |
#' @param use_fixed_values Boolean, if TRUE any parameter value set with |
|
26 |
#' \code{set_params()} will be taken as fixed during the MCMC run. Default |
|
27 |
#' is FALSE. |
|
28 |
#' @param ... Passed to \code{rstan::sampling}. |
|
29 |
#' |
|
30 |
#' @keywords internal |
|
31 |
#' @noRd |
|
32 | ||
33 |
mugen_stan <- function(nm, iter = 2000, chains = 4, euler_control = list(), |
|
34 |
cores = NULL, stanfit = FALSE, |
|
35 |
use_fixed_values = FALSE, ...) { |
|
36 |
# Detect cores |
|
37 | 8x |
cores <- get_n_cores(cores = cores) |
38 |
# Convert network model to stan data |
|
39 | 8x |
stan.data <- prep_stan_data_euler(nm, dt = euler_control[["dt"]], |
40 | 8x |
grid_size = euler_control[["grid_size"]], |
41 | 8x |
use_fixed_values = use_fixed_values) |
42 |
# Fit the model |
|
43 | 8x |
stan.data[["ode_method"]] <- 2 # For Euler scheme |
44 | 8x |
fit <- rstan::sampling(stanmodels[["networkModel"]], |
45 | 8x |
data = stan.data, |
46 | 8x |
iter = iter, |
47 | 8x |
chains = chains, |
48 | 8x |
cores = cores, |
49 | 8x |
pars = c("nonConstantParams", "log_lik", |
50 | 8x |
"rawUniformParams", "rawHcauchyParams", |
51 | 8x |
"rawBetaParams", "rawTrNormParams", |
52 | 8x |
"rawExponentialParams", "rawGammaParams"), ...) |
53 | 8x |
stopifnot(!"isotracer_stan_data" %in% names(attributes(fit))) |
54 | 8x |
attr(fit, "isotracer_stan_data") <- stan.data |
55 |
# Return |
|
56 | 8x |
if (stanfit) { |
57 | ! |
return(fit) |
58 |
} else { |
|
59 | 8x |
return(stanfit_to_named_mcmclist(stanfit = fit)) |
60 |
} |
|
61 |
} |
|
62 | ||
63 |
### * prep_stan_data_euler() |
|
64 | ||
65 |
#' Prepare stan data from a network model |
|
66 |
#' |
|
67 |
#' @param nm A \code{networkModel} object. |
|
68 |
#' @param dt,grid_size Either the time step size for trajectory calculations |
|
69 |
#' (\code{dt}) or the number of points for the calculation |
|
70 |
#' (\code{grid_size}) can be provided. If none is provided, then a default |
|
71 |
#' grid size of 256 steps is used. |
|
72 |
#' @param use_fixed_values Boolean, if TRUE any parameter value set with |
|
73 |
#' \code{set_params()} will be taken as fixed during the MCMC run. Default |
|
74 |
#' is FALSE. |
|
75 |
#' |
|
76 |
#' @keywords internal |
|
77 |
#' @noRd |
|
78 | ||
79 |
prep_stan_data_euler <- function(nm, dt = NULL, grid_size = NULL, |
|
80 |
use_fixed_values = FALSE) { |
|
81 | 8x |
d <- list() |
82 | 8x |
end <- NULL |
83 | 8x |
params_nm <- params(nm, simplify = TRUE) |
84 | 8x |
priors_nm <- priors(nm, fix_set_params = use_fixed_values, |
85 | 8x |
quiet = TRUE) |
86 | 8x |
priors_nm <- priors_nm[match(params_nm, priors_nm[["in_model"]]), ] |
87 | 8x |
stopifnot(all(params_nm == priors_nm[["in_model"]])) |
88 |
# For now the stan model is only implemented for network models with the |
|
89 |
# same number of compartments on each row (a more general case where rows |
|
90 |
# can have different numbers of compartments is easily converted to this |
|
91 |
# case, by adding compartments without connections to fill the topology in |
|
92 |
# each row). |
|
93 | 8x |
stopifnot(length(unique(sapply(comps(nm), length))) == 1) |
94 |
# Counts |
|
95 | 8x |
d[["nComps"]] <- length(comps(nm)[[1]]) |
96 | 8x |
d[["nGroups"]] <- nrow(nm) |
97 | 8x |
d[["nParams"]] <- length(params_nm) |
98 |
# Encode steady state compartments |
|
99 | 8x |
dSteady <- encode_steady(nm) |
100 | 8x |
d <- c(d, dSteady) |
101 |
# Encode split compartments |
|
102 | 8x |
dSplit <- encode_split(nm, params_nm) |
103 | 8x |
d <- c(d, dSplit) |
104 |
# Encode initial conditions |
|
105 | 8x |
dInit <- encode_init(nm) |
106 | 8x |
d <- c(d, dInit) |
107 |
# Encode events |
|
108 | 8x |
dEvents <- encode_events(nm, dt = dt, grid_size = grid_size, end = end) |
109 | 8x |
d <- c(d, dEvents) |
110 |
# Encode observations (including eta and zeta parameter indices) |
|
111 | 8x |
dObs <- encode_obs(nm, params_nm, dt = dt, grid_size = grid_size, end = end) |
112 | 8x |
d <- c(d, dObs) |
113 |
# Encode parameter priors |
|
114 | 8x |
dPriors <- encode_priors(params_nm, priors_nm) |
115 | 8x |
d <- c(d, dPriors) |
116 |
# Encode time schemes |
|
117 | 8x |
dTimeSchemes <- encode_time_schemes(nm, dt = dt, grid_size = grid_size, |
118 | 8x |
end = end) |
119 | 8x |
d <- c(d, dTimeSchemes) |
120 |
# Encode uptake rates (upsilons) |
|
121 | 8x |
dUpsilons <- encode_upsilons(nm, params_nm) |
122 | 8x |
d <- c(d, dUpsilons) |
123 |
# Encode losses (lambdas) |
|
124 | 8x |
dLambdas <- encode_lambdas(nm, params_nm) |
125 | 8x |
d <- c(d, dLambdas) |
126 |
# Encode decay rate for radioactive tracers |
|
127 | 8x |
lambda_decay <- attr(nm, "lambda_hl") |
128 | 8x |
if (is.null(lambda_decay)) { |
129 | 8x |
lambda_decay <- 0 |
130 |
} |
|
131 | 8x |
d[["lambda_decay"]] <- lambda_decay |
132 |
# Encode distribution families (for proportions and sizes) |
|
133 | 8x |
d <- c(d, encode_distrib_families(nm)) |
134 |
# Add encoding for matrix exponential (so that the Stan model does not crash) |
|
135 | 8x |
d[["allParams"]] <- params_nm |
136 | 8x |
matrix_exp <- encode_intervals(nm) |
137 | 8x |
matrix_exp <- c(matrix_exp, encode_unique_obs_times(nm)) |
138 | 8x |
stopifnot(!any(names(matrix_exp) %in% names(d))) |
139 | 8x |
d <- c(d, matrix_exp) |
140 | 8x |
return(d) |
141 |
} |
|
142 | ||
143 |
### * encode_events() |
|
144 | ||
145 |
#' Encode events (for Stan model using Euler integration) |
|
146 |
#' |
|
147 |
#' For now, only pulse events are encoded. |
|
148 |
#' |
|
149 |
#' @param nm A \code{networkModel} object. |
|
150 |
#' @param dt,grid_size Either the time step size for trajectory calculations |
|
151 |
#' (\code{dt}) or the number of points for the calculation |
|
152 |
#' (\code{grid_size}) can be provided. If none is provided, then a default |
|
153 |
#' grid size of 256 steps is used. |
|
154 |
#' @param end Time value for end point. If not provided, the last observation |
|
155 |
#' or event is used. |
|
156 |
#' |
|
157 |
#' @examples |
|
158 |
#' encode_events <- isotracer:::encode_events |
|
159 |
#' encode_events(aquarium_mod) |
|
160 |
#' encode_events(trini_mod) |
|
161 |
#' |
|
162 |
#' @keywords internal |
|
163 |
#' @noRd |
|
164 | ||
165 |
encode_events <- function(nm, dt = NULL, grid_size = NULL, end = NULL) { |
|
166 | 345x |
d <- nm_get_time_schemes(nm, dt = dt, grid_size = grid_size, end = end) |
167 | 345x |
nGroups <- nrow(nm) |
168 | 345x |
o <- list() |
169 | 345x |
events <- nm[["events"]] |
170 | 345x |
stopifnot(all(dplyr::bind_rows(events)[["event"]] == "pulse")) |
171 |
# Encode compartments and timepoints |
|
172 | 345x |
for (i in seq_len(nrow(nm))) { |
173 | 347x |
comps <- colnames(nm$topology[[i]]) |
174 | 347x |
comps <- setNames(seq_along(comps), nm = comps) |
175 | 347x |
if (!is.null(events[[i]])) { |
176 | 2x |
events[[i]]$compartment <- comps[events[[i]]$compartment] |
177 | 2x |
events[[i]]$timepoints <- match(events[[i]]$time, d$timepoints[[i]]) |
178 |
} |
|
179 |
} |
|
180 |
# Encode the number of events |
|
181 | 345x |
if (length(events) == 0) { |
182 | 330x |
nEvents <- rep(0, nrow(nm)) |
183 |
} else { |
|
184 | 15x |
nEvents <- sapply(events, function(x) { |
185 | 14x |
if (is.null(x)) return(0) |
186 | 2x |
return(nrow(x)) |
187 |
}) |
|
188 |
} |
|
189 | 345x |
o[["maxNpulseEvents"]] <- max(nEvents) |
190 | 345x |
o[["nPulseEvents"]] <- setNames(c(nEvents, 0), # Padded |
191 | 345x |
nm = c(paste0("grp", seq_len(nrow(nm))), "padding")) |
192 |
# Encode pulses |
|
193 | 345x |
o[["pulseEventsIndices"]] <- array(0, dim = c(max(nEvents), 2, nGroups), |
194 | 345x |
dimnames = list(seq_len(max(nEvents)), |
195 | 345x |
c("timepoint", "comp"), |
196 | 345x |
paste0("grp", seq_len(nGroups)))) |
197 | 345x |
o[["pulseEventsQuantities"]] <- array(0, dim = c(max(nEvents), 2, nGroups), |
198 | 345x |
dimnames = list(seq_len(max(nEvents)), |
199 | 345x |
c("unmarked", "marked"), |
200 | 345x |
paste0("grp", seq_len(nGroups)))) |
201 | 345x |
for (i in seq_len(nGroups)) { |
202 | 347x |
if (!is.null(events[[i]])) { |
203 | 2x |
o[["pulseEventsIndices"]][1:nEvents[i], 1:2, i] <- |
204 | 2x |
as.matrix(events[[i]][, c("timepoints", "compartment")]) |
205 | 2x |
chars <- lapply(events[[i]]$characteristics, tibble::as_tibble) |
206 | 2x |
chars <- dplyr::bind_rows(chars)[, c("unmarked", "marked")] |
207 | 2x |
o[["pulseEventsQuantities"]][1:nEvents[i], 1:2, i] <- |
208 | 2x |
as.matrix(chars) |
209 |
} |
|
210 |
} |
|
211 |
# Return |
|
212 | 345x |
return(o) |
213 |
} |
|
214 | ||
215 |
### ** nm_get_time_schemes() |
|
216 | ||
217 |
#' Build the time schemes for numerical solving of the system of differential equations |
|
218 |
#' |
|
219 |
#' This function processes each row of a networkModel. It always assumes that |
|
220 |
#' the starting time point is t=0. |
|
221 |
#' |
|
222 |
#' @param nm A networkModel |
|
223 |
#' @param dt,grid_size Either the time step size for trajectory calculations |
|
224 |
#' (\code{dt}) or the number of points for the calculation |
|
225 |
#' (\code{grid_size}) can be provided. If none is provided, then a default |
|
226 |
#' grid size of 256 steps is used. |
|
227 |
#' @param end Time value for end point. If not provided, the last observation |
|
228 |
#' or event is used. |
|
229 |
#' @param at Optional, vector of time values at which the trajectory must be |
|
230 |
#' evaluated |
|
231 |
#' |
|
232 |
#' @keywords internal |
|
233 |
#' @noRd |
|
234 | ||
235 |
nm_get_time_schemes <- function(nm, dt = NULL, grid_size = NULL, end = NULL, |
|
236 |
at = NULL) { |
|
237 |
# Get the time schemes for each row of nm |
|
238 | 380x |
ts <- lapply(seq_len(nrow(nm)), function(i) { |
239 | 397x |
z <- nm_row_get_time_scheme(nm[i, ], dt = dt, grid_size = grid_size, |
240 | 397x |
end = end, at = at) |
241 | 397x |
z <- tibble::as_tibble(lapply(z, list)) |
242 |
}) |
|
243 | 380x |
ts <- dplyr::bind_rows(ts) |
244 | 380x |
return(ts) |
245 |
} |
|
246 | ||
247 |
### ** nm_row_get_time_scheme() |
|
248 | ||
249 |
#' Build the time scheme for numerical solving of the system of differential equations |
|
250 |
#' |
|
251 |
#' This function is applied to one row of a networkModel. It always assumes |
|
252 |
#' that the starting time point is t=0. |
|
253 |
#' |
|
254 |
#' @param nm_row A row from a \code{networkModel} object. |
|
255 |
#' @param dt,grid_size Either the time step size for trajectory calculations |
|
256 |
#' (\code{dt}) or the number of points for the calculation |
|
257 |
#' (\code{grid_size}) can be provided. If none is provided, then a default |
|
258 |
#' grid size of 256 steps is used. |
|
259 |
#' @param end Time value for end point. If not provided, the last observation |
|
260 |
#' or event is used. |
|
261 |
#' @param at Optional, vector of time values at which the trajectory must be |
|
262 |
#' evaluated |
|
263 |
#' |
|
264 |
#' @examples |
|
265 |
#' isotracer:::nm_row_get_time_scheme(aquarium_mod) |
|
266 |
#' |
|
267 |
#' @keywords internal |
|
268 |
#' @noRd |
|
269 | ||
270 |
nm_row_get_time_scheme <- function(nm_row, dt = NULL, grid_size = NULL, end = NULL, |
|
271 |
at = NULL) { |
|
272 | 734x |
nmRow <- nm_row |
273 | 734x |
stopifnot(nrow(nmRow) == 1) |
274 |
# Parse dt and gridsize |
|
275 | 734x |
if (!(is.null(dt) | is.null(grid_size))) { |
276 | ! |
stop("Only \"dt\" or \"grid_size\" can be specified, not both.") |
277 |
} |
|
278 | 734x |
if (is.null(dt) & is.null(grid_size)) { |
279 | 171x |
grid_size <- 256 |
280 |
} |
|
281 |
# Get observation times |
|
282 | 734x |
observations <- nmRow[["observations"]][[1]] |
283 | 734x |
obsTimes <- observations[["time"]] |
284 | 734x |
obsTimes <- sort(unique(obsTimes)) |
285 |
# Get events time |
|
286 | 734x |
eventTimes <- c() |
287 | 734x |
if (!is.null(nmRow[["events"]][[1]])) { |
288 | 8x |
eventTimes <- unique(nmRow[["events"]][[1]]$time) |
289 |
} |
|
290 |
# Get "at" times |
|
291 | 734x |
atTimes <- c() |
292 | 734x |
if (!is.null(at)) { |
293 | 29x |
atTimes <- unique(at) |
294 |
} |
|
295 |
# Get end time |
|
296 | 734x |
if (is.null(end)) { |
297 | 60x |
maxTime <- max(c(obsTimes, eventTimes, atTimes)) |
298 |
} else { |
|
299 | 674x |
maxTime <- end |
300 |
} |
|
301 |
# Calculate dt |
|
302 | 734x |
if (is.null(dt)) { |
303 | 734x |
dt <- maxTime / grid_size |
304 |
} |
|
305 |
# Generate a time scheme |
|
306 | 734x |
timepoints <- c(seq(0, maxTime, by = dt), obsTimes, eventTimes, atTimes) |
307 | 734x |
timepoints <- sort(unique(timepoints)) |
308 | 734x |
timepoints <- timepoints[timepoints <= maxTime] |
309 |
# (if end = NULL, timepoints is a timeline with timesteps at most dt wide |
|
310 |
# and containing all the observations sampling times and the event times, |
|
311 |
# even if not a multiple of dt.) |
|
312 |
# (else, timepoints is truncated at end.) |
|
313 |
# Annotate the observations tibble with the timepoints ids |
|
314 | 734x |
observations[["timepoint"]] <- match(observations[["time"]], timepoints) |
315 | 734x |
if (is.null(end)) { |
316 | 60x |
stopifnot(!any(is.na(observations[["timepoint"]]))) |
317 |
} |
|
318 |
# Get unique dts |
|
319 | 734x |
dts <- timepoints_to_dt(timepoints) |
320 |
# Return |
|
321 | 734x |
return(list(timepoints = timepoints, |
322 | 734x |
unique_dt = dts[["unique_dt"]], |
323 | 734x |
dt_i = dts[["dt_i"]], |
324 | 734x |
observations = observations)) |
325 |
} |
|
326 | ||
327 |
### ** timepoints_to_dt() |
|
328 | ||
329 |
#' Prepare the set of unique dt from a vector of timepoints |
|
330 |
#' |
|
331 |
#' Useful to identify how many different transfer matrices have to be calculated. |
|
332 |
#' |
|
333 |
#' @param timepoints Numeric vector of timepoints, sorted. |
|
334 |
#' |
|
335 |
#' @keywords internal |
|
336 |
#' @noRd |
|
337 | ||
338 |
timepoints_to_dt <- function(timepoints) { |
|
339 | 734x |
dts <- diff(timepoints) |
340 | 734x |
uniqueDts <- sort(unique(dts)) |
341 | 734x |
dt_i <- match(dts, uniqueDts) |
342 | 734x |
stopifnot(!any(is.na(dt_i))) |
343 | 734x |
return(list(unique_dt = uniqueDts, |
344 | 734x |
dt_i = dt_i)) |
345 |
} |
|
346 | ||
347 |
### * encode_obs() |
|
348 | ||
349 |
#' Encode observations for a network model |
|
350 |
#' |
|
351 |
#' @param nm A \code{networkModel} object. |
|
352 |
#' @param allParams Parameters of the network model. |
|
353 |
#' @param dt,grid_size Either the time step size for trajectory calculations |
|
354 |
#' (\code{dt}) or the number of points for the calculation |
|
355 |
#' (\code{grid_size}) can be provided. If none is provided, then a default |
|
356 |
#' grid size of 256 steps is used. |
|
357 |
#' @param end Time value for end point. If not provided, the last observation |
|
358 |
#' or event is used. |
|
359 |
#' |
|
360 |
#' @return NULL if the observations column only contain NULLs. |
|
361 |
#' |
|
362 |
#' @importFrom stats na.omit |
|
363 |
#' |
|
364 |
#' @keywords internal |
|
365 |
#' @noRd |
|
366 | ||
367 |
encode_obs <- function(nm, allParams, dt = NULL, grid_size = NULL, end = NULL) { |
|
368 | 8x |
d <- nm_get_time_schemes(nm, dt = dt, grid_size = grid_size, end = end) |
369 | 8x |
nGroups <- nrow(nm) |
370 | 8x |
zeta_by_comp <- attr(nm, "size_zeta_per_compartment") |
371 | 8x |
if (is.null(zeta_by_comp)) { |
372 | ! |
zeta_by_comp<- FALSE |
373 |
} |
|
374 |
# TODO Add filtering to keep only compartments present in topo |
|
375 |
# TODO Handle gracefully the case without observations |
|
376 | 8x |
if (all(sapply(nm$observations, is.null))) { |
377 | ! |
return(NULL) |
378 |
} |
|
379 |
# Get sizes |
|
380 | 8x |
sizes <- purrr::map(d$observations, function(x) { |
381 | 10x |
na.omit(x[, c("compartment", "size", "timepoint")]) |
382 |
}) |
|
383 |
# Get proportions |
|
384 | 8x |
props <- purrr::map(d$observations, function(x) { |
385 | 10x |
na.omit(x[, c("compartment", "proportion", "timepoint")]) |
386 |
}) |
|
387 |
# Encode compartments |
|
388 | 8x |
for (i in seq_len(nrow(nm))) { |
389 | 10x |
comps <- colnames(nm$topology[[i]]) |
390 | 10x |
comps <- setNames(seq_along(comps), nm = comps) |
391 | 10x |
sizes[[i]]$compartment <- comps[sizes[[i]]$compartment] |
392 | 10x |
props[[i]]$compartment <- comps[props[[i]]$compartment] |
393 |
} |
|
394 |
# Encode sizes and props |
|
395 | 8x |
o <- list() |
396 | 8x |
o[["nSizesObs"]] <- c(purrr::map_dbl(sizes, nrow), 0) # Padded |
397 | 8x |
names(o[["nSizesObs"]]) <- c(paste0("grp", seq_len(nrow(nm))), "padding") |
398 | 8x |
o[["nPropsObs"]] <- c(purrr::map_dbl(props, nrow), 0) # Padded |
399 | 8x |
names(o[["nPropsObs"]]) <- c(paste0("grp", seq_len(nrow(nm))), "padding") |
400 | 8x |
o[["maxNsizesObs"]] <- max(o[["nSizesObs"]]) |
401 | 8x |
o[["maxNpropsObs"]] <- max(o[["nPropsObs"]]) |
402 |
# Prepare containers |
|
403 | 8x |
o[["sizesObsIndices"]] <- array(0, dim = c(o[["maxNsizesObs"]], 3, nGroups), |
404 | 8x |
dimnames = list(seq_len(o[["maxNsizesObs"]]), |
405 | 8x |
c("comp", "timepoint", "zeta"), |
406 | 8x |
paste0("grp", seq_len(nGroups)))) |
407 | 8x |
o[["sizesObs"]] <- array(0, dim = c(o[["maxNsizesObs"]], nGroups), |
408 | 8x |
dimnames = list(seq_len(o[["maxNsizesObs"]]), |
409 | 8x |
paste0("grp", seq_len(nGroups)))) |
410 | 8x |
o[["propsObsIndices"]] <- array(0, dim = c(o[["maxNpropsObs"]], 3, nGroups), |
411 | 8x |
dimnames = list(seq_len(o[["maxNpropsObs"]]), |
412 | 8x |
c("comp", "timepoint", "eta"), |
413 | 8x |
paste0("grp", seq_len(nGroups)))) |
414 | 8x |
o[["propsObs"]] <- array(0, dim = c(o[["maxNpropsObs"]], nGroups), |
415 | 8x |
dimnames = list(seq_len(o[["maxNpropsObs"]]), |
416 | 8x |
paste0("grp", seq_len(nGroups)))) |
417 |
# Fill containers |
|
418 | 8x |
for (g in seq_len(nGroups)) { |
419 | 10x |
if (o[["nSizesObs"]][g] > 0) { |
420 | 10x |
o[["sizesObsIndices"]][1:o[["nSizesObs"]][g], 1:2, g] <- as.matrix(sizes[[g]][, c("compartment", "timepoint")]) |
421 | 10x |
o[["sizesObs"]][1:o[["nSizesObs"]][g], g] <- as.matrix(sizes[[g]][, c("size")]) |
422 | 10x |
if (!zeta_by_comp) { |
423 | 10x |
zeta_global <- nm[["parameters"]][[g]]$in_model[nm[["parameters"]][[g]]$in_replicate == "zeta"] |
424 | 10x |
zeta_index <- match(zeta_global, allParams) |
425 | 10x |
o[["sizesObsIndices"]][1:o[["nSizesObs"]][g], 3, g] <- zeta_index |
426 |
} else { |
|
427 | ! |
comps <- colnames(nm$topology[[g]]) |
428 | ! |
comps <- setNames(seq_along(comps), nm = comps) |
429 | ! |
zeta_replicate <- paste0("zeta_", names(comps)[sizes[[g]][["compartment"]]]) |
430 | ! |
zeta_global <- nm[["parameters"]][[g]]$in_model[match(zeta_replicate, nm[["parameters"]][[g]]$in_replicate)] |
431 | ! |
zeta_index <- match(zeta_global, allParams) |
432 | ! |
o[["sizesObsIndices"]][1:o[["nSizesObs"]][g], 3, g] <- zeta_index |
433 |
} |
|
434 |
} |
|
435 |
} |
|
436 | 8x |
for (g in seq_len(nGroups)) { |
437 | 10x |
if (o[["nPropsObs"]][g] > 0) { |
438 | 10x |
o[["propsObsIndices"]][1:o[["nPropsObs"]][g], 1:2, g] <- as.matrix(props[[g]][, c("compartment", "timepoint")]) |
439 | 10x |
o[["propsObs"]][1:o[["nPropsObs"]][g], g] <- as.matrix(props[[g]][, c("proportion")]) |
440 | 10x |
eta_global <- nm[["parameters"]][[g]]$in_model[nm[["parameters"]][[g]]$in_replicate == "eta"] |
441 | 10x |
eta_index <- match(eta_global, allParams) |
442 | 10x |
o[["propsObsIndices"]][1:o[["nPropsObs"]][g], 3, g] <- eta_index |
443 |
} |
|
444 |
} |
|
445 |
# Return |
|
446 | 8x |
return(o) |
447 |
} |
|
448 | ||
449 |
### * encode_time_schemes() |
|
450 | ||
451 |
#' Encode the time schemes for stan data |
|
452 |
#' |
|
453 |
#' @param nm A \code{networkModel} object. |
|
454 |
#' @param dt,grid_size Either the time step size for trajectory calculations |
|
455 |
#' (\code{dt}) or the number of points for the calculation |
|
456 |
#' (\code{grid_size}) can be provided. If none is provided, then a default |
|
457 |
#' grid size of 256 steps is used. |
|
458 |
#' @param end Time value for end point. If not provided, the last observation |
|
459 |
#' or event is used. |
|
460 |
#' |
|
461 |
#' @keywords internal |
|
462 |
#' @noRd |
|
463 | ||
464 |
encode_time_schemes <- function(nm, dt = NULL, grid_size = NULL, end = NULL) { |
|
465 |
# (timestep and dt are used interchangeably) |
|
466 |
# Get the time schemes |
|
467 | 27x |
ts <- nm_get_time_schemes(nm, dt = dt, grid_size = grid_size, end = end) |
468 |
# Build the arrays |
|
469 | 27x |
nGroups <- nrow(nm) |
470 | 27x |
n_unique_dts <- purrr::map_dbl(ts$unique_dt, length) |
471 | 27x |
maxN_unique_dts <- max(n_unique_dts) |
472 | 27x |
n_timesteps <- purrr::map_dbl(ts$dt_i, length) |
473 | 27x |
maxN_timesteps <- max(n_timesteps) |
474 | 27x |
unique_dts <- array(0, dim = c(maxN_unique_dts, nGroups), |
475 | 27x |
dimnames = list(c(1:maxN_unique_dts), |
476 | 27x |
paste0("grp", 1:nGroups))) |
477 | 27x |
timesteps <- array(0, c(maxN_timesteps, nGroups), |
478 | 27x |
dimnames = list(c(1:maxN_timesteps), |
479 | 27x |
paste0("grp", 1:nGroups))) |
480 |
# Fill the arrays |
|
481 | 27x |
for (i in seq_len(nrow(nm))) { |
482 | 40x |
unique_dts[1:n_unique_dts[i], i] <- ts[["unique_dt"]][[i]] |
483 | 40x |
timesteps[1:n_timesteps[i], i] <- ts[["dt_i"]][[i]] |
484 |
} |
|
485 |
# Prepare data |
|
486 | 27x |
d <- list() |
487 | 27x |
d[["nTimesteps"]] <- setNames(c(n_timesteps, 0), # Padded |
488 | 27x |
nm = c(paste0("grp", 1:nGroups), "padding")) |
489 | 27x |
d[["nUniqueDts"]] <- setNames(c(n_unique_dts, 0), # Padded |
490 | 27x |
nm = c(paste0("grp", 1:nGroups), "padding")) |
491 | 27x |
d[["timesteps"]] <- timesteps |
492 | 27x |
d[["unique_dts"]] <- unique_dts |
493 | 27x |
d[["maxNtimesteps"]] <- max(d[["nTimesteps"]]) |
494 | 27x |
d[["maxNuniqueDts"]] <- max(d[["nUniqueDts"]]) |
495 | 27x |
return(d) |
496 |
} |
1 |
### * TODO |
|
2 | ||
3 |
# Clean-up this file |
|
4 | ||
5 |
### * All functions in this file are exported |
|
6 | ||
7 |
### * plot.networkModel() |
|
8 | ||
9 |
#' Plot observations/trajectories/predictions from a network model |
|
10 |
#' |
|
11 |
#' @param x A \code{networkModel} object. |
|
12 |
#' @param ... Passed to \code{plot_nm}. |
|
13 |
#' |
|
14 |
#' @return Called for side effect (plotting). |
|
15 |
#' |
|
16 |
#' @method plot networkModel |
|
17 |
#' |
|
18 |
#' @export |
|
19 | ||
20 |
plot.networkModel <- function(x, ...) { |
|
21 | 30x |
plot_nm(x, ...) |
22 |
} |
|
23 | ||
24 |
### * plot.ready_for_unit_plot() |
|
25 | ||
26 |
#' Plot output from \code{split_to_unit_plot} |
|
27 |
#' |
|
28 |
#' @param x A \code{ready_for_unit_plot} object. |
|
29 |
#' @param ... Passed to \code{plot_nm}. |
|
30 |
#' |
|
31 |
#' @return Called for side effect (plotting). |
|
32 |
#' |
|
33 |
#' @method plot ready_for_unit_plot |
|
34 |
#' |
|
35 |
#' @export |
|
36 | ||
37 |
plot.ready_for_unit_plot <- function(x, ...) { |
|
38 | ! |
plot_nm(x, ...) |
39 |
} |
|
40 | ||
41 |
### * plot.networkModelStanfit() |
|
42 | ||
43 |
#' @method plot networkModelStanfit |
|
44 |
#' |
|
45 |
#' @export |
|
46 | ||
47 |
plot.networkModelStanfit <- function(x, ...) { |
|
48 | 10x |
plot_traces(x, ...) |
49 |
} |
|
50 | ||
51 |
### * plot.tidy_flows_mcmc.list() |
|
52 | ||
53 |
#' @method plot tidy_flows_mcmc.list |
|
54 |
#' |
|
55 |
#' @export |
|
56 | ||
57 |
plot.tidy_flows_mcmc.list <- function(x, ...) { |
|
58 | ! |
plot_traces(x, ...) |
59 |
} |
|
60 | ||
61 |
### * plot.derived.mcmc.list() |
|
62 | ||
63 |
#' @method plot derived.mcmc.list |
|
64 |
#' |
|
65 |
#' @export |
|
66 | ||
67 |
plot.derived.mcmc.list <- function(x, ...) { |
|
68 | ! |
plot_traces(x, ...) |
69 |
} |
|
70 | ||
71 |
### * traceplot() |
|
72 | ||
73 |
#' Plot mcmc.list objects |
|
74 |
#' |
|
75 |
#' @param x A \code{coda::mcmc.list} object. |
|
76 |
#' @param ... Passed to \code{plot_traces}. |
|
77 |
#' |
|
78 |
#' @return Called for side effect (plotting). |
|
79 |
#' |
|
80 |
#' @export |
|
81 | ||
82 |
traceplot <- function(x, ...) { |
|
83 | ! |
plot_traces(x, ...) |
84 |
} |
|
85 | ||
86 |
### * mcmc_heatmap() |
|
87 | ||
88 |
#' Draw a heatmap based on the correlations between parameters |
|
89 |
#' |
|
90 |
#' Note that the colors represent the strength of the correlations (from 0 to |
|
91 |
#' 1), but do not inform about their sign. The method used to calculate |
|
92 |
#' correlation coefficients is Spearman's rho. |
|
93 |
#' |
|
94 |
#' @param x A \code{coda::mcmc.list} object. |
|
95 |
#' @param col Optional, vectors of colors defining the color ramp. Default uses |
|
96 |
#' the divergent palette "Blue-Red 2" from the colorspace package. |
|
97 |
#' @param ... Passed to \code{\link{heatmap}}. |
|
98 |
#' |
|
99 |
#' @return Called for side effect (plotting). |
|
100 |
#' |
|
101 |
#' @importFrom grDevices heat.colors |
|
102 |
#' @importFrom grDevices colorRampPalette |
|
103 |
#' @importFrom stats dist |
|
104 |
#' @importFrom stats cor |
|
105 |
#' @importFrom stats hclust |
|
106 |
#' @importFrom stats heatmap |
|
107 |
#' |
|
108 |
#' @export |
|
109 | ||
110 |
mcmc_heatmap = function(x, col = NULL, ...) { |
|
111 | ! |
chain <- x |
112 | ! |
f <- 0.45 # Multiplicative factor to adjust the label margin |
113 |
# Prepare color palette (generated with the colorspace Berlin palette) |
|
114 | ! |
if (is.null(col)) { |
115 | ! |
col <- rev(c("#4A6FE3", "#7086E1", "#8F9DE1", "#ABB4E2", "#C7CBE3", "#E2E2E2", |
116 | ! |
"#E6C4C9", "#E5A5B1", "#E28699", "#DB6581", "#D33F6A")) |
117 |
} |
|
118 | ! |
cols <- colorRampPalette(col)(1025) |
119 |
# Draw heatmap |
|
120 | ! |
data <- cor(do.call(rbind, chain), method = "spearman") |
121 | ! |
col_low_index <- 513 + floor(min(data) * 512) |
122 | ! |
col_high_index <- 513 + ceiling(max(data) * 512) |
123 | ! |
cols <- cols[col_low_index:col_high_index] |
124 | ! |
labels <- colnames(data) |
125 | ! |
labels <- gsub("^lossRate_", "__", labels) |
126 | ! |
labels <- gsub("^portion_", "__", labels) |
127 | ! |
labels <- gsub("^uptakeRate_from_", "__", labels) |
128 | ! |
maxLabels <- max(sapply(labels, nchar)) |
129 | ! |
labels <- sapply(colnames(data), varnameToExp) |
130 | ! |
margin <- maxLabels * f |
131 | ! |
heatmap(data, labCol = labels, labRow = labels, |
132 | ! |
distfun = function(x) dist(abs(x)), |
133 | ! |
hclustfun = function(x) hclust(abs(x), method = "ward.D2"), |
134 | ! |
scale = "none", col = cols, margins = rep(margin, 2), ...) |
135 |
} |
|
136 | ||
137 |
### * sankey() |
|
138 | ||
139 |
#' Draw a Sankey plot for a network and estimated flows |
|
140 |
#' |
|
141 |
#' @param topo A topology. |
|
142 |
#' @param nodes Optional, a tibble containing the properties of the nodes. It |
|
143 |
#' should have a `comp` column with the same entries as the topology. It |
|
144 |
#' cannot have `x` and `y` entries. If it has a `label` entry, it will |
|
145 |
#' replace the `comp` values for node labels. |
|
146 |
#' @param flows A tibble containing the values of the flows in the topology. If |
|
147 |
#' NULL (the default), all flows have same width in the plot. |
|
148 |
#' @param layout String, node-placing algorithm to use from the ggraph package |
|
149 |
#' (e.g. "stress"). The ggraph package itself uses some algoritms from the |
|
150 |
#' igraph package. See the Details in the help of |
|
151 |
#' \code{\link[ggraph]{layout_tbl_graph_igraph}} for available |
|
152 |
#' algorithms. The ggraph package must be installed for this argument to be |
|
153 |
#' taken into account. Currently, only the "left2right" and "stress" layout |
|
154 |
#' are implemented in detail, and any other layout will use rough defaults |
|
155 |
#' for the aesthetic adjustments. Other layouts which are kind of working |
|
156 |
#' are "kk", "lgl", "fr", "dh", "mds". Some of those produce |
|
157 |
#' non-reproducible node locations (at least I haven't managed to reproduce |
|
158 |
#' them even by setting the RNG seed before calling the function). |
|
159 |
#' @param new Boolean, create a new page for the plot? |
|
160 |
#' @param debug Boolean, if TRUE then draw a lot of shapes to help with |
|
161 |
#' debugging. |
|
162 |
#' @param node_f,edge_f Multiplicative factor to adjust node and edge size. |
|
163 |
#' @param node_s String defining how node size is calculated. The effect of the |
|
164 |
#' string also depends on the chosen layout. |
|
165 |
#' @param edge_n Integer, number of interpolation points along each edge. |
|
166 |
#' @param cex_lab,cex.lab Expansion factor for label size (both arguments are |
|
167 |
#' synonyms). |
|
168 |
#' @param fit Boolean, if TRUE try to fit all the graphical elements inside the |
|
169 |
#' canvas. |
|
170 |
#' |
|
171 |
#' @return Mostly called for its side effect (plotting), but also returns |
|
172 |
#' invisible the scene object describing the Sankey plot. Note that the |
|
173 |
#' structure of this object is experimental and might change in the future! |
|
174 |
#' |
|
175 |
#' @examples |
|
176 |
#' library(magrittr) |
|
177 |
#' |
|
178 |
#' topo <- topo(trini_mod) |
|
179 |
#' sankey(topo, debug = TRUE) |
|
180 |
#' sankey(topo, layout = "stress") |
|
181 |
#' sankey(topo(aquarium_mod), layout = "stress", edge_f = 0.5) |
|
182 |
#' |
|
183 |
#' m <- new_networkModel() %>% |
|
184 |
#' set_topo(c("subs -> NH3 -> subs", |
|
185 |
#' "NH3 -> Q, E", "E -> Q -> E", |
|
186 |
#' "E -> D, M")) %>% |
|
187 |
#' set_steady("subs") %>% |
|
188 |
#' set_prop_family("normal_sd") |
|
189 |
#' ggtopo(m) |
|
190 |
#' sankey(topo(m), layout = "stress") |
|
191 |
#' |
|
192 |
#' # Debug visualization |
|
193 |
#' |
|
194 |
#' ## Helper functions |
|
195 |
#' flows_from_topo <- function(x) { |
|
196 |
#' x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
|
197 |
#' n_comps <- ncol(x) |
|
198 |
#' links <- which(x > 0) |
|
199 |
#' from <- links %/% n_comps + 1 |
|
200 |
#' to <- links %% n_comps |
|
201 |
#' links <- tibble::tibble(from = from, to = to) |
|
202 |
#' for (i in seq_len(nrow(links))) { |
|
203 |
#' if (links$to[i] == 0) { |
|
204 |
#' links$from[i] <- links$from[i] - 1 |
|
205 |
#' links$to[i] <- n_comps |
|
206 |
#' } |
|
207 |
#' stopifnot(x[links$to[i], links$from[i]] > 0) |
|
208 |
#' } |
|
209 |
#' flows <- tibble::tibble(from = colnames(x)[links$from], |
|
210 |
#' to = rownames(x)[links$to]) |
|
211 |
#' return(flows) |
|
212 |
#' } |
|
213 |
#' nodes_from_topo <- function(x) { |
|
214 |
#' nodes <- tibble::tibble(comp = colnames(x), |
|
215 |
#' label = colnames(x)) |
|
216 |
#' return(nodes) |
|
217 |
#' } |
|
218 |
#' |
|
219 |
#' t <- topo(trini_mod) |
|
220 |
#' nodes <- nodes_from_topo(t) |
|
221 |
#' nodes$label <- as.list(nodes$label) |
|
222 |
#' nodes$label[[2]] <- latex2exp::TeX("$\\beta$") |
|
223 |
#' nodes$size <- runif(nrow(nodes), 1, 2) |
|
224 |
#' flows <- flows_from_topo(t) |
|
225 |
#' flows$width <- runif(nrow(flows), 0.2, 2) |
|
226 |
#' z <- sankey(t, nodes = nodes, flows = flows, layout = "left2right", |
|
227 |
#' debug = TRUE, node_f = 1, edge_f = 0.9, edge_n = 32, |
|
228 |
#' cex_lab = 1.5) |
|
229 |
#' |
|
230 |
#' # Stress layout |
|
231 |
#' y <- new_networkModel() %>% |
|
232 |
#' set_topo(c("subs -> NH3 -> subs", |
|
233 |
#' "NH3 -> Q, E", "E -> Q -> E", |
|
234 |
#' "E -> D, M")) %>% |
|
235 |
#' set_steady("subs") %>% |
|
236 |
#' set_prop_family("normal_sd") |
|
237 |
#' y <- topo(y) |
|
238 |
#' nodes <- nodes_from_topo(y) |
|
239 |
#' nodes$size <- runif(nrow(nodes), 1, 10) |
|
240 |
#' ggtopo(y, edge = "fan") |
|
241 |
#' flows <- flows_from_topo(y) |
|
242 |
#' flows$width <- runif(nrow(flows), 0.2, 5) |
|
243 |
#' z <- sankey(y, nodes = nodes, flows = flows, debug = FALSE, edge_n = 32, |
|
244 |
#' edge_f = 0.4, node_s = "prop") |
|
245 |
#' |
|
246 |
#' # Another example |
|
247 |
#' r <- new_networkModel() %>% |
|
248 |
#' set_topo("infusion -> plasma -> body -> plasma") %>% |
|
249 |
#' set_steady(c("infusion", "body")) |
|
250 |
#' r <- topo(r) |
|
251 |
#' ggtopo(r, edge = "fan") |
|
252 |
#' sankey(r, debug = TRUE, edge_f = 0.2) |
|
253 |
#' |
|
254 |
#' @export |
|
255 |
#' |
|
256 | ||
257 |
sankey <- function(topo, nodes = NULL, flows = NULL, layout = NULL, new = TRUE, |
|
258 |
debug = FALSE, node_f = 1, edge_f = 1, node_s = "auto", |
|
259 |
edge_n = 32, cex_lab = NULL, cex.lab = NULL, fit = TRUE) { |
|
260 |
# Process arguments |
|
261 | 8x |
if (!is.null(cex_lab) & !is.null(cex.lab)) { |
262 | ! |
message("Both `cex_lab` and `cex.lab` provided. Using `cex_lab`.") |
263 |
} |
|
264 | 8x |
if (is.null(cex_lab) & !is.null(cex.lab)) { |
265 | ! |
cex_lab <- cex.lab |
266 |
} |
|
267 | 8x |
if (is.null(cex_lab)) { |
268 | 6x |
cex_lab <- 1 |
269 |
} |
|
270 |
# Create canvas |
|
271 | 7x |
if (new) { grid::grid.newpage() } |
272 | 8x |
if (!debug) { |
273 | 3x |
dev.hold() |
274 | 3x |
on.exit(dev.flush()) |
275 |
} |
|
276 | 8x |
top_vp <- grid::viewport() |
277 | 8x |
grid::pushViewport(top_vp) |
278 | 8x |
canvas_vp <- make_and_push_ortho_vp(width = 0.9, height = 0.9, |
279 | 8x |
debug = debug) |
280 |
# Get default layout if needed |
|
281 | 8x |
layout <- sankey_get_layout(topo, layout) |
282 |
# Build node tibble if needed |
|
283 | 8x |
if (is.null(nodes)) { |
284 | 4x |
nodes <- tibble::tibble(comp = colnames(topo)) |
285 |
} |
|
286 | 8x |
if (!"size" %in% colnames(nodes)) { |
287 | 4x |
nodes <- tibble::add_column(nodes, size = 1) |
288 |
} |
|
289 |
# Get unit flows if needed |
|
290 | 8x |
if (is.null(flows)) { |
291 | 3x |
flows <- flows_from_topo(topo) |
292 |
} |
|
293 | 8x |
if (!"width" %in% colnames(flows)) { |
294 | 3x |
flows <- tibble::add_column(flows, width = 1) |
295 |
} |
|
296 |
# Adjust flow widths |
|
297 | 8x |
total_width <- 1 |
298 | 8x |
flows$width <- flows$width / sum(flows$width) * total_width * edge_f |
299 |
# 1) place_nodes |
|
300 | 8x |
scene <- sankey_place_nodes(topo, nodes, flows, layout, |
301 | 8x |
xlim = canvas_vp$xscale, |
302 | 8x |
ylim = canvas_vp$yscale) |
303 |
# 2) place_edge_sockets_on_nodes |
|
304 | 8x |
scene <- sankey_place_edge_sockets_on_nodes(scene, topo, nodes, flows, layout) |
305 |
# 3) calc_node_shape |
|
306 | 8x |
scene <- sankey_calc_node_shape(scene, topo, nodes, flows, layout, node_f = node_f, |
307 | 8x |
xlim = canvas_vp$xscale, node_s = node_s) |
308 |
# 4) adjust_node_locations |
|
309 | 8x |
scene <- sankey_adjust_node_locations(scene, topo, nodes, flows, layout) |
310 |
# 5) adjust_edge_sockets |
|
311 | 8x |
scene <- sankey_adjust_edge_sockets(scene, topo, nodes, flows, layout) |
312 |
# 6) calc_edge_socket_coordinates |
|
313 | 8x |
scene <- sankey_calc_edge_socket_coordinates(scene, topo, nodes, flows, layout) |
314 |
# 7) place_edge_backbones |
|
315 | 8x |
scene <- sankey_place_edge_backbones(scene, topo, nodes, flows, layout, n = edge_n) |
316 |
# 8) place_labels |
|
317 | 8x |
scene <- sankey_place_labels(scene, topo, nodes, flows, layout, cex_lab = cex_lab) |
318 |
# Draw elements |
|
319 | 8x |
elements_limits <- sankey_get_elements_lims(scene) |
320 | 8x |
ratio_x <- diff(elements_limits[["xlim"]]) / diff(canvas_vp$xscale) |
321 | 8x |
ratio_y <- diff(elements_limits[["ylim"]]) / diff(canvas_vp$yscale) |
322 | 8x |
ratio <- max(ratio_x, ratio_y) |
323 | 8x |
xscale_fit <- (canvas_vp$xscale - mean(canvas_vp$xscale)) * ratio + mean(elements_limits[["xlim"]]) |
324 | 8x |
yscale_fit <- (canvas_vp$yscale - mean(canvas_vp$yscale)) * ratio + mean(elements_limits[["ylim"]]) |
325 | 8x |
if (fit) { |
326 |
# Create a new canvas vp |
|
327 | 8x |
canvas_fit_vp <- grid::dataViewport(xscale = xscale_fit, |
328 | 8x |
yscale = yscale_fit, |
329 | 8x |
name = "ortho_canvas_fit") |
330 | 8x |
grid::pushViewport(canvas_fit_vp) |
331 |
} |
|
332 | 8x |
sankey_draw_edges(scene[["edges"]], debug = debug) |
333 | 8x |
sankey_draw_nodes(scene[["nodes"]], debug = debug, node_s = node_s) |
334 | 8x |
sankey_draw_labels(scene[["labels"]], debug = debug) |
335 | 8x |
if (fit) { |
336 |
# Pop extra canvas viewport |
|
337 | 8x |
grid::popViewport(1) |
338 |
} |
|
339 |
# Pop viewports |
|
340 | 8x |
grid::popViewport(3) |
341 |
# Return elements |
|
342 | 8x |
return(invisible(scene)) |
343 |
} |
|
344 | ||
345 |
### * quick_sankey() |
|
346 | ||
347 |
#' Draw a Sankey plot with basic defaults |
|
348 |
#' |
|
349 |
#' @param flows A tibble containing flows (output from |
|
350 |
#' \code{\link{tidy_flows}}). For now it should have an "average_flow" |
|
351 |
#' column in the tibbles of the "flows" list column. |
|
352 |
#' @param ... Passed to \code{\link{sankey}}. |
|
353 |
#' |
|
354 |
#' @return Mostly called for its side effect (plotting), but also returns |
|
355 |
#' invisible the scene object describing the Sankey plot. Note that the |
|
356 |
#' structure of this object is experimental and might change in the future! |
|
357 |
#' |
|
358 |
#' @export |
|
359 |
#' |
|
360 | ||
361 |
quick_sankey <- function(flows, ...) { |
|
362 | ! |
`!!` <- rlang::`!!` |
363 | ! |
flows <- dplyr::bind_rows(lapply(flows$flows, as.data.frame)) |
364 | ! |
stopifnot(all(c("from", "to", "average_flow") %in% colnames(flows))) |
365 | ! |
flows <- na.omit(flows[, c("from", "to", "average_flow")]) |
366 | ! |
flows <- dplyr::group_by(flows, `!!`(rlang::sym("from")), |
367 | ! |
`!!`(rlang::sym("to"))) |
368 | ! |
flows <- dplyr::summarize(flows, width = mean(`!!`(rlang::sym("average_flow"))), |
369 | ! |
.groups = "drop") |
370 |
# Build topo from flows table |
|
371 | ! |
topo <- make_topology(flows, from = "from", to = "to") |
372 | ! |
sankey(topo = topo, nodes = nodes_from_topo(topo), flows = flows, ...) |
373 |
} |
|
374 |
1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * add_param_mapping() |
|
4 | ||
5 |
#' Build the (default) parameter mapping in a \code{networkModel} object |
|
6 |
#' |
|
7 |
#' For now this function just builds the default base mapping. Improvements |
|
8 |
#' are: taking into account fixed effect of discrete variables and updating |
|
9 |
#' gracefully when grouping or topology is modified. Maybe this is not doable |
|
10 |
#' in a clean way, and the best option is just to reset the mapping when |
|
11 |
#' grouping or topo is modified and warn the user. |
|
12 |
#' |
|
13 |
#' This function also sets the default priors |
|
14 |
#' |
|
15 |
#' @param nm A \code{networkModel} object. |
|
16 |
#' |
|
17 |
#' @return A \code{networkModel} object. |
|
18 |
#' |
|
19 |
#' @keywords internal |
|
20 |
#' @noRd |
|
21 | ||
22 |
add_param_mapping <- function(nm, use_default = FALSE) { |
|
23 |
# Build canonical mapping |
|
24 | 37x |
mapping <- lapply(seq_len(nrow(nm)), function(i) { |
25 | 37x |
params <- nm_base_params(nm[i, ]) |
26 | 37x |
tibble::tibble(in_replicate = params, |
27 | 37x |
in_model = params) |
28 |
}) |
|
29 | 37x |
nm$parameters <- mapping |
30 |
# Set default priors |
|
31 | 37x |
priors <- tibble::tibble(in_model = nm_base_params(nm)) |
32 | 37x |
if (use_default) { |
33 | ! |
priors$prior <- lapply(seq_len(nrow(priors)), function(i) { |
34 | ! |
hcauchy_p(scale = 0.1) |
35 |
}) |
|
36 | ! |
for (i in which(grepl("^portion[.]act_", priors$in_model))) { |
37 | ! |
priors$prior[[i]] <- uniform_p(0, 1) |
38 |
} |
|
39 |
} else { |
|
40 | 37x |
priors$prior <- lapply(seq_len(nrow(priors)), function(i) { |
41 | 306x |
NULL |
42 |
}) |
|
43 |
} |
|
44 | 37x |
attr(nm, "priors") <- priors |
45 |
# Return |
|
46 | 37x |
return(nm) |
47 |
} |
|
48 | ||
49 |
### * nm_base_params() |
|
50 | ||
51 |
#' Get the vector of default parameter names |
|
52 |
#' |
|
53 |
#' @param nm A \code{networkModel} object |
|
54 |
#' |
|
55 |
#' @return A vector of strings |
|
56 |
#' |
|
57 |
#' @keywords internal |
|
58 |
#' @noRd |
|
59 | ||
60 |
nm_base_params <- function(nm) { |
|
61 | 79x |
params <- c(lapply(nm$topology, topo_get_upsilon_names), |
62 | 79x |
lapply(nm$topology, topo_get_lambda_names), |
63 | 79x |
lapply(nm$topology, topo_get_portionAct_names), |
64 | 79x |
"eta") |
65 |
# (eta is a parameter for the variation of observed proportions) |
|
66 |
# Check if zeta should be compartment-specific |
|
67 |
# (zeta is a parameter for the variation of observed sizes) |
|
68 | 79x |
z <- attr(nm, "size_zeta_per_compartment") |
69 | 79x |
if (!is.null(z) && z) { |
70 | ! |
comps <- unique(unlist(lapply(nm$topology, colnames))) |
71 | ! |
zetas <- paste0("zeta_", comps) |
72 | ! |
params <- c(params, zetas) |
73 |
} else { |
|
74 | 79x |
params <- c(params, "zeta") |
75 |
} |
|
76 | 79x |
params <- sort(unique(unlist(params))) |
77 | 79x |
return(params) |
78 |
} |
|
79 | ||
80 |
### * refresh_param_mapping() |
|
81 | ||
82 |
#' Apply a formula on param mapping |
|
83 |
#' |
|
84 |
#' @param nm A \code{networkModel} object. |
|
85 |
#' @param formula A list of formulas describing the parameter dependencies on |
|
86 |
#' covariates. Formulas are applied sequentially. |
|
87 |
#' @param use_regexpr Boolean. Use regular expression to match the left-hand |
|
88 |
#' terms to replicate parameters? |
|
89 |
#' |
|
90 |
#' @return An updated \code{networkModel} |
|
91 |
#' |
|
92 |
#' @importFrom stats update |
|
93 |
#' |
|
94 |
#' @keywords internal |
|
95 |
#' @noRd |
|
96 | ||
97 |
refresh_param_mapping <- function(nm, formula, use_regexpr = TRUE) { |
|
98 | 5x |
base_params <- nm_base_params(nm) |
99 |
# Parse formula |
|
100 | 5x |
leftTerms <- all.vars(update(formula, . ~ 0)) |
101 | 5x |
rightTerms <- all.vars(update(formula, 0 ~ .)) |
102 |
# Apply regexpr if needed |
|
103 | 5x |
if (use_regexpr) { |
104 | 5x |
for (lt in leftTerms) { |
105 | 9x |
matches <- any(grepl(lt, base_params)) |
106 | 9x |
leftTerms <- c(leftTerms, base_params[grepl(lt, base_params)]) |
107 | 9x |
if (matches && !lt %in% base_params) { |
108 |
# Terms with regexpr match are allowed to be dropped if needed |
|
109 | 6x |
leftTerms <- leftTerms[leftTerms != lt] |
110 |
} |
|
111 |
} |
|
112 | 5x |
leftTerms <- unique(leftTerms) |
113 |
} |
|
114 |
# Check that the left terms make sense (they are default parameter names) |
|
115 | 5x |
stopifnot(all(leftTerms %in% c(".", base_params))) |
116 |
# Get all the parameter names if "." was specified |
|
117 | ! |
if ("." %in% leftTerms) { leftTerms <- base_params } |
118 |
# Check that the right terms make sense (they are grouping variables) |
|
119 | 5x |
stopifnot(!is.null(nm$group)) |
120 | 5x |
groups <- groups(nm) |
121 | 5x |
stopifnot(all(rightTerms %in% c(colnames(groups), "."))) |
122 | 5x |
rightTerms <- rightTerms[rightTerms != "."] |
123 |
# Go over the rows of the design data frame |
|
124 | 5x |
for (i in seq_len(nrow(nm))) { |
125 | 16x |
mapping <- nm$parameters[[i]] |
126 | 16x |
if (length(rightTerms) > 0) { |
127 | 16x |
covariates <- groups[, rightTerms] |
128 | 16x |
covariatesString <- apply(covariates, 1, paste, collapse = "|") |
129 | 16x |
for (lt in leftTerms) { |
130 | 78x |
g <- paste(lt, covariatesString, sep = "|") |
131 | 78x |
mapping$in_model[mapping$in_replicate == lt] <- g[i] |
132 |
} |
|
133 |
} else { |
|
134 | ! |
for (lt in leftTerms) { |
135 | ! |
g <- lt |
136 | ! |
mapping$in_model[mapping$in_replicate == lt] <- g |
137 |
} |
|
138 |
} |
|
139 | 16x |
nm$parameters[[i]] <- mapping |
140 |
} |
|
141 |
# Return |
|
142 | 5x |
return(nm) |
143 |
} |
1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * project_row() |
|
4 | ||
5 |
### ** Doc |
|
6 | ||
7 |
#' Calculate the trajectories for one row |
|
8 |
#' |
|
9 |
#' This is the workhorse function doing all the hard-work of projecting |
|
10 |
#' trajectories through numerical integration. All other functions involving |
|
11 |
#' any trajectory projection (except the Stan model itself) should rely on this |
|
12 |
#' function. |
|
13 |
#' |
|
14 |
#' @param nm_row One row of a \code{networkModel} object. |
|
15 |
#' @param dt,grid_size Either the time step size for trajectory calculations |
|
16 |
#' (\code{dt}) or the number of points for the calculation |
|
17 |
#' (\code{grid_size}) can be provided. If none is provided, then a default |
|
18 |
#' grid size of 256 steps is used. |
|
19 |
#' @param at Optional, vector of time values at which the trajectory must be |
|
20 |
#' evaluated. |
|
21 |
#' @param end Time value for end point. If not provided, the last observation |
|
22 |
#' or event is used. |
|
23 |
#' @param flows Boolean, return flows in the output? |
|
24 |
#' @param cached_ts,cached_ee Used for optimization by other functions, not for |
|
25 |
#' use by the package user. |
|
26 |
#' @param lambda_decay If non-NULL, used as the decay rate for marked tracer |
|
27 |
#' (used to model radioactive tracers). |
|
28 |
#' |
|
29 |
#' @return A tibble with one row. If flows are calculated, the returned flow |
|
30 |
#' values are flows during each dt intervals, not instantaneous flows. Note |
|
31 |
#' that in the case of a radioactive tracer, the returned flows do not |
|
32 |
#' incorporate the "loss flow" due to radioactive decay (so if one is to do |
|
33 |
#' some material accounting based on the flow values, some material will |
|
34 |
#' "leak" and appear not to be accounted for and actually corresponds to |
|
35 |
#' the tracer lost by radioactive decay). |
|
36 |
#' |
|
37 |
#' @examples |
|
38 |
#' project_row <- isotracer:::project_row |
|
39 |
#' |
|
40 |
#' m <- aquarium_mod |
|
41 |
#' m <- set_params(m, sample_params(m)) |
|
42 |
#' z <- project_row(m[1,]) |
|
43 |
#' z <- project_row(m[1,], flows = TRUE) |
|
44 |
#' |
|
45 |
#' # Calculate total flows over the projected time window |
|
46 |
#' f <- z$flows[[1]] |
|
47 |
#' f <- f[1:(length(f)-1)] # Drop last entry (which is NA) |
|
48 |
#' f <- dplyr::bind_rows(f) |
|
49 |
#' f <- dplyr::group_by(f, from, to) |
|
50 |
#' f <- dplyr::summarize(f, total_flow = sum(flow)) |
|
51 |
#' duration <- diff(range(z$timepoints[[1]])) |
|
52 |
#' f$mean_instantaneous_flow <- f$total_flow / duration |
|
53 |
#' |
|
54 |
#' @keywords internal |
|
55 |
#' @noRd |
|
56 | ||
57 |
### ** Code |
|
58 | ||
59 |
project_row <- function(nm_row, dt = NULL, grid_size = NULL, at = NULL, end = NULL, |
|
60 |
flows = FALSE, cached_ts = NULL, cached_ee = NULL, |
|
61 |
lambda_decay = NULL, ignore_pulses = FALSE) { |
|
62 |
### * Preprocessing |
|
63 | 309x |
if (is.null(lambda_decay)) { |
64 | 309x |
lambda_decay <- 0 |
65 |
} |
|
66 | 309x |
get_flows <- flows |
67 | 309x |
nmRow <- nm_row |
68 | 309x |
nComps <- ncol(nmRow$topology[[1]]) |
69 | 309x |
comps <- colnames(nmRow$topology[[1]]) |
70 | 309x |
params <- nmRow$parameters[[1]] |
71 | 309x |
params <- setNames(params$value, nm = params$in_replicate) |
72 | 309x |
ss <- attr(nmRow$topology[[1]], "steadyState") |
73 | 309x |
ss <- match(ss, comps) |
74 | 309x |
sp <- attr(nmRow$topology[[1]], "split") |
75 | 309x |
sp <- match(sp, comps) |
76 | 309x |
if (is.null(end)) { |
77 | 295x |
if (is.null(at)) { |
78 | 280x |
end <- max(nmRow$observations[[1]][["time"]]) |
79 | 280x |
if (!is.null(nmRow[["events"]][[1]])) { |
80 | ! |
end <- max(c(end, nmRow$events[[1]][["time"]])) |
81 |
} |
|
82 |
} else { |
|
83 | 15x |
end <- max(at) |
84 |
} |
|
85 |
} |
|
86 | 309x |
if (get_flows) { |
87 | 280x |
flows <- list() |
88 | 280x |
flow_template <- data.frame(from = rep(comps, each = nComps), |
89 | 280x |
to = rep(comps, nComps), |
90 | 280x |
flow = NA, |
91 | 280x |
factor = 1, |
92 | 280x |
in_topo = as.vector(nmRow$topology[[1]]), |
93 | 280x |
stringsAsFactors = FALSE) |
94 | 280x |
flow_template[["in_topo"]][flow_template[["from"]] == flow_template[["to"]]] <- 1 |
95 |
# factor to adjust sign of comps_to_NA flows |
|
96 | 280x |
flow_template[["factor"]][flow_template[["from"]] == flow_template[["to"]]] <- -1 |
97 | 280x |
flow_template[["to"]][flow_template[["from"]] == flow_template[["to"]]] <- NA |
98 | 280x |
flow_template_keep <- flow_template$in_topo > 0 |
99 | 280x |
flow_template$in_topo <- NULL |
100 | 280x |
flow_template_to_fill <- flow_template[flow_template_keep, ] |
101 | 280x |
flow_template_to_fill[["factor"]] <- NULL |
102 | 280x |
flow_template_adjust <- flow_template[["factor"]][flow_template_keep] |
103 |
} |
|
104 |
### * Get time scheme |
|
105 | 309x |
if (is.null(cached_ts)) { |
106 | 309x |
ts <- nm_row_get_time_scheme(nm_row = nmRow, dt = dt, grid_size = grid_size, |
107 | 309x |
end = end, at = at) |
108 |
} else { |
|
109 | ! |
ts <- cached_ts |
110 |
} |
|
111 | 309x |
timepoints <- ts$timepoints |
112 | 309x |
timesteps <- ts$dt_i |
113 | 309x |
dts <- ts$unique_dt |
114 |
### * Get events |
|
115 | 309x |
if (is.null(cached_ee)) { |
116 | 309x |
events <- encode_events(nmRow, end = end, dt = dt, grid_size = grid_size) |
117 |
} else { |
|
118 | ! |
events <- cached_ee |
119 |
} |
|
120 | 309x |
nPulses <- events[["maxNpulseEvents"]] |
121 | 309x |
pulseEvents <- rbind(events[["pulseEventsIndices"]][,,1], |
122 | 309x |
c(0, 0)) # Padding to keep matrix when one event only |
123 | 309x |
pulseQuantities <- rbind(events[["pulseEventsQuantities"]][,,1], |
124 | 309x |
c(0, 0)) |
125 |
### * Build transition matrices |
|
126 | 309x |
transitions <- list() |
127 | 309x |
transitionsDecay <- list() # This one incorporates the effect of lambda_decay |
128 | 309x |
for (i in seq_along(dts)) { |
129 | 685x |
transitions[[i]] <- build_transition_matrix(nmRow$topology[[1]], |
130 | 685x |
nmRow$parameters[[1]], |
131 | 685x |
dts[i]) |
132 | 685x |
transitionsDecay[[i]] <- (transitions[[i]] - |
133 | 685x |
diag(nComps) * lambda_decay * dts[i]) |
134 |
} |
|
135 | 309x |
if (get_flows) { |
136 | 280x |
transfer_mat <- list() |
137 | 280x |
for (i in seq_along(dts)) { |
138 | 560x |
transfer_mat[[i]] <- build_transfer_matrix(nmRow$topology[[1]], |
139 | 560x |
nmRow$parameters[[1]], |
140 | 560x |
dts[i]) |
141 |
} |
|
142 |
} |
|
143 |
### * Initialize event index |
|
144 | 309x |
pulseIndex <- 1 |
145 |
### * Initialize unmarked and marked quantities |
|
146 | 309x |
unmarked <- matrix(NA, ncol = nComps, nrow = length(timepoints)) |
147 | 309x |
marked <- matrix(NA, ncol = nComps, nrow = length(timepoints)) |
148 | 309x |
init <- nmRow$initial[[1]] |
149 | 309x |
init <- init[match(comps, init$compartment), ] |
150 | 309x |
stopifnot(all(comps == init$compartment)) |
151 |
## Update split compartments |
|
152 | 309x |
if (length(sp) > 0) { |
153 | 108x |
initRefr <- list() |
154 | 108x |
for (j in sp) { |
155 | 108x |
portion <- as.vector(params[paste0("portion.act_", comps[j])]) |
156 | 108x |
initRefr[[j]] <- c("unmarked" = init[["size"]][j] * (1 - portion) * (1 - init[["proportion"]][j]), |
157 | 108x |
"marked" = init[["size"]][j] * (1 - portion) * init[["proportion"]][j]) |
158 | 108x |
init[["size"]][j] <- init[["size"]][j] * portion |
159 |
} |
|
160 |
} |
|
161 | 309x |
init$unmarked <- init$size * (1 - init$proportion) |
162 | 309x |
init$marked <- init$size * init$proportion |
163 | 309x |
unmarked[1, ] <- init$unmarked[match(comps, init$compartment)] |
164 | 309x |
marked[1, ] <- init$marked[match(comps, init$compartment)] |
165 |
## Apply pulses |
|
166 | 309x |
if (nPulses > 0 & (!ignore_pulses)) { |
167 | ! |
if (pulseIndex <= nPulses) { |
168 | ! |
while(pulseIndex <= nPulses && pulseEvents[pulseIndex, 1] == 1) { |
169 | ! |
unmarked[1, pulseEvents[pulseIndex, 2]] <- unmarked[1, pulseEvents[pulseIndex, 2]] + pulseQuantities[pulseIndex, 1] |
170 | ! |
marked[1, pulseEvents[pulseIndex, 2]] <- marked[1, pulseEvents[pulseIndex, 2]] + pulseQuantities[pulseIndex, 2] |
171 | ! |
pulseIndex <- pulseIndex + 1 |
172 |
} |
|
173 |
} |
|
174 |
} |
|
175 |
### * Loop over dt |
|
176 | 309x |
for (t in seq_along(timesteps)) { |
177 |
# Apply the transfer matrix |
|
178 | 8902x |
unmarked[t+1, ] <- transitions[[timesteps[t]]] %*% unmarked[t, ] |
179 | 8902x |
marked[t+1, ] <- transitionsDecay[[timesteps[t]]] %*% marked[t, ] |
180 |
# Gather flows |
|
181 | 8902x |
if (get_flows) { |
182 | 1400x |
flows_t <- transfer_mat[[timesteps[t]]] |
183 | 1400x |
sizes_t <- unmarked[t, ] + marked[t, ] |
184 | 1400x |
for (j in seq_along(comps)) { |
185 | 4200x |
flows_t[, j] <- flows_t[, j] * sizes_t[j] |
186 |
} |
|
187 | 1400x |
flows_to_store <- as.vector(flows_t)[flow_template_keep] |
188 | 1400x |
flows[[t]] <- flow_template_to_fill |
189 | 1400x |
flows[[t]]$flow <- flows_to_store * flow_template_adjust # Adjust sign of comp_to_NA flows |
190 |
} |
|
191 |
# Reset steady-state compartments |
|
192 | 8902x |
for (j in ss) { |
193 | 900x |
unmarked[t+1,j] <- unmarked[t,j] |
194 | 900x |
marked[t+1,j] <- marked[t,j] |
195 |
} |
|
196 |
# Apply pulse events |
|
197 | 8902x |
if (nPulses > 0 & (!ignore_pulses)) { |
198 | ! |
if (pulseIndex <= nPulses) { |
199 | ! |
while(pulseIndex <= nPulses && pulseEvents[pulseIndex, 1] == t+1) { |
200 | ! |
unmarked[t+1, pulseEvents[pulseIndex, 2]] <- unmarked[t+1, pulseEvents[pulseIndex, 2]] + pulseQuantities[pulseIndex, 1] |
201 | ! |
marked[t+1, pulseEvents[pulseIndex, 2]] <- marked[t+1, pulseEvents[pulseIndex, 2]] + pulseQuantities[pulseIndex, 2] |
202 | ! |
pulseIndex <- pulseIndex + 1 |
203 |
} |
|
204 |
} |
|
205 |
} |
|
206 |
} # End of loop |
|
207 | ||
208 |
### * Clean-up and return |
|
209 | 309x |
colnames(unmarked) <- comps |
210 | 309x |
colnames(marked) <- comps |
211 | 309x |
if (length(sp) > 0) { |
212 | 108x |
for (j in sp) { |
213 | 108x |
unmarked[, j] <- unmarked[, j] + initRefr[[j]]["unmarked"] |
214 | 108x |
marked[, j] <- marked[, j] + initRefr[[j]]["marked"] |
215 |
} |
|
216 |
} |
|
217 | 309x |
sizes <- unmarked + marked |
218 | 309x |
proportions <- marked / sizes |
219 | 309x |
colnames(proportions) <- comps |
220 | 309x |
o <- tibble::tibble(timepoints = list(timepoints), |
221 | 309x |
unmarked = list(unmarked), |
222 | 309x |
marked = list(marked), |
223 | 309x |
sizes = list(sizes), |
224 | 309x |
proportions = list(proportions)) |
225 | 309x |
if (get_flows) { |
226 | 280x |
o$dt <- list(c(diff(o[["timepoints"]][[1]]), NA)) |
227 | 280x |
o$flows <- list(c(flows, NA)) |
228 |
} |
|
229 | 309x |
return(o) |
230 |
} |
|
231 | ||
232 |
### * build_transition_matrix() |
|
233 | ||
234 |
#' Build the transition matrix for a topology |
|
235 |
#' |
|
236 |
#' @param topo A network topology. |
|
237 |
#' @param params A tibble with "in_replicate" and "value" columns giving the |
|
238 |
#' parameter values. |
|
239 |
#' @param dt Numerical, time step value. |
|
240 |
#' @param apply_steady_state Boolean, if TRUE set values for steady state |
|
241 |
#' compartments to the appropriate 1. |
|
242 |
#' |
|
243 |
#' @keywords internal |
|
244 |
#' @noRd |
|
245 | ||
246 |
build_transition_matrix <- function(topo, params, dt, |
|
247 |
apply_steady_state = FALSE) { |
|
248 | 1246x |
nComps <- ncol(topo) |
249 | 1246x |
comps <- colnames(topo) |
250 | 1246x |
params <- setNames(params$value, nm = params$in_replicate) |
251 | 1246x |
m <- matrix(0, ncol = nComps, nrow= nComps) |
252 | 1246x |
for (i in seq_len(nComps)) { |
253 |
# upsilons |
|
254 | 3698x |
for (j in seq_len(nComps)) { |
255 | 11014x |
if (topo[i,j] == 1) { |
256 | 3618x |
p <- paste("upsilon", comps[j], "to", comps[i], sep = "_") |
257 | 3618x |
m[i,j] <- params[p] |
258 | 3618x |
m[j,j] <- m[j,j] - params[p] |
259 |
} |
|
260 |
} |
|
261 |
# lambdas |
|
262 | 3698x |
p <- paste("lambda", comps[i], sep = "_") |
263 | 3698x |
m[i,i] <- m[i,i] - params[p] |
264 |
} |
|
265 |
# Apply dt |
|
266 | 1246x |
m <- diag(nComps) + m * dt |
267 | 1246x |
colnames(m) <- comps |
268 | 1246x |
rownames(m) <- comps |
269 |
# Apply steady states |
|
270 | 1246x |
if (apply_steady_state) { |
271 | 561x |
ss_comps <- attr(topo, "steadyState") |
272 | 561x |
if (length(ss_comps) > 0) { |
273 | 360x |
for (comp in ss_comps) { |
274 | 360x |
i <- which(comps == comp) |
275 | 360x |
m[i, ] <- 0 |
276 | 360x |
m[i, i] <- 1 |
277 |
} |
|
278 |
} |
|
279 |
} |
|
280 | 1246x |
return(m) |
281 |
} |
|
282 | ||
283 |
### * build_transfer_matrix() |
|
284 | ||
285 |
#' Build the transfer matrix for a topology |
|
286 |
#' |
|
287 |
#' A transfer matrix is similar to a transition matrix, except that the |
|
288 |
#' diagonal doesn't contain the numerical values corresponding to what remains |
|
289 |
#' after transfer. The transfer matrix describes the flows rather than the |
|
290 |
#' compartment states after transition. |
|
291 |
#' |
|
292 |
#' Note that this function does not take into account radioactive decay when |
|
293 |
#' calculating how much material is lost (lambda rates) from a |
|
294 |
#' compartment. This is equivalent to consider that the flows are calculated |
|
295 |
#' for a situation where all isotopes are stable. |
|
296 |
#' |
|
297 |
#' @param topo A network topology. |
|
298 |
#' @param params A tibble with "in_replicate" and "value" columns giving the |
|
299 |
#' parameter values. |
|
300 |
#' @param dt Numerical, time step value. |
|
301 |
#' |
|
302 |
#' @keywords internal |
|
303 |
#' @noRd |
|
304 | ||
305 |
build_transfer_matrix <- function(topo, params, dt) { |
|
306 | 560x |
nComps <- ncol(topo) |
307 | 560x |
comps <- colnames(topo) |
308 | 560x |
params <- setNames(params$value, nm = params$in_replicate) |
309 | 560x |
m <- matrix(0, ncol = nComps, nrow= nComps) |
310 | 560x |
for (i in seq_len(nComps)) { |
311 |
# upsilons |
|
312 | 1680x |
for (j in seq_len(nComps)) { |
313 | 5040x |
if (topo[i,j] == 1) { |
314 | 1680x |
p <- paste("upsilon", comps[j], "to", comps[i], sep = "_") |
315 | 1680x |
m[i,j] <- params[p] |
316 |
} |
|
317 |
} |
|
318 |
# lambdas |
|
319 | 1680x |
p <- paste("lambda", comps[i], sep = "_") |
320 | 1680x |
if (m[i,i] != 0) { |
321 | ! |
stop("A compartment has a flow to itself (which is not permitted in the model).") |
322 |
} |
|
323 | 1680x |
m[i,i] <- m[i,i] - params[p] |
324 |
} |
|
325 |
# Apply dt |
|
326 | 560x |
m <- m * dt |
327 | 560x |
colnames(m) <- comps |
328 | 560x |
rownames(m) <- comps |
329 | 560x |
return(m) |
330 |
} |
|
331 | ||
332 |
### * calc_instantaneous_flow() |
|
333 | ||
334 |
#' Calculate instantaneous flow given a topology, sizes and parameter values |
|
335 |
#' |
|
336 |
#' Note that this function applies portions of active compartments |
|
337 |
#' instantaneously, i.e. it assumes that for a split compartment the size and |
|
338 |
#' active portion values are correct. This means that this function should not |
|
339 |
#' be used to calculate flows in a network which contains split compartments |
|
340 |
#' and which is not at equilibrium over than at t0, since in such a network the |
|
341 |
#' estimated parameters give the active portion values at t0 (i.e. for the |
|
342 |
#' initial conditions). |
|
343 |
#' |
|
344 |
#' @param topo Network topology. |
|
345 |
#' @param sizes Compartement sizes (tibble). |
|
346 |
#' @param parameters Parameter values (tibble). A named vector containing |
|
347 |
#' parameter values is also accepted. |
|
348 |
#' |
|
349 |
#' @return A tibble with instantaneous flows. |
|
350 |
#' |
|
351 |
#' @examples |
|
352 |
#' calc_instantaneous_flow <- isotracer:::calc_instantaneous_flow |
|
353 |
#' |
|
354 |
#' # Small network |
|
355 |
#' m <- aquarium_mod |
|
356 |
#' p <- sample_params(m) |
|
357 |
#' m <- set_params(m, p) |
|
358 |
#' flows <- calc_instantaneous_flow(m$topology[[1]], m$initial[[1]], |
|
359 |
#' m$parameters[[1]]) |
|
360 |
#' |
|
361 |
#' # Large network |
|
362 |
#' m <- trini_mod |
|
363 |
#' p <- sample_params(m) |
|
364 |
#' m <- set_params(m, p) |
|
365 |
#' flows <- calc_instantaneous_flow(m$topology[[1]], m$initial[[1]], |
|
366 |
#' m$parameters[[1]]) |
|
367 |
#' |
|
368 |
#' @keywords internal |
|
369 |
#' @noRd |
|
370 | ||
371 |
calc_instantaneous_flow <- function(topo, sizes, parameters) { |
|
372 |
# Convert parameters to a tibble if needed |
|
373 | ! |
if (!is(parameters, "tbl")) { |
374 | ! |
if (!(is.vector(parameters) & !is.null(attr(parameters, "names")))) { |
375 | ! |
stop("\"parameters\" must be a tibble or a named vector.") |
376 |
} |
|
377 | ! |
parameters <- tibble::tibble(value = parameters, |
378 | ! |
in_replicate = names(parameters)) |
379 |
} |
|
380 |
# Process input |
|
381 | ! |
n_comps <- ncol(topo) |
382 | ! |
comps <- colnames(topo) |
383 | ! |
comp_indices <- setNames(seq_len(n_comps), nm = comps) |
384 | ! |
params <- parameters |
385 | ! |
params <- setNames(params$value, nm = params$in_replicate) |
386 | ! |
ss <- attr(topo, "steadyState") |
387 | ! |
ss <- match(ss, comps) |
388 | ! |
sp <- attr(topo, "split") |
389 | ! |
sp <- match(sp, comps) |
390 |
# Build transfer matrix |
|
391 |
# (could use the build_transfer_matrix() function for consistency) |
|
392 | ! |
tm <- matrix(0, ncol = n_comps, nrow= n_comps) |
393 | ! |
for (i in seq_len(n_comps)) { |
394 |
# upsilons |
|
395 | ! |
for (j in seq_len(n_comps)) { |
396 | ! |
if (topo[i,j] == 1) { |
397 | ! |
p <- paste("upsilon", comps[j], "to", comps[i], sep = "_") |
398 | ! |
tm[i,j] <- params[p] |
399 |
} |
|
400 |
} |
|
401 |
# lambdas |
|
402 | ! |
p <- paste("lambda", comps[i], sep = "_") |
403 | ! |
if (tm[i,i] != 0) { |
404 | ! |
stop("A compartment has a flow to itself (which is not permitted in the model).") |
405 |
} |
|
406 | ! |
tm[i,i] <- tm[i,i] - params[p] |
407 |
} |
|
408 |
# Prepare sizes |
|
409 | ! |
sizes <- sizes[match(comps, sizes$compartment), ] |
410 | ! |
stopifnot(all(comps == sizes$compartment)) |
411 |
## Update split compartments |
|
412 | ! |
if (length(sp) > 0) { |
413 | ! |
sizeRefr <- list() |
414 | ! |
for (j in sp) { |
415 | ! |
portion <- as.vector(params[paste0("portion.act_", comps[j])]) |
416 | ! |
sizeRefr[[j]] <- sizes[["size"]][j] * (1 - portion) |
417 | ! |
sizes[["size"]][j] <- sizes[["size"]][j] * portion |
418 |
} |
|
419 |
} |
|
420 |
# Calculate flows |
|
421 | ! |
flows_mat <- tm |
422 | ! |
stopifnot(ncol(flows_mat) == n_comps) |
423 | ! |
for (j in seq_len(n_comps)) { |
424 | ! |
flows_mat[, j] <- flows_mat[, j] * sizes[["size"]][j] |
425 |
} |
|
426 |
# Put flows into a tibble format |
|
427 | ! |
flows <- tibble::tibble(from = rep(comps, each = n_comps), |
428 | ! |
to = rep(comps, n_comps), |
429 | ! |
instantaneous_flow = as.vector(flows_mat)) |
430 |
## Check that the matrix major order was correct |
|
431 |
# (maybe we can get rid of this checking step?) |
|
432 | ! |
check_mat <- matrix(flows[["instantaneous_flow"]], ncol = n_comps, byrow = FALSE) |
433 | ! |
delta <- max(abs(check_mat - flows_mat)) |
434 | ! |
if (delta > 1e-15) { |
435 | ! |
stop("Issue with matrix major order when formatting flows.") |
436 |
} |
|
437 |
# Post-processing |
|
438 | ! |
flows$in_topo <- as.vector(topo) |
439 | ! |
flows[["in_topo"]][flows[["from"]] == flows[["to"]]] <- 1 |
440 | ! |
flows[["to"]][flows[["from"]] == flows[["to"]]] <- NA |
441 | ! |
flows <- flows[flows$in_topo > 0, ] |
442 | ! |
flows$in_topo <- NULL |
443 |
# Return |
|
444 | ! |
return(flows) |
445 |
} |
|
446 | ||
447 |
### * gather_flows() |
|
448 | ||
449 |
#' Helper function to calculate flows while calculating trajectories |
|
450 |
#' |
|
451 |
#' This function does not take into account any refractory portions, and |
|
452 |
#' assumes that what is passed to it concerns only active portions. |
|
453 |
#' |
|
454 |
#' This function assumes that topo, sizes and transfer_mat are all ordered in |
|
455 |
#' the same way (i.e. compartments match across arguments, and topo has the |
|
456 |
#' same, ordered colnames and rownames). |
|
457 |
#' |
|
458 |
#' @param topo A network topology. |
|
459 |
#' @param sizes A vector containing compartment sizes. |
|
460 |
#' @param transfer_mat A transfer matrix. |
|
461 |
#' |
|
462 |
#' @return A well-formatted tibble. |
|
463 |
#' |
|
464 |
#' @keywords internal |
|
465 |
#' @noRd |
|
466 | ||
467 |
gather_flows <- function(topo, sizes, transfer_mat) { |
|
468 | ! |
comps <- colnames(topo) |
469 | ! |
n_comps <- ncol(topo) |
470 | ! |
flows <- transfer_mat |
471 | ! |
for (i in seq_len(ncol(topo))) { |
472 | ! |
flows[, i] <- flows[, i] * sizes[i] |
473 |
} |
|
474 | ! |
flows <- tibble::tibble(from = rep(comps, each = n_comps), |
475 | ! |
to = rep(comps, n_comps), |
476 | ! |
flow = as.vector(flows)) |
477 | ! |
flows$in_topo <- as.vector(topo) |
478 | ! |
flows[["in_topo"]][flows[["from"]] == flows[["to"]]] <- 1 |
479 | ! |
flows[["to"]][flows[["from"]] == flows[["to"]]] <- NA |
480 | ! |
flows <- flows[flows$in_topo > 0, ] |
481 | ! |
flows$in_topo <- NULL |
482 | ! |
return(flows) |
483 |
} |
|
484 | ||
485 |
### * potential_steady_state() |
|
486 | ||
487 |
#' Simple test to check if a network potentially admis a steady state |
|
488 |
#' |
|
489 |
#' This is an imperfect test, but it will reject the two most obvious cases |
|
490 |
#' where no steady state is (probably) possible: (1) if all lambdas are zero |
|
491 |
#' but one source is in steady state (material accumulates in the network) and |
|
492 |
#' (2) at least one lambda is non-null, but there are no steady state sources |
|
493 |
#' (material leaks from the network without being replaced). |
|
494 |
#' |
|
495 |
#' Note that this is a bit rough, since some exceptions can exists: for example |
|
496 |
#' if independent sub-networks exist in the network. |
|
497 |
#' |
|
498 |
#' @param x A one-row network object. |
|
499 |
#' |
|
500 |
#' @return Boolean. |
|
501 |
#' |
|
502 |
#' @examples |
|
503 |
#' potential_steady_state <- isotracer:::potential_steady_state |
|
504 |
#' |
|
505 |
#' m <- aquarium_mod |
|
506 |
#' m <- set_prior(m, constant_p(0), "lambda") |
|
507 |
#' m <- set_params(m, sample_params(m)) |
|
508 |
#' potential_steady_state(m) |
|
509 |
#' |
|
510 |
#' m <- set_prior(m, normal_p(0, 3), "lambda_NH4") |
|
511 |
#' m <- set_params(m, sample_params(m)) |
|
512 |
#' potential_steady_state(m) |
|
513 |
#' |
|
514 |
#' m <- trini_mod[1, ] |
|
515 |
#' m <- set_params(m, sample_params(m)) |
|
516 |
#' potential_steady_state(m) |
|
517 |
#' |
|
518 |
#' @keywords internal |
|
519 |
#' @noRd |
|
520 | ||
521 |
potential_steady_state <- function(x) { |
|
522 | 564x |
if (nrow(x) != 1) { |
523 | ! |
stop("\"x\" must have exactly one row.") |
524 |
} |
|
525 | 564x |
topo <- topo(x, simplify = TRUE) |
526 | 564x |
params <- x$parameters[[1]] |
527 | 564x |
lambdas <- params$value[grepl("^lambda_", params$in_replicate)] |
528 | 564x |
has_steady_state <- length(attr(topo, "steadyState")) > 0 |
529 | 564x |
has_non_zero_lambda <- any(lambdas != 0) |
530 | 564x |
if (has_steady_state & ! has_non_zero_lambda) { |
531 | ! |
return(FALSE) |
532 |
} |
|
533 | 564x |
if (! has_steady_state & has_non_zero_lambda) { |
534 | 1x |
return(FALSE) |
535 |
} |
|
536 | 563x |
return(TRUE) |
537 |
} |
|
538 | ||
539 |
### * calculate_steady_state_one_row() |
|
540 | ||
541 |
#' Calculate steady-state compartment sizes for a one-row network |
|
542 |
#' |
|
543 |
#' This is an experimental function. It attempts to calculate steady-state |
|
544 |
#' compartment sizes using the set parameter values and the initial compartment |
|
545 |
#' sizes. Use it with caution! |
|
546 |
#' |
|
547 |
#' Note about how steady state sizes for split compartments are calculated: the |
|
548 |
#' steady size of the active portion is calculated divide it is divided by the |
|
549 |
#' active fraction (portion.act parameter) to get the total size including the |
|
550 |
#' refractory portion. In this case we get a "steady-state" refractory portion, |
|
551 |
#' consistent with steady state size of active fraction and with portion.act |
|
552 |
#' parameter. |
|
553 |
#' |
|
554 |
#' @param nm_row A one-row network model, with set parameter values. |
|
555 |
#' |
|
556 |
#' @return A named vector containing the steady state compartment sizes. |
|
557 |
#' |
|
558 |
#' @examples |
|
559 |
#' calculate_steady_state_one_row <- isotracer:::calculate_steady_state_one_row |
|
560 |
#' |
|
561 |
#' # Simple model, no split compartments |
|
562 |
#' m <- aquarium_mod |
|
563 |
#' m <- set_prior(m, constant_p(0), "lambda") |
|
564 |
#' set.seed(4) |
|
565 |
#' m <- set_params(m, sample_params(m)) |
|
566 |
#' proj <- project(m, end = 40) |
|
567 |
#' plot(proj) |
|
568 |
#' calculate_steady_state_one_row(m) |
|
569 |
#' |
|
570 |
#' # With split compartments |
|
571 |
#' m <- trini_mod[1, ] |
|
572 |
#' set.seed(4) |
|
573 |
#' m <- set_params(m, sample_params(m)) |
|
574 |
#' proj <- project(m, end = 40) |
|
575 |
#' plot(proj) |
|
576 |
#' calculate_steady_state_one_row(m) |
|
577 |
#' |
|
578 |
#' @keywords internal |
|
579 |
#' @noRd |
|
580 | ||
581 |
calculate_steady_state_one_row <- function(nm_row) { |
|
582 | 561x |
stopifnot(nrow(nm_row) == 1) |
583 | 561x |
topo <- topo(nm_row, simplify = TRUE) |
584 | 561x |
if (!potential_steady_state(nm_row)) { |
585 | ! |
stop("The network model row seems not to admit a steady state.\n", |
586 | ! |
"(i.e. potential_steady_state(...) returned FALSE).") |
587 |
} |
|
588 |
# Go on |
|
589 | 561x |
has_ss_comps <- length(attr(topo, "steadyState")) != 0 |
590 | 561x |
has_split_comps <- length(attr(topo, "split")) != 0 |
591 | 561x |
params <- nm_row$parameters[[1]] |
592 | 561x |
transition_mat <- build_transition_matrix(topo = topo, params = params, dt = 1, |
593 | 561x |
apply_steady_state = TRUE) |
594 | 561x |
decomp <- eigen(transition_mat) |
595 | 561x |
values <- decomp$values |
596 | 561x |
vectors <- decomp$vectors |
597 |
# Process split compartments |
|
598 | 561x |
if (has_split_comps) { |
599 | 200x |
sp_comps <- attr(topo, "split") |
600 | 200x |
inits <- nm_row$initial[[1]] |
601 | 200x |
original_inits <- inits |
602 | 200x |
original_inits$refr_size <- 0 |
603 | 200x |
original_inits$p_act <- 1 |
604 | 200x |
for (i in seq_len(nrow(inits))) { |
605 | 600x |
if (inits$compartment[i] %in% sp_comps) { |
606 | 200x |
p_act <- params$value[params$in_replicate == paste0("portion.act_", inits$compartment[i])] |
607 | 200x |
original_inits$refr_size[i] <- original_inits$size[i] * (1 - p_act) |
608 | 200x |
original_inits$p_act[i] <- p_act |
609 | 200x |
inits$size[i] <- inits$size[i] * p_act |
610 |
} |
|
611 |
} |
|
612 | 200x |
nm_row$initial[[1]] <- inits |
613 |
} |
|
614 |
# Determine eigen elements of interest |
|
615 | 561x |
if (!has_ss_comps) { |
616 | 201x |
accepted <- which(Im(values) == 0) |
617 | 201x |
if (length(accepted) > 1) { |
618 | 75x |
accepted <- which(Im(values) == 0 & |
619 | 75x |
apply(sign(Re(vectors)), 2, function(x) length(unique(x))) == 1) |
620 |
} |
|
621 | 201x |
if (length(accepted) == 0) { |
622 | ! |
stop("No real eigenvalue for the transition matrix.") |
623 |
} |
|
624 |
} else { |
|
625 | 360x |
accepted <- which(values == 1) |
626 | 360x |
if (length(accepted) == 0) { |
627 | ! |
stop("1 is not an eigenvalue of the transition matrix.") |
628 |
} |
|
629 |
# Check consistency with ss comps |
|
630 | 360x |
ss_comps <- attr(topo, "steadyState") |
631 | 360x |
ss_comps_i <- sapply(ss_comps, function(x) which(colnames(topo) == x)) |
632 | 360x |
for (i in accepted) { |
633 | 360x |
v <- vectors[, i] |
634 | 360x |
values_v <- v[ss_comps_i] |
635 | 360x |
if (!sum(values_v != 0) > 0) { |
636 | ! |
stop("Eigenvector incompatible with steady state compartments.") |
637 |
} |
|
638 | 360x |
if (!sum(values_v != 0) == 1) { |
639 | ! |
stop("Steady state compartments incompatible with current implementation.") |
640 |
} |
|
641 |
} |
|
642 |
} |
|
643 |
# Calculate steady state sizes |
|
644 | 561x |
values <- values[accepted] |
645 | 561x |
vectors <- vectors[, accepted, drop = FALSE] |
646 | 561x |
comps <- colnames(topo) |
647 | 561x |
inits <- nm_row$initial[[1]] |
648 | 561x |
stopifnot(setequal(comps, inits$compartment)) |
649 | 561x |
if (!has_ss_comps) { |
650 | 201x |
total_size <- sum(inits$size) |
651 | 201x |
if (length(accepted) > 1) { |
652 | ! |
stop("Unexpected eigenvalue decomposition: more than one real eigen value found.") |
653 |
} |
|
654 | 201x |
stable_sizes <- vectors[, 1] |
655 | 201x |
stopifnot(all(stable_sizes == Re(stable_sizes))) |
656 | 201x |
stable_sizes <- Re(stable_sizes) |
657 | 201x |
names(stable_sizes) <- colnames(transition_mat) |
658 | 201x |
adjust <- total_size / sum(stable_sizes) |
659 | 201x |
stable_sizes <- adjust * stable_sizes |
660 |
} else { |
|
661 | 360x |
ss_comps <- attr(topo, "steadyState") |
662 | 360x |
ss_comps_i <- sapply(ss_comps, function(x) which(colnames(topo) == x)) |
663 | 360x |
for (v in seq_len(ncol(vectors))) { |
664 | 360x |
focal_ss <- which(vectors[, v][ss_comps_i] > 0) |
665 | 360x |
if (!length(focal_ss) == 1) { |
666 | ! |
stop("More than one steady state compartment participating in an eigenvector.") |
667 |
} |
|
668 | 360x |
focal_init <- inits$size[inits$compartment == colnames(topo)[ss_comps_i[focal_ss]]] |
669 | 360x |
focal_index <- ss_comps_i[focal_ss] |
670 | 360x |
adjust_factor <- focal_init / vectors[, v][focal_index] |
671 | 360x |
vectors[, v] <- vectors[, v] * adjust_factor |
672 |
} |
|
673 | 360x |
stable_sizes <- apply(vectors, 1, sum) |
674 | 360x |
names(stable_sizes) <- colnames(transition_mat) |
675 | 360x |
for (i in ss_comps) { |
676 | 360x |
if (abs(stable_sizes[i] - inits$size[inits$compartment == i]) > .Machine$double.eps * 1000) { |
677 | ! |
stop("Incompatible initial and steady state size for a steady state compartment.\n", |
678 | ! |
"(size difference is ", abs(stable_sizes[i] - inits$size[inits$compartment == i]), ").") |
679 |
} |
|
680 |
} |
|
681 |
} |
|
682 |
# Process split compartments |
|
683 | 561x |
if (has_split_comps) { |
684 | 200x |
p_act <- setNames(original_inits$p_act, |
685 | 200x |
nm = original_inits$compartment) |
686 | 200x |
stable_sizes <- stable_sizes / p_act[names(stable_sizes)] |
687 |
## refr_sizes <- setNames(original_inits$refr_size, |
|
688 |
## nm = original_inits$compartment) |
|
689 |
## stable_sizes <- stable_sizes + refr_sizes[names(stable_sizes)] |
|
690 |
} |
|
691 |
# Return |
|
692 | 561x |
return(stable_sizes) |
693 |
} |
1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * matrix_exp_stan() |
|
4 | ||
5 |
#' Run a stan model from a network model, using matrix exponential to solve ODEs. |
|
6 |
#' |
|
7 |
#' Incorporate a loglik trace: https://mc-stan.org/loo/reference/extract_log_lik.html |
|
8 |
#' |
|
9 |
#' @param nm A \code{networkModel} object. |
|
10 |
#' @param iter A positive integer specifying the number of iterations for each |
|
11 |
#' chain (including warmup). The default is 2000. |
|
12 |
#' @param chains A positive integer specifying the number of Markov chains. |
|
13 |
#' The default is 4. |
|
14 |
#' @param cores Number of cores to use for parallel run. Default is |
|
15 |
#' \code{NULL}, which means to use the value stored in |
|
16 |
#' \code{options()[["mc.cores"]]} (or 1 if this value is not set). |
|
17 |
#' @param stanfit If TRUE, returns a `stanfit` object instead of the more |
|
18 |
#' classical `mcmc.list` object. |
|
19 |
#' @param use_fixed_values Boolean, if TRUE any parameter value set with |
|
20 |
#' \code{set_params()} will be taken as fixed during the MCMC run. Default |
|
21 |
#' is FALSE. |
|
22 |
#' @param vb Boolean, if TRUE will use \code{rstan::vb} for a quick approximate |
|
23 |
#' sampling of the posterior. Important note from \code{?rstan::vb}: |
|
24 |
#' "This is still considered an experimental feature. We recommend calling |
|
25 |
#' \code{stan} or \code{sampling} for final inferences and only using ‘vb’ to |
|
26 |
#' get a rough idea of the parameter distributions." |
|
27 |
#' @param ... Passed to \code{rstan::sampling}. |
|
28 |
#' |
|
29 |
#' @keywords internal |
|
30 |
#' @noRd |
|
31 | ||
32 |
matrix_exp_stan <- function(nm, iter = 2000, chains = 4, cores = NULL, |
|
33 |
stanfit = FALSE, use_fixed_values = FALSE, |
|
34 |
vb = FALSE, ...) { |
|
35 |
# Detect cores |
|
36 | 19x |
cores <- get_n_cores(cores = cores) |
37 |
# Convert network model to stan data |
|
38 | 19x |
stan.data <- prep_stan_data_expm(nm, use_fixed_values = use_fixed_values) |
39 |
# Fit the model |
|
40 | 19x |
stan.data[["ode_method"]] <- 1 # For matrix exponential |
41 |
# Approximate sampling (vb = TRUE) |
|
42 | 19x |
if (vb) { |
43 | ! |
if (stanfit) { |
44 | ! |
stop("vb not implemented for stanfit = TRUE") |
45 |
} |
|
46 | ! |
fits <- lapply(seq_len(chains), function(i) { |
47 | ! |
fit <- rstan::vb(stanmodels[["networkModel"]], |
48 | ! |
data = stan.data, |
49 | ! |
iter = iter, |
50 | ! |
pars = c("nonConstantParams", "log_lik", |
51 | ! |
"rawUniformParams", "rawHcauchyParams", |
52 | ! |
"rawBetaParams", "rawTrNormParams", |
53 | ! |
"rawExponentialParams", "rawGammaParams"), ...) |
54 | ! |
stopifnot(!"isotracer_stan_data" %in% names(attributes(fit))) |
55 | ! |
attr(fit, "isotracer_stan_data") <- stan.data |
56 | ! |
fit |
57 |
}) |
|
58 | ! |
fits <- lapply(fits, stanfit_to_named_mcmclist) |
59 | ! |
fits <- lapply(fits, function(i) i[[1]]) |
60 | ! |
return(coda::as.mcmc.list(fits)) |
61 |
} |
|
62 |
# Accurate sampling (vb = FALSE) |
|
63 | 19x |
fit <- rstan::sampling(stanmodels[["networkModel"]], |
64 | 19x |
data = stan.data, |
65 | 19x |
iter = iter, |
66 | 19x |
chains = chains, |
67 | 19x |
cores = cores, |
68 | 19x |
pars = c("nonConstantParams", "log_lik", |
69 | 19x |
"rawUniformParams", "rawHcauchyParams", |
70 | 19x |
"rawBetaParams", "rawTrNormParams", |
71 | 19x |
"rawExponentialParams", "rawGammaParams"), ...) |
72 | 19x |
stopifnot(!"isotracer_stan_data" %in% names(attributes(fit))) |
73 | 19x |
attr(fit, "isotracer_stan_data") <- stan.data |
74 |
# Return |
|
75 | 19x |
if (stanfit) { |
76 | 1x |
return(fit) |
77 |
} else { |
|
78 | 18x |
return(stanfit_to_named_mcmclist(stanfit = fit)) |
79 |
} |
|
80 |
} |
|
81 | ||
82 |
### * prep_stan_data_expm() |
|
83 | ||
84 |
#' Prepare stan data from a network model |
|
85 |
#' |
|
86 |
#' @param nm A \code{networkModel} object. |
|
87 |
#' @param use_fixed_values Boolean, if TRUE any parameter value set with |
|
88 |
#' \code{set_params()} will be taken as fixed during the MCMC run. Default |
|
89 |
#' is FALSE. |
|
90 |
#' |
|
91 |
#' @keywords internal |
|
92 |
#' @noRd |
|
93 | ||
94 |
prep_stan_data_expm <- function(nm, use_fixed_values = FALSE) { |
|
95 | 19x |
d <- list() |
96 | 19x |
params_nm <- params(nm, simplify = TRUE) |
97 | 19x |
priors_nm <- priors(nm, fix_set_params = use_fixed_values, |
98 | 19x |
quiet = TRUE) |
99 | 19x |
priors_nm <- priors_nm[match(params_nm, priors_nm[["in_model"]]), ] |
100 | 19x |
stopifnot(all(params_nm == priors_nm[["in_model"]])) |
101 |
# For now the stan model is only implemented for network models with the |
|
102 |
# same number of compartments on each row (a more general case where rows |
|
103 |
# can have different numbers of compartments is easily converted to this |
|
104 |
# case, by adding compartments without connections to fill the topology in |
|
105 |
# each row). |
|
106 | 19x |
stopifnot(length(unique(sapply(comps(nm), length))) == 1) |
107 |
# Counts |
|
108 | 19x |
d[["nComps"]] <- length(comps(nm)[[1]]) |
109 | 19x |
d[["nGroups"]] <- nrow(nm) |
110 | 19x |
d[["nParams"]] <- length(params_nm) |
111 |
# Encode priors |
|
112 | 19x |
d <- c(d, encode_priors(params_nm, priors_nm)) |
113 |
# Encode distribution families (for proportions and sizes) |
|
114 | 19x |
d <- c(d, encode_distrib_families(nm)) |
115 |
# Encode initial conditions |
|
116 | 19x |
d <- c(d, encode_init(nm)) |
117 |
# Encode steady state compartments |
|
118 | 19x |
d <- c(d, encode_steady(nm)) |
119 |
# Encode split compartments |
|
120 | 19x |
d <- c(d, encode_split(nm, params_nm)) |
121 |
# Encode decay rate for radioactive tracers |
|
122 | 19x |
lambda_decay <- attr(nm, "lambda_hl") |
123 | 19x |
if (is.null(lambda_decay)) { |
124 | 19x |
lambda_decay <- 0 |
125 |
} |
|
126 | 19x |
d[["lambda_decay"]] <- lambda_decay |
127 |
# Encode intervals in-between events |
|
128 | 19x |
d <- c(d, encode_intervals(nm)) |
129 |
# Encode pulse events |
|
130 | 19x |
d <- c(d, encode_pulse_events(nm)) |
131 |
# Encode unique obs times |
|
132 | 19x |
d <- c(d, encode_unique_obs_times(nm)) |
133 |
# Encode individual obs |
|
134 | 19x |
d <- c(d, encode_individual_obs(nm, params_nm)) |
135 |
# Encode uptake rates (upsilons) |
|
136 | 19x |
d <- c(d, encode_upsilons(nm, params_nm)) |
137 |
# Encode losses (lambdas) |
|
138 | 19x |
d <- c(d, encode_lambdas(nm, params_nm)) |
139 |
# Add encoding for Euler (so that the Stan model does not crash) |
|
140 | 19x |
d[["allParams"]] <- params_nm |
141 | 19x |
euler <- encode_time_schemes(nm) |
142 | 19x |
stopifnot(!any(names(euler) %in% names(d))) |
143 | 19x |
d <- c(d, euler) |
144 | 19x |
return(d) |
145 |
} |
|
146 | ||
147 |
### * encode_intervals() |
|
148 | ||
149 |
#' Encode time intervals between events |
|
150 |
#' |
|
151 |
#' @param nm A \code{networkModel} object. |
|
152 |
#' |
|
153 |
#' @keywords internal |
|
154 |
#' @noRd |
|
155 | ||
156 |
encode_intervals <- function(nm) { |
|
157 | 27x |
o <- list() |
158 | 27x |
nGroups <- nrow(nm) |
159 | 27x |
timelines <- lapply(seq_len(nrow(nm)), function(i) { |
160 | 40x |
nm_row_get_timeline(nm[i, ]) |
161 |
}) |
|
162 | 27x |
nTimeIntervals <- sapply(timelines, '[[', "nIntervals") |
163 | 27x |
o[["maxNtimeIntervals"]] <- max(nTimeIntervals) |
164 | 27x |
o[["nTimeIntervals"]] <- setNames(c(nTimeIntervals, 0), # Padded |
165 | 27x |
nm = c(paste0("grp", seq_len(nrow(nm))), "padding")) |
166 | 27x |
durations <- lapply(timelines, function(x) { |
167 | 40x |
x[["intervalEnds"]] - x[["intervalStarts"]] |
168 |
}) |
|
169 | 27x |
o[["intervalsLengths"]] <- array(0, dim = c(max(nTimeIntervals), nGroups), |
170 | 27x |
dimnames = list(seq_len(max(nTimeIntervals)), |
171 | 27x |
paste0("grp", seq_len(nGroups)))) |
172 | 27x |
for (g in seq_len(nGroups)) { |
173 | 40x |
o[["intervalsLengths"]][1:(o[["nTimeIntervals"]][g]), g] <- durations[[g]] |
174 |
} |
|
175 | 27x |
o |
176 |
} |
|
177 | ||
178 |
### ** nm_row_get_timeline() |
|
179 | ||
180 |
#' @examples |
|
181 |
#' nm_row_get_timeline(aquarium_mod[1, ]) |
|
182 |
#' nm_row_get_timeline(trini_mod[1, ]) |
|
183 |
#' @keywords internal |
|
184 |
#' @noRd |
|
185 |
nm_row_get_timeline <- function(nm_row) { |
|
186 | 140x |
events <- nm_row_get_events(nm_row) |
187 | 140x |
obs <- nm_row_get_obs(nm_row) |
188 | 140x |
o <- c(events, obs) |
189 | 140x |
o[["maxTime"]] <- max(c(o[["eventTimes"]], o[["obsTimes"]])) |
190 | 140x |
o[["nIntervals"]] <- o[["nEventTimes"]] + 1 |
191 | 140x |
o[["intervalStarts"]] <- c(0, o[["eventTimes"]]) |
192 | 140x |
o[["intervalEnds"]] <- c(o[["eventTimes"]], o[["maxTime"]]) |
193 | 140x |
o[["intervalStartsWithAnEvent"]] <- o[["intervalStarts"]] %in% o[["eventTimes"]] |
194 | 140x |
o[["obsIntervalIndex"]] <- sapply(o[["obsTimes"]], function(i) { |
195 | 704x |
w <- which(o[["intervalStarts"]] <= i) |
196 | 704x |
if (length(w) > 0) { return(max(w)) } |
197 | ! |
return(o[["nIntervals"]]) |
198 |
}) |
|
199 | 140x |
o[["elapsedTimeSinceEvent"]] <- o[["obsTimes"]] - o[["intervalStarts"]][o[["obsIntervalIndex"]]] |
200 | 140x |
return(o) |
201 |
} |
|
202 | ||
203 |
### ** plot_nm_row_timeline() |
|
204 | ||
205 |
#' @param nm_row_timeline Output from \code{nm_row_get_timeline}. |
|
206 |
#' @return None, called for side-effect of drawing a timeline plot where |
|
207 |
#' boundaries are light gray, events are red, and observation times are |
|
208 |
#' blue. |
|
209 |
#' @examples |
|
210 |
#' plot_nm_row_timeline(nm_row_get_timeline(aquarium_mod[1, ])) |
|
211 |
#' plot_nm_row_timeline(nm_row_get_timeline(trini_mod[1, ])) |
|
212 |
#' @keywords internal |
|
213 |
#' @noRd |
|
214 |
plot_nm_row_timeline <- function(nm_row_timeline) { |
|
215 | ! |
z <- nm_row_timeline |
216 | ! |
plot(0, type = "n", xlim = c(0, z[["maxTime"]]), ylim = c(0, 1), |
217 | ! |
xlab = "Time", axes = FALSE, ylab = "") |
218 | ! |
graphics::axis(1) |
219 | ! |
graphics::lines(c(0, 0), c(0, 1), col = "lightgray") |
220 | ! |
graphics::lines(rep(z[["maxTime"]], 2), c(0, 1), col = "lightgray") |
221 | ! |
for (i in seq_along(z[["eventTimes"]])) { |
222 | ! |
graphics::lines(rep(z[["eventTimes"]], 2), c(0, 1), col = "red", lty = 2, lwd = 2) |
223 |
} |
|
224 | ! |
graphics::points(z[["obsTimes"]], y = rep(0.5, length(z[["obsTimes"]])), |
225 | ! |
pch = 21, col = "blue", bg = "blue") |
226 |
} |
|
227 | ||
228 |
### ** nm_row_get_events() |
|
229 | ||
230 |
nm_row_get_events <- function(nm_row) { |
|
231 | 140x |
stopifnot(nrow(nm_row) == 1) |
232 | 140x |
o <- list() |
233 | 140x |
if (!"events" %in% colnames(nm_row)) { |
234 | 100x |
o[["eventTimes"]] <- numeric() |
235 |
} else { |
|
236 | 40x |
o[["eventTimes"]] <- sort(unique(nm_row[["events"]][[1]][["time"]])) |
237 |
} |
|
238 | 140x |
o[["nEventTimes"]] <- length(o[["eventTimes"]]) |
239 | 140x |
o <- o[c("nEventTimes", "eventTimes")] |
240 | 140x |
return(o) |
241 |
} |
|
242 | ||
243 |
### ** nm_row_get_obs() |
|
244 | ||
245 |
nm_row_get_obs <- function(nm_row) { |
|
246 | 140x |
stopifnot(nrow(nm_row) == 1) |
247 | 140x |
stopifnot("observations" %in% colnames(nm_row)) |
248 | 140x |
o <- list() |
249 | 140x |
o[["obsTimes"]] <- sort(unique(nm_row[["observations"]][[1]][["time"]])) |
250 | 140x |
o[["nObsTimes"]] <- length(o[["obsTimes"]]) |
251 | 140x |
o <- o[c("nObsTimes", "obsTimes")] |
252 | 140x |
return(o) |
253 |
} |
|
254 | ||
255 |
### * encode_pulse_events() |
|
256 | ||
257 |
#' Encode pulse events (for Stan model with matrix exponential) |
|
258 |
#' |
|
259 |
#' @param nm A \code{networkModel} object. |
|
260 |
#' |
|
261 |
#' @keywords internal |
|
262 |
#' @noRd |
|
263 | ||
264 |
encode_pulse_events <- function(nm) { |
|
265 | 19x |
o <- list() |
266 | 19x |
if (!"events" %in% names(nm)) { |
267 | 18x |
nm[["events"]] <- rep(list(NULL), nrow(nm)) |
268 |
} |
|
269 | 19x |
comps <- comps(nm)[[1]] |
270 | 19x |
nGroups <- nrow(nm) |
271 | 19x |
timelines <- lapply(seq_len(nrow(nm)), function(i) { |
272 | 30x |
nm_row_get_timeline(nm[i, ]) |
273 |
}) |
|
274 | 19x |
nPulseEvents <- sapply(seq_len(nrow(nm)), function(i) { |
275 | 30x |
n <- nrow(nm[i, ][["events"]][[1]]) |
276 | 30x |
ifelse(is.null(n), 0, n) |
277 |
}) |
|
278 | 19x |
o[["maxNpulseEvents"]] <- max(nPulseEvents) |
279 | 19x |
o[["nPulseEvents"]] <- setNames(c(nPulseEvents, 0), # Padded |
280 | 19x |
nm = c(paste0("grp", seq_len(nrow(nm))), "padding")) |
281 | 19x |
interval_indices <- list() |
282 | 19x |
comp_indices <- list() |
283 | 19x |
marked <- list() |
284 | 19x |
unmarked <- list() |
285 | 19x |
for (g in seq_len(nGroups)) { |
286 | 30x |
x <- nm[["events"]][[g]] |
287 | 30x |
if (!is.null(x)) { |
288 | 2x |
x <- x[order(x[["time"]]), ] |
289 | 2x |
stopifnot(all(x$event == "pulse")) |
290 | 2x |
interval_indices[[g]] <- match(x[["time"]], timelines[[g]][["intervalStarts"]]) |
291 | 2x |
comp_indices[[g]] <- match(x[["compartment"]], comps) |
292 | 2x |
marked[[g]] <- sapply(x$characteristics, '[[', "marked") |
293 | 2x |
unmarked[[g]] <- sapply(x$characteristics, '[[', "unmarked") |
294 |
} |
|
295 |
} |
|
296 | 19x |
o[["pulseEventsIndices"]] <- array(0, dim = c(max(nPulseEvents), 2, nGroups), |
297 | 19x |
dimnames = list(seq_len(max(nPulseEvents)), |
298 | 19x |
c("interval", "comp"), |
299 | 19x |
paste0("grp", seq_len(nGroups)))) |
300 | 19x |
o[["pulseEventsQuantities"]] <- array(0, dim = c(max(nPulseEvents), 2, nGroups), |
301 | 19x |
dimnames = list(seq_len(max(nPulseEvents)), |
302 | 19x |
c("unmarked", "marked"), |
303 | 19x |
paste0("grp", seq_len(nGroups)))) |
304 | 19x |
for (g in seq_len(nGroups)) { |
305 | 30x |
if (!is.null(nm[["events"]][[g]])) { |
306 | 2x |
o[["pulseEventsIndices"]][1:(o[["nPulseEvents"]][g]), 1, g] <- interval_indices[[g]] |
307 | 2x |
o[["pulseEventsIndices"]][1:(o[["nPulseEvents"]][g]), 2, g] <- comp_indices[[g]] |
308 | 2x |
o[["pulseEventsQuantities"]][1:(o[["nPulseEvents"]][g]), 1, g] <- unmarked[[g]] |
309 | 2x |
o[["pulseEventsQuantities"]][1:(o[["nPulseEvents"]][g]), 2, g] <- marked[[g]] |
310 |
} |
|
311 |
} |
|
312 | 19x |
o |
313 |
} |
|
314 | ||
315 |
### * encode_unique_obs_times() |
|
316 | ||
317 |
#' Encode unique observation times (for Stan model with matrix exponential) |
|
318 |
#' |
|
319 |
#' @param nm A \code{networkModel} object. |
|
320 |
#' |
|
321 |
#' @keywords internal |
|
322 |
#' @noRd |
|
323 | ||
324 |
encode_unique_obs_times <- function(nm) { |
|
325 | 27x |
o <- list() |
326 | 27x |
comps <- comps(nm)[[1]] |
327 | 27x |
nGroups <- nrow(nm) |
328 | 27x |
timelines <- lapply(seq_len(nrow(nm)), function(i) { |
329 | 40x |
nm_row_get_timeline(nm[i, ]) |
330 |
}) |
|
331 | 27x |
nObsTimes <- sapply(timelines, '[[', "nObsTimes") |
332 | 27x |
o[["maxNobsTimes"]] <- max(nObsTimes) |
333 | 27x |
o[["nObsTimes"]] <- setNames(c(nObsTimes, 0), # Padded |
334 | 27x |
nm = c(paste0("grp", seq_len(nrow(nm))), "padding")) |
335 | 27x |
o[["elapsedTimeSinceEvent"]] <- array(0, dim = c(nGroups, max(nObsTimes)), |
336 | 27x |
dimnames = list(paste0("grp", seq_len(nGroups)), |
337 | 27x |
seq_len(max(nObsTimes)))) |
338 | 27x |
o[["obsIntervalsIndices"]] <- array(0, dim = c(nGroups, max(nObsTimes)), |
339 | 27x |
dimnames = list(paste0("grp", seq_len(nGroups)), |
340 | 27x |
seq_len(max(nObsTimes)))) |
341 | 27x |
for (g in seq_len(nGroups)) { |
342 | 40x |
o[["elapsedTimeSinceEvent"]][g, 1:(o[["nObsTimes"]][g])] <- timelines[[g]][["elapsedTimeSinceEvent"]] |
343 | 40x |
o[["obsIntervalsIndices"]][g, 1:(o[["nObsTimes"]][g])] <- timelines[[g]][["obsIntervalIndex"]] |
344 |
} |
|
345 | 27x |
o |
346 |
} |
|
347 | ||
348 |
### * encode_individual_obs() |
|
349 | ||
350 |
#' Encode individual observations (for Stan model with matrix exponential) |
|
351 |
#' |
|
352 |
#' @param nm A \code{networkModel} object. |
|
353 |
#' @param allParams Parameters of the network model. |
|
354 |
#' |
|
355 |
#' @keywords internal |
|
356 |
#' @noRd |
|
357 | ||
358 |
encode_individual_obs <- function(nm, allParams) { |
|
359 | 19x |
o <- list() |
360 | 19x |
comps <- comps(nm)[[1]] |
361 | 19x |
nGroups <- nrow(nm) |
362 | 19x |
zeta_by_comp <- attr(nm, "size_zeta_per_compartment") |
363 | 19x |
if (is.null(zeta_by_comp)) { |
364 | ! |
zeta_by_comp<- FALSE |
365 |
} |
|
366 | 19x |
timelines <- lapply(seq_len(nrow(nm)), function(i) { |
367 | 30x |
nm_row_get_timeline(nm[i, ]) |
368 |
}) |
|
369 |
# TODO Add filtering to keep only compartments present in topo |
|
370 |
# TODO Handle gracefully the case without observations |
|
371 |
# Get sizes |
|
372 | 19x |
sizes <- purrr::map(nm$observations, function(x) { |
373 | 30x |
na.omit(x[, c("compartment", "size", "time")]) |
374 |
}) |
|
375 |
# Get proportions |
|
376 | 19x |
props <- purrr::map(nm$observations, function(x) { |
377 | 30x |
na.omit(x[, c("compartment", "proportion", "time")]) |
378 |
}) |
|
379 |
# Convert original time into indices of unique observation times |
|
380 | 19x |
for (g in seq_len(nGroups)) { |
381 | 30x |
sizes[[g]][["timepoint"]] <- match(sizes[[g]][["time"]], timelines[[g]][["obsTimes"]]) |
382 | 30x |
props[[g]][["timepoint"]] <- match(props[[g]][["time"]], timelines[[g]][["obsTimes"]]) |
383 |
} |
|
384 |
# Encode compartments |
|
385 | 19x |
for (g in seq_len(nGroups)) { |
386 | 30x |
sizes[[g]][["compartment"]] <- match(sizes[[g]][["compartment"]], comps) |
387 | 30x |
props[[g]][["compartment"]] <- match(props[[g]][["compartment"]], comps) |
388 |
} |
|
389 |
# Encode sizes and props |
|
390 | 19x |
o[["nSizesObs"]] <- c(purrr::map_dbl(sizes, nrow), 0) # Padded |
391 | 19x |
names(o[["nSizesObs"]]) <- c(paste0("grp", seq_len(nrow(nm))), "padding") |
392 | 19x |
o[["nPropsObs"]] <- c(purrr::map_dbl(props, nrow), 0) # Padded |
393 | 19x |
names(o[["nPropsObs"]]) <- c(paste0("grp", seq_len(nrow(nm))), "padding") |
394 | 19x |
o[["maxNsizesObs"]] <- max(o[["nSizesObs"]]) |
395 | 19x |
o[["maxNpropsObs"]] <- max(o[["nPropsObs"]]) |
396 |
# Prepare containers |
|
397 | 19x |
o[["sizesObsIndices"]] <- array(0, dim = c(o[["maxNsizesObs"]], 3, nGroups), |
398 | 19x |
dimnames = list(seq_len(o[["maxNsizesObs"]]), |
399 | 19x |
c("comp", "timepoint", "zeta"), |
400 | 19x |
paste0("grp", seq_len(nGroups)))) |
401 | 19x |
o[["sizesObs"]] <- array(0, dim = c(o[["maxNsizesObs"]], nGroups), |
402 | 19x |
dimnames = list(seq_len(o[["maxNsizesObs"]]), |
403 | 19x |
paste0("grp", seq_len(nGroups)))) |
404 | 19x |
o[["propsObsIndices"]] <- array(0, dim = c(o[["maxNpropsObs"]], 3, nGroups), |
405 | 19x |
dimnames = list(seq_len(o[["maxNpropsObs"]]), |
406 | 19x |
c("comp", "timepoint", "eta"), |
407 | 19x |
paste0("grp", seq_len(nGroups)))) |
408 | 19x |
o[["propsObs"]] <- array(0, dim = c(o[["maxNpropsObs"]], nGroups), |
409 | 19x |
dimnames = list(seq_len(o[["maxNpropsObs"]]), |
410 | 19x |
paste0("grp", seq_len(nGroups)))) |
411 |
# Fill containers |
|
412 | 19x |
for (g in seq_len(nGroups)) { |
413 | 30x |
if (o[["nSizesObs"]][g] > 0) { |
414 | 30x |
o[["sizesObsIndices"]][1:o[["nSizesObs"]][g], 1:2, g] <- as.matrix(sizes[[g]][, c("compartment", "timepoint")]) |
415 | 30x |
o[["sizesObs"]][1:o[["nSizesObs"]][g], g] <- as.matrix(sizes[[g]][, c("size")]) |
416 | 30x |
if (!zeta_by_comp) { |
417 | 30x |
zeta_global <- nm[["parameters"]][[g]]$in_model[nm[["parameters"]][[g]]$in_replicate == "zeta"] |
418 | 30x |
zeta_index <- match(zeta_global, allParams) |
419 | 30x |
o[["sizesObsIndices"]][1:o[["nSizesObs"]][g], 3, g] <- zeta_index |
420 |
} else { |
|
421 | ! |
comps <- colnames(nm$topology[[g]]) |
422 | ! |
comps <- setNames(seq_along(comps), nm = comps) |
423 | ! |
zeta_replicate <- paste0("zeta_", names(comps)[sizes[[g]][["compartment"]]]) |
424 | ! |
zeta_global <- nm[["parameters"]][[g]]$in_model[match(zeta_replicate, nm[["parameters"]][[g]]$in_replicate)] |
425 | ! |
zeta_index <- match(zeta_global, allParams) |
426 | ! |
o[["sizesObsIndices"]][1:o[["nSizesObs"]][g], 3, g] <- zeta_index |
427 |
} |
|
428 |
} |
|
429 |
} |
|
430 | 19x |
for (g in seq_len(nGroups)) { |
431 | 30x |
if (o[["nPropsObs"]][g] > 0) { |
432 | 30x |
o[["propsObsIndices"]][1:o[["nPropsObs"]][g], 1:2, g] <- as.matrix(props[[g]][, c("compartment", "timepoint")]) |
433 | 30x |
o[["propsObs"]][1:o[["nPropsObs"]][g], g] <- as.matrix(props[[g]][, c("proportion")]) |
434 | 30x |
eta_global <- nm[["parameters"]][[g]]$in_model[nm[["parameters"]][[g]]$in_replicate == "eta"] |
435 | 30x |
eta_index <- match(eta_global, allParams) |
436 | 30x |
o[["propsObsIndices"]][1:o[["nPropsObs"]][g], 3, g] <- eta_index |
437 |
} |
|
438 |
} |
|
439 |
# Return |
|
440 | 19x |
return(o) |
441 |
} |
1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * as.derived.mcmc.list() |
|
4 | ||
5 |
#' @keywords internal |
|
6 |
#' @noRd |
|
7 | ||
8 |
as.derived.mcmc.list <- function(x) { |
|
9 | ! |
if (!"derived.mcmc.list" %in% class(x)) { |
10 | ! |
return(structure(x, class = c("derived.mcmc.list", class(x)))) |
11 |
} else { |
|
12 | ! |
return(x) |
13 |
} |
|
14 |
} |
|
15 | ||
16 |
### * build_uptake_mask |
|
17 | ||
18 |
### ** Doc |
|
19 | ||
20 |
#' Build a foodweb uptake mask based on links |
|
21 |
#' |
|
22 |
#' @section Link format: |
|
23 |
#' |
|
24 |
#' Links are defined by a string describing connections between groups of |
|
25 |
#' compartments and their directions using \code{->} and \code{<-}. The groups |
|
26 |
#' being connected can be a single compartment or several compartments. Several |
|
27 |
#' compartments can be separated either by commas or by spaces. Connection |
|
28 |
#' descriptions can be chained in a single string. |
|
29 |
#' |
|
30 |
#' Some valid links are for example \code{"NH4, NO3 -> epi"} or |
|
31 |
#' \code{"NH4, NO3 -> epi -> lepto, tricor"}. |
|
32 |
#' |
|
33 |
#' See the Examples section for more details. |
|
34 |
#' |
|
35 |
#' @param links A vector of strings defining the trophic links between |
|
36 |
#' compartments (see the examples) |
|
37 |
#' |
|
38 |
#' @return An uptake mask matrix with named columns and rows |
|
39 |
#' |
|
40 |
#' @examples |
|
41 |
#' f <- isotracer:::build_uptake_mask(links = "NH4, NO3 -> epi -> pseph, tricor") |
|
42 |
#' f |
|
43 |
#' |
|
44 |
#' # A larger foodweb |
|
45 |
#' links <- c("NH4, NO3 -> seston, epi, CBOM, FBOM", |
|
46 |
#' "seston -> lepto", "epi -> petro, pseph", |
|
47 |
#' "CBOM, FBOM -> eudan", "CBOM -> phyllo", |
|
48 |
#' "FBOM -> tricor -> arg, euthy") |
|
49 |
#' f2 <- isotracer:::build_uptake_mask(links = links) |
|
50 |
#' f2 |
|
51 |
#' |
|
52 |
#' @keywords internal |
|
53 |
#' @noRd |
|
54 | ||
55 |
### ** Code |
|
56 | ||
57 |
build_uptake_mask <- function(links) { |
|
58 |
# Get the compartment names |
|
59 | 40x |
compartments <- compartments_from_links(links) |
60 | 40x |
nComp <- length(compartments) |
61 |
# Split the links |
|
62 | 40x |
elementaryLinks <- unlist(lapply(links, parse_link_string)) |
63 | 40x |
stopifnot(all(grepl("->|<-", elementaryLinks))) |
64 | 40x |
pairs <- strsplit(elementaryLinks, "->|<-") |
65 | 40x |
stopifnot(all(unlist(pairs) %in% compartments)) |
66 |
# Create the uptake rate mask |
|
67 | 40x |
mask <- matrix(0, ncol = nComp, nrow = nComp, |
68 | 40x |
dimnames = list(compartments, compartments)) |
69 |
# Add the links to the uptake rate mask |
|
70 | 40x |
for (k in 1:length(pairs)) { |
71 | 135x |
i <- match(pairs[[k]][1], compartments) |
72 | 135x |
j <- match(pairs[[k]][2], compartments) |
73 | 135x |
if (grepl("->", elementaryLinks[k])) { |
74 | 132x |
mask[j,i] = 1 |
75 |
} else { |
|
76 | 3x |
mask[i,j] = 1 |
77 |
} |
|
78 |
} |
|
79 |
# Return |
|
80 | 40x |
stopifnot(all(diag(mask) == 0)) # No rates to self |
81 |
# Return |
|
82 | 40x |
return(mask) |
83 |
} |
|
84 | ||
85 |
### * compartments_from_links() |
|
86 | ||
87 |
#' Build a vector of unique compartments from link strings |
|
88 |
#' |
|
89 |
#' @param links A vector of link strings; see the examples for details about |
|
90 |
#' their format |
|
91 |
#' |
|
92 |
#' @return A string vector containing unique compartments present in the links |
|
93 |
#' |
|
94 |
#' @examples |
|
95 |
#' links <- c("NH4, NO3 -> epi -> tricor, pseph, petro", "tricor -> arg") |
|
96 |
#' compartments <- isotracer:::compartments_from_links(links) |
|
97 |
#' |
|
98 |
#' @keywords internal |
|
99 |
#' @noRd |
|
100 | ||
101 |
compartments_from_links <- function(links) { |
|
102 | 40x |
links <- unlist(lapply(links, parse_link_string)) |
103 | 40x |
return(sort(unique(unlist(strsplit(links, "->|<-"))))) |
104 |
} |
|
105 | ||
106 |
### * parse_link_string() |
|
107 | ||
108 |
#' Parse a link string into simple links |
|
109 |
#' |
|
110 |
#' @param link String, see the examples for details about the format |
|
111 |
#' |
|
112 |
#' @return A vector containing the simple links. All simple links will have |
|
113 |
#' exactly one "->" or "<-" symbol, between two strings, with no |
|
114 |
#' whitespaces. |
|
115 |
#' |
|
116 |
#' @examples |
|
117 |
#' isotracer:::parse_link_string("NH4, NO3 -> epi -> tricor, pseph, petro") |
|
118 |
#' isotracer:::parse_link_string("tricor <- epi -> pseph") |
|
119 |
#' isotracer:::parse_link_string("NH4 NO3 -> epi seston FBOM CBOM") |
|
120 |
#' |
|
121 |
#' @keywords internal |
|
122 |
#' @noRd |
|
123 | ||
124 |
parse_link_string <- function(link) { |
|
125 |
# Split by connections |
|
126 | 116x |
links <- unlist(strsplit(link, split = "<-|->")) |
127 | 116x |
stopifnot(length(links) > 1) |
128 |
# Build pairs of groups |
|
129 | 116x |
pairs <- list() |
130 | 116x |
kLinks <- 1 |
131 | 116x |
for (i in 1:(length(links)-1)) { |
132 | 224x |
LHS <- unlist(strsplit(links[kLinks], split = " |,")) |
133 | 224x |
LHS <- LHS[LHS != ""] |
134 | 224x |
stopifnot(length(LHS) > 0) |
135 | 224x |
RHS <- unlist(strsplit(links[kLinks+1], split = " |,")) |
136 | 224x |
RHS <- RHS[RHS != ""] |
137 | 224x |
stopifnot(length(RHS) > 0) |
138 | 224x |
pairs[[kLinks]] <- list(LHS, RHS) |
139 | 224x |
kLinks <- kLinks + 1 |
140 |
} |
|
141 |
# Get directions |
|
142 | 116x |
directions <- list() |
143 | 116x |
kLinkString <- 1 |
144 | 116x |
for (i in 1:(length(links)-1)) { |
145 | 224x |
kLinkString <- kLinkString + nchar(links[i]) |
146 | 224x |
directions[[i]] <- substr(link, start = kLinkString, |
147 | 224x |
stop = kLinkString + 1) |
148 | 224x |
kLinkString <- kLinkString + 2 |
149 |
} |
|
150 |
# Function to trim whitespaces |
|
151 |
## https://stackoverflow.com/questions/2261079/how-to-trim-leading-and-trailing-whitespace-in-r |
|
152 | 116x |
trim <- function(x) { |
153 | 810x |
return(gsub("^\\s+|\\s+$", "", x)) |
154 |
} |
|
155 |
# Build simple pairs |
|
156 | 116x |
out <- list() |
157 | 116x |
kOut <- 1 |
158 | 116x |
for (i in seq_along(pairs)) { |
159 | 224x |
for (k in seq_along(pairs[[i]][[1]])) { |
160 | 236x |
for (l in seq_along(pairs[[i]][[2]])) { |
161 | 270x |
out[kOut] <- paste0(trim(pairs[[i]][[1]][k]), trim(directions[[i]]), |
162 | 270x |
trim(pairs[[i]][[2]][l])) |
163 |
|
|
164 | 270x |
kOut <- kOut + 1 |
165 |
} |
|
166 |
} |
|
167 |
} |
|
168 |
# Return |
|
169 | 116x |
return(unlist(out)) |
170 |
} |
|
171 | ||
172 |
### * make_init() |
|
173 | ||
174 |
### ** Doc |
|
175 | ||
176 |
#' Build tibble of initial conditions for network model |
|
177 |
#' |
|
178 |
#' @param data Data frame |
|
179 |
#' @param comp String, name of the column containing the names of the |
|
180 |
#' compartments |
|
181 |
#' @param size String, name of the column containing the sizes of the |
|
182 |
#' compartments |
|
183 |
#' @param prop String, name of the column containing values for the |
|
184 |
#' proportion of marked material |
|
185 |
#' @param group_by Optional, vector of strings giving the name of the columns |
|
186 |
#' to use to group observations (e.g. per stream, per forest, per |
|
187 |
#' experiment, ...) |
|
188 |
#' |
|
189 |
#' @return A tibble with one row per grouping level |
|
190 |
#' |
|
191 |
#' @importFrom stats setNames |
|
192 |
#' |
|
193 |
#' @examples |
|
194 |
#' library(tibble) |
|
195 |
#' data <- tibble(comps = rep(c("NH4", "algae", "daphnia", "NH4", "algae", |
|
196 |
#' "daphnia"), 2), |
|
197 |
#' sizes = runif(12, 1, 10), |
|
198 |
#' props = runif(12, 0.01, 0.05), |
|
199 |
#' aquariumID = c(rep("aq01", 6), rep("aq02", 6)), |
|
200 |
#' temperature = rep(rep(c("low", "high"), each = 3), 2)) |
|
201 |
#' data |
|
202 |
#' |
|
203 |
#' # No grouping variable |
|
204 |
#' z <- isotracer:::make_init(data, "comps", "sizes", "props") |
|
205 |
#' z$initial |
|
206 |
#' |
|
207 |
#' # One grouping variable |
|
208 |
#' z <- isotracer:::make_init(data, "comps", "sizes", "props", group_by = "aquariumID") |
|
209 |
#' z |
|
210 |
#' |
|
211 |
#' # Several grouping variables |
|
212 |
#' z <- isotracer:::make_init(data, "comps", "sizes", "props", |
|
213 |
#' group_by = c("aquariumID", "temperature")) |
|
214 |
#' z |
|
215 |
#' |
|
216 |
#' @keywords internal |
|
217 |
#' @noRd |
|
218 | ||
219 |
### ** Code |
|
220 | ||
221 |
make_init <- function(data, comp, size, prop, group_by = NULL) { |
|
222 |
# Extract the columns containing initial conditions information |
|
223 | 29x |
initial <- tibble::as_tibble(data[, c(comp, size, prop)]) |
224 | 29x |
colnames(initial) <- c("compartment", "size", "proportion") |
225 | 29x |
initial$compartment <- as.character(initial$compartment) |
226 |
# If no grouping variable is given, just return a tibble with one row |
|
227 | 29x |
if (is.null(group_by)) { |
228 | 17x |
out <- tibble::tibble("initial" = list(initial), |
229 | 17x |
"group" = list(NULL)) |
230 |
} else { |
|
231 |
# Else, get group levels |
|
232 | 12x |
for (g in group_by) { |
233 | 15x |
data[[g]] <- as.character(data[[g]]) |
234 |
} |
|
235 | 12x |
group <- as.character(interaction(data[, group_by])) |
236 | 12x |
groupLevels <- unique(group) |
237 |
# Build the initial conditions sets (list of data frames) |
|
238 | 12x |
initialSets <- lapply(seq_along(groupLevels), function(i) { |
239 | 30x |
focalGroup <- groupLevels[i] |
240 | 30x |
indices <- which(group == focalGroup) |
241 | 30x |
return(initial[indices, ]) |
242 |
}) |
|
243 |
# Build the grouping variables sets (list of named vectors) |
|
244 | 12x |
groupingVariablesSets <- lapply(seq_along(groupLevels), function(i) { |
245 | 30x |
focalGroup <- groupLevels[i] |
246 | 30x |
indices <- which(group == focalGroup) |
247 | 30x |
tmp <- unique(data[indices, group_by]) |
248 | 30x |
stopifnot(nrow(tmp) == 1) |
249 | 30x |
tmp <- setNames(as.character(tmp), nm = group_by) |
250 | 30x |
return(tmp) |
251 |
}) |
|
252 |
# Put initial conditions and groups together |
|
253 | 12x |
groups <- tibble::tibble("group" = groupingVariablesSets) |
254 | 12x |
initials <- tibble::tibble("initial" = initialSets) |
255 | 12x |
out <- tibble::as_tibble(cbind(initials, groups)) |
256 |
} |
|
257 |
# Return |
|
258 | 29x |
row.names(out) <- NULL |
259 | 29x |
return(out) |
260 |
} |
|
261 | ||
262 |
### * nm_is_grouped() |
|
263 | ||
264 |
#' Is a network model grouped into replicate units? |
|
265 |
#' |
|
266 |
#' @param nm Network model |
|
267 |
#' |
|
268 |
#' @return Boolean |
|
269 |
#' |
|
270 |
#' @keywords internal |
|
271 |
#' @noRd |
|
272 | ||
273 |
nm_is_grouped <- function(nm) { |
|
274 | 182x |
if (!"group" %in% colnames(nm)) { |
275 | 62x |
return(FALSE) |
276 |
} |
|
277 | 120x |
if (nrow(nm) == 0) { |
278 | ! |
return(FALSE) |
279 |
} |
|
280 | 120x |
if (nrow(nm) == 1 && is.null(nm$group[[1]])) { |
281 | 30x |
return(FALSE) |
282 |
} |
|
283 | 90x |
return(TRUE) |
284 |
} |
|
285 | ||
286 |
### * compatible_groups() |
|
287 | ||
288 |
#' Check that two tibbles have compatible "group" columns for merging |
|
289 |
#' |
|
290 |
#' @param x First tibble |
|
291 |
#' @param y Second tibble |
|
292 |
#' @param error Boolean, raise error when incompatibility is encountered? |
|
293 |
#' |
|
294 |
#' @return TRUE if x and y are compatible. If they are not, return FALSE if |
|
295 |
#' \code{error} is FALSE (the default) or raise an error when \code{error} |
|
296 |
#' is TRUE. |
|
297 |
#' |
|
298 |
#' @keywords internal |
|
299 |
#' @noRd |
|
300 | ||
301 |
compatible_groups <- function(x, y, error = FALSE) { |
|
302 | 11x |
gx <- nm_get_groups(x) |
303 | 11x |
gy <- nm_get_groups(y) |
304 | 11x |
cx <- colnames(gx) |
305 | 11x |
cy <- colnames(gy) |
306 |
# Check that one column set is contained in the other |
|
307 | 11x |
if (!(all(cx %in% cy) | all(cy %in% cx))) { |
308 | ! |
if (!error) { |
309 | ! |
return(FALSE) |
310 |
} else { |
|
311 | ! |
stop("One set of grouping variables is not contained within the other.\n", |
312 | ! |
"In x but not in y: ", paste0(cx[!cx %in% cy], collapse = ", "), "\n", |
313 | ! |
"In y but not in x: ", paste0(cy[!cy %in% cx], collapse = ", ")) |
314 |
} |
|
315 |
} |
|
316 |
# Check that the levels of common columns are compatible |
|
317 | 11x |
cs <- intersect(cx, cy) |
318 | 11x |
for (ics in cs) { |
319 | 14x |
lcx <- unique(gx[[ics]]) |
320 | 14x |
lcy <- unique(gy[[ics]]) |
321 | 14x |
if (!setequal(lcx, lcy)) { |
322 | ! |
if (!error) { |
323 | ! |
return(FALSE) |
324 |
} else { |
|
325 | ! |
stop("The levels for the grouping variable \"", ics, "\" are not the same ", |
326 | ! |
"across x and y.") |
327 |
} |
|
328 |
} |
|
329 |
} |
|
330 |
# Compatible groupings |
|
331 | 11x |
return(TRUE) |
332 |
} |
|
333 | ||
334 |
### * nm_get_groups() |
|
335 | ||
336 |
#' Return a tibble with the groups of a network model |
|
337 |
#' |
|
338 |
#' @param nm A network model |
|
339 |
#' @param error Boolean, if TRUE raise an error if nm is not grouped, otherwise |
|
340 |
#' return NULL if nm is not grouped. |
|
341 |
#' |
|
342 |
#' @return A tibble, with rows in the same order as in the input |
|
343 |
#' |
|
344 |
#' @examples |
|
345 |
#' x <- tibble::tribble(~col, ~group, |
|
346 |
#' "red", c(sp = "dog", name = "toto"), |
|
347 |
#' "blue", c(sp = "dog", name = "tata"), |
|
348 |
#' "yellow", c(sp = "cat", name = "felix")) |
|
349 |
#' isotracer:::nm_get_groups(x) |
|
350 |
#' |
|
351 |
#' @keywords internal |
|
352 |
#' @noRd |
|
353 | ||
354 |
nm_get_groups <- function(nm, error = TRUE) { |
|
355 | 74x |
if (!nm_is_grouped(nm)) { |
356 | 19x |
if (error) { |
357 | ! |
stop("Input is not grouped.") |
358 |
} else { |
|
359 | 19x |
return(NULL) |
360 |
} |
|
361 |
} |
|
362 | 55x |
rows <- purrr::map(nm$group, function(g) { |
363 | 152x |
tidyr::spread(tibble::enframe(g), |
364 | 152x |
key = "name", value = "value") |
365 |
}) |
|
366 | 55x |
return(dplyr::bind_rows(rows)) |
367 |
} |
|
368 | ||
369 |
### * make_obs() |
|
370 | ||
371 |
### ** Doc |
|
372 | ||
373 |
#' Build observed data |
|
374 |
#' |
|
375 |
#' Note that nested grouping variables have to be coded appropriately by the |
|
376 |
#' user. For example, if there are two forests, each with three plots, the |
|
377 |
#' plots IDs cannot be "1", "2", "3" in both forests, but should be for example |
|
378 |
#' "1-1", "1-2", "1-3" in the first forest and "2-1", "2-2" and "2-3" in the |
|
379 |
#' second forest. |
|
380 |
#' |
|
381 |
#' @param data Data frame |
|
382 |
#' @param comp String, name of the column containing the names of the |
|
383 |
#' compartments |
|
384 |
#' @param size String, name of the column containing the sizes of the |
|
385 |
#' compartments |
|
386 |
#' @param prop String, name of the column containing values for the proportion |
|
387 |
#' of heavy tracer |
|
388 |
#' @param time String, name of the column containing values for the time |
|
389 |
#' @param group_by Optional, vector of strings giving the name of the columns |
|
390 |
#' to use to group observations (e.g. per stream, per forest, per |
|
391 |
#' experiment, ...). The name \code{tracerObservations} is reserved and |
|
392 |
#' should not be used. |
|
393 |
#' |
|
394 |
#' @return A tibble with one row per grouping level |
|
395 |
#' |
|
396 |
#' @importFrom stats setNames |
|
397 |
#' |
|
398 |
#' @examples |
|
399 |
#' library(tibble) |
|
400 |
#' data <- tibble(comps = rep(c("NH4", "algae", "daphnia", "NH4", "algae", |
|
401 |
#' "daphnia"), 2), |
|
402 |
#' sizes = runif(12, 1, 10), |
|
403 |
#' props = runif(12, 0.01, 0.05), |
|
404 |
#' time = runif(12, 0, 5), |
|
405 |
#' aquariumID = c(rep("aq01", 6), rep("aq02", 6)), |
|
406 |
#' temperature = rep(rep(c("low", "high"), each = 3), 2)) |
|
407 |
#' data |
|
408 |
#' |
|
409 |
#' # No grouping variable |
|
410 |
#' z <- isotracer:::make_obs(data, "comps", "sizes", "props", "time") |
|
411 |
#' z$observations |
|
412 |
#' |
|
413 |
#' # One grouping variable |
|
414 |
#' z <- isotracer:::make_obs(data, "comps", "sizes", "props", "time", |
|
415 |
#' group_by = "aquariumID") |
|
416 |
#' z |
|
417 |
#' |
|
418 |
#' # Several grouping variables |
|
419 |
#' z <- isotracer:::make_obs(data, "comps", "sizes", "props", "time", |
|
420 |
#' group_by = c("aquariumID", "temperature")) |
|
421 |
#' z |
|
422 |
#' |
|
423 |
#' @keywords internal |
|
424 |
#' @noRd |
|
425 | ||
426 |
### ** Code |
|
427 | ||
428 |
make_obs <- function(data, comp, size, prop, time, group_by = NULL) { |
|
429 |
# Extract the columns containing observations |
|
430 | 25x |
observations <- tibble::as_tibble(data[, c(comp, size, prop, time)]) |
431 | 25x |
colnames(observations) <- c("compartment", "size", "proportion", "time") |
432 | 25x |
observations$compartment <- as.character(observations$compartment) |
433 |
# If no grouping variable is given, just return a tibble with one column |
|
434 |
# and one row |
|
435 | 25x |
if (is.null(group_by)) { |
436 | 13x |
out <- tibble::tibble("observations" = list(observations), |
437 | 13x |
"group" = list(NULL)) |
438 |
} else { |
|
439 |
# Else, build a tibble with the grouping applied and use dplyr::group_by_ |
|
440 | 12x |
for (g in group_by) { |
441 | 15x |
data[[g]] <- as.character(data[[g]]) |
442 |
} |
|
443 | 12x |
group <- as.character(interaction(data[, group_by])) |
444 | 12x |
groupLevels <- unique(group) |
445 |
# Build the observation sets (list of data frames) |
|
446 | 12x |
observationSets <- lapply(seq_along(groupLevels), function(i) { |
447 | 30x |
focalGroup <- groupLevels[i] |
448 | 30x |
indices <- which(group == focalGroup) |
449 | 30x |
return(observations[indices, ]) |
450 |
}) |
|
451 |
# Build the grouping variables sets (list of named vectors) |
|
452 | 12x |
groupingVariablesSets <- lapply(seq_along(groupLevels), function(i) { |
453 | 30x |
focalGroup <- groupLevels[i] |
454 | 30x |
indices <- which(group == focalGroup) |
455 | 30x |
tmp <- unique(data[indices, group_by]) |
456 | 30x |
stopifnot(nrow(tmp) == 1) |
457 | 30x |
tmp <- setNames(as.character(tmp), nm = group_by) |
458 | 30x |
return(tmp) |
459 |
}) |
|
460 |
# Put observations and groups together |
|
461 | 12x |
groups <- tibble::tibble("group" = groupingVariablesSets) |
462 | 12x |
observations <- tibble::tibble("observations" = observationSets) |
463 | 12x |
out <- tibble::as_tibble(cbind(observations, groups)) |
464 |
} |
|
465 |
# Return |
|
466 | 25x |
row.names(out) <- NULL |
467 | 25x |
return(out) |
468 |
} |
|
469 | ||
470 |
### * merge_nm_by_groups() |
|
471 | ||
472 |
#' Merge a network model object and a tibble taking grouping into account |
|
473 |
#' |
|
474 |
#' @param nm The original \code{networkModel} object. |
|
475 |
#' @param tib The tibble to merge into \code{nm}. |
|
476 |
#' @param destination String, name of the column to merge into the network |
|
477 |
#' model. |
|
478 |
#' @param tib_name String, used when an error is raised. |
|
479 |
#' |
|
480 |
#' @return The updated network model object. |
|
481 |
#' |
|
482 |
#' @keywords internal |
|
483 |
#' @noRd |
|
484 | ||
485 |
merge_nm_by_group <- function(nm, tib, destination, tib_name = "unnamed tib") { |
|
486 | 54x |
nm_grouped <- nm_is_grouped(nm) |
487 | 54x |
tib_grouped <- nm_is_grouped(tib) |
488 | 54x |
previous_priors <- attr(nm, "priors") |
489 |
# nm not grouped, tib not grouped |
|
490 | 54x |
if (!nm_grouped & !tib_grouped) { |
491 | 30x |
stopifnot(nrow(nm) == 1 & nrow(tib) == 1) |
492 | 30x |
nm[[destination]][[1]] <- tib[[destination]][[1]] |
493 |
} |
|
494 |
# nm not grouped, tib grouped |
|
495 | 54x |
if (!nm_grouped & tib_grouped) { |
496 | 13x |
stopifnot(nrow(nm) == 1) |
497 | 13x |
nm_orig <- nm |
498 | 13x |
for (j in seq_len(nrow(tib) - 1)) { |
499 | 19x |
nm <- dplyr::bind_rows(nm, nm_orig) |
500 |
} |
|
501 | 13x |
nm[[destination]] <- tib[[destination]] |
502 | 13x |
nm$group <- tib$group |
503 |
} |
|
504 |
# nm grouped, tib not grouped |
|
505 | 54x |
if (nm_grouped & !tib_grouped) { |
506 | ! |
stopifnot(nrow(tib) == 1) |
507 | ! |
tib_orig <- tib |
508 | ! |
for (j in seq_len(nrow(nm) - 1)) { |
509 | ! |
tib <- dplyr::bind_rows(tib, |
510 | ! |
tib_orig) |
511 |
} |
|
512 | ! |
nm[[destination]] <- tib[[destination]] |
513 |
} |
|
514 |
# nm grouped and tib grouped |
|
515 | 54x |
if (nm_grouped & tib_grouped) { |
516 | 11x |
if (!compatible_groups(nm, tib)) { |
517 | ! |
stop("Network model and ", tib_name, " do not have compatible grouping.") |
518 |
} |
|
519 |
# Merge |
|
520 | 11x |
nm_g <- nm_get_groups(nm) |
521 | 11x |
tib_g <- nm_get_groups(tib) |
522 | 11x |
cols <- intersect(colnames(nm_g), colnames(tib_g)) |
523 | 11x |
allCols <- unique(c(colnames(nm_g), colnames(tib_g))) |
524 | 11x |
stopifnot(!".myRowNumberNm" %in% colnames(nm_g)) |
525 | 11x |
stopifnot(!".myRowNumberTib" %in% colnames(tib_g)) |
526 | 11x |
nm_g[[".myRowNumberNm"]] <- seq_len(nrow(nm_g)) |
527 | 11x |
tib_g[[".myRowNumberTib"]] <- seq_len(nrow(tib_g)) |
528 | 11x |
merged <- dplyr::full_join(nm_g, tib_g, by = cols) |
529 | 11x |
out_nm <- nm[merged[[".myRowNumberNm"]], ] |
530 | 11x |
out_tib <- tib[merged[[".myRowNumberTib"]], ] |
531 | 11x |
out_nm[[destination]] <- out_tib[[destination]] |
532 |
# Update groups |
|
533 | 11x |
groups <- merged[, allCols] |
534 | 11x |
groups <- lapply(seq_len(nrow(groups)), function(i) { |
535 | 28x |
unlist(groups[i, ]) |
536 |
}) |
|
537 | 11x |
out_nm[["group"]] <- groups |
538 | 11x |
nm <- out_nm |
539 |
} |
|
540 |
# Return |
|
541 | 54x |
attr(nm, "priors") <- previous_priors |
542 | 54x |
return(nm) |
543 |
} |
|
544 | ||
545 |
### * get_n_cores() |
|
546 | ||
547 |
# https://stackoverflow.com/questions/50571325/r-cran-check-fail-when-using-parallel-functions |
|
548 | ||
549 |
# From https://cran.r-project.org/doc/manuals/r-release/R-ints.html (on 2021-09-13): |
|
550 |
# |
|
551 |
# "_R_CHECK_LIMIT_CORES_ If set, check the usage of too many cores in package |
|
552 |
# parallel. If set to 'warn' gives a warning, to 'false' or 'FALSE' the check |
|
553 |
# is skipped, and any other non-empty value gives an error when more than 2 |
|
554 |
# children are spawned. Default: unset (but 'TRUE' for CRAN submission checks)." |
|
555 | ||
556 |
#' Determine the number of cores to use for parallelizable functions |
|
557 |
#' |
|
558 |
#' @param cores Number of cores to use. Default is \code{NULL}, which means to |
|
559 |
#' use the value stored in \code{options()[["mc.cores"]]} (or 1 if this |
|
560 |
#' value is not set). |
|
561 |
#' |
|
562 |
#' @return An integer, the number of cores to use for parallel computations. |
|
563 |
#' |
|
564 |
#' @keywords internal |
|
565 |
#' @noRd |
|
566 | ||
567 |
get_n_cores <- function(cores = NULL) { |
|
568 | 44x |
n_cores_max <- parallel::detectCores() |
569 | 44x |
n_cores_options <- options()[["mc.cores"]] |
570 | 44x |
on_cran_limit <- Sys.getenv("_R_CHECK_LIMIT_CORES_", "") |
571 | 44x |
on_cran_limit <- (nzchar(on_cran_limit) && on_cran_limit == "TRUE") |
572 | 5x |
if (is.null(cores)) { cores <- n_cores_options } |
573 | 5x |
if (is.null(cores)) { cores <- 1 } |
574 |
# The condition below is a convoluted way to check that cores is an integer |
|
575 |
# (because the simpler e.g. "is.integer(2)" is FALSE). |
|
576 |
# See also ?is.integer and the is.wholenumber() function in the examples. |
|
577 | 44x |
if ((!is.numeric(cores)) || (!((as.integer(cores) - cores) == 0)) || !(cores > 0)) { |
578 | ! |
stop("`cores` must be an integer > 0.") |
579 |
} |
|
580 | 44x |
if (cores > n_cores_max) { |
581 | ! |
warning(paste0("The provided number of cores (", cores, ") is greater ", |
582 | ! |
"than the number of available cores (", n_cores_max, |
583 | ! |
").\n"), |
584 | ! |
"Using the number of available cores instead.") |
585 | ! |
cores <- n_cores_max |
586 |
} |
|
587 | 44x |
if (cores > 1) { |
588 | 39x |
if (.Platform$OS.type == "windows") { |
589 | ! |
warning("Multiple core implementation not working on Windows; using 1 core.") |
590 | ! |
cores <- 1 |
591 |
} |
|
592 |
} |
|
593 | 44x |
if (on_cran_limit) { |
594 | ! |
message("CRAN limit for the number of cores was detected. Using cores <- min(cores, 2).") |
595 | ! |
cores <- min(cores, 2) |
596 |
} |
|
597 | 44x |
return(cores) |
598 |
} |
1 |
### * .onAttach() |
|
2 | ||
3 |
# Inspired by rstan code from |
|
4 |
# https://github.com/stan-dev/rstan/blob/develop/rstan/rstan/R/zzz.R |
|
5 | ||
6 |
.onAttach <- function(...) { |
|
7 | 2x |
packageStartupMessage("To automatically run isotracer in parallel ", |
8 | 2x |
"on a multicore CPU, you can call:\n", |
9 | 2x |
" options(mc.cores = parallel::detectCores())\n") |
10 |
} |
|
11 | ||
12 |
### * .onUnload() |
|
13 | ||
14 |
# Following recommendations from https://r-pkgs.org/src.html |
|
15 | ||
16 |
.onUnload <- function(libpath) { |
|
17 | ! |
library.dynam.unload("isotracer", libpath) |
18 |
} |
|
19 | ||
20 |
### * release_questions() |
|
21 | ||
22 |
# Cf. https://r-pkgs.org/release.html#release-submission |
|
23 |
release_questions <- function() { |
|
24 | ! |
c("Have you rebuilt the precompiled vignettes manually recently?", |
25 | ! |
"Have you updated the CRAN badge by running `.download-cran-version-badge.R`?") |
26 |
} |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * project() |
|
4 | ||
5 |
#' Calculate the trajectories of a network model |
|
6 |
#' |
|
7 |
#' @param nm A \code{networkModel} object. |
|
8 |
#' @param dt,grid_size Either the time step size for trajectory calculations |
|
9 |
#' (\code{dt}) or the number of points for the calculation (\code{grid_size}) |
|
10 |
#' can be provided. If none is provided, then a default grid size of 256 steps |
|
11 |
#' is used. |
|
12 |
#' @param at Optional, vector of time values at which the trajectory must be |
|
13 |
#' evaluated. |
|
14 |
#' @param end Time value for end point. If not provided, the last observation or |
|
15 |
#' event is used. |
|
16 |
#' @param flows Return flow values? The default is "no" and no flows are |
|
17 |
#' calculated. Other values are "total" (total flows summed up from beginning |
|
18 |
#' to end timepoint), "average" (average flows per time unit, equal to total |
|
19 |
#' flows divided by the projection duration), and "per_dt" (detailled flow |
|
20 |
#' values are returned for each interval dt of the projection). |
|
21 |
#' @param cached_ts,cached_ee Used for optimization by other functions, not for |
|
22 |
#' use by the package user. |
|
23 |
#' @param ignore_pulses Default to FALSE (i.e. apply pulses when projecting the |
|
24 |
#' network system). It is set to TRUE when calculating steady-state flows. |
|
25 |
#' |
|
26 |
#' @return A network model object with a \code{"trajectory"} column. |
|
27 |
#' |
|
28 |
#' @examples |
|
29 |
#' m <- aquarium_mod |
|
30 |
#' m <- set_params(m, sample_params(m)) |
|
31 |
#' z <- project(m) |
|
32 |
#' z <- project(m, flows = "per_dt") |
|
33 |
#' z <- project(m, flows = "total") |
|
34 |
#' z <- project(m, flows = "average") |
|
35 |
#' |
|
36 |
#' @export |
|
37 | ||
38 |
project <- function(nm, dt = NULL, grid_size = NULL, at = NULL, end = NULL, |
|
39 |
flows = "no", cached_ts = NULL, cached_ee = NULL, |
|
40 |
ignore_pulses = FALSE) { |
|
41 | 309x |
`!!` <- rlang::`!!` |
42 | 309x |
if (!flows %in% c("no", "total", "average", "per_dt")) { |
43 | ! |
stop("\"flows\" must be on of \"no\", \"total\", \"average\", \"per_dt\".") |
44 |
} |
|
45 | 309x |
if (flows == "no") { |
46 | 29x |
get_flows <- FALSE |
47 |
} else { |
|
48 | 280x |
get_flows <- TRUE |
49 |
} |
|
50 | 309x |
flow_option <- flows |
51 |
# Check that all parameters have values assigned |
|
52 | 309x |
params <- dplyr::bind_rows(nm$parameters) |
53 | 309x |
stopifnot(!any(is.na(params$value))) |
54 |
# Project row by row |
|
55 | 309x |
trajectories <- list() |
56 | 309x |
flows <- list() |
57 | 309x |
for (i in seq_len(nrow(nm))) { |
58 | 309x |
z <- project_row(nm[i, ], dt = dt, grid_size = grid_size, |
59 | 309x |
at = at, end = end, flows = get_flows, |
60 | 309x |
cached_ts = cached_ts[[i]], cached_ee = cached_ee[[i]], |
61 | 309x |
lambda_decay = attr(nm, "lambda_hl"), |
62 | 309x |
ignore_pulses = ignore_pulses) |
63 | 309x |
trajectories[[i]] <- z[, c("timepoints", "unmarked", "marked", "sizes", "proportions")] |
64 | 309x |
if (get_flows) { |
65 | 280x |
flows[[i]] <- z[, c("timepoints", "dt", "flows")] |
66 |
} |
|
67 |
} |
|
68 | 309x |
nm$trajectory <- trajectories |
69 | 309x |
if (get_flows) { |
70 | 280x |
if (flow_option == "per_dt") { |
71 | ! |
nm$flows <- flows |
72 | 280x |
} else if (flow_option == "total") { |
73 | ! |
for (i in seq_along(flows)) { |
74 | ! |
z <- flows[[i]][["flows"]][[1]] |
75 | ! |
if (is.na(z[[length(z)]] )) { |
76 | ! |
z <- z[1:(length(z)-1)] |
77 |
} |
|
78 | ! |
z <- dplyr::bind_rows(z) |
79 | ! |
z <- dplyr::group_by(z, `!!`(rlang::sym("from")), |
80 | ! |
`!!`(rlang::sym("to"))) |
81 | ! |
z <- dplyr::summarize(z, total_flow = sum(`!!`(rlang::sym("flow")))) |
82 | ! |
flows[[i]] <- z |
83 |
} |
|
84 | ! |
nm$flows <- flows |
85 | 280x |
} else if (flow_option == "average") { |
86 | 280x |
for (i in seq_along(flows)) { |
87 | 280x |
z <- flows[[i]][["flows"]][[1]] |
88 | 280x |
if (is.na(z[[length(z)]] )) { |
89 | 280x |
z <- z[1:(length(z)-1)] |
90 |
} |
|
91 | 280x |
z <- dplyr::bind_rows(z) |
92 | 280x |
z <- dplyr::group_by(z, `!!`(rlang::sym("from")), |
93 | 280x |
`!!`(rlang::sym("to"))) |
94 | 280x |
z <- dplyr::summarize(z, total_flow = sum(`!!`(rlang::sym("flow")))) |
95 | 280x |
duration <- diff(range(flows[[i]][["timepoints"]][[1]])) |
96 | 280x |
z$average_flow <- z$total_flow / duration |
97 | 280x |
z$total_flow <- NULL |
98 | 280x |
flows[[i]] <- z |
99 |
} |
|
100 | 280x |
nm$flows <- flows |
101 |
} else { |
|
102 | ! |
stop("Value of \"flows\" not allowed: ", flow_option) |
103 |
} |
|
104 |
} |
|
105 | 309x |
return(nm) |
106 |
} |
|
107 | ||
108 |
### * sample_from() |
|
109 | ||
110 |
#' Generate samples from a network model |
|
111 |
#' |
|
112 |
#' @param nm A \code{networkModel} object. |
|
113 |
#' @param at Vector of time values at which the samples should be taken. |
|
114 |
#' @param dt,grid_size Time step size or grid points, respectively. |
|
115 |
#' @param end Final timepoint used in the projections. |
|
116 |
#' @param error.draws Integer, number of draws from the error distribution for |
|
117 |
#' each sample (default: 1). |
|
118 |
#' @param cached_ts,cached_ee Used for optimization by other functions, not for |
|
119 |
#' use by the package user. |
|
120 |
#' |
|
121 |
#' @return A tibble containing the generated samples. |
|
122 |
#' |
|
123 |
#' @importFrom stats rnorm |
|
124 |
#' @importFrom stats rgamma |
|
125 |
#' @importFrom stats rbeta |
|
126 |
#' |
|
127 |
#' @examples |
|
128 |
#' library(magrittr) |
|
129 |
#' mod <- new_networkModel() %>% |
|
130 |
#' set_topo("NH4 -> algae -> daphnia -> NH4") |
|
131 |
#' inits <- tibble::tribble( |
|
132 |
#' ~comps, ~sizes, ~props, ~treatment, |
|
133 |
#' "NH4", 0.2, 0.8, "light", |
|
134 |
#' "algae", 1, 0.004, "light", |
|
135 |
#' "daphnia", 2, 0.004, "light", |
|
136 |
#' "NH4", 0.5, 0.8, "dark", |
|
137 |
#' "algae", 1.2, 0.004, "dark", |
|
138 |
#' "daphnia", 1.3, 0.004, "dark") |
|
139 |
#' mod <- set_init(mod, inits, comp = "comps", size = "sizes", |
|
140 |
#' prop = "props", group_by = "treatment") |
|
141 |
#' mod <- add_covariates(mod, upsilon_NH4_to_algae ~ treatment) |
|
142 |
#' mod <- mod %>% |
|
143 |
#' set_params(c("eta" = 0.2, "lambda_algae" = 0, "lambda_daphnia" = 0, |
|
144 |
#' "lambda_NH4" = 0, "upsilon_NH4_to_algae|light" = 0.3, |
|
145 |
#' "upsilon_NH4_to_algae|dark" = 0.1, |
|
146 |
#' "upsilon_algae_to_daphnia" = 0.13, |
|
147 |
#' "upsilon_daphnia_to_NH4" = 0.045, "zeta" = 0.1)) |
|
148 |
#' spl <- mod %>% sample_from(at = 1:10) |
|
149 |
#' spl |
|
150 |
#' |
|
151 |
#' @export |
|
152 | ||
153 |
sample_from <- function(nm, at, dt = NULL, grid_size = NULL, end = NULL, error.draws = 1, |
|
154 |
cached_ts = NULL, cached_ee = NULL) { |
|
155 | 15x |
obs <- list() |
156 | 15x |
prop_family <- attr(nm, "prop_family") |
157 | 15x |
size_family <- attr(nm, "size_family") |
158 | 15x |
zeta_by_comp <- attr(nm, "size_zeta_per_compartment") |
159 | 15x |
if (is.null(zeta_by_comp)) { |
160 | ! |
zeta_by_comp <- FALSE |
161 |
} |
|
162 |
# Project nm |
|
163 | 15x |
nm <- project(nm, at = at, dt = dt, grid_size = grid_size, end = end, |
164 | 15x |
cached_ts = cached_ts, cached_ee = cached_ee) |
165 |
# Extract samples |
|
166 | 15x |
for (i in seq_len(nrow(nm))) { |
167 | 15x |
z <- nm$trajectory[[i]] |
168 | 15x |
params <- nm$parameters[[i]] |
169 | 15x |
indices <- sort(unique(match(at, z$timepoints[[1]]))) |
170 | 15x |
sizes <- tibble::as_tibble(z$unmarked[[1]] + z$marked[[1]])[indices, ] |
171 | 15x |
props <- tibble::as_tibble(z$proportions[[1]])[indices, ] |
172 | 15x |
stopifnot(!any(c("time", "comp", "size", "prop") %in% names(sizes))) |
173 | 15x |
sizes$time <- z$timepoints[[1]][indices] |
174 | 15x |
props$time <- z$timepoints[[1]][indices] |
175 | 15x |
sizes <- data.table::melt(data.table::as.data.table(sizes), |
176 | 15x |
id.vars = "time", |
177 | 15x |
variable.name = "comp", value.name = "meanSize", |
178 | 15x |
variable.factor = FALSE) |
179 | 15x |
props <- data.table::melt(data.table::as.data.table(props), |
180 | 15x |
id.vars = "time", |
181 | 15x |
variable.name = "comp", value.name = "meanProp", |
182 | 15x |
variable.factor = FALSE) |
183 | 15x |
sizes <- dplyr::bind_rows(rep(list(tibble::as_tibble(sizes)), error.draws)) |
184 | 15x |
props <- dplyr::bind_rows(rep(list(tibble::as_tibble(props)), error.draws)) |
185 |
# Add zeta values to sizes tibble |
|
186 | 15x |
if (!zeta_by_comp) { |
187 | 15x |
sizes$zeta <- params$value[params$in_replicate == "zeta"] |
188 |
} else { |
|
189 | ! |
zeta_params <- paste0("zeta_", sizes[["comp"]]) |
190 | ! |
sizes$zeta <- params$value[match(zeta_params, params$in_replicate)] |
191 |
} |
|
192 |
# Apply size "noise" |
|
193 | 15x |
if (size_family == "normal_cv") { |
194 | 15x |
sizes$size <- rnorm(nrow(sizes), |
195 | 15x |
mean = sizes$meanSize, |
196 | 15x |
sd = sizes$meanSize * sizes$zeta) |
197 | 15x |
negSizes <- which(sizes$size < 0) |
198 | 15x |
while (length(negSizes) > 0) { |
199 | ! |
sizes$size[negSizes] <- rnorm(length(negSizes), |
200 | ! |
sizes$meanSize[negSizes], |
201 | ! |
sd = sizes$meanSize[negSizes] * sizes$zeta[negSizes]) |
202 | ! |
negSizes <- which(sizes$size < 0) |
203 |
} |
|
204 | ! |
} else if (size_family == "normal_sd") { |
205 | ! |
sizes$size <- rnorm(nrow(sizes), |
206 | ! |
mean = sizes$meanSize, |
207 | ! |
sd = sizes$zeta) |
208 | ! |
negSizes <- which(sizes$size < 0) |
209 | ! |
while (length(negSizes) > 0) { |
210 | ! |
sizes$size[negSizes] <- rnorm(length(negSizes), |
211 | ! |
sizes$meanSize[negSizes], |
212 | ! |
sd = sizes$zeta) |
213 | ! |
negSizes <- which(sizes$size < 0) |
214 |
} |
|
215 |
} |
|
216 | 15x |
sizes$zeta <- NULL |
217 |
# Apply prop "noise" |
|
218 | 15x |
if (prop_family == "gamma_cv") { |
219 | 12x |
shapes <- rep(params$value[params$in_replicate == "eta"], nrow(props))^(-2) |
220 | 12x |
rates <- shapes / props$meanProp |
221 | 12x |
props$prop <- rgamma(nrow(props), shape = shapes, rate = rates) |
222 | 3x |
} else if (prop_family == "normal_cv") { |
223 | 1x |
means <- props$meanProp |
224 | 1x |
sds <- means * params$value[params$in_replicate == "eta"] |
225 | 1x |
props$prop <- rnorm(nrow(props), mean = means, sd = sds) |
226 | 1x |
negProps <- which(props$prop < 0) |
227 | 1x |
while (length(negProps) > 0) { |
228 | ! |
props$prop[negProps] <- rnorm(length(negProps), |
229 | ! |
means[negProps], |
230 | ! |
sd = sds[negProps]) |
231 | ! |
negProps <- which(props$prop < 0) |
232 |
} |
|
233 | 2x |
} else if (prop_family == "normal_sd") { |
234 | 1x |
means <- props$meanProp |
235 | 1x |
sds <- rep(params$value[params$in_replicate == "eta"], nrow(props)) |
236 | 1x |
props$prop <- rnorm(nrow(props), mean = means, sd = sds) |
237 | 1x |
negProps <- which(props$prop < 0) |
238 | 1x |
while (length(negProps) > 0) { |
239 | 6x |
props$prop[negProps] <- rnorm(length(negProps), |
240 | 6x |
means[negProps], |
241 | 6x |
sd = sds[negProps]) |
242 | 6x |
negProps <- which(props$prop < 0) |
243 |
} |
|
244 | 1x |
} else if (prop_family == "beta_phi") { |
245 | 1x |
phis <- rep(params$value[params$in_replicate == "eta"], nrow(props)) |
246 | 1x |
alphas <- props$meanProp * phis |
247 | 1x |
betas <- phis * (1 - props$meanProp) |
248 | 1x |
props$prop <- rbeta(nrow(props), shape1 = alphas, shape2 = betas) |
249 |
} else { |
|
250 | ! |
stop("Unknown distribution family:", prop_family) |
251 |
} |
|
252 | 15x |
stopifnot(all(sizes[["time"]] == props[["time"]])) |
253 | 15x |
stopifnot(all(sizes[["comp"]] == props[["comp"]])) |
254 | 15x |
obs[[i]] <- dplyr::bind_cols(sizes[, c("time", "comp", "size")], |
255 | 15x |
props[, c("prop")]) |
256 |
} |
|
257 | 15x |
if (is.null(groups(nm))) { |
258 | 15x |
stopifnot(nrow(nm) == 1) |
259 | 15x |
return(obs[[1]]) |
260 |
} |
|
261 | ! |
groups <- groups(nm) |
262 | ! |
out <- dplyr::bind_cols(tibble::tibble(obs), groups) |
263 | ! |
out <- tidyr::unnest(out, cols = "obs") |
264 | ! |
return(out) |
265 |
} |
|
266 | ||
267 |
### * calculate_steady_state() |
|
268 | ||
269 |
#' Calculate steady-state compartment sizes for a network |
|
270 |
#' |
|
271 |
#' This is an experimental function. It attempts to calculate steady-state |
|
272 |
#' compartment sizes using the set parameter values and the initial compartment |
|
273 |
#' sizes. Use it with caution! |
|
274 |
#' |
|
275 |
#' Note about how steady state sizes for split compartments are calculated: the |
|
276 |
#' steady size of the active portion is calculated divide it is divided by the |
|
277 |
#' active fraction (portion.act parameter) to get the total size including the |
|
278 |
#' refractory portion. In this case we get a "steady-state" refractory portion, |
|
279 |
#' consistent with steady state size of active fraction and with portion.act |
|
280 |
#' parameter. |
|
281 |
#' |
|
282 |
#' @param nm A network model, with set parameter values. |
|
283 |
#' |
|
284 |
#' @return A tibble containing steady-state compartment sizes. |
|
285 |
#' |
|
286 |
#' @examples |
|
287 |
#' m <- aquarium_mod |
|
288 |
#' m <- set_prior(m, constant_p(0), "lambda") |
|
289 |
#' m <- set_params(m, sample_params(m)) |
|
290 |
#' proj <- project(m, end = 40) |
|
291 |
#' plot(proj) |
|
292 |
#' |
|
293 |
#' z <- calculate_steady_state(m) |
|
294 |
#' z |
|
295 |
#' z$stable_sizes |
|
296 |
#' |
|
297 |
#' @export |
|
298 | ||
299 |
calculate_steady_state <- function(nm) { |
|
300 | ! |
out <- lapply(seq_len(nrow(nm)), function(i) { |
301 | ! |
tibble::tibble(stable_sizes = list(calculate_steady_state_one_row(nm[i, ]))) |
302 |
}) |
|
303 | ! |
out <- dplyr::bind_rows(out) |
304 | ! |
nm[["stable_sizes"]] <- out[["stable_sizes"]] |
305 | ! |
return(nm) |
306 |
} |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * available_priors() |
|
4 | ||
5 |
#' List the available priors for model parameters |
|
6 |
#' |
|
7 |
#' @return A tibble containing information about the available priors. |
|
8 |
#' |
|
9 |
#' @examples |
|
10 |
#' available_priors() |
|
11 |
#' |
|
12 |
#' @export |
|
13 | ||
14 |
available_priors <- function() { |
|
15 | ! |
fun_names <- c("constant_p", |
16 | ! |
"uniform_p", |
17 | ! |
"normal_p", |
18 | ! |
"hcauchy_p", |
19 | ! |
"exponential_p", |
20 | ! |
"gamma_p", |
21 | ! |
"scaled_beta_p") |
22 | ! |
prior_names <- c("Constant value", |
23 | ! |
"Uniform prior", |
24 | ! |
"Normal distribution", |
25 | ! |
"Half-Cauchy distribution", |
26 | ! |
"Exponential distribution", |
27 | ! |
"Gamma distribution", |
28 | ! |
"Scaled beta distribution") |
29 | ! |
usage <- c("constant_p(value)", |
30 | ! |
"uniform_p(min, max)", |
31 | ! |
"normal_p(mean, sd)", |
32 | ! |
"hcauchy_p(scale)", |
33 | ! |
"exponential_p(lambda)", |
34 | ! |
"gamma_p(alpha, beta)", |
35 | ! |
"scaled_beta_p(alpha, beta, scale = 1)") |
36 | ! |
out <- tibble::tibble(function_name = fun_names, |
37 | ! |
description = prior_names, |
38 | ! |
usage = usage) |
39 | ! |
return(structure(out, class = c("prior_tibble", class(out)))) |
40 |
} |
|
41 | ||
42 |
### * Methods for nice display of prior tibble |
|
43 | ||
44 |
### ** format.prior_tibble() |
|
45 | ||
46 |
#' Pretty formatting of a \code{prior_tibble} object |
|
47 |
#' |
|
48 |
#' @param x An object of class \code{prior_tibble}. |
|
49 |
#' @param ... Not used. |
|
50 |
#' |
|
51 |
#' @return A character string for pretty printing of a prior tibble. |
|
52 |
#' |
|
53 |
#' @export |
|
54 | ||
55 |
format.prior_tibble <- function(x, ...) { |
|
56 | ! |
x <- structure(x, class = class(x)[class(x) != "prior_tibble"]) |
57 | ! |
f <- format(x) |
58 | ! |
f <- c(f, |
59 |
"-------------------------------------------------------", |
|
60 | ! |
"(Note: All priors distributions are truncated at zero.)") |
61 | ! |
return(f) |
62 |
} |
|
63 | ||
64 |
### ** print.prior_tibble() |
|
65 | ||
66 |
#' Pretty printing of a \code{prior_tibble} object |
|
67 |
#' |
|
68 |
#' @param x An object of class \code{prior_tibble}. |
|
69 |
#' @param ... Not used. |
|
70 |
#' |
|
71 |
#' @return Mostly called for its side effect of printing, but also returns its |
|
72 |
#' input invisibly. |
|
73 |
#' |
|
74 |
#' @export |
|
75 | ||
76 |
print.prior_tibble <- function(x, ...) { |
|
77 | ! |
cat(format(x), sep = "\n") |
78 | ! |
invisible(x) |
79 |
} |
|
80 | ||
81 |
### * constant_p(value) |
|
82 | ||
83 |
#' Define a fixed-value prior |
|
84 |
#' |
|
85 |
#' This is equivalent to having a fixed parameter. |
|
86 |
#' |
|
87 |
#' @param value The constant value of the parameter. |
|
88 |
#' |
|
89 |
#' @return A list defining the prior. |
|
90 |
#' |
|
91 |
#' @examples |
|
92 |
#' constant_p(2) |
|
93 |
#' |
|
94 |
#' @export |
|
95 | ||
96 |
constant_p <- function(value) { |
|
97 | 8x |
x <- list(type = "constant", |
98 | 8x |
parameters = c(value = value)) |
99 | 8x |
x <- structure(x, class = "prior") |
100 | 8x |
return(x) |
101 |
} |
|
102 | ||
103 |
### * hcauchy_p(scale) |
|
104 | ||
105 |
#' Define a half-Cauchy prior (on [0;+Inf]) |
|
106 |
#' |
|
107 |
#' @param scale Median of the half-Cauchy distribution. |
|
108 |
#' |
|
109 |
#' @return A list defining the prior. |
|
110 |
#' |
|
111 |
#' @importFrom stats rcauchy |
|
112 |
#' |
|
113 |
#' @examples |
|
114 |
#' hcauchy_p(scale = 0.5) |
|
115 |
#' |
|
116 |
#' @export |
|
117 | ||
118 |
hcauchy_p <- function(scale) { |
|
119 | 10x |
x <- list(type = "hcauchy", |
120 | 10x |
parameters = c(scale = scale)) |
121 | 10x |
x <- structure(x, class = "prior") |
122 | 10x |
return(x) |
123 |
} |
|
124 | ||
125 |
### * normal_p(mean, sd) |
|
126 | ||
127 |
#' Define a truncated normal prior (on [0;+Inf]) |
|
128 |
#' |
|
129 |
#' @param mean Mean of the untruncated normal. |
|
130 |
#' @param sd Standard deviation of the untruncated normal. |
|
131 |
#' |
|
132 |
#' @return A list defining the prior. |
|
133 |
#' |
|
134 |
#' @importFrom stats rnorm |
|
135 |
#' |
|
136 |
#' @examples |
|
137 |
#' normal_p(mean = 0, sd = 4) |
|
138 |
#' |
|
139 |
#' @export |
|
140 | ||
141 |
normal_p <- function(mean, sd) { |
|
142 | 10x |
x <- list(type = "trun_normal", |
143 | 10x |
parameters = c(mean = mean, |
144 | 10x |
sd = sd)) |
145 | 10x |
x <- structure(x, class = "prior") |
146 | 10x |
return(x) |
147 |
} |
|
148 | ||
149 |
### * uniform_p(min, max) |
|
150 | ||
151 |
#' Define a uniform prior |
|
152 |
#' |
|
153 |
#' @param min,max Minimum and maximum boundaries for the uniform prior. |
|
154 |
#' |
|
155 |
#' @return A list defining the prior. |
|
156 |
#' |
|
157 |
#' @importFrom stats runif |
|
158 |
#' |
|
159 |
#' @examples |
|
160 |
#' uniform_p(min = 0, max= 1) |
|
161 |
#' |
|
162 |
#' @export |
|
163 | ||
164 |
uniform_p <- function(min, max) { |
|
165 | 9x |
x <- list(type = "uniform", |
166 | 9x |
parameters = c(min = min, max = max)) |
167 | 9x |
x <- structure(x, class = "prior") |
168 | 9x |
return(x) |
169 |
} |
|
170 | ||
171 | ||
172 |
### * scaled_beta_p(alpha, beta, scale=1) |
|
173 | ||
174 |
#' Define a beta prior (on [0;scale]) |
|
175 |
#' |
|
176 |
#' If a random variable X follows a scaled beta distribution with parameters |
|
177 |
#' (alpha, beta, scale), then X/scale follows a beta distribution with |
|
178 |
#' parameters (alpha, beta). |
|
179 |
#' |
|
180 |
#' @param alpha Alpha parameter of the unscaled beta distribution. |
|
181 |
#' @param beta Beta parameter of the unscaled beta distribution. |
|
182 |
#' @param scale The upper boundary of the prior. |
|
183 |
#' |
|
184 |
#' @return A list defining the prior. |
|
185 |
#' |
|
186 |
#' @examples |
|
187 |
#' scaled_beta_p(0.8, 20, scale = 10) |
|
188 |
#' |
|
189 |
#' @export |
|
190 | ||
191 |
scaled_beta_p <- function(alpha, beta, scale = 1) { |
|
192 | ! |
x <- list(type = "scaled_beta", |
193 | ! |
parameters = c(alpha = alpha, beta = beta, scale = scale)) |
194 | ! |
x <- structure(x, class = "prior") |
195 | ! |
return(x) |
196 |
} |
|
197 | ||
198 |
### * exponential_p(lambda) |
|
199 | ||
200 |
#' Define an exponential prior |
|
201 |
#' |
|
202 |
#' @param lambda Lambda parameter (rate) of the exponential distribution. The |
|
203 |
#' mean of the exponential distribution is 1/lambda. |
|
204 |
#' |
|
205 |
#' @return A list defining the prior. |
|
206 |
#' |
|
207 |
#' @examples |
|
208 |
#' exponential_p(0.5) |
|
209 |
#' |
|
210 |
#' @export |
|
211 | ||
212 |
exponential_p <- function(lambda) { |
|
213 | 1x |
x <- list(type = "exponential", |
214 | 1x |
parameters = c(lambda = lambda)) |
215 | 1x |
x <- structure(x, class = "prior") |
216 | 1x |
return(x) |
217 |
} |
|
218 | ||
219 |
### * gamma_p(alpha, beta) |
|
220 | ||
221 |
#' Define a gamma prior |
|
222 |
#' |
|
223 |
#' Note the name of the function to define a prior (\code{gamma_p}), in order |
|
224 |
#' to avoid confusion with the R mathematical function \code{gamma}. |
|
225 |
#' |
|
226 |
#' @param alpha Shape parameter (equivalent to the \code{shape} parameter of |
|
227 |
#' R's \code{rgamma}). |
|
228 |
#' @param beta Rate parameter (equivalent to the \code{rate} parameter of R's |
|
229 |
#' \code{rgamma}). |
|
230 |
#' |
|
231 |
#' @return A list defining the prior. |
|
232 |
#' |
|
233 |
#' @examples |
|
234 |
#' gamma_p(9, 2) |
|
235 |
#' hist(sample_from_prior(gamma_p(9, 2), 1e3)) |
|
236 |
#' |
|
237 |
#' @export |
|
238 | ||
239 |
gamma_p <- function(alpha, beta) { |
|
240 | 1x |
x <- list(type = "gamma", |
241 | 1x |
parameters = c(alpha = alpha, beta = beta)) |
242 | 1x |
x <- structure(x, class = "prior") |
243 | 1x |
return(x) |
244 |
} |
|
245 | ||
246 |
### * Methods for nice display of priors |
|
247 | ||
248 |
### ** format.prior() |
|
249 | ||
250 |
#' Pretty formatting of a \code{prior} object |
|
251 |
#' |
|
252 |
#' @param x An object of class \code{prior}. |
|
253 |
#' @param ... Not used. |
|
254 |
#' |
|
255 |
#' @return A character string for pretty printing of a prior. |
|
256 |
#' |
|
257 |
#' @export |
|
258 | ||
259 |
format.prior <- function(x, ...) { |
|
260 | ! |
params <- paste0("(", |
261 | ! |
paste(paste(names(x[["parameters"]]), x[["parameters"]], |
262 | ! |
sep = "="), collapse = ","), |
263 |
")") |
|
264 | ! |
out <- paste0(x[["type"]], "", params) |
265 | ! |
return(out) |
266 |
} |
|
267 | ||
268 |
### ** print.prior() |
|
269 | ||
270 |
#' Pretty printing of a \code{prior} object |
|
271 |
#' |
|
272 |
#' @param x An object of class \code{prior}. |
|
273 |
#' @param ... Not used. |
|
274 |
#' |
|
275 |
#' @return Mostly called for its side effect of printing, but also returns its |
|
276 |
#' input invisibly. |
|
277 |
#' |
|
278 |
#' @export |
|
279 | ||
280 |
print.prior <- function(x, ...) { |
|
281 | ! |
cat(format(x), sep = "\n") |
282 | ! |
invisible(x) |
283 |
} |
|
284 | ||
285 |
### * Extending tibbles |
|
286 | ||
287 |
# https://cran.r-project.org/web/packages/tibble/vignettes/extending.html |
|
288 | ||
289 |
#' Function used for displaying \code{prior} object in tibbles |
|
290 |
#' |
|
291 |
#' @param x An object of class \code{prior}. |
|
292 |
#' |
|
293 |
#' @return Input formatted with \code{format(x)}. |
|
294 |
#' |
|
295 |
#' @importFrom pillar type_sum |
|
296 |
#' @export |
|
297 |
type_sum.prior <- function(x) { |
|
298 | ! |
format(x) |
299 |
} |
|
300 | ||
301 |
#' Function used for displaying \code{prior} object in tibbles |
|
302 |
#' |
|
303 |
#' @param x An object of class \code{prior}. |
|
304 |
#' |
|
305 |
#' @return Input formatted with \code{format(x)}. |
|
306 |
#' |
|
307 |
#' @importFrom pillar obj_sum |
|
308 |
#' @export |
|
309 |
obj_sum.prior <- function(x) { |
|
310 | ! |
format(x) |
311 |
} |
|
312 | ||
313 |
#' Function used for displaying \code{prior} object in tibbles |
|
314 |
#' |
|
315 |
#' @param x An object of class \code{prior}. |
|
316 |
#' @param ... Not used. |
|
317 |
#' |
|
318 |
#' @return An object prepared with pillar::new_pillar_shaft_simple. |
|
319 |
#' |
|
320 |
#' @importFrom pillar pillar_shaft |
|
321 |
#' @export |
|
322 |
pillar_shaft.prior <- function(x, ...) { |
|
323 | ! |
out <- format(x) |
324 | ! |
out[is.na(x)] <- NA |
325 | ! |
pillar::new_pillar_shaft_simple(out, align = "right") |
326 |
} |
|
327 | ||
328 |
### * Methods for Ops on priors (implementing '==' operator) |
|
329 | ||
330 |
# https://stackoverflow.com/a/35902710 |
|
331 | ||
332 |
#' Implementation of the '==' operator for priors |
|
333 |
#' |
|
334 |
#' @param e1,e2 Objects of class "prior". |
|
335 |
#' |
|
336 |
#' @return Boolean (or throws an error for unsupported operators). |
|
337 |
#' |
|
338 |
#' @examples |
|
339 |
#' p <- constant_p(0) |
|
340 |
#' q <- constant_p(4) |
|
341 |
#' p == q |
|
342 |
#' |
|
343 |
#' p <- hcauchy_p(2) |
|
344 |
#' q <- hcauchy_p(2) |
|
345 |
#' p == q |
|
346 |
#' |
|
347 |
#' @method Ops prior |
|
348 |
#' |
|
349 |
#' @export |
|
350 | ||
351 |
Ops.prior <- function(e1, e2) { |
|
352 | ! |
op <- .Generic[[1]] |
353 | ! |
switch(op, |
354 |
`==` = { |
|
355 | ! |
if (e1$type != e2$type) { |
356 | ! |
return(FALSE) |
357 |
} |
|
358 | ! |
if (!all(e1$parameters == e2$parameters)) { |
359 | ! |
return(FALSE) |
360 |
} |
|
361 | ! |
return(TRUE) |
362 |
}, |
|
363 | ! |
stop("Undefined operation for objects of class \"priors\".") |
364 |
) |
|
365 |
} |
|
366 | ||
367 |
### * sample_from_prior() |
|
368 | ||
369 |
#' Sample from a prior object |
|
370 |
#' |
|
371 |
#' @param x A \code{prior} object. |
|
372 |
#' @param n Integer, number of samples to draw. |
|
373 |
#' |
|
374 |
#' @return A numeric vector of length \code{n}. |
|
375 |
#' |
|
376 |
#' @examples |
|
377 |
#' sample_from_prior(constant_p(1)) |
|
378 |
#' sample_from_prior(constant_p(1), 10) |
|
379 |
#' sample_from_prior(hcauchy_p(0.5), 1) |
|
380 |
#' hist(sample_from_prior(hcauchy_p(0.5), 20)) |
|
381 |
#' hist(sample_from_prior(uniform_p(0, 3), 1000)) |
|
382 |
#' hist(sample_from_prior(scaled_beta_p(3, 7, 2), 1e4)) |
|
383 |
#' |
|
384 |
#' @export |
|
385 |
#' |
|
386 | ||
387 |
sample_from_prior <- function(x, n = 1) { |
|
388 | 88x |
switch(x$type, |
389 |
"constant" = { |
|
390 | 9x |
rep(x$parameters[["value"]], n) |
391 |
}, |
|
392 |
"hcauchy" = { |
|
393 | 17x |
scale <- x$parameters[["scale"]] |
394 | 17x |
replicate(n, |
395 |
{ |
|
396 | 17x |
o <- stats::rcauchy(1, location = 0, scale = scale) |
397 | 17x |
while (o < 0) { |
398 | 20x |
o <- stats::rcauchy(1, location = 0, scale = scale) |
399 |
} |
|
400 | 17x |
return(o) |
401 |
}) |
|
402 |
}, |
|
403 |
"trun_normal" = { |
|
404 | 54x |
mean <- x$parameters[["mean"]] |
405 | 54x |
sd <- x$parameters[["sd"]] |
406 | 54x |
replicate(n, { |
407 | 54x |
o <- stats::rnorm(1, mean = mean, sd = sd) |
408 | 54x |
while (o < 0) { |
409 | 60x |
o <- stats::rnorm(1, mean = mean, sd = sd) |
410 |
} |
|
411 | 54x |
return(o) |
412 |
}) |
|
413 |
}, |
|
414 |
"uniform" = { |
|
415 | 8x |
stats::runif(n, min = x$parameters[["min"]], max = x$parameters[["max"]]) |
416 |
}, |
|
417 |
"scaled_beta" = { |
|
418 | ! |
p <- x$parameters |
419 | ! |
p[["scale"]] * stats::rbeta(n, shape1 = p[["alpha"]], shape2 = p[["beta"]]) |
420 |
}, |
|
421 |
"exponential" = { |
|
422 | ! |
lambda <- x$parameters[["lambda"]] |
423 | ! |
stats::rexp(n = n, rate = lambda) |
424 |
}, |
|
425 |
"gamma" = { |
|
426 | ! |
alpha <- x$parameters[["alpha"]] |
427 | ! |
beta <- x$parameters[["beta"]] |
428 | ! |
stats::rgamma(n = n, shape = alpha, rate = beta) |
429 |
}, |
|
430 | ! |
stop("Unknown prior type.")) |
431 |
} |
1 |
### * TODO |
|
2 | ||
3 |
# Clean-up this file |
|
4 | ||
5 |
### * All functions in this file are exported |
|
6 | ||
7 |
### * predict.networkModel() |
|
8 | ||
9 |
#' Add a column with predictions from a fit |
|
10 |
#' |
|
11 |
#' @param object Network model |
|
12 |
#' @param fit Model fit (mcmc.list object) |
|
13 |
#' @param draws Integer, number of draws from the posteriors |
|
14 |
#' @param error.draws Integer, number of draws from the error distribution, for |
|
15 |
#' a given posterior draw. |
|
16 |
#' @param probs Credible interval (default 0.95). |
|
17 |
#' @param cores Number of cores to use for parallel calculations. Default is |
|
18 |
#' \code{NULL}, which means to use the value stored in |
|
19 |
#' \code{options()[["mc.cores"]]} (or 1 if this value is not set). |
|
20 |
#' @param dt,grid_size Time step size or grid points, respectively. |
|
21 |
#' @param at Timepoints at which the predictions should be returned. |
|
22 |
#' @param end Final timepoint used in the projections. |
|
23 |
#' @param ... Not used. |
|
24 |
#' |
|
25 |
#' @return A network model object with an added column \code{"prediction"}. |
|
26 |
#' |
|
27 |
#' @importFrom stats quantile |
|
28 |
#' |
|
29 |
#' @export |
|
30 | ||
31 |
predict.networkModel <- function(object, fit, draws = NULL, error.draws = 5, |
|
32 |
probs = 0.95, cores = NULL, |
|
33 |
dt = NULL, grid_size = NULL, at = NULL, end = NULL, |
|
34 |
...) { |
|
35 | 12x |
`!!` <- rlang::`!!` |
36 |
# Process `cores` argument |
|
37 | 12x |
cores <- get_n_cores(cores = cores) |
38 |
# Process nm |
|
39 | 12x |
nm <- object |
40 | 12x |
if (!"events" %in% colnames(nm)) { |
41 | 12x |
nm[["events"]] <- rep(list(NULL), nrow(nm)) |
42 |
} |
|
43 | 12x |
if (!"group" %in% colnames(nm)) { |
44 | 10x |
nm[["group"]] <- rep(list(NULL), nrow(nm)) |
45 |
} |
|
46 |
# Process projection arguments (always caching) |
|
47 | 12x |
arg_end <- end |
48 | 12x |
cache <- list() |
49 | 12x |
rows_time_schemes <- list() |
50 | 12x |
rows_encoded_events <- list() |
51 | 12x |
nmRow_ends <- list() |
52 | 12x |
nmRow_ats <- list() |
53 | 12x |
for (i in seq_len(nrow(nm))) { |
54 | 14x |
nmRow <- nm[i, ] |
55 | 14x |
end <- arg_end |
56 | 14x |
if (is.null(end)) { |
57 | 14x |
if (is.null(at)) { |
58 | 12x |
end <- max(nmRow$observations[[1]][["time"]]) |
59 | 12x |
if (!is.null(nmRow[["events"]][[1]])) { |
60 | ! |
end <- max(c(end, nmRow$events[[1]][["time"]])) |
61 |
} |
|
62 |
} else { |
|
63 | 2x |
end <- max(at) |
64 |
} |
|
65 |
} |
|
66 | 14x |
if (is.null(at)) { |
67 | 12x |
at <- seq(0, end, length.out = 65) |
68 |
} |
|
69 | 14x |
nmRow_ends[[i]] <- end |
70 | 14x |
nmRow_ats[[i]] <- at |
71 | 14x |
rows_time_schemes[[i]] <- nm_row_get_time_scheme(nm_row = nmRow, dt = dt, |
72 | 14x |
grid_size = grid_size, |
73 | 14x |
end = end, at = at) |
74 | 14x |
rows_encoded_events[[i]] <- encode_events(nmRow, dt = dt, grid_size = grid_size, |
75 | 14x |
end = end) |
76 |
} |
|
77 | 12x |
cache[["rows_time_schemes"]] <- rows_time_schemes |
78 | 12x |
cache[["rows_encoded_events"]] <- rows_encoded_events |
79 | 12x |
cache[["nmRow_ends"]] <- nmRow_ends |
80 | 12x |
cache[["nmRow_ats"]] <- nmRow_ats |
81 | 12x |
end <- arg_end |
82 |
# See https://github.com/HenrikBengtsson/future/issues/263#issuecomment-445047269 |
|
83 |
# for a reason not to manipulate future::plan() in the package code. |
|
84 | 12x |
if (is.null(draws)) { |
85 | 2x |
draws <- nrow(fit[[1]]) |
86 |
} |
|
87 |
# Get parameter samples |
|
88 | 12x |
samples <- tidy_mcmc_list(fit) |
89 | 12x |
if (draws > nrow(samples)) { |
90 | ! |
warning("Number of draws greater (", draws, |
91 | ! |
") than number of samples (", nrow(samples) ,").\n", |
92 | ! |
"Setting number of draws to ", nrow(samples), ".") |
93 | ! |
draws <- nrow(samples) |
94 |
} |
|
95 | 12x |
samples <- samples[sample(seq_len(nrow(samples)), draws), ] |
96 |
# Project rows for each set of parameter values |
|
97 | 12x |
nm$prediction <- purrr::map(seq_len(nrow(nm)), function(k) { |
98 | 14x |
nmRow <- nm[k, ] |
99 | 14x |
projections <- parallel::mclapply(seq_len(nrow(samples)), function(i) { |
100 | ! |
nmRow <- set_params(nmRow, samples$mcmc.parameters[[i]], force = TRUE, quick = TRUE) |
101 | ! |
pred <- sample_from(nmRow, at = cache[["nmRow_ats"]][[k]], |
102 | ! |
dt = dt, grid_size = grid_size, end = end, error.draws = error.draws, |
103 | ! |
cached_ts = list(cache[["rows_time_schemes"]][[k]]), |
104 | ! |
cached_ee = list(cache[["rows_encoded_events"]][[k]])) |
105 | ! |
return(pred) |
106 | 14x |
}, mc.cores = cores) |
107 |
# Concatenate data |
|
108 | 14x |
pred <- dplyr::bind_rows(projections) |
109 | 14x |
pred <- tidyr::nest(dplyr::group_by(pred, |
110 | 14x |
`!!`(rlang::sym("time")), |
111 | 14x |
`!!`(rlang::sym("comp")))) |
112 | 14x |
pred$sizes <- purrr::map(pred$data, function(x) { |
113 | 2470x |
z <- quantile(x$size, probs = c((1-probs)/2, 1 -(1-probs)/2)) |
114 | 2470x |
m <- mean(x$size) |
115 | 2470x |
out <- c(z[1], m, z[2]) |
116 | 2470x |
names(out) <- c("low", "mean", "high") |
117 | 2470x |
return(out) |
118 |
}) |
|
119 | 14x |
pred$props <- purrr::map(pred$data, function(x) { |
120 | 2470x |
if (length(na.omit(x$prop)) == 0) { # All NAs can happen when the size is zero for example |
121 | ! |
return(c(low = NA, mean = NA, high = NA)) |
122 |
} |
|
123 | 2470x |
z <- quantile(x$prop, probs = c((1-probs)/2, 1 -(1-probs)/2)) |
124 | 2470x |
m <- c(mean = mean(x$prop)) |
125 | 2470x |
out <- c(z[1], m, z[2]) |
126 | 2470x |
names(out) <- c("low", "mean", "high") |
127 | 2470x |
return(out) |
128 |
}) |
|
129 | 14x |
sizes <- do.call(dplyr::bind_rows, pred$sizes) |
130 | 14x |
names(sizes) <- c("size_low", "size_mean", "size_high") |
131 | 14x |
props <- do.call(dplyr::bind_rows, pred$props) |
132 | 14x |
names(props) <- c("prop_low", "prop_mean", "prop_high") |
133 | 14x |
pred <- dplyr::bind_cols(pred[, c("time", "comp")], sizes, props) |
134 | 14x |
pred[["compartment"]] <- pred[["comp"]] |
135 | 14x |
pred[["comp"]] <- NULL |
136 | 14x |
return(pred) |
137 |
}) |
|
138 |
# Return |
|
139 | 12x |
return(nm) |
140 |
} |
|
141 | ||
142 |
### * posterior_predict.networkModelStanfit() |
|
143 | ||
144 |
### ** Generic |
|
145 | ||
146 |
#' Draw from the posterior predictive distribution of the model outcome |
|
147 |
#' |
|
148 |
#' @param object Model from which posterior predictions can be made. |
|
149 |
#' @param ... Passed to the appropriate method. |
|
150 |
#' |
|
151 |
#' @return Usually methods will implement a \code{draw} parameter, and the |
|
152 |
#' returned object is a "draw" by N matrix where N is the number of data |
|
153 |
#' points predicted per draw. |
|
154 |
#' |
|
155 |
#' @export |
|
156 | ||
157 |
posterior_predict <- function(object, ...) { |
|
158 | ! |
UseMethod("posterior_predict") |
159 |
} |
|
160 | ||
161 |
### ** Method |
|
162 | ||
163 |
#' Draw from the posterior predictive distribution of the model outcome |
|
164 |
#' |
|
165 |
#' @method posterior_predict networkModelStanfit |
|
166 |
#' |
|
167 |
#' @param object A networkModelStanfit object. |
|
168 |
#' @param draw Integer, number of draws to perform from the posterior. Default |
|
169 |
#' is 100. |
|
170 |
#' @param newdata Should be the model used to fit the networkStanfit object. |
|
171 |
#' @param cores Number of cores to use for parallel calculations. Default is |
|
172 |
#' \code{NULL}, which means to use the value stored in |
|
173 |
#' \code{options()[["mc.cores"]]} (or 1 if this value is not set). |
|
174 |
#' @param ... Not used for now. |
|
175 |
#' |
|
176 |
#' @return A "draw" by N matrix where N is the number of data points predicted |
|
177 |
#' per draw. |
|
178 |
#' |
|
179 |
#' @export |
|
180 | ||
181 |
posterior_predict.networkModelStanfit <- function(object, newdata, draw = NULL, |
|
182 |
cores = NULL, ...) { |
|
183 | ! |
`!!` <- rlang::`!!` |
184 |
# Process `cores` argument |
|
185 | ! |
cores <- get_n_cores(cores = cores) |
186 | ! |
nm <- newdata |
187 | ! |
fit <- object |
188 | ! |
if (is.null(draw)) { |
189 | ! |
draws <- min(100, nrow(fit[[1]])) |
190 |
} |
|
191 |
# Get parameter samples |
|
192 | ! |
samples <- tidy_mcmc_list(fit) |
193 | ! |
if (draws > nrow(samples)) { |
194 | ! |
warning("Number of draws greater (", draws, |
195 | ! |
") than number of samples (", nrow(samples) ,").\n", |
196 | ! |
"Setting number of draws to ", nrow(samples), ".") |
197 | ! |
draws <- nrow(samples) |
198 |
} |
|
199 | ! |
samples <- samples[sample(seq_len(nrow(samples)), draws), ] |
200 |
# Project rows for each set of parameter values |
|
201 | ! |
nm$prediction <- purrr::map(seq_len(nrow(nm)), function(k) { |
202 | ! |
nmRow <- nm[k, ] |
203 | ! |
at <- unique(nmRow[["observations"]][[1]][["time"]]) |
204 | ! |
projections <- parallel::mclapply(seq_len(nrow(samples)), function(i) { |
205 | ! |
nmRow <- set_params(nmRow, samples$mcmc.parameters[[i]], |
206 | ! |
force = TRUE) |
207 | ! |
nmRow <- project(nmRow, at = at) |
208 | ! |
pred <- sample_from(nmRow, at, error.draws = 1) |
209 | ! |
pred$sample_id <- i |
210 | ! |
return(pred) |
211 | ! |
}, mc.cores = cores) |
212 |
# Concatenate data |
|
213 | ! |
pred <- dplyr::bind_rows(projections) |
214 | ! |
pred$group <- nmRow$group |
215 | ! |
pred$group_str <- group2string(nmRow$group[[1]]) |
216 | ! |
return(pred) |
217 |
}) |
|
218 |
# Return |
|
219 | ! |
return(nm) |
220 |
} |
|
221 | ||
222 |
### * tidy_data() |
|
223 | ||
224 |
#' Extract data from a networkModel object into a tidy tibble. |
|
225 |
#' |
|
226 |
#' @param x A networkModel object. |
|
227 |
#' |
|
228 |
#' @return A tibble (note: row ordering is not the same as in the input). |
|
229 |
#' |
|
230 |
#' @examples |
|
231 |
#' tidy_data(aquarium_mod) |
|
232 |
#' tidy_data(trini_mod) |
|
233 |
#' |
|
234 |
#' @export |
|
235 | ||
236 |
tidy_data <- function(x) { |
|
237 |
# Remove "networkModel" class to avoid dplyr using groups.networkModel method |
|
238 | ! |
stopifnot(class(x)[1] == "networkModel") |
239 | ! |
stopifnot(length(class(x)) > 1) |
240 | ! |
class(x) <- class(x)[2:length(class(x))] |
241 | ! |
if (!"group" %in% colnames(x)) { |
242 | ! |
x[["group"]] <- rep(list(NULL), nrow(x)) |
243 |
} |
|
244 | ! |
group_mapping <- x[["group"]] |
245 | ! |
names(group_mapping) <- sapply(group_mapping, group2string) |
246 | ! |
x[["group"]] <- names(group_mapping) |
247 | ! |
obs <- tidyr::unnest(x[, c("observations", "group")], cols = "observations") |
248 | ! |
tidy_obs <- tidyr::pivot_longer(obs, cols = c("size", "proportion"), |
249 | ! |
names_to = "type", values_to = "value") |
250 | ! |
tidy_obs <- tidy_obs[!is.na(tidy_obs[["value"]]), ] |
251 | ! |
tidy_obs$group_str <- tidy_obs$group |
252 | ! |
tidy_obs[["group"]] <- group_mapping[tidy_obs$group_str] |
253 | ! |
tidy_obs <- tidy_obs[order(tidy_obs$compartment, |
254 | ! |
tidy_obs$time, |
255 | ! |
tidy_obs$group_str, |
256 | ! |
tidy_obs$type), ] |
257 | ! |
tidy_obs <- tidy_obs[, c("compartment", "time", "group_str", "group", |
258 | ! |
"type", "value")] |
259 | ! |
return(tidy_obs) |
260 |
} |
|
261 | ||
262 |
### * tidy_posterior_predict() |
|
263 | ||
264 |
#' Draw from the posterior predictive distribution of the model outcome |
|
265 |
#' |
|
266 |
#' @param object A networkModelStanfit object. |
|
267 |
#' @param draw Integer, number of draws to sample from the posterior. Default |
|
268 |
#' is 100. |
|
269 |
#' @param newdata The original model used to fit the networkStanfit object. |
|
270 |
#' @param cores Number of cores to use for parallel calculations. Default is |
|
271 |
#' \code{NULL}, which means to use the value stored in |
|
272 |
#' \code{options()[["mc.cores"]]} (or 1 if this value is not set). |
|
273 |
#' @param ... Not used for now. |
|
274 |
#' |
|
275 |
#' @return A tidy table. |
|
276 |
#' |
|
277 |
#' @export |
|
278 | ||
279 |
tidy_posterior_predict <- function(object, newdata, draw = NULL, cores = NULL, |
|
280 |
...) { |
|
281 | ! |
`!!` <- rlang::`!!` |
282 |
# Process `cores` argument |
|
283 | ! |
cores <- get_n_cores(cores = cores) |
284 |
# Process nm |
|
285 | ! |
nm <- newdata |
286 | ! |
if (!"group" %in% colnames(nm)) { |
287 | ! |
nm[["group"]] <- rep(list(NULL), nrow(nm)) |
288 |
} |
|
289 | ! |
fit <- object |
290 | ! |
if (is.null(draw)) { |
291 | ! |
draws <- min(100, nrow(fit[[1]])) |
292 |
} else { |
|
293 | ! |
draws <- draw |
294 |
} |
|
295 |
# Get parameter samples |
|
296 | ! |
samples <- tidy_mcmc_list(fit) |
297 | ! |
if (draws > nrow(samples)) { |
298 | ! |
warning("Number of draws greater (", draws, |
299 | ! |
") than number of samples (", nrow(samples) ,").\n", |
300 | ! |
"Setting number of draws to ", nrow(samples), ".") |
301 | ! |
draws <- nrow(samples) |
302 |
} |
|
303 | ! |
samples <- samples[sample(seq_len(nrow(samples)), draws), ] |
304 |
# Project rows for each set of parameter values |
|
305 | ! |
nm$prediction <- purrr::map(seq_len(nrow(nm)), function(k) { |
306 | ! |
nmRow <- nm[k, ] |
307 | ! |
at <- unique(nmRow[["observations"]][[1]][["time"]]) |
308 | ! |
projections <- parallel::mclapply(seq_len(nrow(samples)), function(i) { |
309 | ! |
nmRow <- set_params(nmRow, samples$mcmc.parameters[[i]], |
310 | ! |
force = TRUE) |
311 | ! |
nmRow <- project(nmRow, at = at) |
312 | ! |
pred <- sample_from(nmRow, at, error.draws = 1) |
313 | ! |
pred$random_sample_id <- i |
314 | ! |
return(pred) |
315 | ! |
}, mc.cores = cores) |
316 |
# Concatenate data |
|
317 | ! |
pred <- dplyr::bind_rows(projections) |
318 | ! |
pred$group <- nmRow$group |
319 | ! |
pred$group_str <- group2string(nmRow$group[[1]]) |
320 | ! |
return(pred) |
321 |
}) |
|
322 |
# Return |
|
323 | ! |
z <- dplyr::bind_rows(nm$prediction) |
324 | ! |
z$compartment <- z$comp |
325 | ! |
z$comp <- NULL |
326 | ! |
z <- tidyr::pivot_longer(z, cols = c("size", "prop"), names_to = "type", |
327 | ! |
values_to = "value") |
328 | ! |
z <- z[, c("compartment", "time", "group_str", "group", |
329 | ! |
"type", "value", "random_sample_id")] |
330 | ! |
z <- z[order(z$compartment, z$time, z$group_str, z$random_sample_id, z$type), ] |
331 | ! |
z[["type"]][z[["type"]] == "prop"] <- "proportion" |
332 | ! |
return(z) |
333 |
} |
|
334 | ||
335 |
### * tidy_dpp() |
|
336 | ||
337 |
#' Prepare tidy data and posterior predictions |
|
338 |
#' |
|
339 |
#' This function prepares both tidy data from a model and tidy posterior |
|
340 |
#' predictions from a model fit. Having those two tibbles prepared at the same |
|
341 |
#' time allows to merge them to ensure that observed data, predicted data and |
|
342 |
#' original variables other than observations are all in sync when using y and |
|
343 |
#' y_rep objects for bayesplot functions. |
|
344 |
#' |
|
345 |
#' @param model A networkModel object. |
|
346 |
#' @param fit A networkModelStanfit object. |
|
347 |
#' @param draw Integer, number of draws to sample from the posterior. |
|
348 |
#' @param cores Number of cores to use for parallel calculations. Default is |
|
349 |
#' \code{NULL}, which means to use the value stored in |
|
350 |
#' \code{options()[["mc.cores"]]} (or 1 if this value is not set). |
|
351 |
#' |
|
352 |
#' @return A list with y, y_rep and vars. |
|
353 |
#' |
|
354 |
#' @export |
|
355 | ||
356 |
tidy_dpp <- function(model, fit, draw = NULL, cores = NULL) { |
|
357 | ! |
`!!` <- rlang::`!!` |
358 | ! |
td <- tidy_data(model) |
359 | ! |
tpp <- tidy_posterior_predict(fit, model, draw = draw, cores = cores) |
360 | ! |
tpp$group <- NULL |
361 | ! |
template <- unique(td[, c("compartment", "time", "group_str", "type")]) |
362 | ! |
tpp <- dplyr::left_join(template, tpp, by = c("compartment", "time", |
363 | ! |
"group_str", "type")) |
364 |
# Arrange td and tpp similarly |
|
365 | ! |
tpp <- dplyr::group_by(tpp, |
366 | ! |
`!!`(rlang::sym("compartment")), |
367 | ! |
`!!`(rlang::sym("time")), |
368 | ! |
`!!`(rlang::sym("group_str")), |
369 | ! |
`!!`(rlang::sym("type"))) |
370 | ! |
tpp <- tidyr::nest(tpp) |
371 | ! |
all <- dplyr::left_join(td, tpp, by = c("compartment", "time", "group_str", "type")) |
372 |
# Extract y and y_rep |
|
373 | ! |
y <- all$value |
374 | ! |
y_rep <- lapply(all$data, function(x) { |
375 | ! |
x <- x[["value"]][order(x[["random_sample_id"]])] |
376 | ! |
x |
377 |
}) |
|
378 | ! |
y_rep <- do.call(cbind, y_rep) |
379 |
# Extract vars |
|
380 | ! |
vars <- all[, c("compartment", "time", "type", "group")] |
381 | ! |
groups <- vars[["group"]] |
382 | ! |
groups <- do.call(rbind, groups) |
383 | ! |
if (is.null(groups)) { |
384 | ! |
vars[["group."]] <- rep(list(NULL), nrow(vars)) |
385 | ! |
vars[["group"]] <- NULL |
386 |
} else { |
|
387 | ! |
groups_cols <- paste0("group.", colnames(groups)) |
388 | ! |
if (any(groups_cols %in% colnames(vars))) { |
389 | ! |
stop("One group column name already used (one of ", |
390 | ! |
groups_cols, ")", "\n", |
391 | ! |
"Group column names cannot be: ", colnames(vars), "\n") |
392 |
} |
|
393 | ! |
for (i in seq_along(groups_cols)) { |
394 | ! |
vars[[groups_cols[i]]] <- groups[, colnames(groups)[i]] |
395 |
} |
|
396 | ! |
vars[["groups"]] <- NULL |
397 |
} |
|
398 |
# Return |
|
399 | ! |
out <- list(y = y, |
400 | ! |
y_rep = y_rep, |
401 | ! |
vars = vars) |
402 | ! |
return(structure(out, class = c("ppcNetworkModel", class(out)))) |
403 |
} |
|
404 | ||
405 |
### * filter.ppcNetworkModel() (generic and method) |
|
406 | ||
407 |
# Precious help from: |
|
408 |
# https://r.789695.n4.nabble.com/R-CMD-check-warning-with-S3-method-td4692255.html |
|
409 |
# https://github.com/wch/s3methodtest/blob/master/R/test.r |
|
410 |
# to understand how to provide a filter method without loading dplyr by default. |
|
411 | ||
412 |
#' Filter (alias for filter function from dplyr) |
|
413 |
#' |
|
414 |
#' @param .data Data to filter. |
|
415 |
#' @param ... Passed to dplyr::filter. |
|
416 |
#' @param preserve Ignored. |
|
417 |
#' |
|
418 |
#' @return See the returned value for dplyr::filter. |
|
419 |
#' |
|
420 |
#' @name filter |
|
421 |
#' @importFrom dplyr filter |
|
422 |
#' @export filter |
|
423 |
#' |
|
424 |
NULL |
|
425 | ||
426 |
#' Filter method for output of tidy_data_and_posterior_predict() |
|
427 |
#' |
|
428 |
#' @param .data A ppcNetworkModel object. |
|
429 |
#' @param ... Passed to dplyr::filter. |
|
430 |
#' @param .preserve Ignored. |
|
431 |
#' |
|
432 |
#' @return A pccNetworkModel object filtered appropriately based on the |
|
433 |
#' [["vars"]] tibble. |
|
434 |
#' |
|
435 |
#' @importFrom dplyr filter |
|
436 |
#' @method filter ppcNetworkModel |
|
437 |
#' @export |
|
438 |
#' |
|
439 | ||
440 |
filter.ppcNetworkModel <- function(.data, ..., .preserve = FALSE) { |
|
441 | ! |
out <- .data |
442 | ! |
out[["vars"]][["row_id"]] <- seq_len(nrow(out[["vars"]])) |
443 | ! |
out[["vars"]] <- dplyr::filter(out[["vars"]], ...) |
444 | ! |
out[["y"]] <- out[["y"]][out[["vars"]][["row_id"]]] |
445 | ! |
out[["y_rep"]] <- out[["y_rep"]][, out[["vars"]][["row_id"]]] |
446 | ! |
out[["vars"]][["row_id"]] <- NULL |
447 | ! |
return(out) |
448 |
} |
|
449 | ||
450 |
### * tidy_trajectories() |
|
451 | ||
452 |
#' Build a tidy table with the trajectories for each iteration |
|
453 |
#' |
|
454 |
#' If neither \code{n_per_chain} and \code{n} are provided, all iterations are |
|
455 |
#' used. |
|
456 |
#' |
|
457 |
#' Warning: This function is still maturing and its interface and output might |
|
458 |
#' change in the future. |
|
459 |
#' |
|
460 |
#' @param nm A \code{networkModel} object. |
|
461 |
#' @param mcmc The corresponding output from \code{run_mcmc}. |
|
462 |
#' @param n_per_chain Integer, number of iterations randomly drawn per |
|
463 |
#' chain. Note that iterations are in sync across chains (in practice, |
|
464 |
#' random iterations are chosen, and then parameter values extracted for |
|
465 |
#' those same iterations from all chains). |
|
466 |
#' @param n Integer, number of iterations randomly drawn from \code{mcmc}. Note |
|
467 |
#' that iterations are *not* drawn in sync across chains in this case (use |
|
468 |
#' \code{n_per_chain} if you need to have the same iterations taken across |
|
469 |
#' all chains). |
|
470 |
#' @param n_grid Size of the time grid used to calculate trajectories |
|
471 |
#' @param cores Number of cores to use for parallel calculations. Default is |
|
472 |
#' \code{NULL}, which means to use the value stored in |
|
473 |
#' \code{options()[["mc.cores"]]} (or 1 if this value is not set). |
|
474 |
#' @param dt,grid_size Time step size or grid points, respectively. |
|
475 |
#' @param at Timepoints at which the predictions should be returned. |
|
476 |
#' @param end Final timepoint used in the projections. |
|
477 |
#' @param use_cache Boolean, use cache for faster calculations? |
|
478 |
#' |
|
479 |
#' @return A tidy table containing the mcmc iterations (chain, iteration, |
|
480 |
#' parameters), the grouping variables from the network model and the |
|
481 |
#' trajectories. |
|
482 |
#' |
|
483 |
#' @examples |
|
484 |
#' tt <- tidy_trajectories(aquarium_mod, aquarium_run, n = 10, cores = 2) |
|
485 |
#' tt |
|
486 |
#' |
|
487 |
#' @export |
|
488 | ||
489 |
tidy_trajectories <- function(nm, mcmc, n_per_chain = NULL, n = NULL, n_grid = 64, |
|
490 |
dt = NULL, grid_size = NULL, at = NULL, end = NULL, |
|
491 |
use_cache = TRUE, cores = NULL) { |
|
492 | ! |
cores <- get_n_cores(cores = cores) |
493 | ! |
to <- tidy_mcmc_list(mcmc) |
494 | ! |
arg_end <- end |
495 | ! |
if (use_cache) { |
496 | ! |
cache <- list() |
497 | ! |
rows_time_schemes <- list() |
498 | ! |
rows_encoded_events <- list() |
499 | ! |
for (i in seq_len(nrow(nm))) { |
500 | ! |
nmRow <- nm[i, ] |
501 | ! |
end <- arg_end |
502 | ! |
if (is.null(end)) { |
503 | ! |
if (is.null(at)) { |
504 | ! |
end <- max(nmRow$observations[[1]][["time"]]) |
505 | ! |
if (!is.null(nmRow[["events"]][[1]])) { |
506 | ! |
end <- max(c(end, nmRow$events[[1]][["time"]])) |
507 |
} |
|
508 |
} else { |
|
509 | ! |
end <- max(at) |
510 |
} |
|
511 |
} |
|
512 | ! |
rows_time_schemes[[i]] <- nm_row_get_time_scheme(nm_row = nmRow, dt = dt, |
513 | ! |
grid_size = grid_size, |
514 | ! |
end = end, at = at) |
515 | ! |
rows_encoded_events[[i]] <- encode_events(nmRow, end = end, dt = dt, |
516 | ! |
grid_size = grid_size) |
517 |
} |
|
518 | ! |
cache[["rows_time_schemes"]] <- rows_time_schemes |
519 | ! |
cache[["rows_encoded_events"]] <- rows_encoded_events |
520 |
} else { |
|
521 | ! |
cache <- NULL |
522 |
} |
|
523 | ! |
end <- arg_end |
524 | ! |
if (!is.null(n_per_chain)) { |
525 | ! |
my_iters <- sample(unique(to$mcmc.iteration), size = n_per_chain) |
526 | ! |
to <- to[to$mcmc.iteration %in% my_iters, ] |
527 |
} |
|
528 | ! |
if (!is.null(n)) { |
529 | ! |
if (!is.null(n_per_chain)) { |
530 | ! |
warning("Both \"n_per_chain\" and \"n\" are provided. Using \"n_per_chain\".") |
531 |
} else { |
|
532 | ! |
to <- to[sample(1:nrow(to), size = n), ] |
533 |
} |
|
534 |
} |
|
535 |
# Run networks for each chain and each iteration |
|
536 | ! |
trajectories <- purrr::map(seq_len(nrow(nm)), function(k) { |
537 | ! |
nmRow <- nm[k, ] |
538 | ! |
projections <- parallel::mclapply(seq_len(nrow(to)), function(i) { |
539 | ! |
nmRow <- set_params(nmRow, to$mcmc.parameters[[i]], force = TRUE, quick = TRUE) |
540 | ! |
nmRow <- project(nmRow, grid_size = n_grid, dt = dt, |
541 | ! |
at = at, end = end, cached_ts = cache[["rows_time_schemes"]][k], |
542 | ! |
cached_ee = cache[["rows_encoded_events"]][k]) |
543 | ! |
return(nmRow$trajectory[[1]]) |
544 | ! |
}, mc.cores = cores) |
545 | ! |
out <- to |
546 | ! |
out$trajectories <- projections |
547 | ! |
return(out) |
548 |
}) |
|
549 |
# Unnest |
|
550 | ! |
nm$trajectories <- trajectories |
551 | ! |
if (is.null(groups(nm))) { |
552 | ! |
my_out <- tidyr::unnest(nm[, "trajectories"], "trajectories") |
553 |
} else { |
|
554 |
# Careful unnesting to keep correct groups when there is more than |
|
555 |
# one grouping variable |
|
556 | ! |
groups <- nm[["group"]] |
557 | ! |
nm[["group"]] <- seq_len(nrow(nm)) |
558 | ! |
out <- tidyr::unnest(nm[, c("group", "trajectories")], "trajectories") |
559 | ! |
group_list <- groups[out[["group"]]] |
560 | ! |
out[["group"]] <- group_list |
561 | ! |
my_out <- out |
562 |
} |
|
563 |
# Return |
|
564 | ! |
return(my_out) |
565 |
} |
|
566 | ||
567 |
### * tidy_flows() |
|
568 | ||
569 |
#' Build a tidy table with the flows for each iteration |
|
570 |
#' |
|
571 |
#' If neither \code{n_per_chain} and \code{n} are provided, all iterations are |
|
572 |
#' used. |
|
573 |
#' |
|
574 |
#' Warning: This function is still maturing and its interface and output might |
|
575 |
#' change in the future. |
|
576 |
#' |
|
577 |
#' Note about how steady state sizes for split compartments are calculated: the |
|
578 |
#' steady size of the active portion is calculated divide it is divided by the |
|
579 |
#' active fraction (portion.act parameter) to get the total size including the |
|
580 |
#' refractory portion. In this case we get a "steady-state" refractory portion, |
|
581 |
#' consistent with steady state size of active fraction and with portion.act |
|
582 |
#' parameter. |
|
583 |
#' |
|
584 |
#' @param nm A \code{networkModel} object. |
|
585 |
#' @param mcmc The corresponding output from \code{run_mcmc}. |
|
586 |
#' @param n_per_chain Integer, number of iterations randomly drawn per |
|
587 |
#' chain. Note that iterations are in sync across chains (in practice, random |
|
588 |
#' iterations are chosen, and then parameter values extracted for those same |
|
589 |
#' iterations from all chains). |
|
590 |
#' @param n Integer, number of iterations randomly drawn from \code{mcmc}. Note |
|
591 |
#' that iterations are *not* drawn in sync across chains in this case (use |
|
592 |
#' \code{n_per_chain} if you need to have the same iterations taken across all |
|
593 |
#' chains). |
|
594 |
#' @param n_grid Size of the time grid used to calculate trajectories |
|
595 |
#' @param steady_state Boolean (default: FALSE). If TRUE, then steady state |
|
596 |
#' compartment sizes are calculated for each iteration and steady state flows |
|
597 |
#' are calculated from those compartment sizes. Note that any pulse that |
|
598 |
#' might be specified in the input model \code{nm} is ignored in this case. |
|
599 |
#' @param cores Number of cores to use for parallel calculations. Default is |
|
600 |
#' \code{NULL}, which means to use the value stored in |
|
601 |
#' \code{options()[["mc.cores"]]} (or 1 if this value is not set). |
|
602 |
#' @param dt,grid_size Time step size or grid points, respectively. |
|
603 |
#' @param at Timepoints at which the predictions should be returned. |
|
604 |
#' @param end Final timepoint used in the projections. |
|
605 |
#' @param use_cache Boolean, use cache for faster calculations? |
|
606 |
#' |
|
607 |
#' @return A tidy table containing the mcmc iterations (chain, iteration, |
|
608 |
#' parameters), the grouping variables from the network model and the |
|
609 |
#' flows. The returned flow values are the average flow per unit of time |
|
610 |
#' over the trajectory calculations (or steady state flows if |
|
611 |
#' \code{steady_state} is TRUE). |
|
612 |
#' |
|
613 |
#' @examples |
|
614 |
#' tf <- tidy_flows(aquarium_mod, aquarium_run, n_per_chain = 25, cores = 2) |
|
615 |
#' tf |
|
616 |
#' tfmcmc <- as.mcmc.list(tf) |
|
617 |
#' plot(tfmcmc) |
|
618 |
#' |
|
619 |
#' @export |
|
620 | ||
621 |
tidy_flows <- function(nm, mcmc, n_per_chain = NULL, n = NULL, n_grid = 64, |
|
622 |
steady_state = FALSE, |
|
623 |
dt = NULL, grid_size = NULL, at = NULL, end = NULL, |
|
624 |
use_cache = TRUE, cores = NULL) { |
|
625 | 5x |
cores <- get_n_cores(cores = cores) |
626 | 5x |
to <- tidy_mcmc_list(mcmc) |
627 | 5x |
arg_end <- end |
628 | 5x |
if (use_cache) { |
629 | 5x |
cache <- list() |
630 | 5x |
rows_time_schemes <- list() |
631 | 5x |
rows_encoded_events <- list() |
632 | 5x |
for (i in seq_len(nrow(nm))) { |
633 | 14x |
nmRow <- nm[i, ] |
634 | 14x |
end <- arg_end |
635 | 14x |
if (is.null(end)) { |
636 | 14x |
if (is.null(at)) { |
637 | 14x |
end <- max(nmRow$observations[[1]][["time"]]) |
638 | 14x |
if (!is.null(nmRow[["events"]][[1]])) { |
639 | ! |
end <- max(c(end, nmRow$events[[1]][["time"]])) |
640 |
} |
|
641 |
} else { |
|
642 | ! |
end <- max(at) |
643 |
} |
|
644 |
} |
|
645 | 14x |
rows_time_schemes[[i]] <- nm_row_get_time_scheme(nm_row = nmRow, dt = dt, |
646 | 14x |
grid_size = grid_size, |
647 | 14x |
end = end, at = at) |
648 | 14x |
rows_encoded_events[[i]] <- encode_events(nmRow, end = end, dt = dt, |
649 | 14x |
grid_size = grid_size) |
650 |
} |
|
651 | 5x |
cache[["rows_time_schemes"]] <- rows_time_schemes |
652 | 5x |
cache[["rows_encoded_events"]] <- rows_encoded_events |
653 |
} else { |
|
654 | ! |
cache <- NULL |
655 |
} |
|
656 | 5x |
end <- arg_end |
657 | 5x |
if (!is.null(n_per_chain)) { |
658 | ! |
my_iters <- sample(unique(to$mcmc.iteration), size = n_per_chain) |
659 | ! |
to <- to[to$mcmc.iteration %in% my_iters, ] |
660 |
} |
|
661 | 5x |
if (!is.null(n)) { |
662 | ! |
if (!is.null(n_per_chain)) { |
663 | ! |
warning("Both \"n_per_chain\" and \"n\" are provided. Using \"n_per_chain\".") |
664 |
} else { |
|
665 | ! |
to <- to[sample(1:nrow(to), size = n), ] |
666 |
} |
|
667 |
} |
|
668 |
# Run networks for each chain and each iteration |
|
669 | 5x |
if (!steady_state) { |
670 | ! |
flows <- purrr::map(seq_len(nrow(nm)), function(k) { |
671 | ! |
nmRow <- nm[k, ] |
672 | ! |
flows <- parallel::mclapply(seq_len(nrow(to)), function(i) { |
673 | ! |
nmRow <- set_params(nmRow, to$mcmc.parameters[[i]], force = TRUE, quick = TRUE) |
674 | ! |
nmRow <- project(nmRow, flows = "average", grid_size = n_grid, dt = dt, |
675 | ! |
at = at, end = end, cached_ts = cache[["rows_time_schemes"]][k], |
676 | ! |
cached_ee = cache[["rows_encoded_events"]][k]) |
677 | ! |
return(nmRow$flows[[1]]) |
678 | ! |
}, mc.cores = cores) |
679 | ! |
out <- to |
680 | ! |
out$flows <- flows |
681 | ! |
return(out) |
682 |
}) |
|
683 |
} else { |
|
684 | 5x |
flows <- purrr::map(seq_len(nrow(nm)), function(k) { |
685 | 14x |
nmRow <- nm[k, ] |
686 | 14x |
flows <- parallel::mclapply(seq_len(nrow(to)), function(i) { |
687 | 280x |
nmRow_to <- set_params(nmRow, to$mcmc.parameters[[i]], force = TRUE, quick = TRUE) |
688 | 280x |
ss_sizes <- calculate_steady_state_one_row(nmRow_to) |
689 | 280x |
ss_inits <- tibble::tibble(compartment = names(ss_sizes), |
690 | 280x |
size = ss_sizes, |
691 | 280x |
proportion = 0) |
692 | 280x |
nmRow_to$initial <- list(ss_inits) |
693 | 280x |
nmRow_to <- project(nmRow_to, grid_size = 3, flows = "average", |
694 | 280x |
ignore_pulses = TRUE) |
695 | 280x |
return(nmRow_to$flows[[1]]) |
696 | 14x |
}, mc.cores = cores) |
697 | 14x |
out <- to |
698 | 14x |
out$flows <- flows |
699 | 14x |
return(out) |
700 |
}) |
|
701 |
} |
|
702 |
# Unnest |
|
703 | 5x |
nm$flows <- flows |
704 | 5x |
if (is.null(groups(nm))) { |
705 | 2x |
my_out <- tidyr::unnest(nm[, "flows"], "flows") |
706 |
} else { |
|
707 |
# Careful unnesting to keep correct groups when there is more than |
|
708 |
# one grouping variable |
|
709 | 3x |
groups <- nm[["group"]] |
710 | 3x |
nm[["group"]] <- seq_len(nrow(nm)) |
711 | 3x |
out <- tidyr::unnest(nm[, c("group", "flows")], "flows") |
712 | 3x |
group_list <- groups[out[["group"]]] |
713 | 3x |
out[["group"]] <- group_list |
714 | 3x |
my_out <- out |
715 |
} |
|
716 |
# Add a "tidy_flows" class |
|
717 | 5x |
return(structure(my_out, class = c("tidy_flows", class(my_out)))) |
718 |
} |
|
719 | ||
720 |
### * as.mcmc.list.tidy_flows() method |
|
721 | ||
722 |
#' Convert a \code{tidy_flows} object to an \code{mcmc.list} |
|
723 |
#' |
|
724 |
#' @param x A tidy flow object, as returned by \code{\link{tidy_flows}}. Note |
|
725 |
#' that all chains must have the same iterations extracted (i.e. you must |
|
726 |
#' use \code{n_per_chain} when calling \code{\link{tidy_flows}}). |
|
727 |
#' @param ... Not used for now. |
|
728 |
#' |
|
729 |
#' @return A \code{mcmc.list} object, with ordered iterations. |
|
730 |
#' |
|
731 |
#' @method as.mcmc.list tidy_flows |
|
732 |
#' |
|
733 |
#' @export |
|
734 | ||
735 |
as.mcmc.list.tidy_flows <- function(x, ...) { |
|
736 | ! |
`!!` <- rlang::`!!` |
737 | ! |
if (!length(unique(table(x$mcmc.iteration))) == 1 | |
738 | ! |
all(table(x$mcmc.iteration) == 1)) { |
739 | ! |
stop("Not all chains have the same iterations in \"x\". Make sure you use \"n_per_chain\" (with a value > 1) and not \"n\" when calling \"tidy_flows()\".") |
740 |
} |
|
741 | ! |
groups <- x[["group"]] |
742 | ! |
if (!is.null(groups)) { |
743 | ! |
groups <- sapply(groups, function(z) paste0(z, collapse = ",")) |
744 |
} |
|
745 |
# Convert flow tibbles to named vectors |
|
746 | ! |
for (i in seq_len(nrow(x))) { |
747 | ! |
f <- x$flows[[i]] |
748 | ! |
grp_suffix <- "" |
749 | ! |
if (!is.null(groups)) { |
750 | ! |
grp_suffix <- paste0("|", groups[i]) |
751 |
} |
|
752 | ! |
params <- paste0(f$from, "_to_", f$to, grp_suffix) |
753 | ! |
f <- setNames(f$average_flow, nm = params) |
754 | ! |
x$flows[[i]] <- f |
755 |
} |
|
756 |
# Pool parameter values per iteration per chain |
|
757 | ! |
x <- dplyr::group_by(x[, c("mcmc.chain", "mcmc.iteration", "flows")], |
758 | ! |
`!!`(rlang::sym("mcmc.chain")), |
759 | ! |
`!!`(rlang::sym("mcmc.iteration"))) |
760 | ! |
x <- tidyr::nest(x) |
761 | ! |
x$data <- lapply(x$data, function(z) unlist(z$flows)) |
762 | ! |
x <- dplyr::ungroup(x) |
763 | ! |
x <- tidyr::nest(dplyr::group_by(x, `!!`(rlang::sym("mcmc.chain")))) |
764 | ! |
x$data <- lapply(x$data, function(z) { |
765 | ! |
z <- z[order(z$mcmc.iteration), ] |
766 | ! |
params <- do.call(rbind, z$data) |
767 | ! |
coda::as.mcmc(params) |
768 |
}) |
|
769 |
# Return |
|
770 | ! |
out <- coda::as.mcmc.list(x$data) |
771 | ! |
return(structure(out, class = c("tidy_flows_mcmc.list", class(out)))) |
772 |
} |
|
773 | ||
774 | ||
775 |
### * sample_params() |
|
776 | ||
777 |
#' Sample parameter values from priors |
|
778 |
#' |
|
779 |
#' @param nm A \code{networkModel} object. |
|
780 |
#' |
|
781 |
#' @return A named vector containing parameter values. |
|
782 |
#' |
|
783 |
#' @examples |
|
784 |
#' library(magrittr) |
|
785 |
#' |
|
786 |
#' p <- sample_params(aquarium_mod) |
|
787 |
#' p |
|
788 |
#' |
|
789 |
#' proj <- aquarium_mod %>% set_params(p) %>% project(end = 10) |
|
790 |
#' plot(proj) |
|
791 |
#' |
|
792 |
#' @export |
|
793 | ||
794 |
sample_params <- function(nm) { |
|
795 | 3x |
p <- priors(nm) |
796 | 3x |
p$value <- sapply(p$prior, function(x) sample_from_prior(x, n = 1)) |
797 | 3x |
return(tibble::deframe(p[, c("in_model", "value")])) |
798 |
} |
|
799 | ||
800 |
### * tidy_steady_states() |
|
801 | ||
802 |
#' Build a tidy table with the calculated steady states for each iteration |
|
803 |
#' |
|
804 |
#' If neither \code{n_per_chain} and \code{n} are provided, all iterations are |
|
805 |
#' used. |
|
806 |
#' |
|
807 |
#' Note about how steady state sizes for split compartments are calculated: the |
|
808 |
#' steady size of the active portion is calculated divide it is divided by the |
|
809 |
#' active fraction (portion.act parameter) to get the total size including the |
|
810 |
#' refractory portion. In this case we get a "steady-state" refractory portion, |
|
811 |
#' consistent with steady state size of active fraction and with portion.act |
|
812 |
#' parameter. |
|
813 |
#' |
|
814 |
#' @param nm A \code{networkModel} object. |
|
815 |
#' @param mcmc The corresponding output from \code{run_mcmc}. |
|
816 |
#' @param n_per_chain Integer, number of iterations randomly drawn per |
|
817 |
#' chain. Note that iterations are in sync across chains (in practice, |
|
818 |
#' random iterations are chosen, and then parameter values extracted for |
|
819 |
#' those same iterations from all chains). |
|
820 |
#' @param n Integer, number of iterations randomly drawn from \code{mcmc}. Note |
|
821 |
#' that iterations are *not* drawn in sync across chains in this case (use |
|
822 |
#' \code{n_per_chain} if you need to have the same iterations taken across |
|
823 |
#' all chains). |
|
824 |
#' |
|
825 |
#' @return A tidy table containing the mcmc iterations (chain, iteration, |
|
826 |
#' parameters), the grouping variables from the network model and the |
|
827 |
#' steady state sizes. |
|
828 |
#' |
|
829 |
#' @export |
|
830 | ||
831 |
tidy_steady_states <- function(nm, mcmc, n_per_chain = NULL, n = NULL) { |
|
832 | 5x |
to <- tidy_mcmc_list(mcmc) |
833 | 5x |
if (!is.null(n_per_chain)) { |
834 | ! |
my_iters <- sample(unique(to$mcmc.iteration), size = n_per_chain) |
835 | ! |
to <- to[to$mcmc.iteration %in% my_iters, ] |
836 |
} |
|
837 | 5x |
if (!is.null(n)) { |
838 | ! |
if (!is.null(n_per_chain)) { |
839 | ! |
warning("Both \"n_per_chain\" and \"n\" are provided. Using \"n_per_chain\".") |
840 |
} else { |
|
841 | ! |
to <- to[sample(1:nrow(to), size = n), ] |
842 |
} |
|
843 |
} |
|
844 |
# Calculate steady states for each chain and each iteration |
|
845 | 5x |
ss_sizes <- purrr::map(seq_len(nrow(nm)), function(k) { |
846 | 14x |
nmRow <- nm[k, ] |
847 | 14x |
sizes <- list() |
848 | 14x |
for (i in seq_len(nrow(to))) { |
849 | 280x |
nmRow <- set_params(nmRow, to$mcmc.parameters[[i]], force = TRUE) |
850 | 280x |
sizes[[i]] <- calculate_steady_state_one_row(nmRow) |
851 |
} |
|
852 | 14x |
out <- to |
853 | 14x |
out$stable_sizes <- sizes |
854 | 14x |
return(out) |
855 |
}) |
|
856 |
# Unnest |
|
857 | 5x |
nm$stable_sizes <- ss_sizes |
858 | 5x |
if (is.null(groups(nm))) { |
859 | 2x |
my_out <- tidyr::unnest(nm[, "stable_sizes"], "stable_sizes") |
860 |
} else { |
|
861 |
# Careful unnesting to keep correct groups when there is more than |
|
862 |
# one grouping variable |
|
863 | 3x |
groups <- nm[["group"]] |
864 | 3x |
nm[["group"]] <- seq_len(nrow(nm)) |
865 | 3x |
out <- tidyr::unnest(nm[, c("group", "stable_sizes")], "stable_sizes") |
866 | 3x |
group_list <- groups[out[["group"]]] |
867 | 3x |
out[["group"]] <- group_list |
868 | 3x |
my_out <- out |
869 |
} |
|
870 |
# Add a "tidy_steady_states" class |
|
871 | 5x |
return(structure(my_out, class = c("tidy_steady_states", class(my_out)))) |
872 |
} |
|
873 | ||
874 |
### * as.mcmc.list.tidy_steady_states() method |
|
875 | ||
876 |
#' Convert a \code{tidy_steady_states} object to an \code{mcmc.list} |
|
877 |
#' |
|
878 |
#' @param x A tidy steady states object, as returned by |
|
879 |
#' \code{\link{tidy_steady_states}}. Note that all chains must have the |
|
880 |
#' same iterations extracted (i.e. you must use \code{n_per_chain} when |
|
881 |
#' calling \code{\link{tidy_flows}}). |
|
882 |
#' @param ... Not used for now. |
|
883 |
#' |
|
884 |
#' @return A \code{mcmc.list} object, with ordered iterations. |
|
885 |
#' |
|
886 |
#' @method as.mcmc.list tidy_steady_states |
|
887 |
#' |
|
888 |
#' @export |
|
889 | ||
890 |
as.mcmc.list.tidy_steady_states <- function(x, ...) { |
|
891 | ! |
`!!` <- rlang::`!!` |
892 | ! |
if (!length(unique(table(x$mcmc.iteration))) == 1 | |
893 | ! |
all(table(x$mcmc.iteration) == 1)) { |
894 | ! |
stop("Not all chains have the same iterations in \"x\". Make sure you use \"n_per_chain\" (with a value > 1) and not \"n\" when calling \"tidy_flows()\".") |
895 |
} |
|
896 | ! |
groups <- x[["group"]] |
897 | ! |
if (!is.null(groups)) { |
898 | ! |
groups <- sapply(groups, function(z) paste0(z, collapse = ",")) |
899 |
} |
|
900 |
# Convert steady state names to incorporate group name |
|
901 | ! |
for (i in seq_len(nrow(x))) { |
902 | ! |
f <- x$stable_sizes[[i]] |
903 | ! |
grp_suffix <- "" |
904 | ! |
if (!is.null(groups)) { |
905 | ! |
grp_suffix <- paste0("|", groups[i]) |
906 |
} |
|
907 | ! |
params <- paste0(names(f), grp_suffix) |
908 | ! |
f <- setNames(f, nm = params) |
909 | ! |
x$stable_sizes[[i]] <- f |
910 |
} |
|
911 |
# Pool parameter values per iteration per chain |
|
912 | ! |
x <- dplyr::group_by(x[, c("mcmc.chain", "mcmc.iteration", "stable_sizes")], |
913 | ! |
`!!`(rlang::sym("mcmc.chain")), |
914 | ! |
`!!`(rlang::sym("mcmc.iteration"))) |
915 | ! |
x <- tidyr::nest(x) |
916 | ! |
x$data <- lapply(x$data, function(z) unlist(z$stable_sizes)) |
917 | ! |
x <- dplyr::ungroup(x) |
918 | ! |
x <- tidyr::nest(dplyr::group_by(x, `!!`(rlang::sym("mcmc.chain")))) |
919 | ! |
x$data <- lapply(x$data, function(z) { |
920 | ! |
z <- z[order(z$mcmc.iteration), ] |
921 | ! |
params <- do.call(rbind, z$data) |
922 | ! |
coda::as.mcmc(params) |
923 |
}) |
|
924 |
# Return |
|
925 | ! |
out <- coda::as.mcmc.list(x$data) |
926 | ! |
return(structure(out, class = c("tidy_flows_mcmc.list", class(out)))) |
927 |
} |
|
928 | ||
929 |
### * tidy_mcmc() |
|
930 | ||
931 |
#' Extract a tidy output from an mcmc.list |
|
932 |
#' |
|
933 |
#' @param x An mcmc.list object |
|
934 |
#' @param spread Boolean, spread the parameters into separate columns? |
|
935 |
#' @param include_constant Boolean, include constant parameters as proper |
|
936 |
#' parameter traces? |
|
937 |
#' |
|
938 |
#' @return A tidy table containing one iteration per row |
|
939 |
#' |
|
940 |
#' @examples |
|
941 |
#' fit <- lapply(1:4, function(i) { |
|
942 |
#' z <- matrix(rnorm(200), ncol = 2) |
|
943 |
#' colnames(z) <- c("alpha", "beta") |
|
944 |
#' coda::as.mcmc(z) |
|
945 |
#' }) |
|
946 |
#' fit <- coda::as.mcmc.list(fit) |
|
947 |
#' tidy_mcmc(fit) |
|
948 |
#' tidy_mcmc(fit, spread = TRUE) |
|
949 |
#' |
|
950 |
#' @export |
|
951 | ||
952 |
tidy_mcmc <- function(x, spread = FALSE, include_constant = TRUE) { |
|
953 | 22x |
z <- x |
954 | 22x |
n_iters <- nrow(z[[1]]) |
955 | 22x |
constant_params <- attr(z, "constant_params") |
956 |
# Add traces for constant params (if needed) |
|
957 | 22x |
if (!is.null(constant_params) & include_constant) { |
958 | 10x |
constant_template <- matrix(NA, ncol = length(constant_params), |
959 | 10x |
nrow = n_iters) |
960 | 10x |
for (i in seq_along(constant_params)) { |
961 | 48x |
constant_template[, i] <- constant_params[i] |
962 |
} |
|
963 | 10x |
colnames(constant_template) <- names(constant_params) |
964 | 10x |
for (i in seq_along(z)) { |
965 | 20x |
zi <- coda::as.mcmc(cbind(z[[i]], constant_template)) |
966 | 20x |
attr(zi, "mcpar") <- attr(z[[i]], "mcpar") |
967 | 20x |
z[[i]] <- zi |
968 |
} |
|
969 |
} |
|
970 | 22x |
paramNames <- colnames(z[[1]]) |
971 |
# If spread = FALSE |
|
972 | 22x |
if (!spread) { |
973 | 22x |
tables <- lapply(seq_along(z), function(i) { |
974 | 44x |
my_chain <- as.matrix(z[[i]]) |
975 | 44x |
tibble::tibble(mcmc.chain = i, |
976 | 44x |
mcmc.iteration = seq_len(nrow(my_chain)), |
977 | 44x |
mcmc.parameters = lapply(seq_len(nrow(my_chain)), |
978 | 44x |
function(k) { |
979 | 420x |
setNames(my_chain[k,], paramNames) |
980 |
}) |
|
981 |
) |
|
982 |
}) |
|
983 | 22x |
out <- dplyr::bind_rows(tables) |
984 | 22x |
return(out) |
985 |
} |
|
986 |
# If spread = TRUE |
|
987 | ! |
mcmc_parameters <- lapply(seq_along(z), function(i) { |
988 | ! |
my_chain <- tibble::as_tibble(as.matrix(z[[i]])) |
989 | ! |
mcmc_pars <- tibble::tibble(mcmc.chain = i, |
990 | ! |
mcmc.iteration = seq_len(nrow(my_chain))) |
991 | ! |
dplyr::bind_cols(mcmc_pars, my_chain) |
992 |
}) |
|
993 | ! |
out <- dplyr::bind_rows(mcmc_parameters) |
994 | ! |
return(out) |
995 |
} |
|
996 |
1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * make_topology() |
|
4 | ||
5 |
#' Build a network topology |
|
6 |
#' |
|
7 |
#' @section Link format: |
|
8 |
#' |
|
9 |
#' Links are defined by a string describing connections between groups of |
|
10 |
#' compartments and their directions using \code{->} and \code{<-}. The groups |
|
11 |
#' being connected can be a single compartment or several compartments. Several |
|
12 |
#' compartments can be separated either by commas or by spaces. Connection |
|
13 |
#' descriptions can be chained in a single string. |
|
14 |
#' |
|
15 |
#' Some valid links are for example \code{"NH4, NO3 -> epi"} or |
|
16 |
#' \code{"NH4, NO3 -> epi -> lepto, tricor"}. |
|
17 |
#' |
|
18 |
#' See the Examples section for more details. |
|
19 |
#' |
|
20 |
#' @param links Vector of strings defining the connections between |
|
21 |
#' compartments. Alternatively, can be a data frame with two columns |
|
22 |
#' describing the source and destination of each link, in which case the |
|
23 |
#' arguments "from" and "to" must be provided. |
|
24 |
#' @param from Optional, string containing the column name for sources if |
|
25 |
#' "links" is a data frame |
|
26 |
#' @param to Optional, string containing the column name for destinations if |
|
27 |
#' "links" is a data frame |
|
28 |
#' @param split Optional, vector of strings containing the names of the |
|
29 |
#' compartments which comprise an active and a refractory portions |
|
30 |
#' |
|
31 |
#' @return A matrix describing the topology of the network (with class |
|
32 |
#' topology). A coefficient (i,j) is 1 if material can flow from |
|
33 |
#' compartment j (column) into compartment i (row), and 0 otherwise. |
|
34 |
#' |
|
35 |
#' @examples |
|
36 |
#' topo <- isotracer:::make_topology(links = "NH4, NO3 -> epi -> pseph, tricor") |
|
37 |
#' topo |
|
38 |
#' |
|
39 |
#' # A larger foodweb |
|
40 |
#' links <- c("NH4, NO3 -> seston, epi, CBOM, FBOM", |
|
41 |
#' "seston -> lepto", "epi -> petro, pseph", |
|
42 |
#' "CBOM, FBOM -> eudan", "CBOM -> phyllo", |
|
43 |
#' "FBOM -> tricor -> arg, euthy") |
|
44 |
#' topo2 <- isotracer:::make_topology(links = links, split = "epi") |
|
45 |
#' topo2 |
|
46 |
#' |
|
47 |
#' # Using a data frame to specify the links |
|
48 |
#' links <- data.frame(source = c("NH4", "NO3", "epi"), |
|
49 |
#' consumer = c("epi", "epi", "petro")) |
|
50 |
#' topo3 <- isotracer:::make_topology(links, from = "source", to = "consumer") |
|
51 |
#' topo3 |
|
52 |
#' |
|
53 |
#' @keywords internal |
|
54 |
#' @noRd |
|
55 | ||
56 |
make_topology <- function(links, from = NULL, to = NULL, split = NULL) { |
|
57 |
# Links are provided as strings |
|
58 | 42x |
if (is.null(from) & is.null(to)) { |
59 | 40x |
out <- build_uptake_mask(links = links) |
60 | 40x |
out <- structure(out, class = c("topology", class(out))) |
61 | 40x |
attr(out, "split") = split |
62 | 40x |
return(out) |
63 |
} |
|
64 |
# Links are provided as a data frame |
|
65 |
# Build the links strings from the input data frame |
|
66 | 2x |
links <- paste(links[[from]], links[[to]], sep = " -> ") |
67 | 2x |
return(make_topology(links, split = split)) |
68 |
} |
|
69 | ||
70 |
### * topo_get_upsilon_names() |
|
71 | ||
72 |
#' @keywords internal |
|
73 |
#' @noRd |
|
74 | ||
75 |
topo_get_upsilon_names <- function(topo) { |
|
76 | 90x |
comps <- colnames(topo) |
77 | 90x |
out <- rep(NA, ncol(topo)^2) |
78 | 90x |
k <- 1 |
79 | 90x |
for (i in seq_len(nrow(topo))) { |
80 | 290x |
for (j in seq_len(ncol(topo))) { |
81 | 1058x |
if (topo[i,j] == 1) { |
82 | 270x |
out[k] <- paste("upsilon", comps[j], "to", comps[i], sep = "_") |
83 | 270x |
k = k + 1 |
84 |
} |
|
85 |
} |
|
86 |
} |
|
87 | 90x |
if (k == 1) { |
88 | ! |
return(vector()) |
89 |
} |
|
90 | 90x |
return(out[1:(k-1)]) |
91 |
} |
|
92 | ||
93 |
### * topo_get_lambda_names() |
|
94 | ||
95 |
#' @keywords internal |
|
96 |
#' @noRd |
|
97 | ||
98 |
topo_get_lambda_names <- function(topo) { |
|
99 | 90x |
comps <- colnames(topo) |
100 | 90x |
out <- rep(NA, ncol(topo)) |
101 | 90x |
k <- 1 |
102 | 90x |
for (j in seq_len(ncol(topo))) { |
103 | 290x |
out[k] <- paste("lambda", comps[j], sep = "_") |
104 | 290x |
k = k + 1 |
105 |
} |
|
106 | 90x |
if (k == 1) { |
107 | ! |
return(vector()) |
108 |
} |
|
109 | 90x |
return(out[1:(k-1)]) |
110 |
} |
|
111 | ||
112 |
### * topo_get_portionAct_names() |
|
113 | ||
114 |
#' @keywords internal |
|
115 |
#' @noRd |
|
116 | ||
117 |
topo_get_portionAct_names <- function(topo) { |
|
118 | 90x |
split <- attr(topo, "split") |
119 | 90x |
if (is.null(split)) { |
120 | 86x |
return(NULL) |
121 |
} |
|
122 | 4x |
params <- paste0("portion.act_", split) |
123 | 4x |
return(params) |
124 |
} |
|
125 |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * topo() |
|
4 | ||
5 |
#' Return the list of topologies, or a unique topology if all identical |
|
6 |
#' |
|
7 |
#' @param nm A \code{networkModel} object. |
|
8 |
#' @param simplify Boolean, return only a unique topology if all topologies are |
|
9 |
#' identical or if there is only one? Default is TRUE. |
|
10 |
#' |
|
11 |
#' @return A list of the \code{networkModel} topologies or, if all topologies |
|
12 |
#' are identical (or if there is only one) and \code{simplify} is TRUE, a |
|
13 |
#' single topology (not wrapped into a single-element list). |
|
14 |
#' |
|
15 |
#' @examples |
|
16 |
#' aquarium_mod |
|
17 |
#' topo(aquarium_mod) |
|
18 |
#' |
|
19 |
#' trini_mod |
|
20 |
#' topo(trini_mod) |
|
21 |
#' @export |
|
22 | ||
23 |
topo <- function(nm, simplify = TRUE) { |
|
24 | 1131x |
out <- nm[["topology"]] |
25 | 1131x |
if (simplify && length(unique(out)) == 1) { |
26 | 1128x |
return(unique(out)[[1]]) |
27 |
} |
|
28 | 3x |
return(out) |
29 |
} |
|
30 | ||
31 |
### * prop_family() |
|
32 | ||
33 |
#' Return the distribution family for observed proportions |
|
34 |
#' |
|
35 |
#' @param nm A \code{networkModel} object. |
|
36 |
#' @param quiet Boolean for being quiet about explaining the role of eta |
|
37 |
#' (default is \code{FALSE}). |
|
38 |
#' |
|
39 |
#' @return A character string describing the distribution family used to model |
|
40 |
#' observed proportions. |
|
41 |
#' |
|
42 |
#' @examples |
|
43 |
#' prop_family(aquarium_mod) |
|
44 |
#' prop_family(trini_mod) |
|
45 |
#' |
|
46 |
#' @export |
|
47 | ||
48 |
prop_family <- function(nm, quiet = FALSE) { |
|
49 | ! |
z <- attr(nm, "prop_family") |
50 | ! |
if (!quiet) { |
51 | ! |
describe_z_eta("eta", z) |
52 |
} |
|
53 | ! |
z |
54 |
} |
|
55 | ||
56 |
### * size_family() |
|
57 | ||
58 |
#' Return the distribution family for observed sizes |
|
59 |
#' |
|
60 |
#' @param nm A \code{networkModel} object. |
|
61 |
#' @param quiet Boolean for being quiet about explaining the role of zeta |
|
62 |
#' (default is \code{FALSE}). |
|
63 |
#' |
|
64 |
#' @return A character string describing the distribution family used to model |
|
65 |
#' observed sizes. |
|
66 |
#' |
|
67 |
#' @examples |
|
68 |
#' size_family(aquarium_mod) |
|
69 |
#' size_family(trini_mod) |
|
70 |
#' |
|
71 |
#' @export |
|
72 | ||
73 |
size_family <- function(nm, quiet = FALSE) { |
|
74 | ! |
z <- attr(nm, "size_family") |
75 | ! |
if (!quiet) { |
76 | ! |
describe_z_eta("zeta", z) |
77 |
} |
|
78 | ! |
z |
79 |
} |
|
80 | ||
81 |
### * groups() method for networkModel |
|
82 | ||
83 |
#' Get the grouping for a \code{networkModel} object |
|
84 |
#' |
|
85 |
#' @param x A \code{networkModel} object. |
|
86 |
#' |
|
87 |
#' @return A tibble giving the grouping variable(s) for the input network |
|
88 |
#' model. This tibble is in the same order as the rows of the input network |
|
89 |
#' model. If the input network model did not have any grouping variable, |
|
90 |
#' returns \code{NULL}. |
|
91 |
#' |
|
92 |
#' @importFrom dplyr groups |
|
93 |
#' @method groups networkModel |
|
94 |
#' |
|
95 |
#' @examples |
|
96 |
#' groups(aquarium_mod) |
|
97 |
#' groups(trini_mod) |
|
98 |
#' |
|
99 |
#' @export |
|
100 | ||
101 |
groups.networkModel <- function(x) { |
|
102 | 30x |
nm_get_groups(x, error = FALSE) |
103 |
} |
|
104 | ||
105 |
### * priors() |
|
106 | ||
107 |
#' Return the tibble containing the priors of a networkModel |
|
108 |
#' |
|
109 |
#' @param nm A \code{networkModel} object. |
|
110 |
#' @param fix_set_params If TRUE, parameters for which a value is set are given a |
|
111 |
#' fixed value (i.e. their prior is equivalent to a point value). |
|
112 |
#' @param quiet Boolean to control verbosity. |
|
113 |
#' |
|
114 |
#' @return A tibble giving the current priors defined for the input network |
|
115 |
#' model. |
|
116 |
#' |
|
117 |
#' @examples |
|
118 |
#' priors(aquarium_mod) |
|
119 |
#' priors(trini_mod) |
|
120 |
#' |
|
121 |
#' @export |
|
122 | ||
123 |
priors <- function(nm, fix_set_params = FALSE, quiet = FALSE) { |
|
124 | 74x |
if (fix_set_params) { |
125 | ! |
set_params <- attr(nm, "parameterValues") |
126 | ! |
if (!is.null(set_params)) { |
127 | ! |
for (i in seq_len(nrow(set_params))) { |
128 | ! |
myPrior <- constant_p(value = set_params[["value"]][i]) |
129 | ! |
nm <- set_prior(nm, myPrior, param = set_params[["parameter"]][i], |
130 | ! |
use_regexp = FALSE, quiet = quiet) |
131 |
} |
|
132 |
} |
|
133 |
} |
|
134 | 74x |
out <- attr(nm, "priors") |
135 | 74x |
return(out) |
136 |
} |
|
137 | ||
138 |
### * missing_priors() |
|
139 | ||
140 |
#' Get a table with parameters which are missing priors |
|
141 |
#' |
|
142 |
#' @param nm A \code{networkModel} object. |
|
143 |
#' |
|
144 |
#' @return A tibble containing the parameters which are missing a prior. If no |
|
145 |
#' priors are missing, the tibble contains zero row. |
|
146 |
#' |
|
147 |
#' @examples |
|
148 |
#'# Using a subset of the topology from the Trinidad case study |
|
149 |
#' m <- new_networkModel() %>% |
|
150 |
#' set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph") |
|
151 |
#' |
|
152 |
#' # No prior is set by default |
|
153 |
#' priors(m) |
|
154 |
#' |
|
155 |
#' # Set some priors |
|
156 |
#' m <- set_priors(m, normal_p(0, 10), "lambda") |
|
157 |
#' priors(m) |
|
158 |
#' |
|
159 |
#' # Which parameters are missing a prior? |
|
160 |
#' missing_priors(m) |
|
161 |
#' |
|
162 |
#' @export |
|
163 | ||
164 |
missing_priors <- function(nm) { |
|
165 | ! |
p <- priors(nm) |
166 | ! |
p <- p[sapply(p$prior, is.null), ] |
167 | ! |
return(p) |
168 |
} |
|
169 | ||
170 |
### * params() |
|
171 | ||
172 |
#' Return the parameters of a network model |
|
173 |
#' |
|
174 |
#' @param nm A \code{networkModel} object. |
|
175 |
#' @param simplify If \code{TRUE}, return a vector containing the names of all |
|
176 |
#' model parameters (default: \code{FALSE}). |
|
177 |
#' |
|
178 |
#' @return A tibble containing the parameter names and their current value (if |
|
179 |
#' set). If \code{simplify} is \code{TRUE}, only return a sorted character |
|
180 |
#' vector containing the parameters names. |
|
181 |
#' |
|
182 |
#' @examples |
|
183 |
#' params(aquarium_mod) |
|
184 |
#' params(trini_mod) |
|
185 |
#' params(trini_mod, simplify = TRUE) |
|
186 |
#' |
|
187 |
#' @export |
|
188 | ||
189 |
params <- function(nm, simplify = FALSE) { |
|
190 | 75x |
stopifnot("parameters" %in% colnames(nm)) |
191 | 75x |
params <- dplyr::bind_rows(nm[["parameters"]]) |
192 | 75x |
if (simplify) { |
193 | 75x |
return(sort(unique(params[["in_model"]]))) |
194 |
} |
|
195 | ! |
if (!"value" %in% colnames(params)) { |
196 | ! |
params[["value"]] <- as.numeric(rep(NA, nrow(params))) |
197 |
} |
|
198 | ! |
params <- unique(params[, c("in_model", "value")]) |
199 | ! |
params <- params[order(params[["in_model"]]), ] |
200 | ! |
if (!all(table(params[["in_model"]]) == 1)) { |
201 | ! |
stop("Some parameters have two different values assigned to them.") |
202 |
} |
|
203 | ! |
return(params) |
204 |
} |
|
205 | ||
206 |
### * comps() |
|
207 | ||
208 |
#' Return the compartments of a network model |
|
209 |
#' |
|
210 |
#' @param nm A \code{networkModel} object. |
|
211 |
#' |
|
212 |
#' @return A list of character vectors, with one list element per row of the |
|
213 |
#' input network model (list elements are in the same order as the input |
|
214 |
#' network model rows). Each list element containing the names of the |
|
215 |
#' compartments in the topology defined in the corresponding row of the |
|
216 |
#' input network model. |
|
217 |
#' |
|
218 |
#' @examples |
|
219 |
#' aquarium_mod |
|
220 |
#' comps(aquarium_mod) |
|
221 |
#' |
|
222 |
#' trini_mod |
|
223 |
#' comps(trini_mod) |
|
224 |
#' |
|
225 |
#' @export |
|
226 | ||
227 |
comps <- function(nm) { |
|
228 | 628x |
comps <- nm[, "topology"] |
229 | 628x |
comps$compartments <- lapply(comps$topology, colnames) |
230 | 628x |
return(comps[["compartments"]]) |
231 |
} |
|
232 |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * Description |
|
4 | ||
5 |
# Methods for \code{coda::mcmc.list} objects: |
|
6 |
# |
|
7 |
# - Provide simple math operations for mcmc.list objects and also allows to use |
|
8 |
# math functions such as log() or exp() on mcmc.list objects. |
|
9 |
# |
|
10 |
# - Provide simple interface to select parameters based on their name. |
|
11 |
# |
|
12 |
# Those methods are not designed specifically for this package, and could be |
|
13 |
# submitted e.g. for integration in the coda package. |
|
14 | ||
15 |
### * Helper functions / generics |
|
16 | ||
17 |
### * Ops.mcmc.list() |
|
18 | ||
19 |
# Based on |
|
20 |
# ?groupGeneric |
|
21 |
# ?.Generic |
|
22 |
# https://stackoverflow.com/questions/35902360/r-implement-group-generics-ops-to-enable-comparison-of-s3-objects |
|
23 |
# (cdeterman answer in the above) |
|
24 | ||
25 |
#' Ops generics for \code{\link[coda]{mcmc.list}} objects |
|
26 |
#' |
|
27 |
#' @param e1 First operand |
|
28 |
#' @param e2 Second operand |
|
29 |
#' |
|
30 |
#' @return A \code{mcmc.list} object (with the added class |
|
31 |
#' \code{derived.mcmc.list}). |
|
32 |
#' |
|
33 |
#' @examples |
|
34 |
#' \dontrun{ |
|
35 |
#' # aquarium_run is a coda::mcmc.list object shipped with the isotracer package |
|
36 |
#' a <- aquarium_run |
|
37 |
#' plot(a) |
|
38 |
#' # The calculations below are just given as examples of mathematical |
|
39 |
#' # operations performed on an mcmc.list object, and do not make any sense |
|
40 |
#' # from a modelling point of view. |
|
41 |
#' plot(a[, "upsilon_algae_to_daphnia"] - a[, "lambda_algae"]) |
|
42 |
#' plot(a[, "upsilon_algae_to_daphnia"] + a[, "lambda_algae"]) |
|
43 |
#' plot(a[, "upsilon_algae_to_daphnia"] / a[, "lambda_algae"]) |
|
44 |
#' plot(a[, "upsilon_algae_to_daphnia"] * a[, "lambda_algae"]) |
|
45 |
#' plot(a[, "upsilon_algae_to_daphnia"] - 10) |
|
46 |
#' plot(a[, "upsilon_algae_to_daphnia"] + 10) |
|
47 |
#' plot(a[, "upsilon_algae_to_daphnia"] * 10) |
|
48 |
#' plot(a[, "upsilon_algae_to_daphnia"] / 10) |
|
49 |
#' plot(10 - a[, "upsilon_algae_to_daphnia"]) |
|
50 |
#' plot(10 + a[, "upsilon_algae_to_daphnia"]) |
|
51 |
#' plot(10 * a[, "upsilon_algae_to_daphnia"]) |
|
52 |
#' plot(10 / a[, "upsilon_algae_to_daphnia"]) |
|
53 |
#' } |
|
54 |
#' |
|
55 |
#' @method Ops mcmc.list |
|
56 |
#' |
|
57 |
#' @export |
|
58 | ||
59 |
Ops.mcmc.list = function(e1, e2) { |
|
60 |
# Modified from cdeterman answer from: |
|
61 |
# https://stackoverflow.com/questions/35902360/r-implement-group-generics-ops-to-enable-comparison-of-s3-objects |
|
62 | ! |
op = .Generic[[1]] |
63 | ! |
if ("mcmc.list" %in% class(e1) & "mcmc.list" %in% class(e2)) { |
64 | ! |
areCompatible = function(m1, m2) { |
65 | ! |
COMPATIBLE = TRUE |
66 | ! |
if (coda::nvar(m1) != coda::nvar(m2)) { |
67 | ! |
COMPATIBLE = FALSE |
68 | ! |
} else if (coda::nchain(m1) != coda::nchain(m2)) { |
69 | ! |
COMPATIBLE = FALSE |
70 | ! |
} else if (!all(coda::varnames(m1) == coda::varnames(m1))) { |
71 | ! |
COMPATIBLE = FALSE |
72 | ! |
} else if (!all(coda::mcpar(m1[[1]]) == coda::mcpar(m2[[1]]))) { |
73 | ! |
COMPATIBLE = FALSE |
74 |
} |
|
75 | ! |
return(COMPATIBLE) |
76 |
} |
|
77 | ! |
if (!areCompatible(e1, e2)) { |
78 | ! |
stop("Incompatible inputs") |
79 |
} |
|
80 | ! |
if (coda::nvar(e1) != 1) { |
81 | ! |
stop("Ops implemented only for single-parameter chains") |
82 |
} |
|
83 | ! |
switch(op, |
84 |
"+" = { |
|
85 |
# Addition |
|
86 | ! |
c = e1 |
87 | ! |
for (i in 1:coda::nchain(e1)) { |
88 | ! |
c[[i]] = c[[i]] + e2[[i]] |
89 |
} |
|
90 | ! |
return(as.derived.mcmc.list(c)) |
91 |
}, |
|
92 |
"-" = { |
|
93 |
# Subtraction |
|
94 | ! |
c = e1 |
95 | ! |
for (i in 1:coda::nchain(e1)) { |
96 | ! |
c[[i]] = c[[i]] - e2[[i]] |
97 |
} |
|
98 | ! |
return(as.derived.mcmc.list(c)) |
99 |
}, |
|
100 |
"*" = { |
|
101 |
# Multiplication |
|
102 | ! |
c = e1 |
103 | ! |
for (i in 1:coda::nchain(e1)) { |
104 | ! |
c[[i]] = c[[i]] * e2[[i]] |
105 |
} |
|
106 | ! |
return(as.derived.mcmc.list(c)) |
107 |
}, |
|
108 |
"/" = { |
|
109 |
# Division |
|
110 | ! |
c = e1 |
111 | ! |
for (i in 1:coda::nchain(e1)) { |
112 | ! |
c[[i]] = c[[i]] / e2[[i]] |
113 |
} |
|
114 | ! |
return(as.derived.mcmc.list(c)) |
115 |
}, |
|
116 | ! |
stop("Undefined operation for mcmc.list objects")) |
117 | ! |
} else if ("mcmc.list" %in% class(e1)) { |
118 | ! |
stopifnot("mcmc.list" %in% class(e1) & ! "mcmc.list" %in% class(e2)) |
119 | ! |
stopifnot(is.numeric(e2) & length(e2) == 1) |
120 | ! |
switch(op, |
121 |
"+" = { |
|
122 |
# Addition |
|
123 | ! |
c = e1 |
124 | ! |
for (i in 1:coda::nchain(e1)) { |
125 | ! |
c[[i]] = c[[i]] + e2 |
126 |
} |
|
127 | ! |
return(as.derived.mcmc.list(c)) |
128 |
}, |
|
129 |
"-" = { |
|
130 |
# Subtraction |
|
131 | ! |
c = e1 |
132 | ! |
for (i in 1:coda::nchain(e1)) { |
133 | ! |
c[[i]] = c[[i]] - e2 |
134 |
} |
|
135 | ! |
return(as.derived.mcmc.list(c)) |
136 |
}, |
|
137 |
"*" = { |
|
138 |
# Multiplication |
|
139 | ! |
c = e1 |
140 | ! |
for (i in 1:coda::nchain(e1)) { |
141 | ! |
c[[i]] = c[[i]] * e2 |
142 |
} |
|
143 | ! |
return(as.derived.mcmc.list(c)) |
144 |
}, |
|
145 |
"/" = { |
|
146 |
# Division |
|
147 | ! |
c = e1 |
148 | ! |
for (i in 1:coda::nchain(e1)) { |
149 | ! |
c[[i]] = c[[i]] / e2 |
150 |
} |
|
151 | ! |
return(as.derived.mcmc.list(c)) |
152 |
}, |
|
153 | ! |
stop("Undefined operation for mcmc.list object and numeric") |
154 |
) |
|
155 |
} else { |
|
156 | ! |
stopifnot("mcmc.list" %in% class(e2) & ! "mcmc.list" %in% class(e1)) |
157 | ! |
stopifnot(is.numeric(e1) & length(e1) == 1) |
158 | ! |
switch(op, |
159 |
"+" = { |
|
160 |
# Addition |
|
161 | ! |
c = e2 |
162 | ! |
for (i in 1:coda::nchain(e2)) { |
163 | ! |
c[[i]] = c[[i]] + e1 |
164 |
} |
|
165 | ! |
return(as.derived.mcmc.list(c)) |
166 |
}, |
|
167 |
"-" = { |
|
168 |
# Subtraction |
|
169 | ! |
c = e2 |
170 | ! |
for (i in 1:coda::nchain(e2)) { |
171 | ! |
c[[i]] = e1 - c[[i]] |
172 |
} |
|
173 | ! |
return(as.derived.mcmc.list(c)) |
174 |
}, |
|
175 |
"*" = { |
|
176 |
# Multiplication |
|
177 | ! |
c = e2 |
178 | ! |
for (i in 1:coda::nchain(e2)) { |
179 | ! |
c[[i]] = c[[i]] * e1 |
180 |
} |
|
181 | ! |
return(as.derived.mcmc.list(c)) |
182 |
}, |
|
183 |
"/" = { |
|
184 |
# Division |
|
185 | ! |
c = e2 |
186 | ! |
for (i in 1:coda::nchain(e2)) { |
187 | ! |
c[[i]] = e1 / c[[i]] |
188 |
} |
|
189 | ! |
return(as.derived.mcmc.list(c)) |
190 |
}, |
|
191 | ! |
stop("Undefined operation for mcmc.list object and numeric") |
192 |
) |
|
193 |
} |
|
194 |
} |
|
195 | ||
196 |
### * Math.mcmc.list() |
|
197 | ||
198 |
#' Math generics for mcmc.list objects |
|
199 |
#' |
|
200 |
#' @param x \code{\link[coda]{mcmc.list}} object |
|
201 |
#' @param ... Other arguments passed to corresponding methods |
|
202 |
#' |
|
203 |
#' @return A \code{mcmc.list} object (with the added class |
|
204 |
#' \code{derived.mcmc.list}). |
|
205 |
#' |
|
206 |
#' @method Math mcmc.list |
|
207 |
#' |
|
208 |
#' @export |
|
209 | ||
210 |
Math.mcmc.list = function(x, ...) { |
|
211 | ! |
out = lapply(x, .Generic, ...) |
212 | ! |
out = lapply(out, coda::mcmc) |
213 | ! |
out = coda::mcmc.list(out) |
214 | ! |
out = as.derived.mcmc.list(out) |
215 | ! |
return(out) |
216 |
} |
|
217 | ||
218 |
### * select.mcmc.list() |
|
219 | ||
220 |
#' Select parameters based on their names |
|
221 |
#' |
|
222 |
#' @param .data A \code{coda::mcmc.list} object. |
|
223 |
#' @param ... Strings used to select variables using pattern matching with |
|
224 |
#' \code{grepl}. |
|
225 |
#' |
|
226 |
#' @return An \code{mcmc.list} object, with the same extra class(es) as |
|
227 |
#' \code{.data} (if any). |
|
228 |
#' |
|
229 |
#' @method select mcmc.list |
|
230 |
#' @export |
|
231 | ||
232 |
select.mcmc.list <- function(.data, ...) { |
|
233 | ! |
params <- coda::varnames(.data) |
234 | ! |
patterns <- rlang::ensyms(...) |
235 | ! |
patterns <- purrr::map(patterns, rlang::as_string) |
236 | ! |
matches <- lapply(patterns, function(p) which(grepl(p, params))) |
237 | ! |
matches <- sort(unique(unlist(matches))) |
238 | ! |
if (length(matches) == 0) { return(NULL) } |
239 | ! |
out <- .data[, matches] |
240 | ! |
class(out) <- class(.data) |
241 | ! |
return(out) |
242 |
} |
|
243 | ||
244 | ||
245 |
### * c.mcmc.list() |
|
246 | ||
247 |
#' Combine mcmc.list objects |
|
248 |
#' |
|
249 |
#' @param ... \code{mcmc.list} objects. |
|
250 |
#' |
|
251 |
#' @return A \code{mcmc.list} object. |
|
252 |
#' |
|
253 |
#' @method c mcmc.list |
|
254 |
#' @export |
|
255 | ||
256 |
c.mcmc.list <- function(...) { |
|
257 | ! |
z <- list(...) |
258 | ! |
is_mcmc.list <- sapply(z, function(x) is(x, "mcmc.list")) |
259 | ! |
if (!all(is_mcmc.list)) { |
260 | ! |
stop("Not all arguments are mcmc.list objects.") |
261 |
} |
|
262 | ! |
if (length(z) == 1) { |
263 | ! |
return(z) |
264 |
} |
|
265 |
# Check objects compatibility |
|
266 | ! |
areCompatible <- function(m1, m2) { |
267 | ! |
COMPATIBLE <- TRUE |
268 | ! |
if (coda::nchain(m1) != coda::nchain(m2)) { |
269 | ! |
COMPATIBLE <- FALSE |
270 | ! |
} else if (coda::niter(m1) != coda::niter(m2)) { |
271 | ! |
COMPATIBLE <- FALSE |
272 | ! |
} else if (!all(coda::mcpar(m1[[1]]) == coda::mcpar(m2[[1]]))) { |
273 | ! |
COMPATIBLE <- FALSE |
274 |
} |
|
275 | ! |
return(COMPATIBLE) |
276 |
} |
|
277 | ! |
if (is.null(coda::mcpar(z[[1]]))) { |
278 | ! |
if (is.null(coda::mcpar(z[[1]][[1]]))) { |
279 | ! |
stop("One mcmc.list object has no mcpar attribute.") |
280 |
} |
|
281 | ! |
attr(z[[1]], "mcpar") <- coda::mcpar(z[[1]][[1]]) |
282 |
} |
|
283 | ! |
for (i in 2:length(z)) { |
284 | ! |
if (is.null(coda::mcpar(z[[i]]))) { |
285 | ! |
if (is.null(coda::mcpar(z[[i]][[1]]))) { |
286 | ! |
stop("One mcmc.list object has no mcpar attribute.") |
287 |
} |
|
288 | ! |
attr(z[[i]], "mcpar") <- coda::mcpar(z[[i]][[1]]) |
289 |
} |
|
290 | ! |
compatible <- areCompatible(z[[1]], z[[i]]) |
291 | ! |
if (!compatible) { |
292 | ! |
stop("Not all provided mcmc.list objects are compatible.") |
293 |
} |
|
294 |
} |
|
295 |
# Check that no variable is unnamed |
|
296 | ! |
var_names <- lapply(z, coda::varnames) |
297 | ! |
for (i in seq_along(var_names)) { |
298 | ! |
if (is.null(var_names[[i]])) { |
299 |
# Check that there is only one variable |
|
300 | ! |
if (coda::nvar(z[[i]]) > 1) { |
301 | ! |
stop("One mcmc.list does not have a variable name and contains more than one variable.") |
302 |
} |
|
303 |
# Check that a name was provided |
|
304 | ! |
if (is.null(names(z)) || names(z)[i] == "") { |
305 | ! |
stop("Some mcmc.list have unnamed variables.\n", |
306 | ! |
"You should pass those mcmc.list to c(...) using named arguments.") |
307 |
} |
|
308 |
# Name the variable |
|
309 | ! |
x <- z[[i]] |
310 | ! |
var_name <- names(z)[i] |
311 | ! |
mcpars <- coda::mcpar(x) |
312 | ! |
x <- lapply(x, function(y) { |
313 | ! |
out <- array(as.vector(y), dim = c(length(as.vector(y)), 1)) |
314 | ! |
colnames(out) <- var_name |
315 | ! |
out <- coda::as.mcmc(out) |
316 | ! |
attr(out, "mcpar") <- attr(y, "mcpar") |
317 | ! |
out |
318 |
}) |
|
319 | ! |
attr(x, "mcpar") <- attr(z[[i]], "mcpar") |
320 | ! |
class(x) <- class(z[[i]]) |
321 | ! |
z[[i]] <- x |
322 |
} |
|
323 |
} |
|
324 |
# Check that no variable names is present twice |
|
325 | ! |
var_names <- lapply(z, coda::varnames) |
326 | ! |
if (length(var_names) != length(unique(var_names))) { |
327 | ! |
stop("Some variable names are duplicated.") |
328 |
} |
|
329 |
# Combine the variables |
|
330 | ! |
n_chains <- coda::nchain(z[[1]]) |
331 | ! |
out <- lapply(seq_len(n_chains), function(i) { |
332 | ! |
variables <- lapply(z, function(y) y[[i]]) |
333 | ! |
combined <- coda::as.mcmc(do.call(cbind, variables)) |
334 | ! |
attr(combined, "mcpar") <- attr(z[[1]][[1]], "mcpar") |
335 | ! |
combined |
336 |
}) |
|
337 | ! |
out <- coda::as.mcmc.list(out) |
338 | ! |
attr(out, "mcpar") <- attr(z[[1]], "mcpar") |
339 | ! |
return(as.derived.mcmc.list(out)) |
340 |
} |
|
341 | ||
342 |
### * `[.networkModelStanfit`() |
|
343 | ||
344 |
#' Subset method for \code{networkModelStanfit} objects |
|
345 |
#' |
|
346 |
#' @param x A \code{networkModelStanfit} object. |
|
347 |
#' @param i A vector of iteration indices. |
|
348 |
#' @param j A vector of parameter names or indices. |
|
349 |
#' @param drop Boolean. |
|
350 |
#' |
|
351 |
#' @return A \code{networkModelStanfit} object. |
|
352 |
#' |
|
353 |
#' @method [ networkModelStanfit |
|
354 |
#' |
|
355 |
#' @export |
|
356 | ||
357 |
`[.networkModelStanfit` <- function(x, i, j, drop = TRUE) { |
|
358 | ! |
o <- NextMethod() |
359 | ! |
class(o) <- c("networkModelStanfit", class(o)) |
360 | ! |
return(o) |
361 |
} |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * Ops.topology() |
|
4 | ||
5 |
# Based on |
|
6 |
# ?groupGeneric |
|
7 |
# ?.Generic |
|
8 |
# https://stackoverflow.com/questions/35902360/r-implement-group-generics-ops-to-enable-comparison-of-s3-objects |
|
9 |
# (cdeterman answer in the above) |
|
10 | ||
11 |
#' Ops generics for \code{topology} objects |
|
12 |
#' |
|
13 |
#' @param e1 First operand |
|
14 |
#' @param e2 Second operand |
|
15 |
#' |
|
16 |
#' @return Boolean (or throws an error for unsupported operators). |
|
17 |
#' |
|
18 |
#' @examples |
|
19 |
#' topo(aquarium_mod) == topo(trini_mod) |
|
20 |
#' topo(aquarium_mod) == topo(aquarium_mod) |
|
21 |
#' |
|
22 |
#' @method Ops topology |
|
23 |
#' |
|
24 |
#' @export |
|
25 | ||
26 |
Ops.topology <- function(e1, e2) { |
|
27 | 6x |
op <- .Generic[[1]] |
28 | 6x |
if (!(is(e1, "topology") & is(e2, "topology"))) { |
29 | ! |
stop("Both objects must be topologies!") |
30 |
} |
|
31 | 6x |
switch(op, |
32 |
"==" = { |
|
33 |
# Equality |
|
34 | 5x |
if (ncol(e1) != ncol(e2)) { |
35 | 3x |
return(FALSE) |
36 |
} |
|
37 | 2x |
if (!setequal(colnames(e1), colnames(e2))) { |
38 | ! |
return(FALSE) |
39 |
} |
|
40 | 2x |
e2 <- e2[match(colnames(e2), colnames(e1)), ] |
41 | 2x |
e2 <- e2[, match(rownames(e2), rownames(e1))] |
42 | 2x |
if (!all(unclass(e1) == unclass(e2))) { |
43 | ! |
return(FALSE) |
44 |
} |
|
45 | 2x |
e1_split <- attr(e1, "split") |
46 | 2x |
e2_split <- attr(e2, "split") |
47 | 2x |
split_compatible <- ( |
48 | 2x |
(is.null(e1_split) & is.null(e2_split)) | |
49 | 2x |
(!is.null(e1_split) && !is.null(e2_split) && |
50 | 2x |
all(sort(e1_split) == sort(e2_split))) |
51 |
) |
|
52 | 2x |
if (!split_compatible) { |
53 | 1x |
return(FALSE) |
54 |
} |
|
55 | 1x |
e1_steadyState <- attr(e1, "steadyState") |
56 | 1x |
e2_steadyState <- attr(e2, "steadyState") |
57 | 1x |
steadyState_compatible <- ( |
58 | 1x |
(is.null(e1_steadyState) & is.null(e2_steadyState)) | |
59 | 1x |
(!is.null(e1_steadyState) && !is.null(e2_steadyState) && |
60 | 1x |
all(sort(e1_steadyState) == sort(e2_steadyState))) |
61 |
) |
|
62 | 1x |
if (!steadyState_compatible) { |
63 | ! |
return(FALSE) |
64 |
} |
|
65 | 1x |
return(TRUE) |
66 |
}, |
|
67 |
"!=" = { |
|
68 |
# Difference |
|
69 | 1x |
return(!(e1 == e2)) |
70 |
}, |
|
71 | ! |
stop("Undefined operation for topology objects: `", op, "`")) |
72 |
} |
|
73 | ||
74 |
### * Methods for nice display of topologies |
|
75 | ||
76 |
### ** print.topology() |
|
77 | ||
78 |
#' Pretty printing of a \code{topology} object |
|
79 |
#' |
|
80 |
#' @param x An object of class \code{topology}. |
|
81 |
#' @param help If TRUE, display a short help after the topology object |
|
82 |
#' explaining e.g. the steady state or the split compartment symbols. |
|
83 |
#' @param ... Not used. |
|
84 |
#' |
|
85 |
#' @return Mostly called for its side effect (printing). |
|
86 |
#' |
|
87 |
#' @export |
|
88 | ||
89 |
print.topology <- function(x, help = TRUE, ...) { |
|
90 | ! |
attr(x, "class") <- NULL |
91 | ! |
nComps <- ncol(x) |
92 | ! |
steady <- attr(x, "steadyState") |
93 | ! |
split <- attr(x, "split") |
94 | ! |
merge <- attr(x, "merge") |
95 | ! |
for (s in steady) { |
96 | ! |
i <- which(colnames(x) == s) |
97 | ! |
colnames(x)[i] <- paste0(s, "*") |
98 | ! |
rownames(x)[i] <- paste0(s, "*") |
99 |
} |
|
100 | ! |
for (s in split) { |
101 | ! |
i <- which(colnames(x) == s) |
102 | ! |
colnames(x)[i] <- paste0(s, "<|>") |
103 | ! |
rownames(x)[i] <- paste0(s, "<|>") |
104 |
} |
|
105 | ! |
attr(x, "steadyState") <- NULL |
106 | ! |
attr(x, "split") <- NULL |
107 | ! |
attr(x, "merge") <- NULL |
108 | ! |
cat(paste0("<", nComps, " comps>"), "\n") |
109 | ! |
print(x) |
110 | ! |
if (help) { |
111 | ! |
if (length(steady) > 0) { |
112 | ! |
cat("[ * : steady-state]", "\n") |
113 |
} |
|
114 | ! |
if (length(split) > 0) { |
115 | ! |
cat("[<|>: split]", "\n") |
116 |
} |
|
117 | ! |
if (!is.null(merge)) { |
118 | ! |
for (i in seq_along(merge)) { |
119 | ! |
cat("[", names(merge)[i], " = ", merge[[i]][1], " + ", merge[[i]][2], "]", "\n", |
120 | ! |
sep = "") |
121 |
} |
|
122 |
} |
|
123 |
} |
|
124 | ! |
invisible(x) |
125 |
} |
|
126 | ||
127 |
### * as_tbl_graph() generic and method |
|
128 | ||
129 |
#' Generic for as_tbl_graph() |
|
130 |
#' |
|
131 |
#' Convert a compatible object to a tbl_graph object (from the tidygraph package) |
|
132 |
#' |
|
133 |
#' @param x Object to convert to a tbl_graph. |
|
134 |
#' @param ... Passed to the appropriate method. |
|
135 |
#' |
|
136 |
#' @return A tbl_graph object. |
|
137 |
#' |
|
138 |
#' @export |
|
139 | ||
140 |
as_tbl_graph <- function(x, ...) { |
|
141 | 9x |
UseMethod("as_tbl_graph") |
142 |
} |
|
143 | ||
144 |
#' Convert a network topology to a tbl_graph |
|
145 |
#' |
|
146 |
#' @param x A network topology. |
|
147 |
#' @param ... Not used. |
|
148 |
#' |
|
149 |
#' @return A tbl_graph object. |
|
150 |
#' |
|
151 |
#' @export |
|
152 | ||
153 |
# Note for testing: |
|
154 |
# https://community.rstudio.com/t/how-can-i-make-testthat-think-i-dont-have-a-package-installed/33441/2 |
|
155 | ||
156 |
as_tbl_graph.topology <- function(x, ...) { |
|
157 | 9x |
if (!requireNamespace("tidygraph", quietly = TRUE)) { |
158 | ! |
stop("Package \"tidygraph\" needed for this function to work. Please install it.", |
159 | ! |
call. = FALSE) |
160 |
} |
|
161 | 9x |
if (!requireNamespace("igraph", quietly = TRUE)) { |
162 | ! |
stop("Package \"igraph\" needed for this function to work. Please install it.", |
163 | ! |
call. = FALSE) |
164 |
} |
|
165 | 9x |
graph <- igraph::graph_from_adjacency_matrix(t(x)) |
166 | 9x |
t_graph <- tidygraph::as_tbl_graph(graph) |
167 | 9x |
steady_state <- attr(x, "steadyState") |
168 | 9x |
split <- attr(x, "split") |
169 | 9x |
attributes <- tibble::tibble(name = colnames(x)) |
170 | 9x |
attributes[["steady_state"]] <- attributes[["name"]] %in% steady_state |
171 | 9x |
attributes[["split"]] <- attributes[["name"]] %in% split |
172 | 9x |
t_graph <- dplyr::left_join(t_graph, attributes, by = "name") |
173 | 9x |
return(t_graph) |
174 |
} |