Skip to content

Commit 250cb04

Browse files
committed
chore: version bump, migration stuff
1 parent b4596f8 commit 250cb04

File tree

10 files changed

+299
-84
lines changed

10 files changed

+299
-84
lines changed

DESCRIPTION

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: immunarch
22
Type: Package
33
Title: Multi-Modal Immune Repertoire Analytics for Immunotherapy and Vaccine Design in R
4-
Version: 0.9.1.9001
4+
Version: 0.10.0
55
Authors@R: c(
66
person("Vadim I.", "Nazarov", , "support@immunomind.com", role = c("aut", "cre"),
77
comment = c(ORCID = "0000-0003-3659-2709")),
@@ -19,12 +19,12 @@ Description: A comprehensive analytics framework for building reproducible pipel
1919
Think Scanpy or Seurat, but for AIRR data, a.k.a. Adaptive Immune Receptor Repertoire, VDJ-seq, RepSeq, or
2020
VDJ sequencing data. A successor to our previously published "tcR" R package (Nazarov 2015).
2121
License: Apache License (== 2.0)
22-
URL: https://immunarch.com/, https://github.com/immunomind/immunarch
22+
URL: https://immunomind.github.io/docs/, https://github.com/immunomind/immunarch
2323
BugReports: https://github.com/immunomind/immunarch/issues
2424
Depends:
2525
R (>= 4.1.0),
2626
ggplot2 (>= 3.1.0),
27-
immundata (>= 0.0.3),
27+
immundata (>= 0.0.5),
2828
patchwork
2929
Imports:
3030
dplyr,

NAMESPACE

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ S3method(vis,step_failure_ignored)
4949
export(airr_clonality_line)
5050
export(airr_clonality_prop)
5151
export(airr_clonality_rank)
52+
export(airr_diversity_chao1)
5253
export(airr_diversity_dxx)
5354
export(airr_diversity_hill)
5455
export(airr_diversity_index)
@@ -59,6 +60,8 @@ export(airr_public_jaccard)
5960
export(airr_stats_chains)
6061
export(airr_stats_genes)
6162
export(airr_stats_lengths)
63+
export(annotate_clonality_prop)
64+
export(annotate_clonality_rank)
6265
export(apply_asymm)
6366
export(apply_symm)
6467
export(bunch_translate)
@@ -203,6 +206,7 @@ importFrom(dplyr,slice_head)
203206
importFrom(dplyr,summarise)
204207
importFrom(dplyr,tally)
205208
importFrom(dplyr,top_n)
209+
importFrom(dplyr,transmute)
206210
importFrom(dplyr,ungroup)
207211
importFrom(dplyr,union)
208212
importFrom(dplyr,union_all)
@@ -291,12 +295,14 @@ importFrom(stringr,str_sub)
291295
importFrom(stringr,str_trim)
292296
importFrom(tibble,rownames_to_column)
293297
importFrom(tibble,tibble)
298+
importFrom(tidyr,as_tibble)
294299
importFrom(tidyr,drop_na)
295300
importFrom(tidyr,unite)
296301
importFrom(tidyr,unnest)
297302
importFrom(tidyselect,all_of)
298303
importFrom(tidyselect,any_of)
299304
importFrom(tidyselect,starts_with)
305+
importFrom(utils,adist)
300306
importFrom(utils,capture.output)
301307
importFrom(utils,globalVariables)
302308
importFrom(utils,packageVersion)

R/aaa-registry.R

Lines changed: 126 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,22 @@ IMMUNARCH_METHOD_REGISTRY <- new.env(parent = emptyenv())
33

44
#' Common arguments for immundata helpers
55
#' @keywords internal
6-
#' @param autojoin If TRUE, join repertoire metadata by the schema repertoire id.
7-
#' For `format="analysis"`, metadata is joined to the long table; for
8-
#' `format="ml"`, it’s joined after pivoting to wide. Defaults to
9-
#' `getOption("immundata.autojoin", FALSE)`.
10-
#' @param format One of `"analysis"` (long tibble with `repertoire_id`, facet
11-
#' columns, and `value`) or `"ml"` (wide/unmelted table of features).
12-
#' @param features Character vector of **feature keys** to keep when
13-
#' `format="ml"`. If `NULL`, features are derived from the data. A feature key
14-
#' looks like `family.method|facet1=...;facet2=...` (e.g.,
15-
#' `airr_stats.genes|v_call=TRBV7-2`).
6+
#' @param autojoin Logical. If TRUE, join repertoire metadata by the schema repertoire id.
7+
#' Change the default behaviour by calling `options(immunarch.autojoin = FALSE)`.
8+
#' @param format String. One of `"long"` ("long" tibble with `imd_repertoire_id`, facet
9+
#' columns, and `value`; useful for visualizations) or `"wide"` (wide/unmelted table of features,
10+
#' with each row corresponding to a specific repertoire / pair of repertoires; useful for Machine Learning).
1611
im_common_args <- function(
17-
autojoin = getOption("immundata.autojoin", FALSE),
18-
format = c("analysis", "ml"),
19-
features = NULL) {} # nocov
12+
autojoin = getOption("immundata.autojoin", TRUE),
13+
format = c("long", "wide")) {} # nocov
2014

