diff --git a/nimbleModel/R/modelBaseClass.R b/nimbleModel/R/modelBaseClass.R index ee85b73..4469acc 100644 --- a/nimbleModel/R/modelBaseClass.R +++ b/nimbleModel/R/modelBaseClass.R @@ -370,7 +370,8 @@ modelBase_nClass <- nClass( }, # `self` arg masks the reference to the object. # TODO: perhaps we should rename the arg `includeSelf`, but that is not back compatible. - getDependencies = function(nodes, self = TRUE, determOnly = FALSE, stochOnly = FALSE, + getDependencies = function(nodes, omit = NULL, self = TRUE, + determOnly = FALSE, stochOnly = FALSE, includeData = TRUE, dataOnly = FALSE, includePredictive = nimble::getNimbleOption('getDependenciesIncludesPredictiveNodes'), predictiveOnly = FALSE, includeRHSonly = FALSE, @@ -378,21 +379,21 @@ modelBase_nClass <- nClass( nodesAsChars = getNimbleModelOption("nodesAsChars"), returnScalarComponents = FALSE, .sort = FALSE) { nimbleModel::getDependencies( - thisModel, nodes, self, + thisModel, nodes, omit, self, determOnly, stochOnly, includeData, dataOnly, includePredictive, predictiveOnly, includeRHSonly, downstream, immediateOnly, nodesAsChars, returnScalarComponents, .sort ) }, - getParents = function(nodes, self = FALSE, + getParents = function(nodes, omit = NULL, self = FALSE, determOnly = FALSE, stochOnly = FALSE, includeData = TRUE, dataOnly = FALSE, includeRHSonly = FALSE, upstream = FALSE, immediateOnly = FALSE, nodesAsChars = getNimbleModelOption("nodesAsChars"), returnScalarComponents = FALSE, .sort = FALSE) { nimbleModel::getParents( - thisModel, nodes, self, + thisModel, nodes, omit, self, determOnly, stochOnly, includeData, dataOnly, includeRHSonly, upstream, immediateOnly, nodesAsChars, returnScalarComponents, .sort @@ -446,13 +447,13 @@ modelBase_nClass <- nClass( topologicallySortNodes = function(nodes) { nimbleModel::expandNodeNames(self, nodes, sort = TRUE, unique = TRUE) }, - getConditionallyIndependentSets = function(nodes, givenNodes, + getConditionallyIndependentSets = function(nodes, givenNodes, omit = NULL, explore = c("both", "down", "up"), unknownAsGiven = TRUE, returnScalarComponents = FALSE, endAsGiven = FALSE, nodesAsChars = getNimbleModelOption("nodesAsChars")) { nimbleModel::getConditionallyIndependentSets( - self, nodes, givenNodes, explore, unknownAsGiven, + self, nodes, givenNodes, omit, explore, unknownAsGiven, returnScalarComponents, endAsGiven, nodesAsChars ) }, diff --git a/nimbleModel/R/modelFunctions.R b/nimbleModel/R/modelFunctions.R index 57b87cf..6c1441e 100644 --- a/nimbleModel/R/modelFunctions.R +++ b/nimbleModel/R/modelFunctions.R @@ -216,7 +216,7 @@ taggedClass <- R6Class( ) ) -getDependencies <- function(model, nodes, +getDependencies <- function(model, nodes, omit = NULL, self = TRUE, determOnly = FALSE, stochOnly = FALSE, includeData = TRUE, dataOnly = FALSE, includePredictive = nimble::getNimbleOption('getDependenciesIncludesPredictiveNodes'), @@ -225,7 +225,7 @@ getDependencies <- function(model, nodes, nodesAsChars = getNimbleModelOption("nodesAsChars"), returnScalarComponents = FALSE, .sort = FALSE) { traverseGraph(model$modelDef$downstreamRules, model$modelDef$declRules, - nodes = nodes, + nodes = nodes, omit = omit, down = TRUE, self = self, determOnly = determOnly, stochOnly = stochOnly, includeData = includeData, dataOnly = dataOnly, includePredictive = includePredictive, @@ -236,14 +236,14 @@ getDependencies <- function(model, nodes, ) } -getParents <- function(model, nodes, +getParents <- function(model, nodes, omit = NULL, self = FALSE, determOnly = FALSE, stochOnly = FALSE, includeData = TRUE, dataOnly = FALSE, includeRHSonly = FALSE, upstream = FALSE, immediateOnly = FALSE, nodesAsChars = getNimbleModelOption("nodesAsChars"), returnScalarComponents = FALSE, .sort = FALSE) { traverseGraph(model$modelDef$upstreamRules, model$modelDef$declRules, - nodes = nodes, + nodes = nodes, omit = omit, down = FALSE, self = self, determOnly = determOnly, stochOnly = stochOnly, includeData = includeData, dataOnly = dataOnly, includePredictive = TRUE, @@ -356,7 +356,7 @@ setdiff_nodes <- function(nodeSet1, nodeSet2) { } #' @export -getConditionallyIndependentSets <- function(model, nodes, givenNodes, +getConditionallyIndependentSets <- function(model, nodes, givenNodes, omit = NULL, explore = c("both", "down", "up"), unknownAsGiven = TRUE, returnScalarComponents = FALSE, endAsGiven = FALSE, @@ -428,10 +428,19 @@ getConditionallyIndependentSets <- function(model, nodes, givenNodes, if (unknownAsGiven) { givenNodes <- c(givenNodes, unknownNodes) } # else nodes <- c(nodes, unknownNodes) - + stochDecl <- sapply(model$modelDef$declInfo, \(declInfo) declInfo$stoch) touched <- lapply(model$modelDef$declInfo[stochDecl], \(declInfo) taggedClass$new(declInfo$declRule)) names(touched) <- sapply(model$modelDef$declInfo[stochDecl], \(declInfo) declInfo$declRule$ID) + + if(!is.null(omit)) { + omit <- model$getNodes(omit, nodesAsChars = FALSE) + givenNodes <- setdiff_nodes(givenNodes, omit) + nodes <- setdiff_nodes(nodes, omit) + tmp <- sapply(omit, \(node) touched[[node$decl$declRule$ID]]$tag(node$getIDs())) + } + + given <- lapply(model$modelDef$declInfo[stochDecl], \(declInfo) taggedClass$new(declInfo$declRule)) names(given) <- names(touched) tmp <- sapply(givenNodes, \(node) given[[node$decl$declRule$ID]]$tag(node$getIDs())) @@ -932,7 +941,11 @@ setupMargNodes <- function(model, paramNodes, randomEffectsNodes, calcNodes, nodes = randomEffectsNodes, givenNodes = givenNodes, unknownAsGiven = TRUE ) - } else if (is.numeric(split)) { # TODO: check this makes sense with inputs being nodeRanges. + } else if (is.numeric(split)) { + # TODO: reconsider this - it relies on user knowing what the `randomEffectsNodes` will be + # even if the user provides them as character strings instead of a list of nodeRanges. + if(length(randomEffectsNodes) != length(split)) + stop("when providing numeric vector for `split`, its length must match that of the `randomEffectsNodes`. This will usually mean that `randomEffectsNodes` should be provided by the user as a list of nodeRanges.") reSets <- split(randomEffectsNodes, split) } else { stop("setupMargNodes: Invalid value for `split`") diff --git a/nimbleModel/R/processModelGraph.R b/nimbleModel/R/processModelGraph.R index 49a01b6..2e4611c 100644 --- a/nimbleModel/R/processModelGraph.R +++ b/nimbleModel/R/processModelGraph.R @@ -237,7 +237,7 @@ setSortIDs <- function(calcRules) { # (`follow = TRUE`) or to stop at immediate parent or child # (`immediateOnly = TRUE`). traverseGraph <- function(streamRules, declRules, - nodes, down, self = TRUE, + nodes, omit = NULL, down, self = TRUE, determOnly = FALSE, stochOnly = FALSE, includeData = TRUE, dataOnly = FALSE, includePredictive = TRUE, predictiveOnly = FALSE, includeRHSonly = FALSE, @@ -253,7 +253,10 @@ traverseGraph <- function(streamRules, declRules, if (inherits(nodes, "varRangeClass")) nodes <- list(nodes) # We use `lapply` on 'nodes' later. - results <- traverseGraphRecurse(streamRules, nodes, down, follow, immediateOnly) + if(!is.null(omit)) + omit <- model$getNodes(omit, nodesAsChars = FALSE) + + results <- traverseGraphRecurse(streamRules, nodes, omit, down, follow, immediateOnly, model) results <- model$getNodes(results, determOnly = determOnly, stochOnly = stochOnly, includeData = includeData, dataOnly = dataOnly, @@ -315,8 +318,13 @@ traverseGraph <- function(streamRules, declRules, return(results) } -traverseGraphRecurse <- function(rules, nodes, down, follow = FALSE, immediateOnly = FALSE, firstPass = TRUE) { +traverseGraphRecurse <- function(rules, nodes, omit = NULL, down, follow = FALSE, immediateOnly = FALSE, model = NULL, firstPass = TRUE) { results <- flatten(lapply(nodes, function(node) applyRules(rules, node))) + if(!is.null(omit)) { + results <- model$getNodes(results, nodesAsChars = FALSE) + results <- setdiff_nodes(results, omit) + ## We presumably don't need to convert back to varRange(s) since the nodeRange has all that information. + } if (immediateOnly) { return(results) } @@ -326,18 +334,18 @@ traverseGraphRecurse <- function(rules, nodes, down, follow = FALSE, immediateOn if (!down && !firstPass && !follow) { # For upward traversal, check current rule to see if continue upwards, but always go up on first pass. # (Because we need to determine stochasticity of the next rule up, not stochasticity of starting rule. - stoch <- sapply(results, function(varRange) varRange$fromStochRule) + stoch <- sapply(results, function(x) if(inherits(x, 'nodeRangeClass')) x$decl$stoch else x$fromStochRule) results <- results[!stoch] # Stop here if upwards involves stochastic rule, excluding the upwards result. } propagators <- results if (!follow && down) { # For downward traversal, stop propagating at stochastic cases, but results included. - stoch <- sapply(propagators, function(varRange) varRange$fromStochRule) + stoch <- sapply(propagators, function(x) if(inherits(x, 'nodeRangeClass')) x$decl$stoch else x$fromStochRule) propagators <- propagators[!stoch] } # Continue traversing. if (length(propagators)) { - results <- c(results, traverseGraphRecurse(rules, propagators, down, follow, firstPass = FALSE)) + results <- c(results, traverseGraphRecurse(rules, propagators, omit, down, follow, immediateOnly, model, firstPass = FALSE)) } else { return(results) } diff --git a/nimbleModel/tests/testthat/test-modelGraph.R b/nimbleModel/tests/testthat/test-modelGraph.R index 2b9f947..d251746 100644 --- a/nimbleModel/tests/testthat/test-modelGraph.R +++ b/nimbleModel/tests/testthat/test-modelGraph.R @@ -2605,3 +2605,56 @@ test_that("handling RHSonly with getParents", { c("mu[2]","y[2]")) }) + +test_that("using omit with getDependencies and getParents", { + setNimbleModelOption('nodesAsChars', TRUE) + + code <- nimbleCode({ + y ~ dnorm(mu,1) + mu <- mu0+1 + mu0 ~ dnorm(0,1) + }) + m <- nimbleModel(code) + expect_identical(m$getDependencies('mu0',omit='mu'), "mu0") + expect_identical(m$getParents('y',omit='mu',self=TRUE), "y") + + code <- nimbleCode({ + z ~ dnorm(y,1) + y ~ dnorm(mu,1) + mu <- mu0+1 + mu0~dnorm(0,1) + }) + m <- nimbleModel(code) + expect_identical(m$getDependencies('mu0',omit='y'), c("mu0", "mu")) + expect_identical(m$getParents('y',omit='mu0'), "mu") + + + code <- nimbleCode({ + w ~ dnorm(y,1) + z ~ dnorm(y+mu,1) + y ~ dnorm(mu+b,1) + mu <- mu0+1 + b ~ dnorm(0,1) + mu0~dnorm(0,1) + }) + m <- nimbleModel(code) + expect_identical(m$getDependencies('mu0',omit='y',downstream=TRUE), + c("mu0","mu","lifted_y_plus_mu","lifted_mu_plus_b","z")) + expect_identical(m$getParents('z',omit='y',upstream=TRUE), + c("lifted_y_plus_mu","mu","mu0")) + + setNimbleModelOption('nodesAsChars', FALSE) +}) + +test_that("getDependencies handles non-node-based query", { + code <- nimbleCode({ + for(i in 1:4) + y[i] ~ dnorm(mu[i],1) + mu[1:4]~dmnorm(z[1:4],pr[1:4,1:4]) + }) + m=nimbleModel(code) + expect_identical(m$getDependencies('mu[2]', self = FALSE, nodesAsChars =TRUE), + "y[2]") # Not all of y is included. + expect_identical(m$getDependencies('mu[2]', nodesAsChars =TRUE), + c("mu[1:4]", "y[2]")) # All of mu is included. +}) diff --git a/nimbleModel/tests/testthat/test-setupMargNodes.R b/nimbleModel/tests/testthat/test-setupMargNodes.R index cdb7d62..94f422b 100644 --- a/nimbleModel/tests/testthat/test-setupMargNodes.R +++ b/nimbleModel/tests/testthat/test-setupMargNodes.R @@ -18,8 +18,7 @@ test_that("getConditionallyIndependentSets works in model with a couple of sets" expect_identical(getConditionallyIndependentSets(m, 'y[2]', explore = "down"), list('y[2]')) expect_identical(getConditionallyIndependentSets(m, 'x[1:2]', explore = "up"), list(c('x[1]'), c('x[2]'))) expect_true(nimble:::testConditionallyIndependentSets(m, getConditionallyIndependentSets(m))) - # expect_identical(getConditionallyIndependentSets(m, omit = 'y[2]'), list(c('x[1]', 'y[1]'), c('x[2]'))) - # expect_identical(getConditionallyIndependentSets(m, omit = 5), list(c('x[1]', 'y[1]'), c('x[2]'))) + expect_identical(getConditionallyIndependentSets(m, omit = 'y[2]'), list(c('x[1]', 'y[1]'), c('x[2]'))) expect_identical(getConditionallyIndependentSets(m, 'x[1]'), list(c('x[1]'))) expect_identical(getConditionallyIndependentSets(m, 'x[1]', unknownAsGiven=FALSE), list(c('x[1]', 'y[1]'))) @@ -240,7 +239,7 @@ test_that("getConditionallyIndependentSets works in model with diamond shape", { }) test_that("getConditionallyIndependentSets works in double-state state-space model", { - # two stae-space chains of latent states with one data set that depends on both + # two state-space chains of latent states with one data set that depends on both mc <- nimbleCode({ x[1] ~ dnorm(0, 1) w[1] ~ dnorm(0, 1) @@ -254,8 +253,8 @@ test_that("getConditionallyIndependentSets works in double-state state-space mod expect_identical(getConditionallyIndependentSets(m), list(c(paste0("x[", 2:4, "]"), paste0("w[", 2:4, "]")))) - # expect_identical(getConditionallyIndependentSets(m, omit = "w[2]"), - # list(c("x[2]", "x[3]", "w[3]", "x[4]", "w[4]"))) + expect_identical(getConditionallyIndependentSets(m, omit = "w[2]"), + list(c("x[2]", "x[3]", "x[4]", "w[3]", "w[4]"))) expect_identical(getConditionallyIndependentSets(m, givenNodes = c("y", "w[3]"), unknownAsGiven=FALSE), list(c("x[2]", "x[3]", "x[4]", "x[1]", "w[2]", "w[4]", "w[1]"))) expect_identical(getConditionallyIndependentSets(m, givenNodes = c("y", "x[3]", "w[3]"), unknownAsGiven=FALSE),