diff --git a/DESCRIPTION b/DESCRIPTION index d88bda4..c49f679 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -3,7 +3,7 @@ Type: Package Title: Monarch Knowledge Graph Queries Description: R package for easy access, manipulation, and analysis of Monarch KG data Resources. -Version: 1.7 +Version: 2.0 URL: https://github.com/monarch-initiative/monarchr BugReports: https://github.com/monarch-initiative/monarchr/issues Authors@R: diff --git a/NAMESPACE b/NAMESPACE index 525bf6a..d6aa1af 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -102,6 +102,7 @@ importFrom(stringr,str_replace_all) importFrom(stringr,str_wrap) importFrom(tibble,tibble) importFrom(tidygraph,activate) +importFrom(tidygraph,active) importFrom(tidygraph,as_tibble) importFrom(tidygraph,graph_join) importFrom(tidygraph,tbl_graph) diff --git a/NEWS.md b/NEWS.md index 4539312..138a5e4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,13 @@ +# monarchr 2.0 + +## Breaking changes + +* This breaking release drops support for `drop_unused_query_nodes` in `expand()`, which was both brittle and violated the rule that expansion should return a supergraph of the query + +## Bug fixes + +* Fixes a bug in `expand()` for `neo4j_engine()` causing an infinite loop. + # monarchr 1.7 ## New features diff --git a/R/expand.R b/R/expand.R index 719e841..78359e2 100644 --- a/R/expand.R +++ b/R/expand.R @@ -12,7 +12,6 @@ #' @param predicates A vector of relationship predicates (nodes in g are subjects in the KG), indicating which edges to consider in the neighborhood. If NULL (default), all edges are considered. #' @param categories A vector of node categories, indicating which nodes in the larger KG may be fetched. If NULL (default), all nodes in the larger KG are will be fetched. #' @param transitive If TRUE, include transitive closure of the neighborhood. Default is FALSE. Useful in combination with predicates like `biolink:subclass_of`. -#' @param drop_unused_query_nodes If TRUE, remove query nodes from the result, unless they are at the neighborhood boundary, i.e., required for connecting to the result nodes. Default is FALSE. #' @param ... Other parameters passed to methods. #' #' @return A `tbl_kgx()` graph @@ -59,7 +58,6 @@ expand <- function(graph, predicates = NULL, categories = NULL, transitive = FALSE, - drop_unused_query_nodes = FALSE, ...) { UseMethod("expand") diff --git a/R/expand.tbl_kgx.R b/R/expand.tbl_kgx.R index d6a6fa2..167b447 100644 --- a/R/expand.tbl_kgx.R +++ b/R/expand.tbl_kgx.R @@ -4,16 +4,19 @@ #' @importFrom assertthat assert_that expand.tbl_kgx <- function(graph, ...) { # check to see if g has a last_engine attribute + active_tbl <- active(graph) if(!is.null(attr(graph, "last_engine"))) { engine <- attr(graph, "last_engine") if(any(c("monarch_engine", "neo4j_engine") %in% class(engine))) { - return(expand_neo4j_engine(engine, graph, ...)) + res <- expand_neo4j_engine(engine, graph, ...) |> activate(!!rlang::sym(active_tbl)) + return(res) } else if("file_engine" %in% class(engine)) { - return(expand_file_engine(engine, graph, ...)) + res <- expand_file_engine(engine, graph, ...) |> activate(!!rlang::sym(active_tbl)) + return(res) } else { stop("Error: unknown or incompatible engine.") } - return(expand(engine, graph, ...)) + # return(expand(engine, graph, ...)) # this shouldn't be reachable } else { stop("Error: tbl_kgx object does not have a most recent engine.") } diff --git a/R/expand_file_engine.R b/R/expand_file_engine.R index 9ab6624..f85b382 100644 --- a/R/expand_file_engine.R +++ b/R/expand_file_engine.R @@ -5,22 +5,7 @@ transitive_query_internal <- function(engine, g, direction = "out", predicates = NULL, - categories = NULL, - drop_unused_query_nodes = FALSE) { - - # # TODO: this block will never trigger; the check is done in the main function, remove - # if(length(predicates) > 1) { - # # we call recusively on each predicate - # for(predicate in predicates) { - # g2 <- transitive_query_internal(engine, - # g, - # direction = direction, - # predicates = predicate, - # categories = categories, - # drop_unused_query_nodes = TRUE) - # suppressMessages(g <- tidygraph::graph_join(g, g2), classes = "message") # suppress joining info - # } - # } + categories = NULL) { # assert that direction is "out" or "in" assert_that(direction == "out" | direction == "in", msg = "Direction must be 'out' or 'in' when using transitive closure.") @@ -64,16 +49,6 @@ transitive_query_internal <- function(engine, filter(purrr::map_lgl(category, ~ any(.x %in% categories)) | id %in% query_ids) } - # in this logic, unused query nodes (those without any connection) are kept by default - # so we need to remove them if drop_unused_query_nodes is TRUE - # we can identify them in the result as those with no connected edges - if(drop_unused_query_nodes) { - bfs_edges <- bfs_result %>% activate(edges) %>% as_tibble() - bfs_nodes <- c(bfs_edges$object, bfs_edges$subject) - bfs_result <- bfs_result %>% - filter(id %in% bfs_nodes) - } - attr(bfs_result, "last_engine") <- engine return(bfs_result) } @@ -83,8 +58,7 @@ direction_fetch_internal <- function(engine, g, direction = "out", predicates = NULL, - categories = NULL, - drop_unused_query_nodes = FALSE) { + categories = NULL) { engine_graph <- engine$graph @@ -129,15 +103,7 @@ direction_fetch_internal <- function(engine, filter(purrr::map_lgl(category, ~ any(.x %in% categories)) | id %in% node_ids) } - # the logic above drops unused query nodes, but we can keep them if desired - # to do so we drop all the edges in the query graph, and join the result with new_edges - if(!drop_unused_query_nodes) { - query_no_edges <- g %>% - activate(edges) %>% - filter(FALSE) - - suppressMessages(new_edges <- kg_join(query_no_edges, new_edges), classes = "message") # suppress joining info - } + suppressMessages(new_edges <- kg_join(g, new_edges), classes = "message") # suppress joining info return(new_edges) } @@ -152,8 +118,7 @@ expand_file_engine <- function(engine, direction = "both", predicates = NULL, categories = NULL, - transitive = FALSE, - drop_unused_query_nodes = FALSE) { + transitive = FALSE) { assert_that(is.tbl_graph(graph)) assert_that(direction %in% c("in", "out", "both")) @@ -167,14 +132,14 @@ expand_file_engine <- function(engine, stop("Transitive closure requires exactly one specified predicate.") } else if(transitive) { - new_edges <- transitive_query_internal(engine, graph, direction, predicates, categories, drop_unused_query_nodes) + new_edges <- transitive_query_internal(engine, graph, direction, predicates, categories) } else { if(direction == "out" || direction == "in") { - new_edges <- direction_fetch_internal(engine, graph, direction, predicates, categories, drop_unused_query_nodes) + new_edges <- direction_fetch_internal(engine, graph, direction, predicates, categories) } else if(direction == "both") { - new_out_edges <- direction_fetch_internal(engine, graph, "out", predicates, categories, drop_unused_query_nodes) - new_in_edges <- direction_fetch_internal(engine, graph, "in", predicates, categories, drop_unused_query_nodes) + new_out_edges <- direction_fetch_internal(engine, graph, "out", predicates, categories) + new_in_edges <- direction_fetch_internal(engine, graph, "in", predicates, categories) suppressMessages(new_edges <- kg_join(new_out_edges, new_in_edges), classes = "message") # suppress joining info } } diff --git a/R/expand_n.R b/R/expand_n.R index 54dbf9d..cea8378 100644 --- a/R/expand_n.R +++ b/R/expand_n.R @@ -11,7 +11,8 @@ #' @inheritParams expand #' @param transitive NULL (not used in this function). #' -#' @return A `tbl_kgx()` graph +#' @return +#' A `tbl_kgx()` graph #' @export #' @examples #' ## Using example KGX file packaged with monarchr @@ -30,7 +31,6 @@ expand_n <- function(graph, predicates = NULL, categories = NULL, transitive = NULL, - drop_unused_query_nodes = FALSE, n=1, ...) { ## Check args @@ -63,7 +63,6 @@ expand_n <- function(graph, predicates = check_len(predicates,n,i), categories = check_len(categories,n,i), transitive = FALSE, - drop_unused_query_nodes = check_len(drop_unused_query_nodes,n,i), ...) if(return_each) graph_list[[paste0("iteration",i)]] <- graph message(paste( diff --git a/R/expand_neo4j_engine.R b/R/expand_neo4j_engine.R index 879fb2b..86985f2 100644 --- a/R/expand_neo4j_engine.R +++ b/R/expand_neo4j_engine.R @@ -7,7 +7,6 @@ expand_neo4j_engine <- function(engine, predicates = NULL, categories = NULL, transitive = FALSE, - drop_unused_query_nodes = FALSE, page_size = 1000, limit = NULL) { ## Sanity checks @@ -184,11 +183,7 @@ expand_neo4j_engine <- function(engine, tidygraph::activate(nodes) %>% mutate(pcategory = normalize_categories(category, prefs$category_priority)) - # if drop_unused_query_nodes is FALSE, we'll keep them by - # joining the result with the original graph - if(!drop_unused_query_nodes) { - suppressMessages(result_cumulative <- kg_join(graph, result_cumulative), classes = "message") # suppress joining info - } + suppressMessages(result_cumulative <- kg_join(graph, result_cumulative), classes = "message") # suppress joining info attr(result_cumulative, "last_engine") <- engine return(result_cumulative) diff --git a/R/fetch_nodes.neo4j_engine.R b/R/fetch_nodes.neo4j_engine.R index c949ae0..8d219a5 100644 --- a/R/fetch_nodes.neo4j_engine.R +++ b/R/fetch_nodes.neo4j_engine.R @@ -63,7 +63,7 @@ fetch_nodes.neo4j_engine <- function(engine, ..., query_ids = NULL, page_size = query <- "MATCH (n) WHERE n.id IN $id" params <- list(id = query_ids) } else { - query <- paste0("MATCH (n) WHERE ", generate_cypher_conditionals(...)) + query <- paste0("MATCH (n) WHERE (", generate_cypher_conditionals(...), ")") params <- list() } @@ -112,6 +112,7 @@ fetch_nodes.neo4j_engine <- function(engine, ..., query_ids = NULL, page_size = if(last_result_size > 0) { total_nodes_fetched <- total_nodes_fetched + last_result_size last_max_node_id <- max(nodes(result)$id) + suppressMessages(result_cumulative <- graph_join(result_cumulative, result), class = "message") message(paste("Fetching; fetched", total_nodes_fetched, "of", total_results)) } diff --git a/R/graph_centrality.R b/R/graph_centrality.R index 51aac1a..5d294cb 100644 --- a/R/graph_centrality.R +++ b/R/graph_centrality.R @@ -10,6 +10,8 @@ #' @inheritParams nodes #' @returns Graph object with centrality added as a new node attribute. #' @export +#' @importFrom tidygraph active +#' @importFrom tidygraph activate #' @examples #' filename <- system.file("extdata", "eds_marfan_kg.tar.gz", package = "monarchr") #' g <- file_engine(filename) |> @@ -23,9 +25,11 @@ graph_centrality <- function(graph, fun=igraph::harmonic_centrality, col="centrality", ...){ + active_tbl <- active(graph) message("Computing node centrality.") graph <- graph|> activate(nodes)|> - dplyr::mutate(!!col:=fun(graph, ...)) + dplyr::mutate(!!col:=fun(graph, ...)) |> + activate(!!rlang::sym(active_tbl)) return(graph) } diff --git a/R/graph_semsim.R b/R/graph_semsim.R index 9768520..30113ab 100644 --- a/R/graph_semsim.R +++ b/R/graph_semsim.R @@ -10,6 +10,8 @@ #' @param sparse Return a sparse matrix instead of a dense matrix. #' @param ... Additional arguments passed to the similarity function #' (\code{fun}). +#' @import tidygraph +#' @import dplyr #' @inheritParams nodes #' @returns Graph object with similarity added as a new edge attribute. #' @export @@ -29,6 +31,7 @@ graph_semsim <- function(graph, sparse=TRUE, return_matrix=FALSE, ...){ + active_tbl <- active(graph) from <- to <- NULL; message("Computing pairwise node similarity.") X <- fun(graph, ...) @@ -41,6 +44,8 @@ graph_semsim <- function(graph, graph <- graph|> activate(edges)|> - dplyr::mutate(!!col:=purrr::map2_dbl(from, to, ~ X[.y, .x])) + dplyr::mutate(!!col:=purrr::map2_dbl(from, to, ~ X[.y, .x])) |> + activate(!!rlang::sym(active_tbl)) + return(graph) } diff --git a/R/kg_edge_weights.R b/R/kg_edge_weights.R index 7877146..6572ac5 100644 --- a/R/kg_edge_weights.R +++ b/R/kg_edge_weights.R @@ -9,6 +9,7 @@ #' @param encodings A list of named lists of encoding values for #' different edge attributes. #' @inheritParams nodes +#' @import tidygraph #' @import dplyr #' @export #' @examples @@ -25,6 +26,7 @@ kg_edge_weights <- function(graph, encodings=monarch_edge_weight_encodings(), fun=function(x){rowSums(x, na.rm = TRUE)} ){ + active_tbl <- active(graph) encoded_cols <- c() for(key in names(encodings)){ nm_encoded <- paste0(key,"_encoded") @@ -65,7 +67,8 @@ kg_edge_weights <- function(graph, mutate( across(all_of(encoded_cols), ~(min(.x, na.rm = TRUE)) / (max(.x, na.rm = TRUE)) - )) + )) |> + activate(!!rlang::sym(active_tbl)) } igraph::E(graph)$weight <- fun(edges(graph)[,unique(encoded_cols)]) return(graph) diff --git a/R/transitive_closure.R b/R/transitive_closure.R index b19ac86..1226fe1 100644 --- a/R/transitive_closure.R +++ b/R/transitive_closure.R @@ -23,6 +23,11 @@ transitive_closure <- function(g, predicate = "biolink:subclass_of") { if(length(predicate) != 1) { stop("Error: predicate parameter of transitive_closure() must be length 1.") } + # if there are no edges to close, return the input + p <- predicate + if(nrow(edges(g) |> filter(predicate == p)) == 0) {return(g)} + + active_tbl <- active(g) with_downstream <- g |> activate(nodes) |> @@ -56,7 +61,8 @@ transitive_closure <- function(g, predicate = "biolink:subclass_of") { res <- g |> tidygraph::bind_edges(new_edges) |> activate(edges) |> - select(-edge_key) + select(-edge_key) |> + activate(!!rlang::sym(active_tbl)) res } diff --git a/R/transitive_reduction.R b/R/transitive_reduction.R index 9a15df2..d0fede0 100644 --- a/R/transitive_reduction.R +++ b/R/transitive_reduction.R @@ -32,6 +32,7 @@ #' @export transitive_reduction <- function(g, predicate = "biolink:subclass_of") { # first we make a copy + active_tbl <- active(g) g2 <- g # in the original, remove the predicate edges @@ -56,6 +57,7 @@ transitive_reduction <- function(g, predicate = "biolink:subclass_of") { # merge the original w g_reduced, adding back just the reduction edges suppressMessages(g <- kg_join(g, g_reduced), classes = "message") # suppress joining info + g <- g |> activate(!!rlang::sym(active_tbl)) return(g) } diff --git a/man/expand.Rd b/man/expand.Rd index b095b3f..f1a8ab4 100644 --- a/man/expand.Rd +++ b/man/expand.Rd @@ -11,7 +11,6 @@ expand( predicates = NULL, categories = NULL, transitive = FALSE, - drop_unused_query_nodes = FALSE, ... ) } @@ -28,8 +27,6 @@ expand( \item{transitive}{If TRUE, include transitive closure of the neighborhood. Default is FALSE. Useful in combination with predicates like \code{biolink:subclass_of}.} -\item{drop_unused_query_nodes}{If TRUE, remove query nodes from the result, unless they are at the neighborhood boundary, i.e., required for connecting to the result nodes. Default is FALSE.} - \item{...}{Other parameters passed to methods.} } \value{ diff --git a/man/expand_n.Rd b/man/expand_n.Rd index f56430b..39e7893 100644 --- a/man/expand_n.Rd +++ b/man/expand_n.Rd @@ -11,7 +11,6 @@ expand_n( predicates = NULL, categories = NULL, transitive = NULL, - drop_unused_query_nodes = FALSE, n = 1, ... ) @@ -30,8 +29,6 @@ If FALSE, return the final graph with all expanded edges.} \item{transitive}{NULL (not used in this function).} -\item{drop_unused_query_nodes}{If TRUE, remove query nodes from the result, unless they are at the neighborhood boundary, i.e., required for connecting to the result nodes. Default is FALSE.} - \item{n}{Number of expansion iterations to run.} \item{...}{Other parameters passed to methods.} diff --git a/tests/testthat/test-expand.file_engine.R b/tests/testthat/test-expand.file_engine.R index 1fbc532..af29c88 100644 --- a/tests/testthat/test-expand.file_engine.R +++ b/tests/testthat/test-expand.file_engine.R @@ -29,15 +29,6 @@ test_that("expand file_engine works with transitive", { expect_equal(nrow(edges_df), 20) expect_equal(sum(edges_df$predicate == "biolink:subclass_of"), 20) - ##### Check basic OUT transitive with drop_unused_query_nodes - g <- fetch_nodes(e, query_ids = query_ids) %>% expand(predicate = "biolink:subclass_of", transitive = TRUE, direction = "out", drop_unused_query_nodes = TRUE) - nodes_df <- g %>% activate(nodes) %>% as_tibble() - expect_equal(nrow(nodes_df), 20) - expect_equal(sum(nodes_df$pcategory == "biolink:Disease"), 14) - - edges_df <- g %>% activate(edges) %>% as_tibble() - expect_equal(nrow(edges_df), 26) - ##### Check basic IN transitive query_ids = c("MONDO:0007525", "MONDO:0007524") @@ -49,17 +40,6 @@ test_that("expand file_engine works with transitive", { edges_df <- g %>% activate(edges) %>% as_tibble() expect_equal(nrow(edges_df), 2) expect_equal(sum(edges_df$predicate == "biolink:subclass_of"), 2) - - ##### Check basic IN transitive with drop_unused_query_nodes - g <- fetch_nodes(e, query_ids = query_ids) %>% expand(predicate = "biolink:subclass_of", transitive = TRUE, direction = "in", drop_unused_query_nodes = TRUE) - nodes_df <- g %>% activate(nodes) %>% as_tibble() - expect_equal(nrow(nodes_df), 3) - expect_equal(sum(nodes_df$pcategory == "biolink:Disease"), 3) - - # still 2 edges - edges_df <- g %>% activate(edges) %>% as_tibble() - expect_equal(nrow(edges_df), 2) - expect_equal(sum(edges_df$predicate == "biolink:subclass_of"), 2) }) test_that("fetch_Edges file_engine works", { @@ -91,15 +71,6 @@ test_that("fetch_Edges file_engine works", { edges_df <- g %>% activate(edges) %>% as_tibble() expect_equal(nrow(edges_df), 8) - ##### Check drop_unused_query_nodes with IN - g <- fetch_nodes(e, query_ids = query_ids) %>% expand(direction = "in", drop_unused_query_nodes = TRUE) - nodes_df <- g %>% activate(nodes) %>% as_tibble() - expect_equal(nrow(nodes_df), 9) - expect_equal(sum(nodes_df$pcategory == "biolink:Disease"), 4) - - edges_df <- g %>% activate(edges) %>% as_tibble() - expect_equal(nrow(edges_df), 8) - #### Check basic BOTH g <- fetch_nodes(e, query_ids = query_ids) %>% expand(direction = "both") nodes_df <- g %>% activate(nodes) %>% as_tibble() @@ -112,18 +83,6 @@ test_that("fetch_Edges file_engine works", { expect_equal(nrow(edges_df), 190) expect_equal(sum(edges_df$predicate == "biolink:subclass_of"), 6) - #### Check drop_unused_query_nodes with BOTH - g <- fetch_nodes(e, query_ids = query_ids) %>% expand(direction = "both", drop_unused_query_nodes = TRUE) - nodes_df <- g %>% activate(nodes) %>% as_tibble() - expect_equal(nrow(nodes_df), 169) - expect_equal(sum(nodes_df$pcategory == "biolink:Disease"), 8) - expect_equal(sum(nodes_df$pcategory == "biolink:PhenotypicFeature"), 156) - expect_equal(sum(nodes_df$pcategory == "biolink:Gene"), 3) - - edges_df <- g %>% activate(edges) %>% as_tibble() - expect_equal(nrow(edges_df), 190) - expect_equal(sum(edges_df$predicate == "biolink:subclass_of"), 6) - # Check OUT with has_phenotype g <- fetch_nodes(e, query_ids = query_ids) %>% expand(predicate = "biolink:has_phenotype", direction = "out") nodes_df <- g %>% activate(nodes) %>% as_tibble() @@ -133,20 +92,6 @@ test_that("fetch_Edges file_engine works", { edges_df <- g %>% activate(edges) %>% as_tibble() expect_equal(nrow(edges_df), 175) - - ##### Check OUT with has_phenotype and drop_unused_query_nodes - g <- fetch_nodes(e, query_ids = query_ids) %>% expand(predicate = "biolink:has_phenotype", direction = "out", drop_unused_query_nodes = TRUE) - # this result should have 4 nodes, 2 biolink:Disease, 1 bio:PhenotypicFeature, 1 bio:Gene - nodes_df <- g %>% activate(nodes) %>% as_tibble() - expect_equal(nrow(nodes_df), 159) - expect_equal(sum(nodes_df$pcategory == "biolink:Disease"), 3) - expect_equal(sum(nodes_df$pcategory == "biolink:PhenotypicFeature"), 156) - - # it should have 3 edges, all related_to - edges_df <- g %>% activate(edges) %>% as_tibble() - expect_equal(nrow(edges_df), 175) - expect_equal(sum(edges_df$predicate == "biolink:has_phenotype"), 175) - ##### Check OUT with categories biolink:Disease g <- fetch_nodes(e, query_ids = query_ids) %>% expand(categories = "biolink:Disease", direction = "out") # this result should have 6 nodes, all biolink:Disease @@ -169,16 +114,6 @@ test_that("fetch_Edges file_engine works", { expect_equal(nrow(edges_df), 2) expect_equal(sum(edges_df$predicate == "biolink:subclass_of"), 2) - ##### Check IN with categories biolink:Disease and drop_unused_query_nodes - g <- fetch_nodes(e, query_ids = query_ids) %>% expand(categories = "biolink:Disease", direction = "in", drop_unused_query_nodes = TRUE) - nodes_df <- g %>% activate(nodes) %>% as_tibble() - expect_equal(nrow(nodes_df), 4) - expect_equal(sum(nodes_df$pcategory == "biolink:Disease"), 4) - - edges_df <- g %>% activate(edges) %>% as_tibble() - expect_equal(nrow(edges_df), 2) - expect_equal(sum(edges_df$predicate == "biolink:subclass_of"), 2) - ##### Check BOTH with categories biolink:Disease g <- fetch_nodes(e, query_ids = query_ids) %>% expand(categories = "biolink:Disease", direction = "both") # this result should have 8 nodes, all biolink:Disease @@ -199,14 +134,4 @@ test_that("fetch_Edges file_engine works", { edges_df <- g %>% activate(edges) %>% as_tibble() expect_equal(nrow(edges_df), 3) - - ##### Check OUT with predicates biolink:has_mode_of_inheritance and categories biolink:PhenotypicFeature with drop_unused_query_nodes - g <- fetch_nodes(e, query_ids = c(query_ids, "HGNC:2201")) %>% expand(categories = "biolink:PhenotypicFeature", predicates = "biolink:has_mode_of_inheritance", direction = "out", drop_unused_query_nodes = TRUE) - nodes_df <- g %>% activate(nodes) %>% as_tibble() - expect_equal(nrow(nodes_df), 4) - expect_equal(sum(nodes_df$pcategory == "biolink:Disease"), 3) - - edges_df <- g %>% activate(edges) %>% as_tibble() - expect_equal(nrow(edges_df), 3) - expect_equal(sum(edges_df$predicate == "biolink:has_mode_of_inheritance"), 3) }) diff --git a/vignettes/monarchr.Rmd b/vignettes/monarchr.Rmd index 55e95f8..8c23e06 100644 --- a/vignettes/monarchr.Rmd +++ b/vignettes/monarchr.Rmd @@ -194,8 +194,6 @@ We can limit the expansion based on several factors, which are combined in an "a - `predicates`: a character vector of edge predicates to fetch (all if unspecified) - `categories`: a character vector of node category labels to fetch in the connected neighborhood (all if unspecified) -The `drop_unused_query_nodes` parameter (which defaults to `FALSE`) keeps only the nodes that are part of the recently-expanded neighborhood. For example, given a query graph with many diseases, if we try to fetch neighboring phenotypes, setting this to `TRUE` removes those disease nodes without any phenotypes. - Lastly, the `transitive` parameter (which defaults to `FALSE`) can be used to fetch hierarchical, transitive relationships like `biolink:subclass_of`. When using `transitive = TRUE`, direction must be `"in"` or `"out"`, and `predicates` must be a single entry. The first example above illustrated this; here we explore the super-types and sub-types of Niemann-Pick, by first fetching all of its subtypes, and then considering the collective ancestry of those nodes in the KG. Not all ancestors are diseases (e.g. `biolink:Entity`), so we restrict the result set of the second expansion. ```{r}