2115

22-
im_method <- function(core, family, name) {
16+
im_method <- function(core, family, name, required_cols = NULL, need_repertoires = TRUE) {
2317
checkmate::assert_function(core, args = c("idata"))
2418
checkmate::assert_string(family)
2519
checkmate::assert_string(name)
20+
checkmate::assert_logical(need_repertoires)
2621

27-
# Merge core formals with wrapper defaults to enable autocompletion
2822
core_fmls <- formals(core)
2923
if (!"idata" %in% names(core_fmls)) {
3024
cli::cli_abort("Core method must declare an {.code idata} argument.")
@@ -33,6 +27,11 @@ im_method <- function(core, family, name) {
3327
cli::cli_abort("Core method must not declare {.code autojoin}, {.code format}, or {.code features}.")
3428
}
3529

30+
# required_cols = columns expected in idata$annotations
31+
if (!is.null(required_cols)) {
32+
checkmate::assert_character(required_cols, any.missing = FALSE)
33+
}
34+
3635
wrapper <- function() { }
3736
formals(wrapper) <- c(
3837
core_fmls,
@@ -43,25 +42,131 @@ im_method <- function(core, family, name) {
4342
body(wrapper) <- substitute(
4443
{
4544
format <- match.arg(format)
46-
47-
# Pre: validate idata + schema
4845
checkmate::assert_r6(idata, "ImmunData")
4946

50-
# Build argument list for core from our own formals (now visible to the user)
47+
if (need_repertoires) {
48+
if (is.null(idata$schema_repertoire)) {
49+
cli::cli_abort("Repertoire aggregation is needed for this function. Run {.code ?agg_repertoires} for more info.")
50+
}
51+
}
52+
53+
# For each argument name in `required_cols`, fetch its runtime value
54+
# and ensure the referenced columns exist in idata$annotations.
55+
if (length(required_cols)) {
56+
ann_cols <- colnames(idata$annotations)
57+
for (.arg in required_cols) {
58+
.val <- get(.arg, inherits = TRUE)
59+
60+
# Just in case
61+
if (is.null(.val) || (length(.val) == 1 && is.na(.val))) next
62+
63+
# Allow symbols or character vectors
64+
if (rlang::is_symbol(.val)) {
65+
.val <- rlang::as_string(.val)
66+
}
67+
68+
if (!is.character(.val)) {
69+
cli::cli_abort("Argument {.code {.arg}} must be a character (column name) or character vector; got a {.code {class(.val)[1]}}.")
70+
}
71+
72+
missing <- setdiff(.val, ann_cols)
73+
if (length(missing)) {
74+
suggest <- function(x, pool, n = 3) {
75+
if (!length(pool)) {
76+
return(character())
77+
}
78+
d <- utils::adist(x, pool)
79+
pool[order(d)][seq_len(min(n, length(pool)))]
80+
}
81+
hints <- unique(unlist(lapply(missing, suggest, pool = ann_cols, n = 3)))
82+
cli::cli_abort("Passed column name(s) [{.code {missing}}] is not in the input ImmunData. Did you mean [{.code {hints}}]?")
83+
}
84+
}
85+
}
86+
87+
# Call core with its own formals
5188
.core_args <- mget(names(core_fmls), inherits = TRUE)
5289
out <- do.call(core, .core_args)
5390

91+
# Autojoin: join repertoire metadata if requested and applicable (!)
92+
if (isTRUE(autojoin) && !is.null(idata$repertoires)) {
93+
rep_col <- immundata::imd_schema("repertoire")
94+
if (!is.null(rep_col) && rep_col %in% names(out)) {
95+
out <- dplyr::left_join(out, idata$repertoires |> select(all_of(c(rep_col, idata$schema_repertoire))), by = rep_col)
96+
}
97+
}
98+
5499
out
55100
},
56-
list(core = core, core_fmls = core_fmls)
101+
list(core = core, core_fmls = core_fmls, required_cols = required_cols)
57102
)
58103

59104
wrapper
60105
}
61106

62107

63-
register_immunarch_method <- function(core, family, name, register_family = TRUE) {
64-
fn <- im_method(core, family, name)
108+
#' Register an Immunarch method (developer)
109+
#'
110+
#' `r lifecycle::badge("experimental")`
111+
#'
112+
#' Wrap a core implementation into a user-facing function and (optionally)
113+
#' register it in the in-memory method registry. The wrapper **adds common
114+
#' arguments** and **runs safety checks** so your core stays minimal.
115+
#'
116+
#' ## What your core must look like
117+
#' * Signature: `function(idata, ...)`
118+
#' * **Must not** declare `autojoin`, `format`, or `features` — these are added by the wrapper.
119+
#'
120+
#' ## What the wrapper adds
121+
#' * Common args from `im_common_args()`: `autojoin`, `format`, `features`
122+
#' (with `autojoin` default controlled by `getOption("immunarch.autojoin", FALSE)`).
123+
#' * Validates `idata` is an [immundata::ImmunData] object.
124+
#' * Ensures all columns in `required_cols` exist in `idata$annotations`.
125+
#' * If `autojoin = TRUE` and the result is a data frame containing the repertoire id
126+
#' column (`immundata::imd_schema("repertoire")`), joins repertoire metadata from
127+
#' `idata$repertoires`.
128+
#'
129+
#' @param core A function with signature `function(idata, ...)`. This is your core
130+
#' implementation; it must accept an `ImmunData` as the first argument and **must not**
131+
#' declare `autojoin`, `format`, or `features`.
132+
#' @param family String. Method family name used for dispatch (e.g., `"airr_stats"`).
133+
#' @param name String. Method name within the family (e.g., `"lengths"`).
134+
#' @param register_family Logical (default `TRUE`). If `TRUE`, attempts to create/ensure
135+
#' the family environment by calling `register_airr_family()` when available.
136+
#' @param required_cols Character vector of column names that **must** be present in
137+
#' `idata$annotations`. Use this to declare the minimal input schema your core needs.
138+
#' @param need_repertoires Logical. Use this to declare the necessity of having aggregated
139+
#' repertoires.
140+
#'
141+
#' @return A **function** — the user-facing wrapper around `core`. Typical usage is to
142+
#' assign it to the exported symbol of the method, e.g.:
143+
#' `airr_stats_lengths <- register_immunarch_method(...)`.
144+
#'
145+
#' @examples
146+
#' \dontrun{
147+
#' # Minimal core implementation (must accept `idata`)
148+
#' airr_stats_lengths_impl <- function(idata, seq_col = "cdr3_aa") {
149+
#' dplyr::as_tibble(idata$annotations) |>
150+
#' dplyr::distinct(.data[[immundata::imd_schema("repertoire")]], .data[[seq_col]]) |>
151+
#' dplyr::mutate(seq_len = nchar(.data[[seq_col]])) |>
152+
#' dplyr::count(.data[[immundata::imd_schema("repertoire")]], seq_len, name = "n")
153+
#' }
154+
#'
155+
#' # Register and expose a user-facing function
156+
#' airr_stats_lengths <- register_immunarch_method(
157+
#' core = airr_stats_lengths_impl,
158+
#' family = "airr_stats",
159+
#' name = "lengths",
160+
#' required_cols = c("cdr3_aa", immundata::imd_schema("repertoire"))
161+
#' )
162+
#'
163+
#' # Optional: call via dispatcher
164+
#' # make_airr_dispatcher("airr_stats")(idata = immdata, method = "lengths")
165+
#' }
166+
#'
167+
#' @keywords internal
168+
register_immunarch_method <- function(core, family, name, register_family = TRUE, required_cols = NULL, need_repertoires = TRUE) {
169+
fn <- im_method(core, family, name, required_cols = required_cols, need_repertoires = need_repertoires)
65170

66171
if (isTRUE(register_family) && exists("register_airr_family", mode = "function", inherits = TRUE)) {
67172
try(register_airr_family(family), silent = TRUE)

R/globals.R

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ utils::globalVariables(c(
22
"index",
33
"richness",
44
"shannon",
5-
"dd"
5+
"dd",
6+
"counts",
7+
"annotate_immundata",
8+
"ch",
9+
"clonal_prop_bin",
10+
"clonal_rank_bin",
11+
"prop"
612
))
713

814
#' @keywords internal

R/immunarch-package.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#' @importFrom dplyr select
3333
#' @importFrom dplyr slice_head
3434
#' @importFrom dplyr summarise
35+
#' @importFrom dplyr transmute
3536
#' @importFrom dplyr union
3637
#' @importFrom dplyr union_all
3738
#' @importFrom duckplyr as_duckdb_tibble
@@ -53,6 +54,8 @@
5354
#' @importFrom rlang set_names
5455
#' @importFrom rlang sym
5556
#' @importFrom stats runif
57+
#' @importFrom tidyr as_tibble
58+
#' @importFrom utils adist
5659
#' @importFrom utils globalVariables
5760
#' @useDynLib immunarch, .registration = TRUE
5861
## usethis namespace: end

R/v1_migration_updates.R

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,46 @@
11
#' @keywords internal
2-
immunarch_v1_update_sep_2025 <- function() {
3-
cli::cli_h1("{cli::col_green('immunarch')} {cli::col_yellow('0.9.x')} -- Critical Pre-release Notice")
2+
immunarch_v1_update_oct_2025 <- function() {
3+
cli::cli_h1("{cli::col_green('immunarch')} {cli::col_yellow('0.10.0')} -- Critical Pre-release Notice")
44

5-
cli::cli_alert_warning("Update #1 [Sep 2025] -- Major changes are coming in {cli::col_green('immunarch')} {cli::col_yellow('1.0.0')}!")
5+
cli::cli_alert_warning("Update #1 [Oct 2025] -- Major changes are coming in {cli::col_green('immunarch')} {cli::col_yellow('1.0.0')}!")
66
cli::cli_text(cli::col_yellow(cli::spark_line(runif(110, 0, 1))))
77

88
cli::cli_par()
99
cli::cli_text()
1010
cli::cli_text(
11-
"Hi, this is Vadim Nazarov speaking -- author of {cli::col_green('immunarch')}. ",
12-
"{cli::col_green('immunarch')} is finally graduating from out of the {cli::col_yellow('0.x.y')} development cycle. ",
13-
"I'm preparing our {cli::col_yellow('1.0.0')} release, which will remain stable and free of sudden changes until we approach {cli::col_yellow('2.0.0')}, along with ",
14-
"a scientific publication for proper citations. ",
11+
"Hi, this is Vadim Nazarov speaking - author of {cli::col_green('immunarch')}. ",
12+
"{cli::col_green('immunarch')} is finally graduating out of the {cli::col_yellow('0.x.y')} development cycle. ",
13+
"I'm preparing our {cli::col_yellow('1.0.0')} release, which will remain stable and free of sudden changes until we approach {cli::col_yellow('2.0.0')}. ",
14+
"A scientific publication will accompany it for proper citation. ",
1515
"Significant changes are coming, and I want to ensure you have everything you need to migrate to the new version."
1616
)
1717

1818
cli::cli_par()
1919
cli::cli_text()
2020
cli::cli_text("Here's a preview of what's coming in {cli::col_green('immunarch')} {cli::col_yellow('1.0.0')}:")
2121
cli::cli_bullets(c(
22-
"i" = "Some computationally intensive or advanced features (e.g., distance computations, graph-based analyses, dimensionality reduction techniques) will move to separate packages, making {cli::col_green('immunarch')} much more lightweight to install and manage;",
23-
"i" = "New functions will be introduced instead of the left old ones to make code more readable and maintainable. Legacy functions will remain temporarily, but they won't be updated and will be removed by {cli::col_yellow('~2027')};",
24-
"i" = "We will discontinue support for most custom file formats because the AIRR ecosystem is now mature enough that the majority of tools adhere to the AIRR standard;",
25-
"i" = "The package will transition from data frames to the new {cli::col_blue('ImmunData')} structure -- better suited for handling modern larger, more complex, and multi-modal datasets (e.g., single-cell, spatial);",
26-
"i" = "{cli::col_blue('ImmunData')} is available in the separate {cli::col_blue('immundata')} package, which you can already install via {cli::col_cyan('pak::pkg_install(\"immundata\")')};",
27-
"i" = "The {cli::col_blue('ImmunData')}-based computations will be significantly faster, support datasets larger than RAM, and fully adhere to AIRR Community standards.",
28-
"i" = "There currently only a handful functions which implement {cli::col_blue('ImmunData')}-based computations. However, if you want to start learning it, or you have a large-scale data, now it is the best time: the tutorials are available on {cli::col_cyan('https://github.com/immunomind/immundata') and {cli::col_cyan('https://immunomind.github.io/docs/')}"
22+
"i" = "Some computationally intensive or advanced features (e.g., distance computations, graph-based analyses, dimensionality reduction techniques) will move to separate packages, making {cli::col_green('immunarch')} much lighter to install and manage.",
23+
"i" = "New functions will replace older ones to make code more readable and maintainable. Legacy functions will remain temporarily, but they won't be updated and will be removed {cli::col_yellow('around 2027')}.",
24+
"i" = "We will discontinue support for most custom file formats because the AIRR ecosystem is now mature enough; most tools adhere to the AIRR standard.",
25+
"i" = "The package will transition from data frames to the new {cli::col_blue('ImmunData')} structure, better suited for handling larger, more complex, and multimodal datasets (e.g., single-cell, spatial).",
26+
"i" = "{cli::col_blue('ImmunData')} is available in the separate {cli::col_blue('immundata')} package, which you can already install via {cli::col_cyan('pak::pkg_install(\"immundata\")')}.",
27+
"i" = "The {cli::col_blue('ImmunData')}-based computations will be significantly faster, will support datasets larger than RAM, and will fully adhere to AIRR Community standards.",
28+
"i" = "There are currently only a handful of functions that implement {cli::col_blue('ImmunData')}-based computations. However, if you want to start learning it, or you have large-scale data, now is the best time: tutorials are available at {.url https://github.com/immunomind/immundata} and {.url https://immunomind.github.io/docs/}."
2929
))
3030

3131
cli::cli_par()
3232
cli::cli_text()
3333
cli::cli_text(
34-
"See the dedicated migration guide for migration on what you can do now and how to prepare for the future:"
34+
"See the dedicated migration guide for what you can do now and how to prepare for the future:"
3535
)
36-
cli::cli_text(">> visit {cli::col_cyan('https://immunomind.github.io/docs/tutorials/migration')}")
36+
cli::cli_text(">> visit {.url https://immunomind.github.io/docs/tutorials/migration}")
37+
38+
cli::cli_par()
39+
cli::cli_text()
40+
cli::cli_text(
41+
"See the comprehensive tutorial on how to analyse single-cell AIRR data:"
42+
)
43+
cli::cli_text(">> visit {.url https://immunomind.github.io/docs/tutorials/single_cell}")
3744

3845
cli::cli_par()
3946
cli::cli_text()
@@ -42,9 +49,9 @@ immunarch_v1_update_sep_2025 <- function() {
4249
cli::cli_par()
4350
cli::cli_text()
4451
cli::cli_alert_info("Questions, comments, ideas? I'm available via:")
45-
cli::cli_text(">> Support email: {cli::col_cyan('support@immunomind.com')}")
46-
cli::cli_text(">> GitHub tickets: {cli::col_cyan('https://github.com/immunomind/immunarch')}")
47-
cli::cli_text(">> LinkedIn: {cli::col_cyan('https://www.linkedin.com/in/vdnaz/')}")
52+
cli::cli_text(">> Support email: {.url mailto:support@immunomind.com}")
53+
cli::cli_text(">> GitHub tickets: {.url https://github.com/immunomind/immunarch}")
54+
cli::cli_text(">> LinkedIn: {.url https://www.linkedin.com/in/vdnaz/}")
4855

4956
cli::cli_par()
5057
cli::cli_text()

R/v1_migration_utils.R

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,7 @@ get_immunarch_news <- function(datepoint = "latest") {
3838
#'
3939
#' @export
4040
list_immunarch_news <- function() {
41-
names(immunarch_v1_updates)
41+
for (i in seq_along(names(immunarch_v1_updates))) {
42+
cat(names(immunarch_v1_updates), " -> ", "run immunarch::get_immunarch_news(", '"', names(immunarch_v1_updates), '"', ")", sep = "")
43+
}
4244
}

0 commit comments

Comments
 (0)