Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,12 @@ Always use `parallel::makeCluster()` (socket clusters). **Never use `mclapply`**
Three built-in options via `load_LRM_database()`:
- `"connectomedb2025"` (default) — bundled `.rda` files, human/mouse/rat/pig, no internet required
- `"fantom5"` — bundled `.rda` files, same four species
- `"omnipath"` — requires `OmnipathR` (Suggests) and internet access
- `"omnipath"` — requires internet access; uses `OmnipathR` (Suggests) when available, with a base-R REST fallback

Custom data frames are also accepted; must have `ligand` and `receptor` columns (configurable via `ligand.col`/`receptor.col`).

**OmniPath resilient loading** (ported from NICHESv1 PR #72 / issue #70): newer OmnipathR (≥ ~3.17, incl. 4.1.0) resolves the `organism` argument via `ncbi_taxid()`, which downloads/coalesces species tables from Ensembl, OMA, and UniProt — when any of those ancillary services is unreachable the call aborts with `"Can't combine ..1 and ..3"` even though the OmniPath interaction server is fine. `.load_omnipath()` therefore delegates to `.niches_fetch_omnipath_ligrec()`, which (1) tries the standard OmnipathR path with its logger muted, (2) falls back to a direct OmniPath REST query (`omnipathdb.org/interactions?datasets=ligrecextra&organisms=<taxid>`) using the already-known NCBI taxon id, bypassing the organism-name lookup, and (3) raises an informative network error only if both fail. Fallback is base-R only (`utils::download.file`/`read.delim`), no new dependencies. All four OmniPath helpers live in `R/load_LRM_database.R`.

### Data ingest

- `extract_NICHESInputs_Seurat()` — supports Seurat V4 and V5; detects assay class automatically; **spatial coordinates must be extracted manually** and added to the returned metadata before calling `create_NICHESObject()`
Expand Down
25 changes: 23 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ release will accompany the methods paper.
ConnectomeDB2025 pairs.
- FANTOM5 bundled as pre-filtered `.rda` objects for human, mouse, rat, and
pig; no internet connection required.
- OmniPath loaded via `import_ligrecextra_interactions()` for human, mouse,
and rat (pig not supported by OmniPath).
- OmniPath loaded for human, mouse, and rat (pig not supported by OmniPath).
Uses `OmnipathR::import_ligrecextra_interactions()` when available, with a
base-R REST fallback for resilience against upstream service outages (see
Bug fixes below).
- User-supplied data frames accepted directly with `ligand.col` and
`receptor.col` arguments for non-standard column names.

Expand Down Expand Up @@ -152,6 +154,25 @@ release will accompany the methods paper.

---

## Bug fixes

- **OmniPath loading no longer fails when ancillary services are down.**
Ported from NICHESv1 (PR #72 / issue #70). Newer OmnipathR (>= ~3.17,
including 4.1.0) resolves the `organism` argument through
`OmnipathR::ncbi_taxid()`, which builds an organism-name table by
downloading and coalescing species lists from Ensembl, OMA, and UniProt.
When one of those services is unreachable (e.g. omabrowser.org returning
HTTP 502), that step aborts with `"Can't combine `..1` and `..3`"` even
though the OmniPath interaction server itself is fine. `load_LRM_database("omnipath")`
now first attempts the standard OmnipathR path (with its console logging
muted) and, on failure, falls back to querying the OmniPath REST API
directly with the known NCBI taxon id, bypassing the organism-name lookup.
If both paths fail, an informative network error is raised. The fallback is
base-R only (`utils::download.file` / `utils::read.delim`) and adds no new
dependencies.

---

## Known limitations

- **`[.NICHESObject` does not propagate attributes.** Subsetting a
Expand Down
161 changes: 160 additions & 1 deletion R/load_LRM_database.R
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,18 @@ load_LRM_database <- function(db = "connectomedb2025",
species, organism
))

raw <- OmnipathR::import_ligrecextra_interactions(organism = organism)
# NOTE (ported from NICHESv1 PR #72 / issue #70): newer OmnipathR (>= ~3.17,
# incl. 4.1.0) resolves the `organism` argument through OmnipathR::ncbi_taxid(),
# which builds an organism-name table by downloading and coalescing species
# lists from Ensembl, OMA and UniProt. When one of those ancillary services is
# unreachable (e.g. omabrowser.org returning HTTP 502), that step aborts with
# "Can't combine `..1` and `..3`" - even though the OmniPath interaction server
# itself is fine. Since we have already mapped the species to its NCBI taxon id
# above, we do not need that translation. The helper below first tries the
# normal OmnipathR path (preserving its caching when the services are healthy)
# and, if that fails, falls back to querying the OmniPath REST API directly with
# the known taxon id, which bypasses the organism-name lookup.
raw <- .niches_fetch_omnipath_ligrec(organism, verbose)

