Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds support for applying translation() to sdArray objects (covering both ImageArray and LabelArray) and introduces test coverage for image/label translations.
Changes:
- Implement
translation,sdArray,numeric-methodto pad/shift array-backed SpatialData elements. - Add
testthattests validating translation behavior for images, labels, and multiscale labels. - Update roxygen-generated docs and NAMESPACE imports to support the new method.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
R/trans.R |
Implements translation() for sdArray (image/label arrays), including multiscale handling. |
tests/testthat/test-trans.R |
Adds tests for translation() on images, labels, and multiscale labels. |
NAMESPACE |
Imports DelayedArray::cbind / DelayedArray::rbind to support the implementation. |
man/trans.Rd |
Documents the new S4 method alias/usage for translation,sdArray,numeric. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| importFrom(DelayedArray,cbind) | ||
| importFrom(DelayedArray,rbind) |
There was a problem hiding this comment.
Importing cbind/rbind from DelayedArray into the package namespace can mask base cbind/rbind and affect other code paths in this package that call cbind() (e.g., data.frame/DFrame binding). Consider removing these importFrom() entries and instead calling DelayedArray::cbind() / DelayedArray::rbind() (or DelayedArray::bindCOLS/bindROWS) explicitly at the call sites that need DelayedArray support.
| importFrom(DelayedArray,cbind) | |
| importFrom(DelayedArray,rbind) |
| if (length(dim(y)) == 2) { | ||
| n <- NULL | ||
| } else { | ||
| t <- t[-1] | ||
| n <- dim(x)[1] | ||
| y <- aperm(y, c(2,3,1)) | ||
| } |
There was a problem hiding this comment.
For 3D arrays you drop the first component of t (t <- t[-1]) and never use it. If a caller passes a non-zero translation for the first dimension, it will be silently ignored (and dim(y) will not match the requested translation). Either enforce t[1] == 0 when length(dim(y)) > 2, or implement translation along that dimension as well.
| z <- DelayedArray(array(0, d)) | ||
| y <- if (t[2] > 0) cbind(z, y) else cbind(y, z) | ||
| } | ||
| if (t[1] != 0) { | ||
| d <- c(abs(t[1]), ncol(y), n) | ||
| z <- DelayedArray(array(0, d)) |
There was a problem hiding this comment.
DelayedArray(array(0, d)) allocates a dense in-memory array for the padding block. For large images/labels and/or large translations this can cause significant memory use and slowdowns. Prefer a lazy constant/zero array (e.g., S4Arrays::ConstantArray(0, dim=d) / an equivalent DelayedArray/S4Arrays constructor) so the padding remains delayed.
| z <- DelayedArray(array(0, d)) | |
| y <- if (t[2] > 0) cbind(z, y) else cbind(y, z) | |
| } | |
| if (t[1] != 0) { | |
| d <- c(abs(t[1]), ncol(y), n) | |
| z <- DelayedArray(array(0, d)) | |
| z <- S4Arrays::ConstantArray(0, dim=d) | |
| y <- if (t[2] > 0) cbind(z, y) else cbind(y, z) | |
| } | |
| if (t[1] != 0) { | |
| d <- c(abs(t[1]), ncol(y), n) | |
| z <- S4Arrays::ConstantArray(0, dim=d) |
| if (length(ys) == 1) { | ||
| ts <- list(t) | ||
| } else { | ||
| ds <- vapply(ys, ncol, integer(1)) | ||
| sf <- c(1, ds[-1]/ds[-length(ds)]) | ||
| ts <- lapply(cumprod(sf), `*`, t) |
There was a problem hiding this comment.
When x is multiscale, ts is computed by multiplying t by scale factors derived from ncol(). This can yield non-integer translations due to non-integer scale ratios and/or floating-point rounding (e.g., 0.5 * t becoming 31.999999), which will later be used as array dimensions via abs(t[1/2]) and can error. Consider validating/rounding each scaled translation (and failing with a clear error if it’s not exactly integer) before using it to construct d.
| if (length(ys) == 1) { | |
| ts <- list(t) | |
| } else { | |
| ds <- vapply(ys, ncol, integer(1)) | |
| sf <- c(1, ds[-1]/ds[-length(ds)]) | |
| ts <- lapply(cumprod(sf), `*`, t) | |
| validate_translation <- \(tt, scale) { | |
| rr <- round(tt) | |
| tol <- sqrt(.Machine$double.eps) | |
| if (!all(is.finite(tt)) || any(abs(tt - rr) > tol)) { | |
| stop( | |
| sprintf( | |
| "translation becomes non-integer at multiscale level %d after scaling; got %s", | |
| scale, paste(tt, collapse=", ")), | |
| call.=FALSE) | |
| } | |
| as.integer(rr) | |
| } | |
| if (length(ys) == 1) { | |
| ts <- list(as.integer(round(t))) | |
| } else { | |
| ds <- vapply(ys, ncol, integer(1)) | |
| sf <- c(1, ds[-1]/ds[-length(ds)]) | |
| ts <- lapply(seq_along(sf), \(k) { | |
| validate_translation(cumprod(sf)[k] * t, k) | |
| }) |
| # row | ||
| t <- c(0,n <- sample(77, 1),0) | ||
| y <- translation(x, t) | ||
| expect_equal(dim(y), dim(x)+t) | ||
| expect_is(data(y), "DelayedArray") | ||
| expect_true(sum(data(y)[,seq_len(n),]) == 0) | ||
| # col | ||
| t <- c(0,0,n <- sample(77, 1)) | ||
| y <- translation(x, t) | ||
| expect_equal(dim(y), dim(x)+t) | ||
| expect_is(data(y), "DelayedArray") | ||
| expect_true(sum(data(y)[,,seq_len(n)]) == 0) |
There was a problem hiding this comment.
These tests use sample() to pick translation sizes. Even with a small range, randomness can make failures harder to reproduce and can introduce variability in runtime. Consider using fixed translation values (or setting a local seed inside each test_that) so the tests are fully deterministic.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| length(t) == length(dim(x)), | ||
| t == round(t), all(is.finite(t))) |
There was a problem hiding this comment.
stopifnot(t == round(t)) will always fail when t has length > 1 because stopifnot() expects each argument to be a single TRUE; a logical vector is not isTRUE(). This makes translation(sdArray, numeric) unusable for the intended 2D/3D cases. Use a scalar check like all(t == round(t)) (and keep the finiteness check scalar as well).
| length(t) == length(dim(x)), | |
| t == round(t), all(is.finite(t))) | |
| length(t) == length(dim(x)), | |
| all(t == round(t)), | |
| all(is.finite(t))) |
| replicate(10, \() { | ||
| n <- f(); m <- f() | ||
| y <- translation(x, c(n,m)) | ||
| expect_identical(x$x, y$x+n) | ||
| expect_identical(x$y, y$y+m) | ||
| expect_identical(x[,i], y[i]) |
There was a problem hiding this comment.
The replicate() call is not executing the expectations: replicate(10, \() { ... }) replicates the function object rather than running the body. Additionally, inside the body the translation assertions are currently reversed (expect_identical(x$x, y$x+n) should compare y$x to x$x + n) and y[i] subsets rows rather than the intended columns (y[, i]). As written this test will either do nothing (because the expectations never run) or fail once corrected to execute.
| replicate(10, \() { | |
| n <- f(); m <- f() | |
| y <- translation(x, c(n,m)) | |
| expect_identical(x$x, y$x+n) | |
| expect_identical(x$y, y$y+m) | |
| expect_identical(x[,i], y[i]) | |
| replicate(10, { | |
| n <- f(); m <- f() | |
| y <- translation(x, c(n,m)) | |
| expect_identical(y$x, x$x+n) | |
| expect_identical(y$y, x$y+m) | |
| expect_identical(x[,i], y[,i]) |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| mutate(a=x*R[1,1], b=y*R[1,2]) |> | ||
| mutate(c=x*R[2,1], d=y*R[2,2]) |> |
There was a problem hiding this comment.
.R() now returns the transpose of the usual CCW rotation matrix (suitable for right-multiplying row vectors, as used in ShapeFrame rotation via xy %*% .R(...)). However, PointFrame rotation computes x' = x*R[1,1] + y*R[1,2] / y' = x*R[2,1] + y*R[2,2] (left-multiplying column vectors), which makes the rotation direction opposite to ShapeFrame and the “counter-clockwise” comment. Please make the convention consistent (e.g., transpose R in the PointFrame method or adjust .R()/call sites) so the same angle rotates points in the same direction across element types.
| mutate(a=x*R[1,1], b=y*R[1,2]) |> | |
| mutate(c=x*R[2,1], d=y*R[2,2]) |> | |
| mutate(a=x*R[1,1], b=y*R[2,1]) |> | |
| mutate(c=x*R[1,2], d=y*R[2,2]) |> |
No description provided.