diff --git a/DESCRIPTION b/DESCRIPTION index 3e25564..ab4720e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -16,7 +16,7 @@ URL: https://github.com/flyconnectome/aedes, https://flyconnectome.github.io/aed BugReports: https://github.com/flyconnectome/aedes/issues Depends: R (>= 4.1.0), - fafbseg (>= 0.15.9) + fafbseg (>= 0.15.10) Imports: arrow, bit64, @@ -56,4 +56,4 @@ LazyData: true LazyDataCompression: xz Roxygen: list(markdown = TRUE) Config/Needs/website: pkgdown -Config/roxygen2/version: 8.0.0 +Config/roxygen2/version: 8.1.0 diff --git a/NAMESPACE b/NAMESPACE index dd827ec..1eba95c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -11,6 +11,8 @@ export(aedes_mirror) export(aedes_nm2raw) export(aedes_point_side) export(aedes_raw2nm) +export(aedes_set_group) +export(aedes_set_meta) export(aedes_set_version) export(aedes_soma_position) export(aedes_soma_side) @@ -21,18 +23,22 @@ export(read_aedes_neurons) export(register_aedes_coconat) export(with_aedes) import(fafbseg) -importFrom(dplyr,add_count) -importFrom(dplyr,arrange) -importFrom(dplyr,desc) -importFrom(dplyr,distinct) -importFrom(dplyr,filter) -importFrom(dplyr,mutate) -importFrom(dplyr,select) -importFrom(graphics,par) -importFrom(graphics,plot.new) -importFrom(graphics,plot.window) -importFrom(graphics,rect) -importFrom(graphics,text) +importFrom(dplyr, + add_count, + arrange, + desc, + distinct, + filter, + mutate, + select +) +importFrom(graphics, + par, + plot.new, + plot.window, + rect, + text +) importFrom(magrittr,"%>%") importFrom(memoise,memoise) importFrom(rlang,.data) diff --git a/R/aedes-package.R b/R/aedes-package.R index 39c6346..9aef0a7 100644 --- a/R/aedes-package.R +++ b/R/aedes-package.R @@ -1,4 +1,23 @@ #' @keywords internal +#' @section Package Options: \itemize{ +#' +#' \item{\code{aedes.version}} Default materialisation selector used by +#' \code{\link{aedes_get_version}} (and, transitively, everything that pins a +#' timestamp -- \code{\link{aedes_meta}}, \code{\link{aedes_add_neurons}}, +#' \code{\link{aedes_set_meta}}, \code{\link{aedes_set_group}}). Accepts +#' \code{"latest"} (default when unset), \code{"now"}, an integer +#' materialisation version or an explicit timestamp. Set with +#' \code{\link{aedes_set_version}} or \code{options(aedes.version = ...)}. +#' +#' \item{\code{aedes.initials}} Curator initials used to auto-fill the +#' single-string \code{initials} column and, when +#' \code{annotator = TRUE} / \code{proofreader = TRUE}, appended to the +#' corresponding multi-select column. Consumed by +#' \code{\link{aedes_add_neurons}}, \code{\link{aedes_set_meta}} and +#' \code{\link{aedes_set_group}}. Set once per session with +#' \code{options(aedes.initials = "XY")}. +#' +#' } #' @import fafbseg #' @importFrom magrittr %>% #' @importFrom rlang .data diff --git a/R/aedes_add_neurons.R b/R/aedes_add_neurons.R index e101451..3a55d03 100644 --- a/R/aedes_add_neurons.R +++ b/R/aedes_add_neurons.R @@ -6,58 +6,239 @@ #' reliable. Rows whose `root_id` is already present are updated with any #' extra columns supplied via `...`; rows that are absent are appended with a #' `point_xyz` computed by [aedes_key_point()]. `supervoxel_id` and -#' `serial_id` are left blank — a server-side process fills them in from +#' `serial_id` are left blank -- a server-side process fills them in from #' `point_xyz`. #' +#' @details By default the function also auto-fills `soma_xyz`, `nucleus_id` +#' and `side` for each row via [aedes_soma_position()] and +#' [aedes_point_side()]. When the soma cascade returns no position for an id +#' the fallback for `side` is [aedes_point_side()] on the L2 key point, and +#' a warning naming the affected ids is issued. +#' +#' Auto-fill columns (`soma_xyz`, `nucleus_id`, `side`, `point_xyz`) never +#' overwrite a non-NA value on an existing row. Values passed via `...` +#' always win over the auto-fill and always overwrite on existing rows. +#' #' @param ids Root ids of neurons to add or update. #' @param dryrun If `TRUE` (the default) no writes are performed; the function #' returns the data frames that would have been used. #' @param ... Additional columns to set on each row (e.g. `cell_class = "KC"`). -#' Recycled across all input ids. +#' Recycled across all input ids. Values here always win over the auto-fill +#' below. +#' @param soma If `TRUE` (the default), auto-fill `soma_xyz` and `nucleus_id` +#' from [aedes_soma_position()]. +#' @param side If `TRUE` (the default), auto-fill `side` from +#' [aedes_point_side()] applied to the soma; falls back to the L2 key point +#' (with a warning) for ids where the soma cascade returns nothing. +#' @param status FlyTable status. A shortlist of the common values is exposed +#' in the signature for tab-completion. Any other value is live-checked +#' against the vocabulary already present in `aedes_main`; unknown values +#' error. Must be supplied unless `"status"` is removed from `required`. +#' @param required Columns that must be supplied (via `...`, or via `status` / +#' `initials`). Defaults to `c("superclass", "status", "initials")`. Set to +#' `character(0)` to skip the check. +#' @param initials Curator initials for the single-string `initials` column. +#' Defaults to `getOption("aedes.initials")`; set once per session with +#' `options(aedes.initials = "XY")`. Passed through `...` semantics -- an +#' explicit `initials = ...` in `...` wins over the option. +#' @param annotator Multi-select `annotator` column write policy. `TRUE` +#' (the default) appends `getOption("aedes.initials")` to the cell; `FALSE` +#' leaves the column alone; a character vector (or comma-joined string) +#' appends those tokens explicitly. On existing rows the tokens are merged +#' with the current cell contents (unique, sorted). +#' @param proofreader Multi-select `proofreader` column write policy. Same +#' accepted values as `annotator`; defaults to `FALSE` (leave the column +#' alone). +#' @param wipe If `TRUE`, replace the target multi-select column(s) with just +#' the new tokens instead of merging with existing cell contents. Default +#' `FALSE` (append). #' @return A list. With `dryrun = TRUE` it has elements `up` (rows that would #' be updated) and/or `new` (rows that would be appended). With #' `dryrun = FALSE` only `new` is returned (so the caller can see which #' `point_xyz` values were chosen). #' @export -aedes_add_neurons <- function(ids, dryrun = TRUE, ...) { - fids <- fafbseg::flywire_ids(ids, unique = TRUE) - fids <- setdiff(fids, 0) +aedes_add_neurons <- function(ids, dryrun = TRUE, ..., + soma = TRUE, side = TRUE, + status = c("adequate", "to_review", + "needs_extending", "incomplete", + "missing soma"), + required = c("superclass", "status", "initials"), + initials = getOption("aedes.initials"), + annotator = TRUE, proofreader = FALSE, + wipe = FALSE) { + .aedes_reject_dry_run(...) + extra <- list(...) + ann_toks <- .aedes_resolve_initials(annotator, "annotator") + prf_toks <- .aedes_resolve_initials(proofreader, "proofreader") + + # Unmodified multi-value default => caller didn't supply status. + status_shortlist <- eval(formals(aedes_add_neurons)$status) + if (length(status) > 1L) status <- NULL + if (!is.null(status)) { + if (!is.character(status) || length(status) != 1L || is.na(status)) + stop("`status` must be a single non-NA string.", call. = FALSE) + extra[["status"]] <- status + } + # `initials` from options if not already in `...`. + if (is.null(extra[["initials"]]) && !is.null(initials)) { + if (!is.character(initials) || length(initials) != 1L || is.na(initials)) + stop("`initials` must be a single non-NA string ", + "(set via options(aedes.initials = ...) or the `initials` arg).", + call. = FALSE) + extra[["initials"]] <- initials + } + + # pin a single timestamp for both the supplied ids and the flytable rows, + # reading aedes_main fresh and bringing it to the same timestamp + pin <- .aedes_pin_meta(ids) + ids <- pin$ids + am <- pin$am + ts <- pin$ts + + # Validate status: fast accept if in the shortlist, else live-check against + # the values already present in aedes_main. Case-sensitive on purpose. + if (!is.null(extra[["status"]])) { + st <- extra[["status"]] + if (!st %in% status_shortlist) { + vocab <- setdiff(unique(am$status), NA) + if (!st %in% vocab) + stop(sprintf( + "status '%s' is not in the aedes_main vocabulary. Known values (%d): %s", + st, length(vocab), paste(sort(vocab), collapse = ", ")), + call. = FALSE) + } + } + + # Required-column check. `character(0)` disables entirely. + if (length(required)) { + miss <- setdiff(required, names(extra)) + if (length(miss)) + stop("Missing required column(s): ", paste(miss, collapse = ", "), + ". Pass via `...`, e.g. `superclass = \"KC\"`", + if ("initials" %in% miss) + " -- for `initials` you can also set once with ", + "options(aedes.initials = \"XY\")", + ".", call. = FALSE) + } + + # Base input frame with caller-supplied columns. + indf <- data.frame(root_id = ids, stringsAsFactors = FALSE) + for (nm in names(extra)) indf[[nm]] <- extra[[nm]] + + # Split existing vs missing (order-preserving via match). + iidx <- match(ids, am$root_id) + is_upd <- !is.na(iidx) + iidf <- am[iidx[is_upd], , drop = FALSE] + + # ---- Soma / side enrichment (computed for all ids up-front) ------------ + auto_soma_raw <- rep(NA_character_, length(ids)) + auto_nucleus <- rep(NA_integer_, length(ids)) + auto_side_vec <- rep(NA_character_, length(ids)) + if (soma || side) { + sp_nm <- aedes_soma_position(ids, units = "nm", + version = ts$version, timestamp = ts$timestamp) + ok <- !is.na(sp_nm$position) & nzchar(sp_nm$position) + if (soma) { + if (any(ok)) { + xyz_nm <- nat::xyzmatrix(sp_nm$position[ok]) + auto_soma_raw[ok] <- nat::xyzmatrix2str(aedes_nm2raw(xyz_nm)) + } + auto_nucleus <- as.integer(sp_nm$nucleus_id) + } + if (side && any(ok)) { + xyz_nm <- nat::xyzmatrix(sp_nm$position[ok]) + auto_side_vec[ok] <- aedes_point_side(xyz_nm, units = "nm") + } + } - # pin a single timestamp for both the supplied ids and the flytable rows - ts <- aedes_get_version(timestamp = "now") - ids <- with_aedes(fafbseg::flywire_latestid(fids, timestamp = ts$timestamp)) + # ---- Key points --------------------------------------------------------- + # Needed for: every new row's point_xyz; update rows whose existing + # point_xyz is empty; and (when side=TRUE) any row where the soma cascade + # yielded no position so side must fall back to the L2 key point. + is_str_empty <- function(x) is.na(x) | !nzchar(as.character(x)) + needs_kp <- rep(FALSE, length(ids)) + needs_kp[!is_upd] <- TRUE + if (any(is_upd)) { + upd_pt_empty <- is_str_empty(iidf$point_xyz) + needs_kp[which(is_upd)[upd_pt_empty]] <- TRUE + } + if (side) needs_kp[is.na(auto_side_vec)] <- TRUE - # read aedes_main fresh, then bring its root_ids to the same timestamp - am <- aedes_meta(expiry = 0) - am <- aedes_sequential_update(am, version = ts$version, timestamp = ts$timestamp) + key_pts_raw <- rep(NA_character_, length(ids)) + key_pts_nm <- matrix(NA_real_, nrow = length(ids), ncol = 3) + if (any(needs_kp)) { + pts_raw <- aedes_key_point(ids[needs_kp], raw = TRUE) + key_pts_raw[needs_kp] <- nat::xyzmatrix2str(pts_raw) + key_pts_nm[needs_kp, ] <- aedes_raw2nm(pts_raw) + } - indf <- data.frame(root_id = ids, ..., stringsAsFactors = FALSE) - iidf <- am[am$root_id %in% ids, , drop = FALSE] + # side fallback: rows still NA on side but with a computed key point. + if (side) { + fb <- is.na(auto_side_vec) & !is.na(key_pts_raw) + if (any(fb)) { + auto_side_vec[fb] <- aedes_point_side( + key_pts_nm[fb, , drop = FALSE], units = "nm") + fb_ids <- ids[fb] + warning(length(fb_ids), " id(s) had no soma; side derived from ", + "L2 key point: ", + paste(utils::head(fb_ids, 3), collapse = ", "), + if (length(fb_ids) > 3) + sprintf(" (+%d more)", length(fb_ids) - 3), + call. = FALSE) + } + } + # ---- Attach auto values (caller's `...` wins) --------------------------- + auto_cols <- character(0) + if (soma && !"soma_xyz" %in% names(extra)) { + indf$soma_xyz <- auto_soma_raw + auto_cols <- c(auto_cols, "soma_xyz") + } + if (soma && !"nucleus_id" %in% names(extra)) { + indf$nucleus_id <- auto_nucleus + auto_cols <- c(auto_cols, "nucleus_id") + } + if (side && !"side" %in% names(extra)) { + indf$side <- auto_side_vec + auto_cols <- c(auto_cols, "side") + } + if (!"point_xyz" %in% names(extra)) { + indf$point_xyz <- key_pts_raw + auto_cols <- c(auto_cols, "point_xyz") + } + + # ---- Assemble update / new frames --------------------------------------- rlist <- list() - newdf <- NULL - if (nrow(iidf) > 0) { - updf <- dplyr::left_join(iidf[, c("_id", "root_id")], indf, by = "root_id") - if (any(duplicated(updf[["_id"]]))) - stop("Duplicate rows to update!") - if (length(ids) < nrow(iidf)) - stop("Too few ids!") + + if (any(is_upd)) { + upd_in <- indf[is_upd, , drop = FALSE] + # Auto-fill: don't clobber non-empty existing values -- rewrite those cells + # with the row's current value so the engine writes a no-op for them. This + # no-overwrite policy is add_neurons-specific; the engine writes verbatim. + for (col in auto_cols) { + existing <- iidf[[col]] + keep <- !is_str_empty(existing) + if (any(keep)) upd_in[[col]][keep] <- existing[keep] + } + upd_in <- .aedes_append_multiselect( + upd_in, am, list(annotator = ann_toks, proofreader = prf_toks), + wipe = wipe) + res <- .aedes_update_existing(upd_in, dryrun = dryrun, am = am, ts = ts) + if (dryrun) rlist[["up"]] <- res$updf + } + + if (any(!is_upd)) { + newdf <- indf[!is_upd, , drop = FALSE] + if (any(is.na(newdf$point_xyz))) + stop("Failed to compute point_xyz for ", sum(is.na(newdf$point_xyz)), + " new id(s).", call. = FALSE) + # New-row merge: no existing cells, so the merged list is just the tokens. + newdf <- .aedes_append_multiselect( + newdf, am, list(annotator = ann_toks, proofreader = prf_toks)) + rlist[["new"]] <- newdf if (!dryrun) - fafbseg::flytable_update_rows(updf, table = "aedes_main", append_allowed = FALSE) - else - rlist[["up"]] <- updf - if (length(ids) > nrow(iidf)) - newdf <- indf[!indf$root_id %in% updf$root_id, , drop = FALSE] - } else { - newdf <- indf - } - if (!isTRUE(nrow(newdf) > 0)) - return(rlist) - - pts <- aedes_key_point(newdf$root_id) - newdf$point_xyz <- nat::xyzmatrix2str(pts) - rlist[["new"]] <- newdf - if (!dryrun) - fafbseg::flytable_append_rows(newdf[-1], table = "aedes_main") + # drop root_id -- the server derives it (and supervoxel_id) from point_xyz + fafbseg::flytable_append_rows(newdf[-1], table = "aedes_main") + } rlist } diff --git a/R/flytable.R b/R/flytable.R index 6381a86..2cd6c86 100644 --- a/R/flytable.R +++ b/R/flytable.R @@ -35,6 +35,188 @@ aedes_sequential_update <- function(df, version = NULL, timestamp = NULL) { df } +#' Reject a mistaken `dry_run` argument +#' +#' The standard dry-run argument across aedes (and the wider natverse) is +#' `dryrun`. Functions that also take `...` call this so a mistyped `dry_run` +#' errors clearly instead of being silently captured as a column to write. +#' @noRd +.aedes_reject_dry_run <- function(...) { + bad <- intersect(c("dry_run", "dryRun", "dry.run"), names(list(...))) + if (length(bad)) + stop("Use `dryrun`, not `", bad[1L], "`.", call. = FALSE) + invisible(NULL) +} + +#' Resolve an annotator/proofreader argument to the tokens to append +#' +#' Accepts `TRUE` (use `getOption("aedes.initials")`), `FALSE`/`NULL` (no-op), +#' or a character vector of one or more initials (comma-joined strings are +#' split, symmetric with how a multi-select cell reads back). Returns `NULL` +#' when nothing should be written to the column, or a non-empty character +#' vector of tokens to append. +#' @noRd +.aedes_resolve_initials <- function(x, argname) { + if (isFALSE(x) || is.null(x)) return(NULL) + if (isTRUE(x)) { + ini <- getOption("aedes.initials") + if (!is.character(ini) || length(ini) != 1L || is.na(ini) || !nzchar(ini)) + stop("`", argname, " = TRUE` needs `aedes.initials` set. Set once with ", + "options(aedes.initials = \"XY\") or pass ", argname, + " = \"XY\" explicitly.", call. = FALSE) + x <- ini + } + if (!is.character(x)) + stop("`", argname, "` must be TRUE, FALSE, or a character vector.", + call. = FALSE) + toks <- unlist(strsplit(x, ",", fixed = TRUE), use.names = FALSE) + toks <- trimws(toks[!is.na(toks) & nzchar(toks)]) + if (!length(toks)) + stop("`", argname, "` has no non-empty initials tokens.", call. = FALSE) + unique(toks) +} + +#' Append (or wipe-and-set) tokens on multi-select column(s) of an update frame +#' +#' Generic per-column merge for FlyTable multi-select cells. `values` names +#' any subset of multi-select columns and gives a character vector of tokens +#' to add. For each `updf` row, the existing cell in `am` (indexed by +#' `root_id`) is read, split on commas if a string, unioned with the new +#' tokens (sorted, deduped) and re-emitted as a comma-joined scalar. Columns +#' with `NULL` tokens are skipped; rows absent from `am` (i.e. new rows) merge +#' against empty and end up with just the new tokens. +#' +#' Empty-cell detection is generous: `NA`, the literal strings `"NA"` and +#' `"NaN"`, and empty/whitespace-only strings all count as empty (matches how +#' comma-collapsed seatable cells sometimes round-trip through CSV / pandas). +#' +#' `wipe = TRUE` replaces the cell with just the new tokens (existing content +#' ignored). `wipe = FALSE` (the default) appends. +#' +#' Output is a plain comma-joined scalar per row -- fafbseg's multi-select +#' write path (`flytable_listify_multiselect_col`) splits it back into a list +#' per cell for the JSON payload, so no `I(list(...))` wrapping is needed here. +#' @noRd +.aedes_append_multiselect <- function(updf, am, values, wipe = FALSE) { + values <- values[!vapply(values, is.null, logical(1))] + if (!length(values)) return(updf) + idx <- match(as.character(updf$root_id), as.character(am$root_id)) + + # Drop only actual NA and empty/whitespace-only; treat "NA"/"NaN" as data. + # Legacy stray "NA" tokens from historical writes will sit until a caller + # rewrites with wipe = TRUE; we never silently drop what looks like initials. + clean <- function(x) { + x <- as.character(x) + x <- trimws(x[!is.na(x)]) + x[nzchar(x)] + } + split_cell <- function(cell) { + if (is.list(cell)) return(clean(unlist(cell, use.names = FALSE))) + if (length(cell) == 0L) return(character(0)) + if (length(cell) > 1L) return(clean(cell)) + cell <- as.character(cell) + if (is.na(cell)) return(character(0)) + clean(strsplit(cell, ",", fixed = TRUE)[[1L]]) + } + + for (col in names(values)) { + new_tokens <- clean(values[[col]]) + existing <- if (wipe || !col %in% names(am)) + rep(NA, nrow(updf)) else am[[col]][idx] + updf[[col]] <- vapply(seq_along(existing), function(i) { + cur <- if (wipe) character(0) else split_cell(existing[[i]]) + paste(sort(unique(c(cur, new_tokens))), collapse = ",") + }, character(1)) + } + updf +} + +#' Pin a timestamp and read a timestamp-consistent aedes_main +#' +#' Pins a single timestamp, normalises `ids` to root ids at it, and reads +#' `aedes_main` mapped to the same timestamp -- so that join-by-`root_id` is +#' reliable. The table read relies on [aedes_meta()]'s own `version`/`timestamp` +#' mapping (which brings `root_id` forward via `supervoxel_id`); a single pinned +#' `ts` is shared with the id resolution so both sides agree. Shared by +#' [aedes_add_neurons()] and [.aedes_update_existing()]. +#' +#' @param ids Root ids in any form understood by [fafbseg::flywire_ids()]. +#' @return A list with `ids` (latest root ids), `am` (the mapped table) and +#' `ts` (the pinned version/timestamp from [aedes_get_version()]). +#' @noRd +.aedes_pin_meta <- function(ids) { + fids <- setdiff(fafbseg::flywire_ids(ids, unique = TRUE), 0) + ts <- aedes_get_version(timestamp = "now") + lids <- with_aedes(fafbseg::flywire_latestid(fids, timestamp = ts$timestamp)) + am <- aedes_meta(version = ts$version, timestamp = ts$timestamp, expiry = 0) + list(ids = lids, am = am, ts = ts) +} + +#' Update existing aedes_main rows from a data.frame of column values +#' +#' @description Shared write engine for updating rows that already exist in +#' `aedes_main`. Resolves `df$root_id` to the table `_id` in a +#' timestamp-consistent snapshot and updates exactly the supplied columns on +#' the matching rows. +#' +#' @details Update-only: ids absent from the table are returned in `missing` +#' rather than appended -- the caller decides what to do with them. Values are +#' written verbatim (caller-supplied values always win); any no-overwrite +#' policy is the caller's responsibility, applied to `df` before calling. +#' +#' @param df A data.frame with a `root_id` column plus the columns to write. +#' @param dryrun If `TRUE` (default) assemble and return the update frame +#' without writing. +#' @param on_dup What to do when a `root_id` appears more than once among the +#' matched rows: `"error"` (default) or `"first"` (keep the first occurrence). +#' @param am,ts Optional pre-pinned table and version (see [.aedes_pin_meta()]). +#' When both are supplied `df$root_id` is assumed already at `ts` and is not +#' re-resolved (avoids a second table read). +#' @return A list with `updf` (rows written / to write, keyed by `_id`) and +#' `missing` (root_ids not found in the table). +#' @noRd +.aedes_update_existing <- function(df, dryrun = TRUE, on_dup = c("error", "first"), + am = NULL, ts = NULL) { + on_dup <- match.arg(on_dup) + if (!is.data.frame(df) || !"root_id" %in% names(df)) + stop("`df` must be a data.frame with a `root_id` column.", call. = FALSE) + if (is.null(am) || is.null(ts)) { + pin <- .aedes_pin_meta(df$root_id) + df$root_id <- pin$ids + am <- pin$am + ts <- pin$ts + } + df$root_id <- as.character(df$root_id) + idx <- match(df$root_id, as.character(am$root_id)) + found <- !is.na(idx) + missing <- unique(df$root_id[!found]) + + df_found <- df[found, , drop = FALSE] + idx <- idx[found] + dups <- unique(df_found$root_id[duplicated(df_found$root_id)]) + if (length(dups)) { + if (on_dup == "error") + stop("Duplicated root_id(s) in the update set: ", + paste(utils::head(dups, 5L), collapse = ", "), + if (length(dups) > 5L) sprintf(" (+%d more)", length(dups) - 5L), + call. = FALSE) + keep <- !duplicated(df_found$root_id) + df_found <- df_found[keep, , drop = FALSE] + idx <- idx[keep] + } + + updf <- df_found + updf[["_id"]] <- am[["_id"]][idx] + updf <- updf[c("_id", setdiff(names(updf), "_id"))] + rownames(updf) <- NULL + if (any(duplicated(updf[["_id"]]))) + stop("Multiple root_ids map to the same aedes_main row (`_id`).", call. = FALSE) + + if (!dryrun && nrow(updf) > 0L) + fafbseg::flytable_update_rows(updf, table = "aedes_main", append_allowed = FALSE) + list(updf = updf, missing = missing) +} + #' Update ids in aedes_main table manually #' #' @param update.serial_ids Whether to update the serial_id column uniquely diff --git a/R/groups.R b/R/groups.R new file mode 100644 index 0000000..c7979c2 --- /dev/null +++ b/R/groups.R @@ -0,0 +1,163 @@ +#' Group aedes neurons together in FlyTable +#' +#' @description Assigns a shared `group` id to a set of neurons in the +#' `aedes_main` FlyTable -- the convenient way to build serial / cell-type +#' groups and, via `join_existing`, to add neurons to a group that already +#' exists. +#' +#' @details By convention a group is identified by an integer equal to the +#' smallest `serial_id` among its founding members; `group = 0` (or `NA`) means +#' ungrouped. When the selected neurons are all currently ungrouped a fresh +#' group id is minted from `min(serial_id)`. +#' +#' When some selected neurons already belong to a group, `join_existing` +#' decides what happens (see the argument). Reassigning neurons out of a group +#' whose other members were not supplied emits a warning, since it splits that +#' group. Only the `group` column is written, via the shared update engine that +#' also backs [aedes_set_meta()]; the same pinned table snapshot is reused so +#' the returned preview matches what is written. +#' +#' @param ids Neurons to group, in any form understood by [aedes_ids()] +#' (including a query string). +#' @param group Optional explicit target. An integer forces that group id; `0` +#' or `NA` ungroups; a query / ids joins the group of those neuron(s) +#' ("join-by-example"). When `NULL` (the default) the group id is derived (see +#' Details). +#' @param join_existing Controls behaviour when selected neurons already belong +#' to a group. `NA` (the default): refuse to guess -- warn (dry run) or error +#' (live) and explain how to proceed. `TRUE`: add them to the existing group +#' (its id is kept even if a lower `serial_id` is now available; several +#' existing groups are merged into the smallest, with a warning). `FALSE`: +#' ignore existing membership and mint a fresh group from `min(serial_id)`. +#' @param dryrun logical: if `TRUE` (the default) return a preview without +#' writing to FlyTable. +#' @param annotator Multi-select `annotator` column write policy for rows that +#' actually change group. `TRUE` (the default) appends +#' `getOption("aedes.initials")` to the existing cell; `FALSE` leaves the +#' column alone; a character vector (or comma-joined string) appends those +#' tokens explicitly. +#' @param proofreader Multi-select `proofreader` column write policy. Same +#' accepted values as `annotator`; defaults to `FALSE`. +#' @param wipe If `TRUE`, replace the target multi-select column(s) with just +#' the new tokens instead of merging with existing cell contents. Default +#' `FALSE` (append). +#' @param ... reserved (used to reject a mistaken `dry_run` argument). +#' +#' @returns A preview data.frame with one row per selected neuron: `root_id`, +#' `serial_id`, `group_old`, `group_new` and `changed`. Returned invisibly on a +#' live write. +#' @seealso [aedes_set_meta()], [aedes_add_neurons()] +#' @export +aedes_set_group <- function(ids, group = NULL, join_existing = NA, + dryrun = TRUE, + annotator = TRUE, proofreader = FALSE, + wipe = FALSE, ...) { + .aedes_reject_dry_run(...) + ann_toks <- .aedes_resolve_initials(annotator, "annotator") + prf_toks <- .aedes_resolve_initials(proofreader, "proofreader") + as_int <- function(x) suppressWarnings(as.integer(as.character(x))) + + pin <- .aedes_pin_meta(aedes_ids(ids)) + am <- pin$am + ts <- pin$ts + rids <- pin$ids + if (!length(rids)) stop("No valid ids.", call. = FALSE) + + idx <- match(rids, as.character(am$root_id)) + if (anyNA(idx)) + stop("These ids are not present in aedes_main: ", + paste(rids[is.na(idx)], collapse = ", "), call. = FALSE) + + am_group <- as_int(am$group) + cur_group <- am_group[idx] + cur_group0 <- ifelse(is.na(cur_group), 0L, cur_group) + cur_serial <- as_int(am$serial_id[idx]) + in_group <- cur_group0 > 0L + existing_groups <- sort(unique(cur_group0[in_group])) + + minserial <- suppressWarnings(min(cur_serial, na.rm = TRUE)) + if (!is.finite(minserial)) + stop("No valid serial_id among the selected neurons.", call. = FALSE) + + # ---- Determine the target group id -------------------------------------- + if (!is.null(group)) { + if (length(group) != 1L) + stop("`group` must be a single value.", call. = FALSE) + if (is.na(group)) { + target <- 0L + } else if (is.numeric(group) || grepl("^[0-9]+$", as.character(group))) { + target <- as.integer(group) + } else { + # join-by-example: adopt the group of the referenced neuron(s) + exg <- am_group[match(as.character(aedes_ids(group)), + as.character(am$root_id))] + exg <- exg[!is.na(exg) & exg > 0L] + if (!length(exg)) + stop("The `group` reference neuron(s) have no group to join.", + call. = FALSE) + target <- min(exg) + } + } else if (length(existing_groups)) { + # some selected neurons already grouped -> honour join_existing + if (isTRUE(join_existing)) { + target <- min(existing_groups) + if (length(existing_groups) > 1L) + warning("Merging groups ", paste(existing_groups, collapse = ", "), + " into ", target, ".", call. = FALSE) + } else if (isFALSE(join_existing)) { + target <- minserial + message(sum(in_group), " neuron(s) already in group(s) ", + paste(existing_groups, collapse = ", "), + " reassigned to new group ", target, + " (existing membership ignored).") + } else { + # join_existing = NA -> refuse to guess silently + msg <- paste0( + sum(in_group), " selected neuron(s) already belong to group(s) ", + paste(existing_groups, collapse = ", "), ".\n", + " re-run with join_existing=TRUE to add them to group ", + min(existing_groups), "\n", + " re-run with join_existing=FALSE to start a fresh group ", minserial) + if (dryrun) { + warning(msg, call. = FALSE) + target <- min(existing_groups) # preview the join + } else { + stop(msg, call. = FALSE) + } + } + } else { + target <- minserial + } + target <- as.integer(target) + + # ---- Orphan guard: warn if a reassignment splits an existing group ------ + for (g in setdiff(existing_groups, target)) { + members <- as.character(am$root_id[which(am_group == g)]) + orphans <- setdiff(members, rids) + if (length(orphans)) + warning("Reassigning neurons out of group ", g, " leaves ", + length(orphans), " other member(s) behind (splitting it).", + call. = FALSE) + } + + # ---- Build preview + (optionally) write --------------------------------- + new_group <- rep(target, length(rids)) + changed <- cur_group0 != new_group + preview <- data.frame( + root_id = rids, + serial_id = cur_serial, + group_old = cur_group0, + group_new = new_group, + changed = changed, + stringsAsFactors = FALSE) + + if (any(changed) && !dryrun) { + updf <- data.frame(root_id = rids[changed], group = new_group[changed], + stringsAsFactors = FALSE) + updf <- .aedes_append_multiselect( + updf, am, list(annotator = ann_toks, proofreader = prf_toks), wipe = wipe) + .aedes_update_existing(updf, dryrun = FALSE, am = am, ts = ts) + return(invisible(preview)) + } + preview +} diff --git a/R/meta.R b/R/meta.R index 36c5b20..afb1a18 100644 --- a/R/meta.R +++ b/R/meta.R @@ -8,6 +8,9 @@ #' @param timestamp Optional CAVE timestamp. #' @param unique Whether to drop duplicate `root_id` rows (with duplicates #' attached as an attribute). +#' @param translate_ids Whether to bring explicitly supplied `ids` forward to +#' the requested `version`/`timestamp` before matching (see Details). +#' `NA` (the default) decides automatically. #' @param ... Additional arguments passed to [fafbseg::cam_meta()] (e.g. #' cache controls such as `expiry`, `refresh`). #' @return For `aedes_meta()`, a data.frame of metadata. For `aedes_ids()`, a @@ -20,6 +23,16 @@ #' hour). If you want to be sure that ids match the most up to date state of #' the segmentation possible then you can ask for `timestamp='now'`. #' +#' For a **query string** the match happens against that mapped table, so no +#' further work is needed. For **explicit root `ids`** the join is by +#' `root_id`, so ids that are stale relative to the requested timepoint would +#' silently fail to match. `translate_ids` guards against this by bringing the +#' supplied ids forward with [fafbseg::flywire_latestid()] first. The default +#' (`NA`) turns this on only when it is both needed and meaningful: explicit +#' ids are supplied *and* a `version`/`timestamp` is given. With no +#' version/timestamp nothing is translated, since the flytable is simply at +#' the state of its last half-hourly update. +#' #' @export #' #' @examples @@ -31,7 +44,7 @@ #' aedes_ids("class:ALPN", version='latest') #' } aedes_meta <- function(ids = NULL, ignore.case = FALSE, fixed = FALSE, version = NULL, - timestamp = NULL, unique = FALSE, ...) { + timestamp = NULL, unique = FALSE, translate_ids = NA, ...) { with_aedes(fafbseg::cam_meta( ids = ids, ignore.case = ignore.case, @@ -40,6 +53,7 @@ aedes_meta <- function(ids = NULL, ignore.case = FALSE, fixed = FALSE, version = version = version, timestamp = timestamp, unique = unique, + translate_ids = translate_ids, ... )) } @@ -91,3 +105,90 @@ aedes_ids <- function(ids, ignore.case = FALSE, fixed = FALSE, unique = FALSE, version = vi$version, timestamp = vi$timestamp, ...) am$root_id } + +#' Bulk-update metadata for existing aedes neurons in FlyTable +#' +#' @description Updates rows that already exist in the `aedes_main` FlyTable from +#' a data.frame of per-row metadata. Update-only: every `root_id` must already +#' be present (use [aedes_add_neurons()] to create rows). +#' +#' @details Rows with status `bad_nucleus`, `duplicate` or `not_a_neuron` are +#' dropped before updating; any remaining `root_id` not found in `aedes_main` +#' is an error (nothing is written). Writes go through the shared update engine, +#' which pins a single timestamp so join-by-`root_id` is reliable. +#' +#' @param ids root_ids in any form understood by [aedes_ids()] (including a query +#' string); or, when `df` is `NULL`, a data.frame of metadata that itself +#' contains a `root_id` column. +#' @param df an optional data.frame of metadata, recycled to match `ids`. When +#' supplied together with `ids`, a `root_id` column is prepended from `ids`. +#' @param dryrun logical: if `TRUE` (the default) return the update frame without +#' writing to FlyTable. +#' @param update_roots whether to bring `root_id`s to the pinned timestamp with +#' [fafbseg::flywire_latestid()] before matching. +#' @param annotator Multi-select `annotator` column write policy. `TRUE` +#' (the default) appends `getOption("aedes.initials")` to the existing cell; +#' `FALSE` leaves the column alone; a character vector (or comma-joined +#' string) appends those tokens explicitly. +#' @param proofreader Multi-select `proofreader` column write policy. Same +#' accepted values as `annotator`; defaults to `FALSE`. +#' @param wipe If `TRUE`, replace the target multi-select column(s) with just +#' the new tokens instead of merging with existing cell contents. Default +#' `FALSE` (append). +#' @param ... reserved (used to reject a mistaken `dry_run` argument). +#' +#' @returns a data.frame of the rows written (or, on a dry run, that would be +#' written), keyed by FlyTable `_id`. +#' @seealso [aedes_add_neurons()], [aedes_set_group()] +#' @export +aedes_set_meta <- function(ids = NULL, df = NULL, dryrun = TRUE, + update_roots = TRUE, + annotator = TRUE, proofreader = FALSE, + wipe = FALSE, ...) { + .aedes_reject_dry_run(...) + ann_toks <- .aedes_resolve_initials(annotator, "annotator") + prf_toks <- .aedes_resolve_initials(proofreader, "proofreader") + if (is.null(df)) { + if (!is.data.frame(ids)) + stop("`ids` must be a data.frame if you do not provide a `df` argument!") + df <- ids + } else if (!is.null(ids)) { + ids <- setdiff(aedes_ids(ids), 0) + df <- cbind(data.frame(root_id = ids, stringsAsFactors = FALSE), df) + } + if (!is.data.frame(df) || !"root_id" %in% names(df)) + stop("Provide metadata as a data.frame with a `root_id` column.") + df$root_id <- as.character(df$root_id) + + # Pin one timestamp; reuse the fetched table for hygiene and the engine. + pin <- .aedes_pin_meta(df$root_id) + am <- pin$am + ts <- pin$ts + if (update_roots) + df$root_id <- with_aedes( + fafbseg::flywire_latestid(df$root_id, timestamp = ts$timestamp)) + + # Status hygiene: never edit these rows via this path. + status <- am$status[match(df$root_id, as.character(am$root_id))] + bad <- status %in% c("bad_nucleus", "duplicate", "not_a_neuron") + if (any(bad)) { + message("Dropping ", sum(bad), + " row(s) with status bad_nucleus/duplicate/not_a_neuron.") + df <- df[!bad, , drop = FALSE] + } + + # All-or-nothing: refuse to write if any id is absent from aedes_main. + present <- df$root_id %in% as.character(am$root_id) + if (!all(present)) { + miss <- unique(df$root_id[!present]) + stop("These ids are not present in aedes_main: ", + paste(utils::head(miss, 10L), collapse = ", "), + if (length(miss) > 10L) sprintf(" (+%d more)", length(miss) - 10L), ".", + call. = FALSE) + } + + df <- .aedes_append_multiselect( + df, am, list(annotator = ann_toks, proofreader = prf_toks), wipe = wipe) + res <- .aedes_update_existing(df, dryrun = dryrun, am = am, ts = ts) + res$updf +} diff --git a/_pkgdown.yml b/_pkgdown.yml index c6f7006..2e1bfc3 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -36,6 +36,8 @@ reference: desc: Bulk add or update FlyTable rows. contents: - aedes_add_neurons + - aedes_set_meta + - aedes_set_group - title: Low level CAVE access contents: diff --git a/man/aedes-package.Rd b/man/aedes-package.Rd index 527c05b..bbb0064 100644 --- a/man/aedes-package.Rd +++ b/man/aedes-package.Rd @@ -8,6 +8,28 @@ \description{ Access to the in progress Aedes aegypti dataset. Organised as a thin wrapper around the fafbseg package. Includes support for adding aedes as a dataset supported by coconatfly. } +\section{Package Options}{ + \itemize{ + +\item{\code{aedes.version}} Default materialisation selector used by +\code{\link{aedes_get_version}} (and, transitively, everything that pins a +timestamp -- \code{\link{aedes_meta}}, \code{\link{aedes_add_neurons}}, +\code{\link{aedes_set_meta}}, \code{\link{aedes_set_group}}). Accepts +\code{"latest"} (default when unset), \code{"now"}, an integer +materialisation version or an explicit timestamp. Set with +\code{\link{aedes_set_version}} or \code{options(aedes.version = ...)}. + +\item{\code{aedes.initials}} Curator initials used to auto-fill the +single-string \code{initials} column and, when +\code{annotator = TRUE} / \code{proofreader = TRUE}, appended to the +corresponding multi-select column. Consumed by +\code{\link{aedes_add_neurons}}, \code{\link{aedes_set_meta}} and +\code{\link{aedes_set_group}}. Set once per session with +\code{options(aedes.initials = "XY")}. + +} +} + \seealso{ Useful links: \itemize{ diff --git a/man/aedes_add_neurons.Rd b/man/aedes_add_neurons.Rd index cc4d6d1..1a62d47 100644 --- a/man/aedes_add_neurons.Rd +++ b/man/aedes_add_neurons.Rd @@ -4,7 +4,19 @@ \alias{aedes_add_neurons} \title{Add new neurons (and update existing ones) in the aedes_main flytable} \usage{ -aedes_add_neurons(ids, dryrun = TRUE, ...) +aedes_add_neurons( + ids, + dryrun = TRUE, + ..., + soma = TRUE, + side = TRUE, + status = c("adequate", "to_review", "needs_extending", "incomplete", "missing soma"), + required = c("superclass", "status", "initials"), + initials = getOption("aedes.initials"), + annotator = TRUE, + proofreader = FALSE, + wipe = FALSE +) } \arguments{ \item{ids}{Root ids of neurons to add or update.} @@ -13,7 +25,43 @@ aedes_add_neurons(ids, dryrun = TRUE, ...) returns the data frames that would have been used.} \item{...}{Additional columns to set on each row (e.g. \code{cell_class = "KC"}). -Recycled across all input ids.} +Recycled across all input ids. Values here always win over the auto-fill +below.} + +\item{soma}{If \code{TRUE} (the default), auto-fill \code{soma_xyz} and \code{nucleus_id} +from \code{\link[=aedes_soma_position]{aedes_soma_position()}}.} + +\item{side}{If \code{TRUE} (the default), auto-fill \code{side} from +\code{\link[=aedes_point_side]{aedes_point_side()}} applied to the soma; falls back to the L2 key point +(with a warning) for ids where the soma cascade returns nothing.} + +\item{status}{FlyTable status. A shortlist of the common values is exposed +in the signature for tab-completion. Any other value is live-checked +against the vocabulary already present in \code{aedes_main}; unknown values +error. Must be supplied unless \code{"status"} is removed from \code{required}.} + +\item{required}{Columns that must be supplied (via \code{...}, or via \code{status} / +\code{initials}). Defaults to \code{c("superclass", "status", "initials")}. Set to +\code{character(0)} to skip the check.} + +\item{initials}{Curator initials for the single-string \code{initials} column. +Defaults to \code{getOption("aedes.initials")}; set once per session with +\code{options(aedes.initials = "XY")}. Passed through \code{...} semantics -- an +explicit \code{initials = ...} in \code{...} wins over the option.} + +\item{annotator}{Multi-select \code{annotator} column write policy. \code{TRUE} +(the default) appends \code{getOption("aedes.initials")} to the cell; \code{FALSE} +leaves the column alone; a character vector (or comma-joined string) +appends those tokens explicitly. On existing rows the tokens are merged +with the current cell contents (unique, sorted).} + +\item{proofreader}{Multi-select \code{proofreader} column write policy. Same +accepted values as \code{annotator}; defaults to \code{FALSE} (leave the column +alone).} + +\item{wipe}{If \code{TRUE}, replace the target multi-select column(s) with just +the new tokens instead of merging with existing cell contents. Default +\code{FALSE} (append).} } \value{ A list. With \code{dryrun = TRUE} it has elements \code{up} (rows that would @@ -28,6 +76,17 @@ timestamp via \code{\link[=aedes_sequential_update]{aedes_sequential_update()}} reliable. Rows whose \code{root_id} is already present are updated with any extra columns supplied via \code{...}; rows that are absent are appended with a \code{point_xyz} computed by \code{\link[=aedes_key_point]{aedes_key_point()}}. \code{supervoxel_id} and -\code{serial_id} are left blank — a server-side process fills them in from +\code{serial_id} are left blank -- a server-side process fills them in from \code{point_xyz}. } +\details{ +By default the function also auto-fills \code{soma_xyz}, \code{nucleus_id} +and \code{side} for each row via \code{\link[=aedes_soma_position]{aedes_soma_position()}} and +\code{\link[=aedes_point_side]{aedes_point_side()}}. When the soma cascade returns no position for an id +the fallback for \code{side} is \code{\link[=aedes_point_side]{aedes_point_side()}} on the L2 key point, and +a warning naming the affected ids is issued. + +Auto-fill columns (\code{soma_xyz}, \code{nucleus_id}, \code{side}, \code{point_xyz}) never +overwrite a non-NA value on an existing row. Values passed via \code{...} +always win over the auto-fill and always overwrite on existing rows. +} diff --git a/man/aedes_meta.Rd b/man/aedes_meta.Rd index c48d11f..5f490b0 100644 --- a/man/aedes_meta.Rd +++ b/man/aedes_meta.Rd @@ -12,6 +12,7 @@ aedes_meta( version = NULL, timestamp = NULL, unique = FALSE, + translate_ids = NA, ... ) @@ -40,6 +41,10 @@ rather than regular expressions (default FALSE).} \item{unique}{Whether to drop duplicate \code{root_id} rows (with duplicates attached as an attribute).} +\item{translate_ids}{Whether to bring explicitly supplied \code{ids} forward to +the requested \code{version}/\code{timestamp} before matching (see Details). +\code{NA} (the default) decides automatically.} + \item{...}{Additional arguments passed to \code{\link[fafbseg:cam_meta]{fafbseg::cam_meta()}} (e.g. cache controls such as \code{expiry}, \code{refresh}).} } @@ -57,6 +62,16 @@ returned data frame will be mapped to the corresponding timepoint using the will be simply as returned by the flytable (which updates them every half hour). If you want to be sure that ids match the most up to date state of the segmentation possible then you can ask for \code{timestamp='now'}. + +For a \strong{query string} the match happens against that mapped table, so no +further work is needed. For \strong{explicit root \code{ids}} the join is by +\code{root_id}, so ids that are stale relative to the requested timepoint would +silently fail to match. \code{translate_ids} guards against this by bringing the +supplied ids forward with \code{\link[fafbseg:flywire_latestid]{fafbseg::flywire_latestid()}} first. The default +(\code{NA}) turns this on only when it is both needed and meaningful: explicit +ids are supplied \emph{and} a \code{version}/\code{timestamp} is given. With no +version/timestamp nothing is translated, since the flytable is simply at +the state of its last half-hourly update. } \examples{ \dontrun{ diff --git a/man/aedes_set_group.Rd b/man/aedes_set_group.Rd new file mode 100644 index 0000000..c6bcc3b --- /dev/null +++ b/man/aedes_set_group.Rd @@ -0,0 +1,78 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/groups.R +\name{aedes_set_group} +\alias{aedes_set_group} +\title{Group aedes neurons together in FlyTable} +\usage{ +aedes_set_group( + ids, + group = NULL, + join_existing = NA, + dryrun = TRUE, + annotator = TRUE, + proofreader = FALSE, + wipe = FALSE, + ... +) +} +\arguments{ +\item{ids}{Neurons to group, in any form understood by \code{\link[=aedes_ids]{aedes_ids()}} +(including a query string).} + +\item{group}{Optional explicit target. An integer forces that group id; \code{0} +or \code{NA} ungroups; a query / ids joins the group of those neuron(s) +("join-by-example"). When \code{NULL} (the default) the group id is derived (see +Details).} + +\item{join_existing}{Controls behaviour when selected neurons already belong +to a group. \code{NA} (the default): refuse to guess -- warn (dry run) or error +(live) and explain how to proceed. \code{TRUE}: add them to the existing group +(its id is kept even if a lower \code{serial_id} is now available; several +existing groups are merged into the smallest, with a warning). \code{FALSE}: +ignore existing membership and mint a fresh group from \code{min(serial_id)}.} + +\item{dryrun}{logical: if \code{TRUE} (the default) return a preview without +writing to FlyTable.} + +\item{annotator}{Multi-select \code{annotator} column write policy for rows that +actually change group. \code{TRUE} (the default) appends +\code{getOption("aedes.initials")} to the existing cell; \code{FALSE} leaves the +column alone; a character vector (or comma-joined string) appends those +tokens explicitly.} + +\item{proofreader}{Multi-select \code{proofreader} column write policy. Same +accepted values as \code{annotator}; defaults to \code{FALSE}.} + +\item{wipe}{If \code{TRUE}, replace the target multi-select column(s) with just +the new tokens instead of merging with existing cell contents. Default +\code{FALSE} (append).} + +\item{...}{reserved (used to reject a mistaken \code{dry_run} argument).} +} +\value{ +A preview data.frame with one row per selected neuron: \code{root_id}, +\code{serial_id}, \code{group_old}, \code{group_new} and \code{changed}. Returned invisibly on a +live write. +} +\description{ +Assigns a shared \code{group} id to a set of neurons in the +\code{aedes_main} FlyTable -- the convenient way to build serial / cell-type +groups and, via \code{join_existing}, to add neurons to a group that already +exists. +} +\details{ +By convention a group is identified by an integer equal to the +smallest \code{serial_id} among its founding members; \code{group = 0} (or \code{NA}) means +ungrouped. When the selected neurons are all currently ungrouped a fresh +group id is minted from \code{min(serial_id)}. + +When some selected neurons already belong to a group, \code{join_existing} +decides what happens (see the argument). Reassigning neurons out of a group +whose other members were not supplied emits a warning, since it splits that +group. Only the \code{group} column is written, via the shared update engine that +also backs \code{\link[=aedes_set_meta]{aedes_set_meta()}}; the same pinned table snapshot is reused so +the returned preview matches what is written. +} +\seealso{ +\code{\link[=aedes_set_meta]{aedes_set_meta()}}, \code{\link[=aedes_add_neurons]{aedes_add_neurons()}} +} diff --git a/man/aedes_set_meta.Rd b/man/aedes_set_meta.Rd new file mode 100644 index 0000000..a273e5d --- /dev/null +++ b/man/aedes_set_meta.Rd @@ -0,0 +1,63 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/meta.R +\name{aedes_set_meta} +\alias{aedes_set_meta} +\title{Bulk-update metadata for existing aedes neurons in FlyTable} +\usage{ +aedes_set_meta( + ids = NULL, + df = NULL, + dryrun = TRUE, + update_roots = TRUE, + annotator = TRUE, + proofreader = FALSE, + wipe = FALSE, + ... +) +} +\arguments{ +\item{ids}{root_ids in any form understood by \code{\link[=aedes_ids]{aedes_ids()}} (including a query +string); or, when \code{df} is \code{NULL}, a data.frame of metadata that itself +contains a \code{root_id} column.} + +\item{df}{an optional data.frame of metadata, recycled to match \code{ids}. When +supplied together with \code{ids}, a \code{root_id} column is prepended from \code{ids}.} + +\item{dryrun}{logical: if \code{TRUE} (the default) return the update frame without +writing to FlyTable.} + +\item{update_roots}{whether to bring \code{root_id}s to the pinned timestamp with +\code{\link[fafbseg:flywire_latestid]{fafbseg::flywire_latestid()}} before matching.} + +\item{annotator}{Multi-select \code{annotator} column write policy. \code{TRUE} +(the default) appends \code{getOption("aedes.initials")} to the existing cell; +\code{FALSE} leaves the column alone; a character vector (or comma-joined +string) appends those tokens explicitly.} + +\item{proofreader}{Multi-select \code{proofreader} column write policy. Same +accepted values as \code{annotator}; defaults to \code{FALSE}.} + +\item{wipe}{If \code{TRUE}, replace the target multi-select column(s) with just +the new tokens instead of merging with existing cell contents. Default +\code{FALSE} (append).} + +\item{...}{reserved (used to reject a mistaken \code{dry_run} argument).} +} +\value{ +a data.frame of the rows written (or, on a dry run, that would be +written), keyed by FlyTable \verb{_id}. +} +\description{ +Updates rows that already exist in the \code{aedes_main} FlyTable from +a data.frame of per-row metadata. Update-only: every \code{root_id} must already +be present (use \code{\link[=aedes_add_neurons]{aedes_add_neurons()}} to create rows). +} +\details{ +Rows with status \code{bad_nucleus}, \code{duplicate} or \code{not_a_neuron} are +dropped before updating; any remaining \code{root_id} not found in \code{aedes_main} +is an error (nothing is written). Writes go through the shared update engine, +which pins a single timestamp so join-by-\code{root_id} is reliable. +} +\seealso{ +\code{\link[=aedes_add_neurons]{aedes_add_neurons()}}, \code{\link[=aedes_set_group]{aedes_set_group()}} +}