Skip to content

Commit ace7567

Browse files
committed
Further work latent effects and zoning support in projections #121
1 parent b1857b5 commit ace7567

18 files changed

+306
-53
lines changed

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ Suggests:
8484
xgboost
8585
URL: https://iiasa.github.io/ibis.iSDM/
8686
BugReports: https://github.com/iiasa/ibis.iSDM/issues
87-
RoxygenNote: 7.3.1
87+
RoxygenNote: 7.3.2
8888
Config/testthat/edition: 3
8989
Roxygen: list(markdown = TRUE)
9090
Biarch: true

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ibis.iSDM 0.1.4 (current dev branch)
22

33
#### New features
4+
* Support for carnying over latent spatial effects (`add_latent_spatial()`) to `scenario()` projections.
45
* Convenience functions to remove limits and controls `rm_limits()`/`rm_control()` #121
56
* :fire: Enable stars and multi-temporal SpatRaster zones for `scenario()` and `distribution()` #121
67

R/add_latent.R

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,49 @@ methods::setMethod(
110110
}
111111
)
112112

113+
#### Latent for scenarios ####
114+
#' Add latent spatial effect to the model equation
115+
#' @param layer A [`SpatRaster`] layer describing alternative latent effects to be used instead
116+
#' if \code{"reuse_latent"} is set to \code{FALSE}.
117+
#' @param reuse_latent A [`logical`] flag on whether any latent effects found
118+
#' in the fitted model should be reused (Default \code{TRUE}).
119+
#' @rdname add_latent_spatial
120+
methods::setMethod(
121+
"add_latent_spatial",
122+
methods::signature(x = "BiodiversityScenario"),
123+
function(x, layer = NULL, reuse_latent = TRUE, ...) {
124+
assertthat::assert_that(
125+
inherits(x, "BiodiversityScenario"),
126+
is.Raster(layer) || is.null(layer),
127+
is.logical(reuse_latent)
128+
)
129+
130+
# Check existing model if latent factors are set there
131+
if(reuse_latent){
132+
# Try and see if model has been defined?
133+
fit <- x$get_model()
134+
if(!is.Waiver(fit)){
135+
# Latent-factors specified?
136+
check <- fit$has_latent()
137+
assertthat::assert_that(
138+
check,
139+
msg = "Latent factors are to be reused, but could not be found in fitted model?"
140+
)
141+
}
142+
}
143+
144+
# Build latent object
145+
lat <- list("layer" = layer, "reuse_latent" = reuse_latent)
146+
147+
# Make a clone copy of the object
148+
y <- x$clone(deep = TRUE)
149+
150+
# Add to data to the BiodiversityDistribution object
151+
y$set_latent(lat)
152+
}
153+
)
154+
155+
113156
#' Function to remove a latent effect
114157
#'
115158
#' @description This is just a wrapper function for removing specified offsets
@@ -156,3 +199,23 @@ methods::setMethod(
156199
y$rm_latent()
157200
}
158201
)
202+
203+
#' @rdname rm_latent
204+
methods::setMethod(
205+
"rm_latent",
206+
methods::signature(x = "BiodiversityScenario"),
207+
function(x) {
208+
assertthat::assert_that(inherits(x, "BiodiversityScenario") )
209+
# If no offset can be found, just return proto object
210+
if(is.Waiver(x$latentfactors)){ return(x) }
211+
212+
# Messenger
213+
if(getOption('ibis.setupmessages', default = TRUE)) myLog('[Setup]','yellow','Removing latent effects.')
214+
215+
# Make a clone copy of the object
216+
y <- x$clone(deep = TRUE)
217+
218+
# Now remove the offset
219+
y$rm_latent()
220+
}
221+
)