result <- data.frame(
ligand = raw$source_genesymbol,
Expand Down Expand Up @@ -300,3 +311,151 @@ load_LRM_database <- function(db = "connectomedb2025",

result
}


## OMNIPATH RESILIENT FETCH (ported from NICHESv1 PR #72 / issue #70) ----

#' Resiliently fetch OmniPath ligand-receptor interactions
#'
#' Internal helper for \code{.load_omnipath()} that works around a known upstream
#' OmnipathR failure (NICHESv1 issue #70). Newer OmnipathR versions resolve the
#' \code{organism} argument via an organism-name table built from Ensembl, OMA and
#' UniProt downloads; when one of those services is down the call aborts before any
#' interactions are retrieved. This helper first attempts the standard OmnipathR
#' path, and on failure falls back to a direct OmniPath REST query using the
#' already-known NCBI taxon id, which does not require the organism-name lookup.
#'
#' @param organism Integer NCBI taxonomy id (9606 human, 10090 mouse, 10116 rat).
#' @param verbose Print progress/provenance messages.
#'
#' @return A data.frame of ligand-receptor interactions containing at least the
#' \code{source_genesymbol} and \code{target_genesymbol} columns.
#'
#' @noRd
.niches_fetch_omnipath_ligrec <- function(organism, verbose = TRUE) {

# 1) Preferred path: standard OmnipathR call (uses its cache when services are
# up). The organism-name download noise is muted; if it fails we recover.
result <- tryCatch(
.niches_quiet_omnipath(OmnipathR::import_ligrecextra_interactions(organism = organism)),
error = function(e) e
)
if (!inherits(result, "error")) {
if (verbose) {
ver <- tryCatch(as.character(utils::packageVersion("OmnipathR")),
error = function(e) NA_character_)
message(sprintf(
"[load_LRM_database] Loaded OmniPath via OmnipathR %s (retrieved %s): %d interactions.",
ver, Sys.Date(), nrow(result)))
}
return(result)
}

# 2) Fallback: query the OmniPath REST API directly with the known taxon id.
# This avoids OmnipathR's organism-name translation (the step that fails).
fallback <- tryCatch(.niches_omnipath_rest_ligrec(organism), error = function(e) e)
if (!inherits(fallback, "error") && nrow(fallback) > 0L) {
if (verbose) {
ver <- .niches_omnipath_server_version()
message(sprintf(
"[load_LRM_database] Loaded OmniPath via REST fallback (OmniPath server %s, retrieved %s): %d interactions.",
if (is.na(ver)) "REST API" else ver, Sys.Date(), nrow(fallback)))
}
return(fallback)
}

# 3) Both paths failed: raise an informative error (this stays loud on purpose).
stop(
"[load_LRM_database] Unable to load the OmniPath ligand-receptor database.\n",
" OmnipathR error : ", conditionMessage(result), "\n",
" REST fallback : ",
if (inherits(fallback, "error")) conditionMessage(fallback) else "returned no interactions", "\n",
" The OmniPath interaction server (omnipathdb.org) may be unreachable from\n",
" this machine (check network/proxy access to https://omnipathdb.org).\n",
" See NICHESv1 issue #70 for background.",
call. = FALSE
)
}


#' Run an OmnipathR expression with its console logging muted
#'
#' OmnipathR reports failed ancillary downloads (e.g. the OMA/Ensembl species
#' lists) through its own \code{logger}-based console appender, which is not
#' captured by \code{suppressWarnings}/\code{suppressMessages}. Since NICHES
#' handles those failures itself (issue #70), this helper temporarily raises the
#' OmnipathR console log threshold so the expected noise is hidden, restoring the
#' previous level afterwards. Genuine errors still propagate as R conditions.
#'
#' @param expr An expression calling OmnipathR (evaluated lazily inside here).
#'
#' @noRd
.niches_quiet_omnipath <- function(expr) {
old <- getOption("omnipathr.console_loglevel", default = "success")
try(OmnipathR::omnipath_set_console_loglevel("fatal"), silent = TRUE)
on.exit(try(OmnipathR::omnipath_set_console_loglevel(old), silent = TRUE), add = TRUE)
suppressWarnings(suppressMessages(expr))
}


#' Best-effort OmniPath web server version
#'
#' Reads the OmniPath service banner (\code{https://omnipathdb.org/about}) and
#' extracts the server version for provenance reporting. Returns \code{NA} if the
#' banner cannot be read or parsed - version reporting must never block a load.
#'
#' @return Character server version (e.g. "0.1.5"), or \code{NA_character_}.
#'
#' @noRd
.niches_omnipath_server_version <- function() {
tryCatch({
txt <- paste(readLines("https://omnipathdb.org/about", warn = FALSE, n = 3), collapse = " ")
m <- regmatches(txt, regexec("omnipath-server[[:space:]]+([0-9]+(\\.[0-9]+)*)", txt))[[1]]
if (length(m) >= 2 && nzchar(m[2])) m[2] else NA_character_
}, error = function(e) NA_character_)
}


#' Direct OmniPath REST query for ligrecextra interactions
#'
#' Downloads the \code{ligrecextra} ligand-receptor dataset straight from the
#' OmniPath web service for a given NCBI taxon id, bypassing OmnipathR's
#' organism-name translation. The web service performs ortholog translation
#' server-side, so mouse (10090) and rat (10116) return organism-appropriate
#' gene symbols, matching what OmnipathR would otherwise produce.
#'
#' @param organism Integer NCBI taxonomy id.
#'
#' @return A data.frame with (at least) \code{source_genesymbol} and
#' \code{target_genesymbol} columns.
#'
#' @noRd
.niches_omnipath_rest_ligrec <- function(organism) {

url <- sprintf(
"https://omnipathdb.org/interactions?datasets=ligrecextra&organisms=%s&genesymbols=yes",
as.integer(organism)
)

tmp <- tempfile(fileext = ".tsv")
on.exit(unlink(tmp), add = TRUE)
utils::download.file(url, destfile = tmp, quiet = TRUE, mode = "wb")

df <- utils::read.delim(
tmp,
sep = "\t",
header = TRUE,
quote = "",
stringsAsFactors = FALSE,
check.names = FALSE
)

if (!all(c("source_genesymbol", "target_genesymbol") %in% colnames(df)))
stop("OmniPath REST response did not contain the expected gene symbol columns.")

# Drop rows without gene symbols on either side (mirrors usable interactions).
df <- df[nzchar(df$source_genesymbol) & nzchar(df$target_genesymbol) &
!is.na(df$source_genesymbol) & !is.na(df$target_genesymbol), , drop = FALSE]

df
}
Loading
Loading