Skip to content
Open
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
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ S3method(c,cidlist)
S3method(print,cidlist)
export("%>%")
export(abbreviate_datasets)
export(cf_add_meta)
export(cf_cosine_plot)
export(cf_datasets)
export(cf_ids)
Expand Down
4 changes: 2 additions & 2 deletions R/datasets.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,10 @@ match_datasets <- function(ds) {
if(length(missing_ds)>0) {
# Try approximate matching for each missing dataset
suggestions <- vapply(missing_ds, function(m) {
distances <- adist(m, dss, ignore.case = TRUE)
distances <- utils::adist(m, dss, ignore.case = TRUE, partial=TRUE)
closest_idx <- which.min(distances)
min_dist <- distances[closest_idx]
if (min_dist <= 3) {
if (min_dist <= 2) {
paste0("Did you mean '", dss[closest_idx], "'?")
} else {
""
Expand Down
107 changes: 107 additions & 0 deletions R/meta.R
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,113 @@ cf_meta <- function(ids, bind.rows=TRUE, integer64=FALSE, keep.all=FALSE,
res
}


#' Add metadata to a dataframe containing neuron keys
#'
#' @description Enriches a dataframe with metadata for neurons identified by
#' key columns. This is useful for adding metadata to partner data or other
#' results that contain neuron keys.
#'
#' @param x A data.frame containing one or more columns with keys
#' @param keycol Character vector of column names containing keys.
#' Default \code{"key"}. When multiple columns specified, metadata is joined
#' for each with corresponding suffixes.
#' @param suffix Character vector of suffixes for added columns. Must match
#' length of keycol. Default generates \code{""} for single keycol, or
#' \code{".1"}, \code{".2"}, etc. for multiple. Use \code{""} for no suffix.
#' @param cols Character vector of column names to add from metadata.
#' Default \code{NULL} adds all columns. Warns if any specified columns
#' are not found in metadata. The \code{key} column is always kept for joining.
#' @param ... Additional arguments passed to \code{\link{cf_meta}}
#'
#' @return data.frame with additional metadata columns
#' @export
#'
#' @examples
#' \dontrun{
#' # Add metadata to partner column only
#' partners <- cf_partners(cf_ids(hemibrain='DA2_lPN'), threshold=10, details=FALSE)

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example uses details=FALSE, but cf_partners() now defines details as a character option (partner/query/both/neither). As written, this example will error; update it to a valid value (e.g. details='neither') or remove the argument if the default is intended.

Suggested change
#' partners <- cf_partners(cf_ids(hemibrain='DA2_lPN'), threshold=10, details=FALSE)
#' partners <- cf_partners(cf_ids(hemibrain='DA2_lPN'), threshold=10, details='neither')

Copilot uses AI. Check for mistakes.
#' partners_meta <- cf_add_meta(partners, keycol="post_key")
#'
#' # Add metadata to both pre and post
#' partners_both <- cf_add_meta(partners,
#' keycol = c("pre_key", "post_key"),
#' suffix = c(".pre", ".post"))
#'
#' # Add only type and side columns
#' partners_minimal <- cf_add_meta(partners, keycol="post_key",
#' cols = c("type", "side"))
#' }
cf_add_meta <- function(x, keycol = "key", suffix = NULL, cols = NULL, ...) {
# Validate inputs
if (!is.data.frame(x) || nrow(x) == 0) {
return(x)
}

missing_cols <- setdiff(keycol, names(x))
if (length(missing_cols))
stop("keycol column(s) not found in x: ", paste(missing_cols, collapse = ", "),
"\nHint: use keys(x) to add a '", paste(missing_cols, collapse = "/"),
"' column, or set keycol= to identify the correct key column.")

# Set default suffixes
if (is.null(suffix)) {
suffix <- if (length(keycol) == 1) "" else paste0(".", seq_along(keycol))
}
if (length(suffix) != length(keycol))
stop("Length of suffix must match length of keycol")

# Extract unique keys and fetch metadata once
all_keys <- unique(unlist(x[keycol], use.names = FALSE))
all_keys <- all_keys[!is.na(all_keys) & nzchar(all_keys)]

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all_keys is filtered with nzchar(all_keys), but unlist(x[keycol]) can yield non-character (e.g. factor/integer64) depending on the input data.frame. nzchar() errors on non-character vectors. Coerce to character before nzchar (or use a type-agnostic empty check) to make cf_add_meta robust to common data.frame column types.

Suggested change
all_keys <- all_keys[!is.na(all_keys) & nzchar(all_keys)]
all_keys_chr <- as.character(all_keys)
all_keys <- all_keys_chr[!is.na(all_keys_chr) & nzchar(all_keys_chr)]

Copilot uses AI. Check for mistakes.

if (length(all_keys) == 0) {
warning("No valid keys found in specified columns")
return(x)
}

meta <- cf_meta(all_keys, ...)

if (is.null(meta) || nrow(meta) == 0) {
warning("No metadata found for provided keys")
return(x)
}


# Remove columns that would conflict or are typically already present
meta_cols_to_drop <- c("id", "dataset", "tissue", "sex")
meta <- meta[, setdiff(names(meta), meta_cols_to_drop), drop = FALSE]

# Subset columns if requested (character vector)
if (!is.null(cols)) {
if (!is.character(cols)) {
stop("cols must be a character vector of column names")
}
missing <- setdiff(cols, names(meta))
if (length(missing) > 0) {
warning("Column(s) not found in metadata: ",
paste(missing, collapse = ", "))
}
keep_cols <- c("key", intersect(cols, names(meta)))
meta <- meta[, keep_cols, drop = FALSE]
}

# Join for each keycol
for (i in seq_along(keycol)) {
kc <- keycol[i]
sf <- suffix[i]

# Rename key column to match keycol, add suffix to other columns, then join
x <- meta %>%
dplyr::rename(!!kc := key) %>%
dplyr::rename_with(~paste0(.x, sf), .cols = -dplyr::all_of(kc)) %>%
dplyr::left_join(x, ., by = kc)
}

x
}


# Shared helper for fanc and banc metadata
fancorbanc_meta <- function(table, ids=NULL, ...) {
ol_classes=c("centrifugal", "distal medulla", "distal medulla dorsal rim area",
Expand Down
111 changes: 40 additions & 71 deletions R/partners.R
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@
#' @param partners Whether to return inputs or outputs
#' @param MoreArgs Additional arguments in the form of a hierarchical list
#' (expert use; see details and examples).
#' @param details Which neurons to enrich with metadata. Options:
#' \itemize{
#' \item \code{"partner"} (default): Add metadata for partner neurons only
#' \item \code{"query"}: Add metadata for query neurons only
#' \item \code{"both"}: Add metadata for both with \code{.pre}/\code{.post} suffixes
#' \item \code{"neither"}: No metadata, return minimal columns for speed
#' }
#' Metadata can also be added later via \code{\link{cf_add_meta}}.
#'
#' @inheritParams cf_meta
#' @return A data.frame or a named list (when \code{bind.rows=FALSE})
Expand All @@ -38,9 +46,11 @@
#' }
cf_partners <- function(ids, threshold=1L, partners=c("inputs", "outputs"),
bind.rows=TRUE, MoreArgs=list(), keep.all=FALSE,
details=c("partner", "query", "both", "neither"),
use_superclass=getOption("coconatfly.use_superclass", FALSE),
harmonise_class=getOption("coconatfly.harmonise_class", FALSE)) {
partners=match.arg(partners)
details=match.arg(details)
threshold <- checkmate::assert_integerish(
threshold, lower=0L,len = 1, null.ok = F, all.missing = F)

Expand All @@ -51,7 +61,7 @@ cf_partners <- function(ids, threshold=1L, partners=c("inputs", "outputs"),
if(is.data.frame(ids)) {
ss=split(ids$id, ids$dataset)
res=cf_partners(ss, threshold = threshold, partners = partners,
bind.rows = bind.rows, MoreArgs=MoreArgs,
bind.rows = bind.rows, MoreArgs=MoreArgs, details=details,
use_superclass=use_superclass, harmonise_class=harmonise_class)
return(res)
}
Expand Down Expand Up @@ -90,14 +100,7 @@ cf_partners <- function(ids, threshold=1L, partners=c("inputs", "outputs"),
do.call(PFUN, commonArgs)
}

# Enrich with partner metadata if partnerfun returned minimal data
tres <- add_partner_metadata(tres, dataset = n, partners = partners)

tres=coconat:::standardise_partner_summary(tres)
if(isTRUE(harmonise_class) && "class" %in% colnames(tres))
tres$class=harmonise_top_class_values(tres$class, n)
if("side" %in% colnames(tres))
tres$side=normalise_side(tres$side)
if(nrow(tres)>0) {
tres$dataset=n
tres$tissue=dataset_tissue(n)
Expand All @@ -108,8 +111,37 @@ cf_partners <- function(ids, threshold=1L, partners=c("inputs", "outputs"),
tres$sex=character()
warning("no ", partners, " found for `", n, "` dataset.")
}
# Add keys before metadata enrichment
tres$pre_key=keys(tres, idcol="pre_id")
tres$post_key=keys(tres, idcol='post_id')

# Enrich with metadata based on details option
# Always use cf_add_meta rather than relying on partnerfun metadata
if (details != "neither" && nrow(tres) > 0) {
# Keep only core connectivity columns, drop all partnerfun metadata
# cf_add_meta will re-add metadata from cf_meta for consistency
core_cols <- c("pre_id", "post_id", "weight", "dataset", "tissue", "sex",
"pre_key", "post_key")
tres <- tres[, intersect(names(tres), core_cols), drop = FALSE]

Comment on lines +118 to +126

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When details='neither', this code path only skips cf_add_meta but does not remove any metadata that partnerfun may already have returned. Several built-in partnerfuns (e.g. fanc/banc) left_join metadata before returning, so details='neither' would still include type/class/etc and violate the documented contract. Consider always stripping metadata columns (or subsetting to core connectivity columns) when details=='neither'.

Suggested change
# Enrich with metadata based on details option
# Always use cf_add_meta rather than relying on partnerfun metadata
if (details != "neither" && nrow(tres) > 0) {
# Keep only core connectivity columns, drop all partnerfun metadata
# cf_add_meta will re-add metadata from cf_meta for consistency
core_cols <- c("pre_id", "post_id", "weight", "dataset", "tissue", "sex",
"pre_key", "post_key")
tres <- tres[, intersect(names(tres), core_cols), drop = FALSE]
# Always drop partnerfun-specific metadata, keep only core connectivity columns
# cf_add_meta will re-add standardized metadata from cf_meta as needed
core_cols <- c("pre_id", "post_id", "weight", "dataset", "tissue", "sex",
"pre_key", "post_key")
tres <- tres[, intersect(names(tres), core_cols), drop = FALSE]
# Enrich with metadata based on details option
# Always use cf_add_meta rather than relying on partnerfun metadata
if (details != "neither" && nrow(tres) > 0) {

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +126

Copilot AI Apr 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subsetting to core_cols drops all partnerfun-provided columns beyond the core connectivity fields (including potentially non-metadata columns). This also makes keep.all ineffective for preserving dataset-specific extra columns across datasets. If the goal is to ignore partnerfun metadata only, consider dropping just the known metadata columns (or only those that overlap cf_meta outputs) while preserving any additional connectivity metrics.

Suggested change
# Always use cf_add_meta rather than relying on partnerfun metadata
if (details != "neither" && nrow(tres) > 0) {
# Keep only core connectivity columns, drop all partnerfun metadata
# cf_add_meta will re-add metadata from cf_meta for consistency
core_cols <- c("pre_id", "post_id", "weight", "dataset", "tissue", "sex",
"pre_key", "post_key")
tres <- tres[, intersect(names(tres), core_cols), drop = FALSE]
# Always use cf_add_meta for standardized metadata; preserve any
# additional partnerfun-provided columns (e.g. extra connectivity metrics)
if (details != "neither" && nrow(tres) > 0) {

Copilot uses AI. Check for mistakes.
# Determine which key columns to enrich
query_col <- if (partners == "outputs") "pre_key" else "post_key"
partner_col <- if (partners == "outputs") "post_key" else "pre_key"

keycols <- switch(details,
partner = partner_col,
query = query_col,
both = c("pre_key", "post_key")
)
suffixes <- switch(details,
partner = "",
query = "",
both = c(".pre", ".post")
)

tres <- cf_add_meta(tres, keycol = keycols, suffix = suffixes,
harmonise_class = harmonise_class)
}
res[[n]]=tres
}
if(isTRUE(bind.rows)) {
Expand Down Expand Up @@ -270,66 +302,3 @@ cf_partner_summary <- function(ids, threshold=1L, partners=c("inputs", "outputs"
outputcol = ifelse(partners=='outputs', group.post[1], group.pre[1]),
standardise_input = F, sparse = rval=="sparse")
}


# Add metadata to partner results if the partnerfun returned minimal columns
# This allows partnerfuns to return just ids + weight and have metadata added
# automatically via cf_meta()
add_partner_metadata <- function(tres, dataset, partners) {
if (is.null(tres) || !is.data.frame(tres) || nrow(tres) == 0) {
return(tres)
}

# Check if we need to fetch partner metadata:
# - 3 columns (pre_id, post_id, weight) always needs enrichment
# - <=5 columns without 'type' likely needs enrichment
needs_enrichment <- ncol(tres) == 3 ||
(ncol(tres) <= 5 && !"type" %in% names(tres))

if (!needs_enrichment) {
return(tres)
}

# Find the partner column based on query direction
# For outputs: partners are post_id; for inputs: partners are pre_id
partner_col <- if (partners == "outputs") "post_id" else "pre_id"

# Check if expected column exists, otherwise try to find it

if (!partner_col %in% colnames(tres)) {
# Fallback: look for a single *_id column or 'partner' column
id_cols <- grep("_id$", colnames(tres), value = TRUE)
if (length(id_cols) == 1) {
partner_col <- id_cols
} else {
partner_col <- grep("^partner$", colnames(tres), value = TRUE)
}
}

if (length(partner_col) != 1 || !partner_col %in% colnames(tres)) {
warning("Unable to find partner column for dataset: ", dataset,
"\nPartnerfuns should return pre_id/post_id columns or include metadata")
return(tres)
}

# Fetch metadata for unique partner IDs
pids <- unique(tres[[partner_col]])
# Convert to character for keys() and ensure dataset uses abbreviation
pids_char <- coconat::id2char(pids)
metadf <- cf_meta(keys(data.frame(id = pids_char, dataset = dataset)))

if (is.null(metadf) || nrow(metadf) == 0) {
return(tres)
}

# Remove columns that will be added by cf_partners later
metadf <- metadf[setdiff(colnames(metadf), c("dataset", "key"))]

# Rename id column to match partner column for joining
colnames(metadf)[1] <- partner_col

# Ensure partner column is character for joining
tres[[partner_col]] <- coconat::id2char(tres[[partner_col]])

dplyr::left_join(tres, metadf, by = partner_col)
}
1 change: 1 addition & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ reference:
desc: Fetch neuron metadata and synaptic partner information
contents:
- cf_meta
- cf_add_meta
- cf_partners
- cf_partner_summary
- multi_connection_table
Expand Down
49 changes: 49 additions & 0 deletions man/cf_add_meta.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions man/cf_partners.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions tests/testthat/test-datasets.R
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ test_that("dataset functions work", {

test_that("match_datasets suggests close matches", {
# Typo should suggest correct dataset
expect_error(match_datasets("hemibran"), "Did you mean 'hemibrain'")
expect_error(match_datasets("flywie"), "Did you mean 'flywire'")
expect_error(match_datasets("hemb"), "Did you mean 'hemibrain'")
expect_error(match_datasets("flyb"), "Did you mean 'flywire'")
expect_error(match_datasets("opic"), "Did you mean 'opticlobe'")
})

test_that("match_datasets shows no suggestion for unrelated names", {
Expand Down
Loading
Loading