R/add_predictors.R

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ methods::setMethod(
395395
r <- o[[n]]
396396
# If predictor transformation is specified, apply
397397
if(transform != "none") r <- predictor_transform(r, option = transform)
398-
y$predictors <- y$predictors$set_data(n, r)
398+
y$predictors <- y$predictors$set_data(r)
399399
rm(r)
400400
}
401401
}
@@ -504,7 +504,8 @@ methods::setMethod(
504504
if(is.Waiver(x$predictors)){
505505
y <- add_predictors(y, env = layer, transform = 'none',derivates = 'none', priors)
506506
} else {
507-
y$predictors <- y$predictors$set_data('range_distance', layer)
507+
names(layer) <- "range_distance"
508+
y$predictors <- y$predictors$set_data(layer)
508509
if(!is.null(priors)) {
509510
# FIXME: Ideally attempt to match varnames against supplied predictors vis match.arg or similar
510511
assertthat::assert_that( all( priors$varnames() %in% names(layer) ) )
@@ -608,7 +609,8 @@ methods::setMethod(
608609
if(is.Waiver(x$predictors)){
609610
y <- add_predictors(y, env = dis, transform = 'none',derivates = 'none')
610611
} else {
611-
y$predictors <- y$predictors$set_data('range_distance', dis)
612+
names(dis) <- "range_distance"
613+
y$predictors <- y$predictors$set_data(dis)
612614
}
613615
return(y)
614616
}

R/class-biodiversityscenario.R

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ BiodiversityScenario <- R6::R6Class(
2121
#' @field limits A [`sf`] object used to constraint the prediction.
2222
#' @field predictors A predictor object for projection.
2323
#' @field constraints Any constraints set for projection.
24+
#' @field latentfactors A [`list`] on whether latentfactors are used.
2425
#' @field scenarios The resulting [`stars`] objects.
2526
modelobject = new_waiver(),
2627
modelid = new_waiver(),
2728
limits = new_waiver(),
2829
predictors = new_waiver(),
2930
constraints = new_waiver(),
31+
latentfactors = new_waiver(),
3032
scenarios = new_waiver(),
3133

3234
#' @description
@@ -60,6 +62,10 @@ BiodiversityScenario <- R6::R6Class(
6062
# Thresholds
6163
tr <- self$get_threshold()
6264

65+
# Latent factors if set
66+
lat <- self$get_latent()
67+
if(!is.Waiver(lat))
68+
6369
# Any other simulation outputs modules
6470
simmods <- self$get_simulation()
6571

@@ -71,6 +77,7 @@ BiodiversityScenario <- R6::R6Class(
7177
"\n Time period: ", tp,
7278
ifelse(!is.Waiver(cs)||!is.Waiver(tr), "\n --------- ", ""),
7379
ifelse(is.Waiver(cs),"", paste0("\n Constraints: ", text_green(paste(paste0(names(cs),' (',cs,')'),collapse = ', ')) ) ),
80+
ifelse(is.Waiver(lat),"", paste0("\n Latent factors: ", text_green("<Found>")) ),
7481
ifelse(is.Waiver(tr),"", paste0("\n Threshold: ", round(tr[1], 3),' (',names(tr[1]),')') ),
7582
ifelse(is.Waiver(simmods),"", paste0("\n Simulations: ", text_green(paste(paste0(names(simmods),' (',simmods[[1]][[1]],')'),collapse = ', ')) ) ),
7683
"\n --------- ",
@@ -350,6 +357,36 @@ BiodiversityScenario <- R6::R6Class(
350357
return( self )
351358
},
352359

360+
#' @description
361+
#' Adding latent factors to the object.
362+
#' @note
363+
#' The latent factor is usually obtained from the fitted model object, unless
364+
#' re-specified and added here to the list.
365+
#' @param latent A [`list`] containing the data object.
366+
#' @seealso [add_latent_spatial()]
367+
#' @return This object.
368+
set_latent = function(latent){
369+
assertthat::assert_that(is.list(latent))
370+
self$latentfactors <- latent
371+
return(self)
372+
},
373+
374+
#' @description
375+
#' Get latent factors if found in object.
376+
#' @return A [`list`] with the latent settings
377+
get_latent = function(){
378+
if(is.Waiver(self$latentfactors)) return('None')
379+
self$latentfactors
380+
},
381+
382+
#' @description
383+
#' Remove latent factors if found in object.
384+
#' @return This object.
385+
rm_latent = function(){
386+
self$latentfactors <- new_waiver()
387+
return(self)
388+
},
389+
353390
#' @description
354391
#' Plot the predictions made here.
355392
#' @param what A [`character`] describing the layers to be plotted.

R/class-distributionmodel.R

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,19 @@ DistributionModel <- R6::R6Class(
655655
}
656656
},
657657

658+
#' @description
659+
#' Logical indication if the prediction has added latent factors.
660+
#' @return A [`logical`] flag.
661+
has_latent = function(){
662+
# Check for settings here
663+
settings <- self$settings
664+
if(!is.Waiver(settings)){
665+
return(
666+
settings$get('has_latent')
667+
)
668+
}
669+
},
670+
658671
#' @description
659672
#' Has a offset been used?
660673
#' @return A [`logical`] flag.

R/class-predictors.R

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -279,14 +279,20 @@ PredictorDataset <- R6::R6Class(
279279

280280
#' @description
281281
#' Add a new Predictor dataset to this collection
282-
#' @param x [`character`] of the new name to be stored.
283-
#' @param value A new [`SpatRaster`] object.
282+
#' @param value A new [`SpatRaster`] or [`stars`] object.
284283
#' @return This object
285-
set_data = function(x, value){
286-
assertthat::assert_that(assertthat::is.string(x),
287-
is.Raster(value),
288-
is_comparable_raster(self$get_data(), value))
289-
self$data <- suppressWarnings( c(self$get_data(), value) )
284+
set_data = function(value){
285+
assertthat::assert_that(is.Raster(value) || inherits(value, "stars"))
286+
if(is.Raster(self$get_data()) && is.Raster(value)){
287+
assertthat::assert_that(is_comparable_raster(self$get_data(), value))
288+
self$data <- suppressWarnings( c(self$get_data(), value) )
289+
} else if(inherits(self$get_data(), "stars") && is.Raster(value)) {
290+
# Stars assumed
291+
self$data <- st_add_raster(self$get_data(), value)
292+
} else {
293+
# Simply try combining them
294+
self$data <- suppressWarnings( c(self$get_data(), value) )
295+
}
290296
return(self)
291297
},
292298

R/project.R

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,34 @@ methods::setMethod(
139139
new <- interpolate_gaps(new_preds$get_data(),
140140
date_interpolation = date_interpolation)
141141
# Set new data
142-
new_preds$set_data(new)
142+
new_preds <- new_preds$set_data(new)
143+
}
144+
145+
# Check latent factors and add them to future if found
146+
if(!is.Waiver(mod$latentfactors) || fit$has_latent()){
147+
# Get predictor names
148+
pn <- fit$model$predictors_names
149+
ind <- grep("nearestpoint_|spatialtrend_|kde__coordinates", pn,value = TRUE)
150+
if(length(mod$latentfactors)==2){
151+
# Get the layer if found
152+
if(!is.null(mod$latentfactors$layer) && length(ind)>0){
153+
layer <- mod$latentfactors$layer
154+
new_preds <- new_preds$set_data(layer)
155+
} else if(length(ind)>0){
156+
# Get the predictors
157+
sps <- fit$model$predictors_object$get_data() |> terra::subset(ind)
158+
new_preds <- new_preds$set_data(sps)
159+
}
160+
if(getOption('ibis.setupmessages', default = TRUE)) myLog('[Scenario]','green', "Found latent factors and added them to future predictors stack.")
161+
} else {
162+
if(length(ind)>0){
163+
# Get the predictors
164+
sps <- fit$model$predictors_object$get_data() |> terra::subset(ind)
165+
new_preds <- new_preds$set_data(sps)
166+
}
167+
if(getOption('ibis.setupmessages', default = TRUE)) myLog('[Scenario]','green', "Found latent factors and added them to future predictors stack.")
168+
}
169+
try({rm(pn,ind,sps)},silent = TRUE)
143170
}
144171

145172
# Get limits if flagged as present in scenario object
@@ -149,7 +176,8 @@ methods::setMethod(
149176

150177
# Clip new predictors
151178
# This also takes account of nearest time zones
152-
new_preds$crop_data(limits, apply_time = TRUE)
179+
new_preds$crop_data(limits,
180+
apply_time = ifelse(any(limits$time == "No set time"), FALSE, TRUE))
153181

154182
# Filter to first time slot if present for remaining clipping
155183
if(utils::hasName(limits,"time")){

R/scenario.R

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,23 @@ methods::setMethod(
6161
# Convert limits if provided
6262
if(!is.null(limits)){
6363
# Convert to polygon if raster
64-
if(inherits(limits,'SpatRaster')){
64+
if(is.Raster(limits)){
6565
# Remove 0 from ratified raster assuming this is no-data
6666
limits[limits == 0] <- NA
6767
limits <- terra_to_sf(limits)
6868
}
6969
# Ensure that limits has the same projection as background
7070
if(sf::st_crs(limits) != sf::st_crs(fit$model$background)) limits <- sf::st_transform(limits, fit$model$background)
7171
# Ensure that limits is intersecting the background
72-
if(suppressMessages(length( sf::st_intersects(limits, fit$model$background)))==0) { limits <- NULL; warning('Provided limits do not intersect the background!') }
72+
if(suppressWarnings( suppressMessages(length( sf::st_intersects(limits, fit$model$background)))==0)) {
73+
limits <- NULL; warning('Provided limits do not intersect the background!')
74+
}
7375

7476
# Rename geometry just to be sure
75-
limits <- rename_geometry(limits, "geometry")
76-
names(limits)[1] <- "limit" # The first entry is the layer name
77+
if(inherits(limits, 'sf')){
78+
limits <- rename_geometry(limits, "geometry")
79+
names(limits)[1] <- "limit" # The first entry is the layer name
80+
}
7781
}
7882

7983
# Also check if limits are to be reused if found

R/train.R

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,10 @@ methods::setMethod(
314314
pred <- model$predictors_object$get_data(df = FALSE)
315315
new <- fill_rasters(coords_poly, emptyraster(pred))
316316
for(val in names(new)){
317-
model$predictors_object <- model$predictors_object$set_data(val, new[[val]] )
317+
v <- new[[val]]
318+
names(v) <- val
319+
model$predictors_object <- model$predictors_object$set_data(v)
320+
rm(v)
318321
}
319322
rm(pred, new)
320323
} else if(m == "kde") {
@@ -376,7 +379,13 @@ methods::setMethod(
376379

377380
}
378381
}
379-
} else { model[["latent"]] <- new_waiver() }# End of latent factor loop
382+
383+
# Make a flag to the settings
384+
settings$set('has_latent', TRUE)
385+
} else {
386+
model[["latent"]] <- new_waiver()
387+
settings$set('has_latent', FALSE)
388+
}# End of latent factor loop
380389

381390
# Set offset if existing
382391
if(!is.Waiver(x$offset)){
@@ -415,7 +424,7 @@ methods::setMethod(
415424
settings$set("bias_value", bias$bias_value )
416425
# Check that variable is already in the predictors object
417426
if(!(names(bias$layer) %in% model$predictors_names)){
418-
model$predictors_object <- model$predictors_object$set_data(names(bias$layer), bias$layer)
427+
model$predictors_object <- model$predictors_object$set_data(bias$layer)
419428
# Also set predictor names
420429
model[['predictors_names']] <- model$predictors_object$get_names()
421430
model[['predictors']] <- model$predictors_object$get_data(df = TRUE, na.rm = FALSE)
@@ -727,6 +736,8 @@ methods::setMethod(
727736
layer)
728737
)
729738
)
739+
# Save the zones categories for later too!
740+
settings$set("limits_zones_categories", unique(zones$limit))
730741
# Limit zones (using the full layer here!)
731742
zones <- subset(layer, limit %in% unique(zones$limit) )
732743
} else if(x$get_limits()$limits_method %in% c("nt2", "mess")){
@@ -818,13 +829,6 @@ methods::setMethod(
818829
point_in_polygon(poly = model$background, points = model$predictors[,c('x','y')] )[['limit']]
819830
)),model$predictors_names] <- NA # Fill with NA
820831
}
821-
# The same with offset if specified, Note this operation below is computationally quite costly
822-
# MJ: 18/10/22 Removed below as (re)-extraction further in the pipeline makes this step irrelevant
823-
# if(!is.Waiver(x$offset)){
824-
# model$offset[which( is.na(
825-
# point_in_polygon(poly = zones, points = model$offset[,c('x','y')] )[['limit']]
826-
# )), "spatial_offset" ] <- NA # Fill with NA
827-
# }
828832
}
829833
# Reset the zones, but save the created layer
830834
# Note that we are using the original saved layer here again!
@@ -837,6 +841,8 @@ methods::setMethod(
837841
"mcp_buffer" = x$limits$mcp_buffer,
838842
"limits_clip" = x$limits$limits_clip)
839843
settings$set("limits", l)
844+
# Save the zones categories for later too!
845+
settings$set("limits_zones_categories", unique(zones$limit))
840846
x <- x$set_limits(x = l)
841847
rm(zones)
842848
}

0 commit comments

Comments
 (0)